feat: update report flow

This commit is contained in:
CMK 2022-02-08 12:36:06 +08:00
parent c964255a2a
commit f4bb2d947f
47 changed files with 1994 additions and 1381 deletions

View File

@ -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 youd like to add to the report?",
"content2": "Is there anything the moderators should know about this report?",
"report_sent_title": "Thanks for reporting, well 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": {

View File

@ -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 */,

View File

@ -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>

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
)
}
}

View File

@ -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 {

View File

@ -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
}

View File

@ -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()

View File

@ -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 { }

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -67,7 +67,6 @@ extension UserTimelineViewController {
])
tableView.delegate = self
// tableView.prefetchDataSource = self
viewModel.setupDiffableDataSource(
tableView: tableView,
statusTableViewCellDelegate: self

View File

@ -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

View File

@ -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

View File

@ -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 { }

View File

@ -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, well 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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}
}

View File

@ -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)
}
}

View File

@ -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 { }

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
// )
// }
// }
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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) {
// }
//
//}

View File

@ -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),
])
}
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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),
])
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,9 @@
{
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"provides-namespace" : true
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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")
}

View File

@ -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)
}
}

View File

@ -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) {

View File

@ -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
}