From 7f275e334d478c2183ac1d05f88b72be3fac8856 Mon Sep 17 00:00:00 2001 From: Lakr Aream Date: Wed, 19 Jul 2023 23:21:42 +0900 Subject: [PATCH] Sync Update --- .../Source/Sources/Network/Network.swift | 1 + .../Network/Request/Request+Notes.swift | 19 ++ .../Notification/NotificaitonSource.swift | 8 +- .../Network/Notes/NetworkWrapper+Notes.swift | 10 + .../Source/Tests/SourceTest/Tests/Note.swift | 29 +++ Kimis.xcodeproj/project.pbxproj | 16 +- .../Component/ImageView/MKImageView.swift | 4 +- .../Component/Misc/BlurHashView.swift | 4 +- .../Context/NoteCell+ContextExt.swift | 25 +- .../GenericCell/NoteCell+MoreHeader.swift | 2 - .../NoteCell+MoreReplyHeader.swift | 17 ++ .../NoteCell+MoreReplyPadded.swift | 14 -- .../NoteTableView+Publisher.swift | 6 +- .../NoteOperationStrip+Reaction.swift | 4 +- .../PollView/ChoiceView+Snapshot.swift | 7 +- .../Reactions/ReactionStrip+BaseView.swift | 142 +++++++++++ .../Reactions/ReactionStrip+Element.swift | 24 +- .../Reactions/ReactionStrip+EmojiView.swift | 9 +- .../Reactions/ReactionStrip+ImageView.swift | 38 +-- .../Reactions/ReactionStrip+MoreView.swift | 3 +- .../Reactions/ReactionStrip+Snapshot.swift | 8 +- .../Reactions/ReactionStrip+UserList.swift | 220 ++++++++++++++++++ .../NoteView/Reactions/ReactionStrip.swift | 6 +- .../NotificationTableView+Publisher.swift | 4 +- .../NoteViewController.swift | 10 +- .../PostEditor/Toolbar/Toolbar+Button.swift | 4 +- .../EndpointSwitchPopover.swift | 14 +- .../UserViewController.swift | 8 +- .../UsersListController.swift | 16 +- Kimis/Interface/Main/LLNavController.swift | 6 +- Kimis/Interface/Main/SideBarController.swift | 39 +++- Kimis/Interface/Main/TabBarController.swift | 2 +- Resource/ApiTest/main.sh | 7 +- 33 files changed, 598 insertions(+), 128 deletions(-) create mode 100644 Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+BaseView.swift create mode 100644 Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+UserList.swift diff --git a/Foundation/Source/Sources/Network/Network.swift b/Foundation/Source/Sources/Network/Network.swift index 0c449ff..29f1031 100644 --- a/Foundation/Source/Sources/Network/Network.swift +++ b/Foundation/Source/Sources/Network/Network.swift @@ -285,6 +285,7 @@ public extension Network { setTask(task) task.resume() sem.wait() + session.finishTasksAndInvalidate() } func decodeRequest(with data: Data?) -> T? { diff --git a/Foundation/Source/Sources/Network/Request/Request+Notes.swift b/Foundation/Source/Sources/Network/Request/Request+Notes.swift index f254827..25714e0 100644 --- a/Foundation/Source/Sources/Network/Request/Request+Notes.swift +++ b/Foundation/Source/Sources/Network/Request/Request+Notes.swift @@ -133,6 +133,25 @@ public extension Network { return requestForNote(with: noteId) } + func requestForReactionUserList(with noteId: String, reaction: String, limit: Int) -> [NMUserLite]? { + var request = prepareRequest(for: .notes_reactions) + injectBodyForPost(for: &request, with: ["noteId": noteId]) + injectBodyForPost(for: &request, with: ["type": reaction]) + injectBodyForPost(for: &request, with: ["limit": limit]) + var responseData: Data? + makeRequest(with: request) { data in + responseData = data + } + guard let responseData else { return nil } + guard let firstDecode = ( + try? JSONSerialization.jsonObject(with: responseData) + ) as? [[String: Any]] else { return nil } + let list = firstDecode.compactMap { $0["user"] } + return list.compactMap { element in // get key inside user + decodeRequest(with: try? JSONSerialization.data(withJSONObject: element)) + } + } + /// get replies for this note /// - Parameter noteId: id /// - Returns: replies and extracted notes for cache diff --git a/Foundation/Source/Sources/Source/DataSource/Notification/NotificaitonSource.swift b/Foundation/Source/Sources/Source/DataSource/Notification/NotificaitonSource.swift index 4d2e0cf..bcb2304 100644 --- a/Foundation/Source/Sources/Source/DataSource/Notification/NotificaitonSource.swift +++ b/Foundation/Source/Sources/Source/DataSource/Notification/NotificaitonSource.swift @@ -24,7 +24,8 @@ public class NotificationSource: ObservableObject { } } - @Published public internal(set) var badge: Int = 0 + @Published public internal(set) var badgeCount: Int = 0 + @Published public internal(set) var badge: Bool = false @Published public internal(set) var readDate = Date(timeIntervalSince1970: 0) { didSet { @@ -82,10 +83,11 @@ public class NotificationSource: ObservableObject { func recalculateBadgeValueAndUpdate() { DispatchQueue.global().async { - self.badge = self.dataSource.filter { + let filteredList = self.dataSource.filter { $0.createdAt > self.readDate } - .count + self.badgeCount = filteredList.count + self.badge = !filteredList.isEmpty } } } diff --git a/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift b/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift index 7b0e829..85643ac 100644 --- a/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift +++ b/Foundation/Source/Sources/Source/Network/Notes/NetworkWrapper+Notes.swift @@ -121,6 +121,16 @@ public extension Source.NetworkWrapper { return nil } + @discardableResult + func requestNoteReactionUserList(reactionIdentifier emoji: String?, forNote noteId: NoteID?) -> [User] { + guard let ctx, let noteId, let emoji else { return [] } + let result = ctx.network.requestForReactionUserList(with: noteId, reaction: emoji, limit: 100) ?? [] + ctx.spider.spidering(result) + return result.map { userLite -> User in + User.converting(userLite, defaultHost: ctx.receipt.host) + } + } + @discardableResult func requestForUserNotes(userHandler: String, type: Network.UserNoteFetchType, untilId: String?) -> [NoteID] { guard let ctx else { return [] } diff --git a/Foundation/Source/Tests/SourceTest/Tests/Note.swift b/Foundation/Source/Tests/SourceTest/Tests/Note.swift index baa4af7..d578b21 100644 --- a/Foundation/Source/Tests/SourceTest/Tests/Note.swift +++ b/Foundation/Source/Tests/SourceTest/Tests/Note.swift @@ -192,6 +192,14 @@ extension SourceTest { } } + // check reaction + dispatchAndWait { + let ans = source.network.requestForReactionUserList(with: nid, reaction: emoji, limit: 100) + unwrapOrFail(ans) { lst in + XCTAssert(lst.count == 0) + } + } + // reaction create dispatchAndWait { _ = source.network.requestForReactionCreate(with: nid, reaction: emoji) @@ -202,6 +210,19 @@ extension SourceTest { } } + // check reaction + dispatchAndWait { + let ans = source.network.requestForReactionUserList(with: nid, reaction: emoji, limit: 100) + unwrapOrFail(ans) { reactionUserList in + XCTAssert(reactionUserList.count == 1) + let note = source.network.requestForNote(with: nid) + unwrapOrFail(note) { note in + XCTAssert(note.user.username.count > 0) + XCTAssert(note.user.username.lowercased() == reactionUserList.first?.username.lowercased()) + } + } + } + // reaction delete dispatchAndWait { _ = source.network.requestForReactionDelete(with: nid) @@ -212,6 +233,14 @@ extension SourceTest { } } + // check reaction again + dispatchAndWait { + let ans = source.network.requestForReactionUserList(with: nid, reaction: emoji, limit: 100) + unwrapOrFail(ans) { lst in + XCTAssert(lst.count == 0) + } + } + // favorite create dispatchAndWait { source.network.requestForNoteFavoriteCreate(with: nid) diff --git a/Kimis.xcodeproj/project.pbxproj b/Kimis.xcodeproj/project.pbxproj index 8903319..6c2cef0 100644 --- a/Kimis.xcodeproj/project.pbxproj +++ b/Kimis.xcodeproj/project.pbxproj @@ -63,6 +63,7 @@ 504519CD296DB997009D613D /* SpoilerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504519CC296DB997009D613D /* SpoilerView.swift */; }; 504519D1296E8560009D613D /* UploadRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504519D0296E855F009D613D /* UploadRequest.swift */; }; 504519D3296E8588009D613D /* UploadCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504519D2296E8588009D613D /* UploadCell.swift */; }; + 5046C92C2A67B7100039676B /* ReactionStrip+UserList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5046C92B2A67B7100039676B /* ReactionStrip+UserList.swift */; }; 504E87C62937596500BDE03C /* NotificationTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504E87C52937596500BDE03C /* NotificationTableView.swift */; }; 504E87CA2937783E00BDE03C /* NotificationCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504E87C92937783E00BDE03C /* NotificationCell.swift */; }; 504E87CC293779AA00BDE03C /* NotificationTableView+Footer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504E87CB293779AA00BDE03C /* NotificationTableView+Footer.swift */; }; @@ -248,6 +249,7 @@ 50E8DFD5297B19C600657910 /* PaddedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E8DFD4297B19C600657910 /* PaddedTextField.swift */; }; 50E8DFD8297B433100657910 /* LicenseController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E8DFD7297B433100657910 /* LicenseController.swift */; }; 50E8DFDC297B43C500657910 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 50E8DFDB297B43C500657910 /* LICENSE */; }; + 50FAC1F32A651B4000125B2A /* ReactionStrip+BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FAC1F22A651B4000125B2A /* ReactionStrip+BaseView.swift */; }; 50FD825B29757AC300738C27 /* TextParser+RegEx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FD825A29757AC300738C27 /* TextParser+RegEx.swift */; }; 50FE1B1229336250000CE139 /* HashtagNoteController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FE1B1129336250000CE139 /* HashtagNoteController.swift */; }; 50FE1B152933ADE5000CE139 /* TrendingTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50FE1B142933ADE5000CE139 /* TrendingTableView.swift */; }; @@ -314,6 +316,7 @@ 504519CC296DB997009D613D /* SpoilerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpoilerView.swift; sourceTree = ""; }; 504519D0296E855F009D613D /* UploadRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadRequest.swift; sourceTree = ""; }; 504519D2296E8588009D613D /* UploadCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UploadCell.swift; sourceTree = ""; }; + 5046C92B2A67B7100039676B /* ReactionStrip+UserList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReactionStrip+UserList.swift"; sourceTree = ""; }; 504E87C52937596500BDE03C /* NotificationTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationTableView.swift; sourceTree = ""; }; 504E87C92937783E00BDE03C /* NotificationCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCell.swift; sourceTree = ""; }; 504E87CB293779AA00BDE03C /* NotificationTableView+Footer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationTableView+Footer.swift"; sourceTree = ""; }; @@ -494,6 +497,7 @@ 50E8DFD4297B19C600657910 /* PaddedTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddedTextField.swift; sourceTree = ""; }; 50E8DFD7297B433100657910 /* LicenseController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LicenseController.swift; sourceTree = ""; }; 50E8DFDB297B43C500657910 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + 50FAC1F22A651B4000125B2A /* ReactionStrip+BaseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReactionStrip+BaseView.swift"; sourceTree = ""; }; 50FD825A29757AC300738C27 /* TextParser+RegEx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextParser+RegEx.swift"; sourceTree = ""; }; 50FE1B1129336250000CE139 /* HashtagNoteController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagNoteController.swift; sourceTree = ""; }; 50FE1B142933ADE5000CE139 /* TrendingTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingTableView.swift; sourceTree = ""; }; @@ -556,11 +560,13 @@ isa = PBXGroup; children = ( 50443F952927ED270077523F /* ReactionStrip.swift */, + 50FAC1F22A651B4000125B2A /* ReactionStrip+BaseView.swift */, 50126254292F7763002E1636 /* ReactionStrip+Snapshot.swift */, 5012624C292F76F2002E1636 /* ReactionStrip+Element.swift */, 50126252292F7752002E1636 /* ReactionStrip+EmojiView.swift */, 50126250292F7738002E1636 /* ReactionStrip+MoreView.swift */, 5012624E292F771A002E1636 /* ReactionStrip+ImageView.swift */, + 5046C92B2A67B7100039676B /* ReactionStrip+UserList.swift */, ); path = Reactions; sourceTree = ""; @@ -1526,6 +1532,7 @@ 502F6E582968738F003691BE /* ToolbarView.swift in Sources */, 50AFDFE929305E9B00BEC741 /* NoteOperationStrip+More.swift in Sources */, 508FC83029632AB200B032D8 /* AnySnapshot.swift in Sources */, + 50FAC1F32A651B4000125B2A /* ReactionStrip+BaseView.swift in Sources */, 504E87CE293779C200BDE03C /* NotificationTableView+Publisher.swift in Sources */, 504519A6296D55EC009D613D /* Toolbar+User.swift in Sources */, 50BED20129277D0E00C9D7E2 /* TextParser+TinyEmoji.swift in Sources */, @@ -1638,6 +1645,7 @@ 5069EBC0295EEA3B00677A3F /* UserCell+Render.swift in Sources */, 50AFDFE329305E8B00BEC741 /* NoteOperationStrip+Renote.swift in Sources */, 50BED1B729277D0E00C9D7E2 /* AES.swift in Sources */, + 5046C92C2A67B7100039676B /* ReactionStrip+UserList.swift in Sources */, 5069EBB7295EE2B800677A3F /* UsersListTableView.swift in Sources */, 504E87D029377A1300BDE03C /* NotificationTableView+Render.swift in Sources */, 5057A9442961CFD10088A6D4 /* ChoiceView.swift in Sources */, @@ -1863,7 +1871,7 @@ CODE_SIGN_ENTITLEMENTS = Kimis/KimisDebug.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 6CMYQQFFT8; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1881,7 +1889,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.13; + MARKETING_VERSION = 1.14; PRODUCT_BUNDLE_IDENTIFIER = wiki.qaq.kimis.inhouse; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1902,7 +1910,7 @@ CODE_SIGN_ENTITLEMENTS = Kimis/Kimis.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 130; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 6CMYQQFFT8; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; GENERATE_INFOPLIST_FILE = YES; @@ -1920,7 +1928,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.13; + MARKETING_VERSION = 1.14; PRODUCT_BUNDLE_IDENTIFIER = as.wiki.qaq.kimis; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/Kimis/Interface/Component/ImageView/MKImageView.swift b/Kimis/Interface/Component/ImageView/MKImageView.swift index 25a7895..a0a504f 100644 --- a/Kimis/Interface/Component/ImageView/MKImageView.swift +++ b/Kimis/Interface/Component/ImageView/MKImageView.swift @@ -188,9 +188,9 @@ class MKImageRenderView: UIView { withMainActor { [weak self] in assert(Thread.isMainThread) if let self, ticket == self.ticket, let thumbnail { - self.imageView.image = thumbnail + imageView.image = thumbnail self.imageData = imageData - self.blurView.setImage(withBlurHash: nil) + blurView.setImage(withBlurHash: nil) } } } diff --git a/Kimis/Interface/Component/Misc/BlurHashView.swift b/Kimis/Interface/Component/Misc/BlurHashView.swift index 4f45294..5268dbc 100644 --- a/Kimis/Interface/Component/Misc/BlurHashView.swift +++ b/Kimis/Interface/Component/Misc/BlurHashView.swift @@ -39,10 +39,10 @@ class BlurHashView: UIView { session = builderSession loadImage(forHash: hash) { [weak self] image in assert(Thread.isMainThread) - guard let self, self.session == builderSession else { + guard let self, session == builderSession else { return } - self.imageView.image = image + imageView.image = image } } diff --git a/Kimis/Interface/Component/NoteTableView/NoteCell/Context/NoteCell+ContextExt.swift b/Kimis/Interface/Component/NoteTableView/NoteCell/Context/NoteCell+ContextExt.swift index 9cd7eff..d2da560 100644 --- a/Kimis/Interface/Component/NoteTableView/NoteCell/Context/NoteCell+ContextExt.swift +++ b/Kimis/Interface/Component/NoteTableView/NoteCell/Context/NoteCell+ContextExt.swift @@ -29,9 +29,9 @@ extension NoteCell.Context { note.attachments.map { .init(with: $0) } } - static func createReactionStripElemetns(withNote note: Note, source: Source?) -> [ReactionStrip.Element] { + static func createReactionStripElemetns(withNote note: Note, source: Source?) -> [ReactionStrip.ReactionElement] { guard let source else { return [] } - var buildReactions = [ReactionStrip.Element]() + var buildReactions = [ReactionStrip.ReactionElement]() for (key, value) in note.reactions { if key.hasPrefix(":"), key.hasSuffix(":") { let name = String(key.dropFirst().dropLast()) @@ -39,14 +39,27 @@ extension NoteCell.Context { .appendingPathComponent("emoji") .appendingPathComponent(name) .appendingPathExtension("webp") - buildReactions.append(.init(text: nil, url: url, count: value, highlight: note.userReaction == key)) + buildReactions.append(.init( + noteId: note.noteId, + text: nil, + url: url, + count: value, + highlight: note.userReaction == key, + representReaction: name + )) } else { - buildReactions.append(.init(text: key, url: nil, count: value, highlight: note.userReaction == key)) + buildReactions.append(.init( + noteId: note.noteId, + text: key, + url: nil, + count: value, + highlight: note.userReaction == key + )) } } buildReactions.sort { - if $0.highlight { return true } - if $1.highlight { return false } + if $0.isUserReaction { return true } + if $1.isUserReaction { return false } return ($0.text ?? $0.url?.absoluteString ?? "") < ($1.text ?? $1.url?.absoluteString ?? "") } diff --git a/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreHeader.swift b/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreHeader.swift index 89aadaf..c297aa9 100644 --- a/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreHeader.swift +++ b/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreHeader.swift @@ -29,7 +29,6 @@ extension NoteCell { icon.contentMode = .scaleAspectFit icon.image = UIImage.fluent(.arrow_collapse_all_filled) label.text = "Load More Replies" - label.font = .systemFont(ofSize: CGFloat(AppConfig.current.defaultNoteFontSize), weight: .regular) label.textColor = .accent label.textAlignment = .left label.numberOfLines = 1 @@ -94,7 +93,6 @@ extension NoteCell { ) let horizontalSpacing = padding - label.frame = CGRect( x: padding + avatarSize + horizontalSpacing, y: 0, diff --git a/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyHeader.swift b/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyHeader.swift index 9fcac34..aba665b 100644 --- a/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyHeader.swift +++ b/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyHeader.swift @@ -15,5 +15,22 @@ extension NoteCell { icon.image = .fluent(.arrow_maximize_vertical_filled) label.text = "Expend Collapsed Replies" } + + override func layoutSubviews() { + super.layoutSubviews() + + let bounds = container.bounds + let padding = IH.preferredPadding(usingWidth: bounds.width) + let avatarSize = NotePreview.defaultAvatarSize + IH.preferredAvatarSizeOffset(usingWidth: width) + label.frame = CGRect( + x: padding + avatarSize + NotePreview.verticalSpacing, + y: 0, + width: 200, + height: bounds.height + ) + let fontSize = CGFloat(AppConfig.current.defaultNoteFontSize) + + IH.preferredFontSizeOffset(usingWidth: bounds.width - 2 * padding) + label.font = .systemFont(ofSize: fontSize, weight: .regular) + } } } diff --git a/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyPadded.swift b/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyPadded.swift index 20a4a2e..4a807ef 100644 --- a/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyPadded.swift +++ b/Kimis/Interface/Component/NoteTableView/NoteCell/GenericCell/NoteCell+MoreReplyPadded.swift @@ -10,7 +10,6 @@ import UIKit extension NoteCell { class MoreReplyPaddedCell: NoteCell { - let label = UILabel() var connectorAttach: LeftBottomCurveLine! let connectorBall = UIView() let connectorDown = UIView() @@ -25,12 +24,6 @@ extension NoteCell { container.addSubview(connectorAttach) container.addSubview(connectorDown) container.addSubview(connectorPass) - container.addSubview(label) - - label.text = "Expend Collapsed Replies" - label.font = .systemFont(ofSize: CGFloat(AppConfig.current.defaultNoteFontSize), weight: .regular) - label.textColor = .accent - label.textAlignment = .left connectorBall.backgroundColor = .separator connectorDown.layer.maskedCorners = [ @@ -82,13 +75,6 @@ extension NoteCell { width: connectorBall.frame.minX - 4 - connectorPass.frame.minX, height: bounds.height / 2 + IH.connectorWidth / 2 ) - let horizontalSpacing = padding - label.frame = CGRect( - x: connectorBall.frame.midX + smallerAvatarSize / 2 + horizontalSpacing, - y: 0, - width: 200, - height: bounds.height - ) } } } diff --git a/Kimis/Interface/Component/NoteTableView/NoteTableView/NoteTableView+Publisher.swift b/Kimis/Interface/Component/NoteTableView/NoteTableView/NoteTableView+Publisher.swift index d04327f..ba08630 100644 --- a/Kimis/Interface/Component/NoteTableView/NoteTableView/NoteTableView+Publisher.swift +++ b/Kimis/Interface/Component/NoteTableView/NoteTableView/NoteTableView+Publisher.swift @@ -22,10 +22,10 @@ extension NoteTableView { ) .receive(on: DispatchQueue.main) .sink { [weak self] value in - guard let self, self.option.useBuiltinRender else { return } + guard let self, option.useBuiltinRender else { return } let ticket = UUID() - self.renderTicket = ticket - self.renderQueue.async { + renderTicket = ticket + renderQueue.async { self.requestRenderUpdateReload( target: value.0, width: value.1, diff --git a/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+Reaction.swift b/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+Reaction.swift index f9cab4e..e3047c9 100644 --- a/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+Reaction.swift +++ b/Kimis/Interface/Component/NoteView/OperationStack/NoteOperationStrip+Reaction.swift @@ -22,8 +22,8 @@ extension NoteOperationStrip { private func reactionCreate() { let picker = EmojiPickerViewController(sourceView: reactButton) { [weak self] emoji in - guard let self, let source = self.source, let noteId = self.noteId else { return } - self.callingReactionUpdate(source: source, onNote: noteId, emojiOrDelete: emoji.emoji) + guard let self, let source, let noteId else { return } + callingReactionUpdate(source: source, onNote: noteId, emojiOrDelete: emoji.emoji) } associatedControllers.append(picker) parentViewController?.present(picker, animated: true) diff --git a/Kimis/Interface/Component/NoteView/PollView/ChoiceView+Snapshot.swift b/Kimis/Interface/Component/NoteView/PollView/ChoiceView+Snapshot.swift index 166559d..9fc01e6 100644 --- a/Kimis/Interface/Component/NoteView/PollView/ChoiceView+Snapshot.swift +++ b/Kimis/Interface/Component/NoteView/PollView/ChoiceView+Snapshot.swift @@ -81,7 +81,7 @@ extension PollView.ChoiceView.Snapshot { let verticalPadding: CGFloat = 4 let horizontalPadding: CGFloat = 8 - let iconSize: CGFloat = 24 + let iconSize: CGFloat = 20 var iconRect = CGRect( x: horizontalPadding, @@ -142,11 +142,12 @@ extension PollView.ChoiceView.Snapshot { width: textRect.size.width, height: textRect.size.height ) + let finalIconHeight = max(iconSize, textRect.size.height) iconRect = CGRect( x: iconRect.origin.x, - y: (height - textRect.size.height) / 2, + y: (height - finalIconHeight) / 2, width: iconSize, - height: textRect.size.height + height: finalIconHeight ) countTextRect = CGRect( x: countTextRect.origin.x, diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+BaseView.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+BaseView.swift new file mode 100644 index 0000000..3c248c7 --- /dev/null +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+BaseView.swift @@ -0,0 +1,142 @@ +// +// ReactionStrip+BaseView.swift +// Kimis +// +// Created by QAQ on 2023/7/17. +// + +import UIKit + +extension ReactionStrip { + class ElementBaseView: UIView, UIContextMenuInteractionDelegate { + var representReaction: ReactionElement? + let button = UIButton() + + let contentView = UIView() + let emojiContainer = UIView() + let countView: UILabel = { + let view = UILabel() + view.textAlignment = .center + view.layer.cornerRadius = 6 + view.clipsToBounds = true + view.layer.masksToBounds = true + view.numberOfLines = 1 + view.minimumScaleFactor = 0.5 + view.adjustsFontSizeToFitWidth = true + view.font = .rounded(ofSize: 16, weight: .regular) + return view + }() + + let activityIndicator = UIActivityIndicatorView() + + override init(frame: CGRect) { + super.init(frame: frame) + addSubview(contentView) + addSubview(activityIndicator) + activityIndicator.stopAnimating() + activityIndicator.isHidden = true + + contentView.addSubview(emojiContainer) + contentView.addSubview(countView) + + addSubview(button) + button.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(longPress(_:))) + button.addGestureRecognizer(longPress) + button.addInteraction(UIContextMenuInteraction(delegate: self)) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func layoutSubviews() { + super.layoutSubviews() + button.frame = bounds + activityIndicator.frame = bounds + contentView.frame = bounds + bringSubviewToFront(button) + + emojiContainer.frame = CGRect( + x: 0, + y: 0, + width: contentView.bounds.width / 2, + height: contentView.bounds.height + ).inset(by: .init(inset: 4)) + countView.frame = CGRect( + x: contentView.bounds.width / 2, + y: 0, + width: contentView.bounds.width / 2, + height: contentView.bounds.height + ) + } + + @objc func buttonTapped() { + puddingAnimate() + beginProgress { + guard let source = Account.shared.source, + let reactionElement = self.representReaction + else { return } + if reactionElement.isUserReaction { + _ = source.req.requestNoteReaction(reactionIdentifier: nil, forNote: reactionElement.noteId) + } else if let representReaction = reactionElement.representImageReaction { + var lookup = false + if !lookup, + source.emojis.keys.contains(representReaction) + { lookup = true } + if !lookup, + representReaction.hasSuffix("@."), + source.emojis.keys.contains(String(representReaction.dropLast(2))) + { lookup = true } + if lookup { + _ = source.req.requestNoteReaction( + reactionIdentifier: ":\(representReaction):", + forNote: reactionElement.noteId + ) + } else { + presentError("This reaction is not available") + } + } else if let textEmoji = reactionElement.text { + _ = source.req.requestNoteReaction( + reactionIdentifier: textEmoji, + forNote: reactionElement.noteId + ) + } else { + presentError("Unable to find this reaction") + } + } + } + + func beginProgress(executnigInBackground: @escaping () -> Void) { + assert(Thread.isMainThread) + contentView.isHidden = true + activityIndicator.startAnimating() + activityIndicator.isHidden = false + DispatchQueue.global().async { + executnigInBackground() + // so refresh can go without blink~ + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + self.contentView.isHidden = false + self.activityIndicator.stopAnimating() + self.activityIndicator.isHidden = true + } + } + } + + @objc func longPress(_ guesture: UILongPressGestureRecognizer) { + if guesture.state == .began { postLongPress() } + } + + public func contextMenuInteraction(_: UIContextMenuInteraction, configurationForMenuAtLocation _: CGPoint) -> UIContextMenuConfiguration? { + postLongPress() + return nil + } + + func postLongPress() { + guard let representReaction else { return } + let controller = UserListPopover(sourceView: self, representReaction: representReaction) + window?.topController?.present(controller, animated: true) + } + } +} diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Element.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Element.swift index dff968c..8f44d67 100644 --- a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Element.swift +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Element.swift @@ -8,15 +8,35 @@ import Foundation extension ReactionStrip { - struct Element: Hashable, Equatable { + struct ReactionElement: Hashable, Equatable { + let noteId: NoteID + let text: String? let url: URL? let count: Int - let highlight: Bool + let isUserReaction: Bool var validated: Bool { if text == nil { return url != nil } else { return url == nil } } + + let representImageReaction: String? + + init( + noteId: NoteID, + text: String? = nil, + url: URL? = nil, + count: Int, + highlight: Bool, + representReaction: String? = nil + ) { + self.noteId = noteId + self.text = text + self.url = url + self.count = count + isUserReaction = highlight + representImageReaction = representReaction + } } } diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+EmojiView.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+EmojiView.swift index b2200a3..c9fd812 100644 --- a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+EmojiView.swift +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+EmojiView.swift @@ -8,7 +8,7 @@ import UIKit extension ReactionStrip { - class EmojiView: UIView { + class EmojiView: ElementBaseView { let emoji: String let count: Int @@ -32,8 +32,9 @@ extension ReactionStrip { backgroundColor = highlight ? UIColor.accent.withAlphaComponent(0.1) : UIColor.gray.withAlphaComponent(0.1) - addSubview(label) - label.text = "\(emoji) x\(count)" + emojiContainer.addSubview(label) + label.text = emoji + countView.text = "x\(count)" } @available(*, unavailable) @@ -43,7 +44,7 @@ extension ReactionStrip { override func layoutSubviews() { super.layoutSubviews() - label.frame = bounds + label.frame = emojiContainer.bounds } } } diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+ImageView.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+ImageView.swift index 0002b6e..c5e1027 100644 --- a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+ImageView.swift +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+ImageView.swift @@ -5,10 +5,11 @@ // Created by Lakr Aream on 2022/11/24. // +import Source import UIKit extension ReactionStrip { - class ImageView: UIView { + class ImageView: ElementBaseView { let url: URL let count: Int @@ -19,19 +20,6 @@ extension ReactionStrip { return view }() - let label: UILabel = { - let view = UILabel() - view.textAlignment = .center - view.layer.cornerRadius = 6 - view.clipsToBounds = true - view.layer.masksToBounds = true - view.numberOfLines = 1 - view.minimumScaleFactor = 0.5 - view.adjustsFontSizeToFitWidth = true - view.font = .rounded(ofSize: 16, weight: .regular) - return view - }() - init(url: URL, count: Int, highlight: Bool) { self.url = url self.count = count @@ -40,29 +28,13 @@ extension ReactionStrip { backgroundColor = highlight ? UIColor.accent.withAlphaComponent(0.1) : UIColor.gray.withAlphaComponent(0.1) - addSubview(label) - addSubview(image) - label.text = "x\(count)" + countView.text = "x\(count)" + emojiContainer.addSubview(image) } override func layoutSubviews() { super.layoutSubviews() - - let this = self - let bounds = this.bounds - - label.frame = CGRect( - x: bounds.width / 2, - y: 0, - width: bounds.width / 2, - height: bounds.height - ) - image.frame = CGRect( - x: 0, - y: 0, - width: bounds.width / 2, - height: bounds.height - ).inset(by: .init(inset: 4)) + image.frame = emojiContainer.bounds } override func didMoveToWindow() { diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+MoreView.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+MoreView.swift index b52023a..b74e531 100644 --- a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+MoreView.swift +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+MoreView.swift @@ -8,7 +8,7 @@ import UIKit extension ReactionStrip { - class MoreView: UIView { + class MoreView: ElementBaseView { let label: UILabel = { let view = UILabel() view.textAlignment = .center @@ -28,6 +28,7 @@ extension ReactionStrip { backgroundColor = UIColor.gray.withAlphaComponent(0.1) addSubview(label) label.text = "..." + isUserInteractionEnabled = false } @available(*, unavailable) diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Snapshot.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Snapshot.swift index 660e939..5c30074 100644 --- a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Snapshot.swift +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+Snapshot.swift @@ -17,7 +17,7 @@ extension ReactionStrip { var height: CGFloat = 0 var viewRects: [CGRect] = [] - var viewElements: [ReactionStrip.Element] = [] + var viewElements: [ReactionStrip.ReactionElement] = [] var limitation: Int = 0 func hash(into hasher: inout Hasher) { @@ -32,16 +32,16 @@ extension ReactionStrip { extension ReactionStrip.Snapshot { struct RenderHint { - let viewElements: [ReactionStrip.Element] + let viewElements: [ReactionStrip.ReactionElement] let limitation: Int } - convenience init(usingWidth width: CGFloat, viewElements: [ReactionStrip.Element], limitation: Int) { + convenience init(usingWidth width: CGFloat, viewElements: [ReactionStrip.ReactionElement], limitation: Int) { self.init() render(usingWidth: width, viewElements: viewElements, limitation: limitation) } - func render(usingWidth width: CGFloat, viewElements: [ReactionStrip.Element], limitation: Int) { + func render(usingWidth width: CGFloat, viewElements: [ReactionStrip.ReactionElement], limitation: Int) { renderHint = RenderHint(viewElements: viewElements, limitation: limitation) render(usingWidth: width) } diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+UserList.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+UserList.swift new file mode 100644 index 0000000..7776ff8 --- /dev/null +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip+UserList.swift @@ -0,0 +1,220 @@ +// +// ReactionStrip+UserList.swift +// Kimis +// +// Created by QAQ on 2023/7/19. +// + +import UIKit + +extension ReactionStrip { + class UserListPopover: ViewController, UIPopoverPresentationControllerDelegate { + let contentView = UIView() + let reactionElement: ReactionElement + + let activityIndicator = UIActivityIndicatorView() + let userCollectionView = UserCollectionView() + + // TODO: Current Api has limit for 100 users, make more! + + init(sourceView: UIView, representReaction: ReactionElement) { + reactionElement = representReaction + super.init(nibName: nil, bundle: nil) + modalPresentationStyle = .popover + preferredContentSize = CGSize(width: 400, height: 200) + popoverPresentationController?.delegate = self + popoverPresentationController?.sourceView = sourceView + let padding: CGFloat = 4 + popoverPresentationController?.sourceRect = .init( + x: -padding, + y: -padding, + width: sourceView.frame.width + padding * 2, + height: sourceView.frame.height + padding * 2 + ) + popoverPresentationController?.permittedArrowDirections = .any + view.addSubview(contentView) + contentView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + contentView.addSubview(userCollectionView) + userCollectionView.alpha = 0 + userCollectionView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + contentView.addSubview(activityIndicator) + activityIndicator.snp.makeConstraints { make in + make.center.equalToSuperview() + } + activityIndicator.startAnimating() + } + + override func viewDidLoad() { + super.viewDidLoad() + loadUserList() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func adaptivePresentationStyle( + for _: UIPresentationController, + traitCollection _: UITraitCollection + ) -> UIModalPresentationStyle { + .none + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + } + + func loadUserList() { + var reactionIdentifier: String? + if let representReaction = reactionElement.representImageReaction { + reactionIdentifier = ":\(representReaction):" + } else if let textEmoji = reactionElement.text { + reactionIdentifier = textEmoji + } + + guard let reactionIdentifier, + let source = Account.shared.source + else { return } + let noteId = reactionElement.noteId + + DispatchQueue.global().async { + let userList = source.req.requestNoteReactionUserList( + reactionIdentifier: reactionIdentifier, + forNote: noteId + ) + DispatchQueue.main.async { + withUIKitAnimation { + self.activityIndicator.alpha = 0 + self.userCollectionView.alpha = 1 + self.userCollectionView.userList = userList + } completion: { + self.activityIndicator.stopAnimating() + self.activityIndicator.isHidden = true + } + } + } + } + } +} + +extension ReactionStrip.UserListPopover { + class UserCollectionView: UIView { + let collectionView: UICollectionView + var userList: [User] = [] { + didSet { applySnapshot() } + } + + init() { + let layout = AlignedCollectionViewFlowLayout( + horizontalAlignment: .left, + verticalAlignment: .center + ) + layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize + collectionView = .init(frame: .zero, collectionViewLayout: layout) + super.init(frame: .zero) + addSubview(collectionView) + collectionView.register(UserCollectionCellView.self, forCellWithReuseIdentifier: String(describing: UserCollectionCellView.self)) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError() + } + + enum Section { case main } + typealias DataSource = UICollectionViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + + private lazy var dataSource = makeDataSource() + func makeDataSource() -> DataSource { + let dataSource = DataSource( + collectionView: collectionView, + cellProvider: { collectionView, indexPath, user -> + UICollectionViewCell? in + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: String(describing: UserCollectionCellView.self), + for: indexPath + ) as? UserCollectionCellView + cell?.load(user: user) + return cell + } + ) + return dataSource + } + + func applySnapshot(animatingDifferences: Bool = true) { + var snapshot = Snapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(userList) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + + override func layoutSubviews() { + super.layoutSubviews() + collectionView.frame = bounds + + let inset = IH.preferredPadding(usingWidth: bounds.width) + collectionView.contentInset = .init(top: inset, left: inset, bottom: inset, right: inset) + } + } + + class UserCollectionCellView: UICollectionViewCell { + let avatarView = MKImageView() + let usernameView = TextView.noneInteractive() + + static let cellHeight: CGFloat = 30 + + override init(frame: CGRect) { + super.init(frame: frame) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + func setup() { + contentView.addSubview(avatarView) + contentView.addSubview(usernameView) + avatarView.snp.makeConstraints { make in + make.left.top.bottom.equalToSuperview() + make.width.height.equalTo(Self.cellHeight) + } + usernameView.textAlignment = .left + usernameView.textContainer.maximumNumberOfLines = 1 + usernameView.textContainer.lineBreakMode = .byTruncatingTail + usernameView.snp.makeConstraints { make in + make.left.equalTo(avatarView.snp.right).offset(8) + make.right.equalToSuperview().inset(8) + make.width.greaterThanOrEqualTo(Self.cellHeight) + make.centerY.equalToSuperview() + } + contentView.clipsToBounds = true + contentView.layer.cornerRadius = IH.contentMiniItemCornerRadius + contentView.backgroundColor = .accent.withAlphaComponent(0.1) + } + + override func prepareForReuse() { + super.prepareForReuse() + avatarView.clear() + usernameView.text = "" + } + + func load(user: User) { + avatarView.loadImage(with: .init( + url: user.avatarUrl, + blurHash: user.avatarBlurHash, + sensitive: false + )) + let textParser = TextParser() + usernameView.attributedText = textParser.compileRenoteUserHeader(with: user) + } + } +} diff --git a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip.swift b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip.swift index e14767f..ef48218 100644 --- a/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip.swift +++ b/Kimis/Interface/Component/NoteView/Reactions/ReactionStrip.swift @@ -54,10 +54,12 @@ class ReactionStrip: UIView { } let element = snapshot.viewElements[idx] if let text = element.text { - let view = ReactionStrip.EmojiView(emoji: text, count: element.count, highlight: element.highlight) + let view = ReactionStrip.EmojiView(emoji: text, count: element.count, highlight: element.isUserReaction) + view.representReaction = element views.append(view) } else if let url = element.url { - let view = ReactionStrip.ImageView(url: url, count: element.count, highlight: element.highlight) + let view = ReactionStrip.ImageView(url: url, count: element.count, highlight: element.isUserReaction) + view.representReaction = element views.append(view) } } diff --git a/Kimis/Interface/Component/NotificationTableView/NotificationTableView/NotificationTableView+Publisher.swift b/Kimis/Interface/Component/NotificationTableView/NotificationTableView/NotificationTableView+Publisher.swift index be0d617..fc81fd4 100644 --- a/Kimis/Interface/Component/NotificationTableView/NotificationTableView/NotificationTableView+Publisher.swift +++ b/Kimis/Interface/Component/NotificationTableView/NotificationTableView/NotificationTableView+Publisher.swift @@ -26,8 +26,8 @@ extension NotificationTableView { .sink { [weak self] value in guard let self else { return } let ticket = UUID() - self.renderTicket = ticket - self.renderQueue.async { + renderTicket = ticket + renderQueue.async { self.requestRenderUpdate( target: value.0, readAllBefore: value.1, diff --git a/Kimis/Interface/Controller/NoteViewController/NoteViewController.swift b/Kimis/Interface/Controller/NoteViewController/NoteViewController.swift index 5b87154..f544ea0 100644 --- a/Kimis/Interface/Controller/NoteViewController/NoteViewController.swift +++ b/Kimis/Interface/Controller/NoteViewController/NoteViewController.swift @@ -182,16 +182,16 @@ class NoteViewController: ViewController, RouterDatable { source?.notesChange .filter { [weak self] output in guard let self else { return false } - if self.trim == output { return true } - if self.chain.contains(output) { return true } - if self.main == output { return true } - if self.replies.contains(output) { return true } + if trim == output { return true } + if chain.contains(output) { return true } + if main == output { return true } + if replies.contains(output) { return true } return false } .receive(on: DispatchQueue.main) .sink { [weak self] _ in guard let self else { return } - self.updateDataSource(canFetch: true) + updateDataSource(canFetch: true) } .store(in: &cancellable) } diff --git a/Kimis/Interface/Controller/PostController/PostEditor/Toolbar/Toolbar+Button.swift b/Kimis/Interface/Controller/PostController/PostEditor/Toolbar/Toolbar+Button.swift index aaa221d..dedf33d 100644 --- a/Kimis/Interface/Controller/PostController/PostEditor/Toolbar/Toolbar+Button.swift +++ b/Kimis/Interface/Controller/PostController/PostEditor/Toolbar/Toolbar+Button.swift @@ -98,8 +98,8 @@ extension PostEditorToolbarView { let actions: [UIAction] = toolMenu.map { menuItem in .init(title: menuItem.text, image: menuItem.icon) { [weak self] _ in guard let self else { return } - menuItem.action(self.post, self) - self.isEnabled = self.toolEnabled(self.post) + menuItem.action(post, self) + isEnabled = toolEnabled(post) } } let menu = UIMenu(children: actions) diff --git a/Kimis/Interface/Controller/TimelineController/EndpointSwitchPopover.swift b/Kimis/Interface/Controller/TimelineController/EndpointSwitchPopover.swift index 39e522b..e2f83dc 100644 --- a/Kimis/Interface/Controller/TimelineController/EndpointSwitchPopover.swift +++ b/Kimis/Interface/Controller/TimelineController/EndpointSwitchPopover.swift @@ -27,6 +27,11 @@ class EndpointSwitchPopover: ViewController, UIPopoverPresentationControllerDele ) popoverPresentationController?.permittedArrowDirections = .any view.addSubview(contentView) + + contentView.snp.remakeConstraints { x in + x.left.right.equalToSuperview() + x.centerY.equalToSuperview() + } } let titleLabel = UILabel(text: "👉\nSwitch Endpoint") @@ -69,15 +74,6 @@ class EndpointSwitchPopover: ViewController, UIPopoverPresentationControllerDele ) -> UIModalPresentationStyle { .none } - - override func viewDidLayoutSubviews() { - super.viewDidLayoutSubviews() - - contentView.snp.remakeConstraints { x in - x.left.right.equalToSuperview() - x.centerY.equalToSuperview() - } - } } extension EndpointSwitchPopover { diff --git a/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController.swift b/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController.swift index 2e718f1..cfe9a2b 100644 --- a/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController.swift +++ b/Kimis/Interface/Controller/UserController/UserProfileController/UserViewController.swift @@ -83,15 +83,15 @@ class UserViewController: ViewController, RouterDatable { NotificationCenter.default.publisher(for: .requestUserProfileUpdate) .filter { [weak self] notification in guard let self else { return false } - guard !self.userHandler.isEmpty else { return false } + guard !userHandler.isEmpty else { return false } guard let mathcer = notification.object as? String else { assertionFailure() return false } let isCurrentUser = false - || mathcer == self.userHandler - || mathcer == self.userProfile?.userId - || mathcer == self.userProfile?.absoluteUsername + || mathcer == userHandler + || mathcer == userProfile?.userId + || mathcer == userProfile?.absoluteUsername return isCurrentUser } .debounce(for: .seconds(0.1), scheduler: DispatchQueue.global()) diff --git a/Kimis/Interface/Controller/UsersListController/UsersListController.swift b/Kimis/Interface/Controller/UsersListController/UsersListController.swift index 3536aef..8855a11 100644 --- a/Kimis/Interface/Controller/UsersListController/UsersListController.swift +++ b/Kimis/Interface/Controller/UsersListController/UsersListController.swift @@ -51,15 +51,15 @@ class UsersListController: ViewController { .sink { [weak self] isLoading in guard let self else { return } if isLoading { - self.refreshIndicator.startAnimating() - self.refreshIndicator.isHidden = false - self.refreshButton.isHidden = true - self.tableView.progressView.animate() + refreshIndicator.startAnimating() + refreshIndicator.isHidden = false + refreshButton.isHidden = true + tableView.progressView.animate() } else { - self.refreshButton.isHidden = false - self.refreshIndicator.stopAnimating() - self.refreshIndicator.isHidden = true - self.tableView.progressView.stopAnimate() + refreshButton.isHidden = false + refreshIndicator.stopAnimating() + refreshIndicator.isHidden = true + tableView.progressView.stopAnimate() } } .store(in: &tableView.cancellable) diff --git a/Kimis/Interface/Main/LLNavController.swift b/Kimis/Interface/Main/LLNavController.swift index ba3b580..fc9dece 100644 --- a/Kimis/Interface/Main/LLNavController.swift +++ b/Kimis/Interface/Main/LLNavController.swift @@ -102,11 +102,7 @@ class LLNavController: ViewController, UINavigationControllerDelegate { let rightView = UIView() - #if targetEnvironment(macCatalyst) - let titleLineHeight: CGFloat = 60 - #else // iPadOS - let titleLineHeight: CGFloat = 50 - #endif + let titleLineHeight: CGFloat = 50 var padding = IH.preferredViewPadding() override func viewDidLoad() { diff --git a/Kimis/Interface/Main/SideBarController.swift b/Kimis/Interface/Main/SideBarController.swift index 440eb13..a757912 100644 --- a/Kimis/Interface/Main/SideBarController.swift +++ b/Kimis/Interface/Main/SideBarController.swift @@ -5,6 +5,7 @@ // Created by Lakr Aream on 2022/5/8. // +import Combine import UIKit class SideBarController: ViewController, UINavigationControllerDelegate { @@ -128,7 +129,8 @@ private class SideBarControlPanelView: UIView { left: LLNavController(rootViewController: LargeNotificationController()), right: LLNavController(rootViewController: HashtagTrendController()) ) }, - image: .fluent(.alert_filled) + image: .fluent(.alert_filled), + badgePublisher: Account.shared.source?.notifications.$badge ), .init( target: { LLSplitController( @@ -149,7 +151,9 @@ private class SideBarControlPanelView: UIView { image: .fluent(.settings_filled) ), ] + super.init(frame: .zero) + backgroundColor = .accent.withAlphaComponent(0.05) addSubview(container) container.snp.makeConstraints { make in @@ -252,14 +256,21 @@ private extension SideBarControlPanelView { var button: UIButton let displaying: UIView - convenience init(target: @escaping () -> (UIViewController), image: UIImage) { + let badgeView: UIView = .init() + var cancellable: Set = .init() + + convenience init( + target: @escaping () -> (UIViewController), + image: UIImage, + badgePublisher: Published.Publisher? = nil + ) { let imageView = UIImageView() imageView.image = image imageView.contentMode = .scaleAspectFit - self.init(target: target, displayingView: imageView) + self.init(target: target, displayingView: imageView, badgePublisher: badgePublisher) } - init(target: @escaping () -> (UIViewController), displayingView: UIView) { + init(target: @escaping () -> (UIViewController), displayingView: UIView, badgePublisher: Published.Publisher? = nil) { self.target = target displaying = displayingView super.init(frame: .zero) @@ -278,6 +289,26 @@ private extension SideBarControlPanelView { make.width.equalTo(50) make.height.equalTo(50) } + + let badgeSize: CGFloat = 8 + addSubview(badgeView) + badgeView.backgroundColor = .systemPink.withAlphaComponent(0.9) + badgeView.snp.makeConstraints { make in + make.top.right.equalTo(displayingView) + make.width.height.equalTo(badgeSize) + } + badgeView.layer.cornerRadius = badgeSize / 2 + + badgeView.isHidden = true + + if let badgePublisher { + badgePublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] output in + self?.badgeView.isHidden = !output + } + .store(in: &cancellable) + } } @available(*, unavailable) diff --git a/Kimis/Interface/Main/TabBarController.swift b/Kimis/Interface/Main/TabBarController.swift index 245b845..11bf328 100644 --- a/Kimis/Interface/Main/TabBarController.swift +++ b/Kimis/Interface/Main/TabBarController.swift @@ -57,7 +57,7 @@ class TabBarController: UITabBarController { ] let notificationIdx = 3 - source?.notifications.$badge + source?.notifications.$badgeCount .removeDuplicates() .receive(on: DispatchQueue.main) .sink { [weak self] output in diff --git a/Resource/ApiTest/main.sh b/Resource/ApiTest/main.sh index 55c113e..2dbb4af 100755 --- a/Resource/ApiTest/main.sh +++ b/Resource/ApiTest/main.sh @@ -71,4 +71,9 @@ cd "$PROJECT_ROOT/" cd ./Foundation/Source swift test -echo "[+] test passed" \ No newline at end of file +echo "[+] test passed" + +if [ "$CI_CLEAN_DOCKER_BEFORE_EXIT" = "true" ]; then + echo "[+] cleaning docker containers..." + docker system prune --all -f +fi