forked from zelo72/mastodon-ios
feat: handle profile follow, block, and mute actions
This commit is contained in:
parent
bd89b19724
commit
5d3b6d1943
|
@ -69,6 +69,7 @@
|
|||
<attribute name="acct" attributeType="String"/>
|
||||
<attribute name="avatar" attributeType="String"/>
|
||||
<attribute name="avatarStatic" optional="YES" attributeType="String"/>
|
||||
<attribute name="bot" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
|
||||
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="displayName" attributeType="String"/>
|
||||
<attribute name="domain" attributeType="String"/>
|
||||
|
@ -78,6 +79,7 @@
|
|||
<attribute name="headerStatic" optional="YES" attributeType="String"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="note" optional="YES" attributeType="String"/>
|
||||
<attribute name="statusesCount" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||
|
@ -197,12 +199,12 @@
|
|||
<element name="History" positionX="27" positionY="126" width="128" height="119"/>
|
||||
<element name="HomeTimelineIndex" positionX="0" positionY="0" width="128" height="134"/>
|
||||
<element name="MastodonAuthentication" positionX="18" positionY="162" width="128" height="209"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="629"/>
|
||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="659"/>
|
||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||
<element name="Poll" positionX="72" positionY="162" width="128" height="194"/>
|
||||
<element name="PollOption" positionX="81" positionY="171" width="128" height="134"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="PrivateNote" positionX="72" positionY="153" width="128" height="89"/>
|
||||
<element name="Status" positionX="0" positionY="0" width="128" height="569"/>
|
||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||
</elements>
|
||||
</model>
|
|
@ -29,6 +29,9 @@ final public class MastodonUser: NSManagedObject {
|
|||
@NSManaged public private(set) var followingCount: NSNumber
|
||||
@NSManaged public private(set) var followersCount: NSNumber
|
||||
|
||||
@NSManaged public private(set) var locked: Bool
|
||||
@NSManaged public private(set) var bot: Bool
|
||||
|
||||
@NSManaged public private(set) var createdAt: Date
|
||||
@NSManaged public private(set) var updatedAt: Date
|
||||
|
||||
|
@ -88,6 +91,9 @@ extension MastodonUser {
|
|||
user.followingCount = NSNumber(value: property.followingCount)
|
||||
user.followersCount = NSNumber(value: property.followersCount)
|
||||
|
||||
user.locked = property.locked
|
||||
user.bot = property.bot ?? false
|
||||
|
||||
// Mastodon do not provide relationship on the `Account`
|
||||
// Update relationship via attribute updating interface
|
||||
|
||||
|
@ -158,6 +164,17 @@ extension MastodonUser {
|
|||
self.followersCount = NSNumber(value: followersCount)
|
||||
}
|
||||
}
|
||||
public func update(locked: Bool) {
|
||||
if self.locked != locked {
|
||||
self.locked = locked
|
||||
}
|
||||
}
|
||||
public func update(bot: Bool) {
|
||||
if self.bot != bot {
|
||||
self.bot = bot
|
||||
}
|
||||
}
|
||||
|
||||
public func update(isFollowing: Bool, by mastodonUser: MastodonUser) {
|
||||
if isFollowing {
|
||||
if !(self.followingBy ?? Set()).contains(mastodonUser) {
|
||||
|
@ -249,6 +266,8 @@ extension MastodonUser {
|
|||
public let statusesCount: Int
|
||||
public let followingCount: Int
|
||||
public let followersCount: Int
|
||||
public let locked: Bool
|
||||
public let bot: Bool?
|
||||
|
||||
public let createdAt: Date
|
||||
public let networkDate: Date
|
||||
|
@ -268,6 +287,8 @@ extension MastodonUser {
|
|||
statusesCount: Int,
|
||||
followingCount: Int,
|
||||
followersCount: Int,
|
||||
locked: Bool,
|
||||
bot: Bool?,
|
||||
createdAt: Date,
|
||||
networkDate: Date
|
||||
) {
|
||||
|
@ -286,6 +307,8 @@ extension MastodonUser {
|
|||
self.statusesCount = statusesCount
|
||||
self.followingCount = followingCount
|
||||
self.followersCount = followersCount
|
||||
self.locked = locked
|
||||
self.bot = bot
|
||||
self.createdAt = createdAt
|
||||
self.networkDate = networkDate
|
||||
}
|
||||
|
|
|
@ -69,9 +69,16 @@
|
|||
"firendship": {
|
||||
"follow": "Follow",
|
||||
"following": "Following",
|
||||
"pending": "Pending",
|
||||
"block": "Block",
|
||||
"block_user": "Block %s",
|
||||
"unblock": "Unblock",
|
||||
"unblock_user": "Unblock %s",
|
||||
"blocked": "Blocked",
|
||||
"mute": "Mute",
|
||||
"mute_user": "Mute %s",
|
||||
"unmute": "Unmute",
|
||||
"unmute_user": "Unmute %s",
|
||||
"muted": "Muted",
|
||||
"edit_info": "Edit info"
|
||||
},
|
||||
|
@ -257,6 +264,16 @@
|
|||
"posts": "Posts",
|
||||
"replies": "Replies",
|
||||
"media": "Media"
|
||||
},
|
||||
"relationship_action_alert": {
|
||||
"confirm_unmute_user": {
|
||||
"title": "Unmute Account",
|
||||
"message": "Confirm unmute %s"
|
||||
},
|
||||
"confirm_unblock_usre": {
|
||||
"title": "Unblock Account",
|
||||
"message": "Confirm unblock %s"
|
||||
}
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
|
|
@ -134,7 +134,7 @@
|
|||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; };
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; };
|
||||
DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; };
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileFriendshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */; };
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */; };
|
||||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */; };
|
||||
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB35FC2E26130172006193C9 /* MastodonField.swift */; };
|
||||
DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; };
|
||||
|
@ -259,6 +259,13 @@
|
|||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */; };
|
||||
DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */; };
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; };
|
||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F672615DD60004B8251 /* UserProvider.swift */; };
|
||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */; };
|
||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */; };
|
||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */; };
|
||||
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F932616E28B004B8251 /* APIService+Follow.swift */; };
|
||||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */; };
|
||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */; };
|
||||
DBB525082611EAC0002F1F29 /* Tabman in Frameworks */ = {isa = PBXBuildFile; productRef = DBB525072611EAC0002F1F29 /* Tabman */; };
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */; };
|
||||
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */; };
|
||||
|
@ -473,7 +480,7 @@
|
|||
DB2B3AE825E38850007045F9 /* UIViewPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewPreview.swift; sourceTree = "<group>"; };
|
||||
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
|
||||
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFriendshipActionButton.swift; sourceTree = "<group>"; };
|
||||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileRelationshipActionButton.swift; sourceTree = "<group>"; };
|
||||
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldView.swift; sourceTree = "<group>"; };
|
||||
DB35FC2E26130172006193C9 /* MastodonField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonField.swift; sourceTree = "<group>"; };
|
||||
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
|
||||
|
@ -604,6 +611,13 @@
|
|||
DBA0A10825FB3C2B0079C110 /* RoundedEdgesButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoundedEdgesButton.swift; sourceTree = "<group>"; };
|
||||
DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
|
||||
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
|
||||
DBAE3F672615DD60004B8251 /* UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProvider.swift; sourceTree = "<group>"; };
|
||||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileViewController+UserProvider.swift"; sourceTree = "<group>"; };
|
||||
DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProviderFacade.swift; sourceTree = "<group>"; };
|
||||
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Block.swift"; sourceTree = "<group>"; };
|
||||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Follow.swift"; sourceTree = "<group>"; };
|
||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Mute.swift"; sourceTree = "<group>"; };
|
||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonMetricFormatter.swift; sourceTree = "<group>"; };
|
||||
DBB5250D2611EBAF002F1F29 /* ProfileSegmentedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSegmentedViewController.swift; sourceTree = "<group>"; };
|
||||
DBB525202611EBD6002F1F29 /* ProfilePagingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewController.swift; sourceTree = "<group>"; };
|
||||
DBB5252F2611EBF3002F1F29 /* ProfilePagingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePagingViewModel.swift; sourceTree = "<group>"; };
|
||||
|
@ -885,6 +899,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
2D38F1FC25CD47D900561493 /* StatusProvider */,
|
||||
DBAE3F742615DD63004B8251 /* UserProvider */,
|
||||
DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */,
|
||||
2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */,
|
||||
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */,
|
||||
|
@ -1190,6 +1205,9 @@
|
|||
2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */,
|
||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
||||
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
||||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
||||
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
||||
);
|
||||
path = APIService;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1400,8 +1418,8 @@
|
|||
DB8AF55525C1379F002E6C99 /* Scene */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
2D7631A425C1532200929FB9 /* Share */,
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
DB01409B25C40BB600F9F3CF /* Onboarding */,
|
||||
2D38F1D325CD463600561493 /* HomeTimeline */,
|
||||
|
@ -1503,6 +1521,7 @@
|
|||
DBB525462611ED57002F1F29 /* Header */,
|
||||
DBB5253B2611ECF5002F1F29 /* Timeline */,
|
||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */,
|
||||
DBAE3F812615DDA3004B8251 /* ProfileViewController+UserProvider.swift */,
|
||||
DBB5255D2611F07A002F1F29 /* ProfileViewModel.swift */,
|
||||
DBCC3B8E26148F7B0045B23D /* CachedProfileViewModel.swift */,
|
||||
DBB525632612C988002F1F29 /* MeProfileViewModel.swift */,
|
||||
|
@ -1537,6 +1556,7 @@
|
|||
children = (
|
||||
2D42FF6A25C817D2004A627A /* MastodonStatusContent.swift */,
|
||||
DB35FC2E26130172006193C9 /* MastodonField.swift */,
|
||||
DBAE3FA82617106E004B8251 /* MastodonMetricFormatter.swift */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1558,6 +1578,15 @@
|
|||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBAE3F742615DD63004B8251 /* UserProvider */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DBAE3F672615DD60004B8251 /* UserProvider.swift */,
|
||||
DBAE3F872615DDF4004B8251 /* UserProviderFacade.swift */,
|
||||
);
|
||||
path = UserProvider;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DBB525132611EBB1002F1F29 /* Segmented */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -1603,7 +1632,7 @@
|
|||
DBB5254F2611ED6D002F1F29 /* ProfileHeaderView.swift */,
|
||||
DBB5256D2612D5A1002F1F29 /* ProfileStatusDashboardView.swift */,
|
||||
DBB525842612D6DD002F1F29 /* ProfileStatusDashboardMeterView.swift */,
|
||||
DB35FC1E2612F1D9006193C9 /* ProfileFriendshipActionButton.swift */,
|
||||
DB35FC1E2612F1D9006193C9 /* ProfileRelationshipActionButton.swift */,
|
||||
DB35FC242612FD7A006193C9 /* ProfileFieldView.swift */,
|
||||
);
|
||||
path = View;
|
||||
|
@ -1990,6 +2019,7 @@
|
|||
DBB525212611EBD6002F1F29 /* ProfilePagingViewController.swift in Sources */,
|
||||
DB35FC2F26130172006193C9 /* MastodonField.swift in Sources */,
|
||||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */,
|
||||
DBAE3F8E2616E0B1004B8251 /* APIService+Block.swift in Sources */,
|
||||
5DF1057F25F88A4100D6C0D4 /* TouchBlockingView.swift in Sources */,
|
||||
0FAA0FDF25E0B57E0017CCDE /* WelcomeViewController.swift in Sources */,
|
||||
2D206B8C25F6015000143C56 /* AudioPlaybackService.swift in Sources */,
|
||||
|
@ -2028,6 +2058,7 @@
|
|||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
DB87D4572609DD5300D12C0D /* DeleteBackwardResponseTextField.swift in Sources */,
|
||||
2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */,
|
||||
DBAE3F882615DDF4004B8251 /* UserProviderFacade.swift in Sources */,
|
||||
DBA0A10925FB3C2B0079C110 /* RoundedEdgesButton.swift in Sources */,
|
||||
DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */,
|
||||
DB71FD4625F8C6D200512AE1 /* StatusProvider+UITableViewDataSourcePrefetching.swift in Sources */,
|
||||
|
@ -2096,6 +2127,7 @@
|
|||
DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */,
|
||||
2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */,
|
||||
2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */,
|
||||
DBAE3F682615DD60004B8251 /* UserProvider.swift in Sources */,
|
||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
|
||||
DB4481CC25EE2AFE00BEFB67 /* PollItem.swift in Sources */,
|
||||
|
@ -2106,7 +2138,7 @@
|
|||
2DF75BA125D0E29D00694EC8 /* StatusProvider+StatusTableViewCellDelegate.swift in Sources */,
|
||||
5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */,
|
||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileFriendshipActionButton.swift in Sources */,
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||
DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */,
|
||||
|
@ -2128,6 +2160,7 @@
|
|||
DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */,
|
||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
|
@ -2152,12 +2185,14 @@
|
|||
DB35FC252612FD7A006193C9 /* ProfileFieldView.swift in Sources */,
|
||||
2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */,
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */,
|
||||
DBAE3FA92617106E004B8251 /* MastodonMetricFormatter.swift in Sources */,
|
||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
||||
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||
DB1FD45025F26FA1004CFCFC /* MastodonPickServerViewModel+Diffable.swift in Sources */,
|
||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
||||
DBAE3F9E2616E308004B8251 /* APIService+Mute.swift in Sources */,
|
||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||
DB9D6C0E25E4F9780051B173 /* MosaicImageViewContainer.swift in Sources */,
|
||||
DB71FD3625F8A16C00512AE1 /* APIService+Persist+PersistMemo.swift in Sources */,
|
||||
|
@ -2216,6 +2251,7 @@
|
|||
DBCC3B89261454BA0045B23D /* CGImage.swift in Sources */,
|
||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
||||
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -56,6 +56,7 @@ extension SceneCoordinator {
|
|||
|
||||
// misc
|
||||
case alertController(alertController: UIAlertController)
|
||||
case safari(url: URL)
|
||||
|
||||
#if DEBUG
|
||||
case publicTimeline
|
||||
|
@ -111,6 +112,17 @@ extension SceneCoordinator {
|
|||
guard var presentingViewController = sender ?? sceneDelegate.window?.rootViewController?.topMost else {
|
||||
return nil
|
||||
}
|
||||
// adapt for child controller
|
||||
if let navigationControllerVisibleViewController = presentingViewController.navigationController?.visibleViewController {
|
||||
switch viewController {
|
||||
case is ProfileViewController:
|
||||
let barButtonItem = UIBarButtonItem(title: navigationControllerVisibleViewController.title, style: .plain, target: nil, action: nil)
|
||||
barButtonItem.tintColor = .white
|
||||
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = barButtonItem
|
||||
default:
|
||||
navigationControllerVisibleViewController.navigationItem.backBarButtonItem = nil
|
||||
}
|
||||
}
|
||||
|
||||
if let mainTabBarController = presentingViewController as? MainTabBarController,
|
||||
let navigationController = mainTabBarController.selectedViewController as? UINavigationController,
|
||||
|
@ -222,6 +234,12 @@ private extension SceneCoordinator {
|
|||
)
|
||||
}
|
||||
viewController = alertController
|
||||
case .safari(let url):
|
||||
guard let scheme = url.scheme?.lowercased(),
|
||||
scheme == "http" || scheme == "https" else {
|
||||
return nil
|
||||
}
|
||||
viewController = SFSafariViewController(url: url)
|
||||
#if DEBUG
|
||||
case .publicTimeline:
|
||||
let _viewController = PublicTimelineViewController()
|
||||
|
|
|
@ -9,6 +9,18 @@ import UIKit
|
|||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
import MastodonSDK
|
||||
|
||||
extension Mastodon.Entity.Attachment: Hashable {
|
||||
public static func == (lhs: Mastodon.Entity.Attachment, rhs: Mastodon.Entity.Attachment) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
|
||||
enum PollSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ extension MastodonUser.Property {
|
|||
statusesCount: entity.statusesCount,
|
||||
followingCount: entity.followingCount,
|
||||
followersCount: entity.followersCount,
|
||||
locked: entity.locked,
|
||||
bot: entity.bot,
|
||||
createdAt: entity.createdAt,
|
||||
networkDate: networkDate
|
||||
)
|
||||
|
@ -39,7 +41,12 @@ extension MastodonUser {
|
|||
}
|
||||
|
||||
var acctWithDomain: String {
|
||||
return username + "@" + domain
|
||||
if !acct.contains("@") {
|
||||
// Safe concat due to username cannot contains "@"
|
||||
return username + "@" + domain
|
||||
} else {
|
||||
return acct
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -93,6 +93,11 @@ internal enum Asset {
|
|||
internal enum Connectivity {
|
||||
internal static let photoFillSplit = ImageAsset(name: "Connectivity/photo.fill.split")
|
||||
}
|
||||
internal enum Profile {
|
||||
internal enum Banner {
|
||||
internal static let usernameGray = ColorAsset(name: "Profile/Banner/username.gray")
|
||||
}
|
||||
}
|
||||
internal enum Welcome {
|
||||
internal enum Illustration {
|
||||
internal static let backgroundCyan = ColorAsset(name: "Welcome/illustration/background.cyan")
|
||||
|
|
|
@ -92,6 +92,10 @@ internal enum L10n {
|
|||
internal static let block = L10n.tr("Localizable", "Common.Controls.Firendship.Block")
|
||||
/// Blocked
|
||||
internal static let blocked = L10n.tr("Localizable", "Common.Controls.Firendship.Blocked")
|
||||
/// Block %@
|
||||
internal static func blockUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Firendship.BlockUser", String(describing: p1))
|
||||
}
|
||||
/// Edit info
|
||||
internal static let editInfo = L10n.tr("Localizable", "Common.Controls.Firendship.EditInfo")
|
||||
/// Follow
|
||||
|
@ -102,6 +106,24 @@ internal enum L10n {
|
|||
internal static let mute = L10n.tr("Localizable", "Common.Controls.Firendship.Mute")
|
||||
/// Muted
|
||||
internal static let muted = L10n.tr("Localizable", "Common.Controls.Firendship.Muted")
|
||||
/// Mute %@
|
||||
internal static func muteUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Firendship.MuteUser", String(describing: p1))
|
||||
}
|
||||
/// Pending
|
||||
internal static let pending = L10n.tr("Localizable", "Common.Controls.Firendship.Pending")
|
||||
/// Unblock
|
||||
internal static let unblock = L10n.tr("Localizable", "Common.Controls.Firendship.Unblock")
|
||||
/// Unblock %@
|
||||
internal static func unblockUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Firendship.UnblockUser", String(describing: p1))
|
||||
}
|
||||
/// Unmute
|
||||
internal static let unmute = L10n.tr("Localizable", "Common.Controls.Firendship.Unmute")
|
||||
/// Unmute %@
|
||||
internal static func unmuteUser(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Common.Controls.Firendship.UnmuteUser", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum Status {
|
||||
/// Tap to reveal that may be sensitive
|
||||
|
@ -290,6 +312,24 @@ internal enum L10n {
|
|||
/// posts
|
||||
internal static let posts = L10n.tr("Localizable", "Scene.Profile.Dashboard.Posts")
|
||||
}
|
||||
internal enum RelationshipActionAlert {
|
||||
internal enum ConfirmUnblockUsre {
|
||||
/// Confirm unblock %@
|
||||
internal static func message(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message", String(describing: p1))
|
||||
}
|
||||
/// Unblock Account
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title")
|
||||
}
|
||||
internal enum ConfirmUnmuteUser {
|
||||
/// Confirm unmute %@
|
||||
internal static func message(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message", String(describing: p1))
|
||||
}
|
||||
/// Unmute Account
|
||||
internal static let title = L10n.tr("Localizable", "Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title")
|
||||
}
|
||||
}
|
||||
internal enum SegmentedControl {
|
||||
/// Media
|
||||
internal static let media = L10n.tr("Localizable", "Scene.Profile.SegmentedControl.Media")
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// MastodonMetricFormatter.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-2.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class MastodonMetricFormatter: Formatter {
|
||||
|
||||
func string(from number: Int) -> String? {
|
||||
let isPositive = number >= 0
|
||||
let symbol = isPositive ? "" : "-"
|
||||
|
||||
let numberFormatter = NumberFormatter()
|
||||
|
||||
let value = abs(number)
|
||||
let metric: String
|
||||
|
||||
switch value {
|
||||
case 0..<1000: // 0 ~ 1K
|
||||
metric = String(value)
|
||||
case 1000..<10000: // 1K ~ 10K
|
||||
numberFormatter.maximumFractionDigits = 1
|
||||
let string = numberFormatter.string(from: NSNumber(value: Double(value) / 1000.0)) ?? String(value / 1000)
|
||||
metric = string + "K"
|
||||
case 10000..<1000000: // 10K ~ 1M
|
||||
numberFormatter.maximumFractionDigits = 0
|
||||
let string = numberFormatter.string(from: NSNumber(value: Double(value) / 1000.0)) ?? String(value / 1000)
|
||||
metric = string + "K"
|
||||
default:
|
||||
numberFormatter.maximumFractionDigits = 0
|
||||
let string = numberFormatter.string(from: NSNumber(value: Double(value) / 1000000.0)) ?? String(value / 1000000)
|
||||
metric = string + "M"
|
||||
}
|
||||
|
||||
return symbol + metric
|
||||
}
|
||||
|
||||
}
|
|
@ -84,6 +84,8 @@ extension AvatarConfigurableView {
|
|||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
configureLayerBorder(view: avatarImageView, configuration: configuration)
|
||||
}
|
||||
|
||||
if let avatarButton = configurableAvatarButton {
|
||||
|
@ -110,9 +112,24 @@ extension AvatarConfigurableView {
|
|||
completion: nil
|
||||
)
|
||||
}
|
||||
|
||||
configureLayerBorder(view: avatarButton, configuration: configuration)
|
||||
}
|
||||
}
|
||||
|
||||
func configureLayerBorder(view: UIView, configuration: AvatarConfigurableViewConfiguration) {
|
||||
guard let borderWidth = configuration.borderWidth, borderWidth > 0,
|
||||
let borderColor = configuration.borderColor else {
|
||||
return
|
||||
}
|
||||
|
||||
view.layer.masksToBounds = true
|
||||
view.layer.cornerRadius = Self.configurableAvatarImageCornerRadius
|
||||
view.layer.cornerCurve = .continuous
|
||||
view.layer.borderColor = borderColor.cgColor
|
||||
view.layer.borderWidth = borderWidth
|
||||
}
|
||||
|
||||
func avatarConfigurableView(_ avatarConfigurableView: AvatarConfigurableView, didFinishConfiguration configuration: AvatarConfigurableViewConfiguration) { }
|
||||
|
||||
}
|
||||
|
@ -121,10 +138,19 @@ struct AvatarConfigurableViewConfiguration {
|
|||
|
||||
let avatarImageURL: URL?
|
||||
let placeholderImage: UIImage?
|
||||
let borderColor: UIColor?
|
||||
let borderWidth: CGFloat?
|
||||
|
||||
init(avatarImageURL: URL?, placeholderImage: UIImage? = nil) {
|
||||
init(
|
||||
avatarImageURL: URL?,
|
||||
placeholderImage: UIImage? = nil,
|
||||
borderColor: UIColor? = nil,
|
||||
borderWidth: CGFloat? = nil
|
||||
) {
|
||||
self.avatarImageURL = avatarImageURL
|
||||
self.placeholderImage = placeholderImage
|
||||
self.borderColor = borderColor
|
||||
self.borderWidth = borderWidth
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// UserProvider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
||||
protocol UserProvider: NeedsDependency & DisposeBagCollectable & UIViewController {
|
||||
// async
|
||||
func mastodonUser() -> Future<MastodonUser?, Never>
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
//
|
||||
// UserProviderFacade.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
enum UserProviderFacade { }
|
||||
|
||||
extension UserProviderFacade {
|
||||
|
||||
static func toggleUserFollowRelationship(
|
||||
provider: UserProvider
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
// prepare authentication
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
assertionFailure()
|
||||
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return _toggleUserFollowRelationship(
|
||||
context: provider.context,
|
||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
mastodonUser: provider.mastodonUser().eraseToAnyPublisher()
|
||||
)
|
||||
}
|
||||
|
||||
private static func _toggleUserFollowRelationship(
|
||||
context: AppContext,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
mastodonUser: AnyPublisher<MastodonUser?, Never>
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
mastodonUser
|
||||
.compactMap { mastodonUser -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>? in
|
||||
guard let mastodonUser = mastodonUser else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return context.apiService.toggleFollow(
|
||||
for: mastodonUser,
|
||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.switchToLatest()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserProviderFacade {
|
||||
|
||||
static func toggleUserBlockRelationship(
|
||||
provider: UserProvider
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
// prepare authentication
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
assertionFailure()
|
||||
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return _toggleUserBlockRelationship(
|
||||
context: provider.context,
|
||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
mastodonUser: provider.mastodonUser().eraseToAnyPublisher()
|
||||
)
|
||||
}
|
||||
|
||||
private static func _toggleUserBlockRelationship(
|
||||
context: AppContext,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
mastodonUser: AnyPublisher<MastodonUser?, Never>
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
mastodonUser
|
||||
.compactMap { mastodonUser -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>? in
|
||||
guard let mastodonUser = mastodonUser else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return context.apiService.toggleBlock(
|
||||
for: mastodonUser,
|
||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.switchToLatest()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserProviderFacade {
|
||||
|
||||
static func toggleUserMuteRelationship(
|
||||
provider: UserProvider
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
// prepare authentication
|
||||
guard let activeMastodonAuthenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
assertionFailure()
|
||||
return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return _toggleUserMuteRelationship(
|
||||
context: provider.context,
|
||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox,
|
||||
mastodonUser: provider.mastodonUser().eraseToAnyPublisher()
|
||||
)
|
||||
}
|
||||
|
||||
private static func _toggleUserMuteRelationship(
|
||||
context: AppContext,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox,
|
||||
mastodonUser: AnyPublisher<MastodonUser?, Never>
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
mastodonUser
|
||||
.compactMap { mastodonUser -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error>? in
|
||||
guard let mastodonUser = mastodonUser else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return context.apiService.toggleMute(
|
||||
for: mastodonUser,
|
||||
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.switchToLatest()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension UserProviderFacade {
|
||||
|
||||
static func createProfileActionMenu(
|
||||
for mastodonUser: MastodonUser,
|
||||
isMuting: Bool,
|
||||
isBlocking: Bool,
|
||||
provider: UserProvider
|
||||
) -> UIMenu {
|
||||
var children: [UIMenuElement] = []
|
||||
let name = mastodonUser.displayNameWithFallback
|
||||
|
||||
// mute
|
||||
let muteAction = UIAction(
|
||||
title: isMuting ? L10n.Common.Controls.Firendship.unmuteUser(name) : L10n.Common.Controls.Firendship.mute,
|
||||
image: isMuting ? UIImage(systemName: "speaker") : UIImage(systemName: "speaker.slash"),
|
||||
discoverabilityTitle: isMuting ? nil : L10n.Common.Controls.Firendship.muteUser(name),
|
||||
attributes: isMuting ? [] : .destructive,
|
||||
state: .off
|
||||
) { [weak provider] _ in
|
||||
guard let provider = provider else { return }
|
||||
|
||||
UserProviderFacade.toggleUserMuteRelationship(
|
||||
provider: provider
|
||||
)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &provider.context.disposeBag)
|
||||
}
|
||||
if isMuting {
|
||||
children.append(muteAction)
|
||||
} else {
|
||||
let muteMenu = UIMenu(title: L10n.Common.Controls.Firendship.muteUser(name), image: UIImage(systemName: "speaker.slash"), options: [], children: [muteAction])
|
||||
children.append(muteMenu)
|
||||
}
|
||||
|
||||
// block
|
||||
let blockAction = UIAction(
|
||||
title: isBlocking ? L10n.Common.Controls.Firendship.unblockUser(name) : L10n.Common.Controls.Firendship.block,
|
||||
image: isBlocking ? UIImage(systemName: "hand.raised.slash") : UIImage(systemName: "hand.raised"),
|
||||
discoverabilityTitle: isBlocking ? nil : L10n.Common.Controls.Firendship.blockUser(name),
|
||||
attributes: isBlocking ? [] : .destructive,
|
||||
state: .off
|
||||
) { [weak provider] _ in
|
||||
guard let provider = provider else { return }
|
||||
|
||||
UserProviderFacade.toggleUserBlockRelationship(
|
||||
provider: provider
|
||||
)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &provider.context.disposeBag)
|
||||
}
|
||||
if isBlocking {
|
||||
children.append(blockAction)
|
||||
} else {
|
||||
let blockMenu = UIMenu(title: L10n.Common.Controls.Firendship.blockUser(name), image: UIImage(systemName: "hand.raised"), options: [], children: [blockAction])
|
||||
children.append(blockMenu)
|
||||
}
|
||||
|
||||
return UIMenu(title: "", options: [], children: children)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "0.600",
|
||||
"blue" : "0.961",
|
||||
"green" : "0.922",
|
||||
"red" : "0.922"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -29,12 +29,19 @@ Please check your internet connection.";
|
|||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||
"Common.Controls.Firendship.Block" = "Block";
|
||||
"Common.Controls.Firendship.BlockUser" = "Block %@";
|
||||
"Common.Controls.Firendship.Blocked" = "Blocked";
|
||||
"Common.Controls.Firendship.EditInfo" = "Edit info";
|
||||
"Common.Controls.Firendship.Follow" = "Follow";
|
||||
"Common.Controls.Firendship.Following" = "Following";
|
||||
"Common.Controls.Firendship.Mute" = "Mute";
|
||||
"Common.Controls.Firendship.MuteUser" = "Mute %@";
|
||||
"Common.Controls.Firendship.Muted" = "Muted";
|
||||
"Common.Controls.Firendship.Pending" = "Pending";
|
||||
"Common.Controls.Firendship.Unblock" = "Unblock";
|
||||
"Common.Controls.Firendship.UnblockUser" = "Unblock %@";
|
||||
"Common.Controls.Firendship.Unmute" = "Unmute";
|
||||
"Common.Controls.Firendship.UnmuteUser" = "Unmute %@";
|
||||
"Common.Controls.Status.MediaContentWarning" = "Tap to reveal that may be sensitive";
|
||||
"Common.Controls.Status.Poll.Closed" = "Closed";
|
||||
"Common.Controls.Status.Poll.TimeLeft" = "%@ left";
|
||||
|
@ -96,6 +103,10 @@ tap the link to confirm your account.";
|
|||
"Scene.Profile.Dashboard.Followers" = "followers";
|
||||
"Scene.Profile.Dashboard.Following" = "following";
|
||||
"Scene.Profile.Dashboard.Posts" = "posts";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Message" = "Confirm unblock %@";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.Title" = "Unblock Account";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Message" = "Confirm unmute %@";
|
||||
"Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.Title" = "Unmute Account";
|
||||
"Scene.Profile.SegmentedControl.Media" = "Media";
|
||||
"Scene.Profile.SegmentedControl.Posts" = "Posts";
|
||||
"Scene.Profile.SegmentedControl.Replies" = "Replies";
|
||||
|
|
|
@ -21,7 +21,7 @@ final class ProfileHeaderViewController: UIViewController {
|
|||
|
||||
weak var delegate: ProfileHeaderViewControllerDelegate?
|
||||
|
||||
let profileBannerView = ProfileHeaderView()
|
||||
let profileHeaderView = ProfileHeaderView()
|
||||
let pageSegmentedControl: UISegmentedControl = {
|
||||
let segmenetedControl = UISegmentedControl(items: ["A", "B"])
|
||||
segmenetedControl.selectedSegmentIndex = 0
|
||||
|
@ -31,7 +31,7 @@ final class ProfileHeaderViewController: UIViewController {
|
|||
private var isBannerPinned = false
|
||||
private var bottomShadowAlpha: CGFloat = 0.0
|
||||
|
||||
private var isAdjustBannerImageViewForSafeAreaInset = false
|
||||
// private var isAdjustBannerImageViewForSafeAreaInset = false
|
||||
private var containerSafeAreaInset: UIEdgeInsets = .zero
|
||||
|
||||
deinit {
|
||||
|
@ -47,19 +47,19 @@ extension ProfileHeaderViewController {
|
|||
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
|
||||
profileBannerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(profileBannerView)
|
||||
profileHeaderView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(profileHeaderView)
|
||||
NSLayoutConstraint.activate([
|
||||
profileBannerView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
profileBannerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
profileBannerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
profileHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
profileHeaderView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
profileHeaderView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
])
|
||||
profileBannerView.preservesSuperviewLayoutMargins = true
|
||||
profileHeaderView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
pageSegmentedControl.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(pageSegmentedControl)
|
||||
NSLayoutConstraint.activate([
|
||||
pageSegmentedControl.topAnchor.constraint(equalTo: profileBannerView.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
||||
pageSegmentedControl.topAnchor.constraint(equalTo: profileHeaderView.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
||||
pageSegmentedControl.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
|
||||
pageSegmentedControl.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: pageSegmentedControl.bottomAnchor, constant: ProfileHeaderViewController.segmentedControlMarginHeight),
|
||||
|
@ -72,11 +72,13 @@ extension ProfileHeaderViewController {
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !isAdjustBannerImageViewForSafeAreaInset {
|
||||
isAdjustBannerImageViewForSafeAreaInset = true
|
||||
profileBannerView.bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||
profileBannerView.bannerImageView.frame.size.height += containerSafeAreaInset.top
|
||||
}
|
||||
// Deprecated:
|
||||
// not needs this tweak due to force layout update in the parent
|
||||
// if !isAdjustBannerImageViewForSafeAreaInset {
|
||||
// isAdjustBannerImageViewForSafeAreaInset = true
|
||||
// profileHeaderView.bannerImageView.frame.origin.y = -containerSafeAreaInset.top
|
||||
// profileHeaderView.bannerImageView.frame.size.height += containerSafeAreaInset.top
|
||||
// }
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
|
@ -115,13 +117,13 @@ extension ProfileHeaderViewController {
|
|||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: progress: %.2f", ((#file as NSString).lastPathComponent), #line, #function, progress)
|
||||
updateHeaderBottomShadow(progress: progress)
|
||||
|
||||
let bannerImageView = profileBannerView.bannerImageView
|
||||
let bannerImageView = profileHeaderView.bannerImageView
|
||||
guard bannerImageView.bounds != .zero else {
|
||||
// wait layout finish
|
||||
return
|
||||
}
|
||||
|
||||
let bannerContainerInWindow = profileBannerView.convert(profileBannerView.bannerContainerView.frame, to: nil)
|
||||
let bannerContainerInWindow = profileHeaderView.convert(profileHeaderView.bannerContainerView.frame, to: nil)
|
||||
let bannerContainerBottomOffset = bannerContainerInWindow.origin.y + bannerContainerInWindow.height
|
||||
|
||||
if bannerContainerInWindow.origin.y > containerSafeAreaInset.top {
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
//
|
||||
// ProfileFriendshipActionButton.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class ProfileFriendshipActionButton: RoundedEdgesButton {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileFriendshipActionButton {
|
||||
private func _init() {
|
||||
configure(state: .follow)
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileFriendshipActionButton {
|
||||
enum State {
|
||||
case follow
|
||||
case following
|
||||
case blocked
|
||||
case muted
|
||||
case edit
|
||||
case editing
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .follow: return L10n.Common.Controls.Firendship.follow
|
||||
case .following: return L10n.Common.Controls.Firendship.following
|
||||
case .blocked: return L10n.Common.Controls.Firendship.blocked
|
||||
case .muted: return L10n.Common.Controls.Firendship.muted
|
||||
case .edit: return L10n.Common.Controls.Firendship.editInfo
|
||||
case .editing: return L10n.Common.Controls.Actions.done
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundColor: UIColor {
|
||||
switch self {
|
||||
case .follow: return Asset.Colors.Button.normal.color
|
||||
case .following: return Asset.Colors.Button.normal.color
|
||||
case .blocked: return Asset.Colors.Background.danger.color
|
||||
case .muted: return Asset.Colors.Background.alertYellow.color
|
||||
case .edit: return Asset.Colors.Button.normal.color
|
||||
case .editing: return Asset.Colors.Button.normal.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func configure(state: State) {
|
||||
setTitle(state.title, for: .normal)
|
||||
setTitleColor(.white, for: .normal)
|
||||
setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: state.backgroundColor), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: state.backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: state.backgroundColor.withAlphaComponent(0.5)), for: .disabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import UIKit
|
|||
import ActiveLabel
|
||||
|
||||
protocol ProfileHeaderViewDelegate: class {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton)
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity)
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView)
|
||||
|
@ -22,6 +23,7 @@ final class ProfileHeaderView: UIView {
|
|||
static let avatarImageViewSize = CGSize(width: 56, height: 56)
|
||||
static let avatarImageViewCornerRadius: CGFloat = 6
|
||||
static let friendshipActionButtonSize = CGSize(width: 108, height: 34)
|
||||
static let bannerImageViewPlaceholderColor = UIColor.systemGray
|
||||
|
||||
weak var delegate: ProfileHeaderViewDelegate?
|
||||
|
||||
|
@ -29,10 +31,18 @@ final class ProfileHeaderView: UIView {
|
|||
let bannerImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFill
|
||||
imageView.image = .placeholder(color: .systemGray)
|
||||
imageView.image = .placeholder(color: ProfileHeaderView.bannerImageViewPlaceholderColor)
|
||||
imageView.layer.masksToBounds = true
|
||||
// #if DEBUG
|
||||
// imageView.image = .placeholder(color: .red)
|
||||
// #endif
|
||||
return imageView
|
||||
}()
|
||||
let bannerImageViewOverlayView: UIView = {
|
||||
let overlayView = UIView()
|
||||
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
|
||||
return overlayView
|
||||
}()
|
||||
|
||||
let avatarImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
|
@ -59,14 +69,18 @@ final class ProfileHeaderView: UIView {
|
|||
label.font = UIFontMetrics(forTextStyle: .subheadline).scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||
label.adjustsFontSizeToFitWidth = true
|
||||
label.minimumScaleFactor = 0.5
|
||||
label.textColor = .white
|
||||
label.textColor = Asset.Profile.Banner.usernameGray.color
|
||||
label.text = "@alice"
|
||||
label.applyShadow(color: UIColor.black.withAlphaComponent(0.2), alpha: 0.5, x: 0, y: 2, blur: 2, spread: 0)
|
||||
return label
|
||||
}()
|
||||
|
||||
let statusDashboardView = ProfileStatusDashboardView()
|
||||
let friendshipActionButton = ProfileFriendshipActionButton()
|
||||
let relationshipActionButton: ProfileRelationshipActionButton = {
|
||||
let button = ProfileRelationshipActionButton()
|
||||
button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
return button
|
||||
}()
|
||||
|
||||
let bioContainerView = UIView()
|
||||
let fieldContainerStackView = UIStackView()
|
||||
|
@ -104,6 +118,15 @@ extension ProfileHeaderView {
|
|||
bannerImageView.frame = bannerContainerView.bounds
|
||||
bannerContainerView.addSubview(bannerImageView)
|
||||
|
||||
bannerImageViewOverlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bannerImageView.addSubview(bannerImageViewOverlayView)
|
||||
NSLayoutConstraint.activate([
|
||||
bannerImageViewOverlayView.topAnchor.constraint(equalTo: bannerImageView.topAnchor),
|
||||
bannerImageViewOverlayView.leadingAnchor.constraint(equalTo: bannerImageView.leadingAnchor),
|
||||
bannerImageViewOverlayView.trailingAnchor.constraint(equalTo: bannerImageView.trailingAnchor),
|
||||
bannerImageViewOverlayView.bottomAnchor.constraint(equalTo: bannerImageView.bottomAnchor),
|
||||
])
|
||||
|
||||
// avatar
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
bannerContainerView.addSubview(avatarImageView)
|
||||
|
@ -156,14 +179,14 @@ extension ProfileHeaderView {
|
|||
statusDashboardView.bottomAnchor.constraint(equalTo: dashboardContainerView.bottomAnchor),
|
||||
])
|
||||
|
||||
friendshipActionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
dashboardContainerView.addSubview(friendshipActionButton)
|
||||
relationshipActionButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
dashboardContainerView.addSubview(relationshipActionButton)
|
||||
NSLayoutConstraint.activate([
|
||||
friendshipActionButton.topAnchor.constraint(equalTo: dashboardContainerView.topAnchor),
|
||||
friendshipActionButton.leadingAnchor.constraint(greaterThanOrEqualTo: statusDashboardView.trailingAnchor, constant: 8),
|
||||
friendshipActionButton.trailingAnchor.constraint(equalTo: dashboardContainerView.readableContentGuide.trailingAnchor),
|
||||
friendshipActionButton.widthAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.width).priority(.defaultHigh),
|
||||
friendshipActionButton.heightAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.height).priority(.defaultHigh),
|
||||
relationshipActionButton.topAnchor.constraint(equalTo: dashboardContainerView.topAnchor),
|
||||
relationshipActionButton.leadingAnchor.constraint(greaterThanOrEqualTo: statusDashboardView.trailingAnchor, constant: 8),
|
||||
relationshipActionButton.trailingAnchor.constraint(equalTo: dashboardContainerView.readableContentGuide.trailingAnchor),
|
||||
relationshipActionButton.widthAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.width).priority(.defaultHigh),
|
||||
relationshipActionButton.heightAnchor.constraint(equalToConstant: ProfileHeaderView.friendshipActionButtonSize.height).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
bioContainerView.preservesSuperviewLayoutMargins = true
|
||||
|
@ -184,10 +207,20 @@ extension ProfileHeaderView {
|
|||
bringSubviewToFront(nameContainerStackView)
|
||||
|
||||
bioActiveLabel.delegate = self
|
||||
|
||||
relationshipActionButton.addTarget(self, action: #selector(ProfileHeaderView.relationshipActionButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileHeaderView {
|
||||
@objc private func relationshipActionButtonDidPressed(_ sender: UIButton) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
assert(sender === relationshipActionButton)
|
||||
delegate?.profileHeaderView(self, relationshipButtonDidPressed: relationshipActionButton)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ActiveLabelDelegate
|
||||
extension ProfileHeaderView: ActiveLabelDelegate {
|
||||
func activeLabel(_ activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// ProfileRelationshipActionButton.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-3-30.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class ProfileRelationshipActionButton: RoundedEdgesButton {
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ProfileRelationshipActionButton {
|
||||
private func _init() {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
extension ProfileRelationshipActionButton {
|
||||
func configure(actionOptionSet: ProfileViewModel.RelationshipActionOptionSet) {
|
||||
setTitle(actionOptionSet.title, for: .normal)
|
||||
setTitleColor(.white, for: .normal)
|
||||
setTitleColor(UIColor.white.withAlphaComponent(0.5), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor), for: .normal)
|
||||
setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .highlighted)
|
||||
setBackgroundImage(.placeholder(color: actionOptionSet.backgroundColor.withAlphaComponent(0.5)), for: .disabled)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// ProfileViewController+UserProvider.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
|
||||
extension ProfileViewController: UserProvider {
|
||||
|
||||
func mastodonUser() -> Future<MastodonUser?, Never> {
|
||||
return Future { promise in
|
||||
promise(.success(self.viewModel.mastodonUser.value))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -18,11 +18,17 @@ final class ProfileViewController: UIViewController, NeedsDependency {
|
|||
var disposeBag = Set<AnyCancellable>()
|
||||
var viewModel: ProfileViewModel!
|
||||
|
||||
private var preferredStatusBarStyleForBanner: UIStatusBarStyle = .lightContent {
|
||||
didSet {
|
||||
setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
}
|
||||
private(set) lazy var replyBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "arrowshape.turn.up.left"), style: .plain, target: self, action: #selector(ProfileViewController.replyBarButtonItemPressed(_:)))
|
||||
barButtonItem.tintColor = .white
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let moreMenuBarButtonItem: UIBarButtonItem = {
|
||||
let barButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), style: .plain, target: nil, action: nil)
|
||||
barButtonItem.tintColor = .white
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let refreshControl: UIRefreshControl = {
|
||||
let refreshControl = UIRefreshControl()
|
||||
|
@ -78,7 +84,7 @@ extension ProfileViewController {
|
|||
height: bottomPageHeight + headerViewHeight
|
||||
)
|
||||
self.overlayScrollView.contentSize = contentSize
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: contentSize: %s", ((#file as NSString).lastPathComponent), #line, #function, contentSize.debugDescription)
|
||||
// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: contentSize: %s", ((#file as NSString).lastPathComponent), #line, #function, contentSize.debugDescription)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -86,7 +92,7 @@ extension ProfileViewController {
|
|||
extension ProfileViewController {
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return preferredStatusBarStyleForBanner
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewSafeAreaInsetsDidChange() {
|
||||
|
@ -95,6 +101,10 @@ extension ProfileViewController {
|
|||
profileHeaderViewController.updateHeaderContainerSafeAreaInset(view.safeAreaInsets)
|
||||
}
|
||||
|
||||
override var isViewLoaded: Bool {
|
||||
return super.isViewLoaded
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
@ -105,15 +115,31 @@ extension ProfileViewController {
|
|||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
|
||||
navigationItem.titleView = UIView()
|
||||
|
||||
// if navigationController?.viewControllers.first == self {
|
||||
// navigationItem.leftBarButtonItem = avatarBarButtonItem
|
||||
// }
|
||||
// avatarBarButtonItem.avatarButton.addTarget(self, action: #selector(ProfileViewController.avatarButtonPressed(_:)), for: .touchUpInside)
|
||||
Publishers.CombineLatest(
|
||||
viewModel.isReplyBarButtonItemHidden.eraseToAnyPublisher(),
|
||||
viewModel.isMoreMenuBarButtonItemHidden.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isReplyBarButtonItemHidden, isMoreMenuBarButtonItemHidden in
|
||||
guard let self = self else { return }
|
||||
var items: [UIBarButtonItem] = []
|
||||
if !isReplyBarButtonItemHidden {
|
||||
items.append(self.replyBarButtonItem)
|
||||
}
|
||||
if !isMoreMenuBarButtonItemHidden {
|
||||
items.append(self.moreMenuBarButtonItem)
|
||||
}
|
||||
guard !items.isEmpty else {
|
||||
self.navigationItem.rightBarButtonItems = nil
|
||||
return
|
||||
}
|
||||
self.navigationItem.rightBarButtonItems = items
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// unmuteMenuBarButtonItem.target = self
|
||||
// unmuteMenuBarButtonItem.action = #selector(ProfileViewController.unmuteBarButtonItemPressed(_:))
|
||||
|
||||
// Publishers.CombineLatest4(
|
||||
// viewModel.muted.eraseToAnyPublisher(),
|
||||
|
@ -244,23 +270,15 @@ extension ProfileViewController {
|
|||
profileHeaderViewController.delegate = self
|
||||
profileSegmentedViewController.pagingViewController.pagingDelegate = self
|
||||
|
||||
// // add segmented bar to header
|
||||
// profileSegmentedViewController.pagingViewController.addBar(
|
||||
// bar,
|
||||
// dataSource: profileSegmentedViewController.pagingViewController.viewModel,
|
||||
// at: .custom(view: profileHeaderViewController.view, layout: { bar in
|
||||
// bar.translatesAutoresizingMaskIntoConstraints = false
|
||||
// self.profileHeaderViewController.view.addSubview(bar)
|
||||
// NSLayoutConstraint.activate([
|
||||
// bar.leadingAnchor.constraint(equalTo: self.profileHeaderViewController.view.leadingAnchor),
|
||||
// bar.trailingAnchor.constraint(equalTo: self.profileHeaderViewController.view.trailingAnchor),
|
||||
// bar.bottomAnchor.constraint(equalTo: self.profileHeaderViewController.view.bottomAnchor),
|
||||
// bar.heightAnchor.constraint(equalToConstant: ProfileHeaderViewController.headerMinHeight).priority(.defaultHigh),
|
||||
// ])
|
||||
// })
|
||||
// )
|
||||
|
||||
// bind view model
|
||||
viewModel.name
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] name in
|
||||
guard let self = self else { return }
|
||||
self.title = name
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
Publishers.CombineLatest(
|
||||
viewModel.bannerImageURL.eraseToAnyPublisher(),
|
||||
viewModel.viewDidAppear.eraseToAnyPublisher()
|
||||
|
@ -268,56 +286,29 @@ extension ProfileViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] bannerImageURL, _ in
|
||||
guard let self = self else { return }
|
||||
self.profileHeaderViewController.profileBannerView.bannerImageView.af.cancelImageRequest()
|
||||
let placeholder = UIImage.placeholder(color: Asset.Colors.Background.systemGroupedBackground.color)
|
||||
self.profileHeaderViewController.profileHeaderView.bannerImageView.af.cancelImageRequest()
|
||||
let placeholder = UIImage.placeholder(color: ProfileHeaderView.bannerImageViewPlaceholderColor)
|
||||
guard let bannerImageURL = bannerImageURL else {
|
||||
self.profileHeaderViewController.profileBannerView.bannerImageView.image = placeholder
|
||||
self.profileHeaderViewController.profileHeaderView.bannerImageView.image = placeholder
|
||||
return
|
||||
}
|
||||
self.profileHeaderViewController.profileBannerView.bannerImageView.af.setImage(
|
||||
self.profileHeaderViewController.profileHeaderView.bannerImageView.af.setImage(
|
||||
withURL: bannerImageURL,
|
||||
placeholderImage: placeholder,
|
||||
imageTransition: .crossDissolve(0.3),
|
||||
runImageTransitionIfCached: false,
|
||||
completion: { [weak self] response in
|
||||
guard let self = self else { return }
|
||||
switch response.result {
|
||||
case .success(let image):
|
||||
self.viewModel.headerDomainLumaStyle.value = image.domainLumaCoefficientsStyle ?? .dark
|
||||
case .failure:
|
||||
break
|
||||
guard let image = response.value else { return }
|
||||
guard image.size.width > 1 && image.size.height > 1 else {
|
||||
// restore to placeholder when image invalid
|
||||
self.profileHeaderViewController.profileHeaderView.bannerImageView.image = placeholder
|
||||
return
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.headerDomainLumaStyle
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] style in
|
||||
guard let self = self else { return }
|
||||
let textColor: UIColor
|
||||
let shadowColor: UIColor
|
||||
switch style {
|
||||
case .light:
|
||||
self.preferredStatusBarStyleForBanner = .darkContent
|
||||
textColor = .black
|
||||
shadowColor = .white
|
||||
case .dark:
|
||||
self.preferredStatusBarStyleForBanner = .lightContent
|
||||
textColor = .white
|
||||
shadowColor = .black
|
||||
default:
|
||||
self.preferredStatusBarStyleForBanner = .default
|
||||
textColor = .white
|
||||
shadowColor = .black
|
||||
}
|
||||
|
||||
self.profileHeaderViewController.profileBannerView.nameLabel.textColor = textColor
|
||||
self.profileHeaderViewController.profileBannerView.usernameLabel.textColor = textColor
|
||||
self.profileHeaderViewController.profileBannerView.nameLabel.applyShadow(color: shadowColor, alpha: 0.5, x: 0, y: 2, blur: 2)
|
||||
self.profileHeaderViewController.profileBannerView.usernameLabel.applyShadow(color: shadowColor, alpha: 0.5, x: 0, y: 2, blur: 2)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
Publishers.CombineLatest(
|
||||
viewModel.avatarImageURL.eraseToAnyPublisher(),
|
||||
viewModel.viewDidAppear.eraseToAnyPublisher()
|
||||
|
@ -325,142 +316,95 @@ extension ProfileViewController {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] avatarImageURL, _ in
|
||||
guard let self = self else { return }
|
||||
self.profileHeaderViewController.profileBannerView.configure(
|
||||
with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarImageURL)
|
||||
self.profileHeaderViewController.profileHeaderView.configure(
|
||||
with: AvatarConfigurableViewConfiguration(avatarImageURL: avatarImageURL, borderColor: .white, borderWidth: 2)
|
||||
)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// viewModel.protected
|
||||
// .map { $0 != true }
|
||||
// .assign(to: \.isHidden, on: profileHeaderViewController.profileBannerView.lockImageView)
|
||||
// .store(in: &disposeBag)
|
||||
viewModel.name
|
||||
.map { $0 ?? " " }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.text, on: profileHeaderViewController.profileBannerView.nameLabel)
|
||||
.assign(to: \.text, on: profileHeaderViewController.profileHeaderView.nameLabel)
|
||||
.store(in: &disposeBag)
|
||||
viewModel.username
|
||||
.map { username in username.flatMap { "@" + $0 } ?? " " }
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.text, on: profileHeaderViewController.profileBannerView.usernameLabel)
|
||||
.assign(to: \.text, on: profileHeaderViewController.profileHeaderView.usernameLabel)
|
||||
.store(in: &disposeBag)
|
||||
// viewModel.friendship
|
||||
// .sink { [weak self] friendship in
|
||||
// guard let self = self else { return }
|
||||
// let followingButton = self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.followActionButton
|
||||
// followingButton.isHidden = friendship == nil
|
||||
//
|
||||
// if let friendship = friendship {
|
||||
// switch friendship {
|
||||
// case .following: followingButton.style = .following
|
||||
// case .pending: followingButton.style = .pending
|
||||
// case .none: followingButton.style = .follow
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// viewModel.followedBy
|
||||
// .sink { [weak self] followedBy in
|
||||
// guard let self = self else { return }
|
||||
// let followStatusLabel = self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.followStatusLabel
|
||||
// followStatusLabel.isHidden = followedBy != true
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
//
|
||||
viewModel.relationshipActionOptionSet
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] relationshipActionOptionSet in
|
||||
guard let self = self else { return }
|
||||
guard let mastodonUser = self.viewModel.mastodonUser.value else {
|
||||
self.moreMenuBarButtonItem.menu = nil
|
||||
return
|
||||
}
|
||||
let isMuting = relationshipActionOptionSet.contains(.muting)
|
||||
let isBlocking = relationshipActionOptionSet.contains(.blocking)
|
||||
self.moreMenuBarButtonItem.menu = UserProviderFacade.createProfileActionMenu(for: mastodonUser, isMuting: isMuting, isBlocking: isBlocking, provider: self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.isRelationshipActionButtonHidden
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isHidden in
|
||||
guard let self = self else { return }
|
||||
self.profileHeaderViewController.profileHeaderView.relationshipActionButton.isHidden = isHidden
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
Publishers.CombineLatest(
|
||||
viewModel.relationshipActionOptionSet.eraseToAnyPublisher(),
|
||||
viewModel.isEditing.eraseToAnyPublisher()
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] relationshipActionSet, isEditing in
|
||||
guard let self = self else { return }
|
||||
let friendshipButton = self.profileHeaderViewController.profileHeaderView.relationshipActionButton
|
||||
if relationshipActionSet.contains(.edit) {
|
||||
friendshipButton.configure(actionOptionSet: isEditing ? .editing : .edit)
|
||||
} else {
|
||||
friendshipButton.configure(actionOptionSet: relationshipActionSet)
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.bioDescription
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] bio in
|
||||
guard let self = self else { return }
|
||||
self.profileHeaderViewController.profileBannerView.bioActiveLabel.configure(note: bio ?? "")
|
||||
self.profileHeaderViewController.profileHeaderView.bioActiveLabel.configure(note: bio ?? "")
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
// Publishers.CombineLatest(
|
||||
// viewModel.url.eraseToAnyPublisher(),
|
||||
// viewModel.suspended.eraseToAnyPublisher()
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] url, isSuspended in
|
||||
// guard let self = self else { return }
|
||||
// let url = url.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? " "
|
||||
// self.profileHeaderViewController.profileBannerView.linkButton.setTitle(url, for: .normal)
|
||||
// let isEmpty = url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
// self.profileHeaderViewController.profileBannerView.linkContainer.isHidden = isEmpty || isSuspended
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// Publishers.CombineLatest(
|
||||
// viewModel.location.eraseToAnyPublisher(),
|
||||
// viewModel.suspended.eraseToAnyPublisher()
|
||||
// )
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] location, isSuspended in
|
||||
// guard let self = self else { return }
|
||||
// let location = location.flatMap { $0.trimmingCharacters(in: .whitespacesAndNewlines) } ?? " "
|
||||
// self.profileHeaderViewController.profileBannerView.geoButton.setTitle(location, for: .normal)
|
||||
// let isEmpty = location.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
// self.profileHeaderViewController.profileBannerView.geoContainer.isHidden = isEmpty || isSuspended
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
viewModel.statusesCount
|
||||
.sink { [weak self] count in
|
||||
guard let self = self else { return }
|
||||
let text = count.flatMap { String($0) } ?? "-"
|
||||
self.profileHeaderViewController.profileBannerView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
|
||||
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.postDashboardMeterView.numberLabel.text = text
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.followingCount
|
||||
.sink { [weak self] count in
|
||||
guard let self = self else { return }
|
||||
let text = count.flatMap { String($0) } ?? "-"
|
||||
self.profileHeaderViewController.profileBannerView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
|
||||
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followingDashboardMeterView.numberLabel.text = text
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
viewModel.followersCount
|
||||
.sink { [weak self] count in
|
||||
guard let self = self else { return }
|
||||
let text = count.flatMap { String($0) } ?? "-"
|
||||
self.profileHeaderViewController.profileBannerView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
|
||||
let text = count.flatMap { MastodonMetricFormatter().string(from: $0) } ?? "-"
|
||||
self.profileHeaderViewController.profileHeaderView.statusDashboardView.followersDashboardMeterView.numberLabel.text = text
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
// viewModel.followersCount
|
||||
// .sink { [weak self] count in
|
||||
// guard let self = self else { return }
|
||||
// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.followersStatusItemView.countLabel.text = count.flatMap { "\($0)" } ?? "-"
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// viewModel.listedCount
|
||||
// .sink { [weak self] count in
|
||||
// guard let self = self else { return }
|
||||
// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.listedStatusItemView.countLabel.text = count.flatMap { "\($0)" } ?? "-"
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// viewModel.suspended
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .sink { [weak self] isSuspended in
|
||||
// guard let self = self else { return }
|
||||
// self.profileHeaderViewController.profileBannerView.profileBannerStatusView.isHidden = isSuspended
|
||||
// self.profileHeaderViewController.profileBannerView.profileBannerInfoActionView.isHidden = isSuspended
|
||||
// if isSuspended {
|
||||
// self.profileSegmentedViewController
|
||||
// .pagingViewController.viewModel
|
||||
// .profileTweetPostTimelineViewController.viewModel
|
||||
// .stateMachine
|
||||
// .enter(UserTimelineViewModel.State.Suspended.self)
|
||||
// self.profileSegmentedViewController
|
||||
// .pagingViewController.viewModel
|
||||
// .profileMediaPostTimelineViewController.viewModel
|
||||
// .stateMachine
|
||||
// .enter(UserMediaTimelineViewModel.State.Suspended.self)
|
||||
// self.profileSegmentedViewController
|
||||
// .pagingViewController.viewModel
|
||||
// .profileLikesPostTimelineViewController.viewModel
|
||||
// .stateMachine
|
||||
// .enter(UserLikeTimelineViewModel.State.Suspended.self)
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
|
||||
//
|
||||
profileHeaderViewController.profileBannerView.delegate = self
|
||||
profileHeaderViewController.profileHeaderView.delegate = self
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
// set back button tint color in SceneCoordinator.present(scene:from:transition:)
|
||||
|
||||
// force layout to make banner image tweak take effect
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -483,6 +427,11 @@ extension ProfileViewController {
|
|||
|
||||
extension ProfileViewController {
|
||||
|
||||
@objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
// TODO:
|
||||
}
|
||||
|
||||
@objc private func refreshControlValueChanged(_ sender: UIRefreshControl) {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
|
@ -600,60 +549,95 @@ extension ProfileViewController: ProfilePagingViewControllerDelegate {
|
|||
// setup observer and gesture fallback
|
||||
currentPostTimelineTableViewContentSizeObservation = observeTableViewContentSize(scrollView: postTimelineViewController.scrollView)
|
||||
postTimelineViewController.scrollView.panGestureRecognizer.require(toFail: overlayScrollView.panGestureRecognizer)
|
||||
|
||||
|
||||
// if let userMediaTimelineViewController = postTimelineViewController as? UserMediaTimelineViewController,
|
||||
// let currentState = userMediaTimelineViewController.viewModel.stateMachine.currentState {
|
||||
// switch currentState {
|
||||
// case is UserMediaTimelineViewModel.State.NoMore,
|
||||
// is UserMediaTimelineViewModel.State.NotAuthorized,
|
||||
// is UserMediaTimelineViewModel.State.Blocked:
|
||||
// break
|
||||
// default:
|
||||
// if userMediaTimelineViewController.viewModel.items.value.isEmpty {
|
||||
// userMediaTimelineViewController.viewModel.stateMachine.enter(UserMediaTimelineViewModel.State.Reloading.self)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if let userLikeTimelineViewController = postTimelineViewController as? UserLikeTimelineViewController,
|
||||
// let currentState = userLikeTimelineViewController.viewModel.stateMachine.currentState {
|
||||
// switch currentState {
|
||||
// case is UserLikeTimelineViewModel.State.NoMore,
|
||||
// is UserLikeTimelineViewModel.State.NotAuthorized,
|
||||
// is UserLikeTimelineViewModel.State.Blocked:
|
||||
// break
|
||||
// default:
|
||||
// if userLikeTimelineViewController.viewModel.items.value.isEmpty {
|
||||
// userLikeTimelineViewController.viewModel.stateMachine.enter(UserLikeTimelineViewModel.State.Reloading.self)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ProfileBannerInfoActionViewDelegate
|
||||
//extension ProfileViewController: ProfileBannerInfoActionViewDelegate {
|
||||
//
|
||||
// func profileBannerInfoActionView(_ profileBannerInfoActionView: ProfileBannerInfoActionView, followActionButtonPressed button: FollowActionButton) {
|
||||
// UserProviderFacade
|
||||
// .toggleUserFriendship(provider: self, sender: button)
|
||||
// .sink { _ in
|
||||
// // do nothing
|
||||
// } receiveValue: { _ in
|
||||
// // do nothing
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
// MARK: - ProfileHeaderViewDelegate
|
||||
extension ProfileViewController: ProfileHeaderViewDelegate {
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed entity: ActiveEntity) {
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, relationshipButtonDidPressed button: ProfileRelationshipActionButton) {
|
||||
let relationshipActionSet = viewModel.relationshipActionOptionSet.value
|
||||
if relationshipActionSet.contains(.edit) {
|
||||
viewModel.isEditing.value.toggle()
|
||||
} else {
|
||||
guard let relationshipAction = relationshipActionSet.highPriorityAction(except: .editOptions) else { return }
|
||||
switch relationshipAction {
|
||||
case .none:
|
||||
break
|
||||
case .follow, .following:
|
||||
UserProviderFacade.toggleUserFollowRelationship(provider: self)
|
||||
.sink { _ in
|
||||
|
||||
} receiveValue: { _ in
|
||||
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
case .pending:
|
||||
break
|
||||
case .muting:
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
let name = mastodonUser.displayNameWithFallback
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnmuteUser.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let unmuteAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unmute, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
UserProviderFacade.toggleUserMuteRelationship(provider: self)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.context.disposeBag)
|
||||
}
|
||||
alertController.addAction(unmuteAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
case .blocking:
|
||||
guard let mastodonUser = viewModel.mastodonUser.value else { return }
|
||||
let name = mastodonUser.displayNameWithFallback
|
||||
let alertController = UIAlertController(
|
||||
title: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.title,
|
||||
message: L10n.Scene.Profile.RelationshipActionAlert.ConfirmUnblockUsre.message(name),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
let unblockAction = UIAlertAction(title: L10n.Common.Controls.Firendship.unblock, style: .default) { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
UserProviderFacade.toggleUserBlockRelationship(provider: self)
|
||||
.sink { _ in
|
||||
// do nothing
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
}
|
||||
.store(in: &self.context.disposeBag)
|
||||
}
|
||||
alertController.addAction(unblockAction)
|
||||
let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel, handler: nil)
|
||||
alertController.addAction(cancelAction)
|
||||
present(alertController, animated: true, completion: nil)
|
||||
case .blocked:
|
||||
break
|
||||
default:
|
||||
assertionFailure()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, activeLabel: ActiveLabel, entityDidPressed 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))
|
||||
default:
|
||||
// TODO:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func profileHeaderView(_ profileHeaderView: ProfileHeaderView, profileStatusDashboardView: ProfileStatusDashboardView, postDashboardMeterViewDidPressed dashboardMeterView: ProfileStatusDashboardMeterView) {
|
||||
|
|
|
@ -26,14 +26,12 @@ class ProfileViewModel: NSObject {
|
|||
let mastodonUser: CurrentValueSubject<MastodonUser?, Never>
|
||||
let currentMastodonUser = CurrentValueSubject<MastodonUser?, Never>(nil)
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let headerDomainLumaStyle = CurrentValueSubject<UIUserInterfaceStyle, Never>(.dark) // default dark for placeholder banner
|
||||
|
||||
// output
|
||||
let domain: CurrentValueSubject<String?, Never>
|
||||
let userID: CurrentValueSubject<UserID?, Never>
|
||||
let bannerImageURL: CurrentValueSubject<URL?, Never>
|
||||
let avatarImageURL: CurrentValueSubject<URL?, Never>
|
||||
// let protected: CurrentValueSubject<Bool?, Never>
|
||||
let name: CurrentValueSubject<String?, Never>
|
||||
let username: CurrentValueSubject<String?, Never>
|
||||
let bioDescription: CurrentValueSubject<String?, Never>
|
||||
|
@ -42,13 +40,19 @@ class ProfileViewModel: NSObject {
|
|||
let followingCount: CurrentValueSubject<Int?, Never>
|
||||
let followersCount: CurrentValueSubject<Int?, Never>
|
||||
|
||||
// let friendship: CurrentValueSubject<Friendship?, Never>
|
||||
// let followedBy: CurrentValueSubject<Bool?, Never>
|
||||
// let muted: CurrentValueSubject<Bool, Never>
|
||||
// let blocked: CurrentValueSubject<Bool, Never>
|
||||
//
|
||||
// let suspended = CurrentValueSubject<Bool, Never>(false)
|
||||
//
|
||||
let protected: CurrentValueSubject<Bool?, Never>
|
||||
// let suspended: CurrentValueSubject<Bool, Never>
|
||||
|
||||
let relationshipActionOptionSet = CurrentValueSubject<RelationshipActionOptionSet, Never>(.none)
|
||||
let isEditing = CurrentValueSubject<Bool, Never>(false)
|
||||
let isFollowedBy = CurrentValueSubject<Bool, Never>(false)
|
||||
let isMuting = CurrentValueSubject<Bool, Never>(false)
|
||||
let isBlocking = CurrentValueSubject<Bool, Never>(false)
|
||||
let isBlockedBy = CurrentValueSubject<Bool, Never>(false)
|
||||
|
||||
let isRelationshipActionButtonHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
let isReplyBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
let isMoreMenuBarButtonItemHidden = CurrentValueSubject<Bool, Never>(true)
|
||||
|
||||
init(context: AppContext, optionalMastodonUser mastodonUser: MastodonUser?) {
|
||||
self.context = context
|
||||
|
@ -65,12 +69,15 @@ class ProfileViewModel: NSObject {
|
|||
self.statusesCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.statusesCount) })
|
||||
self.followingCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followingCount) })
|
||||
self.followersCount = CurrentValueSubject(mastodonUser.flatMap { Int(truncating: $0.followersCount) })
|
||||
// self.friendship = CurrentValueSubject(nil)
|
||||
// self.followedBy = CurrentValueSubject(nil)
|
||||
// self.muted = CurrentValueSubject(false)
|
||||
// self.blocked = CurrentValueSubject(false)
|
||||
self.protected = CurrentValueSubject(mastodonUser?.locked)
|
||||
super.init()
|
||||
|
||||
relationshipActionOptionSet
|
||||
.compactMap { $0.highPriorityAction(except: []) }
|
||||
.map { $0 == .none }
|
||||
.assign(to: \.value, on: isRelationshipActionButtonHidden)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
// bind active authentication
|
||||
context.authenticationService.activeMastodonAuthentication
|
||||
.sink { [weak self] activeMastodonAuthentication in
|
||||
|
@ -85,25 +92,53 @@ class ProfileViewModel: NSObject {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
setup()
|
||||
}
|
||||
// query relationship
|
||||
let mastodonUserID = self.mastodonUser.map { $0?.id }
|
||||
let pendingRetryPublisher = CurrentValueSubject<TimeInterval, Never>(1)
|
||||
|
||||
}
|
||||
Publishers.CombineLatest3(
|
||||
mastodonUserID.removeDuplicates().eraseToAnyPublisher(),
|
||||
context.authenticationService.activeMastodonAuthenticationBox.eraseToAnyPublisher(),
|
||||
pendingRetryPublisher.eraseToAnyPublisher()
|
||||
)
|
||||
.compactMap { mastodonUserID, activeMastodonAuthenticationBox, _ -> (String, AuthenticationService.MastodonAuthenticationBox)? in
|
||||
guard let mastodonUserID = mastodonUserID, let activeMastodonAuthenticationBox = activeMastodonAuthenticationBox else { return nil }
|
||||
guard mastodonUserID != activeMastodonAuthenticationBox.userID else { return nil }
|
||||
return (mastodonUserID, activeMastodonAuthenticationBox)
|
||||
}
|
||||
.setFailureType(to: Error.self) // allow failure
|
||||
.flatMap { mastodonUserID, activeMastodonAuthenticationBox -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> in
|
||||
let domain = activeMastodonAuthenticationBox.domain
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch for user %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUserID)
|
||||
|
||||
extension ProfileViewModel {
|
||||
return self.context.apiService.relationship(domain: domain, accountIDs: [mastodonUserID], authorizationBox: activeMastodonAuthenticationBox)
|
||||
//.retry(3)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.sink { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
case .finished:
|
||||
break
|
||||
}
|
||||
} receiveValue: { response in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
|
||||
enum Friendship: CustomDebugStringConvertible {
|
||||
case following
|
||||
case pending
|
||||
case none
|
||||
|
||||
var debugDescription: String {
|
||||
switch self {
|
||||
case .following: return "following"
|
||||
case .pending: return "pending"
|
||||
case .none: return "none"
|
||||
// there are seconds delay after request follow before requested -> following. Query again when needs
|
||||
guard let relationship = response.value.first else { return }
|
||||
if relationship.requested == true {
|
||||
let delay = pendingRetryPublisher.value
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||
guard let _ = self else { return }
|
||||
pendingRetryPublisher.value = min(2 * delay, 60)
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] fetch again due to pending", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
setup()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -117,9 +152,11 @@ extension ProfileViewModel {
|
|||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] mastodonUser, currentMastodonUser in
|
||||
guard let self = self else { return }
|
||||
// Update view model attribute
|
||||
self.update(mastodonUser: mastodonUser)
|
||||
self.update(mastodonUser: mastodonUser, currentMastodonUser: currentMastodonUser)
|
||||
|
||||
// Setup observer for user
|
||||
if let mastodonUser = mastodonUser {
|
||||
// setup observer
|
||||
self.mastodonUserObserver = ManagedObjectObserver.observe(object: mastodonUser)
|
||||
|
@ -147,6 +184,7 @@ extension ProfileViewModel {
|
|||
self.mastodonUserObserver = nil
|
||||
}
|
||||
|
||||
// Setup observer for user
|
||||
if let currentMastodonUser = currentMastodonUser {
|
||||
// setup observer
|
||||
self.currentMastodonUserObserver = ManagedObjectObserver.observe(object: currentMastodonUser)
|
||||
|
@ -179,7 +217,6 @@ extension ProfileViewModel {
|
|||
self.userID.value = mastodonUser?.id
|
||||
self.bannerImageURL.value = mastodonUser?.headerImageURL()
|
||||
self.avatarImageURL.value = mastodonUser?.avatarImageURL()
|
||||
// self.protected.value = twitterUser?.protected
|
||||
self.name.value = mastodonUser?.displayNameWithFallback
|
||||
self.username.value = mastodonUser?.acctWithDomain
|
||||
self.bioDescription.value = mastodonUser?.note
|
||||
|
@ -187,11 +224,159 @@ extension ProfileViewModel {
|
|||
self.statusesCount.value = mastodonUser.flatMap { Int(truncating: $0.statusesCount) }
|
||||
self.followingCount.value = mastodonUser.flatMap { Int(truncating: $0.followingCount) }
|
||||
self.followersCount.value = mastodonUser.flatMap { Int(truncating: $0.followersCount) }
|
||||
self.protected.value = mastodonUser?.locked
|
||||
}
|
||||
|
||||
private func update(mastodonUser: MastodonUser?, currentMastodonUser: MastodonUser?) {
|
||||
// TODO:
|
||||
guard let mastodonUser = mastodonUser,
|
||||
let currentMastodonUser = currentMastodonUser else {
|
||||
// set relationship
|
||||
self.relationshipActionOptionSet.value = .none
|
||||
self.isFollowedBy.value = false
|
||||
self.isMuting.value = false
|
||||
self.isBlocking.value = false
|
||||
self.isBlockedBy.value = false
|
||||
|
||||
// set bar button item state
|
||||
self.isReplyBarButtonItemHidden.value = true
|
||||
self.isMoreMenuBarButtonItemHidden.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if mastodonUser == currentMastodonUser {
|
||||
self.relationshipActionOptionSet.value = [.edit]
|
||||
// set bar button item state
|
||||
self.isReplyBarButtonItemHidden.value = true
|
||||
self.isMoreMenuBarButtonItemHidden.value = true
|
||||
} else {
|
||||
// set with follow action default
|
||||
var relationshipActionSet = RelationshipActionOptionSet([.follow])
|
||||
|
||||
let isFollowing = mastodonUser.followingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isFollowing {
|
||||
relationshipActionSet.insert(.following)
|
||||
}
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowing: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowing.description)
|
||||
|
||||
let isPending = mastodonUser.followRequestedBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isPending {
|
||||
relationshipActionSet.insert(.pending)
|
||||
}
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isPending: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isPending.description)
|
||||
|
||||
let isFollowedBy = currentMastodonUser.followingBy.flatMap { $0.contains(mastodonUser) } ?? false
|
||||
self.isFollowedBy.value = isFollowedBy
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isFollowedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isFollowedBy.description)
|
||||
|
||||
let isMuting = mastodonUser.mutingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isMuting {
|
||||
relationshipActionSet.insert(.muting)
|
||||
}
|
||||
self.isMuting.value = isMuting
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isMuting: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isMuting.description)
|
||||
|
||||
let isBlocking = mastodonUser.blockingBy.flatMap { $0.contains(currentMastodonUser) } ?? false
|
||||
if isBlocking {
|
||||
relationshipActionSet.insert(.blocking)
|
||||
}
|
||||
self.isBlocking.value = isBlocking
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlocking: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlocking.description)
|
||||
|
||||
let isBlockedBy = currentMastodonUser.blockingBy.flatMap { $0.contains(mastodonUser) } ?? false
|
||||
if isBlockedBy {
|
||||
relationshipActionSet.insert(.blocked)
|
||||
}
|
||||
self.isBlockedBy.value = isBlockedBy
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] update %s isBlockedBy: %s", ((#file as NSString).lastPathComponent), #line, #function, mastodonUser.id, isBlockedBy.description)
|
||||
|
||||
self.relationshipActionOptionSet.value = relationshipActionSet
|
||||
|
||||
// set bar button item state
|
||||
self.isReplyBarButtonItemHidden.value = isBlocking || isBlockedBy
|
||||
self.isMoreMenuBarButtonItemHidden.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension ProfileViewModel {
|
||||
|
||||
enum RelationshipAction: Int, CaseIterable {
|
||||
case none // set hide from UI
|
||||
case follow
|
||||
case pending
|
||||
case following
|
||||
case muting
|
||||
case blocking
|
||||
case blocked
|
||||
case edit
|
||||
case editing
|
||||
|
||||
var option: RelationshipActionOptionSet {
|
||||
return RelationshipActionOptionSet(rawValue: 1 << rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
// construct option set on the enum for safe iterator
|
||||
struct RelationshipActionOptionSet: OptionSet {
|
||||
let rawValue: Int
|
||||
|
||||
static let none = RelationshipAction.none.option
|
||||
static let follow = RelationshipAction.follow.option
|
||||
static let pending = RelationshipAction.pending.option
|
||||
static let following = RelationshipAction.following.option
|
||||
static let muting = RelationshipAction.muting.option
|
||||
static let blocking = RelationshipAction.blocking.option
|
||||
static let blocked = RelationshipAction.blocked.option
|
||||
static let edit = RelationshipAction.edit.option
|
||||
static let editing = RelationshipAction.editing.option
|
||||
|
||||
static let editOptions: RelationshipActionOptionSet = [.edit, .editing]
|
||||
|
||||
func highPriorityAction(except: RelationshipActionOptionSet) -> RelationshipAction? {
|
||||
let set = subtracting(except)
|
||||
for action in RelationshipAction.allCases.reversed() where set.contains(action.option) {
|
||||
return action
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var title: String {
|
||||
guard let highPriorityAction = self.highPriorityAction(except: []) else {
|
||||
assertionFailure()
|
||||
return " "
|
||||
}
|
||||
switch highPriorityAction {
|
||||
case .none: return " "
|
||||
case .follow: return L10n.Common.Controls.Firendship.follow
|
||||
case .pending: return L10n.Common.Controls.Firendship.pending
|
||||
case .following: return L10n.Common.Controls.Firendship.following
|
||||
case .muting: return L10n.Common.Controls.Firendship.muted
|
||||
case .blocking: return L10n.Common.Controls.Firendship.blocked
|
||||
case .blocked: return L10n.Common.Controls.Firendship.follow // blocked by user
|
||||
case .edit: return L10n.Common.Controls.Firendship.editInfo
|
||||
case .editing: return L10n.Common.Controls.Actions.done
|
||||
}
|
||||
}
|
||||
|
||||
var backgroundColor: UIColor {
|
||||
guard let highPriorityAction = self.highPriorityAction(except: []) else {
|
||||
assertionFailure()
|
||||
return Asset.Colors.Button.normal.color
|
||||
}
|
||||
switch highPriorityAction {
|
||||
case .none: return Asset.Colors.Button.normal.color
|
||||
case .follow: return Asset.Colors.Button.normal.color
|
||||
case .pending: return Asset.Colors.Button.normal.color
|
||||
case .following: return Asset.Colors.Button.normal.color
|
||||
case .muting: return Asset.Colors.Background.alertYellow.color
|
||||
case .blocking: return Asset.Colors.Background.danger.color
|
||||
case .blocked: return Asset.Colors.Button.disabled.color
|
||||
case .edit: return Asset.Colors.Button.normal.color
|
||||
case .editing: return Asset.Colors.Button.normal.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,167 @@
|
|||
//
|
||||
// APIService+Block.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func toggleBlock(
|
||||
for mastodonUser: MastodonUser,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||
|
||||
return blockUpdateLocal(
|
||||
mastodonUserObjectID: mastodonUser.objectID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents { _ in
|
||||
impactFeedbackGenerator.prepare()
|
||||
} receiveOutput: { _ in
|
||||
impactFeedbackGenerator.impactOccurred()
|
||||
} receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] local relationship update fail", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] local relationship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
break
|
||||
}
|
||||
}
|
||||
.flatMap { blockQueryType, mastodonUserID -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> in
|
||||
return self.blockUpdateRemote(
|
||||
blockQueryType: blockQueryType,
|
||||
mastodonUserID: mastodonUserID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log("%{public}s[%{public}ld], %{public}s: [Relationship] remote friendship update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
// TODO: handle error
|
||||
|
||||
// rollback
|
||||
|
||||
self.blockUpdateLocal(
|
||||
mastodonUserObjectID: mastodonUser.objectID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.sink { completion in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
notificationFeedbackGenerator.prepare()
|
||||
notificationFeedbackGenerator.notificationOccurred(.error)
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
|
||||
case .finished:
|
||||
notificationFeedbackGenerator.notificationOccurred(.success)
|
||||
os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService {
|
||||
|
||||
// update database local and return block query update type for remote request
|
||||
func blockUpdateLocal(
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<(Mastodon.API.Account.BlockQueryType, MastodonUser.ID), Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
||||
var _targetMastodonUserID: MastodonUser.ID?
|
||||
var _queryType: Mastodon.API.Account.BlockQueryType?
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
|
||||
return managedObjectContext.performChanges {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
guard let _requestMastodonUser = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||
_targetMastodonUserID = mastodonUser.id
|
||||
|
||||
let isBlocking = (mastodonUser.blockingBy ?? Set()).contains(_requestMastodonUser)
|
||||
_queryType = isBlocking ? .unblock : .block
|
||||
mastodonUser.update(isBlocking: !isBlocking, by: _requestMastodonUser)
|
||||
}
|
||||
.tryMap { result in
|
||||
switch result {
|
||||
case .success:
|
||||
guard let targetMastodonUserID = _targetMastodonUserID,
|
||||
let queryType = _queryType else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
return (queryType, targetMastodonUserID)
|
||||
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func blockUpdateRemote(
|
||||
blockQueryType: Mastodon.API.Account.BlockQueryType,
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Account.block(
|
||||
session: session,
|
||||
domain: domain,
|
||||
accountID: mastodonUserID,
|
||||
blockQueryType: blockQueryType,
|
||||
authorization: authorization
|
||||
)
|
||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
break
|
||||
case .finished:
|
||||
// TODO: update relationship
|
||||
switch blockQueryType {
|
||||
case .block:
|
||||
break
|
||||
case .unblock:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,187 @@
|
|||
//
|
||||
// APIService+Follow.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
/// Toggle friendship between target MastodonUser and current MastodonUser
|
||||
///
|
||||
/// Following / Following pending <-> Unfollow
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - mastodonUser: target MastodonUser
|
||||
/// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox`
|
||||
/// - Returns: publisher for `Relationship`
|
||||
func toggleFollow(
|
||||
for mastodonUser: MastodonUser,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||
|
||||
return followUpdateLocal(
|
||||
mastodonUserObjectID: mastodonUser.objectID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents { _ in
|
||||
impactFeedbackGenerator.prepare()
|
||||
} receiveOutput: { _ in
|
||||
impactFeedbackGenerator.impactOccurred()
|
||||
} receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] local relationship update fail", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] local relationship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
break
|
||||
}
|
||||
}
|
||||
.flatMap { followQueryType, mastodonUserID -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> in
|
||||
return self.followUpdateRemote(
|
||||
followQueryType: followQueryType,
|
||||
mastodonUserID: mastodonUserID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log("%{public}s[%{public}ld], %{public}s: [Relationship] remote friendship update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
// TODO: handle error
|
||||
|
||||
// rollback
|
||||
|
||||
self.followUpdateLocal(
|
||||
mastodonUserObjectID: mastodonUser.objectID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.sink { completion in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
notificationFeedbackGenerator.prepare()
|
||||
notificationFeedbackGenerator.notificationOccurred(.error)
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
|
||||
case .finished:
|
||||
notificationFeedbackGenerator.notificationOccurred(.success)
|
||||
os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService {
|
||||
|
||||
// update database local and return follow query update type for remote request
|
||||
func followUpdateLocal(
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<(Mastodon.API.Account.FollowQueryType, MastodonUser.ID), Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
||||
var _targetMastodonUserID: MastodonUser.ID?
|
||||
var _queryType: Mastodon.API.Account.FollowQueryType?
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
|
||||
return managedObjectContext.performChanges {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
guard let _requestMastodonUser = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||
_targetMastodonUserID = mastodonUser.id
|
||||
|
||||
let isPending = (mastodonUser.followRequestedBy ?? Set()).contains(_requestMastodonUser)
|
||||
let isFollowing = (mastodonUser.followingBy ?? Set()).contains(_requestMastodonUser)
|
||||
|
||||
if isFollowing || isPending {
|
||||
_queryType = .unfollow
|
||||
mastodonUser.update(isFollowing: false, by: _requestMastodonUser)
|
||||
mastodonUser.update(isFollowRequested: false, by: _requestMastodonUser)
|
||||
} else {
|
||||
_queryType = .follow(query: Mastodon.API.Account.FollowQuery())
|
||||
if mastodonUser.locked {
|
||||
mastodonUser.update(isFollowing: false, by: _requestMastodonUser)
|
||||
mastodonUser.update(isFollowRequested: true, by: _requestMastodonUser)
|
||||
} else {
|
||||
mastodonUser.update(isFollowing: true, by: _requestMastodonUser)
|
||||
mastodonUser.update(isFollowRequested: false, by: _requestMastodonUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
.tryMap { result in
|
||||
switch result {
|
||||
case .success:
|
||||
guard let targetMastodonUserID = _targetMastodonUserID,
|
||||
let queryType = _queryType else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
return (queryType, targetMastodonUserID)
|
||||
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func followUpdateRemote(
|
||||
followQueryType: Mastodon.API.Account.FollowQueryType,
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Account.follow(
|
||||
session: session,
|
||||
domain: domain,
|
||||
accountID: mastodonUserID,
|
||||
followQueryType: followQueryType,
|
||||
authorization: authorization
|
||||
)
|
||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
break
|
||||
case .finished:
|
||||
switch followQueryType {
|
||||
case .follow:
|
||||
break
|
||||
case .unfollow:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
//
|
||||
// APIService+Mute.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-4-2.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import CommonOSLog
|
||||
import MastodonSDK
|
||||
|
||||
extension APIService {
|
||||
|
||||
func toggleMute(
|
||||
for mastodonUser: MastodonUser,
|
||||
activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||
let notificationFeedbackGenerator = UINotificationFeedbackGenerator()
|
||||
|
||||
return muteUpdateLocal(
|
||||
mastodonUserObjectID: mastodonUser.objectID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents { _ in
|
||||
impactFeedbackGenerator.prepare()
|
||||
} receiveOutput: { _ in
|
||||
impactFeedbackGenerator.impactOccurred()
|
||||
} receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] local relationship update fail", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
assertionFailure(error.localizedDescription)
|
||||
case .finished:
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Relationship] local relationship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
break
|
||||
}
|
||||
}
|
||||
.flatMap { muteQueryType, mastodonUserID -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> in
|
||||
return self.muteUpdateRemote(
|
||||
muteQueryType: muteQueryType,
|
||||
mastodonUserID: mastodonUserID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
}
|
||||
.receive(on: DispatchQueue.main)
|
||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
os_log("%{public}s[%{public}ld], %{public}s: [Relationship] remote friendship update fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||
// TODO: handle error
|
||||
|
||||
// rollback
|
||||
|
||||
self.muteUpdateLocal(
|
||||
mastodonUserObjectID: mastodonUser.objectID,
|
||||
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||
)
|
||||
.sink { completion in
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: [Friendship] rollback finish", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
} receiveValue: { _ in
|
||||
// do nothing
|
||||
notificationFeedbackGenerator.prepare()
|
||||
notificationFeedbackGenerator.notificationOccurred(.error)
|
||||
}
|
||||
.store(in: &self.disposeBag)
|
||||
|
||||
case .finished:
|
||||
notificationFeedbackGenerator.notificationOccurred(.success)
|
||||
os_log("%{public}s[%{public}ld], %{public}s: [Friendship] remote friendship update success", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension APIService {
|
||||
|
||||
// update database local and return mute query update type for remote request
|
||||
func muteUpdateLocal(
|
||||
mastodonUserObjectID: NSManagedObjectID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<(Mastodon.API.Account.MuteQueryType, MastodonUser.ID), Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let requestMastodonUserID = mastodonAuthenticationBox.userID
|
||||
|
||||
var _targetMastodonUserID: MastodonUser.ID?
|
||||
var _queryType: Mastodon.API.Account.MuteQueryType?
|
||||
let managedObjectContext = backgroundManagedObjectContext
|
||||
|
||||
return managedObjectContext.performChanges {
|
||||
let request = MastodonUser.sortedFetchRequest
|
||||
request.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID)
|
||||
request.fetchLimit = 1
|
||||
request.returnsObjectsAsFaults = false
|
||||
guard let _requestMastodonUser = managedObjectContext.safeFetch(request).first else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser
|
||||
_targetMastodonUserID = mastodonUser.id
|
||||
|
||||
let isMuting = (mastodonUser.mutingBy ?? Set()).contains(_requestMastodonUser)
|
||||
_queryType = isMuting ? .unmute : .mute
|
||||
mastodonUser.update(isMuting: !isMuting, by: _requestMastodonUser)
|
||||
}
|
||||
.tryMap { result in
|
||||
switch result {
|
||||
case .success:
|
||||
guard let targetMastodonUserID = _targetMastodonUserID,
|
||||
let queryType = _queryType else {
|
||||
throw APIError.implicit(.badRequest)
|
||||
}
|
||||
return (queryType, targetMastodonUserID)
|
||||
|
||||
case .failure(let error):
|
||||
assertionFailure(error.localizedDescription)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func muteUpdateRemote(
|
||||
muteQueryType: Mastodon.API.Account.MuteQueryType,
|
||||
mastodonUserID: MastodonUser.ID,
|
||||
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let domain = mastodonAuthenticationBox.domain
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Account.mute(
|
||||
session: session,
|
||||
domain: domain,
|
||||
accountID: mastodonUserID,
|
||||
muteQueryType: muteQueryType,
|
||||
authorization: authorization
|
||||
)
|
||||
.handleEvents(receiveCompletion: { [weak self] completion in
|
||||
guard let self = self else { return }
|
||||
switch completion {
|
||||
case .failure(let error):
|
||||
// TODO: handle error
|
||||
break
|
||||
case .finished:
|
||||
// TODO: update relationship
|
||||
switch muteQueryType {
|
||||
case .mute:
|
||||
break
|
||||
case .unmute:
|
||||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
// Created by MainasuK Cirno on 2021-4-1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
|
@ -19,47 +19,47 @@ extension APIService {
|
|||
accountIDs: [Mastodon.Entity.Account.ID],
|
||||
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> {
|
||||
fatalError()
|
||||
// let authorization = authorizationBox.userAuthorization
|
||||
// let requestMastodonUserID = authorizationBox.userID
|
||||
// let query = Mastodon.API.Account.AccountStatuseseQuery(
|
||||
// maxID: maxID,
|
||||
// sinceID: sinceID,
|
||||
// excludeReplies: excludeReplies,
|
||||
// excludeReblogs: excludeReblogs,
|
||||
// onlyMedia: onlyMedia,
|
||||
// limit: limit
|
||||
// )
|
||||
//
|
||||
// return Mastodon.API.Account.statuses(
|
||||
// session: session,
|
||||
// domain: domain,
|
||||
// accountID: accountID,
|
||||
// query: query,
|
||||
// authorization: authorization
|
||||
// )
|
||||
// .flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Status]>, Error> in
|
||||
// return APIService.Persist.persistStatus(
|
||||
// managedObjectContext: self.backgroundManagedObjectContext,
|
||||
// domain: domain,
|
||||
// query: nil,
|
||||
// response: response,
|
||||
// persistType: .user,
|
||||
// requestMastodonUserID: requestMastodonUserID,
|
||||
// log: OSLog.api
|
||||
// )
|
||||
// .setFailureType(to: Error.self)
|
||||
// .tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Status]> in
|
||||
// switch result {
|
||||
// case .success:
|
||||
// return response
|
||||
// case .failure(let error):
|
||||
// throw error
|
||||
// }
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
// }
|
||||
// .eraseToAnyPublisher()
|
||||
let authorization = authorizationBox.userAuthorization
|
||||
let requestMastodonUserID = authorizationBox.userID
|
||||
let query = Mastodon.API.Account.RelationshipQuery(
|
||||
ids: accountIDs
|
||||
)
|
||||
|
||||
return Mastodon.API.Account.relationships(
|
||||
session: session,
|
||||
domain: domain,
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
.flatMap { response -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Relationship]>, Error> in
|
||||
let managedObjectContext = self.backgroundManagedObjectContext
|
||||
return managedObjectContext.performChanges {
|
||||
let requestMastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
requestMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, id: requestMastodonUserID)
|
||||
requestMastodonUserRequest.fetchLimit = 1
|
||||
guard let requestMastodonUser = managedObjectContext.safeFetch(requestMastodonUserRequest).first else { return }
|
||||
|
||||
let lookUpMastodonUserRequest = MastodonUser.sortedFetchRequest
|
||||
lookUpMastodonUserRequest.predicate = MastodonUser.predicate(domain: domain, ids: accountIDs)
|
||||
lookUpMastodonUserRequest.fetchLimit = accountIDs.count
|
||||
let lookUpMastodonusers = managedObjectContext.safeFetch(lookUpMastodonUserRequest)
|
||||
|
||||
for user in lookUpMastodonusers {
|
||||
guard let entity = response.value.first(where: { $0.id == user.id }) else { continue }
|
||||
APIService.CoreData.update(user: user, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate)
|
||||
}
|
||||
}
|
||||
.tryMap { result -> Mastodon.Response.Content<[Mastodon.Entity.Relationship]> in
|
||||
switch result {
|
||||
case .success:
|
||||
return response
|
||||
case .failure(let error):
|
||||
throw error
|
||||
}
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ extension APIService.CoreData {
|
|||
networkDate: Date
|
||||
) {
|
||||
guard networkDate > user.updatedAt else { return }
|
||||
guard entity.id != requestMastodonUser.id else { return } // not update relationship for self
|
||||
|
||||
user.update(isFollowing: entity.following, by: requestMastodonUser)
|
||||
entity.requested.flatMap { user.update(isFollowRequested: $0, by: requestMastodonUser) }
|
||||
|
|
|
@ -67,3 +67,350 @@ extension Mastodon.API.Account {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
public enum FollowQueryType {
|
||||
case follow(query: FollowQuery)
|
||||
case unfollow
|
||||
}
|
||||
|
||||
public static func follow(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
followQueryType: FollowQueryType,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
switch followQueryType {
|
||||
case .follow(let query):
|
||||
return follow(session: session, domain: domain, accountID: accountID, query: query, authorization: authorization)
|
||||
case .unfollow:
|
||||
return unfollow(session: session, domain: domain, accountID: accountID, authorization: authorization)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func followEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
|
||||
let pathComponent = "accounts/" + accountID + "/follow"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Follow
|
||||
///
|
||||
/// Follow the given account. Can also be used to update whether to show reblogs or enable notifications.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - accountID: id for account
|
||||
/// - authorization: User token.
|
||||
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||
public static func follow(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
query: FollowQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: followEndpointURL(domain: domain, accountID: accountID),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct FollowQuery: Codable, PostQuery {
|
||||
public let reblogs: Bool?
|
||||
public let notify: Bool?
|
||||
|
||||
public init(reblogs: Bool? = nil , notify: Bool? = nil) {
|
||||
self.reblogs = reblogs
|
||||
self.notify = notify
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func unfollowEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
|
||||
let pathComponent = "accounts/" + accountID + "/unfollow"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Unfollow
|
||||
///
|
||||
/// Unfollow the given account.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - accountID: id for account
|
||||
/// - authorization: User token.
|
||||
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||
public static func unfollow(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: unfollowEndpointURL(domain: domain, accountID: accountID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
public enum BlockQueryType {
|
||||
case block
|
||||
case unblock
|
||||
}
|
||||
|
||||
public static func block(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
blockQueryType: BlockQueryType,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
switch blockQueryType {
|
||||
case .block:
|
||||
return block(session: session, domain: domain, accountID: accountID, authorization: authorization)
|
||||
case .unblock:
|
||||
return unblock(session: session, domain: domain, accountID: accountID, authorization: authorization)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func blockEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
|
||||
let pathComponent = "accounts/" + accountID + "/block"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Block
|
||||
///
|
||||
/// Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline).
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - accountID: id for account
|
||||
/// - authorization: User token.
|
||||
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||
public static func block(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: blockEndpointURL(domain: domain, accountID: accountID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func unblockEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
|
||||
let pathComponent = "accounts/" + accountID + "/unblock"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Unblock
|
||||
///
|
||||
/// Unblock the given account.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - accountID: id for account
|
||||
/// - authorization: User token.
|
||||
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||
public static func unblock(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: unblockEndpointURL(domain: domain, accountID: accountID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
public enum MuteQueryType {
|
||||
case mute
|
||||
case unmute
|
||||
}
|
||||
|
||||
public static func mute(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
muteQueryType: MuteQueryType,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
switch muteQueryType {
|
||||
case .mute:
|
||||
return mute(session: session, domain: domain, accountID: accountID, authorization: authorization)
|
||||
case .unmute:
|
||||
return unmute(session: session, domain: domain, accountID: accountID, authorization: authorization)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func mutekEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
|
||||
let pathComponent = "accounts/" + accountID + "/mute"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Mute
|
||||
///
|
||||
/// Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - accountID: id for account
|
||||
/// - authorization: User token.
|
||||
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||
public static func mute(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: mutekEndpointURL(domain: domain, accountID: accountID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension Mastodon.API.Account {
|
||||
|
||||
static func unmutekEndpointURL(domain: String, accountID: Mastodon.Entity.Account.ID) -> URL {
|
||||
let pathComponent = "accounts/" + accountID + "/unmute"
|
||||
return Mastodon.API.endpointURL(domain: domain).appendingPathComponent(pathComponent)
|
||||
}
|
||||
|
||||
/// Unmute
|
||||
///
|
||||
/// Unmute the given account.
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.3.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/accounts/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - accountID: id for account
|
||||
/// - authorization: User token.
|
||||
/// - Returns: `AnyPublisher` contains `Relationship` nested in the response
|
||||
public static func unmute(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
accountID: Mastodon.Entity.Account.ID,
|
||||
authorization: Mastodon.API.OAuth.Authorization
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Relationship>, Error> {
|
||||
let request = Mastodon.API.post(
|
||||
url: unmutekEndpointURL(domain: domain, accountID: accountID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Relationship.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue