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"?>
|
<?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">
|
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
|
@ -102,6 +102,7 @@
|
||||||
<attribute name="displayName" attributeType="String"/>
|
<attribute name="displayName" attributeType="String"/>
|
||||||
<attribute name="domain" attributeType="String"/>
|
<attribute name="domain" attributeType="String"/>
|
||||||
<attribute name="emojisData" optional="YES" attributeType="Binary"/>
|
<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="followersCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="followingCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="header" attributeType="String"/>
|
<attribute name="header" attributeType="String"/>
|
||||||
|
@ -273,7 +274,7 @@
|
||||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||||
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
<element name="MastodonAuthentication" positionX="0" positionY="0" width="128" height="209"/>
|
||||||
<element name="MastodonNotification" positionX="9" positionY="162" width="128" height="164"/>
|
<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="Mention" positionX="0" positionY="0" width="128" height="149"/>
|
||||||
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
<element name="Poll" positionX="0" positionY="0" width="128" height="194"/>
|
||||||
<element name="PollOption" positionX="0" positionY="0" width="128" height="134"/>
|
<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 url: String?
|
||||||
|
|
||||||
@NSManaged public private(set) var emojisData: Data?
|
@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 statusesCount: NSNumber
|
||||||
@NSManaged public private(set) var followingCount: NSNumber
|
@NSManaged public private(set) var followingCount: NSNumber
|
||||||
|
@ -92,6 +93,7 @@ extension MastodonUser {
|
||||||
user.note = property.note
|
user.note = property.note
|
||||||
user.url = property.url
|
user.url = property.url
|
||||||
user.emojisData = property.emojisData
|
user.emojisData = property.emojisData
|
||||||
|
user.fieldsData = property.fieldsData
|
||||||
|
|
||||||
user.statusesCount = NSNumber(value: property.statusesCount)
|
user.statusesCount = NSNumber(value: property.statusesCount)
|
||||||
user.followingCount = NSNumber(value: property.followingCount)
|
user.followingCount = NSNumber(value: property.followingCount)
|
||||||
|
@ -161,6 +163,11 @@ extension MastodonUser {
|
||||||
self.emojisData = emojisData
|
self.emojisData = emojisData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public func update(fieldsData: Data?) {
|
||||||
|
if self.fieldsData != fieldsData {
|
||||||
|
self.fieldsData = fieldsData
|
||||||
|
}
|
||||||
|
}
|
||||||
public func update(statusesCount: Int) {
|
public func update(statusesCount: Int) {
|
||||||
if self.statusesCount.intValue != statusesCount {
|
if self.statusesCount.intValue != statusesCount {
|
||||||
self.statusesCount = NSNumber(value: statusesCount)
|
self.statusesCount = NSNumber(value: statusesCount)
|
||||||
|
@ -281,6 +288,7 @@ extension MastodonUser {
|
||||||
public let note: String?
|
public let note: String?
|
||||||
public let url: String?
|
public let url: String?
|
||||||
public let emojisData: Data?
|
public let emojisData: Data?
|
||||||
|
public let fieldsData: Data?
|
||||||
public let statusesCount: Int
|
public let statusesCount: Int
|
||||||
public let followingCount: Int
|
public let followingCount: Int
|
||||||
public let followersCount: Int
|
public let followersCount: Int
|
||||||
|
@ -304,6 +312,7 @@ extension MastodonUser {
|
||||||
note: String?,
|
note: String?,
|
||||||
url: String?,
|
url: String?,
|
||||||
emojisData: Data?,
|
emojisData: Data?,
|
||||||
|
fieldsData: Data?,
|
||||||
statusesCount: Int,
|
statusesCount: Int,
|
||||||
followingCount: Int,
|
followingCount: Int,
|
||||||
followersCount: Int,
|
followersCount: Int,
|
||||||
|
@ -326,6 +335,7 @@ extension MastodonUser {
|
||||||
self.note = note
|
self.note = note
|
||||||
self.url = url
|
self.url = url
|
||||||
self.emojisData = emojisData
|
self.emojisData = emojisData
|
||||||
|
self.fieldsData = fieldsData
|
||||||
self.statusesCount = statusesCount
|
self.statusesCount = statusesCount
|
||||||
self.followingCount = followingCount
|
self.followingCount = followingCount
|
||||||
self.followersCount = followersCount
|
self.followersCount = followersCount
|
||||||
|
|
|
@ -404,6 +404,13 @@
|
||||||
"count_followers": "%ld followers"
|
"count_followers": "%ld followers"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"fields": {
|
||||||
|
"add_row": "Add Row",
|
||||||
|
"placeholder": {
|
||||||
|
"label": "Label",
|
||||||
|
"content": "Content"
|
||||||
|
}
|
||||||
|
},
|
||||||
"segmented_control": {
|
"segmented_control": {
|
||||||
"posts": "Posts",
|
"posts": "Posts",
|
||||||
"replies": "Replies",
|
"replies": "Replies",
|
||||||
|
|
|
@ -401,6 +401,12 @@
|
||||||
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */; };
|
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */; };
|
||||||
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */; };
|
DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7A8263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift */; };
|
||||||
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA5E7AA263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.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 */; };
|
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.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 */; };
|
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, ); }; };
|
DBF8AE1A263293E400C9C23C /* NotificationService.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = DBF8AE13263293E400C9C23C /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; };
|
DBF8AE862632992800C9C23C /* Base85 in Frameworks */ = {isa = PBXBuildFile; productRef = DBF8AE852632992800C9C23C /* Base85 */; };
|
||||||
DBF96326262EC0A6001D8D25 /* AuthenticationServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DBF96325262EC0A6001D8D25 /* AuthenticationServices.framework */; };
|
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 */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
@ -961,6 +969,12 @@
|
||||||
DBA5E7A4263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuImagePreviewViewModel.swift; sourceTree = "<group>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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 */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -1402,6 +1418,7 @@
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||||
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||||
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */,
|
||||||
|
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */,
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1463,6 +1480,7 @@
|
||||||
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */,
|
||||||
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
DB6D9F8326358EEC008423CD /* SettingsItem.swift */,
|
||||||
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */,
|
||||||
|
DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */,
|
||||||
);
|
);
|
||||||
path = Item;
|
path = Item;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1619,6 +1637,7 @@
|
||||||
DB6D9F4826353FD6008423CD /* Subscription.swift */,
|
DB6D9F4826353FD6008423CD /* Subscription.swift */,
|
||||||
DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */,
|
DB6D9F4F2635761F008423CD /* SubscriptionAlerts.swift */,
|
||||||
DBAFB7342645463500371D5F /* Emojis.swift */,
|
DBAFB7342645463500371D5F /* Emojis.swift */,
|
||||||
|
DBA94439265CC0FC00C537E1 /* Fields.swift */,
|
||||||
);
|
);
|
||||||
path = CoreDataStack;
|
path = CoreDataStack;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1913,6 +1932,7 @@
|
||||||
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */,
|
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */,
|
||||||
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */,
|
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */,
|
||||||
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */,
|
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */,
|
||||||
|
DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */,
|
||||||
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */,
|
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */,
|
||||||
);
|
);
|
||||||
path = MastodonSDK;
|
path = MastodonSDK;
|
||||||
|
@ -2345,6 +2365,7 @@
|
||||||
DBB525732612D5A5002F1F29 /* View */,
|
DBB525732612D5A5002F1F29 /* View */,
|
||||||
DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */,
|
DBB525402611ED54002F1F29 /* ProfileHeaderViewController.swift */,
|
||||||
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */,
|
DB7F48442620241000796008 /* ProfileHeaderViewModel.swift */,
|
||||||
|
DBA94437265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift */,
|
||||||
);
|
);
|
||||||
path = Header;
|
path = Header;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2357,6 +2378,9 @@
|
||||||
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */,
|
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */,
|
||||||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */,
|
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */,
|
||||||
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */,
|
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */,
|
||||||
|
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */,
|
||||||
|
DBF9814B265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift */,
|
||||||
|
DBF98149265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift */,
|
||||||
);
|
);
|
||||||
path = View;
|
path = View;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -2921,6 +2945,7 @@
|
||||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||||
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */,
|
||||||
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
DB45FB1D25CA9D23005A8AC7 /* APIService+HomeTimeline.swift in Sources */,
|
||||||
|
DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */,
|
||||||
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
DB49A61425FF2C5600B98345 /* EmojiService.swift in Sources */,
|
||||||
DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */,
|
DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */,
|
||||||
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
DBCC3B9526157E6E0045B23D /* APIService+Relationship.swift in Sources */,
|
||||||
|
@ -2992,6 +3017,7 @@
|
||||||
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */,
|
||||||
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */,
|
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */,
|
||||||
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
DBB5256E2612D5A1002F1F29 /* ProfileStatusDashboardView.swift in Sources */,
|
||||||
|
DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */,
|
||||||
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */,
|
||||||
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */,
|
||||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */,
|
||||||
|
@ -3011,6 +3037,7 @@
|
||||||
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */,
|
||||||
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
|
DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */,
|
||||||
DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */,
|
DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */,
|
||||||
|
DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */,
|
||||||
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
2D939AB525EDD8A90076FA61 /* String.swift in Sources */,
|
||||||
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */,
|
||||||
2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */,
|
2D35238126256F690031AF25 /* NotificationTableViewCell.swift in Sources */,
|
||||||
|
@ -3045,6 +3072,7 @@
|
||||||
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
|
5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */,
|
||||||
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
DB59F11825EFA35B001F1DAB /* StripProgressView.swift in Sources */,
|
||||||
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
DB59F10425EF5EBC001F1DAB /* TableViewCellHeightCacheableContainer.swift in Sources */,
|
||||||
|
DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */,
|
||||||
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
|
DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */,
|
||||||
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */,
|
||||||
DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */,
|
DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */,
|
||||||
|
@ -3059,6 +3087,7 @@
|
||||||
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */,
|
||||||
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
2D82B9FF25E7863200E36F0F /* OnboardingViewControllerAppearance.swift in Sources */,
|
||||||
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */,
|
||||||
|
DBA9443A265CC0FC00C537E1 /* Fields.swift in Sources */,
|
||||||
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
2DE0FAC12615F04D00CDF649 /* RecommendHashTagSection.swift in Sources */,
|
||||||
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
|
DBA5E7A5263BD28C004598BB /* ContextMenuImagePreviewViewModel.swift in Sources */,
|
||||||
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
2D38F1E525CD46C100561493 /* HomeTimelineViewModel.swift in Sources */,
|
||||||
|
@ -3115,6 +3144,7 @@
|
||||||
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
2D084B8D26258EA3003AA3AF /* NotificationViewModel+diffable.swift in Sources */,
|
||||||
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
DB6D1B24263684C600ACB481 /* UserDefaults.swift in Sources */,
|
||||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */,
|
||||||
|
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||||
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
DB6F5E2F264E5518009108F4 /* MastodonRegex.swift in Sources */,
|
||||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||||
|
@ -3152,6 +3182,7 @@
|
||||||
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */,
|
||||||
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */,
|
||||||
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */,
|
DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */,
|
||||||
|
DBA94438265CBD4D00C537E1 /* ProfileHeaderViewModel+Diffable.swift in Sources */,
|
||||||
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
0F20220D26134E3F000C64BF /* HashtagTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */,
|
||||||
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */,
|
||||||
|
@ -3261,6 +3292,7 @@
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
||||||
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
||||||
|
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
|
||||||
DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */,
|
DB938F25262438D600E5B6C1 /* ThreadViewController+Provider.swift in Sources */,
|
||||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||||
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>15</integer>
|
<integer>14</integer>
|
||||||
</dict>
|
</dict>
|
||||||
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
<key>Mastodon - RTL.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<key>NotificationService.xcscheme_^#shared#^_</key>
|
<key>NotificationService.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>14</integer>
|
<integer>15</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<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 {
|
switch item {
|
||||||
case .replyTo(let replyToStatusObjectID):
|
case .replyTo(let replyToStatusObjectID):
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeRepliedToStatusContentCollectionViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentCollectionViewCell
|
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 {
|
guard let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -82,7 +85,7 @@ extension ComposeStatusSection {
|
||||||
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusContentCollectionViewCell.self), for: indexPath) as! ComposeStatusContentCollectionViewCell
|
||||||
cell.statusContentWarningEditorView.textView.text = attribute.contentWarningContent.value
|
cell.statusContentWarningEditorView.textView.text = attribute.contentWarningContent.value
|
||||||
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
cell.textEditorView.text = attribute.composeContent.value ?? ""
|
||||||
managedObjectContext.perform {
|
managedObjectContext.performAndWait {
|
||||||
guard let replyToStatusObjectID = replyToStatusObjectID,
|
guard let replyToStatusObjectID = replyToStatusObjectID,
|
||||||
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
let replyTo = managedObjectContext.object(with: replyToStatusObjectID) as? Status else {
|
||||||
cell.statusView.headerContainerView.isHidden = true
|
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 `default`
|
||||||
case statusHeader
|
case statusHeader
|
||||||
case statusName
|
case statusName
|
||||||
case profileField
|
case profileFieldName
|
||||||
|
case profileFieldValue
|
||||||
}
|
}
|
||||||
|
|
||||||
convenience init(style: Style) {
|
convenience init(style: Style) {
|
||||||
|
@ -46,8 +47,12 @@ extension ActiveLabel {
|
||||||
font = .systemFont(ofSize: 17, weight: .semibold)
|
font = .systemFont(ofSize: 17, weight: .semibold)
|
||||||
textColor = Asset.Colors.Label.primary.color
|
textColor = Asset.Colors.Label.primary.color
|
||||||
numberOfLines = 1
|
numberOfLines = 1
|
||||||
case .profileField:
|
case .profileFieldName:
|
||||||
font = .preferredFont(forTextStyle: .body)
|
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
|
textColor = Asset.Colors.Label.primary.color
|
||||||
numberOfLines = 1
|
numberOfLines = 1
|
||||||
}
|
}
|
||||||
|
@ -78,10 +83,10 @@ extension ActiveLabel {
|
||||||
|
|
||||||
extension ActiveLabel {
|
extension ActiveLabel {
|
||||||
/// account field
|
/// account field
|
||||||
func configure(field: String) {
|
func configure(field: String, emojiDict: MastodonStatusContent.EmojiDict) {
|
||||||
activeEntities.removeAll()
|
activeEntities.removeAll()
|
||||||
let parseResult = MastodonField.parse(field: field)
|
let parseResult = MastodonField.parse(field: field, emojiDict: emojiDict)
|
||||||
text = parseResult.value
|
text = parseResult.trimmed
|
||||||
activeEntities = parseResult.activeEntities
|
activeEntities = parseResult.activeEntities
|
||||||
accessibilityLabel = parseResult.value
|
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,
|
headerStatic: entity.headerStatic,
|
||||||
note: entity.note,
|
note: entity.note,
|
||||||
url: entity.url,
|
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,
|
statusesCount: entity.statusesCount,
|
||||||
followingCount: entity.followingCount,
|
followingCount: entity.followingCount,
|
||||||
followersCount: entity.followersCount,
|
followersCount: entity.followersCount,
|
||||||
|
@ -101,3 +102,4 @@ extension MastodonUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MastodonUser: EmojiContinaer { }
|
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 RelationshipActionAlert {
|
||||||
internal enum ConfirmUnblockUsre {
|
internal enum ConfirmUnblockUsre {
|
||||||
/// Confirm unblock %@
|
/// Confirm unblock %@
|
||||||
|
|
|
@ -10,12 +10,25 @@ import ActiveLabel
|
||||||
|
|
||||||
enum MastodonField {
|
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 mentionMatches = string.matches(pattern: "(?:@([a-zA-Z0-9_]+)(@[a-zA-Z0-9_.-]+)?)")
|
||||||
let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
|
let hashtagMatches = string.matches(pattern: "(?:#([^\\s.]+))")
|
||||||
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
let urlMatches = string.matches(pattern: "(?i)https?://\\S+(?:/|\\b)")
|
||||||
|
|
||||||
var entities: [ActiveEntity] = []
|
|
||||||
|
|
||||||
for match in mentionMatches {
|
for match in mentionMatches {
|
||||||
guard let text = string.substring(with: match, at: 0) else { continue }
|
guard let text = string.substring(with: match, at: 0) else { continue }
|
||||||
|
@ -35,7 +48,7 @@ enum MastodonField {
|
||||||
entities.append(entity)
|
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 {
|
extension MastodonField {
|
||||||
struct ParseResult {
|
struct ParseResult {
|
||||||
let value: String
|
let value: String
|
||||||
|
let trimmed: String
|
||||||
let activeEntities: [ActiveEntity]
|
let activeEntities: [ActiveEntity]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ enum MastodonStatusContent {
|
||||||
let pattern = ":\(shortcode):"
|
let pattern = ":\(shortcode):"
|
||||||
content = content.replacingOccurrences(of: pattern, with: emojiNode)
|
content = content.replacingOccurrences(of: pattern, with: emojiNode)
|
||||||
}
|
}
|
||||||
return content
|
return content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}()
|
}()
|
||||||
let rootNode = try Node.parse(document: document)
|
let rootNode = try Node.parse(document: document)
|
||||||
let text = String(rootNode.text)
|
let text = String(rootNode.text)
|
||||||
|
|
|
@ -212,6 +212,9 @@ tap the link to confirm your account.";
|
||||||
"Scene.Profile.Dashboard.Followers" = "followers";
|
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||||
"Scene.Profile.Dashboard.Following" = "following";
|
"Scene.Profile.Dashboard.Following" = "following";
|
||||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
"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.Message" = "Confirm unblock %@";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
|
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@";
|
"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.Followers" = "followers";
|
||||||
"Scene.Profile.Dashboard.Following" = "following";
|
"Scene.Profile.Dashboard.Following" = "following";
|
||||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
"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.Message" = "Confirm unblock %@";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
|
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
|
||||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@";
|
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@";
|
||||||
|
|
|
@ -9,6 +9,7 @@ import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import PhotosUI
|
import PhotosUI
|
||||||
|
import ActiveLabel
|
||||||
import AlamofireImage
|
import AlamofireImage
|
||||||
import CropViewController
|
import CropViewController
|
||||||
import TwitterTextEditor
|
import TwitterTextEditor
|
||||||
|
@ -16,6 +17,7 @@ import TwitterTextEditor
|
||||||
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
protocol ProfileHeaderViewControllerDelegate: AnyObject {
|
||||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, viewLayoutDidUpdate view: UIView)
|
||||||
func profileHeaderViewController(_ viewController: ProfileHeaderViewController, pageSegmentedControlValueChanged segmentedControl: UISegmentedControl, selectedSegmentIndex index: Int)
|
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 {
|
final class ProfileHeaderViewController: UIViewController {
|
||||||
|
@ -96,6 +98,15 @@ extension ProfileHeaderViewController {
|
||||||
])
|
])
|
||||||
profileHeaderView.preservesSuperviewLayoutMargins = true
|
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
|
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(pageSegmentedControl)
|
view.addSubview(pageSegmentedControl)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -190,6 +201,25 @@ extension ProfileHeaderViewController {
|
||||||
}
|
}
|
||||||
.store(in: &disposeBag)
|
.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.menu = createAvatarContextMenu()
|
||||||
profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true
|
profileHeaderView.editAvatarButton.showsMenuAsPrimaryAction = true
|
||||||
}
|
}
|
||||||
|
@ -265,6 +295,48 @@ extension ProfileHeaderViewController {
|
||||||
delegate?.profileHeaderViewController(self, pageSegmentedControlValueChanged: sender, selectedSegmentIndex: sender.selectedSegmentIndex)
|
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 {
|
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)
|
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
||||||
updateHeaderBottomShadow(progress: progress)
|
updateHeaderBottomShadow(progress: progress)
|
||||||
|
|
||||||
|
@ -336,12 +408,12 @@ extension ProfileHeaderViewController {
|
||||||
viewModel.isTitleViewContentOffsetSet.value = true
|
viewModel.isTitleViewContentOffsetSet.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// set avatar
|
// set avatar fade
|
||||||
if progress > 0 {
|
if progress > 0 {
|
||||||
setProfileBannerFade(alpha: 0)
|
setProfileBannerFade(alpha: 0)
|
||||||
} else if progress > -0.3 {
|
} else if progress > -abs(throttle) {
|
||||||
// y = -(10/3)x
|
// y = -(1/0.8T)x
|
||||||
let alpha = -10.0 / 3.0 * progress
|
let alpha = -1 / abs(0.8 * throttle) * progress
|
||||||
setProfileBannerFade(alpha: alpha)
|
setProfileBannerFade(alpha: alpha)
|
||||||
} else {
|
} else {
|
||||||
setProfileBannerFade(alpha: 1)
|
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.
|
// Created by MainasuK Cirno on 2021-4-9.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
import UIKit
|
import UIKit
|
||||||
import Combine
|
import Combine
|
||||||
import Kanna
|
import Kanna
|
||||||
|
@ -12,6 +13,8 @@ import MastodonSDK
|
||||||
|
|
||||||
final class ProfileHeaderViewModel {
|
final class ProfileHeaderViewModel {
|
||||||
|
|
||||||
|
static let maxProfileFieldCount = 4
|
||||||
|
|
||||||
var disposeBag = Set<AnyCancellable>()
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
|
||||||
// input
|
// input
|
||||||
|
@ -20,11 +23,13 @@ final class ProfileHeaderViewModel {
|
||||||
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
let viewDidAppear = CurrentValueSubject<Bool, Never>(false)
|
||||||
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
let needsSetupBottomShadow = CurrentValueSubject<Bool, Never>(true)
|
||||||
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
let isTitleViewContentOffsetSet = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let emojiDict = CurrentValueSubject<MastodonStatusContent.EmojiDict, Never>([:])
|
||||||
|
|
||||||
// output
|
// output
|
||||||
let displayProfileInfo = ProfileInfo()
|
let displayProfileInfo = ProfileInfo()
|
||||||
let editProfileInfo = ProfileInfo()
|
let editProfileInfo = ProfileInfo()
|
||||||
let isTitleViewDisplaying = CurrentValueSubject<Bool, Never>(false)
|
let isTitleViewDisplaying = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
var fieldDiffableDataSource: UICollectionViewDiffableDataSource<ProfileFieldSection, ProfileFieldItem>!
|
||||||
|
|
||||||
init(context: AppContext) {
|
init(context: AppContext) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
@ -38,6 +43,59 @@ final class ProfileHeaderViewModel {
|
||||||
self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name
|
self.editProfileInfo.name.value = self.displayProfileInfo.name.value // set to name
|
||||||
self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty
|
self.editProfileInfo.avatarImageResource.value = .image(nil) // set to empty
|
||||||
self.editProfileInfo.note.value = ProfileHeaderViewModel.normalize(note: self.displayProfileInfo.note.value)
|
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)
|
.store(in: &disposeBag)
|
||||||
}
|
}
|
||||||
|
@ -49,6 +107,7 @@ extension ProfileHeaderViewModel {
|
||||||
let name = CurrentValueSubject<String?, Never>(nil)
|
let name = CurrentValueSubject<String?, Never>(nil)
|
||||||
let avatarImageResource = CurrentValueSubject<ImageResource?, Never>(nil)
|
let avatarImageResource = CurrentValueSubject<ImageResource?, Never>(nil)
|
||||||
let note = CurrentValueSubject<String?, Never>(nil)
|
let note = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let fields = CurrentValueSubject<[ProfileFieldItem.FieldValue], Never>([])
|
||||||
|
|
||||||
enum ImageResource {
|
enum ImageResource {
|
||||||
case url(URL?)
|
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 {
|
extension ProfileHeaderViewModel {
|
||||||
|
|
||||||
static func normalize(note: String?) -> String? {
|
static func normalize(note: String?) -> String? {
|
||||||
|
@ -75,6 +151,19 @@ extension ProfileHeaderViewModel {
|
||||||
guard editProfileInfo.name.value == displayProfileInfo.name.value else { return true }
|
guard editProfileInfo.name.value == displayProfileInfo.name.value else { return true }
|
||||||
guard case let .image(image) = editProfileInfo.avatarImageResource.value, image == nil else { return true }
|
guard case let .image(image) = editProfileInfo.avatarImageResource.value, image == nil else { return true }
|
||||||
guard editProfileInfo.note.value == ProfileHeaderViewModel.normalize(note: displayProfileInfo.note.value) else { return true }
|
guard editProfileInfo.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
|
return false
|
||||||
}
|
}
|
||||||
|
@ -95,6 +184,10 @@ extension ProfileHeaderViewModel {
|
||||||
return image
|
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(
|
let query = Mastodon.API.Account.UpdateCredentialQuery(
|
||||||
discoverable: nil,
|
discoverable: nil,
|
||||||
bot: nil,
|
bot: nil,
|
||||||
|
@ -104,7 +197,7 @@ extension ProfileHeaderViewModel {
|
||||||
header: nil,
|
header: nil,
|
||||||
locked: nil,
|
locked: nil,
|
||||||
source: nil,
|
source: nil,
|
||||||
fieldsAttributes: nil // TODO:
|
fieldsAttributes: fieldsAttributes
|
||||||
)
|
)
|
||||||
return context.apiService.accountUpdateCredentials(
|
return context.apiService.accountUpdateCredentials(
|
||||||
domain: domain,
|
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 UIKit
|
||||||
|
import Combine
|
||||||
import ActiveLabel
|
import ActiveLabel
|
||||||
|
|
||||||
final class ProfileFieldView: UIView {
|
final class ProfileFieldView: UIView {
|
||||||
|
|
||||||
let titleLabel: UILabel = {
|
var disposeBag = Set<AnyCancellable>()
|
||||||
let label = UILabel()
|
|
||||||
label.font = UIFontMetrics(forTextStyle: .headline).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold))
|
// output
|
||||||
label.textColor = Asset.Colors.Label.primary.color
|
let name = PassthroughSubject<String, Never>()
|
||||||
label.text = "Title"
|
let value = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
// for custom emoji display
|
||||||
|
let titleActiveLabel: ActiveLabel = {
|
||||||
|
let label = ActiveLabel(style: .profileFieldName)
|
||||||
|
label.configure(content: "title", emojiDict: [:])
|
||||||
return label
|
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 valueActiveLabel: ActiveLabel = {
|
||||||
let label = ActiveLabel(style: .profileField)
|
let label = ActiveLabel(style: .profileFieldValue)
|
||||||
label.configure(content: "value", emojiDict: [:])
|
label.configure(content: "value", emojiDict: [:])
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let topSeparatorLine = UIView.separatorLine
|
// for editing
|
||||||
let bottomSeparatorLine = UIView.separatorLine
|
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) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
@ -41,42 +65,67 @@ final class ProfileFieldView: UIView {
|
||||||
|
|
||||||
extension ProfileFieldView {
|
extension ProfileFieldView {
|
||||||
private func _init() {
|
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([
|
NSLayoutConstraint.activate([
|
||||||
titleLabel.topAnchor.constraint(equalTo: topAnchor),
|
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
titleLabel.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
titleLabel.bottomAnchor.constraint(equalTo: bottomAnchor),
|
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
titleLabel.heightAnchor.constraint(equalToConstant: 44).priority(.defaultHigh),
|
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
|
valueActiveLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||||
addSubview(valueActiveLabel)
|
containerStackView.addArrangedSubview(valueActiveLabel)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
valueActiveLabel.centerYAnchor.constraint(equalTo: centerYAnchor),
|
valueActiveLabel.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
valueActiveLabel.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor),
|
|
||||||
valueActiveLabel.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
|
||||||
])
|
])
|
||||||
valueActiveLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
|
valueActiveLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
|
||||||
|
valueTextField.translatesAutoresizingMaskIntoConstraints = false
|
||||||
topSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
|
containerStackView.addArrangedSubview(valueTextField)
|
||||||
addSubview(topSeparatorLine)
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
topSeparatorLine.topAnchor.constraint(equalTo: topAnchor),
|
valueTextField.widthAnchor.constraint(greaterThanOrEqualToConstant: 44).priority(.defaultHigh),
|
||||||
topSeparatorLine.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
||||||
topSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
topSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
|
titleTextField.isHidden = true
|
||||||
addSubview(bottomSeparatorLine)
|
valueTextField.isHidden = true
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
|
NotificationCenter.default
|
||||||
bottomSeparatorLine.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
.publisher(for: UITextField.textDidChangeNotification, object: titleTextField)
|
||||||
bottomSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
|
.receive(on: DispatchQueue.main)
|
||||||
bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
.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 {
|
static var previews: some View {
|
||||||
UIViewPreview(width: 375) {
|
UIViewPreview(width: 375) {
|
||||||
let filedView = ProfileFieldView()
|
let filedView = ProfileFieldView()
|
||||||
filedView.valueActiveLabel.configure(field: "https://mastodon.online")
|
filedView.valueActiveLabel.configure(field: "https://mastodon.online", emojiDict: [:])
|
||||||
return filedView
|
return filedView
|
||||||
}
|
}
|
||||||
.previewLayout(.fixed(width: 375, height: 100))
|
.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, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
|
||||||
|
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed dwingDashboardMeterView: ProfileStatusDashboardMeterView)
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followingDashboardMeterViewDidPressed followingDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed dwersDashboardMeterView: ProfileStatusDashboardMeterView)
|
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, followersDashboardMeterViewDidPressed followersDashboardMeterView: ProfileStatusDashboardMeterView)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ProfileHeaderView: UIView {
|
final class ProfileHeaderView: UIView {
|
||||||
|
@ -80,6 +80,7 @@ final class ProfileHeaderView: UIView {
|
||||||
view.layer.masksToBounds = true
|
view.layer.masksToBounds = true
|
||||||
view.layer.cornerCurve = .continuous
|
view.layer.cornerCurve = .continuous
|
||||||
view.layer.cornerRadius = ProfileHeaderView.avatarImageViewCornerRadius
|
view.layer.cornerRadius = ProfileHeaderView.avatarImageViewCornerRadius
|
||||||
|
view.alpha = 0 // set initial state invisible
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -152,6 +153,37 @@ final class ProfileHeaderView: UIView {
|
||||||
return textEditorView
|
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) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
_init()
|
_init()
|
||||||
|
@ -162,6 +194,10 @@ final class ProfileHeaderView: UIView {
|
||||||
_init()
|
_init()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
fieldCollectionViewHeightObservation = nil
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileHeaderView {
|
extension ProfileHeaderView {
|
||||||
|
@ -193,22 +229,22 @@ extension ProfileHeaderView {
|
||||||
])
|
])
|
||||||
|
|
||||||
// avatar
|
// avatar
|
||||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
avatarImageViewBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
bannerContainerView.addSubview(avatarImageView)
|
bannerContainerView.addSubview(avatarImageViewBackgroundView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
avatarImageView.leadingAnchor.constraint(equalTo: bannerContainerView.readableContentGuide.leadingAnchor),
|
avatarImageViewBackgroundView.leadingAnchor.constraint(equalTo: bannerContainerView.readableContentGuide.leadingAnchor),
|
||||||
bannerContainerView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 20),
|
bannerContainerView.bottomAnchor.constraint(equalTo: avatarImageViewBackgroundView.bottomAnchor, constant: 20),
|
||||||
avatarImageView.widthAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.width).priority(.required - 1),
|
|
||||||
avatarImageView.heightAnchor.constraint(equalToConstant: ProfileHeaderView.avatarImageViewSize.height).priority(.required - 1),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
avatarImageViewBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
bannerContainerView.insertSubview(avatarImageViewBackgroundView, belowSubview: avatarImageView)
|
avatarImageViewBackgroundView.addSubview(avatarImageView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
avatarImageView.topAnchor.constraint(equalTo: avatarImageViewBackgroundView.topAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth),
|
avatarImageView.topAnchor.constraint(equalTo: avatarImageViewBackgroundView.topAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth),
|
||||||
avatarImageView.leadingAnchor.constraint(equalTo: avatarImageViewBackgroundView.leadingAnchor, 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.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 0.5 * ProfileHeaderView.avatarImageViewBorderWidth),
|
||||||
avatarImageViewBackgroundView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor, 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
|
editAvatarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -328,8 +364,20 @@ extension ProfileHeaderView {
|
||||||
bioContainerStackView.addArrangedSubview(bioActiveLabelContainer)
|
bioContainerStackView.addArrangedSubview(bioActiveLabelContainer)
|
||||||
bioContainerStackView.addArrangedSubview(bioTextEditorView)
|
bioContainerStackView.addArrangedSubview(bioTextEditorView)
|
||||||
|
|
||||||
fieldContainerStackView.preservesSuperviewLayoutMargins = true
|
fieldCollectionView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
metaContainerStackView.addSubview(fieldContainerStackView)
|
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(bannerContainerView)
|
||||||
bringSubviewToFront(nameContainerStackView)
|
bringSubviewToFront(nameContainerStackView)
|
||||||
|
|
|
@ -368,6 +368,18 @@ extension ProfileViewController {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name)
|
.assign(to: \.value, on: profileHeaderViewController.viewModel.displayProfileInfo.name)
|
||||||
.store(in: &disposeBag)
|
.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
|
viewModel.username
|
||||||
.map { username in username.flatMap { "@" + $0 } ?? " " }
|
.map { username in username.flatMap { "@" + $0 } ?? " " }
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
|
@ -640,7 +652,8 @@ extension ProfileViewController: UIScrollViewDelegate {
|
||||||
|
|
||||||
// elastically banner image
|
// elastically banner image
|
||||||
let headerScrollProgress = containerScrollView.contentOffset.y / topMaxContentOffsetY
|
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
|
// MARK: - ProfilePagingViewControllerDelegate
|
||||||
|
@ -852,20 +878,27 @@ extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||||
case .url(_, _, let url, _):
|
case .url(_, _, let url, _):
|
||||||
guard let url = URL(string: url) else { return }
|
guard let url = URL(string: url) else { return }
|
||||||
coordinator.present(scene: .safari(url: url), from: nil, transition: .safariPresent(animated: true, completion: nil))
|
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:
|
default:
|
||||||
// TODO:
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
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 statusesCount: CurrentValueSubject<Int?, Never>
|
||||||
let followingCount: CurrentValueSubject<Int?, Never>
|
let followingCount: CurrentValueSubject<Int?, Never>
|
||||||
let followersCount: 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 protected: CurrentValueSubject<Bool?, Never>
|
||||||
let suspended: 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.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) })
|
||||||
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
||||||
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
self.suspended = CurrentValueSubject(mastodonUser?.suspended ?? false)
|
||||||
|
self.fileds = CurrentValueSubject(mastodonUser?.fields ?? [])
|
||||||
|
self.emojiDict = CurrentValueSubject(mastodonUser?.emojiDict ?? [:])
|
||||||
super.init()
|
super.init()
|
||||||
|
|
||||||
relationshipActionOptionSet
|
relationshipActionOptionSet
|
||||||
|
@ -231,6 +235,8 @@ extension ProfileViewModel {
|
||||||
self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
|
self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
|
||||||
self.protected.value = mastodonUser?.locked
|
self.protected.value = mastodonUser?.locked
|
||||||
self.suspended.value = mastodonUser?.suspended ?? false
|
self.suspended.value = mastodonUser?.suspended ?? false
|
||||||
|
self.fileds.value = mastodonUser?.fields ?? []
|
||||||
|
self.emojiDict.value = mastodonUser?.emojiDict ?? [:]
|
||||||
}
|
}
|
||||||
|
|
||||||
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
||||||
|
|
|
@ -98,6 +98,8 @@ extension APIService.CoreData {
|
||||||
user.update(locked: property.locked)
|
user.update(locked: property.locked)
|
||||||
property.bot.flatMap { user.update(bot: $0) }
|
property.bot.flatMap { user.update(bot: $0) }
|
||||||
property.suspended.flatMap { user.update(suspended: $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)
|
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.sensitive.flatMap { data.append(Data.multipart(key: "source[privacy]", value: $0)) }
|
||||||
source.language.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 (i, fieldsAttribute) in (fieldsAttributes ?? []).enumerated() {
|
||||||
for fieldsAttribute in fieldsAttributes {
|
data.append(Data.multipart(key: "fields_attributes[\(i)][name]", value: fieldsAttribute.name))
|
||||||
data.append(Data.multipart(key: "fields_attributes[name][]", value: fieldsAttribute.name))
|
data.append(Data.multipart(key: "fields_attributes[\(i)][value]", value: fieldsAttribute.value))
|
||||||
data.append(Data.multipart(key: "fields_attributes[value][]", value: fieldsAttribute.value))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data.append(Data.multipartEnd())
|
data.append(Data.multipartEnd())
|
||||||
|
|
|
@ -27,5 +27,11 @@ extension Mastodon.Entity {
|
||||||
case value
|
case value
|
||||||
case verifiedAt = "verified_at"
|
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