From b15974dcfb4b6ba1913724aa84751bc7fb8b266f Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 13:56:55 +0800 Subject: [PATCH 01/33] feat: add profile field and update ActiveLabel entity supports --- .../CoreData.xcdatamodel/contents | 5 +- CoreDataStack/Entity/MastodonUser.swift | 10 + Localization/app.json | 7 + Mastodon.xcodeproj/project.pbxproj | 32 ++++ .../xcschemes/xcschememanagement.plist | 4 +- .../Diffiable/Item/ProfileFieldItem.swift | 106 +++++++++++ .../Section/ProfileFieldSection.swift | 114 +++++++++++ Mastodon/Extension/ActiveLabel.swift | 8 +- Mastodon/Extension/CoreDataStack/Fields.swift | 27 +++ .../CoreDataStack/MastodonUser.swift | 4 +- .../MastodonSDK/Mastodon+Entity+Field.swift | 19 ++ Mastodon/Generated/Strings.swift | 10 + Mastodon/Helper/MastodonField.swift | 20 +- Mastodon/Helper/MastodonStatusContent.swift | 2 +- .../Resources/ar.lproj/Localizable.strings | 3 + .../Resources/en.lproj/Localizable.strings | 3 + .../Header/ProfileHeaderViewController.swift | 97 ++++++++++ .../ProfileHeaderViewModel+Diffable.swift | 43 +++++ .../Header/ProfileHeaderViewModel.swift | 87 ++++++++- ...ofileFieldAddEntryCollectionViewCell.swift | 170 +++++++++++++++++ .../View/ProfileFieldCollectionViewCell.swift | 179 ++++++++++++++++++ ...eFieldCollectionViewHeaderFooterView.swift | 41 ++++ .../Header/View/ProfileFieldView.swift | 106 +++++++---- .../Header/View/ProfileHeaderView.swift | 54 +++++- .../Scene/Profile/ProfileViewController.swift | 42 +++- Mastodon/Scene/Profile/ProfileViewModel.swift | 6 + .../APIService+CoreData+MastodonUser.swift | 2 + .../Mastodon+API+Account+Credentials.swift | 8 +- .../Entity/Mastodon+Entity+Field.swift | 6 + 29 files changed, 1149 insertions(+), 66 deletions(-) create mode 100644 Mastodon/Diffiable/Item/ProfileFieldItem.swift create mode 100644 Mastodon/Diffiable/Section/ProfileFieldSection.swift create mode 100644 Mastodon/Extension/CoreDataStack/Fields.swift create mode 100644 Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift create mode 100644 Mastodon/Scene/Profile/Header/ProfileHeaderViewModel+Diffable.swift create mode 100644 Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift create mode 100644 Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift create mode 100644 Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index c8c07fbc..b41a1c28 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -102,6 +102,7 @@ + @@ -273,7 +274,7 @@ - + diff --git a/CoreDataStack/Entity/MastodonUser.swift b/CoreDataStack/Entity/MastodonUser.swift index e93d923c..d442ec75 100644 --- a/CoreDataStack/Entity/MastodonUser.swift +++ b/CoreDataStack/Entity/MastodonUser.swift @@ -27,6 +27,7 @@ final public class MastodonUser: NSManagedObject { @NSManaged public private(set) var url: String? @NSManaged public private(set) var emojisData: Data? + @NSManaged public private(set) var fieldsData: Data? @NSManaged public private(set) var statusesCount: NSNumber @NSManaged public private(set) var followingCount: NSNumber @@ -92,6 +93,7 @@ extension MastodonUser { user.note = property.note user.url = property.url user.emojisData = property.emojisData + user.fieldsData = property.fieldsData user.statusesCount = NSNumber(value: property.statusesCount) user.followingCount = NSNumber(value: property.followingCount) @@ -161,6 +163,11 @@ extension MastodonUser { self.emojisData = emojisData } } + public func update(fieldsData: Data?) { + if self.fieldsData != fieldsData { + self.fieldsData = fieldsData + } + } public func update(statusesCount: Int) { if self.statusesCount.intValue != statusesCount { self.statusesCount = NSNumber(value: statusesCount) @@ -281,6 +288,7 @@ extension MastodonUser { public let note: String? public let url: String? public let emojisData: Data? + public let fieldsData: Data? public let statusesCount: Int public let followingCount: Int public let followersCount: Int @@ -304,6 +312,7 @@ extension MastodonUser { note: String?, url: String?, emojisData: Data?, + fieldsData: Data?, statusesCount: Int, followingCount: Int, followersCount: Int, @@ -326,6 +335,7 @@ extension MastodonUser { self.note = note self.url = url self.emojisData = emojisData + self.fieldsData = fieldsData self.statusesCount = statusesCount self.followingCount = followingCount self.followersCount = followersCount diff --git a/Localization/app.json b/Localization/app.json index 8bd1316a..920ec0d2 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -404,6 +404,13 @@ "count_followers": "%ld followers" } }, + "fields": { + "add_row": "Add Row", + "placeholder": { + "label": "Label", + "content": "Content" + } + }, "segmented_control": { "posts": "Posts", "replies": "Replies", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b016a337..28f11361 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -401,6 +401,12 @@ DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */; }; DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */; }; DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7AA263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift */; }; + DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */; }; + DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */; }; + DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94437265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift */; }; + DBA9443A265CC0FC00C537E1 /* Fields.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94439265CC0FC00C537E1 /* Fields.swift */; }; + DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; + DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; }; DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; }; @@ -460,6 +466,8 @@ DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; + DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; + DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -961,6 +969,12 @@ DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewModel.swift; sourceTree = ""; }; DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewController.swift; sourceTree = ""; }; DBA5E7AA263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewCellContextMenuConfiguration.swift; sourceTree = ""; }; + DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldSection.swift; sourceTree = ""; }; + DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = ""; }; + DBA94437265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderViewModel+Diffable.swift"; sourceTree = ""; }; + DBA94439265CC0FC00C537E1 /* Fields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fields.swift; sourceTree = ""; }; + DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; + DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = ""; }; DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = ""; }; @@ -1019,6 +1033,8 @@ DBF8AE15263293E400C9C23C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; DBF8AE17263293E400C9C23C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = System/Library/Frameworks/AuthenticationServices.framework; sourceTree = SDKROOT; }; + DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewHeaderFooterView.swift; sourceTree = ""; }; + DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = ""; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1402,6 +1418,7 @@ DB6D9F7C26358ED4008423CD /* SettingsSection.swift */, 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */, DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */, + DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */, ); path = Section; sourceTree = ""; @@ -1463,6 +1480,7 @@ DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, DB6D9F8326358EEC008423CD /* SettingsItem.swift */, DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */, + DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */, ); path = Item; sourceTree = ""; @@ -1619,6 +1637,7 @@ DB6D9F4826353FD6008423CD /* Subscription.swift */, DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */, DBAFB7342645463500371D5F /* Emojis.swift */, + DBA94439265CC0FC00C537E1 /* Fields.swift */, ); path = CoreDataStack; sourceTree = ""; @@ -1913,6 +1932,7 @@ 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */, 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */, 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */, + DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */, DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */, ); path = MastodonSDK; @@ -2345,6 +2365,7 @@ DBB525732612D5A5002F1F29 /* View */, DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */, DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */, + DBA94437265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift */, ); path = Header; sourceTree = ""; @@ -2357,6 +2378,9 @@ DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */, DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */, DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */, + DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */, + DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */, + DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */, ); path = View; sourceTree = ""; @@ -2921,6 +2945,7 @@ 2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */, + DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */, DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */, @@ -2992,6 +3017,7 @@ 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */, 5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */, DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */, + DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */, 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, @@ -3011,6 +3037,7 @@ DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */, DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */, + DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, 2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */, @@ -3045,6 +3072,7 @@ 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */, DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */, DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */, + DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */, @@ -3059,6 +3087,7 @@ 5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */, 2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */, 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */, + DBA9443A265CC0FC00C537E1 /* Fields.swift in Sources */, 2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */, DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */, 2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */, @@ -3115,6 +3144,7 @@ 2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */, DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, + DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */, 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */, DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */, @@ -3152,6 +3182,7 @@ 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */, + DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */, 0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */, DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */, DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */, @@ -3261,6 +3292,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */, 5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */, 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */, + DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */, DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */, DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */, DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */, diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index f092f973..32685726 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 15 + 14 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 14 + 15 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Item/ProfileFieldItem.swift b/Mastodon/Diffiable/Item/ProfileFieldItem.swift new file mode 100644 index 00000000..684bd7d4 --- /dev/null +++ b/Mastodon/Diffiable/Item/ProfileFieldItem.swift @@ -0,0 +1,106 @@ +// +// ProfileFieldItem.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-25. +// + +import Foundation +import Combine +import MastodonSDK + +enum ProfileFieldItem { + case field(field: FieldValue, attribute: FieldItemAttribute) + case addEntry(attribute: AddEntryItemAttribute) +} + +protocol ProfileFieldListSeparatorLineConfigurable: AnyObject { + var isLast: Bool { get set } +} + +extension ProfileFieldItem { + var listSeparatorLineConfigurable: ProfileFieldListSeparatorLineConfigurable? { + switch self { + case .field(_, let attribute): + return attribute + case .addEntry(let attribute): + return attribute + } + } +} + +extension ProfileFieldItem { + struct FieldValue: Equatable, Hashable { + let id: UUID + + var name: CurrentValueSubject + var value: CurrentValueSubject + + init(id: UUID = UUID(), name: String, value: String) { + self.id = id + self.name = CurrentValueSubject(name) + self.value = CurrentValueSubject(value) + } + + func duplicate() -> FieldValue { + FieldValue(name: name.value, value: value.value) + } + + static func == (lhs: ProfileFieldItem.FieldValue, rhs: ProfileFieldItem.FieldValue) -> Bool { + return lhs.id == rhs.id + && lhs.name.value == rhs.name.value + && lhs.value.value == rhs.value.value + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + } +} + +extension ProfileFieldItem { + class FieldItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable { + let emojiDict = CurrentValueSubject([:]) + + var isEditing = false + var isLast = false + + static func == (lhs: ProfileFieldItem.FieldItemAttribute, rhs: ProfileFieldItem.FieldItemAttribute) -> Bool { + return lhs.isEditing == rhs.isEditing + && lhs.isLast == rhs.isLast + } + } + + class AddEntryItemAttribute: Equatable, ProfileFieldListSeparatorLineConfigurable { + var isLast = false + + static func == (lhs: ProfileFieldItem.AddEntryItemAttribute, rhs: ProfileFieldItem.AddEntryItemAttribute) -> Bool { + return lhs.isLast == rhs.isLast + } + } +} + +extension ProfileFieldItem: Equatable { + static func == (lhs: ProfileFieldItem, rhs: ProfileFieldItem) -> Bool { + switch (lhs, rhs) { + case (.field(let fieldLeft, let attributeLeft), .field(let fieldRight, let attributeRight)): + return fieldLeft.id == fieldRight.id + && attributeLeft == attributeRight + case (.addEntry(let attributeLeft), .addEntry(let attributeRight)): + return attributeLeft == attributeRight + default: + return false + } + } +} + +extension ProfileFieldItem: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .field(let field, _): + hasher.combine(field.id) + case .addEntry: + hasher.combine(String(describing: ProfileFieldItem.addEntry.self)) + } + } +} diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift new file mode 100644 index 00000000..339f754c --- /dev/null +++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift @@ -0,0 +1,114 @@ +// +// ProfileFieldSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-25. +// + +import os +import UIKit +import Combine + +enum ProfileFieldSection: Equatable, Hashable { + case main +} + +extension ProfileFieldSection { + static func collectionViewDiffableDataSource( + for collectionView: UICollectionView, + profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate, + profileFieldAddEntryCollectionViewCellDelegate: ProfileFieldAddEntryCollectionViewCellDelegate + ) -> UICollectionViewDiffableDataSource { + let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { + [ + weak profileFieldCollectionViewCellDelegate, + weak profileFieldAddEntryCollectionViewCellDelegate + ] collectionView, indexPath, item in + switch item { + case .field(let field, let attribute): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell + + // set key + cell.fieldView.titleTextField.text = field.name.value + field.name + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak cell] name in + guard let cell = cell else { return } + cell.fieldView.titleTextField.text = name + } + .store(in: &cell.disposeBag) + + + // set value + cell.fieldView.valueActiveLabel.configure(field: field.value.value, emojiDict: attribute.emojiDict.value) + Publishers.CombineLatest( + field.value.removeDuplicates(), + attribute.emojiDict.removeDuplicates() + ) + .receive(on: RunLoop.main) + .sink { [weak cell] value, emojiDict in + guard let cell = cell else { return } + cell.fieldView.valueActiveLabel.configure(field: value, emojiDict: emojiDict) + cell.fieldView.valueTextField.text = value + } + .store(in: &cell.disposeBag) + + // bind editing + if attribute.isEditing { + cell.fieldView.name + .removeDuplicates() + .receive(on: RunLoop.main) + .assign(to: \.value, on: field.name) + .store(in: &cell.disposeBag) + cell.fieldView.value + .removeDuplicates() + .receive(on: RunLoop.main) + .assign(to: \.value, on: field.value) + .store(in: &cell.disposeBag) + } + + // setup editing state + cell.fieldView.titleTextField.isEnabled = attribute.isEditing + cell.fieldView.valueActiveLabel.isHidden = attribute.isEditing + cell.fieldView.valueTextField.isHidden = !attribute.isEditing + + // set control hidden + let isHidden = !attribute.isEditing + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: update editing state: %s", ((#file as NSString).lastPathComponent), #line, #function, isHidden ? "true" : "false") + cell.editButton.isHidden = isHidden + cell.reorderBarImageView.isHidden = isHidden + + // update separator line + cell.bottomSeparatorLine.isHidden = attribute.isLast + + cell.delegate = profileFieldCollectionViewCellDelegate + + return cell + + case .addEntry(let attribute): + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self), for: indexPath) as! ProfileFieldAddEntryCollectionViewCell + + cell.bottomSeparatorLine.isHidden = attribute.isLast + cell.delegate = profileFieldAddEntryCollectionViewCellDelegate + + return cell + } + } + + dataSource.supplementaryViewProvider = { collectionView, kind, indexPath in + switch kind { + case UICollectionView.elementKindSectionHeader: + let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView + return reusableView + case UICollectionView.elementKindSectionFooter: + let reusableView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer, for: indexPath) as! ProfileFieldCollectionViewHeaderFooterView + return reusableView + default: + return nil + } + } + + return dataSource + } +} diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index c7e35c11..7975fad5 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -47,7 +47,7 @@ extension ActiveLabel { textColor = Asset.Colors.Label.primary.color numberOfLines = 1 case .profileField: - font = .preferredFont(forTextStyle: .body) + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) textColor = Asset.Colors.Label.primary.color numberOfLines = 1 } @@ -78,10 +78,10 @@ extension ActiveLabel { extension ActiveLabel { /// account field - func configure(field: String) { + func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) { activeEntities.removeAll() - let parseResult = MastodonField.parse(field: field) - text = parseResult.value + let parseResult = MastodonField.parse(field: field, emojiDict: emojiDict) + text = parseResult.trimmed activeEntities = parseResult.activeEntities accessibilityLabel = parseResult.value } diff --git a/Mastodon/Extension/CoreDataStack/Fields.swift b/Mastodon/Extension/CoreDataStack/Fields.swift new file mode 100644 index 00000000..795863f8 --- /dev/null +++ b/Mastodon/Extension/CoreDataStack/Fields.swift @@ -0,0 +1,27 @@ +// +// Fields.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-25. +// + +import Foundation +import MastodonSDK + +protocol FieldContinaer { + var fieldsData: Data? { get } +} + +extension FieldContinaer { + + static func encode(fields: [Mastodon.Entity.Field]) -> Data? { + return try? JSONEncoder().encode(fields) + } + + var fields: [Mastodon.Entity.Field]? { + let decoder = JSONDecoder() + return fieldsData.flatMap { try? decoder.decode([Mastodon.Entity.Field].self, from: $0) } + } + +} + diff --git a/Mastodon/Extension/CoreDataStack/MastodonUser.swift b/Mastodon/Extension/CoreDataStack/MastodonUser.swift index 8180b025..f5cdc1af 100644 --- a/Mastodon/Extension/CoreDataStack/MastodonUser.swift +++ b/Mastodon/Extension/CoreDataStack/MastodonUser.swift @@ -23,7 +23,8 @@ extension MastodonUser.Property { headerStatic: entity.headerStatic, note: entity.note, url: entity.url, - emojisData: entity.emojis.flatMap { Status.encode(emojis: $0) }, + emojisData: entity.emojis.flatMap { MastodonUser.encode(emojis: $0) }, + fieldsData: entity.fields.flatMap { MastodonUser.encode(fields: $0) }, statusesCount: entity.statusesCount, followingCount: entity.followingCount, followersCount: entity.followersCount, @@ -101,3 +102,4 @@ extension MastodonUser { } extension MastodonUser: EmojiContinaer { } +extension MastodonUser: FieldContinaer { } diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift new file mode 100644 index 00000000..c7fe6346 --- /dev/null +++ b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift @@ -0,0 +1,19 @@ +// +// Mastodon+Entity+Field.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-25. +// + +import Foundation +import MastodonSDK + +extension Mastodon.Entity.Field: Equatable { + public static func == (lhs: Mastodon.Entity.Field, rhs: Mastodon.Entity.Field) -> Bool { + return lhs.name == rhs.name && + lhs.value == rhs.value && + lhs.verifiedAt == rhs.verifiedAt + } + + +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 7253d6ef..de8403ac 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -626,6 +626,16 @@ internal enum L10n { } } } + internal enum Fields { + /// Add Row + internal static let addRow = L10n.tr("Localizable", "Scene.Profile.Fields.AddRow") + internal enum Placeholder { + /// Content + internal static let content = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Content") + /// Label + internal static let label = L10n.tr("Localizable", "Scene.Profile.Fields.Placeholder.Label") + } + } internal enum RelationshipActionAlert { internal enum ConfirmUnblockUsre { /// Confirm unblock %@ diff --git a/Mastodon/Helper/MastodonField.swift b/Mastodon/Helper/MastodonField.swift index 5f652b32..12b03c91 100644 --- a/Mastodon/Helper/MastodonField.swift +++ b/Mastodon/Helper/MastodonField.swift @@ -10,12 +10,25 @@ import ActiveLabel enum MastodonField { - static func parse(field string: String) -> ParseResult { + static func parse(field string: String, emojiDict: MastodonStatusContent.EmojiDict) -> ParseResult { + // use content parser get emoji entities + let value = string + + var string = string + var entities: [ActiveEntity] = [] + + do { + let contentParseresult = try MastodonStatusContent.parse(content: string, emojiDict: emojiDict) + string = contentParseresult.trimmed + entities.append(contentsOf: contentParseresult.activeEntities) + } catch { + // assertionFailure(error.localizedDescription) + } + let mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)") let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))") let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)") - var entities: [ActiveEntity] = [] for match in mentionMatches { guard let text = string.substring(with: match, at: 0) else { continue } @@ -35,7 +48,7 @@ enum MastodonField { entities.append(entity) } - return ParseResult(value: string, activeEntities: entities) + return ParseResult(value: value, trimmed: string, activeEntities: entities) } } @@ -43,6 +56,7 @@ enum MastodonField { extension MastodonField { struct ParseResult { let value: String + let trimmed: String let activeEntities: [ActiveEntity] } } diff --git a/Mastodon/Helper/MastodonStatusContent.swift b/Mastodon/Helper/MastodonStatusContent.swift index 0f9dbc6c..391519e8 100755 --- a/Mastodon/Helper/MastodonStatusContent.swift +++ b/Mastodon/Helper/MastodonStatusContent.swift @@ -23,7 +23,7 @@ enum MastodonStatusContent { let pattern = ":\(shortcode):" content = content.replacingOccurrences(of: pattern, with: emojiNode) } - return content + return content.trimmingCharacters(in: .whitespacesAndNewlines) }() let rootNode = try Node.parse(document: document) let text = String(rootNode.text) diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index ceb69045..80fce62f 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -212,6 +212,9 @@ tap the link to confirm your account."; "Scene.Profile.Dashboard.Followers" = "followers"; "Scene.Profile.Dashboard.Following" = "following"; "Scene.Profile.Dashboard.Posts" = "posts"; +"Scene.Profile.Fields.AddRow" = "Add Row"; +"Scene.Profile.Fields.Placeholder.Content" = "Content"; +"Scene.Profile.Fields.Placeholder.Label" = "Label"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm unblock %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index ceb69045..80fce62f 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -212,6 +212,9 @@ tap the link to confirm your account."; "Scene.Profile.Dashboard.Followers" = "followers"; "Scene.Profile.Dashboard.Following" = "following"; "Scene.Profile.Dashboard.Posts" = "posts"; +"Scene.Profile.Fields.AddRow" = "Add Row"; +"Scene.Profile.Fields.Placeholder.Content" = "Content"; +"Scene.Profile.Fields.Placeholder.Label" = "Label"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm unblock %@"; "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account"; "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@"; diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 217ea658..87c25e4a 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import PhotosUI +import ActiveLabel import AlamofireImage import CropViewController import TwitterTextEditor @@ -16,6 +17,7 @@ import TwitterTextEditor protocol ProfileHeaderViewControllerDelegate: AnyObject { func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView) func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int) + func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) } final class ProfileHeaderViewController: UIViewController { @@ -96,6 +98,15 @@ extension ProfileHeaderViewController { ]) profileHeaderView.preservesSuperviewLayoutMargins = true + profileHeaderView.fieldCollectionView.delegate = self + viewModel.setupProfileFieldCollectionViewDiffableDataSource( + collectionView: profileHeaderView.fieldCollectionView, + profileFieldCollectionViewCellDelegate: self, + profileFieldAddEntryCollectionViewCellDelegate: self + ) + let longPressReorderGesture = UILongPressGestureRecognizer(target: self, action: #selector(ProfileHeaderViewController.longPressReorderGestureHandler(_:))) + profileHeaderView.fieldCollectionView.addGestureRecognizer(longPressReorderGesture) + pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false view.addSubview(pageSegmentedControl) NSLayoutConstraint.activate([ @@ -190,6 +201,25 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) + Publishers.CombineLatest( + viewModel.isEditing, + viewModel.displayProfileInfo.fields + ) + .receive(on: RunLoop.main) + .sink { [weak self] isEditing, fields in + guard let self = self else { return } + self.profileHeaderView.fieldCollectionView.isHidden = isEditing ? false : fields.isEmpty + } + .store(in: &disposeBag) + + viewModel.isEditing + .receive(on: RunLoop.main) + .sink { [weak self] isEditing in + guard let self = self else { return } + // self.profileHeaderView.fieldCollectionView. + } + .store(in: &disposeBag) + profileHeaderView.editAvatarButton.menu = createAvatarContextMenu() profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true } @@ -265,6 +295,48 @@ extension ProfileHeaderViewController { delegate?.profileHeaderViewController(self, pageSegmentedControlValueChanged: sender, selectedSegmentIndex: sender.selectedSegmentIndex) } + // seealso: ProfileHeaderViewModel.setupProfileFieldCollectionViewDiffableDataSource(…) + @objc private func longPressReorderGestureHandler(_ sender: UILongPressGestureRecognizer) { + guard sender.view === profileHeaderView.fieldCollectionView else { + assertionFailure() + return + } + let collectionView = profileHeaderView.fieldCollectionView + switch(sender.state) { + case .began: + guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), + let cell = collectionView.cellForItem(at: selectedIndexPath) as? ProfileFieldCollectionViewCell else { + break + } + // check if pressing reorder bar no not + let locationInCell = sender.location(in: cell.reorderBarImageView) + guard cell.reorderBarImageView.bounds.contains(locationInCell) else { + return + } + + collectionView.beginInteractiveMovementForItem(at: selectedIndexPath) + case .changed: + guard let selectedIndexPath = collectionView.indexPathForItem(at: sender.location(in: collectionView)), + let diffableDataSource = viewModel.fieldDiffableDataSource else { + break + } + guard let item = diffableDataSource.itemIdentifier(for: selectedIndexPath), + case .field = item else { + collectionView.cancelInteractiveMovement() + return + } + + var position = sender.location(in: collectionView) + position.x = collectionView.frame.width * 0.5 + collectionView.updateInteractiveMovementTargetPosition(position) + case .ended: + collectionView.endInteractiveMovement() + collectionView.reloadData() + default: + collectionView.cancelInteractiveMovement() + } + } + } extension ProfileHeaderViewController { @@ -435,3 +507,28 @@ extension ProfileHeaderViewController: CropViewControllerDelegate { } } +// MARK: - UICollectionViewDelegate +extension ProfileHeaderViewController: UICollectionViewDelegate { + +} + +// MARK: - ProfileFieldCollectionViewCellDelegate +extension ProfileHeaderViewController: ProfileFieldCollectionViewCellDelegate { + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) { + guard let diffableDataSource = viewModel.fieldDiffableDataSource else { return } + guard let indexPath = profileHeaderView.fieldCollectionView.indexPath(for: cell) else { return } + guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } + viewModel.removeFieldItem(item: item) + } + + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { + delegate?.profileHeaderViewController(self, profileFieldCollectionViewCell: cell, activeLabel: activeLabel, didSelectActiveEntity: entity) + } +} + +// MARK: - ProfileFieldAddEntryCollectionViewCellDelegate +extension ProfileHeaderViewController: ProfileFieldAddEntryCollectionViewCellDelegate { + func ProfileFieldAddEntryCollectionViewCellDidPressed(_ cell: ProfileFieldAddEntryCollectionViewCell) { + viewModel.appendFieldItem() + } +} diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel+Diffable.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel+Diffable.swift new file mode 100644 index 00000000..b02eaa61 --- /dev/null +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel+Diffable.swift @@ -0,0 +1,43 @@ +// +// ProfileHeaderViewModel+Diffable.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-25. +// + +import UIKit + +extension ProfileHeaderViewModel { + func setupProfileFieldCollectionViewDiffableDataSource( + collectionView: UICollectionView, + profileFieldCollectionViewCellDelegate: ProfileFieldCollectionViewCellDelegate, + profileFieldAddEntryCollectionViewCellDelegate: ProfileFieldAddEntryCollectionViewCellDelegate + ) { + let diffableDataSource = ProfileFieldSection.collectionViewDiffableDataSource( + for: collectionView, + profileFieldCollectionViewCellDelegate: profileFieldCollectionViewCellDelegate, + profileFieldAddEntryCollectionViewCellDelegate: profileFieldAddEntryCollectionViewCellDelegate + ) + + diffableDataSource.reorderingHandlers.canReorderItem = { item in + switch item { + case .field: return true + default: return false + } + } + + diffableDataSource.reorderingHandlers.didReorder = { [weak self] transaction in + guard let self = self else { return } + + let items = transaction.finalSnapshot.itemIdentifiers + var fieldValues: [ProfileFieldItem.FieldValue] = [] + for item in items { + guard case let .field(field, _) = item else { continue } + fieldValues.append(field) + } + self.editProfileInfo.fields.value = fieldValues + } + + fieldDiffableDataSource = diffableDataSource + } +} diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 6e4fe2de..05cec901 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -5,6 +5,7 @@ // Created by MainasuK Cirno on 2021-4-9. // +import os.log import UIKit import Combine import Kanna @@ -12,6 +13,8 @@ import MastodonSDK final class ProfileHeaderViewModel { + static let maxProfileFieldCount = 4 + var disposeBag = Set() // input @@ -20,11 +23,13 @@ final class ProfileHeaderViewModel { let viewDidAppear = CurrentValueSubject(false) let needsSetupBottomShadow = CurrentValueSubject(true) let isTitleViewContentOffsetSet = CurrentValueSubject(false) + let emojiDict = CurrentValueSubject([:]) // output let displayProfileInfo = ProfileInfo() let editProfileInfo = ProfileInfo() let isTitleViewDisplaying = CurrentValueSubject(false) + var fieldDiffableDataSource: UICollectionViewDiffableDataSource! init(context: AppContext) { self.context = context @@ -35,11 +40,64 @@ final class ProfileHeaderViewModel { .sink { [weak self] isEditing in guard let self = self else { return } // setup editing value when toggle to editing - self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name - self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty + self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name + self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty self.editProfileInfo.note.value = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note.value) + self.editProfileInfo.fields.value = self.displayProfileInfo.fields.value.map { $0.duplicate() } // set to fields } .store(in: &disposeBag) + + Publishers.CombineLatest4( + isEditing.removeDuplicates(), + displayProfileInfo.fields.removeDuplicates(), + editProfileInfo.fields.removeDuplicates(), + emojiDict.removeDuplicates() + ) + .receive(on: RunLoop.main) + .sink { [weak self] isEditing, displayFields, editingFields, emojiDict in + guard let self = self else { return } + guard let diffableDataSource = self.fieldDiffableDataSource else { return } + + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + + let oldSnapshot = diffableDataSource.snapshot() + let oldFieldAttributeDict: [UUID: ProfileFieldItem.FieldItemAttribute] = { + var dict: [UUID: ProfileFieldItem.FieldItemAttribute] = [:] + for item in oldSnapshot.itemIdentifiers { + switch item { + case .field(let field, let attribute): + dict[field.id] = attribute + default: + continue + } + } + return dict + }() + let fields: [ProfileFieldItem.FieldValue] = isEditing ? editingFields : displayFields + var items = fields.map { field -> ProfileFieldItem in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: process field item ID: %s", ((#file as NSString).lastPathComponent), #line, #function, field.id.uuidString) + + let attribute = oldFieldAttributeDict[field.id] ?? ProfileFieldItem.FieldItemAttribute() + attribute.isEditing = isEditing + attribute.emojiDict.value = emojiDict + attribute.isLast = false + return ProfileFieldItem.field(field: field, attribute: attribute) + } + + if isEditing, fields.count < ProfileHeaderViewModel.maxProfileFieldCount { + items.append(.addEntry(attribute: ProfileFieldItem.AddEntryItemAttribute())) + } + + if let last = items.last?.listSeparatorLineConfigurable { + last.isLast = true + } + + snapshot.appendItems(items, toSection: .main) + + diffableDataSource.apply(snapshot, animatingDifferences: false, completion: nil) + } + .store(in: &disposeBag) } } @@ -49,6 +107,7 @@ extension ProfileHeaderViewModel { let name = CurrentValueSubject(nil) let avatarImageResource = CurrentValueSubject(nil) let note = CurrentValueSubject(nil) + let fields = CurrentValueSubject<[ProfileFieldItem.FieldValue], Never>([]) enum ImageResource { case url(URL?) @@ -57,6 +116,23 @@ extension ProfileHeaderViewModel { } } +extension ProfileHeaderViewModel { + func appendFieldItem() { + var fields = editProfileInfo.fields.value + guard fields.count < ProfileHeaderViewModel.maxProfileFieldCount else { return } + fields.append(ProfileFieldItem.FieldValue(name: "", value: "")) + editProfileInfo.fields.value = fields + } + + func removeFieldItem(item: ProfileFieldItem) { + var fields = editProfileInfo.fields.value + guard case let .field(field, _) = item else { return } + guard let removeIndex = fields.firstIndex(of: field) else { return } + fields.remove(at: removeIndex) + editProfileInfo.fields.value = fields + } +} + extension ProfileHeaderViewModel { static func normalize(note: String?) -> String? { @@ -75,6 +151,7 @@ extension ProfileHeaderViewModel { guard editProfileInfo.name.value == displayProfileInfo.name.value else { return true } guard case let .image(image) = editProfileInfo.avatarImageResource.value, image == nil else { return true } guard editProfileInfo.note.value == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note.value) else { return true } + guard editProfileInfo.fields.value == displayProfileInfo.fields.value else { return true } return false } @@ -95,6 +172,10 @@ extension ProfileHeaderViewModel { return image }() + let fieldsAttributes = editProfileInfo.fields.value.map { fieldValue in + Mastodon.Entity.Field(name: fieldValue.name.value, value: fieldValue.value.value) + } + let query = Mastodon.API.Account.UpdateCredentialQuery( discoverable: nil, bot: nil, @@ -104,7 +185,7 @@ extension ProfileHeaderViewModel { header: nil, locked: nil, source: nil, - fieldsAttributes: nil // TODO: + fieldsAttributes: fieldsAttributes ) return context.apiService.accountUpdateCredentials( domain: domain, diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift new file mode 100644 index 00000000..c7800b6a --- /dev/null +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift @@ -0,0 +1,170 @@ +// +// ProfileFieldAddEntryCollectionViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-26. +// + +import os.log +import UIKit +import Combine + +protocol ProfileFieldAddEntryCollectionViewCellDelegate: AnyObject { + func ProfileFieldAddEntryCollectionViewCellDidPressed(_ cell: ProfileFieldAddEntryCollectionViewCell) +} + +final class ProfileFieldAddEntryCollectionViewCell: UICollectionViewCell { + + var disposeBag = Set() + + weak var delegate: ProfileFieldAddEntryCollectionViewCellDelegate? + + let singleTagGestureRecognizer = UITapGestureRecognizer.singleTapGestureRecognizer + + + static let symbolConfiguration = ProfileFieldCollectionViewCell.symbolConfiguration + static let insertButtonImage = UIImage(systemName: "plus.circle.fill", withConfiguration: symbolConfiguration) + + let containerStackView = UIStackView() + + let fieldView = ProfileFieldView() + + let editButton: UIButton = { + let button = HitTestExpandedButton(type: .custom) + button.setImage(ProfileFieldAddEntryCollectionViewCell.insertButtonImage, for: .normal) + button.contentMode = .center + button.tintColor = .systemGreen + return button + }() + + var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! + var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! + let bottomSeparatorLine = UIView.separatorLine + + override func prepareForReuse() { + super.prepareForReuse() + + //resetStackView() + disposeBag.removeAll() + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ProfileFieldAddEntryCollectionViewCell { + + private func _init() { + containerStackView.axis = .horizontal + containerStackView.spacing = 8 + + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), + ]) + + containerStackView.addArrangedSubview(editButton) + containerStackView.addArrangedSubview(fieldView) + + editButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + editButton.setContentHuggingPriority(.required - 1, for: .horizontal) + + bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false + separatorLineToEdgeTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor) + + addSubview(bottomSeparatorLine) + NSLayoutConstraint.activate([ + bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), + bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), + ]) + + fieldView.titleTextField.text = L10n.Scene.Profile.Fields.addRow + fieldView.valueActiveLabel.configure(field: " ", emojiDict: [:]) + + addGestureRecognizer(singleTagGestureRecognizer) + singleTagGestureRecognizer.addTarget(self, action: #selector(ProfileFieldAddEntryCollectionViewCell.singleTapGestureRecognizerHandler(_:))) + + editButton.addTarget(self, action: #selector(ProfileFieldAddEntryCollectionViewCell.addButtonDidPressed(_:)), for: .touchUpInside) + + resetSeparatorLineLayout() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + resetSeparatorLineLayout() + } + +} + +extension ProfileFieldAddEntryCollectionViewCell { + + @objc private func singleTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.ProfileFieldAddEntryCollectionViewCellDidPressed(self) + } + + @objc private func addButtonDidPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.ProfileFieldAddEntryCollectionViewCellDidPressed(self) + } + +} + +extension ProfileFieldAddEntryCollectionViewCell { + private func resetSeparatorLineLayout() { + separatorLineToEdgeTrailingLayoutConstraint.isActive = false + separatorLineToMarginTrailingLayoutConstraint.isActive = false + + if traitCollection.userInterfaceIdiom == .phone { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + if traitCollection.horizontalSizeClass == .compact { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + // to margin + NSLayoutConstraint.activate([ + separatorLineToMarginTrailingLayoutConstraint, + ]) + } + } + } +} + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct ProfileFieldAddEntryCollectionViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + ProfileFieldAddEntryCollectionViewCell() + } + .previewLayout(.fixed(width: 375, height: 44)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift new file mode 100644 index 00000000..cf4d862b --- /dev/null +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift @@ -0,0 +1,179 @@ +// +// ProfileFieldCollectionViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-25. +// + +import os.log +import UIKit +import Combine +import ActiveLabel + +protocol ProfileFieldCollectionViewCellDelegate: AnyObject { + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, editButtonDidPressed button: UIButton) + func profileFieldCollectionViewCell(_ cell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) +} + +final class ProfileFieldCollectionViewCell: UICollectionViewCell { + + var disposeBag = Set() + + weak var delegate: ProfileFieldCollectionViewCellDelegate? + + static let symbolConfiguration = UIImage.SymbolConfiguration(pointSize: 22, weight: .semibold, scale: .medium) + static let removeButtonItem = UIImage(systemName: "minus.circle.fill", withConfiguration: symbolConfiguration) + + let containerStackView = UIStackView() + + let fieldView = ProfileFieldView() + + let editButton: UIButton = { + let button = HitTestExpandedButton(type: .custom) + button.setImage(ProfileFieldCollectionViewCell.removeButtonItem, for: .normal) + button.contentMode = .center + button.tintColor = .systemRed + return button + }() + + let reorderBarImageView: UIImageView = { + let imageView = UIImageView() + imageView.contentMode = .scaleAspectFit + imageView.image = UIImage(systemName: "line.horizontal.3")?.withConfiguration(UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)).withRenderingMode(.alwaysTemplate) + imageView.tintColor = Asset.Colors.Label.secondary.color + return imageView + }() + + var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! + var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! + let bottomSeparatorLine = UIView.separatorLine + + override func prepareForReuse() { + super.prepareForReuse() + + disposeBag.removeAll() + } + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ProfileFieldCollectionViewCell { + + private func _init() { + containerStackView.axis = .horizontal + containerStackView.spacing = 8 + + containerStackView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(containerStackView) + NSLayoutConstraint.activate([ + containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), + ]) + + containerStackView.addArrangedSubview(editButton) + containerStackView.addArrangedSubview(fieldView) + containerStackView.addArrangedSubview(reorderBarImageView) + + editButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + editButton.setContentHuggingPriority(.required - 1, for: .horizontal) + reorderBarImageView.setContentHuggingPriority(.required - 1, for: .horizontal) + reorderBarImageView.setContentCompressionResistancePriority(.required - 1, for: .horizontal) + + bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false + separatorLineToEdgeTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) + separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor) + + addSubview(bottomSeparatorLine) + NSLayoutConstraint.activate([ + bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), + bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), + bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), + ]) + + editButton.addTarget(self, action: #selector(ProfileFieldCollectionViewCell.editButtonDidPressed(_:)), for: .touchUpInside) + + fieldView.valueActiveLabel.delegate = self + + resetSeparatorLineLayout() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + + resetSeparatorLineLayout() + } + +} + +extension ProfileFieldCollectionViewCell { + private func resetSeparatorLineLayout() { + separatorLineToEdgeTrailingLayoutConstraint.isActive = false + separatorLineToMarginTrailingLayoutConstraint.isActive = false + + if traitCollection.userInterfaceIdiom == .phone { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + if traitCollection.horizontalSizeClass == .compact { + // to edge + NSLayoutConstraint.activate([ + separatorLineToEdgeTrailingLayoutConstraint, + ]) + } else { + // to margin + NSLayoutConstraint.activate([ + separatorLineToMarginTrailingLayoutConstraint, + ]) + } + } + } +} + +extension ProfileFieldCollectionViewCell { + @objc private func editButtonDidPressed(_ sender: UIButton) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.profileFieldCollectionViewCell(self, editButtonDidPressed: sender) + } +} + + + +// MARK: - ActiveLabelDelegate +extension ProfileFieldCollectionViewCell: ActiveLabelDelegate { + func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + delegate?.profileFieldCollectionViewCell(self, activeLabel: activeLabel, didSelectActiveEntity: entity) + } +} + + +#if canImport(SwiftUI) && DEBUG +import SwiftUI + +struct ProfileFieldCollectionViewCell_Previews: PreviewProvider { + + static var previews: some View { + UIViewPreview(width: 375) { + ProfileFieldCollectionViewCell() + } + .previewLayout(.fixed(width: 375, height: 44)) + } + +} + +#endif + diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift new file mode 100644 index 00000000..be61691c --- /dev/null +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift @@ -0,0 +1,41 @@ +// +// ProfileFieldCollectionViewHeaderFooterView.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-26. +// + +import UIKit + +final class ProfileFieldCollectionViewHeaderFooterView: UICollectionReusableView { + + static let headerReuseIdentifer = "ProfileFieldCollectionViewHeaderFooterView.Header" + static let footerReuseIdentifer = "ProfileFieldCollectionViewHeaderFooterView.Footer" + + let separatorLine = UIView.separatorLine + + override init(frame: CGRect) { + super.init(frame: frame) + _init() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + _init() + } + +} + +extension ProfileFieldCollectionViewHeaderFooterView { + private func _init() { + separatorLine.translatesAutoresizingMaskIntoConstraints = false + addSubview(separatorLine) + NSLayoutConstraint.activate([ + separatorLine.topAnchor.constraint(equalTo: topAnchor), + separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), + separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), + separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), + separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), + ]) + } +} diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift index 320a495e..1900a00f 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift @@ -6,26 +6,42 @@ // import UIKit +import Combine import ActiveLabel final class ProfileFieldView: UIView { - let titleLabel: UILabel = { - let label = UILabel() - label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)) - label.textColor = Asset.Colors.Label.primary.color - label.text = "Title" - return label + var disposeBag = Set() + + // output + let name = PassthroughSubject() + let value = PassthroughSubject() + + let titleTextField: UITextField = { + let textField = UITextField() + textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) + textField.textColor = Asset.Colors.Label.primary.color + textField.placeholder = L10n.Scene.Profile.Fields.Placeholder.label + textField.isEnabled = false + return textField }() + // for custom emoji display let valueActiveLabel: ActiveLabel = { let label = ActiveLabel(style: .profileField) label.configure(content: "value", emojiDict: [:]) return label }() - let topSeparatorLine = UIView.separatorLine - let bottomSeparatorLine = UIView.separatorLine + // for editing + let valueTextField: UITextField = { + let textField = UITextField() + textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) + textField.textColor = Asset.Colors.Label.primary.color + textField.placeholder = L10n.Scene.Profile.Fields.Placeholder.content + textField.textAlignment = .right + return textField + }() override init(frame: CGRect) { super.init(frame: frame) @@ -41,42 +57,60 @@ final class ProfileFieldView: UIView { extension ProfileFieldView { private func _init() { - titleLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(titleLabel) + + let containerStackView = UIStackView() + containerStackView.axis = .horizontal + containerStackView.alignment = .center + + // note: + // do not use readable layout guide to workaround SDK issue + // otherwise, the `ProfileFieldCollectionViewCell` cannot display edit button and reorder icon + containerStackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(containerStackView) NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: topAnchor), - titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor), - titleLabel.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), + containerStackView.topAnchor.constraint(equalTo: topAnchor), + containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), + containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) - titleLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) + titleTextField.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(titleTextField) + NSLayoutConstraint.activate([ + titleTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), + ]) + titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false - addSubview(valueActiveLabel) + containerStackView.addArrangedSubview(valueActiveLabel) NSLayoutConstraint.activate([ - valueActiveLabel.centerYAnchor.constraint(equalTo: centerYAnchor), - valueActiveLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor), - valueActiveLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor), + valueActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), ]) - valueActiveLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal) - - topSeparatorLine.translatesAutoresizingMaskIntoConstraints = false - addSubview(topSeparatorLine) + valueActiveLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + valueTextField.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(valueTextField) NSLayoutConstraint.activate([ - topSeparatorLine.topAnchor.constraint(equalTo: topAnchor), - topSeparatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), - topSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), - topSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), + valueTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), ]) - bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false - addSubview(bottomSeparatorLine) - NSLayoutConstraint.activate([ - bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), - bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), - bottomSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), - ]) + valueTextField.isHidden = true + + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: titleTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.name.send(self.titleTextField.text ?? "") + } + .store(in: &disposeBag) + + NotificationCenter.default + .publisher(for: UITextField.textDidChangeNotification, object: valueTextField) + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + guard let self = self else { return } + self.value.send(self.valueTextField.text ?? "") + } + .store(in: &disposeBag) } } @@ -88,7 +122,7 @@ struct ProfileFieldView_Previews: PreviewProvider { static var previews: some View { UIViewPreview(width: 375) { let filedView = ProfileFieldView() - filedView.valueActiveLabel.configure(field: "https://mastodon.online") + filedView.valueActiveLabel.configure(field: "https://mastodon.online", emojiDict: [:]) return filedView } .previewLayout(.fixed(width: 375, height: 100)) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index e37427e3..4f38bc1d 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -17,8 +17,8 @@ protocol ProfileHeaderViewDelegate: AnyObject { func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView) - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView) + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView) } final class ProfileHeaderView: UIView { @@ -80,6 +80,7 @@ final class ProfileHeaderView: UIView { view.layer.masksToBounds = true view.layer.cornerCurve = .continuous view.layer.cornerRadius = ProfileHeaderView.avatarImageViewCornerRadius + view.alpha = 0 // set initial state invisible return view }() @@ -152,6 +153,35 @@ final class ProfileHeaderView: UIView { return textEditorView }() + static func createFieldCollectionViewLayout() -> UICollectionViewLayout { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) + let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + + let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(1)) + let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) + let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom) + section.boundarySupplementaryItems = [header, footer] + + return UICollectionViewCompositionalLayout(section: section) + } + + let fieldCollectionView: UICollectionView = { + let collectionViewLayout = ProfileHeaderView.createFieldCollectionViewLayout() + let collectionView = UICollectionView(frame: CGRect(x: 0, y: 0, width: 100, height: 100), collectionViewLayout: collectionViewLayout) + collectionView.register(ProfileFieldCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self)) + collectionView.register(ProfileFieldAddEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self)) + collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.headerReuseIdentifer) + collectionView.register(ProfileFieldCollectionViewHeaderFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: ProfileFieldCollectionViewHeaderFooterView.footerReuseIdentifer) + collectionView.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color + collectionView.isScrollEnabled = false + return collectionView + }() + var fieldCollectionViewHeightLaoutConstraint: NSLayoutConstraint! + var fieldCollectionViewHeightObservation: NSKeyValueObservation? + override init(frame: CGRect) { super.init(frame: frame) _init() @@ -162,6 +192,10 @@ final class ProfileHeaderView: UIView { _init() } + deinit { + fieldCollectionViewHeightObservation = nil + } + } extension ProfileHeaderView { @@ -328,8 +362,20 @@ extension ProfileHeaderView { bioContainerStackView.addArrangedSubview(bioActiveLabelContainer) bioContainerStackView.addArrangedSubview(bioTextEditorView) - fieldContainerStackView.preservesSuperviewLayoutMargins = true - metaContainerStackView.addSubview(fieldContainerStackView) + fieldCollectionView.translatesAutoresizingMaskIntoConstraints = false + metaContainerStackView.addArrangedSubview(fieldCollectionView) + fieldCollectionViewHeightLaoutConstraint = fieldCollectionView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh) + NSLayoutConstraint.activate([ + fieldCollectionViewHeightLaoutConstraint, + ]) + fieldCollectionViewHeightObservation = fieldCollectionView.observe(\.contentSize, options: .new, changeHandler: { [weak self] tableView, _ in + guard let self = self else { return } + guard self.fieldCollectionView.contentSize.height != .zero else { + self.fieldCollectionViewHeightLaoutConstraint.constant = 44 + return + } + self.fieldCollectionViewHeightLaoutConstraint.constant = self.fieldCollectionView.contentSize.height + }) bringSubviewToFront(bannerContainerView) bringSubviewToFront(nameContainerStackView) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 7d3c581b..317d0234 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -368,6 +368,18 @@ extension ProfileViewController { .receive(on: DispatchQueue.main) .assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name) .store(in: &disposeBag) + viewModel.fileds + .removeDuplicates() + .map { fields -> [ProfileFieldItem.FieldValue] in + fields.map { ProfileFieldItem.FieldValue(name: $0.name, value: $0.value) } + } + .receive(on: DispatchQueue.main) + .assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.fields) + .store(in: &disposeBag) + viewModel.emojiDict + .receive(on: DispatchQueue.main) + .assign(to: \.value, on: profileHeaderViewController.viewModel.emojiDict) + .store(in: &disposeBag) viewModel.username .map { username in username.flatMap { "@" + $0 } ?? " " } .receive(on: DispatchQueue.main) @@ -664,6 +676,19 @@ extension ProfileViewController: ProfileHeaderViewControllerDelegate { ) } + func profileHeaderViewController(_ viewController: ProfileHeaderViewController, profileFieldCollectionViewCell: ProfileFieldCollectionViewCell, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) { + switch entity.type { + case .url(_, _, let url, _): + guard let url = URL(string: url) else { return } + coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + case .hashtag(let hashtag, _): + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) + coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) + default: + break + } + } + } // MARK: - ProfilePagingViewControllerDelegate @@ -852,20 +877,27 @@ extension ProfileViewController: ProfileHeaderViewDelegate { case .url(_, _, let url, _): guard let url = URL(string: url) else { return } coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + case .mention(_, let userInfo): + guard let href = userInfo?["href"] as? String, + let url = URL(string: href) else { return } + coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + case .hashtag(let hashtag, _): + let hashtagTimelineViewModel = HashtagTimelineViewModel(context: context, hashtag: hashtag) + coordinator.present(scene: .hashtagTimeline(viewModel: hashtagTimelineViewModel), from: nil, transition: .show) default: - // TODO: break } } func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) { - } - - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView) { } - func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView) { + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView) { + + } + + func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView) { } } diff --git a/Mastodon/Scene/Profile/ProfileViewModel.swift b/Mastodon/Scene/Profile/ProfileViewModel.swift index 445952e9..f9a89909 100644 --- a/Mastodon/Scene/Profile/ProfileViewModel.swift +++ b/Mastodon/Scene/Profile/ProfileViewModel.swift @@ -39,6 +39,8 @@ class ProfileViewModel: NSObject { let statusesCount: CurrentValueSubject let followingCount: CurrentValueSubject let followersCount: CurrentValueSubject + let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never> + let emojiDict: CurrentValueSubject let protected: CurrentValueSubject let suspended: CurrentValueSubject @@ -75,6 +77,8 @@ class ProfileViewModel: NSObject { self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) }) self.protected = CurrentValueSubject(mastodonUser?.locked) self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false) + self.fileds = CurrentValueSubject(mastodonUser?.fields ?? []) + self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:]) super.init() relationshipActionOptionSet @@ -231,6 +235,8 @@ extension ProfileViewModel { self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) } self.protected.value = mastodonUser?.locked self.suspended.value = mastodonUser?.suspended ?? false + self.fileds.value = mastodonUser?.fields ?? [] + self.emojiDict.value = mastodonUser?.emojiDict ?? [:] } private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) { diff --git a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift index 4a123705..90d482bc 100644 --- a/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift +++ b/Mastodon/Service/APIService/CoreData/APIService+CoreData+MastodonUser.swift @@ -98,6 +98,8 @@ extension APIService.CoreData { user.update(locked: property.locked) property.bot.flatMap { user.update(bot: $0) } property.suspended.flatMap { user.update(suspended: $0) } + property.emojisData.flatMap { user.update(emojisData: $0) } + property.fieldsData.flatMap { user.update(fieldsData: $0) } user.didUpdate(at: networkDate) } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift index 6f324627..cbc4fb8e 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift @@ -217,11 +217,9 @@ extension Mastodon.API.Account { source.sensitive.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } source.language.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } } - fieldsAttributes.flatMap { fieldsAttributes in - for fieldsAttribute in fieldsAttributes { - data.append(Data.multipart(key: "fields_attributes[name][]", value: fieldsAttribute.name)) - data.append(Data.multipart(key: "fields_attributes[value][]", value: fieldsAttribute.value)) - } + for (i, fieldsAttribute) in (fieldsAttributes ?? []).enumerated() { + data.append(Data.multipart(key: "fields_attributes[\(i)][name]", value: fieldsAttribute.name)) + data.append(Data.multipart(key: "fields_attributes[\(i)][value]", value: fieldsAttribute.value)) } data.append(Data.multipartEnd()) diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Field.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Field.swift index eb22d6ad..e16e3373 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Field.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+Field.swift @@ -27,5 +27,11 @@ extension Mastodon.Entity { case value case verifiedAt = "verified_at" } + + public init(name: String, value: String, verifiedAt: Date? = nil) { + self.name = name + self.value = value + self.verifiedAt = verifiedAt + } } } From 347f4701147dbdf11da29190b9d61ae6fd613ed8 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 14:06:16 +0800 Subject: [PATCH 02/33] fix: data fulfill may delay when first display compose scene issue --- Mastodon/Diffiable/Section/ComposeStatusSection.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 0c4ae883..0940ddfa 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -55,7 +55,10 @@ extension ComposeStatusSection { switch item { case .replyTo(let replyToStatusObjectID): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell - managedObjectContext.perform { + // set empty text before retrieve real data to fix pseudo-text display issue + cell.statusView.nameLabel.text = " " + cell.statusView.usernameLabel.text = " " + managedObjectContext.performAndWait { guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else { return } @@ -82,7 +85,7 @@ extension ComposeStatusSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell cell.statusContentWarningEditorView.textView.text = attribute.contentWarningContent.value cell.textEditorView.text = attribute.composeContent.value ?? "" - managedObjectContext.perform { + managedObjectContext.performAndWait { guard let replyToStatusObjectID = replyToStatusObjectID, let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else { cell.statusView.headerContainerView.isHidden = true From db71850fb328c57c8d788925f246df6d0aebfb49 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 14:16:02 +0800 Subject: [PATCH 03/33] fix: profile edit diff logic issue --- .../Profile/Header/ProfileHeaderViewModel.swift | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift index 05cec901..d0a29762 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewModel.swift @@ -151,7 +151,19 @@ extension ProfileHeaderViewModel { guard editProfileInfo.name.value == displayProfileInfo.name.value else { return true } guard case let .image(image) = editProfileInfo.avatarImageResource.value, image == nil else { return true } guard editProfileInfo.note.value == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note.value) else { return true } - guard editProfileInfo.fields.value == displayProfileInfo.fields.value else { return true } + let isFieldsEqual: Bool = { + let editFields = editProfileInfo.fields.value + let displayFields = displayProfileInfo.fields.value + guard editFields.count == displayFields.count else { return false } + for (editField, displayField) in zip(editFields, displayFields) { + guard editField.name.value == displayField.name.value, + editField.value.value == displayField.value.value else { + return false + } + } + return true + }() + guard isFieldsEqual else { return true } return false } From 6f55c0288c6357202424c14e795ca0f8420408d3 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 14:36:16 +0800 Subject: [PATCH 04/33] fix: profile field wrong layout margin issue --- Mastodon/Diffiable/Section/ProfileFieldSection.swift | 9 ++++++++- .../View/ProfileFieldAddEntryCollectionViewCell.swift | 11 +++++++---- .../Header/View/ProfileFieldCollectionViewCell.swift | 11 +++++++---- .../Scene/Profile/Header/View/ProfileHeaderView.swift | 2 ++ 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift index 339f754c..41334ba3 100644 --- a/Mastodon/Diffiable/Section/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift @@ -28,6 +28,10 @@ extension ProfileFieldSection { case .field(let field, let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell + let margin = max(0, collectionView.frame.width - collectionView.readableContentGuide.layoutFrame.width) + cell.containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin + // set key cell.fieldView.titleTextField.text = field.name.value field.name @@ -38,7 +42,6 @@ extension ProfileFieldSection { cell.fieldView.titleTextField.text = name } .store(in: &cell.disposeBag) - // set value cell.fieldView.valueActiveLabel.configure(field: field.value.value, emojiDict: attribute.emojiDict.value) @@ -89,6 +92,10 @@ extension ProfileFieldSection { case .addEntry(let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self), for: indexPath) as! ProfileFieldAddEntryCollectionViewCell + let margin = max(0, collectionView.frame.width - collectionView.readableContentGuide.layoutFrame.width) + cell.containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) + cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin + cell.bottomSeparatorLine.isHidden = attribute.isLast cell.delegate = profileFieldAddEntryCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift index c7800b6a..c44bac6c 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift @@ -37,6 +37,7 @@ final class ProfileFieldAddEntryCollectionViewCell: UICollectionViewCell { return button }() + var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint! var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! let bottomSeparatorLine = UIView.separatorLine @@ -70,11 +71,12 @@ extension ProfileFieldAddEntryCollectionViewCell { contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) + containerStackView.isLayoutMarginsRelativeArrangement = true containerStackView.addArrangedSubview(editButton) containerStackView.addArrangedSubview(fieldView) @@ -83,13 +85,14 @@ extension ProfileFieldAddEntryCollectionViewCell { editButton.setContentHuggingPriority(.required - 1, for: .horizontal) bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false + separatorLineToMarginLeadingLayoutConstraint = bottomSeparatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) separatorLineToEdgeTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) - separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor) + separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor) addSubview(bottomSeparatorLine) NSLayoutConstraint.activate([ + separatorLineToMarginLeadingLayoutConstraint, bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), - bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), ]) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift index cf4d862b..58af0ce7 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift @@ -44,6 +44,7 @@ final class ProfileFieldCollectionViewCell: UICollectionViewCell { return imageView }() + var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint! var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint! var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint! let bottomSeparatorLine = UIView.separatorLine @@ -76,11 +77,12 @@ extension ProfileFieldCollectionViewCell { contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) + containerStackView.isLayoutMarginsRelativeArrangement = true containerStackView.addArrangedSubview(editButton) containerStackView.addArrangedSubview(fieldView) @@ -92,13 +94,14 @@ extension ProfileFieldCollectionViewCell { reorderBarImageView.setContentCompressionResistancePriority(.required - 1, for: .horizontal) bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false + separatorLineToMarginLeadingLayoutConstraint = bottomSeparatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor) separatorLineToEdgeTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor) - separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor) + separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor) addSubview(bottomSeparatorLine) NSLayoutConstraint.activate([ + separatorLineToMarginLeadingLayoutConstraint, bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), - bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor), bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), ]) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 4f38bc1d..22e3e848 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -159,6 +159,8 @@ final class ProfileHeaderView: UIView { let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) + // note: manually set layout inset to workaround header footer layout issue + // section.contentInsetsReference = .readableContent let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(1)) let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) From 423bdb24735b75bb68817a8e58c17b1b9d678799 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 14:57:20 +0800 Subject: [PATCH 05/33] feat: support display custom emoji for field name --- .../Section/ProfileFieldSection.swift | 27 ++++++++++++------- Mastodon/Extension/ActiveLabel.swift | 9 +++++-- ...ofileFieldAddEntryCollectionViewCell.swift | 7 ++++- .../Header/View/ProfileFieldView.swift | 17 +++++++++++- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift index 41334ba3..0b67b00a 100644 --- a/Mastodon/Diffiable/Section/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift @@ -33,18 +33,24 @@ extension ProfileFieldSection { cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin // set key + cell.fieldView.titleActiveLabel.configure(field: field.name.value, emojiDict: attribute.emojiDict.value) cell.fieldView.titleTextField.text = field.name.value - field.name - .removeDuplicates() - .receive(on: RunLoop.main) - .sink { [weak cell] name in - guard let cell = cell else { return } - cell.fieldView.titleTextField.text = name - } - .store(in: &cell.disposeBag) + Publishers.CombineLatest( + field.name.removeDuplicates(), + attribute.emojiDict.removeDuplicates() + ) + .receive(on: RunLoop.main) + .sink { [weak cell] name, emojiDict in + guard let cell = cell else { return } + cell.fieldView.titleActiveLabel.configure(field: name, emojiDict: emojiDict) + cell.fieldView.titleTextField.text = name + } + .store(in: &cell.disposeBag) + // set value cell.fieldView.valueActiveLabel.configure(field: field.value.value, emojiDict: attribute.emojiDict.value) + cell.fieldView.valueTextField.text = field.value.value Publishers.CombineLatest( field.value.removeDuplicates(), attribute.emojiDict.removeDuplicates() @@ -72,9 +78,10 @@ extension ProfileFieldSection { } // setup editing state - cell.fieldView.titleTextField.isEnabled = attribute.isEditing - cell.fieldView.valueActiveLabel.isHidden = attribute.isEditing + cell.fieldView.titleTextField.isHidden = !attribute.isEditing cell.fieldView.valueTextField.isHidden = !attribute.isEditing + cell.fieldView.titleActiveLabel.isHidden = attribute.isEditing + cell.fieldView.valueActiveLabel.isHidden = attribute.isEditing // set control hidden let isHidden = !attribute.isEditing diff --git a/Mastodon/Extension/ActiveLabel.swift b/Mastodon/Extension/ActiveLabel.swift index 7975fad5..2c32bc4f 100644 --- a/Mastodon/Extension/ActiveLabel.swift +++ b/Mastodon/Extension/ActiveLabel.swift @@ -16,7 +16,8 @@ extension ActiveLabel { case `default` case statusHeader case statusName - case profileField + case profileFieldName + case profileFieldValue } convenience init(style: Style) { @@ -46,7 +47,11 @@ extension ActiveLabel { font = .systemFont(ofSize: 17, weight: .semibold) textColor = Asset.Colors.Label.primary.color numberOfLines = 1 - case .profileField: + case .profileFieldName: + font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) + textColor = Asset.Colors.Label.primary.color + numberOfLines = 1 + case .profileFieldValue: font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .regular), maximumPointSize: 20) textColor = Asset.Colors.Label.primary.color numberOfLines = 1 diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift index c44bac6c..1801d422 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift @@ -96,8 +96,13 @@ extension ProfileFieldAddEntryCollectionViewCell { bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), ]) - fieldView.titleTextField.text = L10n.Scene.Profile.Fields.addRow + fieldView.titleActiveLabel.isHidden = false + fieldView.titleActiveLabel.configure(field: L10n.Scene.Profile.Fields.addRow, emojiDict: [:]) + fieldView.titleTextField.isHidden = true + + fieldView.valueActiveLabel.isHidden = false fieldView.valueActiveLabel.configure(field: " ", emojiDict: [:]) + fieldView.valueTextField.isHidden = true addGestureRecognizer(singleTagGestureRecognizer) singleTagGestureRecognizer.addTarget(self, action: #selector(ProfileFieldAddEntryCollectionViewCell.singleTapGestureRecognizerHandler(_:))) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift index 1900a00f..14e5e4b3 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift @@ -17,6 +17,14 @@ final class ProfileFieldView: UIView { let name = PassthroughSubject() let value = PassthroughSubject() + // for custom emoji display + let titleActiveLabel: ActiveLabel = { + let label = ActiveLabel(style: .profileFieldName) + label.configure(content: "title", emojiDict: [:]) + return label + }() + + // for editing let titleTextField: UITextField = { let textField = UITextField() textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) @@ -28,7 +36,7 @@ final class ProfileFieldView: UIView { // for custom emoji display let valueActiveLabel: ActiveLabel = { - let label = ActiveLabel(style: .profileField) + let label = ActiveLabel(style: .profileFieldValue) label.configure(content: "value", emojiDict: [:]) return label }() @@ -73,6 +81,12 @@ extension ProfileFieldView { containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) + titleActiveLabel.translatesAutoresizingMaskIntoConstraints = false + containerStackView.addArrangedSubview(titleActiveLabel) + NSLayoutConstraint.activate([ + titleActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), + ]) + titleTextField.setContentHuggingPriority(.defaultLow - 1, for: .horizontal) titleTextField.translatesAutoresizingMaskIntoConstraints = false containerStackView.addArrangedSubview(titleTextField) NSLayoutConstraint.activate([ @@ -92,6 +106,7 @@ extension ProfileFieldView { valueTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh), ]) + titleTextField.isHidden = true valueTextField.isHidden = true NotificationCenter.default From bdb1a120f1655a8ffc82565a5a9b44ff982db43c Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 15:27:12 +0800 Subject: [PATCH 06/33] fix: profile avatar fade animation curve logic issue --- .../Profile/Header/ProfileHeaderViewController.swift | 10 +++++----- Mastodon/Scene/Profile/ProfileViewController.swift | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 87c25e4a..0de6f47e 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -362,7 +362,7 @@ extension ProfileHeaderViewController { } } - func updateHeaderScrollProgress(_ progress: CGFloat) { + func updateHeaderScrollProgress(_ progress: CGFloat, throttle: CGFloat) { // os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress) updateHeaderBottomShadow(progress: progress) @@ -408,12 +408,12 @@ extension ProfileHeaderViewController { viewModel.isTitleViewContentOffsetSet.value = true } - // set avatar + // set avatar fade if progress > 0 { setProfileBannerFade(alpha: 0) - } else if progress > -0.3 { - // y = -(10/3)x - let alpha = -10.0 / 3.0 * progress + } else if progress > -abs(throttle) { + // y = -(1/0.8T)x + let alpha = -1 / abs(0.8 * throttle) * progress setProfileBannerFade(alpha: alpha) } else { setProfileBannerFade(alpha: 1) diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 317d0234..267e5bce 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -652,7 +652,8 @@ extension ProfileViewController: UIScrollViewDelegate { // elastically banner image let headerScrollProgress = containerScrollView.contentOffset.y / topMaxContentOffsetY - profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress) + let throttle = ProfileHeaderViewController.headerMinHeight / topMaxContentOffsetY + profileHeaderViewController.updateHeaderScrollProgress(headerScrollProgress, throttle: throttle) } } From 1f787cf540358b3de7a38012eb2e00bc7cedf8e6 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 15:35:19 +0800 Subject: [PATCH 07/33] fix: profile avatar layer group alpha opacity issue --- .../Header/View/ProfileHeaderView.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 22e3e848..89859c98 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -229,24 +229,24 @@ extension ProfileHeaderView { ]) // avatar - avatarImageView.translatesAutoresizingMaskIntoConstraints = false - bannerContainerView.addSubview(avatarImageView) + avatarImageViewBackgroundView.translatesAutoresizingMaskIntoConstraints = false + bannerContainerView.addSubview(avatarImageViewBackgroundView) NSLayoutConstraint.activate([ - avatarImageView.leadingAnchor.constraint(equalTo: bannerContainerView.readableContentGuide.leadingAnchor), - bannerContainerView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20), - avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1), - avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1), + avatarImageViewBackgroundView.leadingAnchor.constraint(equalTo: bannerContainerView.readableContentGuide.leadingAnchor), + bannerContainerView.bottomAnchor.constraint(equalTo: avatarImageViewBackgroundView.bottomAnchor, constant: 20), ]) - avatarImageViewBackgroundView.translatesAutoresizingMaskIntoConstraints = false - bannerContainerView.insertSubview(avatarImageViewBackgroundView, belowSubview: avatarImageView) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + avatarImageViewBackgroundView.addSubview(avatarImageView) NSLayoutConstraint.activate([ avatarImageView.topAnchor.constraint(equalTo: avatarImageViewBackgroundView.topAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth), avatarImageView.leadingAnchor.constraint(equalTo: avatarImageViewBackgroundView.leadingAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth), avatarImageViewBackgroundView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth), avatarImageViewBackgroundView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth), + avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1), + avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1), ]) - + editAvatarBackgroundView.translatesAutoresizingMaskIntoConstraints = false avatarImageView.addSubview(editAvatarBackgroundView) NSLayoutConstraint.activate([ From 71214a1280c671bd60a36c1a84456d2cd0d96fd6 Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 15:55:37 +0800 Subject: [PATCH 08/33] feat: add status coordinator support for current active user domain --- .../StatusProvider/StatusProviderFacade.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift index 8c75b955..98fecc97 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProviderFacade.swift @@ -129,7 +129,17 @@ extension StatusProviderFacade { coordinateToStatusMentionProfileScene(for: .primary, provider: provider, cell: cell, mention: text) case .url(_, _, let url, _): guard let url = URL(string: url) else { return } - provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + if let domain = provider.context.authenticationService.activeMastodonAuthenticationBox.value?.domain, url.host == domain, + url.pathComponents.count >= 4, + url.pathComponents[0] == "/", + url.pathComponents[1] == "web", + url.pathComponents[2] == "statuses" { + let statusID = url.pathComponents[3] + let threadViewModel = RemoteThreadViewModel(context: provider.context, statusID: statusID) + provider.coordinator.present(scene: .thread(viewModel: threadViewModel), from: nil, transition: .show) + } else { + provider.coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil)) + } default: break } From 4a6b2a9d1d2462933b52c73a9c1f4586e4ad32cd Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 17:01:48 +0800 Subject: [PATCH 09/33] fix: image preview transition using wrong mask when nest as child controller issue --- ...wViewControllerAnimatedTransitioning.swift | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index eb4a43c1..3c7b5b8c 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -163,11 +163,18 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { var needsMaskWithAnimation = true let maskLayerToRect: CGRect? = { guard case .mosaic = transitionItem.source else { return nil } - guard let navigationBar = toVC.navigationController?.navigationBar else { return nil } - let navigationBarFrameInWindow = toVC.view.convert(navigationBar.frame, to: nil) - var rect = transitionMaskView.frame - rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline + guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } + let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) + // crop rect top edge + var rect = transitionMaskView.frame + let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } + if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { + rect.origin.y = toViewFrameInWindow.minY + } else { + rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline + } + if rect.minY < snapshot.frame.minY { needsMaskWithAnimation = false } @@ -177,8 +184,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let maskLayerToPath = maskLayerToRect.flatMap { UIBezierPath(rect: $0) }?.cgPath let maskLayerToFinalRect: CGRect? = { guard case .mosaic = transitionItem.source else { return nil } - guard let tabBarController = toVC.tabBarController else { return nil } - let tabBarFrameInWindow = toVC.view.convert(tabBarController.tabBar.frame, to: nil) + guard let tabBarController = toVC.tabBarController, let tabBarSuperView = tabBarController.tabBar.superview else { return nil } + let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) var rect = maskLayerToRect ?? transitionMaskView.frame let offset = rect.maxY - tabBarFrameInWindow.minY guard offset > 0 else { return rect } @@ -411,11 +418,18 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { var needsMaskWithAnimation = true let maskLayerToRect: CGRect? = { guard case .mosaic = transitionItem.source else { return nil } - guard let navigationBar = toVC.navigationController?.navigationBar else { return nil } - let navigationBarFrameInWindow = toVC.view.convert(navigationBar.frame, to: nil) - var rect = transitionMaskView.frame - rect.origin.y = navigationBarFrameInWindow.maxY + guard let navigationBar = toVC.navigationController?.navigationBar, let navigationBarSuperView = navigationBar.superview else { return nil } + let navigationBarFrameInWindow = navigationBarSuperView.convert(navigationBar.frame, to: nil) + // crop rect top edge + var rect = transitionMaskView.frame + let _toViewFrameInWindow = toVC.view.superview.flatMap { $0.convert(toVC.view.frame, to: nil) } + if let toViewFrameInWindow = _toViewFrameInWindow, toViewFrameInWindow.minY > navigationBarFrameInWindow.maxY { + rect.origin.y = toViewFrameInWindow.minY + } else { + rect.origin.y = navigationBarFrameInWindow.maxY + UIView.separatorLineHeight(of: toVC.view) // extra hairline + } + if rect.minY < snapshot.frame.minY { needsMaskWithAnimation = false } @@ -430,8 +444,8 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { let maskLayerToFinalRect: CGRect? = { guard case .mosaic = transitionItem.source else { return nil } - guard let tabBarController = toVC.tabBarController else { return nil } - let tabBarFrameInWindow = toVC.view.convert(tabBarController.tabBar.frame, to: nil) + guard let tabBarController = toVC.tabBarController, let tabBarSuperView = tabBarController.tabBar.superview else { return nil } + let tabBarFrameInWindow = tabBarSuperView.convert(tabBarController.tabBar.frame, to: nil) var rect = maskLayerToRect ?? transitionMaskView.frame let offset = rect.maxY - tabBarFrameInWindow.minY guard offset > 0 else { return rect } From 252c58ad2c248bc73ef81f445c4affd64e3a4fdf Mon Sep 17 00:00:00 2001 From: CMK Date: Thu, 27 May 2021 17:32:42 +0800 Subject: [PATCH 10/33] fix: remove section inset hacking and set header footer with overflow layout constraint --- Mastodon/Diffiable/Section/ProfileFieldSection.swift | 8 -------- .../Profile/Header/ProfileHeaderViewController.swift | 8 -------- .../View/ProfileFieldCollectionViewHeaderFooterView.swift | 5 +++-- .../Scene/Profile/Header/View/ProfileHeaderView.swift | 5 +++-- 4 files changed, 6 insertions(+), 20 deletions(-) diff --git a/Mastodon/Diffiable/Section/ProfileFieldSection.swift b/Mastodon/Diffiable/Section/ProfileFieldSection.swift index 0b67b00a..82f88f65 100644 --- a/Mastodon/Diffiable/Section/ProfileFieldSection.swift +++ b/Mastodon/Diffiable/Section/ProfileFieldSection.swift @@ -28,10 +28,6 @@ extension ProfileFieldSection { case .field(let field, let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldCollectionViewCell.self), for: indexPath) as! ProfileFieldCollectionViewCell - let margin = max(0, collectionView.frame.width - collectionView.readableContentGuide.layoutFrame.width) - cell.containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin - // set key cell.fieldView.titleActiveLabel.configure(field: field.name.value, emojiDict: attribute.emojiDict.value) cell.fieldView.titleTextField.text = field.name.value @@ -99,10 +95,6 @@ extension ProfileFieldSection { case .addEntry(let attribute): let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ProfileFieldAddEntryCollectionViewCell.self), for: indexPath) as! ProfileFieldAddEntryCollectionViewCell - let margin = max(0, collectionView.frame.width - collectionView.readableContentGuide.layoutFrame.width) - cell.containerStackView.layoutMargins = UIEdgeInsets(top: 0, left: margin, bottom: 0, right: margin) - cell.separatorLineToMarginLeadingLayoutConstraint.constant = margin - cell.bottomSeparatorLine.isHidden = attribute.isLast cell.delegate = profileFieldAddEntryCollectionViewCellDelegate diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index 0de6f47e..c3495b7f 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -228,14 +228,6 @@ extension ProfileHeaderViewController { super.viewDidAppear(animated) viewModel.viewDidAppear.value = true - - // Deprecated: - // not needs this tweak due to force layout update in the parent - // if !isAdjustBannerImageViewForSafeAreaInset { - // isAdjustBannerImageViewForSafeAreaInset = true - // profileHeaderView.bannerImageView.frame.origin.y = -containerSafeAreaInset.top - // profileHeaderView.bannerImageView.frame.size.height += containerSafeAreaInset.top - // } } override func viewDidLayoutSubviews() { diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift index be61691c..83fec9bc 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewHeaderFooterView.swift @@ -32,8 +32,9 @@ extension ProfileFieldCollectionViewHeaderFooterView { addSubview(separatorLine) NSLayoutConstraint.activate([ separatorLine.topAnchor.constraint(equalTo: topAnchor), - separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor), - separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor), + // workaround SDK supplementariesFollowContentInsets not works issue + separatorLine.leadingAnchor.constraint(equalTo: leadingAnchor, constant: -9999), + separatorLine.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 9999), separatorLine.bottomAnchor.constraint(equalTo: bottomAnchor), separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh), ]) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift index 89859c98..54c57139 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileHeaderView.swift @@ -159,13 +159,14 @@ final class ProfileHeaderView: UIView { let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) let section = NSCollectionLayoutSection(group: group) - // note: manually set layout inset to workaround header footer layout issue - // section.contentInsetsReference = .readableContent + section.contentInsetsReference = .readableContent let headerFooterSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(1)) let header = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionHeader, alignment: .top) let footer = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: headerFooterSize, elementKind: UICollectionView.elementKindSectionFooter, alignment: .bottom) section.boundarySupplementaryItems = [header, footer] + // note: toggle this not take effect + // section.supplementariesFollowContentInsets = false return UICollectionViewCompositionalLayout(section: section) } From 90d8de1625da0ae694cf4912fd0aa27420be0c3c Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 28 May 2021 15:55:58 +0800 Subject: [PATCH 11/33] fix: compose scene memory leaking issue --- .../Section/ComposeStatusSection.swift | 23 ++++++++++++------- ...seStatusAttachmentCollectionViewCell.swift | 4 ++++ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 0940ddfa..46d00dbe 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -101,21 +101,24 @@ extension ComposeStatusSection { cell.composeContent .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { text in + .sink { [weak collectionView] text in + guard let collectionView = collectionView else { return } // self size input cell // needs restore content offset to resolve issue #83 let oldContentOffset = collectionView.contentOffset collectionView.collectionViewLayout.invalidateLayout() collectionView.layoutIfNeeded() collectionView.contentOffset = oldContentOffset - + // bind input data attribute.composeContent.value = text } .store(in: &cell.disposeBag) attribute.isContentWarningComposing .receive(on: DispatchQueue.main) - .sink { isContentWarningComposing in + .sink { [weak cell, weak collectionView] isContentWarningComposing in + guard let cell = cell else { return } + guard let collectionView = collectionView else { return } // self size input cell collectionView.collectionViewLayout.invalidateLayout() cell.statusContentWarningEditorView.containerView.isHidden = !isContentWarningComposing @@ -130,7 +133,8 @@ extension ComposeStatusSection { cell.contentWarningContent .removeDuplicates() .receive(on: DispatchQueue.main) - .sink { text in + .sink { [weak collectionView] text in + guard let collectionView = collectionView else { return } // self size input cell collectionView.collectionViewLayout.invalidateLayout() // bind input data @@ -145,9 +149,10 @@ extension ComposeStatusSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value cell.delegate = composeStatusAttachmentTableViewCellDelegate - attachmentService.imageData + attachmentService.data .receive(on: DispatchQueue.main) - .sink { imageData in + .sink { [weak cell] imageData in + guard let cell = cell else { return } let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) guard let imageData = imageData, let image = UIImage(data: imageData) else { @@ -171,7 +176,8 @@ extension ComposeStatusSection { attachmentService.error.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { uploadState, error in + .sink { [weak cell] uploadState, error in + guard let cell = cell else { return } cell.attachmentContainerView.emptyStateView.isHidden = error == nil cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil if let _ = error { @@ -220,7 +226,8 @@ extension ComposeStatusSection { cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) attribute.expiresOption .receive(on: DispatchQueue.main) - .sink { expiresOption in + .sink { [weak cell] expiresOption in + guard let cell = cell else { return } cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) } .store(in: &cell.disposeBag) diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift index 87fe0efa..0fa9319d 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusAttachmentCollectionViewCell.swift @@ -57,6 +57,10 @@ final class ComposeStatusAttachmentCollectionViewCell: UICollectionViewCell { _init() } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension ComposeStatusAttachmentCollectionViewCell { From a9744146cefaf57e4b8ad0c672a33936fee5d9ca Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:42:49 +0800 Subject: [PATCH 12/33] feat: add video attachment post supports --- Localization/app.json | 6 +- Mastodon.xcodeproj/project.pbxproj | 236 +++++++++--------- .../xcschemes/xcschememanagement.plist | 4 +- .../Section/ComposeStatusSection.swift | 25 +- Mastodon/Generated/Strings.swift | 6 + .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Scene/Compose/ComposeViewController.swift | 38 ++- Mastodon/Scene/Compose/ComposeViewModel.swift | 55 ++++ ...tachmentContainerView+EmptyStateView.swift | 2 + ...meTimelineViewController+DebugAction.swift | 10 + .../Header/ProfileHeaderViewController.swift | 4 +- ...astodonAttachmentService+UploadState.swift | 12 +- .../MastodonAttachmentService.swift | 69 ++++- Mastodon/Vender/PHPickerResultLoader.swift | 35 ++- .../MastodonSDK/API/Mastodon+API+Media.swift | 78 +++++- .../Sources/MastodonSDK/Extension/Data.swift | 7 +- .../MastodonSDK/Query/MediaAttachment.swift | 19 +- .../Query/MultipartFormValue.swift | 5 + .../MastodonSDK/Query/SerialStream.swift | 147 +++++++++++ Podfile | 17 +- Podfile.lock | 6 +- 22 files changed, 606 insertions(+), 179 deletions(-) create mode 100644 MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift diff --git a/Localization/app.json b/Localization/app.json index 920ec0d2..69d51360 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -21,7 +21,11 @@ }, "publish_post_failure": { "title": "Publish Failure", - "message": "Failed to publish the post.\nPlease check your internet connection." + "message": "Failed to publish the post.\nPlease check your internet connection.", + "attchments_message": { + "video_attach_with_photo": "Cannot attach a video to a status that already contains images.", + "more_than_one_video": "Cannot attach more than one video." + } }, "sign_out": { "title": "Sign out", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 28f11361..b1aaa14b 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -80,7 +80,7 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; + 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */; }; 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; @@ -90,7 +90,7 @@ 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; @@ -116,7 +116,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; + 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB968263A833E007C1D71 /* DomainBlock.swift */; }; @@ -164,7 +164,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; @@ -178,14 +178,13 @@ 5DFC35DF262068D20045711D /* SearchViewController+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; - 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; - D86919F5080C3F228CCD17D1 /* Pods_Mastodon_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */; }; - DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; + B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; + DB00CA972632DDB600A54956 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -217,7 +216,7 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; }; DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; }; DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; }; - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; @@ -258,7 +257,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; + DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -294,7 +293,7 @@ DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; - DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; + DB6805102637D0F800430867 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* SwiftPackageProductDependency */; }; DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; @@ -307,7 +306,7 @@ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; - DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; }; + DB6D9F42263527CE008423CD /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */; }; DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; }; DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; }; DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; }; @@ -318,8 +317,8 @@ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; }; - DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; }; - DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; }; + DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; @@ -383,7 +382,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; + DB9A487E2603456B008B817C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* SwiftPackageProductDependency */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; }; @@ -417,7 +416,7 @@ DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; + DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; @@ -464,10 +463,11 @@ DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; + DBF8AE862632992800C9C23C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; + EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -552,7 +552,7 @@ files = ( DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, - DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */, + DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -704,10 +704,10 @@ 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Searching.swift"; sourceTree = ""; }; 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchingTableViewCell.swift; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; + 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = ""; }; - 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = ""; }; 5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = ""; }; @@ -741,9 +741,11 @@ 5DF1058425F88AE500D6C0D4 /* NeedsDependency+AVPlayerViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NeedsDependency+AVPlayerViewControllerDelegate.swift"; sourceTree = ""; }; 5DFC35DE262068D20045711D /* SearchViewController+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+Follow.swift"; sourceTree = ""; }; 75E3471C898DDD9631729B6E /* Pods-Mastodon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.release.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.release.xcconfig"; sourceTree = ""; }; - 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_NotificationService.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.debug.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.debug.xcconfig"; sourceTree = ""; }; + 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-NotificationService/Pods-NotificationService.release.xcconfig"; sourceTree = ""; }; 9780A4C98FFC65B32B50D1C0 /* Pods-MastodonTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.release.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.release.xcconfig"; sourceTree = ""; }; + 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.debug.xcconfig"; sourceTree = ""; }; A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.release.xcconfig"; sourceTree = ""; }; B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-NotificationService.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-NotificationService/Pods-Mastodon-NotificationService.release.xcconfig"; sourceTree = ""; }; @@ -1036,6 +1038,8 @@ DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewHeaderFooterView.swift; sourceTree = ""; }; DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = ""; }; EC6E707B68A67DB08EC288FA /* Pods-MastodonTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.debug.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.debug.xcconfig"; sourceTree = ""; }; + ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AppShared.release.xcconfig"; path = "Target Support Files/Pods-AppShared/Pods-AppShared.release.xcconfig"; sourceTree = ""; }; + F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1043,20 +1047,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */, - DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, + DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */, + DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, - DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, - 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, - DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, - 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, + DB9A487E2603456B008B817C /* BuildFile in Frameworks */, + 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */, + DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */, + 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, + DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, - DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, + 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, + DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, + 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, ); @@ -1083,8 +1087,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6805102637D0F800430867 /* KeychainAccess in Frameworks */, - D86919F5080C3F228CCD17D1 /* Pods_Mastodon_AppShared.framework in Frameworks */, + DB6805102637D0F800430867 /* BuildFile in Frameworks */, + EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1108,11 +1112,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */, - DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */, - DBF8AE862632992800C9C23C /* Base85 in Frameworks */, + DB00CA972632DDB600A54956 /* BuildFile in Frameworks */, + DB6D9F42263527CE008423CD /* BuildFile in Frameworks */, + DBF8AE862632992800C9C23C /* BuildFile in Frameworks */, DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */, - 7D603B3DC600BB75956FAD7D /* Pods_Mastodon_NotificationService.framework in Frameworks */, + B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1199,6 +1203,10 @@ B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */, D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */, B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */, + 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */, + ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */, + 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */, + 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */, ); path = Pods; sourceTree = ""; @@ -1544,8 +1552,8 @@ A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */, 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */, 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */, - 79D7EB88A6A8B34DFDFC96FC /* Pods_Mastodon_NotificationService.framework */, - 46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */, + F4A2A2D7000E477CA459ADA9 /* Pods_AppShared.framework */, + 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */, ); name = Frameworks; sourceTree = ""; @@ -2489,17 +2497,17 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* AlamofireImage */, - 5D526FE125BE9AC400460CB9 /* MastodonSDK */, - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, - 2D42FF6025C8177C004A627A /* ActiveLabel */, - DB0140BC25C40D7500F9F3CF /* CommonOSLog */, - DB5086B725CC0D6400C2C187 /* Kingfisher */, - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, - 2D939AC725EE14620076FA61 /* CropViewController */, - DB9A487D2603456B008B817C /* UITextView+Placeholder */, - DBB525072611EAC0002F1F29 /* Tabman */, - DB6F5E31264E7410009108F4 /* TwitterTextEditor */, + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, + DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */, + 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */, + 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */, + DB9A487D2603456B008B817C /* SwiftPackageProductDependency */, + DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */, + DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2560,7 +2568,7 @@ ); name = AppShared; packageProductDependencies = ( - DB68050F2637D0F800430867 /* KeychainAccess */, + DB68050F2637D0F800430867 /* SwiftPackageProductDependency */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -2620,9 +2628,9 @@ ); name = NotificationService; packageProductDependencies = ( - DBF8AE852632992800C9C23C /* Base85 */, - DB00CA962632DDB600A54956 /* CommonOSLog */, - DB6D9F41263527CE008423CD /* AlamofireImage */, + DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */, + DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */, + DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -2677,18 +2685,18 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, - DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, - DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, - DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, + DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */, + 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */, + 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */, + DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */, + DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */, + DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */, + DB6804722637CC1200430867 /* RemoteSwiftPackageReference */, + DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -2780,7 +2788,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mastodon-NotificationService-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-NotificationService-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2863,7 +2871,7 @@ outputFileListPaths = ( ); outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Mastodon-AppShared-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-AppShared-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -3746,7 +3754,7 @@ }; DB6804892637CD4C00430867 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */; + baseConfigurationReference = 9A0982D8F349244EB558CDFD /* Pods-AppShared.debug.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; @@ -3777,7 +3785,7 @@ }; DB68048A2637CD4C00430867 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B31D44635FCF6452F7E1B865 /* Pods-Mastodon-AppShared.release.xcconfig */; + baseConfigurationReference = ECA373ABA86BE3C2D7ED878E /* Pods-AppShared.release.xcconfig */; buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CLANG_ENABLE_MODULES = YES; @@ -3904,7 +3912,7 @@ }; DBF8AE1C263293E400C9C23C /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 8ED8C4B1F1BA2DCFF2926BB1 /* Pods-Mastodon-NotificationService.debug.xcconfig */; + baseConfigurationReference = 9553C689FFA9EBC880CAB78D /* Pods-NotificationService.debug.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; @@ -3927,7 +3935,7 @@ }; DBF8AE1D263293E400C9C23C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B44342AC2B6585F8295F1DDF /* Pods-Mastodon-NotificationService.release.xcconfig */; + baseConfigurationReference = 9776D7C4B79101CF70181127 /* Pods-NotificationService.release.xcconfig */; buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; @@ -4026,7 +4034,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { + 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -4034,7 +4042,7 @@ version = 5.0.2; }; }; - 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { + 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; requirement = { @@ -4042,7 +4050,7 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { + 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -4050,7 +4058,7 @@ minimumVersion = 3.1.0; }; }; - 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { + 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; requirement = { @@ -4058,7 +4066,7 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { + DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -4066,7 +4074,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { + DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -4074,7 +4082,7 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { + DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -4082,7 +4090,7 @@ minimumVersion = 6.1.0; }; }; - DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { + DB6804722637CC1200430867 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4090,7 +4098,7 @@ minimumVersion = 4.2.2; }; }; - DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { + DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git"; requirement = { @@ -4098,7 +4106,7 @@ kind = branch; }; }; - DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { + DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; requirement = { @@ -4106,7 +4114,7 @@ minimumVersion = 1.4.1; }; }; - DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { + DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; requirement = { @@ -4114,7 +4122,7 @@ minimumVersion = 2.11.0; }; }; - DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = { + DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/Base85.git"; requirement = { @@ -4125,78 +4133,78 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* ActiveLabel */ = { + 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; + package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; productName = ActiveLabel; }; - 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { + 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; + package = 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { + 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; + package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; productName = AlamofireNetworkActivityIndicator; }; - 2D939AC725EE14620076FA61 /* CropViewController */ = { + 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; + package = 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */; productName = CropViewController; }; - 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { + 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB00CA962632DDB600A54956 /* CommonOSLog */ = { + DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { + DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; + package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { + DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* Kingfisher */ = { + DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; + package = DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */; productName = Kingfisher; }; - DB68050F2637D0F800430867 /* KeychainAccess */ = { + DB68050F2637D0F800430867 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; + package = DB6804722637CC1200430867 /* RemoteSwiftPackageReference */; productName = KeychainAccess; }; - DB6D9F41263527CE008423CD /* AlamofireImage */ = { + DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; + package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; productName = AlamofireImage; }; - DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = { + DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; + package = DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */; productName = TwitterTextEditor; }; - DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { + DB9A487D2603456B008B817C /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; + package = DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */; productName = "UITextView+Placeholder"; }; - DBB525072611EAC0002F1F29 /* Tabman */ = { + DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; + package = DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */; productName = Tabman; }; - DBF8AE852632992800C9C23C /* Base85 */ = { + DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */ = { isa = XCSwiftPackageProductDependency; - package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */; + package = DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */; productName = Base85; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 32685726..217fe199 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 14 + 15 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 15 + 16 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 46d00dbe..a1e6170e 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -149,13 +149,12 @@ extension ComposeStatusSection { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value cell.delegate = composeStatusAttachmentTableViewCellDelegate - attachmentService.data + attachmentService.thumbnailImage .receive(on: DispatchQueue.main) - .sink { [weak cell] imageData in + .sink { [weak cell] thumbnailImage in guard let cell = cell else { return } let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) - guard let imageData = imageData, - let image = UIImage(data: imageData) else { + guard let image = thumbnailImage else { let placeholder = UIImage.placeholder( size: size, color: Asset.Colors.Background.systemGroupedBackground.color @@ -176,18 +175,32 @@ extension ComposeStatusSection { attachmentService.error.eraseToAnyPublisher() ) .receive(on: DispatchQueue.main) - .sink { [weak cell] uploadState, error in + .sink { [weak cell, weak attachmentService] uploadState, error in guard let cell = cell else { return } + guard let attachmentService = attachmentService else { return } cell.attachmentContainerView.emptyStateView.isHidden = error == nil cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil - if let _ = error { + if let error = error { cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription } else { guard let uploadState = uploadState else { return } switch uploadState { case is MastodonAttachmentService.UploadState.Finish, is MastodonAttachmentService.UploadState.Fail: cell.attachmentContainerView.activityIndicatorView.stopAnimating() + cell.attachmentContainerView.emptyStateView.label.text = { + if let file = attachmentService.file.value { + switch file { + case .jpeg, .png, .gif: + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + case .other: + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) + } + } else { + return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) + } + }() default: break } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index de8403ac..5790f713 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -44,6 +44,12 @@ internal enum L10n { internal static let message = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Message") /// Publish Failure internal static let title = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.Title") + internal enum AttchmentsMessage { + /// Cannot attach more than one video. + internal static let moreThanOneVideo = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo") + /// Cannot attach a video to a status that already contains images. + internal static let videoAttachWithPhoto = L10n.tr("Localizable", "Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto") + } } internal enum SavePhotoFailure { /// Please enable photo libaray access permission to save photo. diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 80fce62f..81bf772e 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -6,6 +6,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. Please check your internet connection."; "Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 80fce62f..81bf772e 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -6,6 +6,8 @@ "Common.Alerts.DeletePost.Title" = "Are you sure you want to delete this post?"; "Common.Alerts.DiscardPostContent.Message" = "Confirm discard composed post content."; "Common.Alerts.DiscardPostContent.Title" = "Discard Publish"; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.MoreThanOneVideo" = "Cannot attach more than one video."; +"Common.Alerts.PublishPostFailure.AttchmentsMessage.VideoAttachWithPhoto" = "Cannot attach a video to a status that already contains images."; "Common.Alerts.PublishPostFailure.Message" = "Failed to publish the post. Please check your internet connection."; "Common.Alerts.PublishPostFailure.Title" = "Publish Failure"; diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 80a64064..2e3e5fe7 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -75,12 +75,15 @@ final class ComposeViewController: UIViewController, NeedsDependency { var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! let composeToolbarBackgroundView = UIView() - private(set) lazy var imagePicker: PHPickerViewController = { + static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { var configuration = PHPickerConfiguration() - configuration.filter = .images - configuration.selectionLimit = 4 - - let imagePicker = PHPickerViewController(configuration: configuration) + configuration.filter = .any(of: [.images, .videos]) + configuration.selectionLimit = selectionLimit + return configuration + } + + private(set) lazy var photoLibraryPicker: PHPickerViewController = { + let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) imagePicker.delegate = self return imagePicker }() @@ -567,12 +570,9 @@ extension ComposeViewController { } private func resetImagePicker() { - var configuration = PHPickerConfiguration() - configuration.filter = .images let selectionLimit = max(1, 4 - viewModel.attachmentServices.value.count) - configuration.selectionLimit = selectionLimit - - imagePicker = createImagePicker(configuration: configuration) + let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) + photoLibraryPicker = createImagePicker(configuration: configuration) } private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { @@ -610,6 +610,16 @@ extension ComposeViewController { @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + do { + try viewModel.checkAttachmentPrecondition() + } catch { + let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + return + } + guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { // TODO: handle error return @@ -913,7 +923,7 @@ extension ComposeViewController: ComposeToolbarViewDelegate { func composeToolbarView(_ composeToolbarView: ComposeToolbarView, cameraButtonDidPressed sender: UIButton, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { switch type { case .photoLibrary: - present(imagePicker, animated: true, completion: nil) + present(photoLibraryPicker, animated: true, completion: nil) case .camera: present(imagePickerController, animated: true, completion: nil) case .browse: @@ -1120,8 +1130,12 @@ extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelega var attachmentServices = viewModel.attachmentServices.value guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } + let removedItem = attachmentServices[index] attachmentServices.remove(at: index) viewModel.attachmentServices.value = attachmentServices + + // cancel task + removedItem.disposeBag.removeAll() } } @@ -1365,7 +1379,7 @@ extension ComposeViewController { case .mediaBrowse: present(documentPickerController, animated: true, completion: nil) case .mediaPhotoLibrary: - present(imagePicker, animated: true, completion: nil) + present(photoLibraryPicker, animated: true, completion: nil) case .mediaCamera: guard UIImagePickerController.isSourceTypeAvailable(.camera) else { return diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 097dd9fd..0ac12458 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -291,6 +291,8 @@ final class ComposeViewModel { ) .receive(on: DispatchQueue.main) .sink { [weak self] attachmentServices, isPollComposing, pollAttributes in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: trigger attachments upload…", ((#file as NSString).lastPathComponent), #line, #function) + guard let self = self else { return } guard let diffableDataSource = self.diffableDataSource else { return } var snapshot = diffableDataSource.snapshot() @@ -405,6 +407,59 @@ extension ComposeViewModel { } } +extension ComposeViewModel { + + enum AttachmentPrecondition: Error, LocalizedError { + case videoAttachWithPhoto + case moreThanOneVideo + + var errorDescription: String? { + return L10n.Common.Alerts.PublishPostFailure.title + } + + var failureReason: String? { + switch self { + case .videoAttachWithPhoto: + return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.videoAttachWithPhoto + case .moreThanOneVideo: + return L10n.Common.Alerts.PublishPostFailure.AttchmentsMessage.moreThanOneVideo + } + } + } + + // check exclusive limit: + // - up to 1 video + // - up to 4 photos + func checkAttachmentPrecondition() throws { + let attachmentServices = self.attachmentServices.value + guard !attachmentServices.isEmpty else { return } + var photoAttachmentServices: [MastodonAttachmentService] = [] + var videoAttachmentServices: [MastodonAttachmentService] = [] + attachmentServices.forEach { service in + guard let file = service.file.value else { + assertionFailure() + return + } + switch file { + case .jpeg, .png, .gif: + photoAttachmentServices.append(service) + case .other: + videoAttachmentServices.append(service) + } + } + + if !videoAttachmentServices.isEmpty { + guard videoAttachmentServices.count == 1 else { + throw AttachmentPrecondition.moreThanOneVideo + } + guard photoAttachmentServices.isEmpty else { + throw AttachmentPrecondition.videoAttachWithPhoto + } + } + } + +} + // MARK: - MastodonAttachmentServiceDelegate extension ComposeViewModel: MastodonAttachmentServiceDelegate { func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { diff --git a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift index 353fe749..ca45e33d 100644 --- a/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift +++ b/Mastodon/Scene/Compose/View/AttachmentContainerView+EmptyStateView.swift @@ -29,6 +29,8 @@ extension AttachmentContainerView { label.textAlignment = .center label.text = L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) label.numberOfLines = 2 + label.adjustsFontSizeToFitWidth = true + label.minimumScaleFactor = 0.3 return label }() diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift index 152bb62f..69eff4a8 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift @@ -11,6 +11,8 @@ import CoreData import CoreDataStack #if DEBUG +import FLEX + extension HomeTimelineViewController { var debugMenu: UIMenu { let menu = UIMenu( @@ -19,6 +21,10 @@ extension HomeTimelineViewController { identifier: nil, options: .displayInline, children: [ + UIAction(title: "Show FLEX", image: nil, attributes: [], handler: { [weak self] action in + guard let self = self else { return } + self.showFLEXAction(action) + }), moveMenu, dropMenu, UIAction(title: "Show Welcome", image: UIImage(systemName: "figure.walk"), attributes: []) { [weak self] action in @@ -115,6 +121,10 @@ extension HomeTimelineViewController { extension HomeTimelineViewController { + @objc private func showFLEXAction(_ sender: UIAction) { + FLEXManager.shared.showExplorer() + } + @objc private func moveToTopGapAction(_ sender: UIAction) { guard let diffableDataSource = viewModel.diffableDataSource else { return } let snapshotTransitioning = diffableDataSource.snapshot() diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index c3495b7f..fbc8be62 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -448,9 +448,9 @@ extension ProfileHeaderViewController: PHPickerViewControllerDelegate { case .finished: break } - } receiveValue: { [weak self] imageData in + } receiveValue: { [weak self] file in guard let self = self else { return } - guard let imageData = imageData else { return } + guard let imageData = file?.data else { return } guard let image = UIImage(data: imageData) else { return } self.cropImage(image: image, pickerViewController: picker) } diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index 9fd4b129..23233ec3 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -35,7 +35,7 @@ extension MastodonAttachmentService.UploadState { return true } - if service?.imageData.value != nil { + if service?.file.value != nil { return stateClass == Uploading.self } else { return stateClass == Fail.self @@ -53,15 +53,8 @@ extension MastodonAttachmentService.UploadState { guard let service = service, let stateMachine = stateMachine else { return } guard let authenticationBox = service.authenticationBox else { return } - guard let imageData = service.imageData.value else { return } + guard let file = service.file.value else { return } - let file: Mastodon.Query.MediaAttachment = { - if imageData.kf.imageFormat == .PNG { - return .png(imageData) - } else { - return .jpeg(imageData) - } - }() let description = service.description.value let query = Mastodon.API.Media.UploadMeidaQuery( file: file, @@ -81,6 +74,7 @@ extension MastodonAttachmentService.UploadState { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) service.error.send(error) + stateMachine.enter(Fail.self) case .finished: os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment success", ((#file as NSString).lastPathComponent), #line, #function) break diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index fd95d263..4b843ef4 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -5,11 +5,13 @@ // Created by MainasuK Cirno on 2021-3-17. // +import os.log import UIKit import Combine import PhotosUI import Kingfisher import GameplayKit +import MobileCoreServices import MastodonSDK protocol MastodonAttachmentServiceDelegate: AnyObject { @@ -26,12 +28,12 @@ final class MastodonAttachmentService { // input let context: AppContext var authenticationBox: AuthenticationService.MastodonAuthenticationBox? + let file = CurrentValueSubject(nil) + let description = CurrentValueSubject(nil) // output - // TODO: handle video/GIF/Audio data - let imageData = CurrentValueSubject(nil) + let thumbnailImage = CurrentValueSubject(nil) let attachment = CurrentValueSubject(nil) - let description = CurrentValueSubject(nil) let error = CurrentValueSubject(nil) private(set) lazy var uploadStateMachine: GKStateMachine = { @@ -58,7 +60,16 @@ final class MastodonAttachmentService { setupServiceObserver() - PHPickerResultLoader.loadImageData(from: pickerResult) + Just(pickerResult) + .flatMap { result -> AnyPublisher in + if result.itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) { + return PHPickerResultLoader.loadImageData(from: result).eraseToAnyPublisher() + } + if result.itemProvider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) { + return PHPickerResultLoader.loadVideoData(from: result).eraseToAnyPublisher() + } + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } .sink { [weak self] completion in guard let self = self else { return } switch completion { @@ -68,12 +79,42 @@ final class MastodonAttachmentService { case .finished: break } - } receiveValue: { [weak self] imageData in + } receiveValue: { [weak self] file in guard let self = self else { return } - self.imageData.value = imageData + self.file.value = file self.uploadStateMachine.enter(UploadState.Initial.self) } .store(in: &disposeBag) + + file + .map { file -> UIImage? in + guard let file = file else { + return nil + } + + switch file { + case .jpeg(let data), .png(let data): + return data.flatMap { UIImage(data: $0) } + case .gif: + // TODO: + return nil + case .other(let url, _, _): + guard let url = url, FileManager.default.fileExists(atPath: url.path) else { return nil } + let asset = AVURLAsset(url: url) + let assetImageGenerator = AVAssetImageGenerator(asset: asset) + assetImageGenerator.appliesPreferredTrackTransform = true // fix orientation + do { + let cgImage = try assetImageGenerator.copyCGImage(at: CMTimeMake(value: 0, timescale: 1), actualTime: nil) + let image = UIImage(cgImage: cgImage) + return image + } catch { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: thumbnail generate fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + return nil + } + } + } + .assign(to: \.value, on: thumbnailImage) + .store(in: &disposeBag) } init( @@ -87,7 +128,7 @@ final class MastodonAttachmentService { setupServiceObserver() - imageData.value = image.jpegData(compressionQuality: 0.75) + file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) uploadStateMachine.enter(UploadState.Initial.self) } @@ -102,7 +143,7 @@ final class MastodonAttachmentService { setupServiceObserver() - self.imageData.value = imageData + self.file.value = .jpeg(imageData) uploadStateMachine.enter(UploadState.Initial.self) } @@ -115,6 +156,18 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension MastodonAttachmentService { + enum AttachmentError: Error { + case invalidAttachmentType + case attachmentTooLarge + } + } extension MastodonAttachmentService { diff --git a/Mastodon/Vender/PHPickerResultLoader.swift b/Mastodon/Vender/PHPickerResultLoader.swift index 7e083001..88a9a755 100644 --- a/Mastodon/Vender/PHPickerResultLoader.swift +++ b/Mastodon/Vender/PHPickerResultLoader.swift @@ -10,12 +10,13 @@ import Foundation import Combine import MobileCoreServices import PhotosUI +import MastodonSDK // load image with low memory usage // Refs: https://christianselig.com/2020/09/phpickerviewcontroller-efficiently/ enum PHPickerResultLoader { - static func loadImageData(from result: PHPickerResult) -> Future { + static func loadImageData(from result: PHPickerResult) -> Future { Future { promise in result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.image.identifier) { url, error in if let error = error { @@ -64,7 +65,37 @@ enum PHPickerResultLoader { let dataSize = ByteCountFormatter.string(fromByteCount: Int64(data.length), countStyle: .memory) os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: load image %s", ((#file as NSString).lastPathComponent), #line, #function, dataSize) - promise(.success(data as Data)) + let file = Mastodon.Query.MediaAttachment.jpeg(data as Data) + promise(.success(file)) + } + } + } + + static func loadVideoData(from result: PHPickerResult) -> Future { + Future { promise in + result.itemProvider.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in + if let error = error { + promise(.failure(error)) + return + } + + guard let url = url else { + promise(.success(nil)) + return + } + + let fileName = UUID().uuidString + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.copyItem(at: url, to: fileURL) + let file = Mastodon.Query.MediaAttachment.other(fileURL, fileExtension: fileURL.pathExtension, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") + promise(.success(file)) + } catch { + promise(.failure(error)) + } + } } } diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift index 5ae344b3..0918cbd0 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift @@ -42,11 +42,17 @@ extension Mastodon.API.Media { authorization: authorization ) request.timeoutInterval = 180 // should > 200 Kb/s for 40 MiB media attachment + let serialStream = query.serialStream + request.httpBodyStream = serialStream.boundStreams.input return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) return Mastodon.Response.Content(value: value, response: response) } + .handleEvents(receiveCancel: { + // retain and handle cancel task + serialStream.boundStreams.output.close() + }) .eraseToAnyPublisher() } @@ -73,15 +79,30 @@ extension Mastodon.API.Media { } var body: Data? { - var data = Data() - - file.flatMap { data.append(Data.multipart(key: "file", value: $0)) } - thumbnail.flatMap { data.append(Data.multipart(key: "thumbnail", value: $0)) } - description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } - focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } + // using stream data + return nil + } + + var serialStream: SerialStream { + var streams: [InputStream] = [] - data.append(Data.multipartEnd()) - return data + file.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "file", value: value))) + value.multipartStreamValue.flatMap { streams.append($0) } + } + thumbnail.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "thumbnail", value: value))) + value.multipartStreamValue.flatMap { streams.append($0) } + } + description.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "description", value: value))) + } + focus.flatMap { value in + streams.append(InputStream(data: Data.multipart(key: "focus", value: value))) + } + streams.append(InputStream(data: Data.multipartEnd())) + + return SerialStream(streams: streams) } } @@ -129,8 +150,45 @@ extension Mastodon.API.Media { } .eraseToAnyPublisher() } - - public typealias UpdateMediaQuery = UploadMeidaQuery + + public struct UpdateMediaQuery: PutQuery { + + public let file: Mastodon.Query.MediaAttachment? + public let thumbnail: Mastodon.Query.MediaAttachment? + public let description: String? + public let focus: String? + + public init( + file: Mastodon.Query.MediaAttachment?, + thumbnail: Mastodon.Query.MediaAttachment?, + description: String?, + focus: String? + ) { + self.file = file + self.thumbnail = thumbnail + self.description = description + self.focus = focus + } + + var contentType: String? { + return Self.multipartContentType() + } + + var queryItems: [URLQueryItem]? { + return nil + } + + var body: Data? { + var data = Data() + + // not modify uploaded binary data + description.flatMap { data.append(Data.multipart(key: "description", value: $0)) } + focus.flatMap { data.append(Data.multipart(key: "focus", value: $0)) } + + data.append(Data.multipartEnd()) + return data + } + } } diff --git a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift index 48e442b9..35c66176 100644 --- a/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift +++ b/MastodonSDK/Sources/MastodonSDK/Extension/Data.swift @@ -26,7 +26,12 @@ extension Data { data.append("Content-Type: \(contentType)\r\n".data(using: .utf8)!) } data.append("\r\n".data(using: .utf8)!) - data.append(value.multipartValue) + if value.multipartStreamValue == nil { + data.append(value.multipartValue) + } else { + // needs append stream multipart value outside + // seealso: SerialStream + } return data } diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift index f3bd8883..ca9388ca 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MediaAttachment.swift @@ -16,21 +16,22 @@ extension Mastodon.Query { /// PNG (Portable Network Graphics) image case png(Data?) /// Other media file - case other(Data?, fileExtension: String, mimeType: String) + /// e.g video + case other(URL?, fileExtension: String, mimeType: String) } } extension Mastodon.Query.MediaAttachment { - var data: Data? { + public var data: Data? { switch self { case .jpeg(let data): return data case .gif(let data): return data case .png(let data): return data - case .other(let data, _, _): return data + case .other: return nil } } - var fileName: String { + public var fileName: String { let name = UUID().uuidString switch self { case .jpeg: return "\(name).jpg" @@ -40,7 +41,7 @@ extension Mastodon.Query.MediaAttachment { } } - var mimeType: String { + public var mimeType: String { switch self { case .jpeg: return "image/jpg" case .gif: return "image/gif" @@ -56,6 +57,14 @@ extension Mastodon.Query.MediaAttachment { extension Mastodon.Query.MediaAttachment: MultipartFormValue { var multipartValue: Data { return data ?? Data() } + var multipartStreamValue: InputStream? { + switch self { + case .other(let url, _, _): + return url.flatMap { InputStream(url: $0) } + default: + return nil + } + } var multipartContentType: String? { return mimeType } var multipartFilename: String? { return fileName } } diff --git a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift index f86a71c8..73a0e9ed 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/MultipartFormValue.swift @@ -13,10 +13,15 @@ enum Multipart { protocol MultipartFormValue { var multipartValue: Data { get } + var multipartStreamValue: InputStream? { get } var multipartContentType: String? { get } var multipartFilename: String? { get } } +extension MultipartFormValue { + var multipartStreamValue: InputStream? { nil } +} + extension Bool: MultipartFormValue { var multipartValue: Data { switch self { diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift new file mode 100644 index 00000000..ad86033e --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift @@ -0,0 +1,147 @@ +// +// SerialStream.swift +// +// +// Created by MainasuK Cirno on 2021-5-28. +// + +import os.log +import Foundation +import Combine + +// ref: +// - https://developer.apple.com/documentation/foundation/url_loading_system/uploading_streams_of_data#3037342 +// - https://forums.swift.org/t/extension-write-to-outputstream/42817/4 +// - https://gist.github.com/khanlou/b5e07f963bedcb6e0fcc5387b46991c3 + +final class SerialStream: NSObject { + var writingTimerSubscriber: AnyCancellable? + + // serial stream source + private var streams: [InputStream] + private var currentStreamIndex = 0 + + private static let bufferSize = 5 * 1024 * 1024 // 5MiB + + private var buffer: UnsafeMutablePointer + private var canWrite = false + + private let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.SerialStream.\(UUID().uuidString)") + + // bound pair stream + private(set) lazy var boundStreams: Streams = { + var inputStream: InputStream? + var outputStream: OutputStream? + Stream.getBoundStreams(withBufferSize: SerialStream.bufferSize, inputStream: &inputStream, outputStream: &outputStream) + guard let input = inputStream, let output = outputStream else { + fatalError() + } + + output.delegate = self + output.schedule(in: .current, forMode: .default) + output.open() + + return Streams(input: input, output: output) + }() + + init(streams: [InputStream]) { + self.streams = streams + self.buffer = UnsafeMutablePointer.allocate(capacity: SerialStream.bufferSize) + self.buffer.initialize(repeating: 0, count: SerialStream.bufferSize) + super.init() + + // Stream worker + writingTimerSubscriber = Timer.publish(every: 0.5, on: .current, in: .default) + .autoconnect() + .receive(on: workingQueue) + .sink { [weak self] timer in + guard let self = self else { return } + guard self.canWrite else { return } + os_log(.debug, "%{public}s[%{public}ld], %{public}s: writing…", ((#file as NSString).lastPathComponent), #line, #function) + + guard self.currentStreamIndex < self.streams.count else { + self.boundStreams.output.close() + self.writingTimerSubscriber = nil // cancel timer after task completed + return + } + + var readBytesCount = 0 + defer { + var baseAddress = 0 + var remainsBytes = readBytesCount + while remainsBytes > 0 { + let result = self.boundStreams.output.write(&self.buffer[baseAddress], maxLength: remainsBytes) + baseAddress += result + remainsBytes -= result + os_log(.debug, "%{public}s[%{public}ld], %{public}s: write %ld/%ld bytes. write result: %ld", ((#file as NSString).lastPathComponent), #line, #function, baseAddress, readBytesCount, result) + } + } + + while readBytesCount < SerialStream.bufferSize { + // close when no more source streams + guard self.currentStreamIndex < self.streams.count else { + break + } + + let inputStream = self.streams[self.currentStreamIndex] + // open input if needs + if inputStream.streamStatus != .open { + inputStream.open() + } + // read next source stream when current drain + guard inputStream.hasBytesAvailable else { + self.currentStreamIndex += 1 + continue + } + + let reaminsCount = SerialStream.bufferSize - readBytesCount + let readCount = inputStream.read(&self.buffer[readBytesCount], maxLength: reaminsCount) + os_log(.debug, "%{public}s[%{public}ld], %{public}s: read source %ld bytes", ((#file as NSString).lastPathComponent), #line, #function, readCount) + + switch readCount { + case 0: + self.currentStreamIndex += 1 + continue + case -1: + self.boundStreams.output.close() + return + default: + self.canWrite = false + readBytesCount += readCount + } + } + } + } + + deinit { + os_log(.debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + +} + +extension SerialStream { + struct Streams { + let input: InputStream + let output: OutputStream + } +} + +// MARK: - StreamDelegate +extension SerialStream: StreamDelegate { + func stream(_ aStream: Stream, handle eventCode: Stream.Event) { + os_log(.debug, "%{public}s[%{public}ld], %{public}s: eventCode %s", ((#file as NSString).lastPathComponent), #line, #function, String(eventCode.rawValue)) + + guard aStream == boundStreams.output else { + return + } + + if eventCode.contains(.hasSpaceAvailable) { + canWrite = true + } + + if eventCode.contains(.errorOccurred) { + // Close the streams and alert the user that the upload failed. + boundStreams.output.close() + } + } +} diff --git a/Podfile b/Podfile index ea7075ea..d9f295c2 100644 --- a/Podfile +++ b/Podfile @@ -13,6 +13,9 @@ target 'Mastodon' do pod 'SwiftGen', '~> 6.4.0' pod 'DateToolsSwift', '~> 5.0.0' pod 'Kanna', '~> 5.2.2' + + # DEBUG + pod 'FLEX', '~> 4.4.0', :configurations => ['Debug'] target 'MastodonTests' do inherit! :search_paths @@ -23,14 +26,16 @@ target 'Mastodon' do # Pods for testing end - target 'NotificationService' do +end - end - - target 'AppShared' do - - end +target 'NotificationService' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! +end +target 'AppShared' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! end plugin 'cocoapods-keys', { diff --git a/Podfile.lock b/Podfile.lock index e341a242..ea8ecc82 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,5 +1,6 @@ PODS: - DateToolsSwift (5.0.0) + - FLEX (4.4.1) - Kanna (5.2.4) - Keys (1.0.1) - SwiftGen (6.4.0) @@ -7,6 +8,7 @@ PODS: DEPENDENCIES: - DateToolsSwift (~> 5.0.0) + - FLEX (~> 4.4.0) - Kanna (~> 5.2.2) - Keys (from `Pods/CocoaPodsKeys`) - SwiftGen (~> 6.4.0) @@ -15,6 +17,7 @@ DEPENDENCIES: SPEC REPOS: trunk: - DateToolsSwift + - FLEX - Kanna - SwiftGen - "UITextField+Shake" @@ -25,11 +28,12 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: DateToolsSwift: 4207ada6ad615d8dc076323d27037c94916dbfa6 + FLEX: 7ca2c8cd3a435ff501ff6d2f2141e9bdc934eaab Kanna: b9d00d7c11428308c7f95e1f1f84b8205f567a8f Keys: a576f4c9c1c641ca913a959a9c62ed3f215a8de9 SwiftGen: 67860cc7c3cfc2ed25b9b74cfd55495fc89f9108 "UITextField+Shake": 298ac5a0f239d731bdab999b19b628c956ca0ac3 -PODFILE CHECKSUM: a8dbae22e6e0bfb84f7db59aef1aa1716793d287 +PODFILE CHECKSUM: 0daad1778e56099e7a7e7ebe3d292d20051840fa COCOAPODS: 1.10.1 From 26b48957cd19c78894e0fe6d6f713c11e190c5a3 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:57:27 +0800 Subject: [PATCH 13/33] fix: profile field alignment issue --- .../Header/View/ProfileFieldAddEntryCollectionViewCell.swift | 5 ++--- .../Profile/Header/View/ProfileFieldCollectionViewCell.swift | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift index 1801d422..d8ecb198 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldAddEntryCollectionViewCell.swift @@ -45,7 +45,6 @@ final class ProfileFieldAddEntryCollectionViewCell: UICollectionViewCell { override func prepareForReuse() { super.prepareForReuse() - //resetStackView() disposeBag.removeAll() } @@ -71,8 +70,8 @@ extension ProfileFieldAddEntryCollectionViewCell { contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift index 58af0ce7..6efa92e1 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldCollectionViewCell.swift @@ -77,8 +77,8 @@ extension ProfileFieldCollectionViewCell { contentView.addSubview(containerStackView) NSLayoutConstraint.activate([ containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + containerStackView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor), + containerStackView.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor), containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), containerStackView.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh), ]) From 3ed43a3575eb1e20ad13be9f42f2af509956564f Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 16:57:48 +0800 Subject: [PATCH 14/33] fix: profile field edit update logic issue --- .../Scene/Profile/Header/View/ProfileFieldView.swift | 1 - .../API/Mastodon+API+Account+Credentials.swift | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift index 14e5e4b3..e008e1bc 100644 --- a/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift +++ b/Mastodon/Scene/Profile/Header/View/ProfileFieldView.swift @@ -30,7 +30,6 @@ final class ProfileFieldView: UIView { textField.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold), maximumPointSize: 20) textField.textColor = Asset.Colors.Label.primary.color textField.placeholder = L10n.Scene.Profile.Fields.Placeholder.label - textField.isEnabled = false return textField }() diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift index cbc4fb8e..e0980165 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Account+Credentials.swift @@ -217,9 +217,15 @@ extension Mastodon.API.Account { source.sensitive.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } source.language.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) } } - for (i, fieldsAttribute) in (fieldsAttributes ?? []).enumerated() { - data.append(Data.multipart(key: "fields_attributes[\(i)][name]", value: fieldsAttribute.name)) - data.append(Data.multipart(key: "fields_attributes[\(i)][value]", value: fieldsAttribute.value)) + if let fieldsAttributes = fieldsAttributes { + if fieldsAttributes.isEmpty { + data.append(Data.multipart(key: "fields_attributes[]", value: "")) + } else { + for (i, fieldsAttribute) in fieldsAttributes.enumerated() { + data.append(Data.multipart(key: "fields_attributes[\(i)][name]", value: fieldsAttribute.name)) + data.append(Data.multipart(key: "fields_attributes[\(i)][value]", value: fieldsAttribute.value)) + } + } } data.append(Data.multipartEnd()) From b9c262c84e3e23e59785971b235c5b3dbf0d74e8 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 17:07:32 +0800 Subject: [PATCH 15/33] chore: suppression project warning --- Mastodon/Coordinator/SceneCoordinator.swift | 4 ---- Mastodon/Diffiable/Section/StatusSection.swift | 3 ++- .../HashtagTimelineViewModel+LoadMiddleState.swift | 2 +- .../Profile/Header/ProfileHeaderViewController.swift | 8 -------- .../PublicTimeline/PublicTimelineViewModel+Diffable.swift | 5 ----- .../Share/View/Container/MosaicImageViewContainer.swift | 6 +++--- .../Scene/Thread/ThreadViewModel+LoadThreadState.swift | 2 +- ...oMediaPreviewViewControllerAnimatedTransitioning.swift | 2 +- Mastodon/Service/EmojiService/Trie.swift | 2 +- 9 files changed, 9 insertions(+), 25 deletions(-) diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index 3768f7d3..f9da785a 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -261,10 +261,6 @@ private extension SceneCoordinator { let _viewController = FavoriteViewController() _viewController.viewModel = viewModel viewController = _viewController - case .settings(let viewModel): - let _viewController = SettingsViewController() - _viewController.viewModel = viewModel - viewController = _viewController case .suggestionAccount(let viewModel): let _viewController = SuggestionAccountViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index d139e061..ed24db92 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -274,7 +274,8 @@ extension StatusSection { } else { meta.blurhashImagePublisher() .receive(on: DispatchQueue.main) - .sink { [weak cell] image in + .sink { [weak blurhashImageCache] image in + guard let blurhashImageCache = blurhashImageCache else { return } blurhashOverlayImageView.image = image image?.pngData().flatMap { blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey) diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift index dcd3f81a..f458b86a 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+LoadMiddleState.swift @@ -58,7 +58,7 @@ extension HashtagTimelineViewModel.LoadMiddleState { stateMachine.enter(Fail.self) return } - let statusIDs = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).compactMap { status in + _ = (viewModel.fetchedResultsController.fetchedResultsController.fetchedObjects ?? []).compactMap { status in status.id } diff --git a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift index fbc8be62..19990379 100644 --- a/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift +++ b/Mastodon/Scene/Profile/Header/ProfileHeaderViewController.swift @@ -212,14 +212,6 @@ extension ProfileHeaderViewController { } .store(in: &disposeBag) - viewModel.isEditing - .receive(on: RunLoop.main) - .sink { [weak self] isEditing in - guard let self = self else { return } - // self.profileHeaderView.fieldCollectionView. - } - .store(in: &disposeBag) - profileHeaderView.editAvatarButton.menu = createAvatarContextMenu() profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true } diff --git a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift index 27336dc5..3270302b 100644 --- a/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/PublicTimeline/PublicTimelineViewModel+Diffable.swift @@ -59,11 +59,6 @@ extension PublicTimelineViewModel: NSFetchedResultsControllerDelegate { var items = [Item]() for (_, status) in indexStatusTuples { - let targetStatus = status.reblog ?? status - let isStatusTextSensitive: Bool = { - guard let spoilerText = targetStatus.spoilerText, !spoilerText.isEmpty else { return false } - return true - }() let attribute = oldSnapshotAttributeDict[status.objectID] ?? Item.StatusAttribute() items.append(Item.status(objectID: status.objectID, attribute: attribute)) if statusIDsWhichHasGap.contains(status.id) { diff --git a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift index 0dccd593..081e99af 100644 --- a/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift +++ b/Mastodon/Scene/Share/View/Container/MosaicImageViewContainer.swift @@ -395,7 +395,7 @@ struct MosaicImageView_Previews: PreviewProvider { let images = self.images.prefix(2) let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac + let (imageView, _) = mosiac imageView.image = images[i] } return view @@ -407,7 +407,7 @@ struct MosaicImageView_Previews: PreviewProvider { let images = self.images.prefix(3) let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac + let (imageView, _) = mosiac imageView.image = images[i] } return view @@ -419,7 +419,7 @@ struct MosaicImageView_Previews: PreviewProvider { let images = self.images.prefix(4) let mosaics = view.setupImageViews(count: images.count, maxHeight: 162) for (i, mosiac) in mosaics.enumerated() { - let (imageView, blurhashOverlayImageView) = mosiac + let (imageView, _) = mosiac imageView.image = images[i] } return view diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 5327edc5..82724264 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -111,7 +111,7 @@ extension ThreadViewModel.LoadThreadState { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } + guard let _ = viewModel, let stateMachine = stateMachine else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 3) { stateMachine.enter(Loading.self) } diff --git a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift index 3c7b5b8c..50a518dc 100644 --- a/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift +++ b/Mastodon/Scene/Transition/MediaPreview/MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift @@ -245,7 +245,7 @@ extension MediaHostToMediaPreviewViewControllerAnimatedTransitioning { private func popInteractiveTransition(using transitionContext: UIViewControllerContextTransitioning) { guard let fromVC = transitionContext.viewController(forKey: .from) as? MediaPreviewViewController, - let fromView = transitionContext.view(forKey: .from), + let _ = transitionContext.view(forKey: .from), let mediaPreviewImageViewController = fromVC.pagingViewConttroller.currentViewController as? MediaPreviewImageViewController, let index = fromVC.pagingViewConttroller.currentIndex else { fatalError() diff --git a/Mastodon/Service/EmojiService/Trie.swift b/Mastodon/Service/EmojiService/Trie.swift index 0eb490ae..3bf9eeaf 100644 --- a/Mastodon/Service/EmojiService/Trie.swift +++ b/Mastodon/Service/EmojiService/Trie.swift @@ -104,7 +104,7 @@ extension Trie { var values: NSSet { let valueSet = NSMutableSet(set: self.valueSet) - for (key, value) in children { + for (_, value) in children { valueSet.addObjects(from: Array(value.values)) } From 6211663508ae5a8b62875757ee13cea166067610 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 31 May 2021 18:03:31 +0800 Subject: [PATCH 16/33] feat: add post video supports for document picker --- .../Scene/Compose/ComposeViewController.swift | 23 +-- .../MastodonAttachmentService.swift | 161 +++++++++++++----- Mastodon/Vender/PHPickerResultLoader.swift | 1 - 3 files changed, 129 insertions(+), 56 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 2e3e5fe7..69fef0a5 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -95,7 +95,7 @@ final class ComposeViewController: UIViewController, NeedsDependency { }() private(set) lazy var documentPickerController: UIDocumentPickerViewController = { - let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image]) + let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) documentPickerController.delegate = self return documentPickerController }() @@ -1102,20 +1102,13 @@ extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationC extension ComposeViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } - - do { - guard url.startAccessingSecurityScopedResource() else { return } - defer { url.stopAccessingSecurityScopedResource() } - let imageData = try Data(contentsOf: url) - let attachmentService = MastodonAttachmentService( - context: context, - imageData: imageData, - initalAuthenticationBox: viewModel.activeAuthenticationBox.value - ) - viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] - } catch { - os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - } + + let attachmentService = MastodonAttachmentService( + context: context, + documentURL: url, + initalAuthenticationBox: viewModel.activeAuthenticationBox.value + ) + viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] } } diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index 4b843ef4..4c541a8d 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -24,7 +24,7 @@ final class MastodonAttachmentService { weak var delegate: MastodonAttachmentServiceDelegate? let identifier = UUID() - + // input let context: AppContext var authenticationBox: AuthenticationService.MastodonAuthenticationBox? @@ -85,6 +85,65 @@ final class MastodonAttachmentService { self.uploadStateMachine.enter(UploadState.Initial.self) } .store(in: &disposeBag) + } + + init( + context: AppContext, + image: UIImage, + initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + ) { + self.context = context + self.authenticationBox = initalAuthenticationBox + // end init + + setupServiceObserver() + + file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) + uploadStateMachine.enter(UploadState.Initial.self) + } + + init( + context: AppContext, + documentURL: URL, + initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + ) { + self.context = context + self.authenticationBox = initalAuthenticationBox + // end init + + setupServiceObserver() + + Just(documentURL) + .flatMap { documentURL -> AnyPublisher in + return MastodonAttachmentService.loadAttachment(url: documentURL) + } + .sink { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + self.error.value = error + self.uploadStateMachine.enter(UploadState.Fail.self) + case .finished: + break + } + } receiveValue: { [weak self] file in + guard let self = self else { return } + self.file.value = file + self.uploadStateMachine.enter(UploadState.Initial.self) + } + .store(in: &disposeBag) + + uploadStateMachine.enter(UploadState.Initial.self) + } + + private func setupServiceObserver() { + uploadStateMachineSubject + .sink { [weak self] state in + guard let self = self else { return } + self.delegate?.mastodonAttachmentService(self, uploadStateDidChange: state) + } + .store(in: &disposeBag) + file .map { file -> UIImage? in @@ -117,45 +176,6 @@ final class MastodonAttachmentService { .store(in: &disposeBag) } - init( - context: AppContext, - image: UIImage, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? - ) { - self.context = context - self.authenticationBox = initalAuthenticationBox - // end init - - setupServiceObserver() - - file.value = .jpeg(image.jpegData(compressionQuality: 0.75)) - uploadStateMachine.enter(UploadState.Initial.self) - } - - init( - context: AppContext, - imageData: Data, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? - ) { - self.context = context - self.authenticationBox = initalAuthenticationBox - // end init - - setupServiceObserver() - - self.file.value = .jpeg(imageData) - uploadStateMachine.enter(UploadState.Initial.self) - } - - private func setupServiceObserver() { - uploadStateMachineSubject - .sink { [weak self] state in - guard let self = self else { return } - self.delegate?.mastodonAttachmentService(self, uploadStateDidChange: state) - } - .store(in: &disposeBag) - } - deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) } @@ -189,3 +209,64 @@ extension MastodonAttachmentService: Equatable, Hashable { } } + +extension MastodonAttachmentService { + + private static func createWorkingQueue() -> DispatchQueue { + return DispatchQueue(label: "org.joinmastodon.Mastodon.MastodonAttachmentService.\(UUID().uuidString)") + } + + static func loadAttachment(url: URL) -> AnyPublisher { + guard let uti = UTType(filenameExtension: url.pathExtension) else { + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } + + if uti.conforms(to: .image) { + return loadImageAttachment(url: url) + } else if uti.conforms(to: .movie) { + return loadVideoAttachment(url: url) + } else { + return Fail(error: AttachmentError.invalidAttachmentType).eraseToAnyPublisher() + } + } + + static func loadImageAttachment(url: URL) -> AnyPublisher { + Future { promise in + createWorkingQueue().async { + do { + guard url.startAccessingSecurityScopedResource() else { return } + defer { url.stopAccessingSecurityScopedResource() } + let imageData = try Data(contentsOf: url) + promise(.success(.jpeg(imageData))) + } catch { + os_log("%{public}s[%{public}ld], %{public}s: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() + } + + static func loadVideoAttachment(url: URL) -> AnyPublisher { + Future { promise in + createWorkingQueue().async { + guard url.startAccessingSecurityScopedResource() else { return } + defer { url.stopAccessingSecurityScopedResource() } + + let fileName = UUID().uuidString + let tempDirectoryURL = FileManager.default.temporaryDirectory + let fileURL = tempDirectoryURL.appendingPathComponent(fileName).appendingPathExtension(url.pathExtension) + do { + try FileManager.default.createDirectory(at: tempDirectoryURL, withIntermediateDirectories: true, attributes: nil) + try FileManager.default.copyItem(at: url, to: fileURL) + let file = Mastodon.Query.MediaAttachment.other(fileURL, fileExtension: fileURL.pathExtension, mimeType: UTType.movie.preferredMIMEType ?? "video/mp4") + promise(.success(file)) + } catch { + promise(.failure(error)) + } + } + } + .eraseToAnyPublisher() + } + +} diff --git a/Mastodon/Vender/PHPickerResultLoader.swift b/Mastodon/Vender/PHPickerResultLoader.swift index 88a9a755..8a3bca62 100644 --- a/Mastodon/Vender/PHPickerResultLoader.swift +++ b/Mastodon/Vender/PHPickerResultLoader.swift @@ -95,7 +95,6 @@ enum PHPickerResultLoader { } catch { promise(.failure(error)) } - } } } From 70b75c8ebaf9e3ce5d936a86fae1ed086c907b61 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 14:07:44 +0800 Subject: [PATCH 17/33] feat: display clean cache result --- Localization/app.json | 4 + Mastodon/Generated/Strings.swift | 8 ++ .../Resources/ar.lproj/Localizable.strings | 2 + .../Resources/en.lproj/Localizable.strings | 2 + .../Settings/SettingsViewController.swift | 63 ++++++----- Mastodon/State/AppContext.swift | 106 ++++++++++++++++++ 6 files changed, 157 insertions(+), 28 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index 69d51360..1833e00e 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -43,6 +43,10 @@ "delete_post": { "title": "Are you sure you want to delete this post?", "delete": "Delete" + }, + "clean_cache": { + "title": "Clean Cache", + "message": "Successfully clean %s cache." } }, "controls": { diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 5790f713..dfae5e2d 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -21,6 +21,14 @@ internal enum L10n { return L10n.tr("Localizable", "Common.Alerts.BlockDomain.Title", String(describing: p1)) } } + internal enum CleanCache { + /// Successfully clean %@ cache. + internal static func message(_ p1: Any) -> String { + return L10n.tr("Localizable", "Common.Alerts.CleanCache.Message", String(describing: p1)) + } + /// Clean Cache + internal static let title = L10n.tr("Localizable", "Common.Alerts.CleanCache.Title") + } internal enum Common { /// Please try again. internal static let pleaseTryAgain = L10n.tr("Localizable", "Common.Alerts.Common.PleaseTryAgain") diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 81bf772e..39c4c4fe 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -1,5 +1,7 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; "Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; +"Common.Alerts.CleanCache.Message" = "Successfully clean %@ cache."; +"Common.Alerts.CleanCache.Title" = "Clean Cache"; "Common.Alerts.Common.PleaseTryAgain" = "Please try again."; "Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; "Common.Alerts.DeletePost.Delete" = "Delete"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 81bf772e..39c4c4fe 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -1,5 +1,7 @@ "Common.Alerts.BlockDomain.BlockEntireDomain" = "Block entire domain"; "Common.Alerts.BlockDomain.Title" = "Are you really, really sure you want to block the entire %@? In most cases a few targeted blocks or mutes are sufficient and preferable. You will not see content from that domain in any public timelines or your notifications. Your followers from that domain will be removed."; +"Common.Alerts.CleanCache.Message" = "Successfully clean %@ cache."; +"Common.Alerts.CleanCache.Title" = "Clean Cache"; "Common.Alerts.Common.PleaseTryAgain" = "Please try again."; "Common.Alerts.Common.PleaseTryAgainLater" = "Please try again later."; "Common.Alerts.DeletePost.Delete" = "Delete"; diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index 0ffb09cf..ad85bee8 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -12,8 +12,7 @@ import ActiveLabel import CoreData import CoreDataStack import MastodonSDK -import AlamofireImage -import Kingfisher + class SettingsViewController: UIViewController, NeedsDependency { @@ -319,36 +318,44 @@ extension SettingsViewController: UITableViewDelegate { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { guard let dataSource = viewModel.dataSource else { return } - let item = dataSource.itemIdentifier(for: indexPath) + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } switch item { - case .boringZone: - guard let url = viewModel.privacyURL else { break } - coordinator.present( - scene: .safari(url: url), - from: self, - transition: .safariPresent(animated: true, completion: nil) - ) - case .spicyZone(let link): - // clear media cache - if link.title == L10n.Scene.Settings.Section.Spicyzone.clear { - // clean image cache for AlamofireImage - let diskBytes = ImageDownloader.defaultURLCache().currentDiskUsage - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: diskBytes %d", ((#file as NSString).lastPathComponent), #line, #function, diskBytes) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: clean image cache", ((#file as NSString).lastPathComponent), #line, #function) - ImageDownloader.defaultURLCache().removeAllCachedResponses() - let cleanedDiskBytes = ImageDownloader.defaultURLCache().currentDiskUsage - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: diskBytes %d", ((#file as NSString).lastPathComponent), #line, #function, cleanedDiskBytes) - - // clean Kingfisher Cache - KingfisherManager.shared.cache.clearDiskCache() - } - // logout - if link.title == L10n.Scene.Settings.Section.Spicyzone.signout { + case .apperance: + // do nothing + break + case .notification: + // do nothing + break + case .boringZone(let link), .spicyZone(let link): + switch link { + case .termsOfService, .privacyPolicy: + // same URL + guard let url = viewModel.privacyURL else { break } + coordinator.present( + scene: .safari(url: url), + from: self, + transition: .safariPresent(animated: true, completion: nil) + ) + case .clearMediaCache: + context.purgeCache() + .receive(on: RunLoop.main) + .sink { [weak self] byteCount in + guard let self = self else { return } + let byteCountformatted = AppContext.byteCountFormatter.string(fromByteCount: Int64(byteCount)) + let alertController = UIAlertController( + title: L10n.Common.Alerts.CleanCache.title, + message: L10n.Common.Alerts.CleanCache.message(byteCountformatted), + preferredStyle: .alert + ) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) + } + .store(in: &disposeBag) + case .signOut: alertToSignout() } - default: - break } } } diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 55d5841f..8c0fa364 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -10,6 +10,8 @@ import UIKit import Combine import CoreData import CoreDataStack +import AlamofireImage +import Kingfisher class AppContext: ObservableObject { @@ -99,3 +101,107 @@ class AppContext: ObservableObject { } } + +extension AppContext { + + typealias ByteCount = Int + + static let byteCountFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + return formatter + }() + + private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.AppContext.purgeCacheWorkingQueue") + + func purgeCache() -> AnyPublisher { + Publishers.MergeMany([ + AppContext.purgeAlamofireImageCache(), + AppContext.purgeKingfisherCache(), + AppContext.purgeTemporaryDirectory(), + ]) + .reduce(0, +) + .eraseToAnyPublisher() + } + + private static func purgeAlamofireImageCache() -> AnyPublisher { + Future { promise in + AppContext.purgeCacheWorkingQueue.async { + // clean image cache for AlamofireImage + let diskBytes = ImageDownloader.defaultURLCache().currentDiskUsage + ImageDownloader.defaultURLCache().removeAllCachedResponses() + let currentDiskBytes = ImageDownloader.defaultURLCache().currentDiskUsage + let purgedDiskBytes = max(0, diskBytes - currentDiskBytes) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: purge AlamofireImage cache bytes: %ld -> %ld (%ld)", ((#file as NSString).lastPathComponent), #line, #function, diskBytes, currentDiskBytes, purgedDiskBytes) + promise(.success(purgedDiskBytes)) + } + } + .eraseToAnyPublisher() + } + + private static func purgeKingfisherCache() -> AnyPublisher { + Future { promise in + KingfisherManager.shared.cache.calculateDiskStorageSize { result in + switch result { + case .success(let diskBytes): + KingfisherManager.shared.cache.clearCache() + KingfisherManager.shared.cache.calculateDiskStorageSize { currentResult in + switch currentResult { + case .success(let currentDiskBytes): + let purgedDiskBytes = max(0, Int(diskBytes) - Int(currentDiskBytes)) + promise(.success(purgedDiskBytes)) + case .failure: + promise(.success(0)) + } + } + case .failure: + promise(.success(0)) + } + } + } + .eraseToAnyPublisher() + } + + private static func purgeTemporaryDirectory() -> AnyPublisher { + Future { promise in + AppContext.purgeCacheWorkingQueue.async { + let fileManager = FileManager.default + let temporaryDirectoryURL = fileManager.temporaryDirectory + + let resourceKeys = Set([.fileSizeKey, .isDirectoryKey]) + guard let directoryEnumerator = fileManager.enumerator( + at: temporaryDirectoryURL, + includingPropertiesForKeys: Array(resourceKeys), + options: .skipsHiddenFiles + ) else { + promise(.success(0)) + return + } + + var fileURLs: [URL] = [] + var totalFileSizeInBytes = 0 + for case let fileURL as URL in directoryEnumerator { + guard let resourceValues = try? fileURL.resourceValues(forKeys: resourceKeys), + let isDirectory = resourceValues.isDirectory else { + continue + } + + guard !isDirectory else { + continue + } + fileURLs.append(fileURL) + totalFileSizeInBytes += resourceValues.fileSize ?? 0 + } + + for fileURL in fileURLs { + try? fileManager.removeItem(at: fileURL) + } + + promise(.success(totalFileSizeInBytes)) + } + } + .eraseToAnyPublisher() + } +// +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: purge temporary directory success", ((#file as NSString).lastPathComponent), #line, #function) +// } +} From deae4bea83d9fafacbf7088888cf7ad60de01844 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 14:08:29 +0800 Subject: [PATCH 18/33] chore: suppression CoreDataStack framework warning --- CoreDataStack/Protocol/Managed.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CoreDataStack/Protocol/Managed.swift b/CoreDataStack/Protocol/Managed.swift index 4bdff9c3..9cf9278f 100644 --- a/CoreDataStack/Protocol/Managed.swift +++ b/CoreDataStack/Protocol/Managed.swift @@ -8,7 +8,7 @@ import Foundation import CoreData -public protocol Managed: AnyObject, NSFetchRequestResult { +public protocol Managed: NSFetchRequestResult { static var entityName: String { get } static var defaultSortDescriptors: [NSSortDescriptor] { get } } From 8b941022891dcb5bf4fd0da6bcf7e1d5172a4b01 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 14:31:31 +0800 Subject: [PATCH 19/33] feat: update status timestamp behavior. resolve #82 --- Localization/app.json | 3 + Mastodon.xcodeproj/project.pbxproj | 204 +++++++++--------- .../Section/ComposeStatusSection.swift | 2 +- .../Section/NotificationSection.swift | 6 +- .../Diffiable/Section/StatusSection.swift | 6 +- Mastodon/Extension/Date.swift | 29 +++ Mastodon/Generated/Strings.swift | 4 + .../Resources/ar.lproj/Localizable.strings | 1 + .../Resources/en.lproj/Localizable.strings | 1 + 9 files changed, 149 insertions(+), 107 deletions(-) create mode 100644 Mastodon/Extension/Date.swift diff --git a/Localization/app.json b/Localization/app.json index 1833e00e..3e90f799 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -173,6 +173,9 @@ "edit_info": "Edit info" }, "timeline": { + "timestamp": { + "now": "Now" + }, "loader": { "load_missing_posts": "Load missing posts", "loading_missing_posts": "Loading missing posts...", diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index b1aaa14b..5d765861 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -67,7 +67,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* MastodonStatusContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -80,7 +80,7 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */; }; + 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; @@ -90,7 +90,7 @@ 2D61254D262547C200299647 /* APIService+Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61254C262547C200299647 /* APIService+Notification.swift */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; @@ -116,7 +116,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */; }; + 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; 2D9DB967263A76FB007C1D71 /* BlockDomainService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB966263A76FB007C1D71 /* BlockDomainService.swift */; }; 2D9DB969263A833E007C1D71 /* DomainBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D9DB968263A833E007C1D71 /* DomainBlock.swift */; }; @@ -164,7 +164,7 @@ 5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; }; 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; @@ -180,11 +180,11 @@ 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; - DB00CA972632DDB600A54956 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */; }; + DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; @@ -216,7 +216,7 @@ DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; }; DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; }; DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; }; - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; @@ -257,7 +257,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */; }; + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB51D172262832380062B7A1 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D170262832380062B7A1 /* BlurHashDecode.swift */; }; DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB51D171262832380062B7A1 /* BlurHashEncode.swift */; }; @@ -293,7 +293,7 @@ DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB6804D12637CE4700430867 /* UserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804D02637CE4700430867 /* UserDefaults.swift */; }; DB6804FD2637CFEC00430867 /* AppSecret.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6804FC2637CFEC00430867 /* AppSecret.swift */; }; - DB6805102637D0F800430867 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* SwiftPackageProductDependency */; }; + DB6805102637D0F800430867 /* KeychainAccess in Frameworks */ = {isa = PBXBuildFile; productRef = DB68050F2637D0F800430867 /* KeychainAccess */; }; DB6805262637D7DD00430867 /* AppShared.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DB68047F2637CD4C00430867 /* AppShared.framework */; }; DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68586325E619B700F0A850 /* NSKeyValueObservation.swift */; }; DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB68A04925E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift */; }; @@ -306,7 +306,7 @@ DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B3C2636857500ACB481 /* AppearancePreference.swift */; }; DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; - DB6D9F42263527CE008423CD /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */; }; + DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB6D9F41263527CE008423CD /* AlamofireImage */; }; DB6D9F4926353FD7008423CD /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4826353FD6008423CD /* Subscription.swift */; }; DB6D9F502635761F008423CD /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */; }; DB6D9F57263577D2008423CD /* APIService+CoreData+Setting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F56263577D2008423CD /* APIService+CoreData+Setting.swift */; }; @@ -317,8 +317,8 @@ DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E2E264E5518009108F4 /* MastodonRegex.swift */; }; - DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; }; - DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; }; + DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DB6F5E31264E7410009108F4 /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB71FD2B25F86A5100512AE1 /* AvatarStackContainerButton.swift */; }; @@ -374,6 +374,7 @@ DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */; }; DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; }; + DB97131F2666078B00BD1E90 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB97131E2666078B00BD1E90 /* Date.swift */; }; DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98336A25C9420100AD9700 /* APIService+App.swift */; }; DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; }; DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; }; @@ -382,7 +383,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A487E2603456B008B817C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* SwiftPackageProductDependency */; }; + DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; DB9A48962603685D008B817C /* MastodonAttachmentService+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A48952603685D008B817C /* MastodonAttachmentService+UploadState.swift */; }; @@ -416,7 +417,7 @@ DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBAFB7352645463500371D5F /* Emojis.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAFB7342645463500371D5F /* Emojis.swift */; }; - DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */; }; + DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; }; DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; }; DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; }; DBB525302611EBF3002F1F29 /* ProfilePagingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */; }; @@ -463,7 +464,7 @@ DBE54ACC2636C8FD004E7C0B /* NotificationPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE54AC52636C89F004E7C0B /* NotificationPreference.swift */; }; DBF8AE16263293E400C9C23C /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF8AE15263293E400C9C23C /* NotificationService.swift */; }; DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBF8AE862632992800C9C23C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */; }; + DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; }; DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; }; DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */; }; DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */; }; @@ -552,7 +553,7 @@ files = ( DB6804872637CD4C00430867 /* AppShared.framework in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, - DB6F5E33264E7410009108F4 /* BuildFile in Embed Frameworks */, + DB6F5E33264E7410009108F4 /* TwitterTextEditor in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -946,6 +947,7 @@ DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; DB938F24262438D600E5B6C1 /* ThreadViewController+Provider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+Provider.swift"; sourceTree = ""; }; DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; + DB97131E2666078B00BD1E90 /* Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; DB98336A25C9420100AD9700 /* APIService+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+App.swift"; sourceTree = ""; }; DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = ""; }; DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = ""; }; @@ -1047,20 +1049,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6F5E32264E7410009108F4 /* BuildFile in Frameworks */, - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, + DB6F5E32264E7410009108F4 /* TwitterTextEditor in Frameworks */, + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, - DB9A487E2603456B008B817C /* BuildFile in Frameworks */, - 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */, - DBB525082611EAC0002F1F29 /* BuildFile in Frameworks */, - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, + DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, + 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, + DBB525082611EAC0002F1F29 /* Tabman in Frameworks */, + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, DB6804862637CD4C00430867 /* AppShared.framework in Frameworks */, - DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */, + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */, - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */, + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, + 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, DB6804C82637CE2F00430867 /* AppShared.framework in Frameworks */, ); @@ -1087,7 +1089,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB6805102637D0F800430867 /* BuildFile in Frameworks */, + DB6805102637D0F800430867 /* KeychainAccess in Frameworks */, EE93E8E8F9E0C39EAAEBD92F /* Pods_AppShared.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1112,9 +1114,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB00CA972632DDB600A54956 /* BuildFile in Frameworks */, - DB6D9F42263527CE008423CD /* BuildFile in Frameworks */, - DBF8AE862632992800C9C23C /* BuildFile in Frameworks */, + DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */, + DB6D9F42263527CE008423CD /* AlamofireImage in Frameworks */, + DBF8AE862632992800C9C23C /* Base85 in Frameworks */, DB6804A52637CDCC00430867 /* AppShared.framework in Frameworks */, B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */, ); @@ -2167,6 +2169,7 @@ DBCC3B2F261440A50045B23D /* UITabBarController.swift */, DBCC3B35261440BA0045B23D /* UINavigationController.swift */, DB6D1B23263684C600ACB481 /* UserDefaults.swift */, + DB97131E2666078B00BD1E90 /* Date.swift */, ); path = Extension; sourceTree = ""; @@ -2497,17 +2500,17 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, - DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */, - 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */, - 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */, - DB9A487D2603456B008B817C /* SwiftPackageProductDependency */, - DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */, - DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */, + DB3D0FF225BAA61700EAA174 /* AlamofireImage */, + 5D526FE125BE9AC400460CB9 /* MastodonSDK */, + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, + 2D42FF6025C8177C004A627A /* ActiveLabel */, + DB0140BC25C40D7500F9F3CF /* CommonOSLog */, + DB5086B725CC0D6400C2C187 /* Kingfisher */, + 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, + 2D939AC725EE14620076FA61 /* CropViewController */, + DB9A487D2603456B008B817C /* UITextView+Placeholder */, + DBB525072611EAC0002F1F29 /* Tabman */, + DB6F5E31264E7410009108F4 /* TwitterTextEditor */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -2568,7 +2571,7 @@ ); name = AppShared; packageProductDependencies = ( - DB68050F2637D0F800430867 /* SwiftPackageProductDependency */, + DB68050F2637D0F800430867 /* KeychainAccess */, ); productName = AppShared; productReference = DB68047F2637CD4C00430867 /* AppShared.framework */; @@ -2628,9 +2631,9 @@ ); name = NotificationService; packageProductDependencies = ( - DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */, - DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */, - DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */, + DBF8AE852632992800C9C23C /* Base85 */, + DB00CA962632DDB600A54956 /* CommonOSLog */, + DB6D9F41263527CE008423CD /* AlamofireImage */, ); productName = NotificationService; productReference = DBF8AE13263293E400C9C23C /* NotificationService.appex */; @@ -2685,18 +2688,18 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, - DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */, - 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */, - 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */, - DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */, - DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */, - DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */, - DB6804722637CC1200430867 /* RemoteSwiftPackageReference */, - DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */, + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, + 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, + 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, + DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, + DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */, + DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */, + DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */, + DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -3052,6 +3055,7 @@ DBE3CDBB261C427900430CC6 /* TimelineHeaderTableViewCell.swift in Sources */, 0FAA101C25E10E760017CCDE /* UIFont.swift in Sources */, 2D38F1D525CD465300561493 /* HomeTimelineViewController.swift in Sources */, + DB97131F2666078B00BD1E90 /* Date.swift in Sources */, DB98338825C945ED00AD9700 /* Assets.swift in Sources */, DB6180E926391BDF0018D199 /* MediaHostToMediaPreviewViewControllerAnimatedTransitioning.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, @@ -4034,7 +4038,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -4042,7 +4046,7 @@ version = 5.0.2; }; }; - 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */ = { + 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; requirement = { @@ -4050,7 +4054,7 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -4058,7 +4062,7 @@ minimumVersion = 3.1.0; }; }; - 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */ = { + 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; requirement = { @@ -4066,7 +4070,7 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -4074,7 +4078,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -4082,7 +4086,7 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */ = { + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -4090,7 +4094,7 @@ minimumVersion = 6.1.0; }; }; - DB6804722637CC1200430867 /* RemoteSwiftPackageReference */ = { + DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kishikawakatsumi/KeychainAccess.git"; requirement = { @@ -4098,7 +4102,7 @@ minimumVersion = 4.2.2; }; }; - DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */ = { + DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/TwitterTextEditor.git"; requirement = { @@ -4106,7 +4110,7 @@ kind = branch; }; }; - DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */ = { + DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; requirement = { @@ -4114,7 +4118,7 @@ minimumVersion = 1.4.1; }; }; - DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */ = { + DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/uias/Tabman"; requirement = { @@ -4122,7 +4126,7 @@ minimumVersion = 2.11.0; }; }; - DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */ = { + DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/Base85.git"; requirement = { @@ -4133,78 +4137,78 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { + 2D42FF6025C8177C004A627A /* ActiveLabel */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; + package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; productName = ActiveLabel; }; - 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */ = { + 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */; + package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; + package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; productName = AlamofireNetworkActivityIndicator; }; - 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */ = { + 2D939AC725EE14620076FA61 /* CropViewController */ = { isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */; + package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; productName = CropViewController; }; - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { + 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB00CA962632DDB600A54956 /* SwiftPackageProductDependency */ = { + DB00CA962632DDB600A54956 /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { + DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { + DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */ = { + DB5086B725CC0D6400C2C187 /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */; + package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - DB68050F2637D0F800430867 /* SwiftPackageProductDependency */ = { + DB68050F2637D0F800430867 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; - package = DB6804722637CC1200430867 /* RemoteSwiftPackageReference */; + package = DB6804722637CC1200430867 /* XCRemoteSwiftPackageReference "KeychainAccess" */; productName = KeychainAccess; }; - DB6D9F41263527CE008423CD /* SwiftPackageProductDependency */ = { + DB6D9F41263527CE008423CD /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB6F5E31264E7410009108F4 /* SwiftPackageProductDependency */ = { + DB6F5E31264E7410009108F4 /* TwitterTextEditor */ = { isa = XCSwiftPackageProductDependency; - package = DB6F5E30264E7410009108F4 /* RemoteSwiftPackageReference */; + package = DB6F5E30264E7410009108F4 /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; productName = TwitterTextEditor; }; - DB9A487D2603456B008B817C /* SwiftPackageProductDependency */ = { + DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { isa = XCSwiftPackageProductDependency; - package = DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */; + package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; - DBB525072611EAC0002F1F29 /* SwiftPackageProductDependency */ = { + DBB525072611EAC0002F1F29 /* Tabman */ = { isa = XCSwiftPackageProductDependency; - package = DBB525062611EAC0002F1F29 /* RemoteSwiftPackageReference */; + package = DBB525062611EAC0002F1F29 /* XCRemoteSwiftPackageReference "Tabman" */; productName = Tabman; }; - DBF8AE852632992800C9C23C /* SwiftPackageProductDependency */ = { + DBF8AE852632992800C9C23C /* Base85 */ = { isa = XCSwiftPackageProductDependency; - package = DBF8AE842632992700C9C23C /* RemoteSwiftPackageReference */; + package = DBF8AE842632992700C9C23C /* XCRemoteSwiftPackageReference "Base85" */; productName = Base85; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index a1e6170e..875a8284 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -76,7 +76,7 @@ extension ComposeStatusSection { //status.emoji cell.statusView.activeTextLabel.configure(content: status.content, emojiDict: [:]) // set date - cell.statusView.dateLabel.text = status.createdAt.shortTimeAgoSinceNow + cell.statusView.dateLabel.text = status.createdAt.slowedTimeAgoSinceNow cell.framePublisher.assign(to: \.value, on: repliedToCellFrameSubscriber).store(in: &cell.disposeBag) } diff --git a/Mastodon/Diffiable/Section/NotificationSection.swift b/Mastodon/Diffiable/Section/NotificationSection.swift index ead5d48f..155ec3fc 100644 --- a/Mastodon/Diffiable/Section/NotificationSection.swift +++ b/Mastodon/Diffiable/Section/NotificationSection.swift @@ -36,7 +36,7 @@ extension NotificationSection { assertionFailure() return nil } - let timeText = notification.createAt.shortTimeAgoSinceNow + let timeText = notification.createAt.slowedTimeAgoSinceNow let actionText = type.actionText let actionImageName = type.actionImageName @@ -59,7 +59,7 @@ extension NotificationSection { ) timestampUpdatePublisher .sink { _ in - let timeText = notification.createAt.shortTimeAgoSinceNow + let timeText = notification.createAt.slowedTimeAgoSinceNow cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) @@ -87,7 +87,7 @@ extension NotificationSection { cell.delegate = delegate timestampUpdatePublisher .sink { _ in - let timeText = notification.createAt.shortTimeAgoSinceNow + let timeText = notification.createAt.slowedTimeAgoSinceNow cell.actionLabel.text = actionText + " · " + timeText } .store(in: &cell.disposeBag) diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index ed24db92..f5460b02 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -468,12 +468,12 @@ extension StatusSection { // set date let createdAt = (status.reblog ?? status).createdAt - cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow + cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow timestampUpdatePublisher .sink { [weak cell] _ in guard let cell = cell else { return } - cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow - cell.statusView.dateLabel.accessibilityLabel = createdAt.timeAgoSinceNow + cell.statusView.dateLabel.text = createdAt.slowedTimeAgoSinceNow + cell.statusView.dateLabel.accessibilityLabel = createdAt.slowedTimeAgoSinceNow } .store(in: &cell.disposeBag) diff --git a/Mastodon/Extension/Date.swift b/Mastodon/Extension/Date.swift new file mode 100644 index 00000000..c1d73a43 --- /dev/null +++ b/Mastodon/Extension/Date.swift @@ -0,0 +1,29 @@ +// +// Date.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-1. +// + +import Foundation +import DateToolsSwift + +extension Date { + + var slowedTimeAgoSinceNow: String { + return self.slowedTimeAgo(since: Date()) + + } + + func slowedTimeAgo(since date: Date) -> String { + let earlierDate = date < self ? date : self + let latest = earlierDate == date ? self : date + + if earlierDate.timeIntervalSince(latest) >= -60 { + return L10n.Common.Controls.Timeline.Timestamp.now + } else { + return latest.shortTimeAgo(since: earlierDate) + } + } + +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index dfae5e2d..34556e8a 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -380,6 +380,10 @@ internal enum L10n { /// Show more replies internal static let showMoreReplies = L10n.tr("Localizable", "Common.Controls.Timeline.Loader.ShowMoreReplies") } + internal enum Timestamp { + /// Now + internal static let now = L10n.tr("Localizable", "Common.Controls.Timeline.Timestamp.Now") + } } } internal enum Countable { diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 39c4c4fe..a95c2cbc 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -132,6 +132,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; +"Common.Controls.Timeline.Timestamp.Now" = "Now"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 39c4c4fe..a95c2cbc 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -132,6 +132,7 @@ Your account looks like this to them."; "Common.Controls.Timeline.Loader.LoadMissingPosts" = "Load missing posts"; "Common.Controls.Timeline.Loader.LoadingMissingPosts" = "Loading missing posts..."; "Common.Controls.Timeline.Loader.ShowMoreReplies" = "Show more replies"; +"Common.Controls.Timeline.Timestamp.Now" = "Now"; "Common.Countable.Photo.Multiple" = "photos"; "Common.Countable.Photo.Single" = "photo"; "Scene.Compose.Accessibility.AppendAttachment" = "Append attachment"; From a44d09d13d8c01eb709cc24fa22cb730e8de57f0 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 1 Jun 2021 16:05:31 +0800 Subject: [PATCH 20/33] chore: update version to 0.6.0 (6) --- Mastodon.xcodeproj/project.pbxproj | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 5d765861..3cf4fc69 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3630,7 +3630,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3638,7 +3638,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.0; + MARKETING_VERSION = 0.6.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3657,7 +3657,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3665,7 +3665,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.5.0; + MARKETING_VERSION = 0.6.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -3920,7 +3920,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3928,7 +3928,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.5.0; + MARKETING_VERSION = 0.6.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -3943,7 +3943,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3951,7 +3951,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 0.5.0; + MARKETING_VERSION = 0.6.0; PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; From 634aef0de3f13f50d5063ae4dd669b653cfe9a61 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 2 Jun 2021 14:59:01 +0800 Subject: [PATCH 21/33] fix: input emoji make auto complete component crash issue --- .../Scene/Compose/ComposeViewController.swift | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 69fef0a5..48db5df7 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -864,12 +864,14 @@ extension ComposeViewController: TextEditorViewChangeObserver { private static func scanAutoCompleteInfo(textEditorView: TextEditorView) -> AutoCompleteInfo? { let text = textEditorView.text - let cursorLocation = textEditorView.selectedRange.location - let cursorIndex = text.index(text.startIndex, offsetBy: cursorLocation) - guard cursorLocation > 0, !text.isEmpty else { return nil } - - let _highlighStartIndex: String.Index? = { - var index = text.index(text.startIndex, offsetBy: cursorLocation - 1) + + guard textEditorView.selectedRange.location > 0, !text.isEmpty, + let selectedRange = Range(textEditorView.selectedRange, in: text) else { + return nil + } + let cursorIndex = selectedRange.upperBound + let _highlightStartIndex: String.Index? = { + var index = text.index(before: cursorIndex) while index > text.startIndex { let char = text[index] if char == "@" || char == "#" || char == ":" { @@ -886,18 +888,18 @@ extension ComposeViewController: TextEditorViewChangeObserver { } }() - guard let highlighStartIndex = _highlighStartIndex else { return nil } - let scanRange = NSRange(highlighStartIndex..= cursorIndex else { return nil } - let symbolRange = highlighStartIndex..= cursorIndex else { return nil } + let symbolRange = highlightStartIndex.. Date: Wed, 2 Jun 2021 14:59:39 +0800 Subject: [PATCH 22/33] fix: typo --- .../Scene/Compose/ComposeViewController.swift | 24 +++++++++---------- .../MastodonAttachmentService.swift | 12 +++++----- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 48db5df7..6fb52d2f 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -730,7 +730,7 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate { guard let emoji = customEmojiViewModel.emoji(shortcode: name) else { continue } os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: handle emoji: %s", ((#file as NSString).lastPathComponent), #line, #function, name) - // set emoji token invisiable (without upper bounce space) + // set emoji token invisible (without upper bounce space) var attributes = [NSAttributedString.Key: Any]() attributes[.font] = UIFont.systemFont(ofSize: 0.01) attributedString.addAttributes(attributes, range: match.range) @@ -812,15 +812,15 @@ extension ComposeViewController: TextEditorViewTextAttributesDelegate { extension ComposeViewController: TextEditorViewChangeObserver { func textEditorView(_ textEditorView: TextEditorView, didChangeWithChangeResult changeResult: TextEditorViewChangeResult) { - guard var autoCompeletion = ComposeViewController.scanAutoCompleteInfo(textEditorView: textEditorView) else { + guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textEditorView: textEditorView) else { viewModel.autoCompleteInfo.value = nil return } - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompeletion.toHighlightEndString), String(autoCompeletion.toCursorString)) + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString)) // get layout text bounding rect var glyphRange = NSRange() - textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange) + textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textEditorView.text), actualGlyphRange: &glyphRange) let textContainer = textEditorView.layoutManager.textContainers[0] let textBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) @@ -838,13 +838,13 @@ extension ComposeViewController: TextEditorViewChangeObserver { viewModel.autoCompleteRetryLayoutTimes.value = 0 // get symbol bounding rect - textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompeletion.symbolRange, in: textEditorView.text), actualGlyphRange: &glyphRange) + textEditorView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textEditorView.text), actualGlyphRange: &glyphRange) let symbolBoundingRect = textEditorView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) // set bounding rect and trigger layout - autoCompeletion.textBoundingRect = textBoundingRect - autoCompeletion.symbolBoundingRect = symbolBoundingRect - viewModel.autoCompleteInfo.value = autoCompeletion + autoCompletion.textBoundingRect = textBoundingRect + autoCompletion.symbolBoundingRect = symbolBoundingRect + viewModel.autoCompleteInfo.value = autoCompletion } struct AutoCompleteInfo { @@ -1017,7 +1017,7 @@ extension ComposeViewController: UICollectionViewDelegate { let emoji = attribute.emoji let textEditorView = self.textEditorView() - // retrive active text input and insert emoji + // retrieve active text input and insert emoji // the leading and trailing space is REQUIRED to fix `UITextStorage` layout issue let reference = viewModel.customEmojiPickerInputViewModel.insertText(" :\(emoji.shortcode): ") @@ -1070,7 +1070,7 @@ extension ComposeViewController: PHPickerViewControllerDelegate { let service = MastodonAttachmentService( context: context, pickerResult: result, - initalAuthenticationBox: viewModel.activeAuthenticationBox.value + initialAuthenticationBox: viewModel.activeAuthenticationBox.value ) return service } @@ -1089,7 +1089,7 @@ extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationC let attachmentService = MastodonAttachmentService( context: context, image: image, - initalAuthenticationBox: viewModel.activeAuthenticationBox.value + initialAuthenticationBox: viewModel.activeAuthenticationBox.value ) viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] } @@ -1108,7 +1108,7 @@ extension ComposeViewController: UIDocumentPickerDelegate { let attachmentService = MastodonAttachmentService( context: context, documentURL: url, - initalAuthenticationBox: viewModel.activeAuthenticationBox.value + initialAuthenticationBox: viewModel.activeAuthenticationBox.value ) viewModel.attachmentServices.value = viewModel.attachmentServices.value + [attachmentService] } diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index 4c541a8d..b302c083 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -52,10 +52,10 @@ final class MastodonAttachmentService { init( context: AppContext, pickerResult: PHPickerResult, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? ) { self.context = context - self.authenticationBox = initalAuthenticationBox + self.authenticationBox = initialAuthenticationBox // end init setupServiceObserver() @@ -90,10 +90,10 @@ final class MastodonAttachmentService { init( context: AppContext, image: UIImage, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? ) { self.context = context - self.authenticationBox = initalAuthenticationBox + self.authenticationBox = initialAuthenticationBox // end init setupServiceObserver() @@ -105,10 +105,10 @@ final class MastodonAttachmentService { init( context: AppContext, documentURL: URL, - initalAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? + initialAuthenticationBox: AuthenticationService.MastodonAuthenticationBox? ) { self.context = context - self.authenticationBox = initalAuthenticationBox + self.authenticationBox = initialAuthenticationBox // end init setupServiceObserver() From 8b47e120875a873b3f4ebeb00a704f596761ec43 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 2 Jun 2021 14:59:59 +0800 Subject: [PATCH 23/33] chore: update version to 0.6.0 (7) --- Mastodon.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3cf4fc69..4406f593 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3630,7 +3630,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3657,7 +3657,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 7; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; From 3ab78f1134e06e62ed148f0801ed15cf34065523 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Jun 2021 18:31:57 +0800 Subject: [PATCH 24/33] feat: adapt AuthenticationSession for authentication --- Mastodon.xcodeproj/project.pbxproj | 24 +-- Mastodon/Coordinator/SceneCoordinator.swift | 7 +- .../MastodonPickServerViewController.swift | 96 +++++++---- .../MastodonPickServerViewModel.swift | 158 +----------------- ...PinBasedAuthenticationViewController.swift | 73 -------- ...todonPinBasedAuthenticationViewModel.swift | 40 ----- ...ationViewModelNavigationDelegateShim.swift | 41 ----- .../Share/AuthenticationViewModel.swift | 17 +- .../MastodonAuthenticationController.swift | 75 +++++++++ .../Service/APIService/APIService+App.swift | 6 +- .../APIService+Authentication.swift | 6 +- .../MastodonSDK/API/Mastodon+API+App.swift | 2 +- .../MastodonSDK/API/Mastodon+API+OAuth.swift | 4 +- 13 files changed, 168 insertions(+), 381 deletions(-) delete mode 100644 Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift delete mode 100644 Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift delete mode 100644 Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift create mode 100644 Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 4406f593..7d63b303 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -181,11 +181,9 @@ 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A4ABE34829701A4496C5BB64 /* Pods_Mastodon.framework */; }; B914FC6B0B8AF18573C0B291 /* Pods_NotificationService.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 374AA339A20E0FAC75BCDA6D /* Pods_NotificationService.framework */; }; DB00CA972632DDB600A54956 /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB00CA962632DDB600A54956 /* CommonOSLog */; }; - DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; - DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; - DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; + DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB029E94266A20430062874E /* MastodonAuthenticationController.swift */; }; DB02CDAB26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */; }; DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB040ECD26526EA600BEE9D8 /* ComposeCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */; }; @@ -753,10 +751,8 @@ BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.release.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.release.xcconfig"; sourceTree = ""; }; CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D7D7CF93E262178800077512 /* Pods-Mastodon-AppShared.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-AppShared.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-AppShared/Pods-Mastodon-AppShared.debug.xcconfig"; sourceTree = ""; }; - DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewController.swift; sourceTree = ""; }; - DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift; sourceTree = ""; }; - DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPinBasedAuthenticationViewModel.swift; sourceTree = ""; }; DB0140CE25C42AEE00F9F3CF /* OSLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OSLog.swift; sourceTree = ""; }; + DB029E94266A20430062874E /* MastodonAuthenticationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonAuthenticationController.swift; sourceTree = ""; }; DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadReplyLoaderTableViewCell.swift; sourceTree = ""; }; DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB040ECC26526EA600BEE9D8 /* ComposeCollectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeCollectionView.swift; sourceTree = ""; }; @@ -1618,7 +1614,6 @@ DB68A03825E900CC00CFDF14 /* Share */, 0FAA0FDD25E0B5700017CCDE /* Welcome */, 0FAA102525E1125D0017CCDE /* PickServer */, - DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */, DBE0821A25CD382900FD6BBD /* Register */, DB72602125E36A2500235243 /* ServerRules */, 2D364F7025E66D5B00204FDC /* ResendEmail */, @@ -1627,16 +1622,6 @@ path = Onboarding; sourceTree = ""; }; - DB0140A625C40C0900F9F3CF /* PinBasedAuthentication */ = { - isa = PBXGroup; - children = ( - DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */, - DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */, - DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */, - ); - path = PinBasedAuthentication; - sourceTree = ""; - }; DB084B5125CBC56300F898ED /* CoreDataStack */ = { isa = PBXGroup; children = ( @@ -1921,6 +1906,7 @@ children = ( 2D82B9FE25E7863200E36F0F /* OnboardingViewControllerAppearance.swift */, DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */, + DB029E94266A20430062874E /* MastodonAuthenticationController.swift */, ); path = Share; sourceTree = ""; @@ -3043,6 +3029,7 @@ DB49A62B25FF36C700B98345 /* APIService+CustomEmoji.swift in Sources */, DBCBED1D26132E1A00B49291 /* StatusFetchedResultsController.swift in Sources */, 2D79E701261EA5550011E398 /* APIService+CoreData+Tag.swift in Sources */, + DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */, 5B90C461262599800002E742 /* SettingsLinkTableViewCell.swift in Sources */, DB6180DD263918E30018D199 /* MediaPreviewViewController.swift in Sources */, DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, @@ -3149,7 +3136,6 @@ DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, - DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, DB789A1C25F9F76A0071ACA0 /* ComposeStatusContentCollectionViewCell.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, @@ -3220,7 +3206,6 @@ 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */, DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */, - DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */, DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */, DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */, DB68046C2636DC9E00430867 /* MastodonNotification.swift in Sources */, @@ -3280,7 +3265,6 @@ DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */, DB51D173262832380062B7A1 /* BlurHashEncode.swift in Sources */, DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */, - DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */, 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DB71FD2C25F86A5100512AE1 /* AvatarStackContainerButton.swift in Sources */, DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, diff --git a/Mastodon/Coordinator/SceneCoordinator.swift b/Mastodon/Coordinator/SceneCoordinator.swift index f9da785a..11053e66 100644 --- a/Mastodon/Coordinator/SceneCoordinator.swift +++ b/Mastodon/Coordinator/SceneCoordinator.swift @@ -42,7 +42,6 @@ extension SceneCoordinator { // onboarding case welcome case mastodonPickServer(viewMode: MastodonPickServerViewModel) - case mastodonPinBasedAuthentication(viewModel: MastodonPinBasedAuthenticationViewModel) case mastodonRegister(viewModel: MastodonRegisterViewModel) case mastodonServerRules(viewModel: MastodonServerRulesViewModel) case mastodonConfirmEmail(viewModel: MastodonConfirmEmailViewModel) @@ -78,6 +77,7 @@ extension SceneCoordinator { case safari(url: URL) case alertController(alertController: UIAlertController) case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?) + #if DEBUG case publicTimeline #endif @@ -86,7 +86,6 @@ extension SceneCoordinator { switch self { case .welcome, .mastodonPickServer, - .mastodonPinBasedAuthentication, .mastodonRegister, .mastodonServerRules, .mastodonConfirmEmail, @@ -217,10 +216,6 @@ private extension SceneCoordinator { let _viewController = MastodonPickServerViewController() _viewController.viewModel = viewModel viewController = _viewController - case .mastodonPinBasedAuthentication(let viewModel): - let _viewController = MastodonPinBasedAuthenticationViewController() - _viewController.viewModel = viewModel - viewController = _viewController case .mastodonRegister(let viewModel): let _viewController = MastodonRegisterViewController() _viewController.viewModel = viewModel diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift index 7c7e7b0e..2a978c69 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewController.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Combine import GameController +import AuthenticationServices final class MastodonPickServerViewController: UIViewController, NeedsDependency { @@ -19,6 +20,11 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } var viewModel: MastodonPickServerViewModel! + private(set) lazy var authenticationViewModel = AuthenticationViewModel( + context: context, + coordinator: coordinator, + isAuthenticationExist: false + ) private var expandServerDomainSet = Set() @@ -50,6 +56,8 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency }() var nextStepButtonBottomLayoutConstraint: NSLayoutConstraint! + var mastodonAuthenticationController: MastodonAuthenticationController? + deinit { tableViewObservation = nil os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -182,23 +190,26 @@ extension MastodonPickServerViewController { .assign(to: \.isEnabled, on: nextStepButton) .store(in: &disposeBag) - viewModel.error - .compactMap { $0 } - .receive(on: DispatchQueue.main) - .sink { [weak self] error in - guard let self = self else { return } - let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) - alertController.addAction(okAction) - self.coordinator.present( - scene: .alertController(alertController: alertController), - from: nil, - transition: .alertController(animated: true, completion: nil) - ) - } - .store(in: &disposeBag) + Publishers.Merge( + viewModel.error, + authenticationViewModel.error + ) + .compactMap { $0 } + .receive(on: DispatchQueue.main) + .sink { [weak self] error in + guard let self = self else { return } + let alertController = UIAlertController(for: error, title: "Error", preferredStyle: .alert) + let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil) + alertController.addAction(okAction) + self.coordinator.present( + scene: .alertController(alertController: alertController), + from: nil, + transition: .alertController(animated: true, completion: nil) + ) + } + .store(in: &disposeBag) - viewModel + authenticationViewModel .authenticated .flatMap { [weak self] (domain, user) -> AnyPublisher, Never> in guard let self = self else { return Just(.success(false)).eraseToAnyPublisher() } @@ -217,7 +228,7 @@ extension MastodonPickServerViewController { } .store(in: &disposeBag) - viewModel.isAuthenticating + authenticationViewModel.isAuthenticating .receive(on: DispatchQueue.main) .sink { [weak self] isAuthenticating in guard let self = self else { return } @@ -273,11 +284,15 @@ extension MastodonPickServerViewController { private func doSignIn() { guard let server = viewModel.selectedServer.value else { return } - viewModel.isAuthenticating.send(true) + authenticationViewModel.isAuthenticating.send(true) context.apiService.createApplication(domain: server.domain) - .tryMap { response -> MastodonPickServerViewModel.AuthenticateInfo in + .tryMap { response -> AuthenticationViewModel.AuthenticateInfo in let application = response.value - guard let info = MastodonPickServerViewModel.AuthenticateInfo(domain: server.domain, application: application) else { + guard let info = AuthenticationViewModel.AuthenticateInfo( + domain: server.domain, + application: application, + redirectURI: response.value.redirectURI ?? MastodonAuthenticationController.callbackURL + ) else { throw APIService.APIError.explicit(.badResponse) } return info @@ -285,7 +300,7 @@ extension MastodonPickServerViewController { .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self = self else { return } - self.viewModel.isAuthenticating.send(false) + self.authenticationViewModel.isAuthenticating.send(false) switch completion { case .failure(let error): @@ -296,15 +311,19 @@ extension MastodonPickServerViewController { } } receiveValue: { [weak self] info in guard let self = self else { return } - let mastodonPinBasedAuthenticationViewModel = MastodonPinBasedAuthenticationViewModel(authenticateURL: info.authorizeURL) - self.viewModel.authenticate( - info: info, - pinCodePublisher: mastodonPinBasedAuthenticationViewModel.pinCodePublisher + let authenticationController = MastodonAuthenticationController( + context: self.context, + authenticateURL: info.authorizeURL ) - self.viewModel.mastodonPinBasedAuthenticationViewController = self.coordinator.present( - scene: .mastodonPinBasedAuthentication(viewModel: mastodonPinBasedAuthenticationViewModel), - from: nil, - transition: .modal(animated: true, completion: nil) + + self.mastodonAuthenticationController = authenticationController + authenticationController.authenticationSession?.prefersEphemeralWebBrowserSession = true + authenticationController.authenticationSession?.presentationContextProvider = self + authenticationController.authenticationSession?.start() + + self.authenticationViewModel.authenticate( + info: info, + pinCodePublisher: authenticationController.pinCodePublisher ) } .store(in: &disposeBag) @@ -313,7 +332,7 @@ extension MastodonPickServerViewController { private func doSignUp() { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) guard let server = viewModel.selectedServer.value else { return } - viewModel.isAuthenticating.send(true) + authenticationViewModel.isAuthenticating.send(true) context.apiService.instance(domain: server.domain) .compactMap { [weak self] response -> AnyPublisher? in @@ -328,7 +347,10 @@ extension MastodonPickServerViewController { .switchToLatest() .tryMap { response -> MastodonPickServerViewModel.SignUpResponseSecond in let application = response.application.value - guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo(domain: server.domain, application: application) else { + guard let authenticateInfo = AuthenticationViewModel.AuthenticateInfo( + domain: server.domain, + application: application + ) else { throw APIService.APIError.explicit(.badResponse) } return MastodonPickServerViewModel.SignUpResponseSecond(instance: response.instance, authenticateInfo: authenticateInfo) @@ -340,7 +362,8 @@ extension MastodonPickServerViewController { return self.context.apiService.applicationAccessToken( domain: server.domain, clientID: authenticateInfo.clientID, - clientSecret: authenticateInfo.clientSecret + clientSecret: authenticateInfo.clientSecret, + redirectURI: authenticateInfo.redirectURI ) .map { MastodonPickServerViewModel.SignUpResponseThird(instance: instance, authenticateInfo: authenticateInfo, applicationToken: $0) } .eraseToAnyPublisher() @@ -349,7 +372,7 @@ extension MastodonPickServerViewController { .receive(on: DispatchQueue.main) .sink { [weak self] completion in guard let self = self else { return } - self.viewModel.isAuthenticating.send(false) + self.authenticationViewModel.isAuthenticating.send(false) switch completion { case .failure(let error): @@ -519,3 +542,10 @@ extension MastodonPickServerViewController: PickServerCellDelegate { // MARK: - OnboardingViewControllerAppearance extension MastodonPickServerViewController: OnboardingViewControllerAppearance { } + +// MARK: - ASWebAuthenticationPresentationContextProviding +extension MastodonPickServerViewController: ASWebAuthenticationPresentationContextProviding { + func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { + return view.window! + } +} diff --git a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift index a576632a..8348f884 100644 --- a/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift +++ b/Mastodon/Scene/Onboarding/PickServer/MastodonPickServerViewModel.swift @@ -58,15 +58,11 @@ class MastodonPickServerViewModel: NSObject { let filteredIndexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([]) let servers = CurrentValueSubject<[Mastodon.Entity.Server], Error>([]) let selectedServer = CurrentValueSubject(nil) - let error = PassthroughSubject() - let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() - let isAuthenticating = CurrentValueSubject(false) + let error = CurrentValueSubject(nil) let isLoadingIndexedServers = CurrentValueSubject(false) let emptyStateViewState = CurrentValueSubject(.none) - - var mastodonPinBasedAuthenticationViewController: UIViewController? - + init(context: AppContext, mode: PickServerMode) { self.context = context self.mode = mode @@ -233,156 +229,6 @@ extension MastodonPickServerViewModel { } } } - - -// MARK: - SignIn methods & structs -extension MastodonPickServerViewModel { - enum AuthenticationError: Error, LocalizedError { - case badCredentials - case registrationClosed - - var errorDescription: String? { - switch self { - case .badCredentials: return "Bad Credentials" - case .registrationClosed: return "Registration Closed" - } - } - - var failureReason: String? { - switch self { - case .badCredentials: return "Credentials invalid." - case .registrationClosed: return "Server disallow registration." - } - } - - var helpAnchor: String? { - switch self { - case .badCredentials: return "Please try again." - case .registrationClosed: return "Please try another domain." - } - } - } - - struct AuthenticateInfo { - let domain: String - let clientID: String - let clientSecret: String - let authorizeURL: URL - - init?(domain: String, application: Mastodon.Entity.Application) { - self.domain = domain - guard let clientID = application.clientID, - let clientSecret = application.clientSecret else { return nil } - self.clientID = clientID - self.clientSecret = clientSecret - self.authorizeURL = { - let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID) - let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) - return url - }() - } - } - - func authenticate(info: AuthenticateInfo, pinCodePublisher: PassthroughSubject) { - pinCodePublisher - .handleEvents(receiveOutput: { [weak self] _ in - guard let self = self else { return } -// self.isAuthenticating.value = true - self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil) - self.mastodonPinBasedAuthenticationViewController = nil - }) - .compactMap { [weak self] code -> AnyPublisher, Error>? in - guard let self = self else { return nil } - return self.context.apiService - .userAccessToken( - domain: info.domain, - clientID: info.clientID, - clientSecret: info.clientSecret, - code: code - ) - .flatMap { response -> AnyPublisher, Error> in - let token = response.value - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: sign in success. Token: %s", ((#file as NSString).lastPathComponent), #line, #function, token.accessToken) - return Self.verifyAndSaveAuthentication( - context: self.context, - info: info, - userToken: token - ) - } - .eraseToAnyPublisher() - } - .switchToLatest() - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: swap user access token swap fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) -// self.isAuthenticating.value = false - self.error.send(error) - case .finished: - break - } - } receiveValue: { [weak self] response in - guard let self = self else { return } - let account = response.value - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user %s sign in success", ((#file as NSString).lastPathComponent), #line, #function, account.username) - - self.authenticated.send((domain: info.domain, account: account)) - } - .store(in: &self.disposeBag) - } - - static func verifyAndSaveAuthentication( - context: AppContext, - info: AuthenticateInfo, - userToken: Mastodon.Entity.Token - ) -> AnyPublisher, Error> { - let authorization = Mastodon.API.OAuth.Authorization(accessToken: userToken.accessToken) - let managedObjectContext = context.backgroundManagedObjectContext - - return context.apiService.accountVerifyCredentials( - domain: info.domain, - authorization: authorization - ) - .flatMap { response -> AnyPublisher, Error> in - let account = response.value - let mastodonUserRequest = MastodonUser.sortedFetchRequest - mastodonUserRequest.predicate = MastodonUser.predicate(domain: info.domain, id: account.id) - mastodonUserRequest.fetchLimit = 1 - guard let mastodonUser = try? managedObjectContext.fetch(mastodonUserRequest).first else { - return Fail(error: AuthenticationError.badCredentials).eraseToAnyPublisher() - } - - let property = MastodonAuthentication.Property( - domain: info.domain, - userID: mastodonUser.id, - username: mastodonUser.username, - appAccessToken: userToken.accessToken, // TODO: swap app token - userAccessToken: userToken.accessToken, - clientID: info.clientID, - clientSecret: info.clientSecret - ) - return managedObjectContext.performChanges { - _ = APIService.CoreData.createOrMergeMastodonAuthentication( - into: managedObjectContext, - for: mastodonUser, - in: info.domain, - property: property, - networkDate: response.networkDate - ) - } - .tryMap { result in - switch result { - case .failure(let error): throw error - case .success: return response - } - } - .eraseToAnyPublisher() - } - .eraseToAnyPublisher() - } -} // MARK: - SignUp methods & structs extension MastodonPickServerViewModel { diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift deleted file mode 100644 index d566da4c..00000000 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewController.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// MastodonPinBasedAuthenticationViewController.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/1/29. -// - -import os.log -import UIKit -import Combine -import WebKit - -final class MastodonPinBasedAuthenticationViewController: UIViewController, NeedsDependency { - - weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } - weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } - - var disposeBag = Set() - var viewModel: MastodonPinBasedAuthenticationViewModel! - - let webView: WKWebView = { - let configuration = WKWebViewConfiguration() - configuration.processPool = WKProcessPool() - let webView = WKWebView(frame: .zero, configuration: configuration) - return webView - }() - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - // cleanup cookie - let httpCookieStore = webView.configuration.websiteDataStore.httpCookieStore - httpCookieStore.getAllCookies { cookies in - for cookie in cookies { - httpCookieStore.delete(cookie, completionHandler: nil) - } - } - } - -} - -extension MastodonPinBasedAuthenticationViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - title = "Authentication" - navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(MastodonPinBasedAuthenticationViewController.cancelBarButtonItemPressed(_:))) - - webView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(webView) - NSLayoutConstraint.activate([ - webView.topAnchor.constraint(equalTo: view.topAnchor), - webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - let request = URLRequest(url: viewModel.authenticateURL) - webView.navigationDelegate = viewModel.navigationDelegate - webView.load(request) - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: authenticate via: %s", ((#file as NSString).lastPathComponent), #line, #function, viewModel.authenticateURL.debugDescription) - } - -} - -extension MastodonPinBasedAuthenticationViewController { - - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - dismiss(animated: true, completion: nil) - } - -} diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift deleted file mode 100644 index 5eac359e..00000000 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModel.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// MastodonPinBasedAuthenticationViewModel.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021/1/29. -// - -import os.log -import Foundation -import Combine -import WebKit - -final class MastodonPinBasedAuthenticationViewModel { - - // input - let authenticateURL: URL - - // output - let pinCodePublisher = PassthroughSubject() - private var navigationDelegateShim: MastodonPinBasedAuthenticationViewModelNavigationDelegateShim? - - init(authenticateURL: URL) { - self.authenticateURL = authenticateURL - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - -} - -extension MastodonPinBasedAuthenticationViewModel { - - var navigationDelegate: WKNavigationDelegate { - let navigationDelegateShim = MastodonPinBasedAuthenticationViewModelNavigationDelegateShim(viewModel: self) - self.navigationDelegateShim = navigationDelegateShim - return navigationDelegateShim - } - -} diff --git a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift b/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift deleted file mode 100644 index dd890172..00000000 --- a/Mastodon/Scene/Onboarding/PinBasedAuthentication/MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift -// Mastodon -// -// Created by Cirno MainasuK on 2021/1/29. -// - -import os.log -import Foundation -import WebKit - -final class MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: NSObject { - - weak var viewModel: MastodonPinBasedAuthenticationViewModel? - - init(viewModel: MastodonPinBasedAuthenticationViewModel) { - self.viewModel = viewModel - } - - deinit { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } -} - - -// MARK: - WKNavigationDelegate -extension MastodonPinBasedAuthenticationViewModelNavigationDelegateShim: WKNavigationDelegate { - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - guard let url = webView.url, - let components = URLComponents(url: url, resolvingAgainstBaseURL: false), - let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), - let code = codeQueryItem.value else { - return - } - - viewModel?.pinCodePublisher.send(code) - } - -} - diff --git a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift index 0bd1bf09..eb3cf572 100644 --- a/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift +++ b/Mastodon/Scene/Onboarding/Share/AuthenticationViewModel.swift @@ -31,9 +31,7 @@ final class AuthenticationViewModel { let isIdle = CurrentValueSubject(true) let authenticated = PassthroughSubject<(domain: String, account: Mastodon.Entity.Account), Never>() let error = CurrentValueSubject(nil) - - var mastodonPinBasedAuthenticationViewController: UIViewController? - + init(context: AppContext, coordinator: SceneCoordinator, isAuthenticationExist: Bool) { self.context = context self.coordinator = coordinator @@ -118,18 +116,24 @@ extension AuthenticationViewModel { let clientID: String let clientSecret: String let authorizeURL: URL + let redirectURI: String - init?(domain: String, application: Mastodon.Entity.Application) { + init?( + domain: String, + application: Mastodon.Entity.Application, + redirectURI: String = MastodonAuthenticationController.callbackURL + ) { self.domain = domain guard let clientID = application.clientID, let clientSecret = application.clientSecret else { return nil } self.clientID = clientID self.clientSecret = clientSecret self.authorizeURL = { - let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID) + let query = Mastodon.API.OAuth.AuthorizeQuery(clientID: clientID, redirectURI: redirectURI) let url = Mastodon.API.OAuth.authorizeURL(domain: domain, query: query) return url }() + self.redirectURI = redirectURI } } @@ -138,8 +142,6 @@ extension AuthenticationViewModel { .handleEvents(receiveOutput: { [weak self] _ in guard let self = self else { return } self.isAuthenticating.value = true - self.mastodonPinBasedAuthenticationViewController?.dismiss(animated: true, completion: nil) - self.mastodonPinBasedAuthenticationViewController = nil }) .compactMap { [weak self] code -> AnyPublisher, Error>? in guard let self = self else { return nil } @@ -148,6 +150,7 @@ extension AuthenticationViewModel { domain: info.domain, clientID: info.clientID, clientSecret: info.clientSecret, + redirectURI: info.redirectURI, code: code ) .flatMap { response -> AnyPublisher, Error> in diff --git a/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift new file mode 100644 index 00000000..c97fc148 --- /dev/null +++ b/Mastodon/Scene/Onboarding/Share/MastodonAuthenticationController.swift @@ -0,0 +1,75 @@ +// +// MastodonAuthenticationController.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-4. +// + +import os.log +import UIKit +import Combine +import AuthenticationServices + +final class MastodonAuthenticationController { + + static let callbackURLScheme = "mastodon" + static let callbackURL = "mastodon://joinmastodon.org/oauth" + + var disposeBag = Set() + + // input + var context: AppContext! + let authenticateURL: URL + var authenticationSession: ASWebAuthenticationSession? + + // output + let isAuthenticating = CurrentValueSubject(false) + let error = CurrentValueSubject(nil) + let pinCodePublisher = PassthroughSubject() + + init( + context: AppContext, + authenticateURL: URL + ) { + self.context = context + self.authenticateURL = authenticateURL + + authentication() + } + +} + +extension MastodonAuthenticationController { + private func authentication() { + authenticationSession = ASWebAuthenticationSession( + url: authenticateURL, + callbackURLScheme: MastodonAuthenticationController.callbackURLScheme + ) { [weak self] callback, error in + guard let self = self else { return } + os_log("%{public}s[%{public}ld], %{public}s: callback: %s, error: %s", ((#file as NSString).lastPathComponent), #line, #function, callback?.debugDescription ?? "", error.debugDescription) + + if let error = error { + if let error = error as? ASWebAuthenticationSessionError { + if error.errorCode == ASWebAuthenticationSessionError.canceledLogin.rawValue { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: user cancel authentication", ((#file as NSString).lastPathComponent), #line, #function) + self.isAuthenticating.value = false + return + } + } + + self.isAuthenticating.value = false + self.error.value = error + return + } + + guard let url = callback, + let components = URLComponents(url: url, resolvingAgainstBaseURL: false), + let codeQueryItem = components.queryItems?.first(where: { $0.name == "code" }), + let code = codeQueryItem.value else { + return + } + + self.pinCodePublisher.send(code) + } + } +} diff --git a/Mastodon/Service/APIService/APIService+App.swift b/Mastodon/Service/APIService/APIService+App.swift index 9726a6ee..b5b1d868 100644 --- a/Mastodon/Service/APIService/APIService+App.swift +++ b/Mastodon/Service/APIService/APIService+App.swift @@ -20,7 +20,11 @@ extension APIService { #endif func createApplication(domain: String) -> AnyPublisher, Error> { - let query = Mastodon.API.App.CreateQuery(clientName: APIService.clientName, website: nil) + let query = Mastodon.API.App.CreateQuery( + clientName: APIService.clientName, + redirectURIs: MastodonAuthenticationController.callbackURL, + website: nil + ) return Mastodon.API.App.create( session: session, domain: domain, diff --git a/Mastodon/Service/APIService/APIService+Authentication.swift b/Mastodon/Service/APIService/APIService+Authentication.swift index 55a188a5..ffd9afd7 100644 --- a/Mastodon/Service/APIService/APIService+Authentication.swift +++ b/Mastodon/Service/APIService/APIService+Authentication.swift @@ -17,11 +17,13 @@ extension APIService { domain: String, clientID: String, clientSecret: String, + redirectURI: String, code: String ) -> AnyPublisher, Error> { let query = Mastodon.API.OAuth.AccessTokenQuery( clientID: clientID, clientSecret: clientSecret, + redirectURI: redirectURI, code: code, grantType: "authorization_code" ) @@ -35,11 +37,13 @@ extension APIService { func applicationAccessToken( domain: String, clientID: String, - clientSecret: String + clientSecret: String, + redirectURI: String ) -> AnyPublisher, Error> { let query = Mastodon.API.OAuth.AccessTokenQuery( clientID: clientID, clientSecret: clientSecret, + redirectURI: redirectURI, code: nil, grantType: "client_credentials" ) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift index 3993afa6..48d78d68 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+App.swift @@ -103,7 +103,7 @@ extension Mastodon.API.App { public init( clientName: String, - redirectURIs: String = "urn:ietf:wg:oauth:2.0:oob", + redirectURIs: String, scopes: String? = "read write follow push", website: String? ) { diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift index 332d78a3..b5451e8f 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+OAuth.swift @@ -139,7 +139,7 @@ extension Mastodon.API.OAuth { forceLogin: String? = nil, responseType: String = "code", clientID: String, - redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", + redirectURI: String, scope: String? = "read write follow push" ) { self.forceLogin = forceLogin @@ -166,7 +166,7 @@ extension Mastodon.API.OAuth { public init( clientID: String, clientSecret: String, - redirectURI: String = "urn:ietf:wg:oauth:2.0:oob", + redirectURI: String, scope: String? = "read write follow push", code: String?, grantType: String From 7115951f6bf72d33fe8f6ce5c9147b8b1519fa2b Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Jun 2021 18:42:22 +0800 Subject: [PATCH 25/33] feat: make server search text field handle return key --- .../TableViewCell/PickServerSearchCell.swift | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift index dc048f67..546db989 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift @@ -52,6 +52,7 @@ class PickServerSearchCell: UITableViewCell { textField.clearButtonMode = .whileEditing textField.autocapitalizationType = .none textField.autocorrectionType = .no + textField.returnKeyType = .done return textField }() @@ -78,6 +79,7 @@ extension PickServerSearchCell { backgroundColor = Asset.Colors.Background.systemGroupedBackground.color searchTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) + searchTextField.delegate = self contentView.addSubview(bgView) contentView.addSubview(textFieldBgView) @@ -107,3 +109,12 @@ extension PickServerSearchCell { delegate?.pickServerSearchCell(self, searchTextDidChange: textField.text) } } + +// MARK: - UITextFieldDelegate +extension PickServerSearchCell: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return false + } +} From a5938eb0e120b2d7e325db2466c76bf300902486 Mon Sep 17 00:00:00 2001 From: CMK Date: Fri, 4 Jun 2021 18:42:35 +0800 Subject: [PATCH 26/33] chore: update version to 0.6.0 (8) --- Mastodon.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 7d63b303..c127c5b8 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3614,7 +3614,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3641,7 +3641,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 7; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3904,7 +3904,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3927,7 +3927,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 6; + CURRENT_PROJECT_VERSION = 8; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From 02f9a15069d7bb99e90a4d266eb182f1d61b0d33 Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 7 Jun 2021 14:22:03 +0800 Subject: [PATCH 27/33] fix: custom emoji picker using compact default height on the iPad issue --- .../xcschemes/xcschememanagement.plist | 4 ++-- .../Diffiable/Section/ComposeStatusSection.swift | 16 ++++++++-------- .../Scene/Compose/ComposeViewController.swift | 3 ++- .../View/CustomEmojiPickerInputViewModel.swift | 14 +++++++------- 4 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 217fe199..5fbf0684 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 15 + 16 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 16 + 15 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Section/ComposeStatusSection.swift b/Mastodon/Diffiable/Section/ComposeStatusSection.swift index 875a8284..836d91e7 100644 --- a/Mastodon/Diffiable/Section/ComposeStatusSection.swift +++ b/Mastodon/Diffiable/Section/ComposeStatusSection.swift @@ -141,8 +141,8 @@ extension ComposeStatusSection { attribute.contentWarningContent.value = text } .store(in: &cell.disposeBag) - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag) - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.textEditorView, disposeBag: &cell.disposeBag) + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, disposeBag: &cell.disposeBag) return cell case .attachment(let attachmentService): @@ -228,7 +228,7 @@ extension ComposeStatusSection { .assign(to: \.value, on: attribute.option) .store(in: &cell.disposeBag) cell.delegate = composeStatusPollOptionCollectionViewCellDelegate - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplacableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) + ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) return cell case .pollOptionAppendEntry: let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell @@ -295,7 +295,7 @@ protocol CustomEmojiReplaceableTextInput: AnyObject { var isFirstResponder: Bool { get } } -class CustomEmojiReplacableTextInputReference { +class CustomEmojiReplaceableTextInputReference { weak var value: CustomEmojiReplaceableTextInput? init(value: CustomEmojiReplaceableTextInput? = nil) { @@ -320,7 +320,7 @@ extension ComposeStatusSection { static func configureCustomEmojiPicker( viewModel: CustomEmojiPickerInputViewModel?, - customEmojiReplacableTextInput: CustomEmojiReplaceableTextInput, + customEmojiReplaceableTextInput: CustomEmojiReplaceableTextInput, disposeBag: inout Set ) { guard let viewModel = viewModel else { return } @@ -328,9 +328,9 @@ extension ComposeStatusSection { .receive(on: DispatchQueue.main) .sink { [weak viewModel] isCustomEmojiComposing in guard let viewModel = viewModel else { return } - customEmojiReplacableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil - customEmojiReplacableTextInput.reloadInputViews() - viewModel.append(customEmojiReplacableTextInput: customEmojiReplacableTextInput) + customEmojiReplaceableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil + customEmojiReplaceableTextInput.reloadInputViews() + viewModel.append(customEmojiReplaceableTextInput: customEmojiReplaceableTextInput) } .store(in: &disposeBag) } diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 6fb52d2f..595c8743 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -61,7 +61,8 @@ final class ComposeViewController: UIViewController, NeedsDependency { var systemKeyboardHeight: CGFloat = .zero { didSet { // note: some system AutoLayout warning here - customEmojiPickerInputView.frame.size.height = systemKeyboardHeight != .zero ? systemKeyboardHeight : 300 + let height = max(300, systemKeyboardHeight) + customEmojiPickerInputView.frame.size.height = height } } diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift index fec205d6..8314dfc3 100644 --- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift +++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift @@ -12,7 +12,7 @@ final class CustomEmojiPickerInputViewModel { var disposeBag = Set() - private var customEmojiReplacableTextInputReferences: [CustomEmojiReplacableTextInputReference] = [] + private var customEmojiReplaceableTextInputReferences: [CustomEmojiReplaceableTextInputReference] = [] // input weak var customEmojiPickerInputView: CustomEmojiPickerInputView? @@ -25,27 +25,27 @@ final class CustomEmojiPickerInputViewModel { extension CustomEmojiPickerInputViewModel { private func removeEmptyReferences() { - customEmojiReplacableTextInputReferences.removeAll(where: { element in + customEmojiReplaceableTextInputReferences.removeAll(where: { element in element.value == nil }) } - func append(customEmojiReplacableTextInput textInput: CustomEmojiReplaceableTextInput) { + func append(customEmojiReplaceableTextInput textInput: CustomEmojiReplaceableTextInput) { removeEmptyReferences() - let isContains = customEmojiReplacableTextInputReferences.contains(where: { element in + let isContains = customEmojiReplaceableTextInputReferences.contains(where: { element in element.value === textInput }) guard !isContains else { return } - customEmojiReplacableTextInputReferences.append(CustomEmojiReplacableTextInputReference(value: textInput)) + customEmojiReplaceableTextInputReferences.append(CustomEmojiReplaceableTextInputReference(value: textInput)) } - func insertText(_ text: String) -> CustomEmojiReplacableTextInputReference? { + func insertText(_ text: String) -> CustomEmojiReplaceableTextInputReference? { removeEmptyReferences() - for reference in customEmojiReplacableTextInputReferences { + for reference in customEmojiReplaceableTextInputReferences { guard reference.value?.isFirstResponder == true else { continue } reference.value?.insertText(text) return reference From 5983455849dbe33045168a8c53b4e92b1f54428c Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 7 Jun 2021 14:30:42 +0800 Subject: [PATCH 28/33] feat: add keyboard click sound for custom emoji picker --- .../mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist | 4 ++-- Mastodon/Scene/Compose/ComposeViewController.swift | 3 +++ .../Scene/Compose/View/CustomEmojiPickerInputView.swift | 6 ++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 5fbf0684..217fe199 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -12,7 +12,7 @@ CoreDataStack.xcscheme_^#shared#^_ orderHint - 16 + 15 Mastodon - RTL.xcscheme_^#shared#^_ @@ -32,7 +32,7 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 15 + 16 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index 595c8743..90f3e448 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -1031,6 +1031,9 @@ extension ComposeViewController: UICollectionViewDelegate { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.collectionView.collectionViewLayout.invalidateLayout() + + // make click sound + UIDevice.current.playInputClick() } } } else { diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift index 6bfe31d3..2fa58288 100644 --- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift +++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputView.swift @@ -84,3 +84,9 @@ extension CustomEmojiPickerInputView { return layout } } + +extension CustomEmojiPickerInputView: UIInputViewAudioFeedback { + var enableInputClicksWhenVisible: Bool { + return true + } +} From c4e38e76ee0e404cf1c0c0f2e50fd765895e93d9 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Jun 2021 17:05:20 +0800 Subject: [PATCH 29/33] feat: set server picker search text field keyboard type to URL --- .../PickServer/TableViewCell/PickServerSearchCell.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift index 546db989..5d10a39b 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerSearchCell.swift @@ -53,6 +53,7 @@ class PickServerSearchCell: UITableViewCell { textField.autocapitalizationType = .none textField.autocorrectionType = .no textField.returnKeyType = .done + textField.keyboardType = .URL return textField }() From 0b77c1985753630ff2f4f18bee0bf8ee8ad951ec Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Jun 2021 17:26:12 +0800 Subject: [PATCH 30/33] fix: setting scene background color not elevated under Dark Mode issue --- Mastodon/Scene/Settings/SettingsViewController.swift | 11 +++++++++-- .../View/Cell/SettingsAppearanceTableViewCell.swift | 2 +- .../Scene/Settings/View/SettingsSectionHeader.swift | 3 ++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Mastodon/Scene/Settings/SettingsViewController.swift b/Mastodon/Scene/Settings/SettingsViewController.swift index ad85bee8..07705846 100644 --- a/Mastodon/Scene/Settings/SettingsViewController.swift +++ b/Mastodon/Scene/Settings/SettingsViewController.swift @@ -94,7 +94,7 @@ class SettingsViewController: UIViewController, NeedsDependency { tableView.translatesAutoresizingMaskIntoConstraints = false tableView.delegate = self tableView.rowHeight = UITableView.automaticDimension - tableView.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + tableView.backgroundColor = .clear tableView.register(SettingsAppearanceTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsAppearanceTableViewCell.self)) tableView.register(SettingsToggleTableViewCell.self, forCellReuseIdentifier: String(describing: SettingsToggleTableViewCell.self)) @@ -185,7 +185,14 @@ class SettingsViewController: UIViewController, NeedsDependency { } private func setupView() { - view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color + view.backgroundColor = UIColor(dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceLevel { + case .elevated where traitCollection.userInterfaceStyle == .dark: + return Asset.Colors.Background.systemElevatedBackground.color + default: + return Asset.Colors.Background.secondarySystemBackground.color + } + }) setupNavigation() view.addSubview(tableView) diff --git a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift index a58bebf8..9a9ff745 100644 --- a/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift +++ b/Mastodon/Scene/Settings/View/Cell/SettingsAppearanceTableViewCell.swift @@ -176,7 +176,7 @@ class SettingsAppearanceTableViewCell: UITableViewCell { // MARK: Private methods private func setupUI() { - backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + backgroundColor = .clear selectionStyle = .none contentView.addSubview(stackView) diff --git a/Mastodon/Scene/Settings/View/SettingsSectionHeader.swift b/Mastodon/Scene/Settings/View/SettingsSectionHeader.swift index ccd7fd87..0ce45101 100644 --- a/Mastodon/Scene/Settings/View/SettingsSectionHeader.swift +++ b/Mastodon/Scene/Settings/View/SettingsSectionHeader.swift @@ -39,7 +39,8 @@ class SettingsSectionHeader: UIView { init(frame: CGRect, customView: UIView? = nil) { super.init(frame: frame) - backgroundColor = Asset.Colors.Background.systemGroupedBackground.color + backgroundColor = .clear + stackView.addArrangedSubview(titleLabel) if let view = customView { stackView.addArrangedSubview(view) From 96783ae1aab19b6361fbf66368dcd45443db532f Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 8 Jun 2021 17:31:27 +0800 Subject: [PATCH 31/33] fix: compose scene custom emoji button image not set adaptive appearance issue --- Mastodon/Scene/Compose/View/ComposeToolbarView.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift index 68f5eed0..9f2b5e74 100644 --- a/Mastodon/Scene/Compose/View/ComposeToolbarView.swift +++ b/Mastodon/Scene/Compose/View/ComposeToolbarView.swift @@ -218,6 +218,12 @@ extension ComposeToolbarView { } private func updateToolbarButtonUserInterfaceStyle() { + // reset emoji + let emojiButtonImage = Asset.Human.faceSmilingAdaptive.image + .af.imageScaled(to: CGSize(width: 20, height: 20)) + .withRenderingMode(.alwaysTemplate) + emojiButton.setImage(emojiButtonImage, for: .normal) + switch traitCollection.userInterfaceStyle { case .light: mediaButton.setImage(UIImage(systemName: "photo", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))!, for: .normal) From 6e9cd644737da6b8ca3577790074a8ae5708a823 Mon Sep 17 00:00:00 2001 From: CMK Date: Wed, 9 Jun 2021 14:42:58 +0800 Subject: [PATCH 32/33] chore: update version to 0.6.0 (9) --- Mastodon.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index c127c5b8..3aca3eb7 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3614,7 +3614,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3641,7 +3641,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = Mastodon/Info.plist; @@ -3904,7 +3904,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -3927,7 +3927,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 9; DEVELOPMENT_TEAM = 7LFDZ96332; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From 1298131bc206ab9a2409e328e926b5ff73bba85b Mon Sep 17 00:00:00 2001 From: CMK Date: Sat, 12 Jun 2021 04:37:54 +0800 Subject: [PATCH 33/33] chore: update bundle ID. Update version to 0.6.0 (10) --- AppShared/AppName.swift | 2 +- AppShared/AppSecret.swift | 2 +- Mastodon.xcodeproj/project.pbxproj | 48 +++++++++---------- .../xcschemes/xcschememanagement.plist | 6 +-- Mastodon/Activity/SafariActivity.swift | 2 +- Mastodon/Mastodon.entitlements | 2 +- .../ViewModel/MosaicImageViewModel.swift | 2 +- .../ViewModel/VideoPlayerViewModel.swift | 2 +- Mastodon/Service/AudioPlaybackService.swift | 2 +- .../Service/EmojiService/EmojiService.swift | 2 +- .../MastodonAttachmentService.swift | 2 +- Mastodon/Service/NotificationService.swift | 2 +- .../Service/StatusPrefetchingService.swift | 2 +- Mastodon/Service/StatusPublishService.swift | 2 +- Mastodon/Service/ViedeoPlaybackService.swift | 2 +- Mastodon/State/AppContext.swift | 2 +- .../MastodonSDK/Query/SerialStream.swift | 2 +- .../NotificationService.entitlements | 2 +- 18 files changed, 43 insertions(+), 43 deletions(-) diff --git a/AppShared/AppName.swift b/AppShared/AppName.swift index 9dbca78d..e2d35626 100644 --- a/AppShared/AppName.swift +++ b/AppShared/AppName.swift @@ -8,5 +8,5 @@ import Foundation public enum AppName { - public static let groupID = "group.org.joinmastodon.mastodon-temp" + public static let groupID = "group.org.joinmastodon.app" } diff --git a/AppShared/AppSecret.swift b/AppShared/AppSecret.swift index e2305ef1..7ef7a082 100644 --- a/AppShared/AppSecret.swift +++ b/AppShared/AppSecret.swift @@ -13,7 +13,7 @@ import Keys public final class AppSecret { - public static let keychain = Keychain(service: "org.joinmastodon.Mastodon.keychain", accessGroup: AppName.groupID) + public static let keychain = Keychain(service: "org.joinmastodon.app.keychain", accessGroup: AppName.groupID) static let notificationPrivateKeyName = "notification-private-key-base64" static let notificationAuthName = "notification-auth-base64" diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 3aca3eb7..90d2a135 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -3614,16 +3614,16 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.6.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -3641,16 +3641,16 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Mastodon/Mastodon.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "Mastodon/Resources/Preview\\ Assets.xcassets"; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = Mastodon/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); MARKETING_VERSION = 0.6.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; @@ -3664,7 +3664,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3685,7 +3685,7 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3705,7 +3705,7 @@ baseConfigurationReference = 459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3725,7 +3725,7 @@ baseConfigurationReference = BB482D32A7B9825BF5327C4F /* Pods-Mastodon-MastodonUITests.release.xcconfig */; buildSettings = { CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = MastodonUITests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3749,7 +3749,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -3760,7 +3760,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.AppShared; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -3780,7 +3780,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -3791,7 +3791,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.AppShared; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.AppShared; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -3808,7 +3808,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -3837,7 +3837,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -3863,7 +3863,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = CoreDataStackTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3883,7 +3883,7 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = 7LFDZ96332; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = CoreDataStackTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3904,8 +3904,8 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; - DEVELOPMENT_TEAM = 7LFDZ96332; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3913,7 +3913,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 0.6.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -3927,8 +3927,8 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = NotificationService/NotificationService.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; - DEVELOPMENT_TEAM = 7LFDZ96332; + CURRENT_PROJECT_VERSION = 10; + DEVELOPMENT_TEAM = 5Z4GVSS33P; INFOPLIST_FILE = NotificationService/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -3936,7 +3936,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 0.6.0; - PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.Mastodon.NotificationService; + PRODUCT_BUNDLE_IDENTIFIER = org.joinmastodon.app.NotificationService; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 217fe199..6d27be6a 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,7 +7,7 @@ AppShared.xcscheme_^#shared#^_ orderHint - 18 + 14 CoreDataStack.xcscheme_^#shared#^_ @@ -27,12 +27,12 @@ Mastodon.xcscheme_^#shared#^_ orderHint - 1 + 13 NotificationService.xcscheme_^#shared#^_ orderHint - 16 + 12 SuppressBuildableAutocreation diff --git a/Mastodon/Activity/SafariActivity.swift b/Mastodon/Activity/SafariActivity.swift index e10a0b08..a43e34f9 100644 --- a/Mastodon/Activity/SafariActivity.swift +++ b/Mastodon/Activity/SafariActivity.swift @@ -18,7 +18,7 @@ final class SafariActivity: UIActivity { } override var activityType: UIActivity.ActivityType? { - return UIActivity.ActivityType("org.joinmastodon.Mastodon.safari-activity") + return UIActivity.ActivityType("org.joinmastodon.app.safari-activity") } override var activityTitle: String? { diff --git a/Mastodon/Mastodon.entitlements b/Mastodon/Mastodon.entitlements index 8917adbf..0135ecdd 100644 --- a/Mastodon/Mastodon.entitlements +++ b/Mastodon/Mastodon.entitlements @@ -6,7 +6,7 @@ development com.apple.security.application-groups - group.org.joinmastodon.mastodon-temp + group.org.joinmastodon.app diff --git a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift index 9563a19c..7d3dd275 100644 --- a/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift @@ -46,7 +46,7 @@ struct MosaicMeta { let blurhash: String? let altText: String? - let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent) + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.MosaicMeta.working-queue", qos: .userInitiated, attributes: .concurrent) func blurhashImagePublisher() -> AnyPublisher { return Future { promise in diff --git a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift index d34e73eb..6da59aef 100644 --- a/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift +++ b/Mastodon/Scene/Share/ViewModel/VideoPlayerViewModel.swift @@ -14,7 +14,7 @@ import UIKit final class VideoPlayerViewModel { var disposeBag = Set() - static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.video-playback-service.appWillPlayVideo") + static let appWillPlayVideoNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.video-playback-service.appWillPlayVideo") // input let previewImageURL: URL? let videoURL: URL diff --git a/Mastodon/Service/AudioPlaybackService.swift b/Mastodon/Service/AudioPlaybackService.swift index 34ceb3bb..274cd559 100644 --- a/Mastodon/Service/AudioPlaybackService.swift +++ b/Mastodon/Service/AudioPlaybackService.swift @@ -14,7 +14,7 @@ import os.log final class AudioPlaybackService: NSObject { - static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.Mastodon.audio-playback-service.appWillPlayAudio") + static let appWillPlayAudioNotification = NSNotification.Name(rawValue: "org.joinmastodon.app.audio-playback-service.appWillPlayAudio") var disposeBag = Set() diff --git a/Mastodon/Service/EmojiService/EmojiService.swift b/Mastodon/Service/EmojiService/EmojiService.swift index 3883d4ba..a11ebb8b 100644 --- a/Mastodon/Service/EmojiService/EmojiService.swift +++ b/Mastodon/Service/EmojiService/EmojiService.swift @@ -15,7 +15,7 @@ final class EmojiService { weak var apiService: APIService? - let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.EmojiService.working-queue") + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.EmojiService.working-queue") private(set) var customEmojiViewModelDict: [String: CustomEmojiViewModel] = [:] init(apiService: APIService) { diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index b302c083..ede5c64b 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -213,7 +213,7 @@ extension MastodonAttachmentService: Equatable, Hashable { extension MastodonAttachmentService { private static func createWorkingQueue() -> DispatchQueue { - return DispatchQueue(label: "org.joinmastodon.Mastodon.MastodonAttachmentService.\(UUID().uuidString)") + return DispatchQueue(label: "org.joinmastodon.app.MastodonAttachmentService.\(UUID().uuidString)") } static func loadAttachment(url: URL) -> AnyPublisher { diff --git a/Mastodon/Service/NotificationService.swift b/Mastodon/Service/NotificationService.swift index e21a3cff..f1a5bb49 100644 --- a/Mastodon/Service/NotificationService.swift +++ b/Mastodon/Service/NotificationService.swift @@ -17,7 +17,7 @@ final class NotificationService { var disposeBag = Set() - let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.NotificationService.working-queue") + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.NotificationService.working-queue") // input weak var apiService: APIService? diff --git a/Mastodon/Service/StatusPrefetchingService.swift b/Mastodon/Service/StatusPrefetchingService.swift index e1337204..7828c5cf 100644 --- a/Mastodon/Service/StatusPrefetchingService.swift +++ b/Mastodon/Service/StatusPrefetchingService.swift @@ -16,7 +16,7 @@ final class StatusPrefetchingService { typealias TaskID = String - let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.StatusPrefetchingService.working-queue") + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPrefetchingService.working-queue") var disposeBag = Set() private(set) var statusPrefetchingDisposeBagDict: [TaskID: AnyCancellable] = [:] diff --git a/Mastodon/Service/StatusPublishService.swift b/Mastodon/Service/StatusPublishService.swift index 4728af8c..f797d9f6 100644 --- a/Mastodon/Service/StatusPublishService.swift +++ b/Mastodon/Service/StatusPublishService.swift @@ -16,7 +16,7 @@ final class StatusPublishService { var disposeBag = Set() - let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.StatusPublishService.working-queue") + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.StatusPublishService.working-queue") // input var viewModels = CurrentValueSubject<[ComposeViewModel], Never>([]) // use strong reference to retain the view models diff --git a/Mastodon/Service/ViedeoPlaybackService.swift b/Mastodon/Service/ViedeoPlaybackService.swift index 523af310..a15431f0 100644 --- a/Mastodon/Service/ViedeoPlaybackService.swift +++ b/Mastodon/Service/ViedeoPlaybackService.swift @@ -14,7 +14,7 @@ import os.log final class VideoPlaybackService { var disposeBag = Set() - let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.VideoPlaybackService.working-queue") + let workingQueue = DispatchQueue(label: "org.joinmastodon.app.VideoPlaybackService.working-queue") private(set) var viewPlayerViewModelDict: [URL: VideoPlayerViewModel] = [:] // only for video kind diff --git a/Mastodon/State/AppContext.swift b/Mastodon/State/AppContext.swift index 8c0fa364..89771dfb 100644 --- a/Mastodon/State/AppContext.swift +++ b/Mastodon/State/AppContext.swift @@ -111,7 +111,7 @@ extension AppContext { return formatter }() - private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.AppContext.purgeCacheWorkingQueue") + private static let purgeCacheWorkingQueue = DispatchQueue(label: "org.joinmastodon.app.AppContext.purgeCacheWorkingQueue") func purgeCache() -> AnyPublisher { Publishers.MergeMany([ diff --git a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift index ad86033e..cde09b71 100644 --- a/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift +++ b/MastodonSDK/Sources/MastodonSDK/Query/SerialStream.swift @@ -26,7 +26,7 @@ final class SerialStream: NSObject { private var buffer: UnsafeMutablePointer private var canWrite = false - private let workingQueue = DispatchQueue(label: "org.joinmastodon.Mastodon.SerialStream.\(UUID().uuidString)") + private let workingQueue = DispatchQueue(label: "org.joinmastodon.app.SerialStream.\(UUID().uuidString)") // bound pair stream private(set) lazy var boundStreams: Streams = { diff --git a/NotificationService/NotificationService.entitlements b/NotificationService/NotificationService.entitlements index d334a5e6..c3bc3f81 100644 --- a/NotificationService/NotificationService.entitlements +++ b/NotificationService/NotificationService.entitlements @@ -4,7 +4,7 @@ com.apple.security.application-groups - group.org.joinmastodon.mastodon-temp + group.org.joinmastodon.app