feat: update report flow
This commit is contained in:
parent
c964255a2a
commit
f4bb2d947f
|
@ -555,14 +555,17 @@
|
|||
}
|
||||
},
|
||||
"report": {
|
||||
"title_report": "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?",
|
||||
"report_sent_title": "Thanks for reporting, we’ll look into this.",
|
||||
"send": "Send Report",
|
||||
"skip_to_send": "Send without comment",
|
||||
"text_placeholder": "Type or paste additional comments"
|
||||
"text_placeholder": "Type or paste additional comments",
|
||||
"reported": "REPORTED"
|
||||
},
|
||||
"preview": {
|
||||
"keyboard": {
|
||||
|
|
|
@ -95,7 +95,6 @@
|
|||
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 */; };
|
||||
5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C459262599800002E742 /* SettingsToggleTableViewCell.swift */; };
|
||||
5B90C460262599800002E742 /* SettingsAppearanceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B90C45A262599800002E742 /* SettingsAppearanceTableViewCell.swift */; };
|
||||
|
@ -104,9 +103,6 @@
|
|||
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 */; };
|
||||
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 */; };
|
||||
5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; };
|
||||
|
@ -437,6 +433,19 @@
|
|||
DB98337125C9443200AD9700 /* APIService+Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337025C9443200AD9700 /* APIService+Authentication.swift */; };
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98337E25C9452D00AD9700 /* APIService+APIError.swift */; };
|
||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; };
|
||||
DB98EB4727B0DFAA0082E365 /* ReportViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */; };
|
||||
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */; };
|
||||
DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */; };
|
||||
DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5227B0F9890082E365 /* ReportHeadlineTableViewCell.swift */; };
|
||||
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5527B0FF1B0082E365 /* ReportViewControllerAppearance.swift */; };
|
||||
DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5827B109890082E365 /* ReportSupplementaryViewController.swift */; };
|
||||
DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5B27B10A730082E365 /* ReportSupplementaryViewModel.swift */; };
|
||||
DB98EB5E27B10A7A0082E365 /* ReportSupplementaryViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5D27B10A7A0082E365 /* ReportSupplementaryViewModel+Diffable.swift */; };
|
||||
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */; };
|
||||
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */; };
|
||||
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6427B216500082E365 /* ReportResultViewModel.swift */; };
|
||||
DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */; };
|
||||
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */; };
|
||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; };
|
||||
DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; };
|
||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; };
|
||||
|
@ -785,7 +794,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -794,9 +802,6 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
5CE45680252519F42FEA2D13 /* Pods-ShareActionExtension.asdk - release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - release.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - release.xcconfig"; sourceTree = "<group>"; };
|
||||
5D03938F2612D259007FE196 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1169,6 +1174,19 @@
|
|||
DB98337025C9443200AD9700 /* APIService+Authentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Authentication.swift"; sourceTree = "<group>"; };
|
||||
DB98337E25C9452D00AD9700 /* APIService+APIError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+APIError.swift"; sourceTree = "<group>"; };
|
||||
DB98339B25C96DE600AD9700 /* APIService+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Account.swift"; sourceTree = "<group>"; };
|
||||
DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportViewModel+State.swift"; sourceTree = "<group>"; };
|
||||
DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
|
||||
DB98EB5227B0F9890082E365 /* ReportHeadlineTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportHeadlineTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98EB5527B0FF1B0082E365 /* ReportViewControllerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewControllerAppearance.swift; sourceTree = "<group>"; };
|
||||
DB98EB5827B109890082E365 /* ReportSupplementaryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSupplementaryViewController.swift; sourceTree = "<group>"; };
|
||||
DB98EB5B27B10A730082E365 /* ReportSupplementaryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportSupplementaryViewModel.swift; sourceTree = "<group>"; };
|
||||
DB98EB5D27B10A7A0082E365 /* ReportSupplementaryViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportSupplementaryViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCommentTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewController.swift; sourceTree = "<group>"; };
|
||||
DB98EB6427B216500082E365 /* ReportResultViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultViewModel.swift; sourceTree = "<group>"; };
|
||||
DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportResultViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportResultActionTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentContainerView+EmptyStateView.swift"; sourceTree = "<group>"; };
|
||||
DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+PublishState.swift"; sourceTree = "<group>"; };
|
||||
DB9A488F26035963008B817C /* APIService+Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Media.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1802,13 +1820,10 @@
|
|||
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 */,
|
||||
DB98EB5727B0FF1F0082E365 /* Share */,
|
||||
DB98EB4F27B0F9300082E365 /* ReportStatus */,
|
||||
DB98EB5A27B109900082E365 /* ReportSupplementary */,
|
||||
DB98EB6327B216490082E365 /* ReportResult */,
|
||||
);
|
||||
path = Report;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2723,6 +2738,58 @@
|
|||
name = "Recovered References";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98EB4A27B0F0F50082E365 /* Cell */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB98EB5227B0F9890082E365 /* ReportHeadlineTableViewCell.swift */,
|
||||
DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */,
|
||||
DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */,
|
||||
DB98EB5F27B10E150082E365 /* ReportCommentTableViewCell.swift */,
|
||||
DB98EB6827B21A7C0082E365 /* ReportResultActionTableViewCell.swift */,
|
||||
);
|
||||
path = Cell;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98EB4F27B0F9300082E365 /* ReportStatus */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5BB04FD4262E7AFF0043BFF6 /* ReportViewController.swift */,
|
||||
5B24BBD7262DB14800A9381B /* ReportViewModel.swift */,
|
||||
5B24BBD8262DB14800A9381B /* ReportViewModel+Diffable.swift */,
|
||||
DB98EB4627B0DFAA0082E365 /* ReportViewModel+State.swift */,
|
||||
);
|
||||
path = ReportStatus;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98EB5727B0FF1F0082E365 /* Share */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB98EB4A27B0F0F50082E365 /* Cell */,
|
||||
DB98EB5527B0FF1B0082E365 /* ReportViewControllerAppearance.swift */,
|
||||
);
|
||||
path = Share;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98EB5A27B109900082E365 /* ReportSupplementary */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB98EB5827B109890082E365 /* ReportSupplementaryViewController.swift */,
|
||||
DB98EB5B27B10A730082E365 /* ReportSupplementaryViewModel.swift */,
|
||||
DB98EB5D27B10A7A0082E365 /* ReportSupplementaryViewModel+Diffable.swift */,
|
||||
);
|
||||
path = ReportSupplementary;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB98EB6327B216490082E365 /* ReportResult */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
DB98EB6127B215EB0082E365 /* ReportResultViewController.swift */,
|
||||
DB98EB6427B216500082E365 /* ReportResultViewModel.swift */,
|
||||
DB98EB6627B216560082E365 /* ReportResultViewModel+Diffable.swift */,
|
||||
);
|
||||
path = ReportResult;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DB9A489B26036E19008B817C /* MastodonAttachmentService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3739,9 +3806,7 @@
|
|||
DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */,
|
||||
2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */,
|
||||
5BB04FEF262F0DCB0043BFF6 /* ReportViewModel+Data.swift in Sources */,
|
||||
DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */,
|
||||
5B8E055826319E47006E3C53 /* ReportFooterView.swift in Sources */,
|
||||
DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */,
|
||||
DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */,
|
||||
DB023D26279FFB0A005AC798 /* ShareActivityProvider.swift in Sources */,
|
||||
|
@ -3801,6 +3866,7 @@
|
|||
DB0618032785A7100030EE79 /* RegisterSection.swift in Sources */,
|
||||
DB63F76B279A5ED300455B82 /* NotificationTimelineViewModel+LoadOldestState.swift in Sources */,
|
||||
DBF1D251269DB01200C1C08A /* SearchHistoryViewController.swift in Sources */,
|
||||
DB98EB5E27B10A7A0082E365 /* ReportSupplementaryViewModel+Diffable.swift in Sources */,
|
||||
DB0FCB7C2795821F006C02E2 /* StatusThreadRootTableViewCell.swift in Sources */,
|
||||
DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */,
|
||||
DBB5250E2611EBAF002F1F29 /* ProfileSegmentedViewController.swift in Sources */,
|
||||
|
@ -3883,6 +3949,7 @@
|
|||
DB73BF47271199CA00781945 /* Instance.swift in Sources */,
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */,
|
||||
DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */,
|
||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
|
||||
|
@ -3942,6 +4009,7 @@
|
|||
DB6D9F76263587C7008423CD /* SettingFetchedResultController.swift in Sources */,
|
||||
DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */,
|
||||
5D0393902612D259007FE196 /* WebViewController.swift in Sources */,
|
||||
DB98EB6227B215EB0082E365 /* ReportResultViewController.swift in Sources */,
|
||||
DB6B74FA272FC2B500C70B6E /* APIService+Follower.swift in Sources */,
|
||||
DB6B74F4272FBAE700C70B6E /* FollowerListViewModel+Diffable.swift in Sources */,
|
||||
DB6B74F2272FB67600C70B6E /* FollowerListViewModel.swift in Sources */,
|
||||
|
@ -3955,10 +4023,12 @@
|
|||
DB84811727883C2600BBEABA /* MastodonRegisterPasswordHintTableViewCell.swift in Sources */,
|
||||
2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */,
|
||||
DB35FC1F2612F1D9006193C9 /* ProfileRelationshipActionButton.swift in Sources */,
|
||||
DB98EB6727B216560082E365 /* ReportResultViewModel+Diffable.swift in Sources */,
|
||||
DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */,
|
||||
DB025B95278D6530002F581E /* Persistence+MastodonUser.swift in Sources */,
|
||||
DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */,
|
||||
DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */,
|
||||
DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */,
|
||||
DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */,
|
||||
DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */,
|
||||
DB63F769279A5EBB00455B82 /* NotificationTimelineViewModel+Diffable.swift in Sources */,
|
||||
|
@ -3969,6 +4039,7 @@
|
|||
DBB525562611EDCA002F1F29 /* UserTimelineViewModel.swift in Sources */,
|
||||
DB0618012785732C0030EE79 /* ServerRulesTableViewCell.swift in Sources */,
|
||||
DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */,
|
||||
DB98EB5C27B10A730082E365 /* ReportSupplementaryViewModel.swift in Sources */,
|
||||
DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */,
|
||||
DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */,
|
||||
2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */,
|
||||
|
@ -3977,6 +4048,7 @@
|
|||
DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */,
|
||||
DB336F28278D6EC70031E64B /* MastodonFieldContainer.swift in Sources */,
|
||||
DBF156E42702DB3F00EC00B7 /* HandleTapAction.swift in Sources */,
|
||||
DB98EB4727B0DFAA0082E365 /* ReportViewModel+State.swift in Sources */,
|
||||
2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */,
|
||||
DB6B74F6272FBCDB00C70B6E /* FollowerListViewModel+State.swift in Sources */,
|
||||
DB87D4452609BE0500D12C0D /* ComposeStatusPollOptionCollectionViewCell.swift in Sources */,
|
||||
|
@ -4004,7 +4076,6 @@
|
|||
DB73BF4B27140C0800781945 /* UITableViewDiffableDataSource.swift in Sources */,
|
||||
DBB525642612C988002F1F29 /* MeProfileViewModel.swift in Sources */,
|
||||
DB6B74EF272FB55000C70B6E /* FollowerListViewController.swift in Sources */,
|
||||
5BB04FE9262EFC300043BFF6 /* ReportedStatusTableviewCell.swift in Sources */,
|
||||
DB0FCB942797E2B0006C02E2 /* SearchResultViewModel+Diffable.swift in Sources */,
|
||||
DB63F752279944AA00455B82 /* SearchHistorySectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DBBC24C426A544B900398BB9 /* Theme.swift in Sources */,
|
||||
|
@ -4018,7 +4089,6 @@
|
|||
DB1FD44425F26CCC004CFCFC /* PickServerSection.swift in Sources */,
|
||||
0FB3D30F25E525CD00AAD544 /* PickServerCategoryView.swift in Sources */,
|
||||
DB6180E626391B550018D199 /* MediaPreviewTransitionController.swift in Sources */,
|
||||
5BB04FDB262EA3070043BFF6 /* ReportHeaderView.swift in Sources */,
|
||||
DB0FCB922796DE19006C02E2 /* TrendSectionHeaderCollectionReusableView.swift in Sources */,
|
||||
DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */,
|
||||
DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */,
|
||||
|
@ -4049,6 +4119,7 @@
|
|||
DB6180F826391D660018D199 /* MediaPreviewingViewController.swift in Sources */,
|
||||
DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */,
|
||||
DB71C7CB271D5A0300BE3819 /* LineChartView.swift in Sources */,
|
||||
DB98EB5627B0FF1B0082E365 /* ReportViewControllerAppearance.swift in Sources */,
|
||||
DB938F1526241FDF00E5B6C1 /* APIService+Thread.swift in Sources */,
|
||||
2D206B8625F5FB0900143C56 /* Double.swift in Sources */,
|
||||
DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */,
|
||||
|
@ -4061,6 +4132,7 @@
|
|||
2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */,
|
||||
DB084B5725CBC56C00F898ED /* Status.swift in Sources */,
|
||||
2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */,
|
||||
DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */,
|
||||
DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */,
|
||||
DB9282B225F3222800823B15 /* PickServerEmptyStateView.swift in Sources */,
|
||||
DB697DDF278F524F004EF2F7 /* DataSourceFacade+Profile.swift in Sources */,
|
||||
|
@ -4158,20 +4230,24 @@
|
|||
DB63F756279949BD00455B82 /* Persistence+SearchHistory.swift in Sources */,
|
||||
2D4AD8A226316CD200613EFC /* SelectedAccountSection.swift in Sources */,
|
||||
DB63F775279A997D00455B82 /* NotificationTableViewCell+ViewModel.swift in Sources */,
|
||||
DB98EB5927B109890082E365 /* ReportSupplementaryViewController.swift in Sources */,
|
||||
DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */,
|
||||
DB789A1225F9F2CC0071ACA0 /* ComposeViewModel.swift in Sources */,
|
||||
DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */,
|
||||
DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */,
|
||||
DB6D1B3D2636857500ACB481 /* AppearancePreference.swift in Sources */,
|
||||
DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */,
|
||||
DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */,
|
||||
DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */,
|
||||
DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */,
|
||||
5B24BBE2262DB19100A9381B /* APIService+Report.swift in Sources */,
|
||||
DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */,
|
||||
DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */,
|
||||
DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */,
|
||||
5BB04FF5262F0E6D0043BFF6 /* ReportSection.swift in Sources */,
|
||||
DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */,
|
||||
DB023D2A27A0FE5C005AC798 /* DataSourceProvider+NotificationTableViewCellDelegate.swift in Sources */,
|
||||
DB98EB6027B10E150082E365 /* ReportCommentTableViewCell.swift in Sources */,
|
||||
DB0FCB962797E6C2006C02E2 /* SearchResultViewController+DataSourceProvider.swift in Sources */,
|
||||
DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */,
|
||||
DB6180E326391A4C0018D199 /* ViewControllerAnimatedTransitioning.swift in Sources */,
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<key>AppShared.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>22</integer>
|
||||
<integer>19</integer>
|
||||
</dict>
|
||||
<key>CoreDataStack.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -97,7 +97,7 @@
|
|||
<key>MastodonIntent.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>20</integer>
|
||||
<integer>18</integer>
|
||||
</dict>
|
||||
<key>MastodonIntents.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
|
@ -117,7 +117,7 @@
|
|||
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>19</integer>
|
||||
<integer>20</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
|
|
|
@ -184,6 +184,8 @@ extension SceneCoordinator {
|
|||
|
||||
// report
|
||||
case report(viewModel: ReportViewModel)
|
||||
case reportSupplementary(viewModel: ReportSupplementaryViewModel)
|
||||
case reportResult(viewModel: ReportResultViewModel)
|
||||
|
||||
// suggestion account
|
||||
case suggestionAccount(viewModel: SuggestionAccountViewModel)
|
||||
|
@ -441,6 +443,18 @@ private extension SceneCoordinator {
|
|||
let _viewController = FollowingListViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportSupplementary(let viewModel):
|
||||
let _viewController = ReportSupplementaryViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .reportResult(let viewModel):
|
||||
let _viewController = ReportResultViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .suggestionAccount(let viewModel):
|
||||
let _viewController = SuggestionAccountViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
|
@ -476,10 +490,6 @@ private extension SceneCoordinator {
|
|||
let _viewController = SettingsViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
case .report(let viewModel):
|
||||
let _viewController = ReportViewController()
|
||||
_viewController.viewModel = viewModel
|
||||
viewController = _viewController
|
||||
}
|
||||
|
||||
setupDependency(for: viewController as? NeedsDependency)
|
||||
|
|
|
@ -21,7 +21,11 @@ extension PickServerSection {
|
|||
dependency: NeedsDependency,
|
||||
pickServerCellDelegate: PickServerCellDelegate
|
||||
) -> UITableViewDiffableDataSource<PickServerSection, PickServerItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { [
|
||||
weak dependency,
|
||||
weak pickServerCellDelegate
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
|
|
|
@ -6,7 +6,35 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import CoreDataStack
|
||||
|
||||
enum ReportItem: Hashable {
|
||||
|
||||
case header(context: HeaderContext)
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
case comment(context: CommentContext)
|
||||
case result(record: ManagedObjectRecord<MastodonUser>)
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
extension ReportItem {
|
||||
struct HeaderContext: Hashable {
|
||||
let primaryLabelText: String
|
||||
let secondaryLabelText: String
|
||||
}
|
||||
|
||||
class CommentContext: Hashable {
|
||||
let id = UUID()
|
||||
@Published var comment: String = ""
|
||||
|
||||
static func == (
|
||||
lhs: ReportItem.CommentContext,
|
||||
rhs: ReportItem.CommentContext
|
||||
) -> Bool {
|
||||
lhs.comment == rhs.comment
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import CoreDataStack
|
|||
import Foundation
|
||||
import MastodonSDK
|
||||
import UIKit
|
||||
import AVKit
|
||||
import os.log
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
@ -21,50 +20,98 @@ enum ReportSection: Equatable, Hashable {
|
|||
}
|
||||
|
||||
extension ReportSection {
|
||||
static func tableViewDiffableDataSource(
|
||||
for tableView: UITableView,
|
||||
dependency: ReportViewController,
|
||||
managedObjectContext: NSManagedObjectContext,
|
||||
timestampUpdatePublisher: AnyPublisher<Date, Never>
|
||||
|
||||
struct Configuration {
|
||||
}
|
||||
|
||||
static func diffableDataSource(
|
||||
tableView: UITableView,
|
||||
context: AppContext,
|
||||
configuration: Configuration
|
||||
) -> UITableViewDiffableDataSource<ReportSection, ReportItem> {
|
||||
UITableViewDiffableDataSource(tableView: tableView) {[
|
||||
weak dependency
|
||||
] tableView, indexPath, item -> UITableViewCell? in
|
||||
return UITableViewCell()
|
||||
guard let dependency = dependency else { return UITableViewCell() }
|
||||
|
||||
tableView.register(ReportHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: ReportHeadlineTableViewCell.self))
|
||||
tableView.register(ReportStatusTableViewCell.self, forCellReuseIdentifier: String(describing: ReportStatusTableViewCell.self))
|
||||
tableView.register(ReportCommentTableViewCell.self, forCellReuseIdentifier: String(describing: ReportCommentTableViewCell.self))
|
||||
tableView.register(ReportResultActionTableViewCell.self, forCellReuseIdentifier: String(describing: ReportResultActionTableViewCell.self))
|
||||
tableView.register(TimelineBottomLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self))
|
||||
|
||||
// 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,
|
||||
// tableView: tableView,
|
||||
// timelineContext: .report,
|
||||
// dependency: dependency,
|
||||
// readableLayoutFrame: tableView.readableContentGuide.layoutFrame,
|
||||
// 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
|
||||
// }
|
||||
return UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item -> UITableViewCell? in
|
||||
switch item {
|
||||
case .header(let headerContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportHeadlineTableViewCell.self), for: indexPath) as! ReportHeadlineTableViewCell
|
||||
cell.primaryLabel.text = headerContext.primaryLabelText
|
||||
cell.secondaryLabel.text = headerContext.secondaryLabelText
|
||||
return cell
|
||||
case .status(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportStatusTableViewCell.self), for: indexPath) as! ReportStatusTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let status = record.object(in: context.managedObjectContext) else { return }
|
||||
configure(
|
||||
context: context,
|
||||
tableView: tableView,
|
||||
cell: cell,
|
||||
viewModel: .init(value: status),
|
||||
configuration: configuration
|
||||
)
|
||||
}
|
||||
return cell
|
||||
case .comment(let commentContext):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportCommentTableViewCell.self), for: indexPath) as! ReportCommentTableViewCell
|
||||
cell.commentTextView.text = commentContext.comment
|
||||
NotificationCenter.default.publisher(for: UITextView.textDidChangeNotification, object: cell.commentTextView)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak cell] notification in
|
||||
guard let cell = cell else { return }
|
||||
commentContext.comment = cell.commentTextView.text
|
||||
|
||||
// fix shadow get animation issue when cell height changes
|
||||
UIView.performWithoutAnimation {
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
.store(in: &cell.disposeBag)
|
||||
return cell
|
||||
case .result(let record):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ReportResultActionTableViewCell.self), for: indexPath) as! ReportResultActionTableViewCell
|
||||
context.managedObjectContext.performAndWait {
|
||||
guard let user = record.object(in: context.managedObjectContext) else { return }
|
||||
cell.avatarImageView.configure(configuration: .init(url: user.avatarImageURL()))
|
||||
}
|
||||
return cell
|
||||
case .bottomLoader:
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: TimelineBottomLoaderTableViewCell.self), for: indexPath) as! TimelineBottomLoaderTableViewCell
|
||||
cell.activityIndicatorView.startAnimating()
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportSection {
|
||||
|
||||
static func configure(
|
||||
context: AppContext,
|
||||
tableView: UITableView,
|
||||
cell: ReportStatusTableViewCell,
|
||||
viewModel: ReportStatusTableViewCell.ViewModel,
|
||||
configuration: Configuration
|
||||
) {
|
||||
StatusSection.setupStatusPollDataSource(
|
||||
context: context,
|
||||
statusView: cell.statusView
|
||||
)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0 as UserIdentifier? }
|
||||
.assign(to: \.userIdentifier, on: cell.statusView.viewModel)
|
||||
.store(in: &cell.disposeBag)
|
||||
|
||||
cell.configure(
|
||||
tableView: tableView,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,29 +13,10 @@ enum StatusItem: Hashable {
|
|||
case feed(record: ManagedObjectRecord<Feed>)
|
||||
case feedLoader(record: ManagedObjectRecord<Feed>)
|
||||
case status(record: ManagedObjectRecord<Status>)
|
||||
// case statusLoader(record: ManagedObjectRecord<Status>, context: StatusLoaderContext)
|
||||
case thread(Thread)
|
||||
case topLoader
|
||||
case bottomLoader
|
||||
}
|
||||
|
||||
//extension StatusItem {
|
||||
// final class StatusLoaderContext: Hashable {
|
||||
// let id = UUID()
|
||||
// @Published var isFetching = false
|
||||
//
|
||||
// static func == (
|
||||
// lhs: StatusItem.StatusLoaderContext,
|
||||
// rhs: StatusItem.StatusLoaderContext
|
||||
// ) -> Bool {
|
||||
// return lhs.id == rhs.id
|
||||
// }
|
||||
//
|
||||
// func hash(into hasher: inout Hasher) {
|
||||
// hasher.combine(id)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
extension StatusItem {
|
||||
enum Thread: Hashable {
|
||||
|
|
|
@ -17,13 +17,6 @@ import MastodonSDK
|
|||
import NaturalLanguage
|
||||
import MastodonUI
|
||||
|
||||
// import LinkPresentation
|
||||
|
||||
//protocol StatusCell: DisposeBagCollectable {
|
||||
// var statusView: StatusView { get }
|
||||
// var isFiltered: Bool { get set }
|
||||
//}
|
||||
|
||||
enum StatusSection: Equatable, Hashable {
|
||||
case main
|
||||
}
|
||||
|
|
|
@ -209,7 +209,22 @@ extension DataSourceFacade {
|
|||
alertController.addAction(cancelAction)
|
||||
dependency.present(alertController, animated: true, completion: nil)
|
||||
case .reportUser:
|
||||
assertionFailure()
|
||||
Task {
|
||||
guard let user = menuContext.author else { return }
|
||||
|
||||
let reportViewModel = ReportViewModel(
|
||||
context: dependency.context,
|
||||
user: user,
|
||||
status: menuContext.status
|
||||
)
|
||||
|
||||
dependency.coordinator.present(
|
||||
scene: .report(viewModel: reportViewModel),
|
||||
from: dependency,
|
||||
transition: .modal(animated: true, completion: nil)
|
||||
)
|
||||
} // end Task
|
||||
|
||||
case .shareUser:
|
||||
guard let user = menuContext.author else {
|
||||
assertionFailure()
|
||||
|
|
|
@ -603,19 +603,6 @@ extension HomeTimelineViewController: ScrollViewContainer {
|
|||
|
||||
}
|
||||
|
||||
// MARK: - AVPlayerViewControllerDelegate
|
||||
//extension HomeTimelineViewController: AVPlayerViewControllerDelegate {
|
||||
//
|
||||
// func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
// handlePlayerViewController(playerViewController, willBeginFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
// }
|
||||
//
|
||||
// func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
|
||||
// handlePlayerViewController(playerViewController, willEndFullScreenPresentationWithAnimationCoordinator: coordinator)
|
||||
// }
|
||||
//
|
||||
//}
|
||||
|
||||
// MARK: - StatusTableViewCellDelegate
|
||||
extension HomeTimelineViewController: StatusTableViewCellDelegate { }
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ final class HomeTimelineViewModel: NSObject {
|
|||
let context: AppContext
|
||||
let fetchedResultsController: FeedFetchedResultsController
|
||||
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||
//let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
let viewDidAppear = PassthroughSubject<Void, Never>()
|
||||
let homeTimelineNavigationBarTitleViewModel: HomeTimelineNavigationBarTitleViewModel
|
||||
let lastAutomaticFetchTimestamp = CurrentValueSubject<Date?, Never>(nil)
|
||||
|
@ -83,28 +82,6 @@ final class HomeTimelineViewModel: NSObject {
|
|||
self.homeTimelineNavigationBarTitleViewModel = HomeTimelineNavigationBarTitleViewModel(context: context)
|
||||
super.init()
|
||||
|
||||
// fetchedResultsController.delegate = self
|
||||
|
||||
// timelinePredicate
|
||||
// .receive(on: DispatchQueue.main)
|
||||
// .compactMap { $0 }
|
||||
// .first() // set once
|
||||
// .sink { [weak self] predicate in
|
||||
// guard let self = self else { return }
|
||||
// self.fetchedResultsController.fetchRequest.predicate = predicate
|
||||
// do {
|
||||
// self.diffableDataSource?.defaultRowAnimation = .fade
|
||||
// try self.fetchedResultsController.performFetch()
|
||||
// DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
||||
// guard let self = self else { return }
|
||||
// self.diffableDataSource?.defaultRowAnimation = .automatic
|
||||
// }
|
||||
// } catch {
|
||||
// assertionFailure(error.localizedDescription)
|
||||
// }
|
||||
// }
|
||||
// .store(in: &disposeBag)
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.sink { [weak self] authenticationBox in
|
||||
guard let self = self else { return }
|
||||
|
|
|
@ -37,9 +37,6 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.register(OnboardingHeadlineTableViewCell.self, forCellReuseIdentifier: String(describing: OnboardingHeadlineTableViewCell.self))
|
||||
tableView.register(PickServerCell.self, forCellReuseIdentifier: String(describing: PickServerCell.self))
|
||||
tableView.register(PickServerLoaderTableViewCell.self, forCellReuseIdentifier: String(describing: PickServerLoaderTableViewCell.self))
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
|
@ -115,7 +112,6 @@ extension MastodonPickServerViewController {
|
|||
self.tableView.contentInset.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
|
||||
// fix AutoLayout warning when observe before view appear
|
||||
viewModel.viewWillAppear
|
||||
|
|
|
@ -39,6 +39,14 @@ final class NavigationActionView: UIView {
|
|||
return button
|
||||
}()
|
||||
|
||||
var hidesBackButton: Bool = false {
|
||||
didSet { backButtonShadowContainer.isHidden = hidesBackButton }
|
||||
}
|
||||
|
||||
var hidesNextButton: Bool = false {
|
||||
didSet { nextButtonShadowContainer.isHidden = hidesNextButton }
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
|
@ -52,6 +60,7 @@ final class NavigationActionView: UIView {
|
|||
}
|
||||
|
||||
extension NavigationActionView {
|
||||
|
||||
private func _init() {
|
||||
buttonContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
buttonContainer.preservesSuperviewLayoutMargins = true
|
||||
|
|
|
@ -67,7 +67,6 @@ extension UserTimelineViewController {
|
|||
])
|
||||
|
||||
tableView.delegate = self
|
||||
// tableView.prefetchDataSource = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView,
|
||||
statusTableViewCellDelegate: self
|
||||
|
|
|
@ -1,110 +0,0 @@
|
|||
//
|
||||
// ReportFooterView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/22.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
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 = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor
|
||||
|
||||
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
|
|
@ -1,129 +0,0 @@
|
|||
//
|
||||
// ReportView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/20.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
let bottomSeparatorLine = UIView.separatorLine
|
||||
|
||||
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 = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor
|
||||
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
|
||||
)
|
||||
])
|
||||
|
||||
bottomSeparatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(bottomSeparatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
bottomSeparatorLine.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
bottomSeparatorLine.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
bottomSeparatorLine.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
bottomSeparatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: self)).priority(.defaultHigh),
|
||||
])
|
||||
}
|
||||
|
||||
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,113 @@
|
|||
//
|
||||
// ReportResultViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-8.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class ReportResultViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: ReportResultViewModel!
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.backgroundColor = Asset.Scene.Report.background.color
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.allowsMultipleSelection = true
|
||||
if #available(iOS 15.0, *) {
|
||||
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
navigationActionView.hidesBackButton = true
|
||||
navigationActionView.nextButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal)
|
||||
return navigationActionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportResultViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupAppearance()
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
|
||||
navigationItem.hidesBackButton = true
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView
|
||||
)
|
||||
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = navigationActionView.frame.height
|
||||
self.tableView.contentInset.bottom = inset
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(ReportSupplementaryViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportResultViewController {
|
||||
|
||||
@objc func nextButtonDidPressed(_ sender: UIButton) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension ReportResultViewController: UITableViewDelegate { }
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// ReportResultViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension ReportResultViewModel {
|
||||
|
||||
static let reportItemHeaderContext = ReportItem.HeaderContext(
|
||||
primaryLabelText: "Thanks for reporting, we’ll look into this.",
|
||||
secondaryLabelText: ""
|
||||
)
|
||||
|
||||
func setupDiffableDataSource(
|
||||
tableView: UITableView
|
||||
) {
|
||||
diffableDataSource = ReportSection.diffableDataSource(
|
||||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: ReportSection.Configuration()
|
||||
)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, ReportItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems([.header(context: ReportResultViewModel.reportItemHeaderContext)], toSection: .main)
|
||||
snapshot.appendItems([.result(record: user)], toSection: .main)
|
||||
diffableDataSource?.apply(snapshot)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
//
|
||||
// ReportResultViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-8.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import MastodonSDK
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
class ReportResultViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, ReportItem>?
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
user: ManagedObjectRecord<MastodonUser>
|
||||
) {
|
||||
self.context = context
|
||||
self.user = user
|
||||
// end init
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,221 @@
|
|||
//
|
||||
// ReportViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/20.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
class ReportViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: ReportViewModel!
|
||||
|
||||
// MAKK: - UI
|
||||
lazy var cancelBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(ReportViewController.cancelBarButtonItemDidPressed(_:))
|
||||
)
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.backgroundColor = Asset.Scene.Report.background.color
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
tableView.allowsMultipleSelection = true
|
||||
if #available(iOS 15.0, *) {
|
||||
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
navigationActionView.backButton.setTitle(L10n.Common.Controls.Actions.skip, for: .normal)
|
||||
return navigationActionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupAppearance()
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
|
||||
navigationItem.rightBarButtonItem = cancelBarButtonItem
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView
|
||||
)
|
||||
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = navigationActionView.frame.height
|
||||
self.tableView.contentInset.bottom = inset
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
// setup batch fetch
|
||||
viewModel.listBatchFetchViewModel.setup(scrollView: tableView)
|
||||
viewModel.listBatchFetchViewModel.shouldFetch
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
guard self.view.window != nil else { return }
|
||||
self.viewModel.stateMachine.enter(ReportViewModel.State.Loading.self)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.$isNextButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(ReportViewController.skipButtonDidPressed(_:)), for: .touchUpInside)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(ReportViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportViewController {
|
||||
|
||||
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func skipButtonDidPressed(_ sender: UIButton) {
|
||||
var selectStatuses: [ManagedObjectRecord<Status>] = []
|
||||
if let selectStatus = viewModel.status {
|
||||
selectStatuses.append(selectStatus)
|
||||
}
|
||||
|
||||
let reportSupplementaryViewModel = ReportSupplementaryViewModel(
|
||||
context: context,
|
||||
user: viewModel.user,
|
||||
selectStatuses: selectStatuses
|
||||
)
|
||||
coordinator.present(
|
||||
scene: .reportSupplementary(viewModel: reportSupplementaryViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
@objc func nextButtonDidPressed(_ sender: UIButton) {
|
||||
let selectStatuses = Array(viewModel.selectStatuses)
|
||||
guard !selectStatuses.isEmpty else { return }
|
||||
|
||||
let reportSupplementaryViewModel = ReportSupplementaryViewModel(
|
||||
context: context,
|
||||
user: viewModel.user,
|
||||
selectStatuses: selectStatuses
|
||||
)
|
||||
coordinator.present(
|
||||
scene: .reportSupplementary(viewModel: reportSupplementaryViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension ReportViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath),
|
||||
case .status = item
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return indexPath
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath),
|
||||
case let .status(record) = item
|
||||
else {
|
||||
tableView.deselectRow(at: indexPath, animated: true)
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.selectStatuses.append(record)
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, willDeselectRowAt indexPath: IndexPath) -> IndexPath? {
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath),
|
||||
case let .status(record) = item
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
// disallow deselect initial selection
|
||||
guard record != viewModel.status else { return nil }
|
||||
|
||||
return indexPath
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
|
||||
guard let item = viewModel.diffableDataSource?.itemIdentifier(for: indexPath),
|
||||
case let .status(record) = item
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.selectStatuses.remove(record)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension ReportViewController: UIAdaptivePresentationControllerDelegate {
|
||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// ReportViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/19.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension ReportViewModel {
|
||||
|
||||
static let reportItemHeaderContext = ReportItem.HeaderContext(
|
||||
primaryLabelText: L10n.Scene.Report.content1,
|
||||
secondaryLabelText: L10n.Scene.Report.step1
|
||||
)
|
||||
|
||||
func setupDiffableDataSource(
|
||||
tableView: UITableView
|
||||
) {
|
||||
diffableDataSource = ReportSection.diffableDataSource(
|
||||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: ReportSection.Configuration()
|
||||
)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, ReportItem>()
|
||||
snapshot.appendSections([.main])
|
||||
diffableDataSource?.apply(snapshot)
|
||||
|
||||
statusFetchedResultsController.$records
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] records in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, ReportItem>()
|
||||
snapshot.appendSections([.main])
|
||||
|
||||
snapshot.appendItems([.header(context: ReportViewModel.reportItemHeaderContext)], toSection: .main)
|
||||
|
||||
let items = records.map { ReportItem.status(record: $0) }
|
||||
snapshot.appendItems(items, toSection: .main)
|
||||
|
||||
let selectItems = items.filter { item in
|
||||
guard case let .status(record) = item else { return false }
|
||||
return self.selectStatuses.contains(record)
|
||||
}
|
||||
|
||||
guard let currentState = self.stateMachine.currentState else { return }
|
||||
switch currentState {
|
||||
case is State.Initial,
|
||||
is State.Loading,
|
||||
is State.Idle,
|
||||
is State.Fail:
|
||||
snapshot.appendItems([.bottomLoader], toSection: .main)
|
||||
case is State.NoMore:
|
||||
break
|
||||
default:
|
||||
assertionFailure()
|
||||
break
|
||||
}
|
||||
|
||||
diffableDataSource.applySnapshot(snapshot, animated: false) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let diffableDataSource = self.diffableDataSource else { return }
|
||||
|
||||
let selectIndexPaths = selectItems.compactMap { item in
|
||||
diffableDataSource.indexPath(for: item)
|
||||
}
|
||||
|
||||
// Only the first selection make the initial selection
|
||||
// The later selection could be ignored
|
||||
for indexPath in selectIndexPaths {
|
||||
tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
//
|
||||
// ReportViewModel+State.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import func QuartzCore.CACurrentMediaTime
|
||||
import Foundation
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import GameplayKit
|
||||
|
||||
extension ReportViewModel {
|
||||
class State: GKState {
|
||||
|
||||
let logger = Logger(subsystem: "ReportViewModel.State", category: "StateMachine")
|
||||
|
||||
let id = UUID()
|
||||
|
||||
var name: String {
|
||||
String(describing: Self.self)
|
||||
}
|
||||
|
||||
weak var viewModel: ReportViewModel?
|
||||
|
||||
init(viewModel: ReportViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
let previousState = previousState as? ReportViewModel.State
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] enter \(self.name), previous: \(previousState?.name ?? "<nil>")")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func enter(state: State.Type) {
|
||||
stateMachine?.enter(state)
|
||||
}
|
||||
|
||||
deinit {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportViewModel.State {
|
||||
class Initial: ReportViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
guard let _ = viewModel else { return false }
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Loading: ReportViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Fail.Type:
|
||||
return true
|
||||
case is Idle.Type:
|
||||
return true
|
||||
case is NoMore.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
|
||||
guard let authenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
stateMachine.enter(Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
let maxID = viewModel.statusFetchedResultsController.statusIDs.value.last
|
||||
|
||||
Task {
|
||||
let managedObjectContext = viewModel.context.managedObjectContext
|
||||
let _userID: MastodonUser.ID? = try await managedObjectContext.perform {
|
||||
guard let user = viewModel.user.object(in: managedObjectContext) else { return nil }
|
||||
return user.id
|
||||
}
|
||||
guard let userID = _userID else {
|
||||
await enter(state: Fail.self)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
let response = try await viewModel.context.apiService.userTimeline(
|
||||
accountID: userID,
|
||||
maxID: maxID,
|
||||
sinceID: nil,
|
||||
excludeReplies: true,
|
||||
excludeReblogs: true,
|
||||
onlyMedia: false,
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
|
||||
var hasNewStatusesAppend = false
|
||||
var statusIDs = viewModel.statusFetchedResultsController.statusIDs.value
|
||||
for status in response.value {
|
||||
guard !statusIDs.contains(status.id) else { continue }
|
||||
statusIDs.append(status.id)
|
||||
hasNewStatusesAppend = true
|
||||
}
|
||||
|
||||
if hasNewStatusesAppend {
|
||||
await enter(state: Idle.self)
|
||||
} else {
|
||||
await enter(state: NoMore.self)
|
||||
}
|
||||
viewModel.statusFetchedResultsController.statusIDs.value = statusIDs
|
||||
|
||||
} catch {
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch user timeline fail: \(error.localizedDescription)")
|
||||
await enter(state: Fail.self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Fail: ReportViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Idle: ReportViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
switch stateClass {
|
||||
case is Loading.Type:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class NoMore: ReportViewModel.State {
|
||||
override func isValidNextState(_ stateClass: AnyClass) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
override func didEnter(from previousState: GKState?) {
|
||||
super.didEnter(from: previousState)
|
||||
guard let viewModel = viewModel, let _ = stateMachine else { return }
|
||||
guard let diffableDataSource = viewModel.diffableDataSource else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
var snapshot = diffableDataSource.snapshot()
|
||||
snapshot.deleteItems([.bottomLoader])
|
||||
diffableDataSource.apply(snapshot)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// ReportViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/19.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Foundation
|
||||
import GameplayKit
|
||||
import MastodonSDK
|
||||
import OrderedCollections
|
||||
import os.log
|
||||
import UIKit
|
||||
|
||||
class ReportViewModel {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
// input
|
||||
let context: AppContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let status: ManagedObjectRecord<Status>?
|
||||
let statusFetchedResultsController: StatusFetchedResultsController
|
||||
let listBatchFetchViewModel = ListBatchFetchViewModel()
|
||||
|
||||
@Published var selectStatuses = OrderedSet<ManagedObjectRecord<Status>>()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, ReportItem>?
|
||||
private(set) lazy var stateMachine: GKStateMachine = {
|
||||
let stateMachine = GKStateMachine(states: [
|
||||
State.Initial(viewModel: self),
|
||||
State.Fail(viewModel: self),
|
||||
State.Idle(viewModel: self),
|
||||
State.Loading(viewModel: self),
|
||||
State.NoMore(viewModel: self),
|
||||
])
|
||||
stateMachine.enter(State.Initial.self)
|
||||
return stateMachine
|
||||
}()
|
||||
|
||||
@Published var isNextButtonEnabled = false
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
status: ManagedObjectRecord<Status>?
|
||||
) {
|
||||
self.context = context
|
||||
self.user = user
|
||||
self.status = status
|
||||
self.statusFetchedResultsController = StatusFetchedResultsController(
|
||||
managedObjectContext: context.managedObjectContext,
|
||||
domain: nil,
|
||||
additionalTweetPredicate: nil
|
||||
)
|
||||
// end init
|
||||
|
||||
if let status = status {
|
||||
selectStatuses.append(status)
|
||||
}
|
||||
|
||||
context.authenticationService.activeMastodonAuthenticationBox
|
||||
.map { $0?.domain }
|
||||
.assign(to: \.value, on: statusFetchedResultsController.domain)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
$selectStatuses
|
||||
.map { statuses -> Bool in
|
||||
return status == nil ? !statuses.isEmpty : statuses.count > 1
|
||||
}
|
||||
.assign(to: &$isNextButtonEnabled)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,181 @@
|
|||
//
|
||||
// ReportSupplementaryViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class ReportSupplementaryViewController: UIViewController, NeedsDependency, ReportViewControllerAppearance {
|
||||
|
||||
let logger = Logger(subsystem: "ReportSupplementaryViewController", category: "ViewController")
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
private var observations = Set<NSKeyValueObservation>()
|
||||
|
||||
weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
|
||||
weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
var viewModel: ReportSupplementaryViewModel! { willSet { precondition(!isViewLoaded) } }
|
||||
|
||||
|
||||
// MAKK: - UI
|
||||
lazy var cancelBarButtonItem = UIBarButtonItem(
|
||||
barButtonSystemItem: .cancel,
|
||||
target: self,
|
||||
action: #selector(ReportSupplementaryViewController.cancelBarButtonItemDidPressed(_:))
|
||||
)
|
||||
|
||||
let activityIndicatorBarButtonItem: UIBarButtonItem = {
|
||||
let activityIndicatorView = UIActivityIndicatorView(style: .medium)
|
||||
activityIndicatorView.startAnimating()
|
||||
let barButtonItem = UIBarButtonItem(customView: activityIndicatorView)
|
||||
return barButtonItem
|
||||
}()
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView = ControlContainableTableView()
|
||||
tableView.backgroundColor = Asset.Scene.Report.background.color
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = .clear
|
||||
tableView.keyboardDismissMode = .onDrag
|
||||
if #available(iOS 15.0, *) {
|
||||
tableView.sectionHeaderTopPadding = .leastNonzeroMagnitude
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
return tableView
|
||||
}()
|
||||
|
||||
let navigationActionView: NavigationActionView = {
|
||||
let navigationActionView = NavigationActionView()
|
||||
navigationActionView.backgroundColor = Asset.Scene.Onboarding.onboardingBackground.color
|
||||
navigationActionView.backButton.setTitle(L10n.Common.Controls.Actions.skip, for: .normal)
|
||||
return navigationActionView
|
||||
}()
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportSupplementaryViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupAppearance()
|
||||
defer { setupNavigationBarBackgroundView() }
|
||||
|
||||
navigationItem.rightBarButtonItem = cancelBarButtonItem
|
||||
|
||||
viewModel.$isReporting
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] isReporting in
|
||||
guard let self = self else { return }
|
||||
self.navigationActionView.isUserInteractionEnabled = !isReporting
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
tableView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(tableView)
|
||||
NSLayoutConstraint.activate([
|
||||
tableView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
|
||||
tableView.delegate = self
|
||||
viewModel.setupDiffableDataSource(
|
||||
tableView: tableView
|
||||
)
|
||||
|
||||
navigationActionView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationActionView)
|
||||
defer {
|
||||
view.bringSubviewToFront(navigationActionView)
|
||||
}
|
||||
NSLayoutConstraint.activate([
|
||||
navigationActionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationActionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
view.bottomAnchor.constraint(equalTo: navigationActionView.bottomAnchor),
|
||||
])
|
||||
|
||||
navigationActionView
|
||||
.observe(\.bounds, options: [.initial, .new]) { [weak self] navigationActionView, _ in
|
||||
guard let self = self else { return }
|
||||
let inset = navigationActionView.frame.height
|
||||
self.tableView.contentInset.bottom = inset
|
||||
self.tableView.verticalScrollIndicatorInsets.bottom = inset
|
||||
}
|
||||
.store(in: &observations)
|
||||
|
||||
viewModel.$isNextButtonEnabled
|
||||
.receive(on: DispatchQueue.main)
|
||||
.assign(to: \.isEnabled, on: navigationActionView.nextButton)
|
||||
.store(in: &disposeBag)
|
||||
|
||||
navigationActionView.backButton.addTarget(self, action: #selector(ReportSupplementaryViewController.skipButtonDidPressed(_:)), for: .touchUpInside)
|
||||
navigationActionView.nextButton.addTarget(self, action: #selector(ReportSupplementaryViewController.nextButtonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportSupplementaryViewController {
|
||||
private func report(withComment: Bool) {
|
||||
Task {
|
||||
do {
|
||||
let _ = try await viewModel.report(withComment: withComment)
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): report success")
|
||||
|
||||
let reportResultViewModel = ReportResultViewModel(
|
||||
context: context,
|
||||
user: viewModel.user
|
||||
)
|
||||
|
||||
coordinator.present(
|
||||
scene: .reportResult(viewModel: reportResultViewModel),
|
||||
from: self,
|
||||
transition: .show
|
||||
)
|
||||
|
||||
} catch {
|
||||
let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
|
||||
let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
|
||||
alertController.addAction(okAction)
|
||||
self.coordinator.present(
|
||||
scene: .alertController(alertController: alertController),
|
||||
from: nil,
|
||||
transition: .alertController(animated: true, completion: nil)
|
||||
)
|
||||
}
|
||||
} // end Task
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportSupplementaryViewController {
|
||||
|
||||
@objc private func cancelBarButtonItemDidPressed(_ sender: UIBarButtonItem) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func skipButtonDidPressed(_ sender: UIButton) {
|
||||
report(withComment: false)
|
||||
}
|
||||
|
||||
@objc func nextButtonDidPressed(_ sender: UIButton) {
|
||||
report(withComment: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
extension ReportSupplementaryViewController: UITableViewDelegate { }
|
|
@ -0,0 +1,38 @@
|
|||
//
|
||||
// ReportSupplementaryViewModel+Diffable.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
extension ReportSupplementaryViewModel {
|
||||
|
||||
static let reportItemHeaderContext = ReportItem.HeaderContext(
|
||||
primaryLabelText: L10n.Scene.Report.content2,
|
||||
secondaryLabelText: L10n.Scene.Report.step2
|
||||
)
|
||||
|
||||
func setupDiffableDataSource(
|
||||
tableView: UITableView
|
||||
) {
|
||||
diffableDataSource = ReportSection.diffableDataSource(
|
||||
tableView: tableView,
|
||||
context: context,
|
||||
configuration: ReportSection.Configuration()
|
||||
)
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<ReportSection, ReportItem>()
|
||||
snapshot.appendSections([.main])
|
||||
snapshot.appendItems([.header(context: ReportSupplementaryViewModel.reportItemHeaderContext)], toSection: .main)
|
||||
snapshot.appendItems([.comment(context: commentContext)], toSection: .main)
|
||||
|
||||
diffableDataSource?.apply(snapshot, animatingDifferences: false)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// ReportSupplementaryViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import CoreDataStack
|
||||
import MastodonSDK
|
||||
|
||||
class ReportSupplementaryViewModel {
|
||||
|
||||
// Input
|
||||
var context: AppContext
|
||||
let user: ManagedObjectRecord<MastodonUser>
|
||||
let selectStatuses: [ManagedObjectRecord<Status>]
|
||||
let commentContext = ReportItem.CommentContext()
|
||||
|
||||
// output
|
||||
var diffableDataSource: UITableViewDiffableDataSource<ReportSection, ReportItem>?
|
||||
@Published var isNextButtonEnabled = false
|
||||
@Published var isReporting = false
|
||||
@Published var isReportSuccess = false
|
||||
|
||||
init(
|
||||
context: AppContext,
|
||||
user: ManagedObjectRecord<MastodonUser>,
|
||||
selectStatuses: [ManagedObjectRecord<Status>]
|
||||
) {
|
||||
self.context = context
|
||||
self.user = user
|
||||
self.selectStatuses = selectStatuses
|
||||
// end init
|
||||
|
||||
commentContext.$comment
|
||||
.map { comment -> Bool in
|
||||
return !comment.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
}
|
||||
.assign(to: &$isNextButtonEnabled)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportSupplementaryViewModel {
|
||||
func report(withComment: Bool) async throws {
|
||||
guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
let managedObjectContext = context.managedObjectContext
|
||||
let _query: Mastodon.API.Reports.FileReportQuery? = try await managedObjectContext.perform {
|
||||
guard let user = self.user.object(in: managedObjectContext) else { return nil }
|
||||
let statusIDs = self.selectStatuses.compactMap { record -> Status.ID? in
|
||||
guard let status = record.object(in: managedObjectContext) else { return nil }
|
||||
return status.id
|
||||
}
|
||||
return Mastodon.API.Reports.FileReportQuery(
|
||||
accountID: user.id,
|
||||
statusIDs: statusIDs,
|
||||
comment: withComment ? self.commentContext.comment : nil,
|
||||
forward: nil
|
||||
)
|
||||
}
|
||||
|
||||
guard let query = _query else { return }
|
||||
|
||||
do {
|
||||
isReporting = true
|
||||
let _ = try await context.apiService.report(
|
||||
query: query,
|
||||
authenticationBox: authenticationBox
|
||||
)
|
||||
isReportSuccess = true
|
||||
} catch {
|
||||
isReporting = false
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,370 +0,0 @@
|
|||
//
|
||||
// ReportViewController.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/20.
|
||||
//
|
||||
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import os.log
|
||||
import UIKit
|
||||
import MastodonSDK
|
||||
import MastodonMeta
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
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 = ThemeService.shared.currentTheme.value.systemElevatedBackgroundColor
|
||||
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!
|
||||
|
||||
let titleView = DoubleTitleLabelNavigationBarTitleView()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
setupView()
|
||||
|
||||
viewModel.setupDiffableDataSource(
|
||||
for: tableView,
|
||||
dependency: self
|
||||
)
|
||||
|
||||
bindViewModel()
|
||||
bindActions()
|
||||
}
|
||||
|
||||
// MAKR: - Private methods
|
||||
private func setupView() {
|
||||
view.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemBackgroundColor
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] theme in
|
||||
guard let self = self else { return }
|
||||
self.view.backgroundColor = theme.secondarySystemBackgroundColor
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
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
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.bottomConstraint.constant = padding
|
||||
UIView.animate(withDuration: 0.33) {
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func setupNavigation() {
|
||||
navigationItem.rightBarButtonItem
|
||||
= UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.cancel,
|
||||
target: self,
|
||||
action: #selector(doneButtonDidClick))
|
||||
navigationItem.rightBarButtonItem?.tintColor = ThemeService.tintColor
|
||||
|
||||
// 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.titleView = titleView
|
||||
if let user = beReportedUser {
|
||||
do {
|
||||
let mastodonContent = MastodonContent(content: user.displayNameWithFallback, emojis: user.emojis.asDictionary)
|
||||
let metaContent = try MastodonMetaContent.convert(document: mastodonContent)
|
||||
titleView.update(titleMetaContent: metaContent, subtitle: nil)
|
||||
} catch {
|
||||
let metaContent = PlaintextMetaContent(string: user.displayNameWithFallback)
|
||||
titleView.update(titleMetaContent: metaContent, subtitle: nil)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
//
|
||||
// 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: MastodonAuthenticationBox
|
||||
) {
|
||||
fatalError()
|
||||
// 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() {
|
||||
fatalError()
|
||||
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]) {
|
||||
fatalError()
|
||||
// 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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
//
|
||||
// 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
|
||||
) {
|
||||
fatalError()
|
||||
// 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)
|
||||
}
|
||||
}
|
|
@ -1,215 +0,0 @@
|
|||
//
|
||||
// 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, StatusItem>?
|
||||
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: 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)
|
||||
}
|
||||
}
|
|
@ -1,221 +0,0 @@
|
|||
//
|
||||
// ReportedStatusTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by ihugo on 2021/4/20.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import AVKit
|
||||
import Combine
|
||||
import CoreData
|
||||
import CoreDataStack
|
||||
import Meta
|
||||
import MetaTextKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class ReportedStatusTableViewCell: UITableViewCell {
|
||||
|
||||
static let bottomPaddingHeight: CGFloat = 10
|
||||
|
||||
weak var dependency: ReportViewController?
|
||||
private var _disposeBag = Set<AnyCancellable>()
|
||||
var disposeBag = Set<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!
|
||||
|
||||
// not support filter
|
||||
var isFiltered: Bool = false
|
||||
|
||||
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 setHighlighted(_ highlighted: Bool, animated: Bool) {
|
||||
super.setHighlighted(highlighted, animated: animated)
|
||||
if highlighted {
|
||||
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
checkbox.tintColor = Asset.Colors.brandBlue.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 = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
ThemeService.shared.currentTheme
|
||||
.receive(on: RunLoop.main)
|
||||
.sink { [weak self] theme in
|
||||
guard let self = self else { return }
|
||||
self.backgroundColor = ThemeService.shared.currentTheme.value.secondarySystemGroupedBackgroundColor
|
||||
}
|
||||
.store(in: &_disposeBag)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
fatalError()
|
||||
// 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, avatarImageViewDidPressed imageView: UIImageView) {
|
||||
// }
|
||||
//
|
||||
// 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, metaText: MetaText, didSelectMeta meta: Meta) {
|
||||
// }
|
||||
//
|
||||
//}
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// ReportCommentTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
import UITextView_Placeholder
|
||||
|
||||
final class ReportCommentTableViewCell: UITableViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let commentTextViewShadowBackgroundContainer: ShadowBackgroundContainer = {
|
||||
let shadowBackgroundContainer = ShadowBackgroundContainer()
|
||||
return shadowBackgroundContainer
|
||||
}()
|
||||
|
||||
let commentTextView: UITextView = {
|
||||
let textView = UITextView()
|
||||
let font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
textView.font = font
|
||||
textView.attributedPlaceholder = NSAttributedString(
|
||||
string: L10n.Scene.Report.textPlaceholder,
|
||||
attributes: [
|
||||
.font: font
|
||||
]
|
||||
)
|
||||
textView.textContainerInset = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
||||
textView.isScrollEnabled = false
|
||||
textView.layer.masksToBounds = true
|
||||
textView.layer.cornerRadius = 10
|
||||
return textView
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportCommentTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
commentTextViewShadowBackgroundContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(commentTextViewShadowBackgroundContainer)
|
||||
NSLayoutConstraint.activate([
|
||||
commentTextViewShadowBackgroundContainer.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24),
|
||||
commentTextViewShadowBackgroundContainer.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
commentTextViewShadowBackgroundContainer.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: commentTextViewShadowBackgroundContainer.bottomAnchor, constant: 24),
|
||||
])
|
||||
|
||||
commentTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
commentTextViewShadowBackgroundContainer.addSubview(commentTextView)
|
||||
NSLayoutConstraint.activate([
|
||||
commentTextView.topAnchor.constraint(equalTo: commentTextViewShadowBackgroundContainer.topAnchor),
|
||||
commentTextView.leadingAnchor.constraint(equalTo: commentTextViewShadowBackgroundContainer.leadingAnchor),
|
||||
commentTextView.trailingAnchor.constraint(equalTo: commentTextViewShadowBackgroundContainer.trailingAnchor),
|
||||
commentTextView.bottomAnchor.constraint(equalTo: commentTextViewShadowBackgroundContainer.bottomAnchor),
|
||||
commentTextView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100).priority(.defaultHigh),
|
||||
])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// ReportHeadlineTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
final class ReportHeadlineTableViewCell: UITableViewCell {
|
||||
|
||||
let primaryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 28, weight: .bold))
|
||||
label.textColor = Asset.Colors.Label.primary.color
|
||||
label.text = L10n.Scene.Report.content1
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let secondaryLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = UIFontMetrics(forTextStyle: .largeTitle).scaledFont(for: .systemFont(ofSize: 17, weight: .regular))
|
||||
label.textColor = Asset.Colors.Label.secondary.color
|
||||
label.text = L10n.Scene.Report.step1
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportHeadlineTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
let container = UIStackView()
|
||||
container.axis = .vertical
|
||||
container.spacing = 16
|
||||
container.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(container)
|
||||
NSLayoutConstraint.activate([
|
||||
container.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
container.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
container.trailingAnchor.constraint(equalTo: contentView.readableContentGuide.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: 11),
|
||||
])
|
||||
|
||||
container.addArrangedSubview(secondaryLabel) // put secondary label before primary
|
||||
container.addArrangedSubview(primaryLabel)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
//
|
||||
// ReportResultActionTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-8.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonAsset
|
||||
import MastodonUI
|
||||
import MastodonLocalization
|
||||
|
||||
final class ReportResultActionTableViewCell: UITableViewCell {
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let containerView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
return stackView
|
||||
}()
|
||||
|
||||
let avatarImageView: AvatarImageView = {
|
||||
let imageView = AvatarImageView()
|
||||
imageView.configure(cornerConfiguration: .init(corner: .fixed(radius: 27)))
|
||||
return imageView
|
||||
}()
|
||||
|
||||
let reportBannerShadowContainer = ShadowBackgroundContainer()
|
||||
let reportBannerLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
let padding = Array(repeating: " ", count: 2).joined()
|
||||
label.text = padding + "Reported" + padding // TODO: i18n
|
||||
label.textColor = Asset.Scene.Report.reportBanner.color
|
||||
label.font = FontFamily.Staatliches.regular.font(size: 49)
|
||||
label.backgroundColor = Asset.Scene.Report.background.color
|
||||
label.layer.borderColor = Asset.Scene.Report.reportBanner.color.cgColor
|
||||
label.layer.borderWidth = 6
|
||||
label.layer.masksToBounds = true
|
||||
label.layer.cornerRadius = 12
|
||||
return label
|
||||
}()
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportResultActionTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(containerView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: contentView.layoutMarginsGuide.trailingAnchor),
|
||||
containerView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
])
|
||||
|
||||
let avatarContainer = UIStackView()
|
||||
avatarContainer.axis = .horizontal
|
||||
containerView.addArrangedSubview(avatarContainer)
|
||||
|
||||
let avatarLeadingPaddingView = UIView()
|
||||
let avatarTrailingPaddingView = UIView()
|
||||
avatarLeadingPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addArrangedSubview(avatarLeadingPaddingView)
|
||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addArrangedSubview(avatarImageView)
|
||||
avatarTrailingPaddingView.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addArrangedSubview(avatarTrailingPaddingView)
|
||||
NSLayoutConstraint.activate([
|
||||
avatarImageView.widthAnchor.constraint(equalToConstant: 106).priority(.required - 1),
|
||||
avatarImageView.heightAnchor.constraint(equalToConstant: 106).priority(.required - 1),
|
||||
avatarLeadingPaddingView.widthAnchor.constraint(equalTo: avatarTrailingPaddingView.widthAnchor).priority(.defaultHigh),
|
||||
])
|
||||
|
||||
reportBannerShadowContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
avatarContainer.addSubview(reportBannerShadowContainer)
|
||||
NSLayoutConstraint.activate([
|
||||
reportBannerShadowContainer.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
|
||||
reportBannerShadowContainer.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||
])
|
||||
reportBannerShadowContainer.transform = CGAffineTransform(rotationAngle: -(.pi / 180 * 5))
|
||||
|
||||
reportBannerLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
reportBannerShadowContainer.addSubview(reportBannerLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
reportBannerLabel.topAnchor.constraint(equalTo: reportBannerShadowContainer.topAnchor),
|
||||
reportBannerLabel.leadingAnchor.constraint(equalTo: reportBannerShadowContainer.leadingAnchor),
|
||||
reportBannerLabel.trailingAnchor.constraint(equalTo: reportBannerShadowContainer.trailingAnchor),
|
||||
reportBannerLabel.bottomAnchor.constraint(equalTo: reportBannerShadowContainer.bottomAnchor),
|
||||
])
|
||||
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
reportBannerShadowContainer.layer.setupShadow(
|
||||
color: .black,
|
||||
alpha: 0.25,
|
||||
x: 1,
|
||||
y: 0.64,
|
||||
blur: 0.64,
|
||||
spread: 0,
|
||||
roundedRect: reportBannerShadowContainer.bounds,
|
||||
byRoundingCorners: .allCorners,
|
||||
cornerRadii: CGSize(width: 12, height: 12)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import SwiftUI
|
||||
struct ReportResultActionTableViewCell_Preview: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UIViewPreview(width: 375) {
|
||||
let cell = ReportResultActionTableViewCell()
|
||||
cell.avatarImageView.configure(configuration: .init(image: .placeholder(color: .blue)))
|
||||
return cell
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 106))
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// ReportStatusTableViewCell+ViewModel.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreDataStack
|
||||
|
||||
extension ReportStatusTableViewCell {
|
||||
final class ViewModel {
|
||||
let value: Status
|
||||
|
||||
init(value: Status) {
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReportStatusTableViewCell {
|
||||
|
||||
func configure(
|
||||
tableView: UITableView,
|
||||
viewModel: ViewModel
|
||||
) {
|
||||
if statusView.frame == .zero {
|
||||
// set status view width
|
||||
statusView.frame.size.width = tableView.frame.width - ReportStatusTableViewCell.checkboxLeadingMargin - ReportStatusTableViewCell.checkboxSize.width - ReportStatusTableViewCell.statusViewLeadingSpacing
|
||||
logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): did layout for new cell")
|
||||
}
|
||||
|
||||
statusView.configure(status: viewModel.value)
|
||||
|
||||
statusView.viewModel.$isContentReveal
|
||||
.removeDuplicates()
|
||||
.dropFirst()
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak tableView, weak self] isContentReveal in
|
||||
guard let tableView = tableView else { return }
|
||||
guard let _ = self else { return }
|
||||
|
||||
tableView.beginUpdates()
|
||||
tableView.endUpdates()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
//
|
||||
// ReportStatusTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import os.log
|
||||
import UIKit
|
||||
import Combine
|
||||
import MastodonUI
|
||||
import MastodonAsset
|
||||
|
||||
final class ReportStatusTableViewCell: UITableViewCell {
|
||||
|
||||
static let checkboxLeadingMargin: CGFloat = 16
|
||||
static let checkboxSize = CGSize(width: 32, height: 32)
|
||||
static let statusViewLeadingSpacing: CGFloat = 22
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
let logger = Logger(subsystem: "ReportStatusTableViewCell", category: "View")
|
||||
|
||||
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
|
||||
}()
|
||||
|
||||
let statusView = StatusView()
|
||||
|
||||
let separatorLine = UIView.separatorLine
|
||||
|
||||
override func prepareForReuse() {
|
||||
super.prepareForReuse()
|
||||
|
||||
disposeBag.removeAll()
|
||||
statusView.prepareForReuse()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ReportStatusTableViewCell {
|
||||
|
||||
private func _init() {
|
||||
selectionStyle = .none
|
||||
backgroundColor = .clear
|
||||
|
||||
checkbox.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(checkbox)
|
||||
NSLayoutConstraint.activate([
|
||||
checkbox.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: ReportStatusTableViewCell.checkboxLeadingMargin),
|
||||
checkbox.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
|
||||
checkbox.heightAnchor.constraint(equalToConstant: ReportStatusTableViewCell.checkboxSize.width).priority(.required - 1),
|
||||
checkbox.widthAnchor.constraint(equalToConstant: ReportStatusTableViewCell.checkboxSize.height).priority(.required - 1),
|
||||
])
|
||||
|
||||
statusView.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(statusView)
|
||||
NSLayoutConstraint.activate([
|
||||
statusView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 24),
|
||||
statusView.leadingAnchor.constraint(equalTo: checkbox.trailingAnchor, constant: ReportStatusTableViewCell.statusViewLeadingSpacing),
|
||||
statusView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
contentView.bottomAnchor.constraint(equalTo: statusView.bottomAnchor, constant: 24),
|
||||
])
|
||||
statusView.setup(style: .report)
|
||||
|
||||
separatorLine.translatesAutoresizingMaskIntoConstraints = false
|
||||
contentView.addSubview(separatorLine)
|
||||
NSLayoutConstraint.activate([
|
||||
separatorLine.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||
separatorLine.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
||||
separatorLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||
separatorLine.heightAnchor.constraint(equalToConstant: UIView.separatorLineHeight(of: contentView)).priority(.required - 1),
|
||||
])
|
||||
}
|
||||
|
||||
override func setSelected(_ selected: Bool, animated: Bool) {
|
||||
super.setSelected(selected, animated: animated)
|
||||
if selected {
|
||||
checkbox.image = UIImage(systemName: "checkmark.circle.fill")
|
||||
checkbox.tintColor = Asset.Colors.Label.primary.color
|
||||
} else {
|
||||
checkbox.image = UIImage(systemName: "circle")
|
||||
checkbox.tintColor = Asset.Colors.Label.secondary.color
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//
|
||||
// ReportViewControllerAppearance.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK on 2022-2-7.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import MastodonAsset
|
||||
import MastodonLocalization
|
||||
|
||||
protocol ReportViewControllerAppearance: UIViewController {
|
||||
func setupAppearance()
|
||||
func setupNavigationBarAppearance()
|
||||
}
|
||||
|
||||
extension ReportViewControllerAppearance {
|
||||
|
||||
|
||||
func setupAppearance() {
|
||||
|
||||
title = "Report" // TODO: i18n
|
||||
view.backgroundColor = Asset.Scene.Report.background.color
|
||||
|
||||
setupNavigationBarAppearance()
|
||||
|
||||
let backItem = UIBarButtonItem(
|
||||
title: L10n.Common.Controls.Actions.back,
|
||||
style: .plain,
|
||||
target: nil,
|
||||
action: nil
|
||||
)
|
||||
navigationItem.backBarButtonItem = backItem
|
||||
}
|
||||
|
||||
func setupNavigationBarAppearance() {
|
||||
// use TransparentBackground so view push / dismiss will be more visual nature
|
||||
// please add opaque background for status bar manually if needs
|
||||
|
||||
let barAppearance = UINavigationBarAppearance()
|
||||
barAppearance.configureWithTransparentBackground()
|
||||
navigationItem.standardAppearance = barAppearance
|
||||
navigationItem.compactAppearance = barAppearance
|
||||
navigationItem.scrollEdgeAppearance = barAppearance
|
||||
if #available(iOS 15.0, *) {
|
||||
navigationItem.compactScrollEdgeAppearance = barAppearance
|
||||
} else {
|
||||
// Fallback on earlier versions
|
||||
}
|
||||
}
|
||||
|
||||
func setupNavigationBarBackgroundView() {
|
||||
let navigationBarBackgroundView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Scene.Report.background.color
|
||||
return view
|
||||
}()
|
||||
|
||||
navigationBarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(navigationBarBackgroundView)
|
||||
NSLayoutConstraint.activate([
|
||||
navigationBarBackgroundView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
navigationBarBackgroundView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
navigationBarBackgroundView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
navigationBarBackgroundView.bottomAnchor.constraint(equalTo: view.layoutMarginsGuide.topAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
}
|
|
@ -12,12 +12,17 @@ import Combine
|
|||
extension APIService {
|
||||
|
||||
func report(
|
||||
domain: String,
|
||||
query: Mastodon.API.Reports.FileReportQuery,
|
||||
mastodonAuthenticationBox: MastodonAuthenticationBox
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Bool>, Error> {
|
||||
let authorization = mastodonAuthenticationBox.userAuthorization
|
||||
|
||||
return Mastodon.API.Reports.fileReport(session: session, domain: domain, query: query, authorization: authorization)
|
||||
authenticationBox: MastodonAuthenticationBox
|
||||
) async throws -> Mastodon.Response.Content<Bool> {
|
||||
let response = try await Mastodon.API.Reports.fileReport(
|
||||
session: session,
|
||||
domain: authenticationBox.domain,
|
||||
query: query,
|
||||
authorization: authenticationBox.userAuthorization
|
||||
).singleOutput()
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.969",
|
||||
"green" : "0.949",
|
||||
"red" : "0.949"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.129",
|
||||
"green" : "0.106",
|
||||
"red" : "0.098"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x55",
|
||||
"green" : "0x98",
|
||||
"red" : "0x03"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -108,6 +108,10 @@ public enum Asset {
|
|||
public static let usernameGray = ColorAsset(name: "Scene/Profile/Banner/username.gray")
|
||||
}
|
||||
}
|
||||
public enum Report {
|
||||
public static let background = ColorAsset(name: "Scene/Report/background")
|
||||
public static let reportBanner = ColorAsset(name: "Scene/Report/report.banner")
|
||||
}
|
||||
public enum Sidebar {
|
||||
public static let logo = ImageAsset(name: "Scene/Sidebar/logo")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,78 @@
|
|||
// swiftlint:disable all
|
||||
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||
|
||||
// No fonts found
|
||||
#if os(OSX)
|
||||
import AppKit.NSFont
|
||||
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||
import UIKit.UIFont
|
||||
#endif
|
||||
|
||||
// Deprecated typealiases
|
||||
@available(*, deprecated, renamed: "FontConvertible.Font", message: "This typealias will be removed in SwiftGen 7.0")
|
||||
public typealias Font = FontConvertible.Font
|
||||
|
||||
// swiftlint:disable superfluous_disable_command
|
||||
// swiftlint:disable file_length
|
||||
|
||||
// MARK: - Fonts
|
||||
|
||||
// swiftlint:disable identifier_name line_length type_body_length
|
||||
public enum FontFamily {
|
||||
public enum Staatliches {
|
||||
public static let regular = FontConvertible(name: "Staatliches-Regular", family: "Staatliches", path: "Staatliches-Regular.ttf")
|
||||
public static let all: [FontConvertible] = [regular]
|
||||
}
|
||||
public static let allCustomFonts: [FontConvertible] = [Staatliches.all].flatMap { $0 }
|
||||
public static func registerAllCustomFonts() {
|
||||
allCustomFonts.forEach { $0.register() }
|
||||
}
|
||||
}
|
||||
// swiftlint:enable identifier_name line_length type_body_length
|
||||
|
||||
// MARK: - Implementation Details
|
||||
|
||||
public struct FontConvertible {
|
||||
public let name: String
|
||||
public let family: String
|
||||
public let path: String
|
||||
|
||||
#if os(OSX)
|
||||
public typealias Font = NSFont
|
||||
#elseif os(iOS) || os(tvOS) || os(watchOS)
|
||||
public typealias Font = UIFont
|
||||
#endif
|
||||
|
||||
public func font(size: CGFloat) -> Font {
|
||||
guard let font = Font(font: self, size: size) else {
|
||||
fatalError("Unable to initialize font '\(name)' (\(family))")
|
||||
}
|
||||
return font
|
||||
}
|
||||
|
||||
public func register() {
|
||||
// swiftlint:disable:next conditional_returns_on_newline
|
||||
guard let url = url else { return }
|
||||
CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
|
||||
}
|
||||
|
||||
fileprivate var url: URL? {
|
||||
// swiftlint:disable:next implicit_return
|
||||
return Bundle.module.url(forResource: path, withExtension: nil)
|
||||
}
|
||||
}
|
||||
|
||||
public extension FontConvertible.Font {
|
||||
convenience init?(font: FontConvertible, size: CGFloat) {
|
||||
#if os(iOS) || os(tvOS) || os(watchOS)
|
||||
if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) {
|
||||
font.register()
|
||||
}
|
||||
#elseif os(OSX)
|
||||
if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none {
|
||||
font.register()
|
||||
}
|
||||
#endif
|
||||
|
||||
self.init(name: font.name, size: size)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -245,13 +245,6 @@ extension StatusView.ViewModel {
|
|||
statusView.dateLabel.configure(content: PlaintextMetaContent(string: text))
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
$isSensitive
|
||||
.sink { isSensitive in
|
||||
if !isSensitive {
|
||||
statusView.setMenuButtonDisplay()
|
||||
}
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
private func bindContent(statusView: StatusView) {
|
||||
|
|
|
@ -222,7 +222,6 @@ public final class StatusView: UIView {
|
|||
}
|
||||
|
||||
headerContainerView.isHidden = true
|
||||
menuButton.isHidden = true
|
||||
contentWarningToggleButton.isHidden = true
|
||||
setSpoilerOverlayViewHidden(true)
|
||||
mediaContainerView.isHidden = true
|
||||
|
@ -333,6 +332,7 @@ extension StatusView {
|
|||
public enum Style {
|
||||
case inline
|
||||
case plain
|
||||
case report
|
||||
case notification
|
||||
case notificationQuote
|
||||
case composeStatusReplica
|
||||
|
@ -346,6 +346,7 @@ extension StatusView.Style {
|
|||
switch self {
|
||||
case .inline: inline(statusView: statusView)
|
||||
case .plain: plain(statusView: statusView)
|
||||
case .report: report(statusView: statusView)
|
||||
case .notification: notification(statusView: statusView)
|
||||
case .notificationQuote: notificationQuote(statusView: statusView)
|
||||
case .composeStatusReplica: composeStatusReplica(statusView: statusView)
|
||||
|
@ -420,6 +421,7 @@ extension StatusView.Style {
|
|||
authorPrimaryMetaContainer.addArrangedSubview(UIView())
|
||||
// menuButton
|
||||
authorPrimaryMetaContainer.addArrangedSubview(statusView.menuButton)
|
||||
statusView.menuButton.setContentCompressionResistancePriority(.required - 1, for: .horizontal)
|
||||
|
||||
// authorSecondaryMetaContainer: H - [ authorUsername | usernameTrialingDotLabel | dateLabel | (padding) ]
|
||||
let authorSecondaryMetaContainer = UIStackView()
|
||||
|
@ -527,6 +529,14 @@ extension StatusView.Style {
|
|||
.store(in: &statusView._disposeBag)
|
||||
}
|
||||
|
||||
func report(statusView: StatusView) {
|
||||
inline(statusView: statusView) // override the inline style
|
||||
|
||||
statusView.menuButton.removeFromSuperview()
|
||||
statusView.statusVisibilityView.removeFromSuperview()
|
||||
statusView.actionToolbarContainer.removeFromSuperview()
|
||||
}
|
||||
|
||||
func notification(statusView: StatusView) {
|
||||
inline(statusView: statusView) // override the inline style
|
||||
|
||||
|
@ -573,10 +583,6 @@ extension StatusView {
|
|||
headerContainerView.isHidden = false
|
||||
}
|
||||
|
||||
func setMenuButtonDisplay() {
|
||||
menuButton.isHidden = false
|
||||
}
|
||||
|
||||
func setContentWarningToggleButtonDisplay() {
|
||||
contentWarningToggleButton.isHidden = false
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue