Merge pull request #139 from tootsuite/feature/profile-fields
Add profile fields display
This commit is contained in:
commit
680f8509dd
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E232" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="18154" systemVersion="20E241" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
|
@ -102,6 +102,7 @@
|
|||
<attribute name="displayName" attributeType="String"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
<attribute name="emojisData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="fieldsData" optional="YES" attributeType="Binary"/>
|
||||
<attribute name="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="header" attributeType="String"/>
|
||||
|
@ -273,7 +274,7 @@
|
|||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="689"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="704"/>
|
||||
<element name="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -404,6 +404,13 @@
|
|||
"count_followers": "%ld followers"
|
||||
}
|
||||
},
|
||||
"fields": {
|
||||
"add_row": "Add Row",
|
||||
"placeholder": {
|
||||
"label": "Label",
|
||||
"content": "Content"
|
||||
}
|
||||
},
|
||||
"segmented_control": {
|
||||
"posts": "Posts",
|
||||
"replies": "Replies",
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewController.swift; sourceTree = "<group>"; };
|
||||
DBA5E7AA263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTableViewCellContextMenuConfiguration.swift; sourceTree = "<group>"; };
|
||||
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldSection.swift; sourceTree = "<group>"; };
|
||||
DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = "<group>"; };
|
||||
DBA94437265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DBA94439265CC0FC00C537E1 /* Fields.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fields.swift; sourceTree = "<group>"; };
|
||||
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = "<group>"; };
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = "<group>"; };
|
||||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1019,6 +1033,8 @@
|
|||
DBF8AE15263293E400C9C23C /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = "<group>"; };
|
||||
DBF8AE17263293E400C9C23C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldAddEntryCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -1402,6 +1418,7 @@
|
|||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
||||
);
|
||||
path = Section;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1463,6 +1480,7 @@
|
|||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
||||
DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */,
|
||||
);
|
||||
path = Item;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1619,6 +1637,7 @@
|
|||
DB6D9F4826353FD6008423CD /* Subscription.swift */,
|
||||
DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */,
|
||||
DBAFB7342645463500371D5F /* Emojis.swift */,
|
||||
DBA94439265CC0FC00C537E1 /* Fields.swift */,
|
||||
);
|
||||
path = CoreDataStack;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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 = "<group>";
|
||||
|
@ -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 */,
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>15</integer>
|
||||
<integer>14</integer>
|
||||
</dict>
|
||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>14</integer>
|
||||
<integer>15</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -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<String, Never>
|
||||
var value: CurrentValueSubject<String, Never>
|
||||
|
||||
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<MastodonStatusContent.EmojiDict, Never>([:])
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// 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<ProfileFieldSection, ProfileFieldItem> {
|
||||
let dataSource = UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>(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
|
||||
|
||||
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
|
||||
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()
|
||||
)
|
||||
.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.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
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -16,7 +16,8 @@ extension ActiveLabel {
|
|||
case `default`
|
||||
case statusHeader
|
||||
case statusName
|
||||
case profileField
|
||||
case profileFieldName
|
||||
case profileFieldValue
|
||||
}
|
||||
|
||||
convenience init(style: Style) {
|
||||
|
@ -46,8 +47,12 @@ extension ActiveLabel {
|
|||
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
textColor = Asset.Colors.Label.primary.color
|
||||
numberOfLines = 1
|
||||
case .profileField:
|
||||
font = .preferredFont(forTextStyle: .body)
|
||||
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
|
||||
}
|
||||
|
@ -78,10 +83,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
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 { }
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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 %@
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 %@";
|
||||
|
|
|
@ -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 %@";
|
||||
|
|
|
@ -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 {
|
||||
|
@ -290,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)
|
||||
|
||||
|
@ -336,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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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<AnyCancellable>()
|
||||
|
||||
// input
|
||||
|
@ -20,11 +23,13 @@ final class ProfileHeaderViewModel {
|
|||
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
||||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||
|
||||
// output
|
||||
let displayProfileInfo = ProfileInfo()
|
||||
let editProfileInfo = ProfileInfo()
|
||||
let isTitleViewDisplaying = CurrentValueSubject<Bool, Never>(false)
|
||||
var fieldDiffableDataSource: UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>!
|
||||
|
||||
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<ProfileFieldSection, ProfileFieldItem>()
|
||||
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<String?, Never>(nil)
|
||||
let avatarImageResource = CurrentValueSubject<ImageResource?, Never>(nil)
|
||||
let note = CurrentValueSubject<String?, Never>(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,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 }
|
||||
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
|
||||
}
|
||||
|
@ -95,6 +184,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 +197,7 @@ extension ProfileHeaderViewModel {
|
|||
header: nil,
|
||||
locked: nil,
|
||||
source: nil,
|
||||
fieldsAttributes: nil // TODO:
|
||||
fieldsAttributes: fieldsAttributes
|
||||
)
|
||||
return context.apiService.accountUpdateCredentials(
|
||||
domain: domain,
|
||||
|
|
|
@ -0,0 +1,178 @@
|
|||
//
|
||||
// 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<AnyCancellable>()
|
||||
|
||||
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 separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
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.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)
|
||||
|
||||
editButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
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: contentView.readableContentGuide.trailingAnchor)
|
||||
|
||||
addSubview(bottomSeparatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToMarginLeadingLayoutConstraint,
|
||||
bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
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(_:)))
|
||||
|
||||
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
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
//
|
||||
// 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<AnyCancellable>()
|
||||
|
||||
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 separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
|
||||
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.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)
|
||||
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
|
||||
separatorLineToMarginLeadingLayoutConstraint = bottomSeparatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
|
||||
separatorLineToEdgeTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
||||
separatorLineToMarginTrailingLayoutConstraint = bottomSeparatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor)
|
||||
|
||||
addSubview(bottomSeparatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLineToMarginLeadingLayoutConstraint,
|
||||
bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
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
|
||||
|
|
@ -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),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -6,26 +6,50 @@
|
|||
//
|
||||
|
||||
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"
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// output
|
||||
let name = PassthroughSubject<String, Never>()
|
||||
let value = PassthroughSubject<String, Never>()
|
||||
|
||||
// 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)
|
||||
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)
|
||||
let label = ActiveLabel(style: .profileFieldValue)
|
||||
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 +65,67 @@ 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)
|
||||
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([
|
||||
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),
|
||||
])
|
||||
titleTextField.isHidden = true
|
||||
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 +137,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))
|
||||
|
|
|
@ -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,37 @@ 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)
|
||||
// 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)
|
||||
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 +194,10 @@ final class ProfileHeaderView: UIView {
|
|||
_init()
|
||||
}
|
||||
|
||||
deinit {
|
||||
fieldCollectionViewHeightObservation = nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileHeaderView {
|
||||
|
@ -193,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([
|
||||
|
@ -328,8 +364,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)
|
||||
|
|
|
@ -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)
|
||||
|
@ -640,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)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -664,6 +677,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 +878,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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ class ProfileViewModel: NSObject {
|
|||
let statusesCount: CurrentValueSubject<Int?, Never>
|
||||
let followingCount: CurrentValueSubject<Int?, Never>
|
||||
let followersCount: CurrentValueSubject<Int?, Never>
|
||||
let fileds: CurrentValueSubject<[Mastodon.Entity.Field], Never>
|
||||
let emojiDict: CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>
|
||||
|
||||
let protected: CurrentValueSubject<Bool?, Never>
|
||||
let suspended: CurrentValueSubject<Bool, Never>
|
||||
|
@ -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?) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue