Merge branch develop into feature/push-notification
# Conflicts: # Mastodon.xcodeproj/project.pbxproj # Mastodon/Scene/HomeTimeline/HomeTimelineViewController+DebugAction.swift # Mastodon/Scene/Settings/SettingsViewModel.swift
This commit is contained in:
commit
5d52fc4578
|
@ -53,7 +53,9 @@
|
||||||
"share_user": "Share %s",
|
"share_user": "Share %s",
|
||||||
"open_in_safari": "Open in Safari",
|
"open_in_safari": "Open in Safari",
|
||||||
"find_people": "Find people to follow",
|
"find_people": "Find people to follow",
|
||||||
"manually_search": "Manually search instead"
|
"manually_search": "Manually search instead",
|
||||||
|
"skip": "Skip",
|
||||||
|
"report_user": "Report %s"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"user_reblogged": "%s reblogged",
|
"user_reblogged": "%s reblogged",
|
||||||
|
@ -348,7 +350,7 @@
|
||||||
"reblog": "rebloged your post",
|
"reblog": "rebloged your post",
|
||||||
"poll": "Your poll has ended",
|
"poll": "Your poll has ended",
|
||||||
"mention": "mentioned you"
|
"mention": "mentioned you"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
"thread": {
|
"thread": {
|
||||||
"back_title": "Post",
|
"back_title": "Post",
|
||||||
|
@ -396,6 +398,16 @@
|
||||||
"signout": "Sign Out"
|
"signout": "Sign Out"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"report": {
|
||||||
|
"title": "Report %s",
|
||||||
|
"step1": "Step 1 of 2",
|
||||||
|
"step2": "Step 2 of 2",
|
||||||
|
"content1": "Are there any other posts you’d like to add to the report?",
|
||||||
|
"content2": "Is there anything the moderators should know about this report?",
|
||||||
|
"send": "Send Report",
|
||||||
|
"skip_to_send": "Send without comment",
|
||||||
|
"text_placeholder": "Type or paste additional comments"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -139,6 +139,10 @@
|
||||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; };
|
||||||
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; };
|
2DFAD5272616F9D300F9EE7C /* SearchViewController+Searching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5262616F9D300F9EE7C /* SearchViewController+Searching.swift */; };
|
||||||
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */; };
|
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFAD5362617010500F9EE7C /* SearchingTableViewCell.swift */; };
|
||||||
|
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD7262DB14800A9381B /* ReportViewModel.swift */; };
|
||||||
|
5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */; };
|
||||||
|
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B24BBE1262DB19100A9381B /* APIService+Report.swift */; };
|
||||||
|
5B8E055826319E47006E3C53 /* ReportFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8E055726319E47006E3C53 /* ReportFooterView.swift */; };
|
||||||
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; };
|
5B90C45E262599800002E742 /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C456262599800002E742 /* SettingsViewModel.swift */; };
|
||||||
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; };
|
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; };
|
||||||
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; };
|
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; };
|
||||||
|
@ -149,6 +153,11 @@
|
||||||
5B90C47F26259BA90002E742 /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */; };
|
5B90C47F26259BA90002E742 /* SubscriptionAlerts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */; };
|
||||||
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */; };
|
5B90C48526259BF10002E742 /* APIService+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */; };
|
||||||
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */; };
|
5B90C48B26259C120002E742 /* APIService+CoreData+Subscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */; };
|
||||||
|
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */; };
|
||||||
|
5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */; };
|
||||||
|
5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */; };
|
||||||
|
5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */; };
|
||||||
|
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */; };
|
||||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
|
5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; };
|
||||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
||||||
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; };
|
||||||
|
@ -669,7 +678,11 @@
|
||||||
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
459EA4F43058CAB47719E963 /* Pods-Mastodon-MastodonUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon-MastodonUITests.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon-MastodonUITests/Pods-Mastodon-MastodonUITests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||||
46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
46531DECCAB422F507B2274D /* Pods_Mastodon_AppShared.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_AppShared.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
5B24BBE1262DB19100A9381B /* APIService+Report.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Report.swift"; sourceTree = "<group>"; };
|
||||||
|
5B8E055726319E47006E3C53 /* ReportFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportFooterView.swift; sourceTree = "<group>"; };
|
||||||
5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
5B90C456262599800002E742 /* SettingsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = "<group>"; };
|
||||||
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = "<group>"; };
|
5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsToggleTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = "<group>"; };
|
5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAppearanceTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -680,6 +693,11 @@
|
||||||
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = "<group>"; };
|
5B90C47E26259BA90002E742 /* SubscriptionAlerts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionAlerts.swift; sourceTree = "<group>"; };
|
||||||
5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Subscriptions.swift"; sourceTree = "<group>"; };
|
5B90C48426259BF10002E742 /* APIService+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+Subscriptions.swift"; sourceTree = "<group>"; };
|
||||||
5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Subscriptions.swift"; sourceTree = "<group>"; };
|
5B90C48A26259C120002E742 /* APIService+CoreData+Subscriptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Subscriptions.swift"; sourceTree = "<group>"; };
|
||||||
|
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewController.swift; sourceTree = "<group>"; };
|
||||||
|
5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportedStatusTableviewCell.swift; sourceTree = "<group>"; };
|
||||||
|
5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+Data.swift"; sourceTree = "<group>"; };
|
||||||
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSection.swift; sourceTree = "<group>"; };
|
||||||
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||||
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
|
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
|
||||||
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = "<group>"; };
|
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -1313,6 +1331,7 @@
|
||||||
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */,
|
||||||
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */,
|
||||||
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */,
|
||||||
|
5BB04FF4262F0E6D0043BFF6 /* ReportSection.swift */,
|
||||||
);
|
);
|
||||||
path = Section;
|
path = Section;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1440,6 +1459,20 @@
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
5B24BBD6262DB14800A9381B /* Report */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */,
|
||||||
|
5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */,
|
||||||
|
5BB04FEE262F0DCB0043BFF6 /* ReportViewModel+Data.swift */,
|
||||||
|
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */,
|
||||||
|
5BB04FDA262EA3070043BFF6 /* ReportHeaderView.swift */,
|
||||||
|
5B8E055726319E47006E3C53 /* ReportFooterView.swift */,
|
||||||
|
5BB04FE8262EFC300043BFF6 /* ReportedStatusTableviewCell.swift */,
|
||||||
|
);
|
||||||
|
path = Report;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
5B90C455262599800002E742 /* Settings */ = {
|
5B90C455262599800002E742 /* Settings */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1659,6 +1692,7 @@
|
||||||
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
2D34D9DA261494120081BFC0 /* APIService+Search.swift */,
|
||||||
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */,
|
0F202212261351F5000C64BF /* APIService+HashtagTimeline.swift */,
|
||||||
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
DBCC3B9426157E6E0045B23D /* APIService+Relationship.swift */,
|
||||||
|
5B24BBE1262DB19100A9381B /* APIService+Report.swift */,
|
||||||
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
DBAE3F932616E28B004B8251 /* APIService+Follow.swift */,
|
||||||
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
DBAE3F8D2616E0B1004B8251 /* APIService+Block.swift */,
|
||||||
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
DBAE3F9D2616E308004B8251 /* APIService+Mute.swift */,
|
||||||
|
@ -1914,6 +1948,7 @@
|
||||||
DB01409B25C40BB600F9F3CF /* Onboarding */,
|
DB01409B25C40BB600F9F3CF /* Onboarding */,
|
||||||
2D38F1D325CD463600561493 /* HomeTimeline */,
|
2D38F1D325CD463600561493 /* HomeTimeline */,
|
||||||
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
2D76316325C14BAC00929FB9 /* PublicTimeline */,
|
||||||
|
5B24BBD6262DB14800A9381B /* Report */,
|
||||||
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
0F2021F5261325ED000C64BF /* HashtagTimeline */,
|
||||||
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */,
|
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */,
|
||||||
DB9D6BEE25E4F5370051B173 /* Search */,
|
DB9D6BEE25E4F5370051B173 /* Search */,
|
||||||
|
@ -2718,10 +2753,13 @@
|
||||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||||
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */,
|
2DFAD5372617010500F9EE7C /* SearchingTableViewCell.swift in Sources */,
|
||||||
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
|
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
|
||||||
|
5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */,
|
||||||
|
5B8E055826319E47006E3C53 /* ReportFooterView.swift in Sources */,
|
||||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||||
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
0FB3D33225E5F50E00AAD544 /* PickServerSearchCell.swift in Sources */,
|
||||||
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
|
DB71FD5225F8CCAA00512AE1 /* APIService+Status.swift in Sources */,
|
||||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
|
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */,
|
||||||
|
5B24BBDA262DB14800A9381B /* ReportViewModel.swift in Sources */,
|
||||||
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */,
|
||||||
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
|
DB1E347825F519300079D7DF /* PickServerItem.swift in Sources */,
|
||||||
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
DB1FD45A25F27898004CFCFC /* CategoryPickerItem.swift in Sources */,
|
||||||
|
@ -2834,6 +2872,7 @@
|
||||||
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */,
|
||||||
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
DB49A61F25FF32AA00B98345 /* EmojiService+CustomEmojiViewModel.swift in Sources */,
|
||||||
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */,
|
||||||
|
5B24BBDB262DB14800A9381B /* ReportViewModel+Diffable.swift in Sources */,
|
||||||
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
DB71FD3C25F8A1C500512AE1 /* APIService+Persist+PersistCache.swift in Sources */,
|
||||||
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
2DA6055125F74407006356F9 /* AudioContainerViewModel.swift in Sources */,
|
||||||
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
0FB3D2FE25E4CB6400AAD544 /* PickServerTitleCell.swift in Sources */,
|
||||||
|
@ -2888,6 +2927,7 @@
|
||||||
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */,
|
||||||
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
2D61254D262547C200299647 /* APIService+Notification.swift in Sources */,
|
||||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||||
|
5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */,
|
||||||
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
DBAE3F822615DDA3004B8251 /* ProfileViewController+UserProvider.swift in Sources */,
|
||||||
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */,
|
||||||
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
DBC7A67C260DFADE00E57475 /* StatusPublishService.swift in Sources */,
|
||||||
|
@ -2895,6 +2935,7 @@
|
||||||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */,
|
||||||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||||
|
5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */,
|
||||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||||
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
|
DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */,
|
||||||
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */,
|
||||||
|
@ -3005,6 +3046,8 @@
|
||||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
||||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||||
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */,
|
||||||
|
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
||||||
|
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
||||||
DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */,
|
DB938F25262438D600E5B6C1 /* ThreadViewController+StatusProvider.swift in Sources */,
|
||||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||||
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */,
|
||||||
|
@ -3017,6 +3060,7 @@
|
||||||
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */,
|
||||||
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToStatusContentCollectionViewCell.swift in Sources */,
|
||||||
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
DBE3CE13261D7D4200430CC6 /* StatusTableViewControllerAspect.swift in Sources */,
|
||||||
|
5BB04FD5262E7AFF0043BFF6 /* ReportViewController.swift in Sources */,
|
||||||
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */,
|
DBAE3F942616E28B004B8251 /* APIService+Follow.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
@ -64,6 +64,9 @@ extension SceneCoordinator {
|
||||||
|
|
||||||
// setting
|
// setting
|
||||||
case settings(viewModel: SettingsViewModel)
|
case settings(viewModel: SettingsViewModel)
|
||||||
|
|
||||||
|
// report
|
||||||
|
case report(viewModel: ReportViewModel)
|
||||||
|
|
||||||
// suggestion account
|
// suggestion account
|
||||||
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
||||||
|
@ -72,7 +75,6 @@ extension SceneCoordinator {
|
||||||
case safari(url: URL)
|
case safari(url: URL)
|
||||||
case alertController(alertController: UIAlertController)
|
case alertController(alertController: UIAlertController)
|
||||||
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
|
case activityViewController(activityViewController: UIActivityViewController, sourceView: UIView?, barButtonItem: UIBarButtonItem?)
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
case publicTimeline
|
case publicTimeline
|
||||||
#endif
|
#endif
|
||||||
|
@ -283,6 +285,14 @@ private extension SceneCoordinator {
|
||||||
activityViewController.popoverPresentationController?.sourceView = sourceView
|
activityViewController.popoverPresentationController?.sourceView = sourceView
|
||||||
activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
|
activityViewController.popoverPresentationController?.barButtonItem = barButtonItem
|
||||||
viewController = activityViewController
|
viewController = activityViewController
|
||||||
|
case .settings(let viewModel):
|
||||||
|
let _viewController = SettingsViewController()
|
||||||
|
_viewController.viewModel = viewModel
|
||||||
|
viewController = _viewController
|
||||||
|
case .report(let viewModel):
|
||||||
|
let _viewController = ReportViewController()
|
||||||
|
_viewController.viewModel = viewModel
|
||||||
|
viewController = _viewController
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
case .publicTimeline:
|
case .publicTimeline:
|
||||||
let _viewController = PublicTimelineViewController()
|
let _viewController = PublicTimelineViewController()
|
||||||
|
|
|
@ -32,6 +32,9 @@ enum Item {
|
||||||
case bottomLoader
|
case bottomLoader
|
||||||
|
|
||||||
case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
|
case emptyStateHeader(attribute: EmptyStateHeaderAttribute)
|
||||||
|
|
||||||
|
// reports
|
||||||
|
case reportStatus(objectID: NSManagedObjectID, attribute: ReportStatusAttribute)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item {
|
extension Item {
|
||||||
|
@ -79,6 +82,15 @@ extension Item {
|
||||||
hasher.combine(id)
|
hasher.combine(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ReportStatusAttribute: StatusAttribute {
|
||||||
|
var isSelected: Bool
|
||||||
|
|
||||||
|
init(isSeparatorLineHidden: Bool = false, isSelected: Bool = false) {
|
||||||
|
self.isSelected = isSelected
|
||||||
|
super.init(isSeparatorLineHidden: isSeparatorLineHidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Item: Equatable {
|
extension Item: Equatable {
|
||||||
|
@ -106,6 +118,8 @@ extension Item: Equatable {
|
||||||
return true
|
return true
|
||||||
case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
|
case (.emptyStateHeader(let attributeLeft), .emptyStateHeader(let attributeRight)):
|
||||||
return attributeLeft == attributeRight
|
return attributeLeft == attributeRight
|
||||||
|
case (.reportStatus(let objectIDLeft, _), .reportStatus(let objectIDRight, _)):
|
||||||
|
return objectIDLeft == objectIDRight
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -139,6 +153,8 @@ extension Item: Hashable {
|
||||||
hasher.combine(String(describing: Item.bottomLoader.self))
|
hasher.combine(String(describing: Item.bottomLoader.self))
|
||||||
case .emptyStateHeader(let attribute):
|
case .emptyStateHeader(let attribute):
|
||||||
hasher.combine(attribute)
|
hasher.combine(attribute)
|
||||||
|
case .reportStatus(let objectID, _):
|
||||||
|
hasher.combine(objectID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
//
|
||||||
|
// ReportSection.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
enum ReportSection: Equatable, Hashable {
|
||||||
|
case main
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReportSection {
|
||||||
|
static func tableViewDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
dependency: ReportViewController,
|
||||||
|
managedObjectContext: NSManagedObjectContext,
|
||||||
|
timestampUpdatePublisher: AnyPublisher<Date, Never>
|
||||||
|
) -> UITableViewDiffableDataSource<ReportSection, Item> {
|
||||||
|
UITableViewDiffableDataSource(tableView: tableView) {[
|
||||||
|
weak dependency
|
||||||
|
] tableView, indexPath, item -> UITableViewCell? in
|
||||||
|
guard let dependency = dependency else { return UITableViewCell() }
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .reportStatus(let objectID, let attribute):
|
||||||
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportedStatusTableViewCell.self), for: indexPath) as! ReportedStatusTableViewCell
|
||||||
|
cell.dependency = dependency
|
||||||
|
let activeMastodonAuthenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value
|
||||||
|
let requestUserID = activeMastodonAuthenticationBox?.userID ?? ""
|
||||||
|
managedObjectContext.performAndWait { [weak dependency] in
|
||||||
|
guard let dependency = dependency else { return }
|
||||||
|
let status = managedObjectContext.object(with: objectID) as! Status
|
||||||
|
StatusSection.configure(
|
||||||
|
cell: cell,
|
||||||
|
dependency: dependency,
|
||||||
|
readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher,
|
||||||
|
status: status,
|
||||||
|
requestUserID: requestUserID,
|
||||||
|
statusItemAttribute: attribute
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// defalut to select the report status
|
||||||
|
if attribute.isSelected {
|
||||||
|
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||||
|
} else {
|
||||||
|
tableView.deselectRow(at: indexPath, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cell
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,6 +125,8 @@ extension StatusSection {
|
||||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineHeaderTableViewCell.self), for: indexPath) as! TimelineHeaderTableViewCell
|
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineHeaderTableViewCell.self), for: indexPath) as! TimelineHeaderTableViewCell
|
||||||
StatusSection.configureEmptyStateHeader(cell: cell, attribute: attribute)
|
StatusSection.configureEmptyStateHeader(cell: cell, attribute: attribute)
|
||||||
return cell
|
return cell
|
||||||
|
case .reportStatus:
|
||||||
|
return UITableViewCell()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +149,8 @@ extension StatusSection {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { [weak cell] change in
|
||||||
|
guard let cell = cell else { return }
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let newStatus = object as? Status else { return }
|
let newStatus = object as? Status else { return }
|
||||||
StatusSection.configureHeader(cell: cell, status: newStatus)
|
StatusSection.configureHeader(cell: cell, status: newStatus)
|
||||||
|
@ -219,7 +222,8 @@ extension StatusSection {
|
||||||
} else {
|
} else {
|
||||||
meta.blurhashImagePublisher()
|
meta.blurhashImagePublisher()
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { image in
|
.sink { [weak cell] image in
|
||||||
|
guard let cell = cell else { return }
|
||||||
blurhashOverlayImageView.image = image
|
blurhashOverlayImageView.image = image
|
||||||
image?.pngData().flatMap {
|
image?.pngData().flatMap {
|
||||||
blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey)
|
blurhashImageCache.setObject($0 as NSData, forKey: blurhashImageDataKey)
|
||||||
|
@ -244,7 +248,8 @@ extension StatusSection {
|
||||||
statusItemAttribute.isRevealing
|
statusItemAttribute.isRevealing
|
||||||
)
|
)
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { isImageLoaded, isMediaRevealing in
|
.sink { [weak cell] isImageLoaded, isMediaRevealing in
|
||||||
|
guard let cell = cell else { return }
|
||||||
guard isImageLoaded else {
|
guard isImageLoaded else {
|
||||||
blurhashOverlayImageView.alpha = 1
|
blurhashOverlayImageView.alpha = 1
|
||||||
blurhashOverlayImageView.isHidden = false
|
blurhashOverlayImageView.isHidden = false
|
||||||
|
@ -299,6 +304,9 @@ extension StatusSection {
|
||||||
case is NotificationStatusTableViewCell:
|
case is NotificationStatusTableViewCell:
|
||||||
let notificationTableViewCell = cell as! NotificationStatusTableViewCell
|
let notificationTableViewCell = cell as! NotificationStatusTableViewCell
|
||||||
parent = notificationTableViewCell.delegate?.parent()
|
parent = notificationTableViewCell.delegate?.parent()
|
||||||
|
case is ReportedStatusTableViewCell:
|
||||||
|
let reportTableViewCell = cell as! ReportedStatusTableViewCell
|
||||||
|
parent = reportTableViewCell.dependency
|
||||||
default:
|
default:
|
||||||
parent = nil
|
parent = nil
|
||||||
assertionFailure("unknown cell")
|
assertionFailure("unknown cell")
|
||||||
|
@ -350,7 +358,8 @@ extension StatusSection {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { [weak dependency] change in
|
} receiveValue: { [weak dependency, weak cell] change in
|
||||||
|
guard let cell = cell else { return }
|
||||||
guard let dependency = dependency else { return }
|
guard let dependency = dependency else { return }
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let status = object as? Status else { return }
|
let status = object as? Status else { return }
|
||||||
|
@ -377,7 +386,8 @@ extension StatusSection {
|
||||||
ManagedObjectObserver.observe(object: poll)
|
ManagedObjectObserver.observe(object: poll)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { [weak cell] change in
|
||||||
|
guard let cell = cell else { return }
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let newPoll = object as? Poll else { return }
|
let newPoll = object as? Poll else { return }
|
||||||
StatusSection.configurePoll(
|
StatusSection.configurePoll(
|
||||||
|
@ -392,7 +402,12 @@ extension StatusSection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// toolbar
|
// toolbar
|
||||||
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
StatusSection.configureActionToolBar(
|
||||||
|
cell: cell,
|
||||||
|
dependency: dependency,
|
||||||
|
status: status,
|
||||||
|
requestUserID: requestUserID
|
||||||
|
)
|
||||||
|
|
||||||
// separator line
|
// separator line
|
||||||
if let statusTableViewCell = cell as? StatusTableViewCell {
|
if let statusTableViewCell = cell as? StatusTableViewCell {
|
||||||
|
@ -403,7 +418,8 @@ extension StatusSection {
|
||||||
let createdAt = (status.reblog ?? status).createdAt
|
let createdAt = (status.reblog ?? status).createdAt
|
||||||
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||||
timestampUpdatePublisher
|
timestampUpdatePublisher
|
||||||
.sink { _ in
|
.sink { [weak cell] _ in
|
||||||
|
guard let cell = cell else { return }
|
||||||
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
cell.statusView.dateLabel.text = createdAt.shortTimeAgoSinceNow
|
||||||
}
|
}
|
||||||
.store(in: &cell.disposeBag)
|
.store(in: &cell.disposeBag)
|
||||||
|
@ -413,10 +429,17 @@ extension StatusSection {
|
||||||
.receive(on: DispatchQueue.main)
|
.receive(on: DispatchQueue.main)
|
||||||
.sink { _ in
|
.sink { _ in
|
||||||
// do nothing
|
// do nothing
|
||||||
} receiveValue: { change in
|
} receiveValue: { [weak dependency, weak cell] change in
|
||||||
|
guard let cell = cell else { return }
|
||||||
|
guard let dependency = dependency else { return }
|
||||||
guard case .update(let object) = change.changeType,
|
guard case .update(let object) = change.changeType,
|
||||||
let status = object as? Status else { return }
|
let status = object as? Status else { return }
|
||||||
StatusSection.configureActionToolBar(cell: cell, status: status, requestUserID: requestUserID)
|
StatusSection.configureActionToolBar(
|
||||||
|
cell: cell,
|
||||||
|
dependency: dependency,
|
||||||
|
status: status,
|
||||||
|
requestUserID: requestUserID
|
||||||
|
)
|
||||||
|
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue)
|
os_log("%{public}s[%{public}ld], %{public}s: reblog count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.reblogsCount.intValue)
|
||||||
os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue)
|
os_log("%{public}s[%{public}ld], %{public}s: like count label for status %s did update: %ld", (#file as NSString).lastPathComponent, #line, #function, status.id, status.favouritesCount.intValue)
|
||||||
|
@ -571,6 +594,7 @@ extension StatusSection {
|
||||||
|
|
||||||
static func configureActionToolBar(
|
static func configureActionToolBar(
|
||||||
cell: StatusCell,
|
cell: StatusCell,
|
||||||
|
dependency: NeedsDependency,
|
||||||
status: Status,
|
status: Status,
|
||||||
requestUserID: String
|
requestUserID: String
|
||||||
) {
|
) {
|
||||||
|
@ -598,6 +622,8 @@ extension StatusSection {
|
||||||
}()
|
}()
|
||||||
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
cell.statusView.actionToolbarContainer.favoriteButton.setTitle(favoriteCountTitle, for: .normal)
|
||||||
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
|
cell.statusView.actionToolbarContainer.isFavoriteButtonHighlight = isLike
|
||||||
|
|
||||||
|
self.setupStatusMoreButtonMenu(cell: cell, dependency: dependency, status: status)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func configurePoll(
|
static func configurePoll(
|
||||||
|
@ -724,4 +750,39 @@ extension StatusSection {
|
||||||
guard let number = number, number > 0 else { return "" }
|
guard let number = number, number > 0 else { return "" }
|
||||||
return String(number)
|
return String(number)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func setupStatusMoreButtonMenu(
|
||||||
|
cell: StatusCell,
|
||||||
|
dependency: NeedsDependency,
|
||||||
|
status: Status) {
|
||||||
|
|
||||||
|
cell.statusView.actionToolbarContainer.moreButton.menu = nil
|
||||||
|
|
||||||
|
guard let authenticationBox = dependency.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let author = (status.reblog ?? status).author
|
||||||
|
guard authenticationBox.userID != author.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var children: [UIMenuElement] = []
|
||||||
|
let name = author.displayNameWithFallback
|
||||||
|
let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) {
|
||||||
|
[weak dependency] _ in
|
||||||
|
guard let dependency = dependency else { return }
|
||||||
|
let viewModel = ReportViewModel(
|
||||||
|
context: dependency.context,
|
||||||
|
domain: authenticationBox.domain,
|
||||||
|
user: status.author,
|
||||||
|
status: status)
|
||||||
|
dependency.coordinator.present(
|
||||||
|
scene: .report(viewModel: viewModel),
|
||||||
|
from: nil,
|
||||||
|
transition: .modal(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
children.append(reportAction)
|
||||||
|
cell.statusView.actionToolbarContainer.moreButton.menu = UIMenu(title: "", options: [], children: children)
|
||||||
|
cell.statusView.actionToolbarContainer.moreButton.showsMenuAsPrimaryAction = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ internal enum Asset {
|
||||||
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
internal static let secondaryGroupedSystemBackground = ColorAsset(name: "Colors/Background/secondary.grouped.system.background")
|
||||||
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
internal static let secondarySystemBackground = ColorAsset(name: "Colors/Background/secondary.system.background")
|
||||||
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
internal static let systemBackground = ColorAsset(name: "Colors/Background/system.background")
|
||||||
|
internal static let systemElevatedBackground = ColorAsset(name: "Colors/Background/system.elevated.background")
|
||||||
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
internal static let systemGroupedBackground = ColorAsset(name: "Colors/Background/system.grouped.background")
|
||||||
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
internal static let tertiarySystemBackground = ColorAsset(name: "Colors/Background/tertiary.system.background")
|
||||||
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
internal static let tertiarySystemGroupedBackground = ColorAsset(name: "Colors/Background/tertiary.system.grouped.background")
|
||||||
|
|
|
@ -84,6 +84,10 @@ internal enum L10n {
|
||||||
internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview")
|
internal static let preview = L10n.tr("Localizable", "Common.Controls.Actions.Preview")
|
||||||
/// Remove
|
/// Remove
|
||||||
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
internal static let remove = L10n.tr("Localizable", "Common.Controls.Actions.Remove")
|
||||||
|
/// Report %@
|
||||||
|
internal static func reportUser(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Common.Controls.Actions.ReportUser", String(describing: p1))
|
||||||
|
}
|
||||||
/// Save
|
/// Save
|
||||||
internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save")
|
internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save")
|
||||||
/// Save photo
|
/// Save photo
|
||||||
|
@ -100,6 +104,8 @@ internal enum L10n {
|
||||||
internal static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn")
|
internal static let signIn = L10n.tr("Localizable", "Common.Controls.Actions.SignIn")
|
||||||
/// Sign Up
|
/// Sign Up
|
||||||
internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp")
|
internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp")
|
||||||
|
/// Skip
|
||||||
|
internal static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip")
|
||||||
/// Take photo
|
/// Take photo
|
||||||
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto")
|
||||||
/// Try Again
|
/// Try Again
|
||||||
|
@ -531,6 +537,26 @@ internal enum L10n {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
internal enum Report {
|
||||||
|
/// Are there any other posts you’d like to add to the report?
|
||||||
|
internal static let content1 = L10n.tr("Localizable", "Scene.Report.Content1")
|
||||||
|
/// Is there anything the moderators should know about this report?
|
||||||
|
internal static let content2 = L10n.tr("Localizable", "Scene.Report.Content2")
|
||||||
|
/// Send Report
|
||||||
|
internal static let send = L10n.tr("Localizable", "Scene.Report.Send")
|
||||||
|
/// Send without comment
|
||||||
|
internal static let skipToSend = L10n.tr("Localizable", "Scene.Report.SkipToSend")
|
||||||
|
/// Step 1 of 2
|
||||||
|
internal static let step1 = L10n.tr("Localizable", "Scene.Report.Step1")
|
||||||
|
/// Step 2 of 2
|
||||||
|
internal static let step2 = L10n.tr("Localizable", "Scene.Report.Step2")
|
||||||
|
/// Type or paste additional comments
|
||||||
|
internal static let textPlaceholder = L10n.tr("Localizable", "Scene.Report.TextPlaceholder")
|
||||||
|
/// Report %@
|
||||||
|
internal static func title(_ p1: Any) -> String {
|
||||||
|
return L10n.tr("Localizable", "Scene.Report.Title", String(describing: p1))
|
||||||
|
}
|
||||||
|
}
|
||||||
internal enum Search {
|
internal enum Search {
|
||||||
internal enum Recommend {
|
internal enum Recommend {
|
||||||
/// See All
|
/// See All
|
||||||
|
|
|
@ -498,6 +498,34 @@ extension StatusProviderFacade {
|
||||||
.store(in: &dependency.context.disposeBag)
|
.store(in: &dependency.context.disposeBag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func responseToStatusContentWarningRevealAction(dependency: ReportViewController, cell: UITableViewCell) {
|
||||||
|
let status = Future<Status?, Never> { promise in
|
||||||
|
guard let diffableDataSource = dependency.viewModel.diffableDataSource,
|
||||||
|
let indexPath = dependency.tableView.indexPath(for: cell),
|
||||||
|
let item = diffableDataSource.itemIdentifier(for: indexPath) else {
|
||||||
|
promise(.success(nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let managedObjectContext = dependency.viewModel.statusFetchedResultsController
|
||||||
|
.fetchedResultsController
|
||||||
|
.managedObjectContext
|
||||||
|
|
||||||
|
switch item {
|
||||||
|
case .reportStatus(let objectID, _):
|
||||||
|
managedObjectContext.perform {
|
||||||
|
let status = managedObjectContext.object(with: objectID) as! Status
|
||||||
|
promise(.success(status))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
promise(.success(nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_responseToStatusContentWarningRevealAction(
|
||||||
|
dependency: dependency,
|
||||||
|
status: status
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusProviderFacade {
|
extension StatusProviderFacade {
|
||||||
|
|
|
@ -219,6 +219,24 @@ extension UserProviderFacade {
|
||||||
children.append(shareAction)
|
children.append(shareAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let reportAction = UIAction(title: L10n.Common.Controls.Actions.reportUser(name), image: UIImage(systemName: "exclamationmark.bubble"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak provider] _ in
|
||||||
|
guard let provider = provider else { return }
|
||||||
|
guard let authenticationBox = provider.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let viewModel = ReportViewModel(
|
||||||
|
context: provider.context,
|
||||||
|
domain: authenticationBox.domain,
|
||||||
|
user: mastodonUser,
|
||||||
|
status: nil)
|
||||||
|
provider.coordinator.present(
|
||||||
|
scene: .report(viewModel: viewModel),
|
||||||
|
from: provider,
|
||||||
|
transition: .modal(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
children.append(reportAction)
|
||||||
|
|
||||||
return UIMenu(title: "", options: [], children: children)
|
return UIMenu(title: "", options: [], children: children)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0xFE",
|
"blue" : "254",
|
||||||
"green" : "0xFF",
|
"green" : "255",
|
||||||
"red" : "0xFE"
|
"red" : "254"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x2E",
|
"blue" : "46",
|
||||||
"green" : "0x2C",
|
"green" : "44",
|
||||||
"red" : "0x2C"
|
"red" : "44"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -5,9 +5,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0xE8",
|
"blue" : "232",
|
||||||
"green" : "0xE1",
|
"green" : "225",
|
||||||
"red" : "0xD9"
|
"red" : "217"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
@ -23,9 +23,9 @@
|
||||||
"color-space" : "srgb",
|
"color-space" : "srgb",
|
||||||
"components" : {
|
"components" : {
|
||||||
"alpha" : "1.000",
|
"alpha" : "1.000",
|
||||||
"blue" : "0x2E",
|
"blue" : "46",
|
||||||
"green" : "0x2C",
|
"green" : "44",
|
||||||
"red" : "0x2C"
|
"red" : "44"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"idiom" : "universal"
|
"idiom" : "universal"
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "1.000",
|
||||||
|
"green" : "1.000",
|
||||||
|
"red" : "1.000"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x1E",
|
||||||
|
"green" : "0x1C",
|
||||||
|
"red" : "0x1C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,7 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
"Common.Controls.Actions.OpenInSafari" = "Open in Safari";
|
||||||
"Common.Controls.Actions.Preview" = "Preview";
|
"Common.Controls.Actions.Preview" = "Preview";
|
||||||
"Common.Controls.Actions.Remove" = "Remove";
|
"Common.Controls.Actions.Remove" = "Remove";
|
||||||
|
"Common.Controls.Actions.ReportUser" = "Report %@";
|
||||||
"Common.Controls.Actions.Save" = "Save";
|
"Common.Controls.Actions.Save" = "Save";
|
||||||
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
"Common.Controls.Actions.SavePhoto" = "Save photo";
|
||||||
"Common.Controls.Actions.SeeMore" = "See More";
|
"Common.Controls.Actions.SeeMore" = "See More";
|
||||||
|
@ -33,6 +34,7 @@ Please check your internet connection.";
|
||||||
"Common.Controls.Actions.ShareUser" = "Share %@";
|
"Common.Controls.Actions.ShareUser" = "Share %@";
|
||||||
"Common.Controls.Actions.SignIn" = "Sign In";
|
"Common.Controls.Actions.SignIn" = "Sign In";
|
||||||
"Common.Controls.Actions.SignUp" = "Sign Up";
|
"Common.Controls.Actions.SignUp" = "Sign Up";
|
||||||
|
"Common.Controls.Actions.Skip" = "Skip";
|
||||||
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
"Common.Controls.Actions.TakePhoto" = "Take photo";
|
||||||
"Common.Controls.Actions.TryAgain" = "Try Again";
|
"Common.Controls.Actions.TryAgain" = "Try Again";
|
||||||
"Common.Controls.Firendship.Block" = "Block";
|
"Common.Controls.Firendship.Block" = "Block";
|
||||||
|
@ -171,6 +173,14 @@ tap the link to confirm your account.";
|
||||||
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
|
"Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken.";
|
||||||
"Scene.Register.Input.Username.Placeholder" = "username";
|
"Scene.Register.Input.Username.Placeholder" = "username";
|
||||||
"Scene.Register.Title" = "Tell us about you.";
|
"Scene.Register.Title" = "Tell us about you.";
|
||||||
|
"Scene.Report.Content1" = "Are there any other posts you’d like to add to the report?";
|
||||||
|
"Scene.Report.Content2" = "Is there anything the moderators should know about this report?";
|
||||||
|
"Scene.Report.Send" = "Send Report";
|
||||||
|
"Scene.Report.SkipToSend" = "Send without comment";
|
||||||
|
"Scene.Report.Step1" = "Step 1 of 2";
|
||||||
|
"Scene.Report.Step2" = "Step 2 of 2";
|
||||||
|
"Scene.Report.TextPlaceholder" = "Type or paste additional comments";
|
||||||
|
"Scene.Report.Title" = "Report %@";
|
||||||
"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account.";
|
"Scene.Search.Recommend.Accounts.Description" = "Except for Sam, you will not like his account.";
|
||||||
"Scene.Search.Recommend.Accounts.Follow" = "Follow";
|
"Scene.Search.Recommend.Accounts.Follow" = "Follow";
|
||||||
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
|
"Scene.Search.Recommend.Accounts.Title" = "Accounts you might like";
|
||||||
|
|
|
@ -338,7 +338,11 @@ extension HomeTimelineViewController {
|
||||||
@objc private func showSettings(_ sender: UIAction) {
|
@objc private func showSettings(_ sender: UIAction) {
|
||||||
guard let currentSetting = context.settingService.currentSetting.value else { return }
|
guard let currentSetting = context.settingService.currentSetting.value else { return }
|
||||||
let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting)
|
let settingsViewModel = SettingsViewModel(context: context, setting: currentSetting)
|
||||||
coordinator.present(scene: .settings(viewModel: settingsViewModel), from: self, transition: .modal(animated: true, completion: nil))
|
coordinator.present(
|
||||||
|
scene: .settings(viewModel: settingsViewModel),
|
||||||
|
from: self,
|
||||||
|
transition: .modal(animated: true, completion: nil)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
//
|
||||||
|
// ReportFooterView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ReportFooterView: UIView {
|
||||||
|
enum Step: Int {
|
||||||
|
case one
|
||||||
|
case two
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var stackview: UIStackView = {
|
||||||
|
let view = UIStackView()
|
||||||
|
view.axis = .vertical
|
||||||
|
view.alignment = .fill
|
||||||
|
view.spacing = 8
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var nextStepButton: PrimaryActionButton = {
|
||||||
|
let button = PrimaryActionButton()
|
||||||
|
button.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var skipButton: UIButton = {
|
||||||
|
let button = UIButton(type: .system)
|
||||||
|
button.tintColor = Asset.Colors.brandBlue.color
|
||||||
|
button.setTitle(L10n.Common.Controls.Actions.skip, for: .normal)
|
||||||
|
button.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return button
|
||||||
|
}()
|
||||||
|
|
||||||
|
var step: Step = .one {
|
||||||
|
didSet {
|
||||||
|
switch step {
|
||||||
|
case .one:
|
||||||
|
nextStepButton.setTitle(L10n.Common.Controls.Actions.continue, for: .normal)
|
||||||
|
skipButton.setTitle(L10n.Common.Controls.Actions.skip, for: .normal)
|
||||||
|
case .two:
|
||||||
|
nextStepButton.setTitle(L10n.Scene.Report.send, for: .normal)
|
||||||
|
skipButton.setTitle(L10n.Scene.Report.skipToSend, for: .normal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.backgroundColor = Asset.Colors.Background.systemElevatedBackground.color
|
||||||
|
|
||||||
|
stackview.addArrangedSubview(nextStepButton)
|
||||||
|
stackview.addArrangedSubview(skipButton)
|
||||||
|
addSubview(stackview)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackview.topAnchor.constraint(
|
||||||
|
equalTo: self.topAnchor,
|
||||||
|
constant: ReportView.continuTopMargin
|
||||||
|
),
|
||||||
|
stackview.leadingAnchor.constraint(
|
||||||
|
equalTo: self.readableContentGuide.leadingAnchor,
|
||||||
|
constant: ReportView.horizontalMargin
|
||||||
|
),
|
||||||
|
stackview.bottomAnchor.constraint(
|
||||||
|
equalTo: self.safeAreaLayoutGuide.bottomAnchor,
|
||||||
|
constant: -1 * ReportView.skipBottomMargin
|
||||||
|
),
|
||||||
|
stackview.trailingAnchor.constraint(
|
||||||
|
equalTo: self.readableContentGuide.trailingAnchor,
|
||||||
|
constant: -1 * ReportView.horizontalMargin
|
||||||
|
),
|
||||||
|
nextStepButton.heightAnchor.constraint(
|
||||||
|
equalToConstant: ReportView.buttonHeight
|
||||||
|
),
|
||||||
|
skipButton.heightAnchor.constraint(
|
||||||
|
equalTo: nextStepButton.heightAnchor
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ReportFooterView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) { () -> UIView in
|
||||||
|
return ReportFooterView(frame: CGRect(origin: .zero, size: CGSize(width: 375, height: 164)))
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 164))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,115 @@
|
||||||
|
//
|
||||||
|
// ReportView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ReportView {
|
||||||
|
static var horizontalMargin: CGFloat { return 12 }
|
||||||
|
static var verticalMargin: CGFloat { return 22 }
|
||||||
|
static var buttonHeight: CGFloat { return 46 }
|
||||||
|
static var skipBottomMargin: CGFloat { return 8 }
|
||||||
|
static var continuTopMargin: CGFloat { return 22 }
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ReportHeaderView: UIView {
|
||||||
|
enum Step: Int {
|
||||||
|
case one
|
||||||
|
case two
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy var titleLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.secondary.color
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .subheadline)
|
||||||
|
.scaledFont(for: .systemFont(ofSize: 15, weight: .regular))
|
||||||
|
label.numberOfLines = 0
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var contentLabel: UILabel = {
|
||||||
|
let label = UILabel()
|
||||||
|
label.textColor = Asset.Colors.Label.primary.color
|
||||||
|
label.font = UIFontMetrics(forTextStyle: .title3)
|
||||||
|
.scaledFont(for: .systemFont(ofSize: 20, weight: .semibold))
|
||||||
|
label.numberOfLines = 0
|
||||||
|
return label
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var stackview: UIStackView = {
|
||||||
|
let view = UIStackView()
|
||||||
|
view.axis = .vertical
|
||||||
|
view.alignment = .leading
|
||||||
|
view.spacing = 2
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
var step: Step = .one {
|
||||||
|
didSet {
|
||||||
|
switch step {
|
||||||
|
case .one:
|
||||||
|
titleLabel.text = L10n.Scene.Report.step1
|
||||||
|
contentLabel.text = L10n.Scene.Report.content1
|
||||||
|
case .two:
|
||||||
|
titleLabel.text = L10n.Scene.Report.step2
|
||||||
|
contentLabel.text = L10n.Scene.Report.content2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.backgroundColor = Asset.Colors.Background.systemElevatedBackground.color
|
||||||
|
stackview.addArrangedSubview(titleLabel)
|
||||||
|
stackview.addArrangedSubview(contentLabel)
|
||||||
|
addSubview(stackview)
|
||||||
|
|
||||||
|
stackview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackview.topAnchor.constraint(
|
||||||
|
equalTo: self.topAnchor,
|
||||||
|
constant: ReportView.verticalMargin
|
||||||
|
),
|
||||||
|
stackview.leadingAnchor.constraint(
|
||||||
|
equalTo: self.readableContentGuide.leadingAnchor,
|
||||||
|
constant: ReportView.horizontalMargin
|
||||||
|
),
|
||||||
|
self.bottomAnchor.constraint(
|
||||||
|
equalTo: stackview.bottomAnchor,
|
||||||
|
constant: ReportView.verticalMargin
|
||||||
|
),
|
||||||
|
self.readableContentGuide.trailingAnchor.constraint(
|
||||||
|
equalTo: stackview.trailingAnchor,
|
||||||
|
constant: ReportView.horizontalMargin
|
||||||
|
)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if canImport(SwiftUI) && DEBUG
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ReportHeaderView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview { () -> UIView in
|
||||||
|
let view = ReportHeaderView()
|
||||||
|
view.step = .one
|
||||||
|
view.contentLabel.preferredMaxLayoutWidth = 335
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 110))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,343 @@
|
||||||
|
//
|
||||||
|
// ReportViewController.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import TwitterTextEditor
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
class ReportViewController: UIViewController, NeedsDependency {
|
||||||
|
static let kAnimationDuration: TimeInterval = 0.33
|
||||||
|
|
||||||
|
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
|
||||||
|
var viewModel: ReportViewModel! { willSet { precondition(!isViewLoaded) } }
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
let didToggleSelected = PassthroughSubject<Item, Never>()
|
||||||
|
let comment = CurrentValueSubject<String?, Never>(nil)
|
||||||
|
let step1Continue = PassthroughSubject<Void, Never>()
|
||||||
|
let step1Skip = PassthroughSubject<Void, Never>()
|
||||||
|
let step2Continue = PassthroughSubject<Void, Never>()
|
||||||
|
let step2Skip = PassthroughSubject<Void, Never>()
|
||||||
|
let cancel = PassthroughSubject<Void, Never>()
|
||||||
|
|
||||||
|
// MAKK: - UI
|
||||||
|
lazy var header: ReportHeaderView = {
|
||||||
|
let view = ReportHeaderView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var footer: ReportFooterView = {
|
||||||
|
let view = ReportFooterView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var contentView: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||||
|
view.backgroundColor = Asset.Colors.Background.systemElevatedBackground.color
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var stackview: UIStackView = {
|
||||||
|
let view = UIStackView()
|
||||||
|
view.axis = .vertical
|
||||||
|
view.alignment = .fill
|
||||||
|
view.distribution = .fill
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var tableView: UITableView = {
|
||||||
|
let tableView = ControlContainableTableView()
|
||||||
|
tableView.register(ReportedStatusTableViewCell.self, forCellReuseIdentifier: String(describing: ReportedStatusTableViewCell.self))
|
||||||
|
tableView.rowHeight = UITableView.automaticDimension
|
||||||
|
tableView.separatorStyle = .none
|
||||||
|
tableView.backgroundColor = .clear
|
||||||
|
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
tableView.delegate = self
|
||||||
|
tableView.prefetchDataSource = self
|
||||||
|
tableView.allowsMultipleSelection = true
|
||||||
|
return tableView
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var textView: UITextView = {
|
||||||
|
let textView = UITextView()
|
||||||
|
textView.font = .preferredFont(forTextStyle: .body)
|
||||||
|
textView.isScrollEnabled = false
|
||||||
|
textView.placeholder = L10n.Scene.Report.textPlaceholder
|
||||||
|
textView.backgroundColor = .clear
|
||||||
|
textView.delegate = self
|
||||||
|
textView.isScrollEnabled = true
|
||||||
|
textView.keyboardDismissMode = .onDrag
|
||||||
|
return textView
|
||||||
|
}()
|
||||||
|
|
||||||
|
lazy var bottomSpacing: UIView = {
|
||||||
|
let view = UIView()
|
||||||
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return view
|
||||||
|
}()
|
||||||
|
|
||||||
|
var bottomConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
setupView()
|
||||||
|
|
||||||
|
viewModel.setupDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: self
|
||||||
|
)
|
||||||
|
|
||||||
|
bindViewModel()
|
||||||
|
bindActions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MAKR: - Private methods
|
||||||
|
private func setupView() {
|
||||||
|
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
|
setupNavigation()
|
||||||
|
|
||||||
|
stackview.addArrangedSubview(header)
|
||||||
|
stackview.addArrangedSubview(contentView)
|
||||||
|
stackview.addArrangedSubview(footer)
|
||||||
|
stackview.addArrangedSubview(bottomSpacing)
|
||||||
|
|
||||||
|
contentView.addSubview(tableView)
|
||||||
|
|
||||||
|
view.addSubview(stackview)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
stackview.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||||
|
stackview.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
stackview.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
stackview.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
tableView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
tableView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
tableView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
self.bottomConstraint = bottomSpacing.heightAnchor.constraint(equalToConstant: 0)
|
||||||
|
bottomConstraint.isActive = true
|
||||||
|
|
||||||
|
header.step = .one
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindActions() {
|
||||||
|
footer.nextStepButton.addTarget(self, action: #selector(continueButtonDidClick), for: .touchUpInside)
|
||||||
|
footer.skipButton.addTarget(self, action: #selector(skipButtonDidClick), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func bindViewModel() {
|
||||||
|
let input = ReportViewModel.Input(
|
||||||
|
didToggleSelected: didToggleSelected.eraseToAnyPublisher(),
|
||||||
|
comment: comment.eraseToAnyPublisher(),
|
||||||
|
step1Continue: step1Continue.eraseToAnyPublisher(),
|
||||||
|
step1Skip: step1Skip.eraseToAnyPublisher(),
|
||||||
|
step2Continue: step2Continue.eraseToAnyPublisher(),
|
||||||
|
step2Skip: step2Skip.eraseToAnyPublisher(),
|
||||||
|
cancel: cancel.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
let output = viewModel.transform(input: input)
|
||||||
|
output?.currentStep
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink(receiveValue: { [weak self] (step) in
|
||||||
|
guard step == .two else { return }
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.header.step = .two
|
||||||
|
self.footer.step = .two
|
||||||
|
self.switchToStep2Content()
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
output?.continueEnableSubject
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] _ in
|
||||||
|
guard let step = self?.viewModel.currentStep.value, step == .one else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
.assign(to: \.nextStepButton.isEnabled, on: footer)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
output?.sendEnableSubject
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.filter { [weak self] _ in
|
||||||
|
guard let step = self?.viewModel.currentStep.value, step == .two else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
.assign(to: \.nextStepButton.isEnabled, on: footer)
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
output?.reportResult
|
||||||
|
.print()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink(receiveCompletion: { _ in
|
||||||
|
}, receiveValue: { [weak self] data in
|
||||||
|
let (success, error) = data
|
||||||
|
if success {
|
||||||
|
self?.dismiss(animated: true, completion: nil)
|
||||||
|
} else if let error = error {
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fail to file a report : %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
|
||||||
|
let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
|
||||||
|
let okAction = UIAlertAction(title: "OK", style: .default, handler: nil)
|
||||||
|
alertController.addAction(okAction)
|
||||||
|
self?.coordinator.present(
|
||||||
|
scene: .alertController(alertController: alertController),
|
||||||
|
from: nil,
|
||||||
|
transition: .alertController(animated: true, completion: nil)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
Publishers.CombineLatest(
|
||||||
|
KeyboardResponderService.shared.state.eraseToAnyPublisher(),
|
||||||
|
KeyboardResponderService.shared.endFrame.eraseToAnyPublisher()
|
||||||
|
)
|
||||||
|
.sink(receiveValue: { [weak self] state, endFrame in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
guard state == .dock else {
|
||||||
|
self.bottomConstraint.constant = 0.0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let contentFrame = self.view.convert(self.view.frame, to: nil)
|
||||||
|
let padding = contentFrame.maxY - endFrame.minY
|
||||||
|
guard padding > 0 else {
|
||||||
|
self.bottomConstraint.constant = 0.0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.bottomConstraint.constant = padding
|
||||||
|
})
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupNavigation() {
|
||||||
|
navigationItem.rightBarButtonItem
|
||||||
|
= UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel,
|
||||||
|
target: self,
|
||||||
|
action: #selector(doneButtonDidClick))
|
||||||
|
navigationItem.rightBarButtonItem?.tintColor = Asset.Colors.Label.highlight.color
|
||||||
|
|
||||||
|
// fetch old mastodon user
|
||||||
|
let beReportedUser: MastodonUser? = {
|
||||||
|
guard let domain = context.authenticationService.activeMastodonAuthenticationBox.value?.domain else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let request = MastodonUser.sortedFetchRequest
|
||||||
|
request.predicate = MastodonUser.predicate(domain: domain, id: viewModel.user.id)
|
||||||
|
request.fetchLimit = 1
|
||||||
|
request.returnsObjectsAsFaults = false
|
||||||
|
do {
|
||||||
|
return try viewModel.statusFetchedResultsController.fetchedResultsController.managedObjectContext.fetch(request).first
|
||||||
|
} catch {
|
||||||
|
assertionFailure(error.localizedDescription)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
navigationItem.title = L10n.Scene.Report.title(
|
||||||
|
beReportedUser?.displayNameWithFallback ?? ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func switchToStep2Content() {
|
||||||
|
self.contentView.addSubview(self.textView)
|
||||||
|
self.textView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
self.textView.topAnchor.constraint(equalTo: self.contentView.topAnchor),
|
||||||
|
self.textView.leadingAnchor.constraint(
|
||||||
|
equalTo: self.contentView.readableContentGuide.leadingAnchor,
|
||||||
|
constant: ReportView.horizontalMargin
|
||||||
|
),
|
||||||
|
self.textView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor),
|
||||||
|
self.contentView.trailingAnchor.constraint(
|
||||||
|
equalTo: self.textView.trailingAnchor,
|
||||||
|
constant: ReportView.horizontalMargin
|
||||||
|
),
|
||||||
|
])
|
||||||
|
self.textView.layoutIfNeeded()
|
||||||
|
|
||||||
|
UIView.transition(
|
||||||
|
with: contentView,
|
||||||
|
duration: ReportViewController.kAnimationDuration,
|
||||||
|
options: UIView.AnimationOptions.transitionCrossDissolve) {
|
||||||
|
[weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.contentView.addSubview(self.textView)
|
||||||
|
self.tableView.isHidden = true
|
||||||
|
} completion: { (_) in
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark: - Actions
|
||||||
|
@objc func doneButtonDidClick() {
|
||||||
|
dismiss(animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func continueButtonDidClick() {
|
||||||
|
if viewModel.currentStep.value == .one {
|
||||||
|
step1Continue.send()
|
||||||
|
} else {
|
||||||
|
step2Continue.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func skipButtonDidClick() {
|
||||||
|
if viewModel.currentStep.value == .one {
|
||||||
|
step1Skip.send()
|
||||||
|
} else {
|
||||||
|
step2Skip.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDelegate
|
||||||
|
extension ReportViewController: UITableViewDelegate {
|
||||||
|
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||||
|
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
didToggleSelected.send(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||||
|
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
didToggleSelected.send(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITableViewDataSourcePrefetching
|
||||||
|
extension ReportViewController: UITableViewDataSourcePrefetching {
|
||||||
|
func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
viewModel.prefetchData(prefetchRowsAt: indexPaths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UITextViewDelegate
|
||||||
|
extension ReportViewController: UITextViewDelegate {
|
||||||
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
|
self.comment.send(textView.text)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
//
|
||||||
|
// ReportViewModel+Data.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
extension ReportViewModel {
|
||||||
|
func requestRecentStatus(
|
||||||
|
domain: String,
|
||||||
|
accountId: String,
|
||||||
|
authorizationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) {
|
||||||
|
context.apiService.userTimeline(
|
||||||
|
domain: domain,
|
||||||
|
accountID: accountId,
|
||||||
|
excludeReblogs: true,
|
||||||
|
authorizationBox: authorizationBox
|
||||||
|
)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] completion in
|
||||||
|
switch completion {
|
||||||
|
case .failure(let error):
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch user timeline fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let reportStatusId = self.status?.id else { return }
|
||||||
|
var statusIDs = self.statusFetchedResultsController.statusIDs.value
|
||||||
|
guard statusIDs.contains(reportStatusId) else { return }
|
||||||
|
|
||||||
|
statusIDs.append(reportStatusId)
|
||||||
|
self.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
|
case .finished:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
var statusIDs = response.value.map { $0.id }
|
||||||
|
if let reportStatusId = self.status?.id, !statusIDs.contains(reportStatusId) {
|
||||||
|
statusIDs.append(reportStatusId)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchStatus() {
|
||||||
|
let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext
|
||||||
|
statusFetchedResultsController.objectIDs.eraseToAnyPublisher()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] objectIDs in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||||
|
|
||||||
|
var items: [Item] = []
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, Item>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
|
||||||
|
defer {
|
||||||
|
// not animate when empty items fix loader first appear layout issue
|
||||||
|
diffableDataSource.apply(snapshot, animatingDifferences: !items.isEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldSnapshotAttributeDict: [NSManagedObjectID : Item.ReportStatusAttribute] = [:]
|
||||||
|
let oldSnapshot = diffableDataSource.snapshot()
|
||||||
|
for item in oldSnapshot.itemIdentifiers {
|
||||||
|
guard case let .reportStatus(objectID, attribute) = item else { continue }
|
||||||
|
oldSnapshotAttributeDict[objectID] = attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
for objectID in objectIDs {
|
||||||
|
let attribute = oldSnapshotAttributeDict[objectID] ?? Item.ReportStatusAttribute()
|
||||||
|
let item = Item.reportStatus(objectID: objectID, attribute: attribute)
|
||||||
|
items.append(item)
|
||||||
|
|
||||||
|
guard let status = managedObjectContext.object(with: objectID) as? Status else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if status.id == self.status?.id {
|
||||||
|
attribute.isSelected = true
|
||||||
|
self.append(statusID: status.id)
|
||||||
|
self.continueEnableSubject.send(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot.appendItems(items, toSection: .main)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prefetchData(prefetchRowsAt indexPaths: [IndexPath]) {
|
||||||
|
guard let diffableDataSource = diffableDataSource else { return }
|
||||||
|
|
||||||
|
// prefetch reply status
|
||||||
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { return }
|
||||||
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
|
||||||
|
var statusObjectIDs: [NSManagedObjectID] = []
|
||||||
|
for indexPath in indexPaths {
|
||||||
|
let item = diffableDataSource.itemIdentifier(for: indexPath)
|
||||||
|
switch item {
|
||||||
|
case .reportStatus(let objectID, _):
|
||||||
|
statusObjectIDs.append(objectID)
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let backgroundManagedObjectContext = context.backgroundManagedObjectContext
|
||||||
|
backgroundManagedObjectContext.perform { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
for objectID in statusObjectIDs {
|
||||||
|
let status = backgroundManagedObjectContext.object(with: objectID) as! Status
|
||||||
|
guard let replyToID = status.inReplyToID, status.replyTo == nil else {
|
||||||
|
// skip
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
self.context.statusPrefetchingService.prefetchReplyTo(
|
||||||
|
domain: domain,
|
||||||
|
statusObjectID: status.objectID,
|
||||||
|
statusID: status.id,
|
||||||
|
replyToStatusID: replyToID,
|
||||||
|
authorizationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
//
|
||||||
|
// ReportViewModel+Diffable.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
extension ReportViewModel {
|
||||||
|
func setupDiffableDataSource(
|
||||||
|
for tableView: UITableView,
|
||||||
|
dependency: ReportViewController
|
||||||
|
) {
|
||||||
|
let timestampUpdatePublisher = Timer.publish(every: 1.0, on: .main, in: .common)
|
||||||
|
.autoconnect()
|
||||||
|
.share()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
|
||||||
|
diffableDataSource = ReportSection.tableViewDiffableDataSource(
|
||||||
|
for: tableView,
|
||||||
|
dependency: dependency,
|
||||||
|
managedObjectContext: statusFetchedResultsController.fetchedResultsController.managedObjectContext,
|
||||||
|
timestampUpdatePublisher: timestampUpdatePublisher
|
||||||
|
)
|
||||||
|
|
||||||
|
// set empty section to make update animation top-to-bottom style
|
||||||
|
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, Item>()
|
||||||
|
snapshot.appendSections([.main])
|
||||||
|
diffableDataSource?.apply(snapshot)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,215 @@
|
||||||
|
//
|
||||||
|
// ReportViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import UIKit
|
||||||
|
import os.log
|
||||||
|
|
||||||
|
class ReportViewModel: NSObject {
|
||||||
|
typealias FileReportQuery = Mastodon.API.Reports.FileReportQuery
|
||||||
|
|
||||||
|
enum Step: Int {
|
||||||
|
case one
|
||||||
|
case two
|
||||||
|
}
|
||||||
|
|
||||||
|
// confirm set only once
|
||||||
|
weak var context: AppContext! { willSet { precondition(context == nil) } }
|
||||||
|
var user: MastodonUser
|
||||||
|
var status: Status?
|
||||||
|
|
||||||
|
var statusIDs = [Mastodon.Entity.Status.ID]()
|
||||||
|
var comment: String?
|
||||||
|
|
||||||
|
var reportQuery: FileReportQuery
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
let currentStep = CurrentValueSubject<Step, Never>(.one)
|
||||||
|
let statusFetchedResultsController: StatusFetchedResultsController
|
||||||
|
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, Item>?
|
||||||
|
let continueEnableSubject = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
let sendEnableSubject = CurrentValueSubject<Bool, Never>(false)
|
||||||
|
|
||||||
|
struct Input {
|
||||||
|
let didToggleSelected: AnyPublisher<Item, Never>
|
||||||
|
let comment: AnyPublisher<String?, Never>
|
||||||
|
let step1Continue: AnyPublisher<Void, Never>
|
||||||
|
let step1Skip: AnyPublisher<Void, Never>
|
||||||
|
let step2Continue: AnyPublisher<Void, Never>
|
||||||
|
let step2Skip: AnyPublisher<Void, Never>
|
||||||
|
let cancel: AnyPublisher<Void, Never>
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Output {
|
||||||
|
let currentStep: AnyPublisher<Step, Never>
|
||||||
|
let continueEnableSubject: AnyPublisher<Bool, Never>
|
||||||
|
let sendEnableSubject: AnyPublisher<Bool, Never>
|
||||||
|
let reportResult: AnyPublisher<(Bool, Error?), Never>
|
||||||
|
}
|
||||||
|
|
||||||
|
init(context: AppContext,
|
||||||
|
domain: String,
|
||||||
|
user: MastodonUser,
|
||||||
|
status: Status?
|
||||||
|
) {
|
||||||
|
self.context = context
|
||||||
|
self.user = user
|
||||||
|
self.status = status
|
||||||
|
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||||
|
managedObjectContext: context.managedObjectContext,
|
||||||
|
domain: domain,
|
||||||
|
additionalTweetPredicate: Status.notDeleted()
|
||||||
|
)
|
||||||
|
|
||||||
|
self.reportQuery = FileReportQuery(
|
||||||
|
accountID: user.id,
|
||||||
|
statusIDs: [],
|
||||||
|
comment: nil,
|
||||||
|
forward: nil
|
||||||
|
)
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
|
||||||
|
func transform(input: Input?) -> Output? {
|
||||||
|
guard let input = input else { return nil }
|
||||||
|
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let domain = activeMastodonAuthenticationBox.domain
|
||||||
|
|
||||||
|
// data binding
|
||||||
|
bindData(input: input)
|
||||||
|
|
||||||
|
// step1 and step2 binding
|
||||||
|
bindForStep1(input: input)
|
||||||
|
let reportResult = bindForStep2(
|
||||||
|
input: input,
|
||||||
|
domain: domain,
|
||||||
|
activeMastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
|
||||||
|
requestRecentStatus(
|
||||||
|
domain: domain,
|
||||||
|
accountId: self.user.id,
|
||||||
|
authorizationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
|
||||||
|
fetchStatus()
|
||||||
|
|
||||||
|
return Output(
|
||||||
|
currentStep: currentStep.eraseToAnyPublisher(),
|
||||||
|
continueEnableSubject: continueEnableSubject.eraseToAnyPublisher(),
|
||||||
|
sendEnableSubject: sendEnableSubject.eraseToAnyPublisher(),
|
||||||
|
reportResult: reportResult
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private methods
|
||||||
|
func bindData(input: Input) {
|
||||||
|
input.didToggleSelected.sink { [weak self] (item) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard case let .reportStatus(objectID, attribute) = item else { return }
|
||||||
|
let managedObjectContext = self.statusFetchedResultsController.fetchedResultsController.managedObjectContext
|
||||||
|
guard let status = managedObjectContext.object(with: objectID) as? Status else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
attribute.isSelected = !attribute.isSelected
|
||||||
|
if attribute.isSelected {
|
||||||
|
self.append(statusID: status.id)
|
||||||
|
} else {
|
||||||
|
self.remove(statusID: status.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
let continueEnable = self.statusIDs.count > 0
|
||||||
|
self.continueEnableSubject.send(continueEnable)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
|
||||||
|
input.comment.sink { [weak self] (comment) in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
self.comment = comment
|
||||||
|
|
||||||
|
let sendEnable = (comment?.length ?? 0) > 0
|
||||||
|
self.sendEnableSubject.send(sendEnable)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindForStep1(input: Input) {
|
||||||
|
let skip = input.step1Skip.map { [weak self] value -> Void in
|
||||||
|
guard let self = self else { return value }
|
||||||
|
self.reportQuery.statusIDs?.removeAll()
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
let step1Continue = input.step1Continue.map { [weak self] value -> Void in
|
||||||
|
guard let self = self else { return value }
|
||||||
|
self.reportQuery.statusIDs = self.statusIDs
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
Publishers.Merge(skip, step1Continue)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.currentStep.value = .two
|
||||||
|
self?.sendEnableSubject.send(false)
|
||||||
|
}
|
||||||
|
.store(in: &disposeBag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func bindForStep2(input: Input, domain: String, activeMastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox) -> AnyPublisher<(Bool, Error?), Never> {
|
||||||
|
let skip = input.step2Skip.map { [weak self] value -> Void in
|
||||||
|
guard let self = self else { return value }
|
||||||
|
self.reportQuery.comment = nil
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
let step2Continue = input.step2Continue.map { [weak self] value -> Void in
|
||||||
|
guard let self = self else { return value }
|
||||||
|
self.reportQuery.comment = self.comment
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
return Publishers.Merge(skip, step2Continue)
|
||||||
|
.flatMap { [weak self] (_) -> AnyPublisher<(Bool, Error?), Never> in
|
||||||
|
guard let self = self else {
|
||||||
|
return Empty(completeImmediately: true).eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.context.apiService.report(
|
||||||
|
domain: domain,
|
||||||
|
query: self.reportQuery,
|
||||||
|
mastodonAuthenticationBox: activeMastodonAuthenticationBox
|
||||||
|
)
|
||||||
|
.map({ (content) -> (Bool, Error?) in
|
||||||
|
return (true, nil)
|
||||||
|
})
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
.tryCatch({ (error) -> AnyPublisher<(Bool, Error?), Never> in
|
||||||
|
return Just((false, error)).eraseToAnyPublisher()
|
||||||
|
})
|
||||||
|
// to covert to AnyPublisher<(Bool, Error?), Never>
|
||||||
|
.replaceError(with: (false, nil))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(statusID: Mastodon.Entity.Status.ID) {
|
||||||
|
guard self.statusIDs.contains(statusID) != true else { return }
|
||||||
|
self.statusIDs.append(statusID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(statusID: String) {
|
||||||
|
guard let index = self.statusIDs.firstIndex(of: statusID) else { return }
|
||||||
|
self.statusIDs.remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,217 @@
|
||||||
|
//
|
||||||
|
// ReportedStatusTableViewCell.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/20.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import UIKit
|
||||||
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
import CoreData
|
||||||
|
import CoreDataStack
|
||||||
|
import ActiveLabel
|
||||||
|
|
||||||
|
final class ReportedStatusTableViewCell: UITableViewCell, StatusCell {
|
||||||
|
|
||||||
|
static let bottomPaddingHeight: CGFloat = 10
|
||||||
|
|
||||||
|
weak var dependency: ReportViewController?
|
||||||
|
var disposeBag = Set<AnyCancellable>()
|
||||||
|
var pollCountdownSubscription: AnyCancellable?
|
||||||
|
var observations = Set<NSKeyValueObservation>()
|
||||||
|
|
||||||
|
let statusView = StatusView()
|
||||||
|
let separatorLine = UIView.separatorLine
|
||||||
|
|
||||||
|
let checkbox: UIImageView = {
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(textStyle: .body)
|
||||||
|
imageView.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
return imageView
|
||||||
|
}()
|
||||||
|
|
||||||
|
var separatorLineToEdgeLeadingLayoutConstraint: NSLayoutConstraint!
|
||||||
|
var separatorLineToEdgeTrailingLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
var separatorLineToMarginLeadingLayoutConstraint: NSLayoutConstraint!
|
||||||
|
var separatorLineToMarginTrailingLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
override func prepareForReuse() {
|
||||||
|
super.prepareForReuse()
|
||||||
|
statusView.updateContentWarningDisplay(isHidden: true, animated: false)
|
||||||
|
statusView.statusMosaicImageViewContainer.contentWarningOverlayView.isUserInteractionEnabled = true
|
||||||
|
statusView.pollTableView.dataSource = nil
|
||||||
|
statusView.playerContainerView.reset()
|
||||||
|
statusView.playerContainerView.contentWarningOverlayView.isUserInteractionEnabled = true
|
||||||
|
statusView.playerContainerView.isHidden = true
|
||||||
|
disposeBag.removeAll()
|
||||||
|
observations.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||||
|
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
// precondition: app is active
|
||||||
|
guard UIApplication.shared.applicationState == .active else { return }
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.statusView.drawContentWarningImageView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||||
|
super.setHighlighted(highlighted, animated: animated)
|
||||||
|
if highlighted {
|
||||||
|
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||||
|
checkbox.tintColor = Asset.Colors.Label.highlight.color
|
||||||
|
} else if !isSelected {
|
||||||
|
checkbox.image = UIImage(systemName: "circle")
|
||||||
|
checkbox.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||||
|
super.setSelected(selected, animated: animated)
|
||||||
|
|
||||||
|
if isSelected {
|
||||||
|
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||||
|
} else {
|
||||||
|
checkbox.image = UIImage(systemName: "circle")
|
||||||
|
}
|
||||||
|
checkbox.tintColor = Asset.Colors.Label.secondary.color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReportedStatusTableViewCell {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
backgroundColor = Asset.Colors.Background.systemBackground.color
|
||||||
|
|
||||||
|
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(checkbox)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
checkbox.widthAnchor.constraint(equalToConstant: 23),
|
||||||
|
checkbox.heightAnchor.constraint(equalToConstant: 22),
|
||||||
|
checkbox.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: 12),
|
||||||
|
checkbox.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(statusView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20),
|
||||||
|
statusView.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: 20),
|
||||||
|
contentView.readableContentGuide.trailingAnchor.constraint(equalTo: statusView.trailingAnchor),
|
||||||
|
contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 20),
|
||||||
|
])
|
||||||
|
|
||||||
|
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(separatorLine)
|
||||||
|
separatorLineToEdgeLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor)
|
||||||
|
separatorLineToEdgeTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
|
||||||
|
separatorLineToMarginLeadingLayoutConstraint = separatorLine.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor)
|
||||||
|
separatorLineToMarginTrailingLayoutConstraint = separatorLine.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)),
|
||||||
|
])
|
||||||
|
resetSeparatorLineLayout()
|
||||||
|
|
||||||
|
selectionStyle = .none
|
||||||
|
statusView.delegate = self
|
||||||
|
statusView.statusMosaicImageViewContainer.delegate = self
|
||||||
|
statusView.actionToolbarContainer.isHidden = true
|
||||||
|
statusView.contentWarningOverlayView.blurContentImageView.backgroundColor = backgroundColor
|
||||||
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
|
||||||
|
resetSeparatorLineLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReportedStatusTableViewCell {
|
||||||
|
private func resetSeparatorLineLayout() {
|
||||||
|
separatorLineToEdgeLeadingLayoutConstraint.isActive = false
|
||||||
|
separatorLineToEdgeTrailingLayoutConstraint.isActive = false
|
||||||
|
separatorLineToMarginLeadingLayoutConstraint.isActive = false
|
||||||
|
separatorLineToMarginTrailingLayoutConstraint.isActive = false
|
||||||
|
|
||||||
|
if traitCollection.userInterfaceIdiom == .phone {
|
||||||
|
// to edge
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLineToEdgeLeadingLayoutConstraint,
|
||||||
|
separatorLineToEdgeTrailingLayoutConstraint,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
if traitCollection.horizontalSizeClass == .compact {
|
||||||
|
// to edge
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLineToEdgeLeadingLayoutConstraint,
|
||||||
|
separatorLineToEdgeTrailingLayoutConstraint,
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
// to margin
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
separatorLineToMarginLeadingLayoutConstraint,
|
||||||
|
separatorLineToMarginTrailingLayoutConstraint,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReportedStatusTableViewCell: MosaicImageViewContainerDelegate {
|
||||||
|
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, didTapImageView imageView: UIImageView, atIndex index: Int) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func mosaicImageViewContainer(_ mosaicImageViewContainer: MosaicImageViewContainer, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
|
||||||
|
guard let dependency = self.dependency else { return }
|
||||||
|
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ReportedStatusTableViewCell: StatusViewDelegate {
|
||||||
|
func statusView(_ statusView: StatusView, headerInfoLabelDidPressed label: UILabel) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, avatarButtonDidPressed button: UIButton) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, revealContentWarningButtonDidPressed button: UIButton) {
|
||||||
|
guard let dependency = self.dependency else { return }
|
||||||
|
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
guard let dependency = self.dependency else { return }
|
||||||
|
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, playerContainerView: PlayerContainerView, contentWarningOverlayViewDidPressed contentWarningOverlayView: ContentWarningOverlayView) {
|
||||||
|
guard let dependency = self.dependency else { return }
|
||||||
|
StatusProviderFacade.responseToStatusContentWarningRevealAction(dependency: dependency, cell: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, pollVoteButtonPressed button: UIButton) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func statusView(_ statusView: StatusView, activeLabel: ActiveLabel, didSelectActiveEntity entity: ActiveEntity) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -162,7 +162,7 @@ class SettingsViewController: UIViewController, NeedsDependency {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupView() {
|
private func setupView() {
|
||||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
view.backgroundColor = Asset.Colors.Background.secondarySystemBackground.color
|
||||||
setupNavigation()
|
setupNavigation()
|
||||||
|
|
||||||
view.addSubview(tableView)
|
view.addSubview(tableView)
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
//
|
||||||
|
// APIService+Report.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import MastodonSDK
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
extension APIService {
|
||||||
|
|
||||||
|
func report(
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Reports.FileReportQuery,
|
||||||
|
mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Bool>, Error> {
|
||||||
|
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||||
|
|
||||||
|
return Mastodon.API.Reports.fileReport(session: session, domain: domain, query: query, authorization: authorization)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
//
|
||||||
|
// File.swift
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Created by ihugo on 2021/4/19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import enum NIOHTTP1.HTTPResponseStatus
|
||||||
|
|
||||||
|
extension Mastodon.API.Reports {
|
||||||
|
static func reportsEndpointURL(domain: String) -> URL {
|
||||||
|
Mastodon.API.endpointURL(domain: domain).appendingPathComponent("reports")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// File a report
|
||||||
|
///
|
||||||
|
/// Version history:
|
||||||
|
/// 1.1 - added
|
||||||
|
/// 2.3.0 - add forward parameter
|
||||||
|
/// # Reference
|
||||||
|
/// [Document](https://docs.joinmastodon.org/methods/accounts/reports/)
|
||||||
|
/// - Parameters:
|
||||||
|
/// - session: `URLSession`
|
||||||
|
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||||
|
/// - query: fileReportQuery query
|
||||||
|
/// - authorization: User token
|
||||||
|
/// - Returns: `AnyPublisher` contains status indicate if report sucessfully.
|
||||||
|
public static func fileReport(
|
||||||
|
session: URLSession,
|
||||||
|
domain: String,
|
||||||
|
query: Mastodon.API.Reports.FileReportQuery,
|
||||||
|
authorization: Mastodon.API.OAuth.Authorization
|
||||||
|
) -> AnyPublisher<Mastodon.Response.Content<Bool>, Error> {
|
||||||
|
let request = Mastodon.API.post(
|
||||||
|
url: reportsEndpointURL(domain: domain),
|
||||||
|
query: query,
|
||||||
|
authorization: authorization
|
||||||
|
)
|
||||||
|
return session.dataTaskPublisher(for: request)
|
||||||
|
.tryMap { data, response in
|
||||||
|
guard let response = response as? HTTPURLResponse else {
|
||||||
|
assertionFailure()
|
||||||
|
throw NSError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.statusCode == 200 {
|
||||||
|
return Mastodon.Response.Content(
|
||||||
|
value: true,
|
||||||
|
response: response
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let httpResponseStatus = HTTPResponseStatus(statusCode: response.statusCode)
|
||||||
|
throw Mastodon.API.Error(
|
||||||
|
httpResponseStatus: httpResponseStatus,
|
||||||
|
mastodonError: nil
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public extension Mastodon.API.Reports {
|
||||||
|
class FileReportQuery: Codable, PostQuery {
|
||||||
|
public let accountID: Mastodon.Entity.Account.ID
|
||||||
|
public var statusIDs: [Mastodon.Entity.Status.ID]?
|
||||||
|
public var comment: String?
|
||||||
|
public let forward: Bool?
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case accountID = "account_id"
|
||||||
|
case statusIDs = "status_ids"
|
||||||
|
case comment
|
||||||
|
case forward
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(
|
||||||
|
accountID: Mastodon.Entity.Account.ID,
|
||||||
|
statusIDs: [Mastodon.Entity.Status.ID]?,
|
||||||
|
comment: String?,
|
||||||
|
forward: Bool?) {
|
||||||
|
self.accountID = accountID
|
||||||
|
self.statusIDs = statusIDs
|
||||||
|
self.comment = comment
|
||||||
|
self.forward = forward
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -116,6 +116,7 @@ extension Mastodon.API {
|
||||||
public enum Suggestions { }
|
public enum Suggestions { }
|
||||||
public enum Notifications { }
|
public enum Notifications { }
|
||||||
public enum Subscriptions { }
|
public enum Subscriptions { }
|
||||||
|
public enum Reports { }
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.API.V2 {
|
extension Mastodon.API.V2 {
|
||||||
|
|
Loading…
Reference in New Issue