diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 9a38be2b6..d2eda9b89 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -28,7 +28,6 @@ 2D206B8625F5FB0900143C56 /* Double.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B8525F5FB0900143C56 /* Double.swift */; }; 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D206B9125F60EA700143C56 /* UIControl.swift */; }; 2D24E1232626ED9D00A59D4F /* UIView+Gesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */; }; - 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D35237926256D920031AF25 /* NotificationSection.swift */; }; 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; }; 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; @@ -44,12 +43,10 @@ 2D571B2F26004EC000540450 /* NavigationBarProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */; }; 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */; }; 2D5981A125E4A593000FB903 /* MastodonConfirmEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */; }; - 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */; }; 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */; }; 2D5A3D3825CF8D9F002347D6 /* ScrollViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */; }; 2D5A3D6225CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */; }; 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D607AD726242FC500B70763 /* NotificationViewModel.swift */; }; - 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */; }; 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */; }; 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; }; 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76319E25C1521200929FB9 /* StatusSection.swift */; }; @@ -62,13 +59,9 @@ 2D84350525FF858100EECE90 /* UIScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D84350425FF858100EECE90 /* UIScrollView.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; - 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; }; - 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */; }; - 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */; }; 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */; }; 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */; }; 2DAC9E46262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */; }; - 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */; }; 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */; }; 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */; }; 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF123A625C3B0210020F248 /* ActiveLabel.swift */; }; @@ -84,9 +77,6 @@ 5D0393902612D259007FE196 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D03938F2612D259007FE196 /* WebViewController.swift */; }; 5D0393962612D266007FE196 /* WebViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D0393952612D266007FE196 /* WebViewModel.swift */; }; 5DA732CC2629CEF500A92342 /* UIView+Remove.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */; }; - 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */; }; - 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */; }; - 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */; }; 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; 5E0DEC05797A7E6933788DDB /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */; }; 5E44BF88AD33646E64727BCF /* Pods_MastodonTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CD92E0F10BDE4FE7C4B999F2 /* Pods_MastodonTests.framework */; }; @@ -110,8 +100,6 @@ DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */; }; DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */; }; DB03A795272A981400EE37C5 /* ContentSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */; }; - DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */; }; - DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB03F7F42689B782007B274C /* ComposeTableView.swift */; }; DB0617EB277EF3820030EE79 /* GradientBorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EA277EF3820030EE79 /* GradientBorderView.swift */; }; DB0617ED277F02C50030EE79 /* OnboardingNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */; }; DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0617EE277F12720030EE79 /* NavigationActionView.swift */; }; @@ -130,8 +118,6 @@ DB0F9D54283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */; }; DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */; }; DB0FCB68279507EF006C02E2 /* DataSourceFacade+Meta.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */; }; - DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */; }; - DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7127952986006C02E2 /* NamingState.swift */; }; DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */; }; DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */; }; DB0FCB7827957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0FCB7727957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift */; }; @@ -174,14 +160,6 @@ DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; }; DB336F3F278E668C0031E64B /* StatusTableViewCell+ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */; }; - DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F40278E68480031E64B /* StatusView+Configuration.swift */; }; - DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB336F42278EB1680031E64B /* MediaView+Configuration.swift */; }; - DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */; }; - DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */; }; - DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */; }; - DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */; }; - DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */; }; - DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */; }; DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */; }; DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */; }; DB3E6FE22806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB3E6FE12806A50100B035AE /* DiscoveryHashtagsViewModel+Diffable.swift */; }; @@ -208,8 +186,6 @@ DB427DF825BAA00100D1B89D /* MastodonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DF725BAA00100D1B89D /* MastodonUITests.swift */; }; DB443CD42694627B00159B29 /* AppearanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB443CD32694627B00159B29 /* AppearanceView.swift */; }; DB44767B260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */; }; - DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */; }; - DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */; }; DB447691260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */; }; DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */; }; DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB4481B825EE289600BEFB67 /* UITableView.swift */; }; @@ -282,8 +258,6 @@ DB64BA482851F29300ADF1B7 /* Account+Fetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */; }; DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB65C63627A2AF6C008BAC2E /* ReportItem.swift */; }; DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */; }; - DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729525F9F91600D60309 /* ComposeStatusSection.swift */; }; - DB66729C25F9F91F00D60309 /* ComposeStatusItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */; }; DB6746EB278ED8B0008A6B94 /* PollOptionView+Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */; }; DB6746ED278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */; }; DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */; }; @@ -311,14 +285,10 @@ DB6B74FE272FF59000C70B6E /* UserItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FD272FF59000C70B6E /* UserItem.swift */; }; DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */; }; DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */; }; - DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */; }; - DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */; }; DB6D9F3526351B7A008423CD /* NotificationService+Decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */; }; DB6D9F7D26358ED4008423CD /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F7C26358ED4008423CD /* SettingsSection.swift */; }; DB6D9F8426358EEC008423CD /* SettingsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F8326358EEC008423CD /* SettingsItem.swift */; }; DB6D9F9726367249008423CD /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6D9F9626367249008423CD /* SettingsViewController.swift */; }; - DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */; }; - DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */; }; DB72601C25E36A2100235243 /* MastodonServerRulesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */; }; DB72602725E36A6F00235243 /* MastodonServerRulesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */; }; DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */; }; @@ -353,7 +323,6 @@ DB938F0926240F3C00E5B6C1 /* RemoteThreadViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */; }; DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */; }; DB938F1F2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */; }; - DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */; }; DB98EB4727B0DFAA0082E365 /* ReportStatusViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+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 */; }; @@ -387,7 +356,6 @@ DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */; }; DBA94436265CBB7400C537E1 /* ProfileFieldItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */; }; DBA9443E265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */; }; - DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */; }; DBABE3EC25ECAC4B00879EE5 /* WelcomeIllustrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */; }; DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */; }; DBB3BA2A26A81C020004F2D4 /* FLAnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */; }; @@ -408,20 +376,12 @@ DBB8AB4626AECDE200F6D281 /* SendPostIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */; }; DBB8AB4F26AED13F00F6D281 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBB9759B262462E1004620BD /* ThreadMetaView.swift */; }; - DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */; }; - DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */; }; DBBC24DC26A54BCB00398BB9 /* MastodonRegex.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */; }; DBBE1B4525F3474B0081417A /* MastodonPickServerAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */; }; - DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */; }; - DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */; }; - DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */; }; - DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */; }; - DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */; }; - DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */; }; - DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ShareViewController.swift */; }; + DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6461426A170AB00B0E31B /* ComposeViewController.swift */; }; DBC6461826A170AB00B0E31B /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DBC6461626A170AB00B0E31B /* MainInterface.storyboard */; }; DBC6461C26A170AB00B0E31B /* ShareActionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ShareViewModel.swift */; }; + DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC6462226A1712000B0E31B /* ComposeViewModel.swift */; }; DBC6462826A1736300B0E31B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB427DDE25BAA00100D1B89D /* Assets.xcassets */; }; DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */; }; DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBCA0EBB282BB38A0029E2B0 /* PageboyNavigateable.swift */; }; @@ -474,14 +434,6 @@ DBFEEC99279BDCDE004F81DD /* ProfileAboutViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC98279BDCDE004F81DD /* ProfileAboutViewModel.swift */; }; DBFEEC9B279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC9A279BDDD9004F81DD /* ProfileAboutViewModel+Diffable.swift */; }; DBFEEC9D279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEEC9C279C12C1004F81DD /* ProfileFieldEditCollectionViewCell.swift */; }; - DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05726A576EE006D7ED1 /* ComposeViewModel.swift */; }; - DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05526A576EE006D7ED1 /* StatusEditorView.swift */; }; - DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05826A576EE006D7ED1 /* ContentWarningEditorView.swift */; }; - DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05626A576EE006D7ED1 /* ComposeView.swift */; }; - DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05926A576EE006D7ED1 /* StatusAuthorView.swift */; }; - DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF05A26A576EE006D7ED1 /* StatusAttachmentView.swift */; }; - DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06226A577F2006D7ED1 /* StatusAttachmentViewModel.swift */; }; - DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBFEF06C26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -572,7 +524,6 @@ 2D206B8525F5FB0900143C56 /* Double.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Double.swift; sourceTree = ""; }; 2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = ""; }; 2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = ""; }; - 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = ""; }; 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = ""; }; 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; @@ -588,12 +539,10 @@ 2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = ""; }; 2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = ""; }; 2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = ""; }; - 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = ""; }; 2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = ""; }; 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = ""; }; 2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = ""; }; 2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = ""; }; - 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = ""; }; 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = ""; }; 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = ""; }; @@ -606,14 +555,10 @@ 2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = ""; }; 2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewController+Avatar.swift"; sourceTree = ""; }; - 2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = ""; }; - 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = ""; }; - 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = ""; }; 2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewController.swift; sourceTree = ""; }; 2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewModel.swift; sourceTree = ""; }; 2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewCell.swift; sourceTree = ""; }; - 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = ""; }; 2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = ""; }; 2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = ""; }; 2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = ""; }; @@ -638,9 +583,6 @@ 5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = ""; }; 5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = ""; }; 5DA82A9B4ABDAFA3AB9A49C7 /* Pods-MastodonTests.asdk.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MastodonTests.asdk.xcconfig"; path = "Target Support Files/Pods-MastodonTests/Pods-MastodonTests.asdk.xcconfig"; sourceTree = ""; }; - 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = ""; }; - 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = ""; }; - 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = ""; }; 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = ""; }; 6130CBE4B26E3C976ACC1688 /* Pods-ShareActionExtension.asdk - debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareActionExtension.asdk - debug.xcconfig"; path = "Target Support Files/Pods-ShareActionExtension/Pods-ShareActionExtension.asdk - debug.xcconfig"; sourceTree = ""; }; 6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = ""; }; @@ -684,8 +626,6 @@ DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = ""; }; DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListHeaderView.swift; sourceTree = ""; }; DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = ""; }; - DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = ""; }; - DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = ""; }; DB0617EA277EF3820030EE79 /* GradientBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientBorderView.swift; sourceTree = ""; }; DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = ""; }; DB0617EE277F12720030EE79 /* NavigationActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionView.swift; sourceTree = ""; }; @@ -707,8 +647,6 @@ DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = ""; }; DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+Configuration.swift"; sourceTree = ""; }; DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = ""; }; - DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = ""; }; - DB0FCB7127952986006C02E2 /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = ""; }; DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status.swift"; sourceTree = ""; }; DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+DataSourceProvider.swift"; sourceTree = ""; }; DB0FCB7727957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+UITableViewDelegate.swift"; sourceTree = ""; }; @@ -748,14 +686,6 @@ DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = ""; }; DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = ""; }; DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; - DB336F40278E68480031E64B /* StatusView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusView+Configuration.swift"; sourceTree = ""; }; - DB336F42278EB1680031E64B /* MediaView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaView+Configuration.swift"; sourceTree = ""; }; - DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = ""; }; - DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = ""; }; - DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = ""; }; - DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollTableViewCell.swift; sourceTree = ""; }; - DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = ""; }; - DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = ""; }; DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = ""; }; DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewController.swift; sourceTree = ""; }; DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewModel.swift; sourceTree = ""; }; @@ -789,8 +719,6 @@ DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = ""; }; DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = ""; }; - DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = ""; }; - DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItem.swift; sourceTree = ""; }; DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = ""; }; DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = ""; }; DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; }; @@ -893,8 +821,6 @@ DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = ""; }; DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = ""; }; DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = ""; }; - DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = ""; }; - DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = ""; }; DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = ""; }; DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolRelayDelegate.swift; sourceTree = ""; }; DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolDelegate.swift; sourceTree = ""; }; @@ -922,14 +848,10 @@ DB6B74FD272FF59000C70B6E /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = ""; }; DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = ""; }; DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = ""; }; - DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = ""; }; - DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = ""; }; DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = ""; }; DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = ""; }; DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = ""; }; DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; - DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = ""; }; - DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = ""; }; DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = ""; }; DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = ""; }; DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = ""; }; @@ -976,7 +898,6 @@ DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = ""; }; DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = ""; }; DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = ""; }; - DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = ""; }; DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = ""; }; DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = ""; }; DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = ""; }; @@ -1022,7 +943,6 @@ DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldSection.swift; sourceTree = ""; }; DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = ""; }; DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = ""; }; - DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = ""; }; DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = ""; }; DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = ""; }; DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = ""; }; @@ -1042,20 +962,13 @@ DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = ""; }; DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = ""; }; DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = ""; }; - DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = ""; }; DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = ""; }; DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = ""; }; - DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = ""; }; - DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = ""; }; - DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+Diffable.swift"; sourceTree = ""; }; - DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+State.swift"; sourceTree = ""; }; - DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteSection.swift; sourceTree = ""; }; - DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteItem.swift; sourceTree = ""; }; DBC6461226A170AB00B0E31B /* ShareActionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareActionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - DBC6461426A170AB00B0E31B /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = ""; }; DBC6461726A170AB00B0E31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - DBC6462226A1712000B0E31B /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = ""; }; + DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = ""; }; DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = ""; }; DBC9E3A3282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = ""; }; DBC9E3A4282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -1320,8 +1233,6 @@ 2D152A8A25C295B8009AA50C /* Content */ = { isa = PBXGroup; children = ( - DB336F40278E68480031E64B /* StatusView+Configuration.swift */, - DB336F42278EB1680031E64B /* MediaView+Configuration.swift */, DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */, DB0FCB992797F7AD006C02E2 /* UserView+Configuration.swift */, DB63F776279A9A2A00455B82 /* NotificationView+Configuration.swift */, @@ -1406,7 +1317,6 @@ 2D5A3D0125CF8640002347D6 /* Vender */ = { isa = PBXGroup; children = ( - 2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */, DB6180EC26391C6C0018D199 /* TransitioningMath.swift */, DB75BF1D263C1C1B00EDBF1F /* CustomScheduler.swift */, DBF156E32702DB3F00EC00B7 /* HandleTapAction.swift */, @@ -1420,7 +1330,6 @@ isa = PBXGroup; children = ( DB697DD7278F4C34004EF2F7 /* Provider */, - DB0FCB7127952986006C02E2 /* NamingState.swift */, 2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */, DB4AA6B227BA34B6009EC082 /* CellFrameCacheContainer.swift */, 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */, @@ -1443,7 +1352,6 @@ DB0FCB892796BE1E006C02E2 /* RecommandAccount */, DB4F097926A039C400D62E92 /* Status */, DB65C63527A2AF52008BAC2E /* Report */, - DB4F097626A0398000D62E92 /* Compose */, DB0617F727855B010030EE79 /* Notification */, DB4F097726A039A200D62E92 /* Search */, DB3E6FE52806A5BA00B035AE /* Discovery */, @@ -1467,7 +1375,6 @@ 2D7631A525C1532D00929FB9 /* View */ = { isa = PBXGroup; children = ( - 2DA504672601ADBA008F4E6C /* Decoration */, 2D42FF8325C82245004A627A /* Button */, DBA9B90325F1D4420012E7B6 /* Control */, 2D152A8A25C295B8009AA50C /* Content */, @@ -1487,11 +1394,6 @@ DB0FCB7D27958957006C02E2 /* StatusThreadRootTableViewCell+ViewModel.swift */, DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */, DB0FCB972797F6BF006C02E2 /* UserTableViewCell+ViewModel.swift */, - 2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */, - DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */, - 2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */, - 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */, - DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */, DBE3CDBA261C427900430CC6 /* TimelineHeaderTableViewCell.swift */, DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */, DB02CDAA26256A9500D0A2AF /* ThreadReplyLoaderTableViewCell.swift */, @@ -1499,14 +1401,6 @@ path = TableviewCell; sourceTree = ""; }; - 2DA504672601ADBA008F4E6C /* Decoration */ = { - isa = PBXGroup; - children = ( - 2DA504682601ADE7008F4E6C /* SawToothView.swift */, - ); - path = Decoration; - sourceTree = ""; - }; 2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = { isa = PBXGroup; children = ( @@ -1638,17 +1532,6 @@ path = Onboarding; sourceTree = ""; }; - DB03F7F1268990A2007B274C /* TableViewCell */ = { - isa = PBXGroup; - children = ( - DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */, - DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */, - DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */, - DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */, - ); - path = TableViewCell; - sourceTree = ""; - }; DB0617F727855B010030EE79 /* Notification */ = { isa = PBXGroup; children = ( @@ -1808,13 +1691,6 @@ path = Discovery; sourceTree = ""; }; - DB3E6FEA2806BD2500B035AE /* MastodonUI */ = { - isa = PBXGroup; - children = ( - ); - path = MastodonUI; - sourceTree = ""; - }; DB3E6FED2806D7FC00B035AE /* News */ = { isa = PBXGroup; children = ( @@ -1934,23 +1810,6 @@ path = SearchResult; sourceTree = ""; }; - DB4F097626A0398000D62E92 /* Compose */ = { - isa = PBXGroup; - children = ( - DB66729525F9F91600D60309 /* ComposeStatusSection.swift */, - DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */, - DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */, - DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */, - DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */, - DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */, - DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */, - DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */, - DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */, - DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */, - ); - path = Compose; - sourceTree = ""; - }; DB4F097726A039A200D62E92 /* Search */ = { isa = PBXGroup; children = ( @@ -2016,7 +1875,6 @@ DB55D32225FB4D320002F825 /* View */ = { isa = PBXGroup; children = ( - DB03F7F42689B782007B274C /* ComposeTableView.swift */, DBA0A11225FB3FC10079C110 /* ComposeToolbarView.swift */, DB8190C52601FF0400020C08 /* AttachmentContainerView.swift */, DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */, @@ -2260,34 +2118,6 @@ path = Follower; sourceTree = ""; }; - DB6C8C0525F0921200AAA452 /* MastodonSDK */ = { - isa = PBXGroup; - children = ( - DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */, - 5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */, - 5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */, - 5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */, - 2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */, - 2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */, - DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */, - DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */, - ); - path = MastodonSDK; - sourceTree = ""; - }; - DB6F5E36264E78EA009108F4 /* AutoComplete */ = { - isa = PBXGroup; - children = ( - DBBF1DC02652402000E5B703 /* View */, - DBBF1DC326524D3100E5B703 /* Cell */, - DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */, - DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */, - DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */, - DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */, - ); - path = AutoComplete; - sourceTree = ""; - }; DB72602125E36A2500235243 /* ServerRules */ = { isa = PBXGroup; children = ( @@ -2312,10 +2142,8 @@ DB789A1025F9F29B0071ACA0 /* Compose */ = { isa = PBXGroup; children = ( - DB6F5E36264E78EA009108F4 /* AutoComplete */, DB55D32225FB4D320002F825 /* View */, DB789A2125F9F76D0071ACA0 /* CollectionViewCell */, - DB03F7F1268990A2007B274C /* TableViewCell */, DB789A0A25F9F2950071ACA0 /* ComposeViewController.swift */, DB789A1125F9F2CC0071ACA0 /* ComposeViewModel.swift */, DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */, @@ -2404,8 +2232,6 @@ DB8AF56225C138BC002E6C99 /* Extension */ = { isa = PBXGroup; children = ( - DB3E6FEA2806BD2500B035AE /* MastodonUI */, - DB6C8C0525F0921200AAA452 /* MastodonSDK */, 2DF123A625C3B0210020F248 /* ActiveLabel.swift */, 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, 2D206B8525F5FB0900143C56 /* Double.swift */, @@ -2700,22 +2526,6 @@ path = Helper; sourceTree = ""; }; - DBBF1DC02652402000E5B703 /* View */ = { - isa = PBXGroup; - children = ( - DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */, - ); - path = View; - sourceTree = ""; - }; - DBBF1DC326524D3100E5B703 /* Cell */ = { - isa = PBXGroup; - children = ( - DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */, - ); - path = Cell; - sourceTree = ""; - }; DBC6461326A170AB00B0E31B /* ShareActionExtension */ = { isa = PBXGroup; children = ( @@ -2897,8 +2707,8 @@ isa = PBXGroup; children = ( DBFEF05426A576EE006D7ED1 /* View */, - DBC6462226A1712000B0E31B /* ShareViewModel.swift */, - DBC6461426A170AB00B0E31B /* ShareViewController.swift */, + DBC6462226A1712000B0E31B /* ComposeViewModel.swift */, + DBC6461426A170AB00B0E31B /* ComposeViewController.swift */, ); path = Scene; sourceTree = ""; @@ -3347,19 +3157,14 @@ DB65C63727A2AF6C008BAC2E /* ReportItem.swift in Sources */, DB5B54B22833C24B00DEF8B2 /* RebloggedByViewController+DataSourceProvider.swift in Sources */, 2D59819B25E4A581000FB903 /* MastodonConfirmEmailViewController.swift in Sources */, - DBA94440265D137600C537E1 /* Mastodon+Entity+Field.swift in Sources */, - DBBF1DC7265251D400E5B703 /* AutoCompleteViewModel+State.swift in Sources */, DB03A793272A7E5700EE37C5 /* SidebarListHeaderView.swift in Sources */, DB4FFC2B269EC39600D62E92 /* SearchToSearchDetailViewControllerAnimatedTransitioning.swift in Sources */, DB5B7298273112C800081888 /* FollowingListViewModel.swift in Sources */, 0FB3D2F725E4C24D00AAD544 /* MastodonPickServerViewModel.swift in Sources */, DB5B54AE2833C15F00DEF8B2 /* UserListViewModel+Diffable.swift in Sources */, - DB336F43278EB1690031E64B /* MediaView+Configuration.swift in Sources */, - DB66729625F9F91600D60309 /* ComposeStatusSection.swift in Sources */, DB482A3F261331E8008AE74C /* UserTimelineViewModel+State.swift in Sources */, DB3E6FE02806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift in Sources */, 2D38F1F725CD47AC00561493 /* HomeTimelineViewModel+LoadOldestState.swift in Sources */, - DB447681260B3ED600B66B82 /* CustomEmojiPickerSection.swift in Sources */, DB0FCB7427956939006C02E2 /* DataSourceFacade+Status.swift in Sources */, DBB525502611ED6D002F1F29 /* ProfileHeaderView.swift in Sources */, DB63F75A279953F200455B82 /* SearchHistoryUserCollectionViewCell+ViewModel.swift in Sources */, @@ -3395,13 +3200,11 @@ DB697DD9278F4CED004EF2F7 /* HomeTimelineViewController+DataSourceProvider.swift in Sources */, DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */, DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */, - 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */, 2D8434F525FF465D00EECE90 /* HomeTimelineNavigationBarTitleViewModel.swift in Sources */, DB938F0F2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift in Sources */, DB6180F226391CF40018D199 /* MediaPreviewImageViewModel.swift in Sources */, 62FD27D12893707600B205C5 /* BookmarkViewController.swift in Sources */, DBD5B1F627BCD3D200BD6B38 /* SuggestionAccountTableViewCell+ViewModel.swift in Sources */, - 5DDDF1932617442700311060 /* Mastodon+Entity+Account.swift in Sources */, DB63F767279A5EB300455B82 /* NotificationTimelineViewModel.swift in Sources */, 2D607AD826242FC500B70763 /* NotificationViewModel.swift in Sources */, DB5B54AB2833C12A00DEF8B2 /* RebloggedByViewController.swift in Sources */, @@ -3447,7 +3250,6 @@ DBCBED1726132DB500B49291 /* UserTimelineViewModel+Diffable.swift in Sources */, 2DE0FACE2615F7AD00CDF649 /* RecommendAccountSection.swift in Sources */, 2DAC9E3E262FC2400062E1A6 /* SuggestionAccountViewModel.swift in Sources */, - DB3667A8268AE2900027D07F /* ComposeStatusPollItem.swift in Sources */, DB603113279EBEBA00A935FE /* DataSourceFacade+Block.swift in Sources */, DB63F777279A9A2A00455B82 /* NotificationView+Configuration.swift in Sources */, DB029E95266A20430062874E /* MastodonAuthenticationController.swift in Sources */, @@ -3457,7 +3259,6 @@ DBE3CDEC261C6B2900430CC6 /* FavoriteViewController.swift in Sources */, DB938EE62623F50700E5B6C1 /* ThreadViewController.swift in Sources */, DB6180F426391D110018D199 /* MediaPreviewImageView.swift in Sources */, - DB336F41278E68480031E64B /* StatusView+Configuration.swift in Sources */, DBF9814A265E24F500E4BA07 /* ProfileFieldCollectionViewHeaderFooterView.swift in Sources */, 2D939AB525EDD8A90076FA61 /* String.swift in Sources */, DB4481B925EE289600BEFB67 /* UITableView.swift in Sources */, @@ -3470,12 +3271,9 @@ DB5B549F2833A72500DEF8B2 /* FamiliarFollowersViewModel+Diffable.swift in Sources */, DB6B351E2601FAEE00DC1E11 /* ComposeStatusAttachmentCollectionViewCell.swift in Sources */, DB8F7076279E954700E1225B /* DataSourceFacade+Follow.swift in Sources */, - DB36679F268ABAF20027D07F /* ComposeStatusAttachmentSection.swift in Sources */, - 2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */, DB63F7542799491600455B82 /* DataSourceFacade+SearchHistory.swift in Sources */, DB7A9F912818EAF10016AF98 /* MastodonRegisterView.swift in Sources */, DBF1572F27046F1A00EC00B7 /* SecondaryPlaceholderViewController.swift in Sources */, - DB03F7F32689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift in Sources */, 2D4AD8A826316D3500613EFC /* SelectedAccountItem.swift in Sources */, DBE3CDFB261C6CA500430CC6 /* FavoriteViewModel.swift in Sources */, DBE3CE01261D623D00430CC6 /* FavoriteViewModel+State.swift in Sources */, @@ -3487,7 +3285,6 @@ DB63F7452799056400455B82 /* HashtagTableViewCell.swift in Sources */, DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */, DB3EA8EB281B7E0700598866 /* DiscoveryCommunityViewModel+State.swift in Sources */, - DB0FCB7227952986006C02E2 /* NamingState.swift in Sources */, DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */, DB98EB5327B0F9890082E365 /* ReportHeadlineTableViewCell.swift in Sources */, DB5B729C273113C200081888 /* FollowingListViewModel+Diffable.swift in Sources */, @@ -3496,14 +3293,12 @@ DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */, DB5B729E273113F300081888 /* FollowingListViewModel+State.swift in Sources */, 2DF123A725C3B0210020F248 /* ActiveLabel.swift in Sources */, - 5DDDF1A92617489F00311060 /* Mastodon+Entity+History.swift in Sources */, DBF9814C265E339500E4BA07 /* ProfileFieldAddEntryCollectionViewCell.swift in Sources */, DB63F76227996B6600455B82 /* SearchHistoryViewController+DataSourceProvider.swift in Sources */, DB73BF4927140BA300781945 /* UICollectionViewDiffableDataSource.swift in Sources */, DBA5E7AB263BD3F5004598BB /* TimelineTableViewCellContextMenuConfiguration.swift in Sources */, DB73B490261F030A002E9E9F /* SafariActivity.swift in Sources */, DB63F7492799126300455B82 /* FollowerListViewController+DataSourceProvider.swift in Sources */, - DB6D1B44263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift in Sources */, 2D198643261BF09500F0B013 /* SearchResultItem.swift in Sources */, 2DAC9E38262FC2320062E1A6 /* SuggestionAccountViewController.swift in Sources */, DB66728C25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift in Sources */, @@ -3529,14 +3324,12 @@ DB852D1926FAEB6B00FC9D81 /* SidebarViewController.swift in Sources */, 2D206B9225F60EA700143C56 /* UIControl.swift in Sources */, DBDFF1932805554900557A48 /* DiscoveryPostsViewModel.swift in Sources */, - DBBF1DC92652538500E5B703 /* AutoCompleteSection.swift in Sources */, DB3E6FE72806A7A200B035AE /* DiscoveryItem.swift in Sources */, DB8AF55D25C138B7002E6C99 /* UIViewController.swift in Sources */, DBEFCD79282A147000C0ABEA /* ReportStatusViewModel.swift in Sources */, DB7F48452620241000796008 /* ProfileHeaderViewModel.swift in Sources */, DB0A322E280EE9FD001729D2 /* DiscoveryIntroBannerView.swift in Sources */, 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */, - 5DDDF1992617447F00311060 /* Mastodon+Entity+Tag.swift in Sources */, 5B90C45F262599800002E742 /* SettingsToggleTableViewCell.swift in Sources */, 2D694A7425F9EB4E0038ADDC /* ContentWarningOverlayView.swift in Sources */, DB0FCB7827957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift in Sources */, @@ -3550,12 +3343,9 @@ 0F20222D261457EE000C64BF /* HashtagTimelineViewModel+State.swift in Sources */, DB0009A626AEE5DC009B9D2D /* Intents.intentdefinition in Sources */, 5B90C462262599800002E742 /* SettingsSectionHeader.swift in Sources */, - DB44768B260B3F2100B66B82 /* CustomEmojiPickerItem.swift in Sources */, 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */, DB3E6FEF2806D82600B035AE /* DiscoveryNewsViewModel.swift in Sources */, - DBBF1DCB2652539E00E5B703 /* AutoCompleteItem.swift in Sources */, DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, - DB3667A6268AE2620027D07F /* ComposeStatusPollSection.swift in Sources */, DB6B750427300B4000C70B6E /* TimelineFooterTableViewCell.swift in Sources */, DB98EB4C27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift in Sources */, DB852D1F26FB037800FC9D81 /* SidebarViewModel.swift in Sources */, @@ -3568,7 +3358,6 @@ DB0617EF277F12720030EE79 /* NavigationActionView.swift in Sources */, DB1FD43625F26899004CFCFC /* MastodonPickServerViewModel+LoadIndexedServerState.swift in Sources */, 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */, - DB3667A1268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift in Sources */, DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */, DBA94434265CBB5300C537E1 /* ProfileFieldSection.swift in Sources */, DBCA0EBC282BB38A0029E2B0 /* PageboyNavigateable.swift in Sources */, @@ -3587,11 +3376,8 @@ DB023D2C27A10464005AC798 /* NotificationTimelineViewController+DataSourceProvider.swift in Sources */, DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */, DBF1D257269DBAC600C1C08A /* SearchDetailViewModel.swift in Sources */, - DB03F7F52689B782007B274C /* ComposeTableView.swift in Sources */, DBB45B5927B39FE4002DC5A7 /* MediaPreviewVideoViewModel.swift in Sources */, - DB6C8C0F25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift in Sources */, DB0FCB76279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift in Sources */, - DB0FCB7027951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift in Sources */, DB1E346825F518E20079D7DF /* CategoryPickerSection.swift in Sources */, DB7274F4273BB9B200577D95 /* ListBatchFetchViewModel.swift in Sources */, DB0618052785A73D0030EE79 /* RegisterItem.swift in Sources */, @@ -3605,7 +3391,6 @@ DB3E6FDD2806A40F00B035AE /* DiscoveryHashtagsViewController.swift in Sources */, DB938EED2623F79B00E5B6C1 /* ThreadViewModel.swift in Sources */, DB6988DE2848D11C002398EF /* PagerTabStripNavigateable.swift in Sources */, - DBBC24AC26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift in Sources */, 2DCB73FD2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift in Sources */, DB852D1C26FB021500FC9D81 /* RootSplitViewController.swift in Sources */, DB697DD1278F4871004EF2F7 /* AutoGenerateTableViewDelegate.swift in Sources */, @@ -3619,7 +3404,6 @@ DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */, DB63F779279ABF9C00455B82 /* DataSourceFacade+Reblog.swift in Sources */, DB4F0963269ED06300D62E92 /* SearchResultViewController.swift in Sources */, - DBBF1DC5265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift in Sources */, DB603111279EB38500A935FE /* DataSourceFacade+Mute.swift in Sources */, DB68A04A25E9027700CFDF14 /* AdaptiveStatusBarStyleNavigationController.swift in Sources */, 0FB3D33825E6401400AAD544 /* PickServerCell.swift in Sources */, @@ -3636,7 +3420,6 @@ DBA5E7A9263BD3A4004598BB /* ContextMenuImagePreviewViewController.swift in Sources */, DBF156E22702DA6900EC00B7 /* UIStatusBarManager+HandleTapAction.m in Sources */, 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */, - DB6F5E35264E78E7009108F4 /* AutoCompleteViewController.swift in Sources */, DB697DE1278F5296004EF2F7 /* DataSourceFacade+Model.swift in Sources */, DBCC3B8F26148F7B0045B23D /* CachedProfileViewModel.swift in Sources */, DB4F097526A037F500D62E92 /* SearchHistoryViewModel.swift in Sources */, @@ -3650,9 +3433,7 @@ DB9F58F126EF512300E7BBE9 /* AccountListTableViewCell.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, DB938F0326240EA300E5B6C1 /* CachedThreadViewModel.swift in Sources */, - 2D650FAB25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift in Sources */, DBA5A53526F0A36A00CACBAA /* AddAccountTableViewCell.swift in Sources */, - 2DB72C8C262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift in Sources */, 2D35237A26256D920031AF25 /* NotificationSection.swift in Sources */, 2D4AD89C263165B500613EFC /* SuggestionAccountCollectionViewCell.swift in Sources */, DB98EB6927B21A7C0082E365 /* ReportResultActionTableViewCell.swift in Sources */, @@ -3672,8 +3453,6 @@ DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, DBEFCD74282A130400C0ABEA /* ReportReasonViewModel.swift in Sources */, - 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, - DB36679D268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, DBFEEC96279BDC67004F81DD /* ProfileAboutViewController.swift in Sources */, DB63F74F2799405600455B82 /* SearchHistoryViewModel+Diffable.swift in Sources */, @@ -3689,10 +3468,8 @@ DB6B74FC272FF55800C70B6E /* UserSection.swift in Sources */, DB0FCB862796BDA1006C02E2 /* SearchSection.swift in Sources */, DB1D61CF26F1B33600DA8662 /* WelcomeViewModel.swift in Sources */, - 2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */, DBD376B2269302A4007FEC24 /* UITableViewCell.swift in Sources */, DB4F0966269ED52200D62E92 /* SearchResultViewModel.swift in Sources */, - DBBF1DBF2652401B00E5B703 /* AutoCompleteViewModel.swift in Sources */, DB6180FA26391F2E0018D199 /* MediaPreviewViewModel.swift in Sources */, 2D7631A825C1535600929FB9 /* StatusTableViewCell.swift in Sources */, DB6B7500272FF73800C70B6E /* UserTableViewCell.swift in Sources */, @@ -3708,7 +3485,6 @@ DB3E6FE42806A5B800B035AE /* DiscoverySection.swift in Sources */, DB8190C62601FF0400020C08 /* AttachmentContainerView.swift in Sources */, DB697DDB278F4DE3004EF2F7 /* DataSourceProvider+StatusTableViewCellDelegate.swift in Sources */, - 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */, DB87D4512609CF1E00D12C0D /* ComposeStatusPollOptionAppendEntryCollectionViewCell.swift in Sources */, DBB45B5627B39FC9002DC5A7 /* MediaPreviewVideoViewController.swift in Sources */, DB0FCB8027968F70006C02E2 /* MastodonStatusThreadViewModel.swift in Sources */, @@ -3724,7 +3500,6 @@ DB3E6FF12806D96900B035AE /* DiscoveryNewsViewModel+Diffable.swift in Sources */, DB3E6FF82807C45300B035AE /* DiscoveryForYouViewModel.swift in Sources */, DB0F9D56283EB46200379AF8 /* ProfileHeaderView+Configuration.swift in Sources */, - DB6F5E38264E994A009108F4 /* AutoCompleteTopChevronView.swift in Sources */, DB6746F0278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift in Sources */, DBB525412611ED54002F1F29 /* ProfileHeaderViewController.swift in Sources */, DBDFF19A28055A1400557A48 /* DiscoveryViewController.swift in Sources */, @@ -3738,10 +3513,7 @@ DB63F74B279914A000455B82 /* FollowingListViewController+DataSourceProvider.swift in Sources */, DBEFCD7D282A2A3B00C0ABEA /* ReportServerRulesViewController.swift in Sources */, DBB525362611ECEB002F1F29 /* UserTimelineViewController.swift in Sources */, - DB938F3326243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift in Sources */, DB98EB4927B0F0CD0082E365 /* ReportStatusTableViewCell.swift in Sources */, - DB3667A4268AE2370027D07F /* ComposeStatusPollTableViewCell.swift in Sources */, - DBBF1DC226524D2900E5B703 /* AutoCompleteTableViewCell.swift in Sources */, DBF3B7412733EB9400E21627 /* MastodonLocalCode.swift in Sources */, DB98EB6527B216500082E365 /* ReportResultViewModel.swift in Sources */, DB4F096A269EDAD200D62E92 /* SearchResultViewModel+State.swift in Sources */, @@ -3751,7 +3523,6 @@ 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 */, DBD5B1F827BCFD9D00BD6B38 /* DataSourceProvider+TableViewControllerNavigateable.swift in Sources */, 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, @@ -3796,18 +3567,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - DBFEF05E26A57715006D7ED1 /* ComposeView.swift in Sources */, - DBFEF06026A57715006D7ED1 /* StatusAttachmentView.swift in Sources */, - DBC6462326A1712000B0E31B /* ShareViewModel.swift in Sources */, - DBFEF06D26A67FB7006D7ED1 /* StatusAttachmentViewModel+UploadState.swift in Sources */, - DBFEF05F26A57715006D7ED1 /* StatusAuthorView.swift in Sources */, - DBFEF05D26A57715006D7ED1 /* ContentWarningEditorView.swift in Sources */, - DBFEF05C26A57715006D7ED1 /* StatusEditorView.swift in Sources */, + DBC6462326A1712000B0E31B /* ComposeViewModel.swift in Sources */, DBB3BA2B26A81D060004F2D4 /* FLAnimatedImageView.swift in Sources */, - DBBC24A826A52F9000398BB9 /* ComposeToolbarView.swift in Sources */, - DBFEF05B26A57715006D7ED1 /* ComposeViewModel.swift in Sources */, - DBFEF06326A577F2006D7ED1 /* StatusAttachmentViewModel.swift in Sources */, - DBC6461526A170AB00B0E31B /* ShareViewController.swift in Sources */, + DBC6461526A170AB00B0E31B /* ComposeViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 21906eb03..33eda54f1 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -112,12 +112,12 @@ NotificationService.xcscheme_^#shared#^_ orderHint - 25 + 18 ShareActionExtension.xcscheme_^#shared#^_ orderHint - 24 + 17 SuppressBuildableAutocreation diff --git a/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift b/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift deleted file mode 100644 index e55e8849d..000000000 --- a/Mastodon/Diffiable/Compose/CustomEmojiPickerSection.swift +++ /dev/null @@ -1,62 +0,0 @@ -// -// CustomEmojiPickerSection.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-3-24. -// - -import UIKit - -enum CustomEmojiPickerSection: Equatable, Hashable { - case emoji(name: String) -} - -extension CustomEmojiPickerSection { - static func collectionViewDiffableDataSource( - for collectionView: UICollectionView, - dependency: NeedsDependency - ) -> UICollectionViewDiffableDataSource { - let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in - guard let _ = dependency else { return nil } - switch item { - case .emoji(let attribute): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell - let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill) - .af.imageRounded(withCornerRadius: 4) - - let isAnimated = !UserDefaults.shared.preferredStaticEmoji - let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL) - cell.emojiImageView.sd_setImage( - with: url, - placeholderImage: placeholder, - options: [], - context: nil - ) - cell.accessibilityLabel = attribute.emoji.shortcode - return cell - } - } - - dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in - guard let dataSource = dataSource else { return nil } - let sections = dataSource.snapshot().sectionIdentifiers - guard indexPath.section < sections.count else { return nil } - let section = sections[indexPath.section] - - switch kind { - case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self): - let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView - switch section { - case .emoji(let name): - header.titleLabel.text = name - } - return header - default: - assertionFailure() - return nil - } - } - - return dataSource - } -} diff --git a/Mastodon/Diffiable/Notification/NotificationSection.swift b/Mastodon/Diffiable/Notification/NotificationSection.swift index a67d407ee..387affbc7 100644 --- a/Mastodon/Diffiable/Notification/NotificationSection.swift +++ b/Mastodon/Diffiable/Notification/NotificationSection.swift @@ -15,6 +15,7 @@ import MetaTextKit import MastodonMeta import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization enum NotificationSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/Report/ReportSection.swift b/Mastodon/Diffiable/Report/ReportSection.swift index b815975b0..ba3c5525a 100644 --- a/Mastodon/Diffiable/Report/ReportSection.swift +++ b/Mastodon/Diffiable/Report/ReportSection.swift @@ -14,6 +14,7 @@ import UIKit import os.log import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization enum ReportSection: Equatable, Hashable { diff --git a/Mastodon/Diffiable/User/UserSection.swift b/Mastodon/Diffiable/User/UserSection.swift index 6bb402b3a..20812b7e8 100644 --- a/Mastodon/Diffiable/User/UserSection.swift +++ b/Mastodon/Diffiable/User/UserSection.swift @@ -10,8 +10,9 @@ import UIKit import CoreData import CoreDataStack import MastodonCore -import MetaTextKit +import MastodonUI import MastodonMeta +import MetaTextKit enum UserSection: Hashable { case main diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift deleted file mode 100644 index 09bbb3d8a..000000000 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// Mastodon+Entity+Account.swift -// Mastodon -// -// Created by xiaojian sun on 2021/4/2. -// - -import UIKit -import MastodonSDK -import MastodonMeta - -extension Mastodon.Entity.Account: Hashable { - public func hash(into hasher: inout Hasher) { - hasher.combine(id) - } - - public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { - return lhs.id == rhs.id - } -} - -extension Mastodon.Entity.Account { - - var displayNameWithFallback: String { - return !displayName.isEmpty ? displayName : username - } - -} - -extension Mastodon.Entity.Account { - public func avatarImageURL() -> URL? { - let string = UserDefaults.shared.preferredStaticAvatar ? avatarStatic ?? avatar : avatar - return URL(string: string) - } - - public func avatarImageURLWithFallback(domain: String) -> URL { - return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! - } -} - -extension Mastodon.Entity.Account { - var emojiMeta: MastodonContent.Emojis { - let isAnimated = !UserDefaults.shared.preferredStaticEmoji - - var dict = MastodonContent.Emojis() - for emoji in emojis ?? [] { - dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL - } - return dict - } -} diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift b/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift deleted file mode 100644 index 6251d1814..000000000 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Tag.swift +++ /dev/null @@ -1,18 +0,0 @@ -// -// Mastodon+Entity+Tag.swift -// Mastodon -// -// Created by xiaojian sun on 2021/4/2. -// - -import MastodonSDK - -//extension Mastodon.Entity.Tag: Hashable { -// public func hash(into hasher: inout Hasher) { -// hasher.combine(name) -// } -// -// public static func == (lhs: Mastodon.Entity.Tag, rhs: Mastodon.Entity.Tag) -> Bool { -// return lhs.name == rhs.name -// } -//} diff --git a/Mastodon/Protocol/NamingState.swift b/Mastodon/Protocol/NamingState.swift deleted file mode 100644 index edf6265e8..000000000 --- a/Mastodon/Protocol/NamingState.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// NamingState.swift -// Mastodon -// -// Created by MainasuK on 2022-1-17. -// - -import Foundation - -protocol NamingState { - var name: String { get } -} diff --git a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift index aecfe6a8d..53755e00a 100644 --- a/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift +++ b/Mastodon/Protocol/Provider/DataSourceFacade+Status.swift @@ -103,8 +103,8 @@ extension DataSourceFacade { let composeViewModel = ComposeViewModel( context: provider.context, - composeKind: .reply(status: status), - authContext: provider.authContext + authContext: provider.authContext, + kind: .reply(status: status) ) _ = provider.coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift index 390f246d4..e7b55f91c 100644 --- a/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift +++ b/Mastodon/Protocol/Provider/DataSourceProvider+StatusTableViewControllerNavigateable.swift @@ -99,10 +99,10 @@ extension StatusTableViewControllerNavigateableCore where Self: DataSourceProvid let composeViewModel = ComposeViewModel( context: self.context, - composeKind: .reply(status: status), - authContext: authContext + authContext: authContext, + kind: .reply(status: status) ) - self.coordinator.present( + _ = self.coordinator.present( scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil) diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift b/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift deleted file mode 100644 index 742188726..000000000 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+Diffable.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// AutoCompleteViewModel+Diffable.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-5-17. -// - -import UIKit - -extension AutoCompleteViewModel { - - func setupDiffableDataSource( - for tableView: UITableView - ) { - diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView) - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - diffableDataSource?.apply(snapshot) - } - -} diff --git a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift index ac32129cc..e5b043adb 100644 --- a/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift +++ b/Mastodon/Scene/Compose/CollectionViewCell/ComposeStatusPollExpiresOptionCollectionViewCell.swift @@ -13,66 +13,66 @@ import MastodonCore import MastodonUI import MastodonLocalization -protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { - func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) -} - -final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { - - var disposeBag = Set() - weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? - - let durationButton: UIButton = { - let button = HighlightDimmableButton() - button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12)) - button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20) - button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal) - button.setTitleColor(Asset.Colors.brand.color, for: .normal) - return button - }() - - override init(frame: CGRect) { - super.init(frame: frame) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusPollExpiresOptionCollectionViewCell { - - private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption - - private func _init() { - durationButton.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(durationButton) - NSLayoutConstraint.activate([ - durationButton.topAnchor.constraint(equalTo: contentView.topAnchor), - durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), - durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - - let children = ExpiresOption.allCases.map { expiresOption -> UIAction in - UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in - guard let self = self else { return } - self.expiresOptionActionHandler(action, expiresOption: expiresOption) - } - } - durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) - durationButton.showsMenuAsPrimaryAction = true - } - -} - -extension ComposeStatusPollExpiresOptionCollectionViewCell { - - private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title) - delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption) - } - -} +//protocol ComposeStatusPollExpiresOptionCollectionViewCellDelegate: AnyObject { +// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) +//} +// +//final class ComposeStatusPollExpiresOptionCollectionViewCell: UICollectionViewCell { +// +// var disposeBag = Set() +// weak var delegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? +// +// let durationButton: UIButton = { +// let button = HighlightDimmableButton() +// button.titleLabel?.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 12)) +// button.expandEdgeInsets = UIEdgeInsets(top: 0, left: -10, bottom: -20, right: -20) +// button.setTitle(L10n.Scene.Compose.Poll.durationTime(L10n.Scene.Compose.Poll.thirtyMinutes), for: .normal) +// button.setTitleColor(Asset.Colors.brand.color, for: .normal) +// return button +// }() +// +// override init(frame: CGRect) { +// super.init(frame: frame) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension ComposeStatusPollExpiresOptionCollectionViewCell { +// +// private typealias ExpiresOption = ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption +// +// private func _init() { +// durationButton.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(durationButton) +// NSLayoutConstraint.activate([ +// durationButton.topAnchor.constraint(equalTo: contentView.topAnchor), +// durationButton.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor, constant: PollOptionView.checkmarkBackgroundLeadingMargin), +// durationButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// +// let children = ExpiresOption.allCases.map { expiresOption -> UIAction in +// UIAction(title: expiresOption.title, image: nil, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off) { [weak self] action in +// guard let self = self else { return } +// self.expiresOptionActionHandler(action, expiresOption: expiresOption) +// } +// } +// durationButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: .displayInline, children: children) +// durationButton.showsMenuAsPrimaryAction = true +// } +// +//} +// +//extension ComposeStatusPollExpiresOptionCollectionViewCell { +// +// private func expiresOptionActionHandler(_ sender: UIAction, expiresOption: ExpiresOption) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, expiresOption.title) +// delegate?.composeStatusPollExpiresOptionCollectionViewCell(self, didSelectExpiresOption: expiresOption) +// } +// +//} diff --git a/Mastodon/Scene/Compose/ComposeViewController.swift b/Mastodon/Scene/Compose/ComposeViewController.swift index b9605bb78..1fd3b0670 100644 --- a/Mastodon/Scene/Compose/ComposeViewController.swift +++ b/Mastodon/Scene/Compose/ComposeViewController.swift @@ -30,56 +30,65 @@ final class ComposeViewController: UIViewController, NeedsDependency { let logger = Logger(subsystem: "ComposeViewController", category: "logic") - private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) - let characterCountLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: 15, weight: .regular) - label.text = "500" - label.textColor = Asset.Colors.Label.secondary.color - label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500) - return label + lazy var composeContentViewModel: ComposeContentViewModel = { + return ComposeContentViewModel(context: context, kind: viewModel.kind) }() - private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem(customView: characterCountLabel) - return barButtonItem + private(set) lazy var composeContentViewController: ComposeContentViewController = { + let composeContentViewController = ComposeContentViewController() + composeContentViewController.viewModel = composeContentViewModel + return composeContentViewController }() - let publishButton: UIButton = { - let button = RoundedEdgesButton(type: .custom) - button.cornerRadius = 10 - button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height - button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) - button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) - return button - }() - private(set) lazy var publishBarButtonItem: UIBarButtonItem = { - configurePublishButtonApperance() - let shadowBackgroundContainer = ShadowBackgroundContainer() - publishButton.translatesAutoresizingMaskIntoConstraints = false - shadowBackgroundContainer.addSubview(publishButton) - NSLayoutConstraint.activate([ - publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor), - publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor), - publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor), - publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor), - ]) - let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) - return barButtonItem - }() - - private func configurePublishButtonApperance() { - publishButton.adjustsImageWhenHighlighted = false - publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal) - publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted) - publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) - publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) - } +// private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) +// let characterCountLabel: UILabel = { +// let label = UILabel() +// label.font = .systemFont(ofSize: 15, weight: .regular) +// label.text = "500" +// label.textColor = Asset.Colors.Label.secondary.color +// label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500) +// return label +// }() +// private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = { +// let barButtonItem = UIBarButtonItem(customView: characterCountLabel) +// return barButtonItem +// }() +// +// let publishButton: UIButton = { +// let button = RoundedEdgesButton(type: .custom) +// button.cornerRadius = 10 +// button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height +// button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) +// button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) +// return button +// }() +// private(set) lazy var publishBarButtonItem: UIBarButtonItem = { +// configurePublishButtonApperance() +// let shadowBackgroundContainer = ShadowBackgroundContainer() +// publishButton.translatesAutoresizingMaskIntoConstraints = false +// shadowBackgroundContainer.addSubview(publishButton) +// NSLayoutConstraint.activate([ +// publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor), +// publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor), +// publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor), +// publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor), +// ]) +// let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer) +// return barButtonItem +// }() +// +// private func configurePublishButtonApperance() { +// publishButton.adjustsImageWhenHighlighted = false +// publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal) +// publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted) +// publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) +// publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal) +// } - let scrollView: UIScrollView = { - let scrollView = UIScrollView() - scrollView.alwaysBounceVertical = true - return scrollView - }() +// let scrollView: UIScrollView = { +// let scrollView = UIScrollView() +// scrollView.alwaysBounceVertical = true +// return scrollView +// }() // let tableView: ComposeTableView = { // let tableView = ComposeTableView() @@ -92,56 +101,56 @@ final class ComposeViewController: UIViewController, NeedsDependency { // return tableView // }() - var systemKeyboardHeight: CGFloat = .zero { - didSet { - // note: some system AutoLayout warning here - let height = max(300, systemKeyboardHeight) - customEmojiPickerInputView.frame.size.height = height - } - } - - // CustomEmojiPickerView - let customEmojiPickerInputView: CustomEmojiPickerInputView = { - let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard) - return view - }() - - let composeToolbarView = ComposeToolbarView() - var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! - let composeToolbarBackgroundView = UIView() - - static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { - var configuration = PHPickerConfiguration() - configuration.filter = .any(of: [.images, .videos]) - configuration.selectionLimit = selectionLimit - return configuration - } - - private(set) lazy var photoLibraryPicker: PHPickerViewController = { - let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) - imagePicker.delegate = self - return imagePicker - }() - private(set) lazy var imagePickerController: UIImagePickerController = { - let imagePickerController = UIImagePickerController() - imagePickerController.sourceType = .camera - imagePickerController.delegate = self - return imagePickerController - }() - - private(set) lazy var documentPickerController: UIDocumentPickerViewController = { - let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) - documentPickerController.delegate = self - return documentPickerController - }() - - private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { - let viewController = AutoCompleteViewController() - viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext) - viewController.delegate = self - viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel - return viewController - }() +// var systemKeyboardHeight: CGFloat = .zero { +// didSet { +// // note: some system AutoLayout warning here +// let height = max(300, systemKeyboardHeight) +// customEmojiPickerInputView.frame.size.height = height +// } +// } +// +// // CustomEmojiPickerView +// let customEmojiPickerInputView: CustomEmojiPickerInputView = { +// let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard) +// return view +// }() +// +// let composeToolbarView = ComposeToolbarView() +// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! +// let composeToolbarBackgroundView = UIView() +// +// static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration { +// var configuration = PHPickerConfiguration() +// configuration.filter = .any(of: [.images, .videos]) +// configuration.selectionLimit = selectionLimit +// return configuration +// } +// +// private(set) lazy var photoLibraryPicker: PHPickerViewController = { +// let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration()) +// imagePicker.delegate = self +// return imagePicker +// }() +// private(set) lazy var imagePickerController: UIImagePickerController = { +// let imagePickerController = UIImagePickerController() +// imagePickerController.sourceType = .camera +// imagePickerController.delegate = self +// return imagePickerController +// }() +// +// private(set) lazy var documentPickerController: UIDocumentPickerViewController = { +// let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie]) +// documentPickerController.delegate = self +// return documentPickerController +// }() +// +// private(set) lazy var autoCompleteViewController: AutoCompleteViewController = { +// let viewController = AutoCompleteViewController() +// viewController.viewModel = AutoCompleteViewModel(context: context, authContext: viewModel.authContext) +// viewController.delegate = self +// viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel +// return viewController +// }() deinit { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -167,77 +176,88 @@ extension ComposeViewController { override func viewDidLoad() { super.viewDidLoad() - - configureNavigationBarTitleStyle() - viewModel.traitCollectionDidChangePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - self.configureNavigationBarTitleStyle() - } - .store(in: &disposeBag) - viewModel.$title - .receive(on: DispatchQueue.main) - .sink { [weak self] title in - guard let self = self else { return } - self.title = title - } - .store(in: &disposeBag) - self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: RunLoop.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) - navigationItem.leftBarButtonItem = cancelBarButtonItem - navigationItem.rightBarButtonItem = publishBarButtonItem - viewModel.traitCollectionDidChangePublisher - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let self = self else { return } - guard self.traitCollection.userInterfaceIdiom == .pad else { return } - var items = [self.publishBarButtonItem] - if self.traitCollection.horizontalSizeClass == .regular { - items.append(self.characterCountBarButtonItem) - } - self.navigationItem.rightBarButtonItems = items - } - .store(in: &disposeBag) - publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + addChild(composeContentViewController) + composeContentViewController.view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(composeContentViewController.view) + NSLayoutConstraint.activate([ + composeContentViewController.view.topAnchor.constraint(equalTo: view.topAnchor), + composeContentViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), + composeContentViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), + composeContentViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + composeContentViewController.didMove(toParent: self) - - scrollView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(scrollView) - NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - composeToolbarView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(composeToolbarView) - composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) - NSLayoutConstraint.activate([ - composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - composeToolbarViewBottomLayoutConstraint, - composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), - ]) - composeToolbarView.preservesSuperviewLayoutMargins = true - composeToolbarView.delegate = self - - composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false - view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) - NSLayoutConstraint.activate([ - composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), - composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), - composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), - view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), - ]) +// configureNavigationBarTitleStyle() +// viewModel.traitCollectionDidChangePublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let self = self else { return } +// self.configureNavigationBarTitleStyle() +// } +// .store(in: &disposeBag) +// +// viewModel.$title +// .receive(on: DispatchQueue.main) +// .sink { [weak self] title in +// guard let self = self else { return } +// self.title = title +// } +// .store(in: &disposeBag) +// self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) +// ThemeService.shared.currentTheme +// .receive(on: RunLoop.main) +// .sink { [weak self] theme in +// guard let self = self else { return } +// self.setupBackgroundColor(theme: theme) +// } +// .store(in: &disposeBag) +// navigationItem.leftBarButtonItem = cancelBarButtonItem +// navigationItem.rightBarButtonItem = publishBarButtonItem +// viewModel.traitCollectionDidChangePublisher +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let self = self else { return } +// guard self.traitCollection.userInterfaceIdiom == .pad else { return } +// var items = [self.publishBarButtonItem] +// if self.traitCollection.horizontalSizeClass == .regular { +// items.append(self.characterCountBarButtonItem) +// } +// self.navigationItem.rightBarButtonItems = items +// } +// .store(in: &disposeBag) +// publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) +// +// +// scrollView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(scrollView) +// NSLayoutConstraint.activate([ +// scrollView.topAnchor.constraint(equalTo: view.topAnchor), +// scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// +// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(composeToolbarView) +// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) +// NSLayoutConstraint.activate([ +// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// composeToolbarViewBottomLayoutConstraint, +// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), +// ]) +// composeToolbarView.preservesSuperviewLayoutMargins = true +// composeToolbarView.delegate = self +// +// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false +// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) +// NSLayoutConstraint.activate([ +// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), +// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), +// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), +// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), +// ]) // tableView.delegate = self // viewModel.setupDataSource( @@ -558,14 +578,14 @@ extension ComposeViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - viewModel.isViewAppeared = true +// viewModel.isViewAppeared = true } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) - configurePublishButtonApperance() - viewModel.traitCollectionDidChangePublisher.send() +// configurePublishButtonApperance() +// viewModel.traitCollectionDidChangePublisher.send() } override func viewDidLayoutSubviews() { @@ -576,508 +596,508 @@ extension ComposeViewController { private func updateAutoCompleteViewControllerLayout() { // pin autoCompleteViewController frame to current view - if let containerView = autoCompleteViewController.view.superview { - let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view) - if viewFrameInWindow.origin.x != 0 { - autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x - } - autoCompleteViewController.view.frame.size.width = view.frame.width - } - } - -} - -extension ComposeViewController { - - private var textEditorView: MetaText { - return viewModel.composeStatusContentTableViewCell.metaText - } - - private func markTextEditorViewBecomeFirstResponser() { - textEditorView.textView.becomeFirstResponder() - } - - private func contentWarningEditorTextView() -> UITextView? { - viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView - } - - private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? { - guard case .pollOption = item else { return nil } - guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } - guard let indexPath = dataSource.indexPath(for: item), - let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else { - return nil - } - - return cell - } - - private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { - guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } - let items = dataSource.snapshot().itemIdentifiers(inSection: .main) - let firstPollItem = items.first { item -> Bool in - guard case .pollOption = item else { return false } - return true - } - - guard let item = firstPollItem else { - return nil - } - - return pollOptionCollectionViewCell(of: item) - } - - private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { - guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } - let items = dataSource.snapshot().itemIdentifiers(inSection: .main) - let lastPollItem = items.last { item -> Bool in - guard case .pollOption = item else { return false } - return true - } - - guard let item = lastPollItem else { - return nil - } - - return pollOptionCollectionViewCell(of: item) - } - - private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() { - guard let cell = firstPollOptionCollectionViewCell() else { return } - cell.pollOptionView.optionTextField.becomeFirstResponder() - } - - private func markLastPollOptionCollectionViewCellBecomeFirstResponser() { - guard let cell = lastPollOptionCollectionViewCell() else { return } - cell.pollOptionView.optionTextField.becomeFirstResponder() - } - - private func showDismissConfirmAlertController() { - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in - guard let self = self else { return } - self.dismiss(animated: true, completion: nil) - } - alertController.addAction(discardAction) - let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) - alertController.addAction(cancelAction) - alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem - present(alertController, animated: true, completion: nil) - } - - private func resetImagePicker() { - let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.count) - let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) - photoLibraryPicker = createImagePicker(configuration: configuration) - } - - private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { - let imagePicker = PHPickerViewController(configuration: configuration) - imagePicker.delegate = self - return imagePicker - } - - private func setupBackgroundColor(theme: Theme) { - let backgroundColor = UIColor(dynamicProvider: { traitCollection in - switch traitCollection.userInterfaceStyle { - case .light: - return .systemBackground - default: - return theme.systemElevatedBackgroundColor - } - }) - view.backgroundColor = backgroundColor -// tableView.backgroundColor = backgroundColor -// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor - } - - // keyboard shortcutBar - private func setupInputAssistantItem(item: UITextInputAssistantItem) { - let barButtonItems = [ - composeToolbarView.mediaBarButtonItem, - composeToolbarView.pollBarButtonItem, - composeToolbarView.contentWarningBarButtonItem, - composeToolbarView.visibilityBarButtonItem, - ] - let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil) - - item.trailingBarButtonGroups = [group] - } - - private func configureToolbarDisplay(keyboardHasShortcutBar: Bool) { - switch self.traitCollection.userInterfaceIdiom { - case .pad: - let shouldHideToolbar = keyboardHasShortcutBar && self.traitCollection.horizontalSizeClass == .regular - self.composeToolbarView.alpha = shouldHideToolbar ? 0 : 1 - self.composeToolbarBackgroundView.alpha = shouldHideToolbar ? 0 : 1 - default: - break - } - } - - private func configureNavigationBarTitleStyle() { - switch traitCollection.userInterfaceIdiom { - case .pad: - navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular - default: - break - } - } - -} - -extension ComposeViewController { - - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - guard viewModel.shouldDismiss else { - showDismissConfirmAlertController() - return - } - dismiss(animated: true, completion: nil) - } - - @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - do { - try viewModel.checkAttachmentPrecondition() - } 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) - coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) - return - } - - guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { - // TODO: handle error - return - } - - // context.statusPublishService.publish(composeViewModel: viewModel) - assertionFailure() - - dismiss(animated: true, completion: nil) - } - -} - -// MARK: - MetaTextDelegate -extension ComposeViewController: MetaTextDelegate { - func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { - let string = metaText.textStorage.string - let content = MastodonContent( - content: string, - emojis: viewModel.customEmojiViewModel?.emojiMapping.value ?? [:] - ) - let metaContent = MastodonMetaContent.convert(text: content) - return metaContent - } -} - -// MARK: - UITextViewDelegate -extension ComposeViewController: UITextViewDelegate { - - func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - setupInputAssistantItem(item: textView.inputAssistantItem) - return true - } - - func textViewDidChange(_ textView: UITextView) { - switch textView { - case textEditorView.textView: - // update model - let metaText = self.textEditorView - let backedString = metaText.backedString - viewModel.composeStatusAttribute.composeContent = backedString - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)") - - // configure auto completion - setupAutoComplete(for: textView) - default: - assertionFailure() - } - } - - struct AutoCompleteInfo { - // model - let inputText: Substring - // range - let symbolRange: Range - let symbolString: Substring - let toCursorRange: Range - let toCursorString: Substring - let toHighlightEndRange: Range - let toHighlightEndString: Substring - // geometry - var textBoundingRect: CGRect = .zero - var symbolBoundingRect: CGRect = .zero - } - - private func setupAutoComplete(for textView: UITextView) { - guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else { - viewModel.autoCompleteInfo = nil - return - } - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString)) - - // get layout text bounding rect - var glyphRange = NSRange() - textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange) - let textContainer = textView.layoutManager.textContainers[0] - let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) - - let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes - guard textBoundingRect.size != .zero else { - viewModel.autoCompleteRetryLayoutTimes += 1 - // avoid infinite loop - guard retryLayoutTimes < 3 else { return } - // needs retry calculate layout when the rect position changing - DispatchQueue.main.async { - self.setupAutoComplete(for: textView) - } - return - } - viewModel.autoCompleteRetryLayoutTimes = 0 - - // get symbol bounding rect - textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange) - let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) - - // set bounding rect and trigger layout - autoCompletion.textBoundingRect = textBoundingRect - autoCompletion.symbolBoundingRect = symbolBoundingRect - viewModel.autoCompleteInfo = autoCompletion - } - - private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? { - guard let text = textView.text, - textView.selectedRange.location > 0, !text.isEmpty, - let selectedRange = Range(textView.selectedRange, in: text) else { - return nil - } - let cursorIndex = selectedRange.upperBound - let _highlightStartIndex: String.Index? = { - var index = text.index(before: cursorIndex) - while index > text.startIndex { - let char = text[index] - if char == "@" || char == "#" || char == ":" { - return index - } - index = text.index(before: index) - } - assert(index == text.startIndex) - let char = text[index] - if char == "@" || char == "#" || char == ":" { - return index - } else { - return nil - } - }() - - guard let highlightStartIndex = _highlightStartIndex else { return nil } - let scanRange = NSRange(highlightStartIndex..= cursorIndex else { return nil } - let symbolRange = highlightStartIndex.. Bool { - switch textView { - case textEditorView.textView: - return false - default: - return true - } - } - - func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { - switch textView { - case textEditorView.textView: - return false - default: - return true - } - } - -} - -// MARK: - ComposeToolbarViewDelegate -extension ComposeViewController: ComposeToolbarViewDelegate { - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, mediaButtonDidPressed sender: Any, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { - switch type { - case .photoLibrary: - present(photoLibraryPicker, animated: true, completion: nil) - case .camera: - present(imagePickerController, animated: true, completion: nil) - case .browse: - #if SNAPSHOT - guard let image = UIImage(named: "Athens") else { return } - - let attachmentService = MastodonAttachmentService( - context: context, - image: image, - initialAuthenticationBox: viewModel.authenticationBox - ) - viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] - #else - present(documentPickerController, animated: true, completion: nil) - #endif - } - } - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) { - // toggle poll composing state - viewModel.isPollComposing.toggle() - - // cancel custom picker input - viewModel.isCustomEmojiComposing = false - - // setup initial poll option if needs - if viewModel.isPollComposing, viewModel.pollOptionAttributes.isEmpty { - viewModel.pollOptionAttributes = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()] - } - - if viewModel.isPollComposing { - // Magic RunLoop - DispatchQueue.main.async { - self.markFirstPollOptionCollectionViewCellBecomeFirstResponser() - } - } else { - markTextEditorViewBecomeFirstResponser() - } - } - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: Any) { - viewModel.isCustomEmojiComposing.toggle() - } - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: Any) { - // cancel custom picker input - viewModel.isCustomEmojiComposing = false - - // restore first responder for text editor when content warning dismiss - if viewModel.isContentWarningComposing { - if contentWarningEditorTextView()?.isFirstResponder == true { - markTextEditorViewBecomeFirstResponser() - } - } - - // toggle composing status - viewModel.isContentWarningComposing.toggle() - - // active content warning after toggled - if viewModel.isContentWarningComposing { - contentWarningEditorTextView()?.becomeFirstResponder() - } - } - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: Any, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { - viewModel.selectedStatusVisibility = type - } - -} - -// MARK: - UIScrollViewDelegate -extension ComposeViewController { -// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { -// guard scrollView === tableView else { return } -// -// let repliedToCellFrame = viewModel.repliedToCellFrame -// guard repliedToCellFrame != .zero else { return } -// -// // try to find some patterns: -// // print(""" -// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) -// // scrollView.contentOffset.y: \(scrollView.contentOffset.y) -// // scrollView.contentSize.height: \(scrollView.contentSize.height) -// // scrollView.frame: \(scrollView.frame) -// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) -// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) -// // """) -// -// switch viewModel.collectionViewState { -// case .fold: -// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) -// guard velocity.y < 0 else { return } -// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top -// if offsetY < -44 { -// tableView.contentInset.top = 0 -// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) -// viewModel.collectionViewState = .expand +// if let containerView = autoCompleteViewController.view.superview { +// let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view) +// if viewFrameInWindow.origin.x != 0 { +// autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x // } +// autoCompleteViewController.view.frame.size.width = view.frame.width +// } + } + +} + +//extension ComposeViewController { +// +// private var textEditorView: MetaText { +// return viewModel.composeStatusContentTableViewCell.metaText +// } +// +// private func markTextEditorViewBecomeFirstResponser() { +// textEditorView.textView.becomeFirstResponder() +// } +// +// private func contentWarningEditorTextView() -> UITextView? { +// viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView +// } +// +// private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? { +// guard case .pollOption = item else { return nil } +// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } +// guard let indexPath = dataSource.indexPath(for: item), +// let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else { +// return nil +// } // -// case .expand: -// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) -// guard velocity.y > 0 else { return } -// // check if top across -// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height +// return cell +// } +// +// private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { +// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } +// let items = dataSource.snapshot().itemIdentifiers(inSection: .main) +// let firstPollItem = items.first { item -> Bool in +// guard case .pollOption = item else { return false } +// return true +// } // -// // check if bottom bounce -// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) -// let bottomOffset = bottomOffsetY - scrollView.contentSize.height +// guard let item = firstPollItem else { +// return nil +// } // -// if topOffset > 44 { -// // do not interrupt user scrolling -// viewModel.collectionViewState = .fold -// } else if bottomOffset > 44 { -// tableView.contentInset.top = -repliedToCellFrame.height -// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) -// viewModel.collectionViewState = .fold +// return pollOptionCollectionViewCell(of: item) +// } +// +// private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? { +// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil } +// let items = dataSource.snapshot().itemIdentifiers(inSection: .main) +// let lastPollItem = items.last { item -> Bool in +// guard case .pollOption = item else { return false } +// return true +// } +// +// guard let item = lastPollItem else { +// return nil +// } +// +// return pollOptionCollectionViewCell(of: item) +// } +// +// private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() { +// guard let cell = firstPollOptionCollectionViewCell() else { return } +// cell.pollOptionView.optionTextField.becomeFirstResponder() +// } +// +// private func markLastPollOptionCollectionViewCellBecomeFirstResponser() { +// guard let cell = lastPollOptionCollectionViewCell() else { return } +// cell.pollOptionView.optionTextField.becomeFirstResponder() +// } +// +// private func showDismissConfirmAlertController() { +// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) +// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in +// guard let self = self else { return } +// self.dismiss(animated: true, completion: nil) +// } +// alertController.addAction(discardAction) +// let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel) +// alertController.addAction(cancelAction) +// alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem +// present(alertController, animated: true, completion: nil) +// } +// +// private func resetImagePicker() { +// let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.count) +// let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit) +// photoLibraryPicker = createImagePicker(configuration: configuration) +// } +// +// private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController { +// let imagePicker = PHPickerViewController(configuration: configuration) +// imagePicker.delegate = self +// return imagePicker +// } +// +// private func setupBackgroundColor(theme: Theme) { +// let backgroundColor = UIColor(dynamicProvider: { traitCollection in +// switch traitCollection.userInterfaceStyle { +// case .light: +// return .systemBackground +// default: +// return theme.systemElevatedBackgroundColor // } +// }) +// view.backgroundColor = backgroundColor +//// tableView.backgroundColor = backgroundColor +//// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor +// } +// +// // keyboard shortcutBar +// private func setupInputAssistantItem(item: UITextInputAssistantItem) { +// let barButtonItems = [ +// composeToolbarView.mediaBarButtonItem, +// composeToolbarView.pollBarButtonItem, +// composeToolbarView.contentWarningBarButtonItem, +// composeToolbarView.visibilityBarButtonItem, +// ] +// let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil) +// +// item.trailingBarButtonGroups = [group] +// } +// +// private func configureToolbarDisplay(keyboardHasShortcutBar: Bool) { +// switch self.traitCollection.userInterfaceIdiom { +// case .pad: +// let shouldHideToolbar = keyboardHasShortcutBar && self.traitCollection.horizontalSizeClass == .regular +// self.composeToolbarView.alpha = shouldHideToolbar ? 0 : 1 +// self.composeToolbarBackgroundView.alpha = shouldHideToolbar ? 0 : 1 +// default: +// break // } // } -} - -// MARK: - UITableViewDelegate -extension ComposeViewController: UITableViewDelegate { } - -// MARK: - UICollectionViewDelegate -extension ComposeViewController: UICollectionViewDelegate { - - func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) - - if collectionView === customEmojiPickerInputView.collectionView { - guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return } - let item = diffableDataSource.itemIdentifier(for: indexPath) - guard case let .emoji(attribute) = item else { return } - let emoji = attribute.emoji - - // make click sound - UIDevice.current.playInputClick() - - // retrieve active text input and insert emoji - // the trailing space is REQUIRED to make regex happy - _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ") - } else { - // do nothing - } - } -} +// +// private func configureNavigationBarTitleStyle() { +// switch traitCollection.userInterfaceIdiom { +// case .pad: +// navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular +// default: +// break +// } +// } +// +//} +// +//extension ComposeViewController { +// +// @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// guard viewModel.shouldDismiss else { +// showDismissConfirmAlertController() +// return +// } +// dismiss(animated: true, completion: nil) +// } +// +// @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// do { +// try viewModel.checkAttachmentPrecondition() +// } 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) +// coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil)) +// return +// } +// +// guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else { +// // TODO: handle error +// return +// } +// +// // context.statusPublishService.publish(composeViewModel: viewModel) +// assertionFailure() +// +// dismiss(animated: true, completion: nil) +// } +// +//} +// +//// MARK: - MetaTextDelegate +//extension ComposeViewController: MetaTextDelegate { +// func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? { +// let string = metaText.textStorage.string +// let content = MastodonContent( +// content: string, +// emojis: viewModel.customEmojiViewModel?.emojiMapping.value ?? [:] +// ) +// let metaContent = MastodonMetaContent.convert(text: content) +// return metaContent +// } +//} +// +//// MARK: - UITextViewDelegate +//extension ComposeViewController: UITextViewDelegate { +// +// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { +// setupInputAssistantItem(item: textView.inputAssistantItem) +// return true +// } +// +// func textViewDidChange(_ textView: UITextView) { +// switch textView { +// case textEditorView.textView: +// // update model +// let metaText = self.textEditorView +// let backedString = metaText.backedString +// viewModel.composeStatusAttribute.composeContent = backedString +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)") +// +// // configure auto completion +// setupAutoComplete(for: textView) +// default: +// assertionFailure() +// } +// } +// +// struct AutoCompleteInfo { +// // model +// let inputText: Substring +// // range +// let symbolRange: Range +// let symbolString: Substring +// let toCursorRange: Range +// let toCursorString: Substring +// let toHighlightEndRange: Range +// let toHighlightEndString: Substring +// // geometry +// var textBoundingRect: CGRect = .zero +// var symbolBoundingRect: CGRect = .zero +// } +// +// private func setupAutoComplete(for textView: UITextView) { +// guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else { +// viewModel.autoCompleteInfo = nil +// return +// } +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString)) +// +// // get layout text bounding rect +// var glyphRange = NSRange() +// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange) +// let textContainer = textView.layoutManager.textContainers[0] +// let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) +// +// let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes +// guard textBoundingRect.size != .zero else { +// viewModel.autoCompleteRetryLayoutTimes += 1 +// // avoid infinite loop +// guard retryLayoutTimes < 3 else { return } +// // needs retry calculate layout when the rect position changing +// DispatchQueue.main.async { +// self.setupAutoComplete(for: textView) +// } +// return +// } +// viewModel.autoCompleteRetryLayoutTimes = 0 +// +// // get symbol bounding rect +// textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange) +// let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) +// +// // set bounding rect and trigger layout +// autoCompletion.textBoundingRect = textBoundingRect +// autoCompletion.symbolBoundingRect = symbolBoundingRect +// viewModel.autoCompleteInfo = autoCompletion +// } +// +// private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? { +// guard let text = textView.text, +// textView.selectedRange.location > 0, !text.isEmpty, +// let selectedRange = Range(textView.selectedRange, in: text) else { +// return nil +// } +// let cursorIndex = selectedRange.upperBound +// let _highlightStartIndex: String.Index? = { +// var index = text.index(before: cursorIndex) +// while index > text.startIndex { +// let char = text[index] +// if char == "@" || char == "#" || char == ":" { +// return index +// } +// index = text.index(before: index) +// } +// assert(index == text.startIndex) +// let char = text[index] +// if char == "@" || char == "#" || char == ":" { +// return index +// } else { +// return nil +// } +// }() +// +// guard let highlightStartIndex = _highlightStartIndex else { return nil } +// let scanRange = NSRange(highlightStartIndex..= cursorIndex else { return nil } +// let symbolRange = highlightStartIndex.. Bool { +// switch textView { +// case textEditorView.textView: +// return false +// default: +// return true +// } +// } +// +// func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { +// switch textView { +// case textEditorView.textView: +// return false +// default: +// return true +// } +// } +// +//} +// +//// MARK: - ComposeToolbarViewDelegate +//extension ComposeViewController: ComposeToolbarViewDelegate { +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, mediaButtonDidPressed sender: Any, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) { +// switch type { +// case .photoLibrary: +// present(photoLibraryPicker, animated: true, completion: nil) +// case .camera: +// present(imagePickerController, animated: true, completion: nil) +// case .browse: +// #if SNAPSHOT +// guard let image = UIImage(named: "Athens") else { return } +// +// let attachmentService = MastodonAttachmentService( +// context: context, +// image: image, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] +// #else +// present(documentPickerController, animated: true, completion: nil) +// #endif +// } +// } +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) { +// // toggle poll composing state +// viewModel.isPollComposing.toggle() +// +// // cancel custom picker input +// viewModel.isCustomEmojiComposing = false +// +// // setup initial poll option if needs +// if viewModel.isPollComposing, viewModel.pollOptionAttributes.isEmpty { +// viewModel.pollOptionAttributes = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()] +// } +// +// if viewModel.isPollComposing { +// // Magic RunLoop +// DispatchQueue.main.async { +// self.markFirstPollOptionCollectionViewCellBecomeFirstResponser() +// } +// } else { +// markTextEditorViewBecomeFirstResponser() +// } +// } +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: Any) { +// viewModel.isCustomEmojiComposing.toggle() +// } +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: Any) { +// // cancel custom picker input +// viewModel.isCustomEmojiComposing = false +// +// // restore first responder for text editor when content warning dismiss +// if viewModel.isContentWarningComposing { +// if contentWarningEditorTextView()?.isFirstResponder == true { +// markTextEditorViewBecomeFirstResponser() +// } +// } +// +// // toggle composing status +// viewModel.isContentWarningComposing.toggle() +// +// // active content warning after toggled +// if viewModel.isContentWarningComposing { +// contentWarningEditorTextView()?.becomeFirstResponder() +// } +// } +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: Any, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { +// viewModel.selectedStatusVisibility = type +// } +// +//} +// +//// MARK: - UIScrollViewDelegate +//extension ComposeViewController { +//// func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { +//// guard scrollView === tableView else { return } +//// +//// let repliedToCellFrame = viewModel.repliedToCellFrame +//// guard repliedToCellFrame != .zero else { return } +//// +//// // try to find some patterns: +//// // print(""" +//// // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height) +//// // scrollView.contentOffset.y: \(scrollView.contentOffset.y) +//// // scrollView.contentSize.height: \(scrollView.contentSize.height) +//// // scrollView.frame: \(scrollView.frame) +//// // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top) +//// // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom) +//// // """) +//// +//// switch viewModel.collectionViewState { +//// case .fold: +//// os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function) +//// guard velocity.y < 0 else { return } +//// let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top +//// if offsetY < -44 { +//// tableView.contentInset.top = 0 +//// targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top) +//// viewModel.collectionViewState = .expand +//// } +//// +//// case .expand: +//// os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function) +//// guard velocity.y > 0 else { return } +//// // check if top across +//// let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height +//// +//// // check if bottom bounce +//// let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom) +//// let bottomOffset = bottomOffsetY - scrollView.contentSize.height +//// +//// if topOffset > 44 { +//// // do not interrupt user scrolling +//// viewModel.collectionViewState = .fold +//// } else if bottomOffset > 44 { +//// tableView.contentInset.top = -repliedToCellFrame.height +//// targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height) +//// viewModel.collectionViewState = .fold +//// } +//// } +//// } +//} +// +//// MARK: - UITableViewDelegate +//extension ComposeViewController: UITableViewDelegate { } +// +//// MARK: - UICollectionViewDelegate +//extension ComposeViewController: UICollectionViewDelegate { +// +// func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription) +// +// if collectionView === customEmojiPickerInputView.collectionView { +// guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return } +// let item = diffableDataSource.itemIdentifier(for: indexPath) +// guard case let .emoji(attribute) = item else { return } +// let emoji = attribute.emoji +// +// // make click sound +// UIDevice.current.playInputClick() +// +// // retrieve active text input and insert emoji +// // the trailing space is REQUIRED to make regex happy +// _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ") +// } else { +// // do nothing +// } +// } +//} // MARK: - UIAdaptivePresentationControllerDelegate extension ComposeViewController: UIAdaptivePresentationControllerDelegate { @@ -1091,15 +1111,14 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } } - func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { - return viewModel.shouldDismiss - } +// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { +// return viewModel.shouldDismiss +// } - func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - showDismissConfirmAlertController() - - } +// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// showDismissConfirmAlertController() +// } func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) @@ -1107,357 +1126,357 @@ extension ComposeViewController: UIAdaptivePresentationControllerDelegate { } -// MARK: - PHPickerViewControllerDelegate -extension ComposeViewController: PHPickerViewControllerDelegate { - func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - picker.dismiss(animated: true, completion: nil) - - let attachmentServices: [MastodonAttachmentService] = results.map { result in - let service = MastodonAttachmentService( - context: context, - pickerResult: result, - initialAuthenticationBox: viewModel.authenticationBox - ) - return service - } - viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices - } -} - -// MARK: - UIImagePickerControllerDelegate -extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { - - func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { - picker.dismiss(animated: true, completion: nil) - - guard let image = info[.originalImage] as? UIImage else { return } - - let attachmentService = MastodonAttachmentService( - context: context, - image: image, - initialAuthenticationBox: viewModel.authenticationBox - ) - viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] - } - - func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { - os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - picker.dismiss(animated: true, completion: nil) - } -} - -// MARK: - UIDocumentPickerDelegate -extension ComposeViewController: UIDocumentPickerDelegate { - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let url = urls.first else { return } - - let attachmentService = MastodonAttachmentService( - context: context, - documentURL: url, - initialAuthenticationBox: viewModel.authenticationBox - ) - viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] - } -} - -// MARK: - ComposeStatusAttachmentTableViewCellDelegate -extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate { - - func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) { - guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return } - guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return } - guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } - guard case let .attachment(attachmentService) = item else { return } - - var attachmentServices = viewModel.attachmentServices - guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } - let removedItem = attachmentServices[index] - attachmentServices.remove(at: index) - viewModel.attachmentServices = attachmentServices - - // cancel task - removedItem.disposeBag.removeAll() - } - -} - -// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate -extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate { - - func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) { - - setupInputAssistantItem(item: textField.inputAssistantItem) - - // FIXME: make poll section visible - // DispatchQueue.main.async { - // self.collectionView.scroll(to: .bottom, animated: true) - // } - } - - - // handle delete backward event for poll option input - func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) { - guard (text ?? "").isEmpty else { return } - guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } - guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } - guard let item = dataSource.itemIdentifier(for: indexPath) else { return } - guard case let .pollOption(attribute) = item else { return } - - var pollAttributes = viewModel.pollOptionAttributes - guard let index = pollAttributes.firstIndex(of: attribute) else { return } - - // mark previous (fallback to next) item of removed middle poll option become first responder - let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main) - if let indexOfItem = pollItems.firstIndex(of: item), index > 0 { - func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? { - guard index > 0 else { return nil } - let indexBeforeRemoved = pollItems.index(before: indexOfItem) - let itemBeforeRemoved = pollItems[indexBeforeRemoved] - return pollOptionCollectionViewCell(of: itemBeforeRemoved) - } - - func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? { - guard index < pollItems.count - 1 else { return nil } - let indexAfterRemoved = pollItems.index(after: index) - let itemAfterRemoved = pollItems[indexAfterRemoved] - return pollOptionCollectionViewCell(of: itemAfterRemoved) - } - - var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved() - if cell == nil { - cell = cellAfterRemoved() - } - cell?.pollOptionView.optionTextField.becomeFirstResponder() - } - - guard pollAttributes.count > 2 else { - return - } - pollAttributes.remove(at: index) - - // update data source - viewModel.pollOptionAttributes = pollAttributes - } - - // handle keyboard return event for poll option input - func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) { - guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } - guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } - let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in - guard case .pollOption = item else { return false } - return true - } - guard let item = dataSource.itemIdentifier(for: indexPath) else { return } - guard let index = pollItems.firstIndex(of: item) else { return } - - if index == pollItems.count - 1 { - // is the last - viewModel.createNewPollOptionIfPossible() - DispatchQueue.main.async { - self.markLastPollOptionCollectionViewCellBecomeFirstResponser() - } - } else { - // not the last - let indexAfter = pollItems.index(after: index) - let itemAfter = pollItems[indexAfter] - let cell = pollOptionCollectionViewCell(of: itemAfter) - cell?.pollOptionView.optionTextField.becomeFirstResponder() - } - } - -} - -// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate -extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate { - func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) { - viewModel.createNewPollOptionIfPossible() - DispatchQueue.main.async { - self.markLastPollOptionCollectionViewCellBecomeFirstResponser() - } - } -} - -// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate -extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate { - func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) { - viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption - } -} - -// MARK: - ComposeStatusContentTableViewCellDelegate -extension ComposeViewController: ComposeStatusContentTableViewCellDelegate { - func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool { - setupInputAssistantItem(item: textView.inputAssistantItem) - return true - } -} - -// MARK: - AutoCompleteViewControllerDelegate -extension ComposeViewController: AutoCompleteViewControllerDelegate { - func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) { - guard let info = viewModel.autoCompleteInfo else { return } - let _replacedText: String? = { - var text: String - switch item { - case .hashtag(let hashtag): - text = "#" + hashtag.name - case .hashtagV1(let hashtagName): - text = "#" + hashtagName - case .account(let account): - text = "@" + account.acct - case .emoji(let emoji): - text = ":" + emoji.shortcode + ":" - case .bottomLoader: - return nil - } - return text - }() - guard let replacedText = _replacedText else { return } - guard let text = textEditorView.textView.text else { return } - - let range = NSRange(info.toHighlightEndRange, in: text) - textEditorView.textStorage.replaceCharacters(in: range, with: replacedText) - DispatchQueue.main.async { - self.textEditorView.textView.insertText(" ") // trigger textView delegate update - } - viewModel.autoCompleteInfo = nil - - switch item { - case .emoji, .bottomLoader: - break - default: - // set selected range except emoji - let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0) - guard textEditorView.textStorage.length <= newRange.location else { return } - textEditorView.textView.selectedRange = newRange - } - } -} - -extension ComposeViewController { - override var keyCommands: [UIKeyCommand]? { - composeKeyCommands - } -} - -extension ComposeViewController { - - enum ComposeKeyCommand: String, CaseIterable { - case discardPost - case publishPost - case mediaBrowse - case mediaPhotoLibrary - case mediaCamera - case togglePoll - case toggleContentWarning - case selectVisibilityPublic - // TODO: remove selectVisibilityUnlisted from codebase - // case selectVisibilityUnlisted - case selectVisibilityPrivate - case selectVisibilityDirect - - var title: String { - switch self { - case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost - case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost - case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse) - case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary) - case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera) - case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll - case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning - case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public) - // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) - case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private) - case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct) - } - } - - // UIKeyCommand input - var input: String { - switch self { - case .discardPost: return "w" // + command - case .publishPost: return "\r" // (enter) + command - case .mediaBrowse: return "b" // + option + command - case .mediaPhotoLibrary: return "p" // + option + command - case .mediaCamera: return "c" // + option + command - case .togglePoll: return "p" // + shift + command - case .toggleContentWarning: return "c" // + shift + command - case .selectVisibilityPublic: return "1" // + command - // case .selectVisibilityUnlisted: return "2" // + command - case .selectVisibilityPrivate: return "2" // + command - case .selectVisibilityDirect: return "3" // + command - } - } - - var modifierFlags: UIKeyModifierFlags { - switch self { - case .discardPost: return [.command] - case .publishPost: return [.command] - case .mediaBrowse: return [.alternate, .command] - case .mediaPhotoLibrary: return [.alternate, .command] - case .mediaCamera: return [.alternate, .command] - case .togglePoll: return [.shift, .command] - case .toggleContentWarning: return [.shift, .command] - case .selectVisibilityPublic: return [.command] - // case .selectVisibilityUnlisted: return [.command] - case .selectVisibilityPrivate: return [.command] - case .selectVisibilityDirect: return [.command] - } - } - - var propertyList: Any { - return rawValue - } - } - - var composeKeyCommands: [UIKeyCommand]? { - ComposeKeyCommand.allCases.map { command in - UIKeyCommand( - title: command.title, - image: nil, - action: #selector(Self.composeKeyCommandHandler(_:)), - input: command.input, - modifierFlags: command.modifierFlags, - propertyList: command.propertyList, - alternates: [], - discoverabilityTitle: nil, - attributes: [], - state: .off - ) - } - } - - @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) { - guard let rawValue = sender.propertyList as? String, - let command = ComposeKeyCommand(rawValue: rawValue) else { return } - - switch command { - case .discardPost: - cancelBarButtonItemPressed(cancelBarButtonItem) - case .publishPost: - publishBarButtonItemPressed(publishBarButtonItem) - case .mediaBrowse: - present(documentPickerController, animated: true, completion: nil) - case .mediaPhotoLibrary: - present(photoLibraryPicker, animated: true, completion: nil) - case .mediaCamera: - guard UIImagePickerController.isSourceTypeAvailable(.camera) else { - return - } - present(imagePickerController, animated: true, completion: nil) - case .togglePoll: - composeToolbarView.pollButton.sendActions(for: .touchUpInside) - case .toggleContentWarning: - composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside) - case .selectVisibilityPublic: - viewModel.selectedStatusVisibility = .public - // case .selectVisibilityUnlisted: - // viewModel.selectedStatusVisibility.value = .unlisted - case .selectVisibilityPrivate: - viewModel.selectedStatusVisibility = .private - case .selectVisibilityDirect: - viewModel.selectedStatusVisibility = .direct - } - } - -} +//// MARK: - PHPickerViewControllerDelegate +//extension ComposeViewController: PHPickerViewControllerDelegate { +// func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { +// picker.dismiss(animated: true, completion: nil) +// +// let attachmentServices: [MastodonAttachmentService] = results.map { result in +// let service = MastodonAttachmentService( +// context: context, +// pickerResult: result, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// return service +// } +// viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices +// } +//} +// +//// MARK: - UIImagePickerControllerDelegate +//extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate { +// +// func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { +// picker.dismiss(animated: true, completion: nil) +// +// guard let image = info[.originalImage] as? UIImage else { return } +// +// let attachmentService = MastodonAttachmentService( +// context: context, +// image: image, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] +// } +// +// func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { +// os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// picker.dismiss(animated: true, completion: nil) +// } +//} +// +//// MARK: - UIDocumentPickerDelegate +//extension ComposeViewController: UIDocumentPickerDelegate { +// func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { +// guard let url = urls.first else { return } +// +// let attachmentService = MastodonAttachmentService( +// context: context, +// documentURL: url, +// initialAuthenticationBox: viewModel.authenticationBox +// ) +// viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService] +// } +//} +// +//// MARK: - ComposeStatusAttachmentTableViewCellDelegate +//extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate { +// +// func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) { +// guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return } +// guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return } +// guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return } +// guard case let .attachment(attachmentService) = item else { return } +// +// var attachmentServices = viewModel.attachmentServices +// guard let index = attachmentServices.firstIndex(of: attachmentService) else { return } +// let removedItem = attachmentServices[index] +// attachmentServices.remove(at: index) +// viewModel.attachmentServices = attachmentServices +// +// // cancel task +// removedItem.disposeBag.removeAll() +// } +// +//} +// +//// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate +//extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate { +// +// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) { +// +// setupInputAssistantItem(item: textField.inputAssistantItem) +// +// // FIXME: make poll section visible +// // DispatchQueue.main.async { +// // self.collectionView.scroll(to: .bottom, animated: true) +// // } +// } +// +// +// // handle delete backward event for poll option input +// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) { +// guard (text ?? "").isEmpty else { return } +// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } +// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } +// guard let item = dataSource.itemIdentifier(for: indexPath) else { return } +// guard case let .pollOption(attribute) = item else { return } +// +// var pollAttributes = viewModel.pollOptionAttributes +// guard let index = pollAttributes.firstIndex(of: attribute) else { return } +// +// // mark previous (fallback to next) item of removed middle poll option become first responder +// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main) +// if let indexOfItem = pollItems.firstIndex(of: item), index > 0 { +// func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? { +// guard index > 0 else { return nil } +// let indexBeforeRemoved = pollItems.index(before: indexOfItem) +// let itemBeforeRemoved = pollItems[indexBeforeRemoved] +// return pollOptionCollectionViewCell(of: itemBeforeRemoved) +// } +// +// func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? { +// guard index < pollItems.count - 1 else { return nil } +// let indexAfterRemoved = pollItems.index(after: index) +// let itemAfterRemoved = pollItems[indexAfterRemoved] +// return pollOptionCollectionViewCell(of: itemAfterRemoved) +// } +// +// var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved() +// if cell == nil { +// cell = cellAfterRemoved() +// } +// cell?.pollOptionView.optionTextField.becomeFirstResponder() +// } +// +// guard pollAttributes.count > 2 else { +// return +// } +// pollAttributes.remove(at: index) +// +// // update data source +// viewModel.pollOptionAttributes = pollAttributes +// } +// +// // handle keyboard return event for poll option input +// func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) { +// guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return } +// guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return } +// let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in +// guard case .pollOption = item else { return false } +// return true +// } +// guard let item = dataSource.itemIdentifier(for: indexPath) else { return } +// guard let index = pollItems.firstIndex(of: item) else { return } +// +// if index == pollItems.count - 1 { +// // is the last +// viewModel.createNewPollOptionIfPossible() +// DispatchQueue.main.async { +// self.markLastPollOptionCollectionViewCellBecomeFirstResponser() +// } +// } else { +// // not the last +// let indexAfter = pollItems.index(after: index) +// let itemAfter = pollItems[indexAfter] +// let cell = pollOptionCollectionViewCell(of: itemAfter) +// cell?.pollOptionView.optionTextField.becomeFirstResponder() +// } +// } +// +//} +// +//// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate +//extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate { +// func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) { +// viewModel.createNewPollOptionIfPossible() +// DispatchQueue.main.async { +// self.markLastPollOptionCollectionViewCellBecomeFirstResponser() +// } +// } +//} +// +//// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate +//extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate { +// func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) { +// viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption +// } +//} +// +//// MARK: - ComposeStatusContentTableViewCellDelegate +//extension ComposeViewController: ComposeStatusContentTableViewCellDelegate { +// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool { +// setupInputAssistantItem(item: textView.inputAssistantItem) +// return true +// } +//} +// +//// MARK: - AutoCompleteViewControllerDelegate +//extension ComposeViewController: AutoCompleteViewControllerDelegate { +// func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) { +// guard let info = viewModel.autoCompleteInfo else { return } +// let _replacedText: String? = { +// var text: String +// switch item { +// case .hashtag(let hashtag): +// text = "#" + hashtag.name +// case .hashtagV1(let hashtagName): +// text = "#" + hashtagName +// case .account(let account): +// text = "@" + account.acct +// case .emoji(let emoji): +// text = ":" + emoji.shortcode + ":" +// case .bottomLoader: +// return nil +// } +// return text +// }() +// guard let replacedText = _replacedText else { return } +// guard let text = textEditorView.textView.text else { return } +// +// let range = NSRange(info.toHighlightEndRange, in: text) +// textEditorView.textStorage.replaceCharacters(in: range, with: replacedText) +// DispatchQueue.main.async { +// self.textEditorView.textView.insertText(" ") // trigger textView delegate update +// } +// viewModel.autoCompleteInfo = nil +// +// switch item { +// case .emoji, .bottomLoader: +// break +// default: +// // set selected range except emoji +// let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0) +// guard textEditorView.textStorage.length <= newRange.location else { return } +// textEditorView.textView.selectedRange = newRange +// } +// } +//} +// +//extension ComposeViewController { +// override var keyCommands: [UIKeyCommand]? { +// composeKeyCommands +// } +//} +// +//extension ComposeViewController { +// +// enum ComposeKeyCommand: String, CaseIterable { +// case discardPost +// case publishPost +// case mediaBrowse +// case mediaPhotoLibrary +// case mediaCamera +// case togglePoll +// case toggleContentWarning +// case selectVisibilityPublic +// // TODO: remove selectVisibilityUnlisted from codebase +// // case selectVisibilityUnlisted +// case selectVisibilityPrivate +// case selectVisibilityDirect +// +// var title: String { +// switch self { +// case .discardPost: return L10n.Scene.Compose.Keyboard.discardPost +// case .publishPost: return L10n.Scene.Compose.Keyboard.publishPost +// case .mediaBrowse: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse) +// case .mediaPhotoLibrary: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary) +// case .mediaCamera: return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera) +// case .togglePoll: return L10n.Scene.Compose.Keyboard.togglePoll +// case .toggleContentWarning: return L10n.Scene.Compose.Keyboard.toggleContentWarning +// case .selectVisibilityPublic: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public) +// // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted) +// case .selectVisibilityPrivate: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private) +// case .selectVisibilityDirect: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct) +// } +// } +// +// // UIKeyCommand input +// var input: String { +// switch self { +// case .discardPost: return "w" // + command +// case .publishPost: return "\r" // (enter) + command +// case .mediaBrowse: return "b" // + option + command +// case .mediaPhotoLibrary: return "p" // + option + command +// case .mediaCamera: return "c" // + option + command +// case .togglePoll: return "p" // + shift + command +// case .toggleContentWarning: return "c" // + shift + command +// case .selectVisibilityPublic: return "1" // + command +// // case .selectVisibilityUnlisted: return "2" // + command +// case .selectVisibilityPrivate: return "2" // + command +// case .selectVisibilityDirect: return "3" // + command +// } +// } +// +// var modifierFlags: UIKeyModifierFlags { +// switch self { +// case .discardPost: return [.command] +// case .publishPost: return [.command] +// case .mediaBrowse: return [.alternate, .command] +// case .mediaPhotoLibrary: return [.alternate, .command] +// case .mediaCamera: return [.alternate, .command] +// case .togglePoll: return [.shift, .command] +// case .toggleContentWarning: return [.shift, .command] +// case .selectVisibilityPublic: return [.command] +// // case .selectVisibilityUnlisted: return [.command] +// case .selectVisibilityPrivate: return [.command] +// case .selectVisibilityDirect: return [.command] +// } +// } +// +// var propertyList: Any { +// return rawValue +// } +// } +// +// var composeKeyCommands: [UIKeyCommand]? { +// ComposeKeyCommand.allCases.map { command in +// UIKeyCommand( +// title: command.title, +// image: nil, +// action: #selector(Self.composeKeyCommandHandler(_:)), +// input: command.input, +// modifierFlags: command.modifierFlags, +// propertyList: command.propertyList, +// alternates: [], +// discoverabilityTitle: nil, +// attributes: [], +// state: .off +// ) +// } +// } +// +// @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) { +// guard let rawValue = sender.propertyList as? String, +// let command = ComposeKeyCommand(rawValue: rawValue) else { return } +// +// switch command { +// case .discardPost: +// cancelBarButtonItemPressed(cancelBarButtonItem) +// case .publishPost: +// publishBarButtonItemPressed(publishBarButtonItem) +// case .mediaBrowse: +// present(documentPickerController, animated: true, completion: nil) +// case .mediaPhotoLibrary: +// present(photoLibraryPicker, animated: true, completion: nil) +// case .mediaCamera: +// guard UIImagePickerController.isSourceTypeAvailable(.camera) else { +// return +// } +// present(imagePickerController, animated: true, completion: nil) +// case .togglePoll: +// composeToolbarView.pollButton.sendActions(for: .touchUpInside) +// case .toggleContentWarning: +// composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside) +// case .selectVisibilityPublic: +// viewModel.selectedStatusVisibility = .public +// // case .selectVisibilityUnlisted: +// // viewModel.selectedStatusVisibility.value = .unlisted +// case .selectVisibilityPrivate: +// viewModel.selectedStatusVisibility = .private +// case .selectVisibilityDirect: +// viewModel.selectedStatusVisibility = .direct +// } +// } +// +//} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift index f8694376a..b3d8f52dc 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+DataSource.swift @@ -18,497 +18,473 @@ import MastodonSDK extension ComposeViewModel { - func setupDataSource( - tableView: UITableView, - metaTextDelegate: MetaTextDelegate, - metaTextViewDelegate: UITextViewDelegate, - customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, - composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, - composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, - composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, - composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate - ) { - // UI - bind() - - // content - bind(cell: composeStatusContentTableViewCell, tableView: tableView) - composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate - composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate - - // attachment - bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView) - composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate - - // poll - bind(cell: composeStatusPollTableViewCell, tableView: tableView) - composeStatusPollTableViewCell.delegate = self - composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel - composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate - composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate - composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate - - // setup data source - tableView.dataSource = self - } - - func setupCustomEmojiPickerDiffableDataSource( - for collectionView: UICollectionView, - dependency: NeedsDependency - ) { - let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource( - for: collectionView, - dependency: dependency - ) - self.customEmojiPickerDiffableDataSource = diffableDataSource - - let _domain = customEmojiViewModel?.domain - customEmojiViewModel?.emojis - .receive(on: DispatchQueue.main) - .sink { [weak self, weak diffableDataSource] emojis in - guard let _ = self else { return } - guard let diffableDataSource = diffableDataSource else { return } - - var snapshot = NSDiffableDataSourceSnapshot() - let domain = _domain?.uppercased() ?? " " - let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain) - snapshot.appendSections([customEmojiSection]) - let items: [CustomEmojiPickerItem] = { - var items = [CustomEmojiPickerItem]() - for emoji in emojis where emoji.visibleInPicker { - let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji) - let item = CustomEmojiPickerItem.emoji(attribute: attribute) - items.append(item) - } - return items - }() - snapshot.appendItems(items, toSection: customEmojiSection) - - diffableDataSource.apply(snapshot) - } - .store(in: &disposeBag) - } +// func setupDataSource( +// tableView: UITableView, +// metaTextDelegate: MetaTextDelegate, +// metaTextViewDelegate: UITextViewDelegate, +// customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel, +// composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate, +// composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate, +// composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate, +// composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate +// ) { +// // UI +// bind() +// +// // content +// bind(cell: composeStatusContentTableViewCell, tableView: tableView) +// composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate +// composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate +// +// // attachment +// bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView) +// composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate +// +// // poll +// bind(cell: composeStatusPollTableViewCell, tableView: tableView) +// composeStatusPollTableViewCell.delegate = self +// composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel +// composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate +// composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate +// composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate +// +// // setup data source +// tableView.dataSource = self +// } +// +// func setupCustomEmojiPickerDiffableDataSource( +// for collectionView: UICollectionView, +// dependency: NeedsDependency +// ) { +// let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource( +// for: collectionView, +// dependency: dependency +// ) +// self.customEmojiPickerDiffableDataSource = diffableDataSource +// +// let _domain = customEmojiViewModel?.domain +// customEmojiViewModel?.emojis +// .receive(on: DispatchQueue.main) +// .sink { [weak self, weak diffableDataSource] emojis in +// guard let _ = self else { return } +// guard let diffableDataSource = diffableDataSource else { return } +// +// var snapshot = NSDiffableDataSourceSnapshot() +// let domain = _domain?.uppercased() ?? " " +// let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain) +// snapshot.appendSections([customEmojiSection]) +// let items: [CustomEmojiPickerItem] = { +// var items = [CustomEmojiPickerItem]() +// for emoji in emojis where emoji.visibleInPicker { +// let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji) +// let item = CustomEmojiPickerItem.emoji(attribute: attribute) +// items.append(item) +// } +// return items +// }() +// snapshot.appendItems(items, toSection: customEmojiSection) +// +// diffableDataSource.apply(snapshot) +// } +// .store(in: &disposeBag) +// } } -// MARK: - UITableViewDataSource -extension ComposeViewModel: UITableViewDataSource { +//// MARK: - UITableViewDataSource +//extension ComposeViewModel: UITableViewDataSource { - enum Section: CaseIterable { - case repliedTo - case status - case attachment - case poll - } +// func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { +// switch Section.allCases[indexPath.section] { +// case .repliedTo: +// let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell +// guard case let .reply(record) = composeKind else { return cell } +// +// // bind frame publisher +// cell.framePublisher +// .receive(on: DispatchQueue.main) +// .assign(to: \.repliedToCellFrame, on: self) +// .store(in: &cell.disposeBag) +// +// // set initial width +// if cell.statusView.frame.width == .zero { +// cell.statusView.frame.size.width = tableView.frame.width +// } +// +// // configure status +// context.managedObjectContext.performAndWait { +// guard let replyTo = record.object(in: context.managedObjectContext) else { return } +// cell.statusView.configure(status: replyTo) +// } +// +// return cell +// case .status: +// return composeStatusContentTableViewCell +// case .attachment: +// return composeStatusAttachmentTableViewCell +// case .poll: +// return composeStatusPollTableViewCell +// } +// } +//} - func numberOfSections(in tableView: UITableView) -> Int { - return Section.allCases.count - } - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - switch Section.allCases[section] { - case .repliedTo: - switch composeKind { - case .reply: return 1 - default: return 0 - } - case .status: return 1 - case .attachment: return 1 - case .poll: return 1 - } - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - switch Section.allCases[indexPath.section] { - case .repliedTo: - let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell - guard case let .reply(record) = composeKind else { return cell } - - // bind frame publisher - cell.framePublisher - .receive(on: DispatchQueue.main) - .assign(to: \.repliedToCellFrame, on: self) - .store(in: &cell.disposeBag) - - // set initial width - if cell.statusView.frame.width == .zero { - cell.statusView.frame.size.width = tableView.frame.width - } - - // configure status - context.managedObjectContext.performAndWait { - guard let replyTo = record.object(in: context.managedObjectContext) else { return } - cell.statusView.configure(status: replyTo) - } - - return cell - case .status: - return composeStatusContentTableViewCell - case .attachment: - return composeStatusAttachmentTableViewCell - case .poll: - return composeStatusPollTableViewCell - } - } -} - -// MARK: - ComposeStatusPollTableViewCellDelegate -extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate { - func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - - self.pollOptionAttributes = options - } -} - -extension ComposeViewModel { - private func bind() { - $isCustomEmojiComposing - .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing) - .store(in: &disposeBag) - - $isContentWarningComposing - .assign(to: \.isContentWarningComposing, on: composeStatusAttribute) - .store(in: &disposeBag) - - // bind compose toolbar UI state - Publishers.CombineLatest( - $isPollComposing, - $attachmentServices - ) - .receive(on: DispatchQueue.main) - .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in - guard let self = self else { return } - let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments - let shouldPollDisable = attachmentServices.count > 0 - - self.isMediaToolbarButtonEnabled = !shouldMediaDisable - self.isPollToolbarButtonEnabled = !shouldPollDisable - }) - .store(in: &disposeBag) - - // calculate `Idempotency-Key` - let content = Publishers.CombineLatest3( - composeStatusAttribute.$isContentWarningComposing, - composeStatusAttribute.$contentWarningContent, - composeStatusAttribute.$composeContent - ) - .map { isContentWarningComposing, contentWarningContent, composeContent -> String in - if isContentWarningComposing { - return contentWarningContent + (composeContent ?? "") - } else { - return composeContent ?? "" - } - } - let attachmentIDs = $attachmentServices.map { attachments -> String in - let attachmentIDs = attachments.compactMap { $0.attachment.value?.id } - return attachmentIDs.joined(separator: ",") - } - let pollOptionsAndDuration = Publishers.CombineLatest3( - $isPollComposing, - $pollOptionAttributes, - pollExpiresOptionAttribute.expiresOption - ) - .map { isPollComposing, pollOptionAttributes, expiresOption -> String in - guard isPollComposing else { - return "" - } - - let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",") - return pollOptions + expiresOption.rawValue - } - - Publishers.CombineLatest4( - content, - attachmentIDs, - pollOptionsAndDuration, - $selectedStatusVisibility - ) - .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in - var hasher = Hasher() - hasher.combine(content) - hasher.combine(attachmentIDs) - hasher.combine(pollOptionsAndDuration) - hasher.combine(selectedStatusVisibility.visibility.rawValue) - let hashValue = hasher.finalize() - return "\(hashValue)" - } - .assign(to: \.value, on: idempotencyKey) - .store(in: &disposeBag) - - // bind modal dismiss state - composeStatusAttribute.$composeContent - .receive(on: DispatchQueue.main) - .map { [weak self] content in - let content = content ?? "" - if content.isEmpty { - return true - } - // if preInsertedContent plus a space is equal to the content, simply dismiss the modal - if let preInsertedContent = self?.preInsertedContent { - return content == preInsertedContent - } - return false - } - .assign(to: &$shouldDismiss) - - // bind compose bar button item UI state - let isComposeContentEmpty = composeStatusAttribute.$composeContent - .map { ($0 ?? "").isEmpty } - let isComposeContentValid = $characterCount - .compactMap { [weak self] characterCount -> Bool in - guard let self = self else { return characterCount <= 500 } - return characterCount <= self.composeContentLimit - } - let isMediaEmpty = $attachmentServices - .map { $0.isEmpty } - let isMediaUploadAllSuccess = $attachmentServices - .map { services in - services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish } - } - let isPollAttributeAllValid = $pollOptionAttributes - .map { pollAttributes in - pollAttributes.allSatisfy { attribute -> Bool in - !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } - } - - let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( - isComposeContentEmpty, - isComposeContentValid, - isMediaEmpty, - isMediaUploadAllSuccess - ) - .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in - if isMediaEmpty { - return isComposeContentValid && !isComposeContentEmpty - } else { - return isComposeContentValid && isMediaUploadAllSuccess - } - } - .eraseToAnyPublisher() - - let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( - isComposeContentEmpty, - isComposeContentValid, - $isPollComposing, - isPollAttributeAllValid - ) - .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in - if isPollComposing { - return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid - } else { - return isComposeContentValid && !isComposeContentEmpty - } - } - .eraseToAnyPublisher() - - Publishers.CombineLatest( - isPublishBarButtonItemEnabledPrecondition1, - isPublishBarButtonItemEnabledPrecondition2 - ) - .map { $0 && $1 } - .assign(to: &$isPublishBarButtonItemEnabled) - } -} - -extension ComposeViewModel { - private func bind( - cell: ComposeStatusContentTableViewCell, - tableView: UITableView - ) { - // bind status content character count - Publishers.CombineLatest3( - composeStatusAttribute.$composeContent, - composeStatusAttribute.$isContentWarningComposing, - composeStatusAttribute.$contentWarningContent - ) - .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in - let composeContent = composeContent ?? "" - var count = composeContent.count - if isContentWarningComposing { - count += contentWarningContent.count - } - return count - } - .assign(to: &$characterCount) - - // bind content warning - composeStatusAttribute.$isContentWarningComposing - .receive(on: DispatchQueue.main) - .sink { [weak cell, weak tableView] isContentWarningComposing in - guard let cell = cell else { return } - guard let tableView = tableView else { return } - - // self size input cell - cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing - cell.statusContentWarningEditorView.alpha = 0 - UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { - cell.statusContentWarningEditorView.alpha = 1 - tableView.beginUpdates() - tableView.endUpdates() - } completion: { _ in - // do nothing - } - } - .store(in: &disposeBag) - - cell.contentWarningContent - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak tableView, weak self] text in - guard let self = self else { return } - // bind input data - self.composeStatusAttribute.contentWarningContent = text - - // self size input cell - guard let tableView = tableView else { return } - UIView.performWithoutAnimation { - tableView.beginUpdates() - tableView.endUpdates() - } - } - .store(in: &cell.disposeBag) - - // configure custom emoji picker - ComposeStatusSection.configureCustomEmojiPicker( - viewModel: customEmojiPickerInputViewModel, - customEmojiReplaceableTextInput: cell.metaText.textView, - disposeBag: &disposeBag - ) - ComposeStatusSection.configureCustomEmojiPicker( - viewModel: customEmojiPickerInputViewModel, - customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, - disposeBag: &disposeBag - ) - } -} - -extension ComposeViewModel { - private func bind( - cell: ComposeStatusPollTableViewCell, - tableView: UITableView - ) { - Publishers.CombineLatest( - $isPollComposing, - $pollOptionAttributes - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] isPollComposing, pollOptionAttributes in - guard let self = self else { return } - guard self.isViewAppeared else { return } - - let cell = self.composeStatusPollTableViewCell - guard let dataSource = cell.dataSource else { return } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - var items: [ComposeStatusPollItem] = [] - if isPollComposing { - for attribute in pollOptionAttributes { - items.append(.pollOption(attribute: attribute)) - } - if pollOptionAttributes.count < self.maxPollOptions { - items.append(.pollOptionAppendEntry) - } - items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)) - } - snapshot.appendItems(items, toSection: .main) - - tableView.performBatchUpdates { - if #available(iOS 15.0, *) { - dataSource.apply(snapshot, animatingDifferences: false) - } else { - dataSource.apply(snapshot, animatingDifferences: true) - } - } - } - .store(in: &disposeBag) - - // bind delegate - $pollOptionAttributes - .sink { [weak self] pollAttributes in - guard let self = self else { return } - pollAttributes.forEach { $0.delegate = self } - } - .store(in: &disposeBag) - } -} - -extension ComposeViewModel { - private func bind( - cell: ComposeStatusAttachmentTableViewCell, - tableView: UITableView - ) { - cell.collectionViewHeightDidUpdate - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - guard let _ = self else { return } - tableView.beginUpdates() - tableView.endUpdates() - } - .store(in: &disposeBag) - - $attachmentServices - .removeDuplicates() - .receive(on: DispatchQueue.main) - .sink { [weak self] attachmentServices in - guard let self = self else { return } - guard self.isViewAppeared else { return } - - let cell = self.composeStatusAttachmentTableViewCell - guard let dataSource = cell.dataSource else { return } - - var snapshot = NSDiffableDataSourceSnapshot() - snapshot.appendSections([.main]) - let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) } - snapshot.appendItems(items, toSection: .main) - - if #available(iOS 15.0, *) { - dataSource.applySnapshotUsingReloadData(snapshot) - } else { - dataSource.apply(snapshot, animatingDifferences: false) - } - } - .store(in: &disposeBag) - - // setup attribute updater - $attachmentServices - .receive(on: DispatchQueue.main) - .debounce(for: 0.3, scheduler: DispatchQueue.main) - .sink { attachmentServices in - // drive service upload state - // make image upload in the queue - for attachmentService in attachmentServices { - // skip when prefix N task when task finish OR fail OR uploading - guard let currentState = attachmentService.uploadStateMachine.currentState else { break } - if currentState is MastodonAttachmentService.UploadState.Fail { - continue - } - if currentState is MastodonAttachmentService.UploadState.Finish { - continue - } - if currentState is MastodonAttachmentService.UploadState.Processing { - continue - } - if currentState is MastodonAttachmentService.UploadState.Uploading { - break - } - // trigger uploading one by one - if currentState is MastodonAttachmentService.UploadState.Initial { - attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self) - break - } - } - } - .store(in: &disposeBag) - - // bind delegate - $attachmentServices - .sink { [weak self] attachmentServices in - guard let self = self else { return } - attachmentServices.forEach { $0.delegate = self } - } - .store(in: &disposeBag) - } -} +//// MARK: - ComposeStatusPollTableViewCellDelegate +//extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate { +// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// +// self.pollOptionAttributes = options +// } +//} +// +//extension ComposeViewModel { +// private func bind() { +// $isCustomEmojiComposing +// .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing) +// .store(in: &disposeBag) +// +// $isContentWarningComposing +// .assign(to: \.isContentWarningComposing, on: composeStatusAttribute) +// .store(in: &disposeBag) +// +// // bind compose toolbar UI state +// Publishers.CombineLatest( +// $isPollComposing, +// $attachmentServices +// ) +// .receive(on: DispatchQueue.main) +// .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in +// guard let self = self else { return } +// let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments +// let shouldPollDisable = attachmentServices.count > 0 +// +// self.isMediaToolbarButtonEnabled = !shouldMediaDisable +// self.isPollToolbarButtonEnabled = !shouldPollDisable +// }) +// .store(in: &disposeBag) +// +// // calculate `Idempotency-Key` +// let content = Publishers.CombineLatest3( +// composeStatusAttribute.$isContentWarningComposing, +// composeStatusAttribute.$contentWarningContent, +// composeStatusAttribute.$composeContent +// ) +// .map { isContentWarningComposing, contentWarningContent, composeContent -> String in +// if isContentWarningComposing { +// return contentWarningContent + (composeContent ?? "") +// } else { +// return composeContent ?? "" +// } +// } +// let attachmentIDs = $attachmentServices.map { attachments -> String in +// let attachmentIDs = attachments.compactMap { $0.attachment.value?.id } +// return attachmentIDs.joined(separator: ",") +// } +// let pollOptionsAndDuration = Publishers.CombineLatest3( +// $isPollComposing, +// $pollOptionAttributes, +// pollExpiresOptionAttribute.expiresOption +// ) +// .map { isPollComposing, pollOptionAttributes, expiresOption -> String in +// guard isPollComposing else { +// return "" +// } +// +// let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",") +// return pollOptions + expiresOption.rawValue +// } +// +// Publishers.CombineLatest4( +// content, +// attachmentIDs, +// pollOptionsAndDuration, +// $selectedStatusVisibility +// ) +// .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in +// var hasher = Hasher() +// hasher.combine(content) +// hasher.combine(attachmentIDs) +// hasher.combine(pollOptionsAndDuration) +// hasher.combine(selectedStatusVisibility.visibility.rawValue) +// let hashValue = hasher.finalize() +// return "\(hashValue)" +// } +// .assign(to: \.value, on: idempotencyKey) +// .store(in: &disposeBag) +// +// // bind modal dismiss state +// composeStatusAttribute.$composeContent +// .receive(on: DispatchQueue.main) +// .map { [weak self] content in +// let content = content ?? "" +// if content.isEmpty { +// return true +// } +// // if preInsertedContent plus a space is equal to the content, simply dismiss the modal +// if let preInsertedContent = self?.preInsertedContent { +// return content == preInsertedContent +// } +// return false +// } +// .assign(to: &$shouldDismiss) +// +// // bind compose bar button item UI state +// let isComposeContentEmpty = composeStatusAttribute.$composeContent +// .map { ($0 ?? "").isEmpty } +// let isComposeContentValid = $characterCount +// .compactMap { [weak self] characterCount -> Bool in +// guard let self = self else { return characterCount <= 500 } +// return characterCount <= self.composeContentLimit +// } +// let isMediaEmpty = $attachmentServices +// .map { $0.isEmpty } +// let isMediaUploadAllSuccess = $attachmentServices +// .map { services in +// services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish } +// } +// let isPollAttributeAllValid = $pollOptionAttributes +// .map { pollAttributes in +// pollAttributes.allSatisfy { attribute -> Bool in +// !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty +// } +// } +// +// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( +// isComposeContentEmpty, +// isComposeContentValid, +// isMediaEmpty, +// isMediaUploadAllSuccess +// ) +// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in +// if isMediaEmpty { +// return isComposeContentValid && !isComposeContentEmpty +// } else { +// return isComposeContentValid && isMediaUploadAllSuccess +// } +// } +// .eraseToAnyPublisher() +// +// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4( +// isComposeContentEmpty, +// isComposeContentValid, +// $isPollComposing, +// isPollAttributeAllValid +// ) +// .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in +// if isPollComposing { +// return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid +// } else { +// return isComposeContentValid && !isComposeContentEmpty +// } +// } +// .eraseToAnyPublisher() +// +// Publishers.CombineLatest( +// isPublishBarButtonItemEnabledPrecondition1, +// isPublishBarButtonItemEnabledPrecondition2 +// ) +// .map { $0 && $1 } +// .assign(to: &$isPublishBarButtonItemEnabled) +// } +//} +// +//extension ComposeViewModel { +// private func bind( +// cell: ComposeStatusContentTableViewCell, +// tableView: UITableView +// ) { +// // bind status content character count +// Publishers.CombineLatest3( +// composeStatusAttribute.$composeContent, +// composeStatusAttribute.$isContentWarningComposing, +// composeStatusAttribute.$contentWarningContent +// ) +// .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in +// let composeContent = composeContent ?? "" +// var count = composeContent.count +// if isContentWarningComposing { +// count += contentWarningContent.count +// } +// return count +// } +// .assign(to: &$characterCount) +// +// // bind content warning +// composeStatusAttribute.$isContentWarningComposing +// .receive(on: DispatchQueue.main) +// .sink { [weak cell, weak tableView] isContentWarningComposing in +// guard let cell = cell else { return } +// guard let tableView = tableView else { return } +// +// // self size input cell +// cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing +// cell.statusContentWarningEditorView.alpha = 0 +// UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) { +// cell.statusContentWarningEditorView.alpha = 1 +// tableView.beginUpdates() +// tableView.endUpdates() +// } completion: { _ in +// // do nothing +// } +// } +// .store(in: &disposeBag) +// +// cell.contentWarningContent +// .removeDuplicates() +// .receive(on: DispatchQueue.main) +// .sink { [weak tableView, weak self] text in +// guard let self = self else { return } +// // bind input data +// self.composeStatusAttribute.contentWarningContent = text +// +// // self size input cell +// guard let tableView = tableView else { return } +// UIView.performWithoutAnimation { +// tableView.beginUpdates() +// tableView.endUpdates() +// } +// } +// .store(in: &cell.disposeBag) +// +// // configure custom emoji picker +// ComposeStatusSection.configureCustomEmojiPicker( +// viewModel: customEmojiPickerInputViewModel, +// customEmojiReplaceableTextInput: cell.metaText.textView, +// disposeBag: &disposeBag +// ) +// ComposeStatusSection.configureCustomEmojiPicker( +// viewModel: customEmojiPickerInputViewModel, +// customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView, +// disposeBag: &disposeBag +// ) +// } +//} +// +//extension ComposeViewModel { +// private func bind( +// cell: ComposeStatusPollTableViewCell, +// tableView: UITableView +// ) { +// Publishers.CombineLatest( +// $isPollComposing, +// $pollOptionAttributes +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] isPollComposing, pollOptionAttributes in +// guard let self = self else { return } +// guard self.isViewAppeared else { return } +// +// let cell = self.composeStatusPollTableViewCell +// guard let dataSource = cell.dataSource else { return } +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// var items: [ComposeStatusPollItem] = [] +// if isPollComposing { +// for attribute in pollOptionAttributes { +// items.append(.pollOption(attribute: attribute)) +// } +// if pollOptionAttributes.count < self.maxPollOptions { +// items.append(.pollOptionAppendEntry) +// } +// items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute)) +// } +// snapshot.appendItems(items, toSection: .main) +// +// tableView.performBatchUpdates { +// if #available(iOS 15.0, *) { +// dataSource.apply(snapshot, animatingDifferences: false) +// } else { +// dataSource.apply(snapshot, animatingDifferences: true) +// } +// } +// } +// .store(in: &disposeBag) +// +// // bind delegate +// $pollOptionAttributes +// .sink { [weak self] pollAttributes in +// guard let self = self else { return } +// pollAttributes.forEach { $0.delegate = self } +// } +// .store(in: &disposeBag) +// } +//} +// +//extension ComposeViewModel { +// private func bind( +// cell: ComposeStatusAttachmentTableViewCell, +// tableView: UITableView +// ) { +// cell.collectionViewHeightDidUpdate +// .receive(on: DispatchQueue.main) +// .sink { [weak self] _ in +// guard let _ = self else { return } +// tableView.beginUpdates() +// tableView.endUpdates() +// } +// .store(in: &disposeBag) +// +// $attachmentServices +// .removeDuplicates() +// .receive(on: DispatchQueue.main) +// .sink { [weak self] attachmentServices in +// guard let self = self else { return } +// guard self.isViewAppeared else { return } +// +// let cell = self.composeStatusAttachmentTableViewCell +// guard let dataSource = cell.dataSource else { return } +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) } +// snapshot.appendItems(items, toSection: .main) +// +// if #available(iOS 15.0, *) { +// dataSource.applySnapshotUsingReloadData(snapshot) +// } else { +// dataSource.apply(snapshot, animatingDifferences: false) +// } +// } +// .store(in: &disposeBag) +// +// // setup attribute updater +// $attachmentServices +// .receive(on: DispatchQueue.main) +// .debounce(for: 0.3, scheduler: DispatchQueue.main) +// .sink { attachmentServices in +// // drive service upload state +// // make image upload in the queue +// for attachmentService in attachmentServices { +// // skip when prefix N task when task finish OR fail OR uploading +// guard let currentState = attachmentService.uploadStateMachine.currentState else { break } +// if currentState is MastodonAttachmentService.UploadState.Fail { +// continue +// } +// if currentState is MastodonAttachmentService.UploadState.Finish { +// continue +// } +// if currentState is MastodonAttachmentService.UploadState.Processing { +// continue +// } +// if currentState is MastodonAttachmentService.UploadState.Uploading { +// break +// } +// // trigger uploading one by one +// if currentState is MastodonAttachmentService.UploadState.Initial { +// attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self) +// break +// } +// } +// } +// .store(in: &disposeBag) +// +// // bind delegate +// $attachmentServices +// .sink { [weak self] attachmentServices in +// guard let self = self else { return } +// attachmentServices.forEach { $0.delegate = self } +// } +// .store(in: &disposeBag) +// } +//} diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift index 761391814..b9ed18c45 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift @@ -12,153 +12,153 @@ import CoreDataStack import GameplayKit import MastodonSDK -extension ComposeViewModel { - class PublishState: GKState { - weak var viewModel: ComposeViewModel? - - init(viewModel: ComposeViewModel) { - self.viewModel = viewModel - } - - override func didEnter(from previousState: GKState?) { - os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) - viewModel?.publishStateMachinePublisher.value = self - } - } -} +//extension ComposeViewModel { +// class PublishState: GKState { +// weak var viewModel: ComposeViewModel? +// +// init(viewModel: ComposeViewModel) { +// self.viewModel = viewModel +// } +// +// override func didEnter(from previousState: GKState?) { +// os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription) +// viewModel?.publishStateMachinePublisher.value = self +// } +// } +//} -extension ComposeViewModel.PublishState { - class Initial: ComposeViewModel.PublishState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Publishing.self - } - } - - class Publishing: ComposeViewModel.PublishState { - - var publishingSubscription: AnyCancellable? - - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Finish.self - } - - override func didEnter(from previousState: GKState?) { - super.didEnter(from: previousState) - guard let viewModel = viewModel, let stateMachine = stateMachine else { return } - - viewModel.updatePublishDate() - - let authenticationBox = viewModel.authenticationBox - let domain = authenticationBox.domain - let attachmentServices = viewModel.attachmentServices - let mediaIDs = attachmentServices.compactMap { attachmentService in - attachmentService.attachment.value?.id - } - let pollOptions: [String]? = { - guard viewModel.isPollComposing else { return nil } - return viewModel.pollOptionAttributes.map { attribute in attribute.option.value } - }() - let pollExpiresIn: Int? = { - guard viewModel.isPollComposing else { return nil } - return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds - }() - let inReplyToID: Mastodon.Entity.Status.ID? = { - guard case let .reply(status) = viewModel.composeKind else { return nil } - var id: Mastodon.Entity.Status.ID? - viewModel.context.managedObjectContext.performAndWait { - guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return } - id = replyTo.id - } - return id - }() - let sensitive: Bool = viewModel.isContentWarningComposing - let spoilerText: String? = { - let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines) - guard !text.isEmpty else { - return nil - } - return text - }() - let visibility = viewModel.selectedStatusVisibility.visibility - - let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { - var subscriptions: [AnyPublisher, Error>] = [] - for attachmentService in attachmentServices { - guard let attachmentID = attachmentService.attachment.value?.id else { continue } - let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" - guard !description.isEmpty else { continue } - let query = Mastodon.API.Media.UpdateMediaQuery( - file: nil, - thumbnail: nil, - description: description, - focus: nil - ) - let subscription = viewModel.context.apiService.updateMedia( - domain: domain, - attachmentID: attachmentID, - query: query, - mastodonAuthenticationBox: authenticationBox - ) - subscriptions.append(subscription) - } - return subscriptions - }() - - let idempotencyKey = viewModel.idempotencyKey.value - - publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions) - .collect() - .asyncMap { attachments -> Mastodon.Response.Content in - let query = Mastodon.API.Statuses.PublishStatusQuery( - status: viewModel.composeStatusAttribute.composeContent, - mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, - pollOptions: pollOptions, - pollExpiresIn: pollExpiresIn, - inReplyToID: inReplyToID, - sensitive: sensitive, - spoilerText: spoilerText, - visibility: visibility - ) - return try await viewModel.context.apiService.publishStatus( - domain: domain, - idempotencyKey: idempotencyKey, - query: query, - authenticationBox: authenticationBox - ) - } - .receive(on: DispatchQueue.main) - .sink { completion in - switch completion { - case .failure(let error): - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) - stateMachine.enter(Fail.self) - case .finished: - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function) - stateMachine.enter(Finish.self) - } - } receiveValue: { response in - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri) - } - } - } - - class Fail: ComposeViewModel.PublishState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - // allow discard publishing - return stateClass == Publishing.self || stateClass == Discard.self - } - } - - class Discard: ComposeViewModel.PublishState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false - } - } - - class Finish: ComposeViewModel.PublishState { - override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return false - } - } - -} +//extension ComposeViewModel.PublishState { +// class Initial: ComposeViewModel.PublishState { +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Publishing.self +// } +// } +// +// class Publishing: ComposeViewModel.PublishState { +// +// var publishingSubscription: AnyCancellable? +// +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return stateClass == Fail.self || stateClass == Finish.self +// } +// +// override func didEnter(from previousState: GKState?) { +// super.didEnter(from: previousState) +// guard let viewModel = viewModel, let stateMachine = stateMachine else { return } +// +// viewModel.updatePublishDate() +// +// let authenticationBox = viewModel.authenticationBox +// let domain = authenticationBox.domain +// let attachmentServices = viewModel.attachmentServices +// let mediaIDs = attachmentServices.compactMap { attachmentService in +// attachmentService.attachment.value?.id +// } +// let pollOptions: [String]? = { +// guard viewModel.isPollComposing else { return nil } +// return viewModel.pollOptionAttributes.map { attribute in attribute.option.value } +// }() +// let pollExpiresIn: Int? = { +// guard viewModel.isPollComposing else { return nil } +// return viewModel.pollExpiresOptionAttribute.expiresOption.value.seconds +// }() +// let inReplyToID: Mastodon.Entity.Status.ID? = { +// guard case let .reply(status) = viewModel.composeKind else { return nil } +// var id: Mastodon.Entity.Status.ID? +// viewModel.context.managedObjectContext.performAndWait { +// guard let replyTo = status.object(in: viewModel.context.managedObjectContext) else { return } +// id = replyTo.id +// } +// return id +// }() +// let sensitive: Bool = viewModel.isContentWarningComposing +// let spoilerText: String? = { +// let text = viewModel.composeStatusAttribute.contentWarningContent.trimmingCharacters(in: .whitespacesAndNewlines) +// guard !text.isEmpty else { +// return nil +// } +// return text +// }() +// let visibility = viewModel.selectedStatusVisibility.visibility +// +// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { +// var subscriptions: [AnyPublisher, Error>] = [] +// for attachmentService in attachmentServices { +// guard let attachmentID = attachmentService.attachment.value?.id else { continue } +// let description = attachmentService.description.value?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" +// guard !description.isEmpty else { continue } +// let query = Mastodon.API.Media.UpdateMediaQuery( +// file: nil, +// thumbnail: nil, +// description: description, +// focus: nil +// ) +// let subscription = viewModel.context.apiService.updateMedia( +// domain: domain, +// attachmentID: attachmentID, +// query: query, +// mastodonAuthenticationBox: authenticationBox +// ) +// subscriptions.append(subscription) +// } +// return subscriptions +// }() +// +// let idempotencyKey = viewModel.idempotencyKey.value +// +// publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions) +// .collect() +// .asyncMap { attachments -> Mastodon.Response.Content in +// let query = Mastodon.API.Statuses.PublishStatusQuery( +// status: viewModel.composeStatusAttribute.composeContent, +// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, +// pollOptions: pollOptions, +// pollExpiresIn: pollExpiresIn, +// inReplyToID: inReplyToID, +// sensitive: sensitive, +// spoilerText: spoilerText, +// visibility: visibility +// ) +// return try await viewModel.context.apiService.publishStatus( +// domain: domain, +// idempotencyKey: idempotencyKey, +// query: query, +// authenticationBox: authenticationBox +// ) +// } +// .receive(on: DispatchQueue.main) +// .sink { completion in +// switch completion { +// case .failure(let error): +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) +// stateMachine.enter(Fail.self) +// case .finished: +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: publish status success", ((#file as NSString).lastPathComponent), #line, #function) +// stateMachine.enter(Finish.self) +// } +// } receiveValue: { response in +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: status %s published: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.uri) +// } +// } +// } +// +// class Fail: ComposeViewModel.PublishState { +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// // allow discard publishing +// return stateClass == Publishing.self || stateClass == Discard.self +// } +// } +// +// class Discard: ComposeViewModel.PublishState { +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return false +// } +// } +// +// class Finish: ComposeViewModel.PublishState { +// override func isValidNextState(_ stateClass: AnyClass) -> Bool { +// return false +// } +// } +// +//} diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index de088c68b..6e9a50fcc 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -28,159 +28,159 @@ final class ComposeViewModel: NSObject { // input let context: AppContext - let composeKind: ComposeStatusSection.ComposeKind let authContext: AuthContext + let kind: ComposeContentViewModel.Kind - var authenticationBox: MastodonAuthenticationBox { - authContext.mastodonAuthenticationBox - } - - @Published var isPollComposing = false - @Published var isCustomEmojiComposing = false - @Published var isContentWarningComposing = false - - @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType - @Published var repliedToCellFrame: CGRect = .zero - @Published var autoCompleteRetryLayoutTimes = 0 - @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil +// var authenticationBox: MastodonAuthenticationBox { +// authContext.mastodonAuthenticationBox +// } +// +// @Published var isPollComposing = false +// @Published var isCustomEmojiComposing = false +// @Published var isContentWarningComposing = false +// +// @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType +// @Published var repliedToCellFrame: CGRect = .zero +// @Published var autoCompleteRetryLayoutTimes = 0 +// @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil - let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit - var isViewAppeared = false +// let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit +// var isViewAppeared = false // output - let instanceConfiguration: Mastodon.Entity.Instance.Configuration? - var composeContentLimit: Int { - guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 } - return max(1, maxCharacters) - } - var maxMediaAttachments: Int { - guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else { - return 4 - } - // FIXME: update timeline media preview UI - return min(4, max(1, maxMediaAttachments)) - // return max(1, maxMediaAttachments) - } - var maxPollOptions: Int { - guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 } - return max(2, maxOptions) - } - - let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute() - let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() - let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() - let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() - - // var dataSource: UITableViewDiffableDataSource? - var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource? - private(set) lazy var publishStateMachine: GKStateMachine = { - // exclude timeline middle fetcher state - let stateMachine = GKStateMachine(states: [ - PublishState.Initial(viewModel: self), - PublishState.Publishing(viewModel: self), - PublishState.Fail(viewModel: self), - PublishState.Discard(viewModel: self), - PublishState.Finish(viewModel: self), - ]) - stateMachine.enter(PublishState.Initial.self) - return stateMachine - }() - private(set) lazy var publishStateMachinePublisher = CurrentValueSubject(nil) - private(set) var publishDate = Date() // update it when enter Publishing state - - // TODO: group post material into Hashable class - var idempotencyKey = CurrentValueSubject(UUID().uuidString) - - // UI & UX - @Published var title: String - @Published var shouldDismiss = true - @Published var isPublishBarButtonItemEnabled = false - @Published var isMediaToolbarButtonEnabled = true - @Published var isPollToolbarButtonEnabled = true - @Published var characterCount = 0 - @Published var collectionViewState: CollectionViewState = .fold - - // for hashtag: "# " - // for mention: "@ " - var preInsertedContent: String? - - // custom emojis - let customEmojiViewModel: EmojiService.CustomEmojiViewModel? - let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() - @Published var isLoadingCustomEmoji = false - - // attachment - @Published var attachmentServices: [MastodonAttachmentService] = [] - - // polls - @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] - let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() +// let instanceConfiguration: Mastodon.Entity.Instance.Configuration? +// var composeContentLimit: Int { +// guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 } +// return max(1, maxCharacters) +// } +// var maxMediaAttachments: Int { +// guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else { +// return 4 +// } +// // FIXME: update timeline media preview UI +// return min(4, max(1, maxMediaAttachments)) +// // return max(1, maxMediaAttachments) +// } +// var maxPollOptions: Int { +// guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 } +// return max(2, maxOptions) +// } +// +// let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute() +// let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell() +// let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell() +// let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell() +// +// // var dataSource: UITableViewDiffableDataSource? +// var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource? +// private(set) lazy var publishStateMachine: GKStateMachine = { +// // exclude timeline middle fetcher state +// let stateMachine = GKStateMachine(states: [ +// PublishState.Initial(viewModel: self), +// PublishState.Publishing(viewModel: self), +// PublishState.Fail(viewModel: self), +// PublishState.Discard(viewModel: self), +// PublishState.Finish(viewModel: self), +// ]) +// stateMachine.enter(PublishState.Initial.self) +// return stateMachine +// }() +// private(set) lazy var publishStateMachinePublisher = CurrentValueSubject(nil) +// private(set) var publishDate = Date() // update it when enter Publishing state +// +// // TODO: group post material into Hashable class +// var idempotencyKey = CurrentValueSubject(UUID().uuidString) +// +// // UI & UX +// @Published var title: String +// @Published var shouldDismiss = true +// @Published var isPublishBarButtonItemEnabled = false +// @Published var isMediaToolbarButtonEnabled = true +// @Published var isPollToolbarButtonEnabled = true +// @Published var characterCount = 0 +// @Published var collectionViewState: CollectionViewState = .fold +// +// // for hashtag: "# " +// // for mention: "@ " +// var preInsertedContent: String? +// +// // custom emojis +// let customEmojiViewModel: EmojiService.CustomEmojiViewModel? +// let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel() +// @Published var isLoadingCustomEmoji = false +// +// // attachment +// @Published var attachmentServices: [MastodonAttachmentService] = [] +// +// // polls +// @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = [] +// let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute() init( context: AppContext, - composeKind: ComposeStatusSection.ComposeKind, - authContext: AuthContext + authContext: AuthContext, + kind: ComposeContentViewModel.Kind ) { self.context = context - self.composeKind = composeKind self.authContext = authContext + self.kind = kind - self.title = { - switch composeKind { - case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost - case .reply: return L10n.Scene.Compose.Title.newReply - } - }() - self.selectedStatusVisibility = { - // default private when user locked - var visibility: ComposeToolbarView.VisibilitySelectionType = { - guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user - else { - return .public - } - return author.locked ? .private : .public - }() - // set visibility for reply post - switch composeKind { - case .reply(let record): - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { - assertionFailure() - return - } - let repliedStatusVisibility = status.visibility - switch repliedStatusVisibility { - case .public, .unlisted: - // keep default - break - case .private: - visibility = .private - case .direct: - visibility = .direct - case ._other: - assertionFailure() - break - } - } - default: - break - } - return visibility - }() - // set limit - self.instanceConfiguration = { - var configuration: Mastodon.Entity.Instance.Configuration? = nil - context.managedObjectContext.performAndWait { - guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return } - configuration = authentication.instance?.configuration - } - return configuration - }() - self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain) - super.init() - // end init - - setup(cell: composeStatusContentTableViewCell) +// self.title = { +// switch composeKind { +// case .post, .hashtag, .mention: return L10n.Scene.Compose.Title.newPost +// case .reply: return L10n.Scene.Compose.Title.newReply +// } +// }() +// self.selectedStatusVisibility = { +// // default private when user locked +// var visibility: ComposeToolbarView.VisibilitySelectionType = { +// guard let author = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user +// else { +// return .public +// } +// return author.locked ? .private : .public +// }() +// // set visibility for reply post +// switch composeKind { +// case .reply(let record): +// context.managedObjectContext.performAndWait { +// guard let status = record.object(in: context.managedObjectContext) else { +// assertionFailure() +// return +// } +// let repliedStatusVisibility = status.visibility +// switch repliedStatusVisibility { +// case .public, .unlisted: +// // keep default +// break +// case .private: +// visibility = .private +// case .direct: +// visibility = .direct +// case ._other: +// assertionFailure() +// break +// } +// } +// default: +// break +// } +// return visibility +// }() +// // set limit +// self.instanceConfiguration = { +// var configuration: Mastodon.Entity.Instance.Configuration? = nil +// context.managedObjectContext.performAndWait { +// guard let authentication = authContext.mastodonAuthenticationBox.authenticationRecord.object(in: context.managedObjectContext) else { return } +// configuration = authentication.instance?.configuration +// } +// return configuration +// }() +// self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authContext.mastodonAuthenticationBox.domain) +// super.init() +// // end init +// +// setup(cell: composeStatusContentTableViewCell) } deinit { @@ -190,199 +190,192 @@ final class ComposeViewModel: NSObject { } extension ComposeViewModel { - enum CollectionViewState { - case fold // snap to input - case expand // snap to reply - } +// func createNewPollOptionIfPossible() { +// guard pollOptionAttributes.count < maxPollOptions else { return } +// +// let attribute = ComposeStatusPollItem.PollOptionAttribute() +// pollOptionAttributes = pollOptionAttributes + [attribute] +// } +// +// func updatePublishDate() { +// publishDate = Date() +// } } -extension ComposeViewModel { - func createNewPollOptionIfPossible() { - guard pollOptionAttributes.count < maxPollOptions else { return } - - let attribute = ComposeStatusPollItem.PollOptionAttribute() - pollOptionAttributes = pollOptionAttributes + [attribute] - } - - func updatePublishDate() { - publishDate = Date() - } -} - -extension ComposeViewModel { - - enum AttachmentPrecondition: Error, LocalizedError { - case videoAttachWithPhoto - case moreThanOneVideo - - var errorDescription: String? { - return L10n.Common.Alerts.PublishPostFailure.title - } - - var failureReason: String? { - switch self { - case .videoAttachWithPhoto: - return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto - case .moreThanOneVideo: - return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo - } - } - } - - // check exclusive limit: - // - up to 1 video - // - up to N photos - func checkAttachmentPrecondition() throws { - let attachmentServices = self.attachmentServices - guard !attachmentServices.isEmpty else { return } - var photoAttachmentServices: [MastodonAttachmentService] = [] - var videoAttachmentServices: [MastodonAttachmentService] = [] - attachmentServices.forEach { service in - guard let file = service.file.value else { - assertionFailure() - return - } - switch file { - case .jpeg, .png, .gif: - photoAttachmentServices.append(service) - case .other: - videoAttachmentServices.append(service) - } - } - - if !videoAttachmentServices.isEmpty { - guard videoAttachmentServices.count == 1 else { - throw AttachmentPrecondition.moreThanOneVideo - } - guard photoAttachmentServices.isEmpty else { - throw AttachmentPrecondition.videoAttachWithPhoto - } - } - } - -} - -// MARK: - MastodonAttachmentServiceDelegate -extension ComposeViewModel: MastodonAttachmentServiceDelegate { - func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { - // trigger new output event - attachmentServices = attachmentServices - } -} - -// MARK: - ComposePollAttributeDelegate -extension ComposeViewModel: ComposePollAttributeDelegate { - func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) { - // trigger update - pollOptionAttributes = pollOptionAttributes - } -} - -extension ComposeViewModel { - private func setup( - cell: ComposeStatusContentTableViewCell - ) { - setupStatusHeader(cell: cell) - setupStatusAuthor(cell: cell) - setupStatusContent(cell: cell) - } - - private func setupStatusHeader( - cell: ComposeStatusContentTableViewCell - ) { - // configure header - let managedObjectContext = context.managedObjectContext - managedObjectContext.performAndWait { - guard case let .reply(record) = self.composeKind, - let replyTo = record.object(in: managedObjectContext) - else { - cell.statusView.viewModel.header = .none - return - } - - let info: StatusView.ViewModel.Header.ReplyInfo - do { - let content = MastodonContent( - content: replyTo.author.displayNameWithFallback, - emojis: replyTo.author.emojis.asDictionary - ) - let metaContent = try MastodonMetaContent.convert(document: content) - info = .init(header: metaContent) - } catch { - let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback) - info = .init(header: metaContent) - } - cell.statusView.viewModel.header = .reply(info: info) - } - } - - private func setupStatusAuthor( - cell: ComposeStatusContentTableViewCell - ) { - self.context.managedObjectContext.performAndWait { - guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } - cell.statusView.configureAuthor(author: author) - } - } - - private func setupStatusContent( - cell: ComposeStatusContentTableViewCell - ) { - switch composeKind { - case .reply(let record): - context.managedObjectContext.performAndWait { - guard let status = record.object(in: context.managedObjectContext) else { return } - let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user - - var mentionAccts: [String] = [] - if author?.id != status.author.id { - mentionAccts.append("@" + status.author.acct) - } - let mentions = status.mentions - .filter { author?.id != $0.id } - for mention in mentions { - let acct = "@" + mention.acct - guard !mentionAccts.contains(acct) else { continue } - mentionAccts.append(acct) - } - for acct in mentionAccts { - UITextChecker.learnWord(acct) - } - if let spoilerText = status.spoilerText, !spoilerText.isEmpty { - self.isContentWarningComposing = true - self.composeStatusAttribute.contentWarningContent = spoilerText - } - - let initialComposeContent = mentionAccts.joined(separator: " ") - let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " - self.preInsertedContent = preInsertedContent - self.composeStatusAttribute.composeContent = preInsertedContent - } - case .hashtag(let hashtag): - let initialComposeContent = "#" + hashtag - UITextChecker.learnWord(initialComposeContent) - let preInsertedContent = initialComposeContent + " " - self.preInsertedContent = preInsertedContent - self.composeStatusAttribute.composeContent = preInsertedContent - case .mention(let record): - context.managedObjectContext.performAndWait { - guard let user = record.object(in: context.managedObjectContext) else { return } - let initialComposeContent = "@" + user.acct - UITextChecker.learnWord(initialComposeContent) - let preInsertedContent = initialComposeContent + " " - self.preInsertedContent = preInsertedContent - self.composeStatusAttribute.composeContent = preInsertedContent - } - case .post: - self.preInsertedContent = nil - } - - // configure content warning - if let composeContent = composeStatusAttribute.composeContent { - cell.metaText.textView.text = composeContent - } - - // configure content warning - cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent - } -} +//extension ComposeViewModel { +// +// enum AttachmentPrecondition: Error, LocalizedError { +// case videoAttachWithPhoto +// case moreThanOneVideo +// +// var errorDescription: String? { +// return L10n.Common.Alerts.PublishPostFailure.title +// } +// +// var failureReason: String? { +// switch self { +// case .videoAttachWithPhoto: +// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto +// case .moreThanOneVideo: +// return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo +// } +// } +// } +// +// // check exclusive limit: +// // - up to 1 video +// // - up to N photos +// func checkAttachmentPrecondition() throws { +// let attachmentServices = self.attachmentServices +// guard !attachmentServices.isEmpty else { return } +// var photoAttachmentServices: [MastodonAttachmentService] = [] +// var videoAttachmentServices: [MastodonAttachmentService] = [] +// attachmentServices.forEach { service in +// guard let file = service.file.value else { +// assertionFailure() +// return +// } +// switch file { +// case .jpeg, .png, .gif: +// photoAttachmentServices.append(service) +// case .other: +// videoAttachmentServices.append(service) +// } +// } +// +// if !videoAttachmentServices.isEmpty { +// guard videoAttachmentServices.count == 1 else { +// throw AttachmentPrecondition.moreThanOneVideo +// } +// guard photoAttachmentServices.isEmpty else { +// throw AttachmentPrecondition.videoAttachWithPhoto +// } +// } +// } +// +//} +// +//// MARK: - MastodonAttachmentServiceDelegate +//extension ComposeViewModel: MastodonAttachmentServiceDelegate { +// func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) { +// // trigger new output event +// attachmentServices = attachmentServices +// } +//} +// +//// MARK: - ComposePollAttributeDelegate +//extension ComposeViewModel: ComposePollAttributeDelegate { +// func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) { +// // trigger update +// pollOptionAttributes = pollOptionAttributes +// } +//} +// +//extension ComposeViewModel { +// private func setup( +// cell: ComposeStatusContentTableViewCell +// ) { +// setupStatusHeader(cell: cell) +// setupStatusAuthor(cell: cell) +// setupStatusContent(cell: cell) +// } +// +// private func setupStatusHeader( +// cell: ComposeStatusContentTableViewCell +// ) { +// // configure header +// let managedObjectContext = context.managedObjectContext +// managedObjectContext.performAndWait { +// guard case let .reply(record) = self.composeKind, +// let replyTo = record.object(in: managedObjectContext) +// else { +// cell.statusView.viewModel.header = .none +// return +// } +// +// let info: StatusView.ViewModel.Header.ReplyInfo +// do { +// let content = MastodonContent( +// content: replyTo.author.displayNameWithFallback, +// emojis: replyTo.author.emojis.asDictionary +// ) +// let metaContent = try MastodonMetaContent.convert(document: content) +// info = .init(header: metaContent) +// } catch { +// let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback) +// info = .init(header: metaContent) +// } +// cell.statusView.viewModel.header = .reply(info: info) +// } +// } +// +// private func setupStatusAuthor( +// cell: ComposeStatusContentTableViewCell +// ) { +// self.context.managedObjectContext.performAndWait { +// guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return } +// cell.statusView.configureAuthor(author: author) +// } +// } +// +// private func setupStatusContent( +// cell: ComposeStatusContentTableViewCell +// ) { +// switch composeKind { +// case .reply(let record): +// context.managedObjectContext.performAndWait { +// guard let status = record.object(in: context.managedObjectContext) else { return } +// let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user +// +// var mentionAccts: [String] = [] +// if author?.id != status.author.id { +// mentionAccts.append("@" + status.author.acct) +// } +// let mentions = status.mentions +// .filter { author?.id != $0.id } +// for mention in mentions { +// let acct = "@" + mention.acct +// guard !mentionAccts.contains(acct) else { continue } +// mentionAccts.append(acct) +// } +// for acct in mentionAccts { +// UITextChecker.learnWord(acct) +// } +// if let spoilerText = status.spoilerText, !spoilerText.isEmpty { +// self.isContentWarningComposing = true +// self.composeStatusAttribute.contentWarningContent = spoilerText +// } +// +// let initialComposeContent = mentionAccts.joined(separator: " ") +// let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " " +// self.preInsertedContent = preInsertedContent +// self.composeStatusAttribute.composeContent = preInsertedContent +// } +// case .hashtag(let hashtag): +// let initialComposeContent = "#" + hashtag +// UITextChecker.learnWord(initialComposeContent) +// let preInsertedContent = initialComposeContent + " " +// self.preInsertedContent = preInsertedContent +// self.composeStatusAttribute.composeContent = preInsertedContent +// case .mention(let record): +// context.managedObjectContext.performAndWait { +// guard let user = record.object(in: context.managedObjectContext) else { return } +// let initialComposeContent = "@" + user.acct +// UITextChecker.learnWord(initialComposeContent) +// let preInsertedContent = initialComposeContent + " " +// self.preInsertedContent = preInsertedContent +// self.composeStatusAttribute.composeContent = preInsertedContent +// } +// case .post: +// self.preInsertedContent = nil +// } +// +// // configure content warning +// if let composeContent = composeStatusAttribute.composeContent { +// cell.metaText.textView.text = composeContent +// } +// +// // configure content warning +// cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent +// } +//} diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift deleted file mode 100644 index 8ff4e7242..000000000 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusAttachmentTableViewCell.swift +++ /dev/null @@ -1,171 +0,0 @@ -// -// ComposeStatusAttachmentTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-29. -// - -import UIKit -import SwiftUI -import Combine -import AlamofireImage -import MastodonAsset -import MastodonLocalization -import UIHostingConfigurationBackport - -final class ComposeStatusAttachmentTableViewCell: UITableViewCell { - - private(set) var dataSource: UICollectionViewDiffableDataSource! - weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate? - var observations = Set() - - private static func createLayout() -> UICollectionViewLayout { - let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) - let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) - let section = NSCollectionLayoutSection(group: group) - section.contentInsetsReference = .readableContent - return UICollectionViewCompositionalLayout(section: section) - } - - private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! - let collectionView: UICollectionView = { - let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout() - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) - collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self)) - collectionView.backgroundColor = .clear - collectionView.alwaysBounceVertical = true - collectionView.isScrollEnabled = false - return collectionView - }() - let collectionViewHeightDidUpdate = PassthroughSubject() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusAttachmentTableViewCell { - - private func _init() { - backgroundColor = .clear - contentView.backgroundColor = .clear - - collectionView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(collectionView) - collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh) - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), - collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - collectionViewHeightLayoutConstraint, - ]) - - collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in - guard let self = self else { return } - self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height - self.collectionViewHeightDidUpdate.send() - } - .store(in: &observations) - - self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { - [weak self] collectionView, indexPath, item -> UICollectionViewCell? in - guard let _ = self else { return UICollectionViewCell() } - switch item { - case .attachment: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell - cell.contentConfiguration = UIHostingConfigurationBackport { - HStack { - Image(systemName: "star") - Text("Favorites") - Spacer() - } - } -// cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value -// cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate -// attachmentService.thumbnailImage -// .receive(on: DispatchQueue.main) -// .sink { [weak cell] thumbnailImage in -// guard let cell = cell else { return } -// let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) -// guard let image = thumbnailImage else { -// let placeholder = UIImage.placeholder( -// size: size, -// color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor -// ) -// .af.imageRounded( -// withCornerRadius: AttachmentContainerView.containerViewCornerRadius -// ) -// cell.attachmentContainerView.previewImageView.image = placeholder -// return -// } -// // cannot get correct size. set corner radius on layer -// cell.attachmentContainerView.previewImageView.image = image -// } -// .store(in: &cell.disposeBag) -// Publishers.CombineLatest( -// attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(), -// attachmentService.error.eraseToAnyPublisher() -// ) -// .receive(on: DispatchQueue.main) -// .sink { [weak cell, weak attachmentService] uploadState, error in -// guard let cell = cell else { return } -// guard let attachmentService = attachmentService else { return } -// cell.attachmentContainerView.emptyStateView.isHidden = error == nil -// cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil -// if let error = error { -// cell.attachmentContainerView.activityIndicatorView.stopAnimating() -// cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription -// } else { -// guard let uploadState = uploadState else { return } -// switch uploadState { -// case is MastodonAttachmentService.UploadState.Finish: -// cell.attachmentContainerView.activityIndicatorView.stopAnimating() -// case is MastodonAttachmentService.UploadState.Fail: -// cell.attachmentContainerView.activityIndicatorView.stopAnimating() -// // FIXME: not display -// cell.attachmentContainerView.emptyStateView.label.text = { -// if let file = attachmentService.file.value { -// switch file { -// case .jpeg, .png, .gif: -// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) -// case .other: -// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) -// } -// } else { -// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) -// } -// }() -// default: -// break -// } -// } -// } -// .store(in: &cell.disposeBag) -// NotificationCenter.default.publisher( -// for: UITextView.textDidChangeNotification, -// object: cell.attachmentContainerView.descriptionTextView -// ) -// .receive(on: DispatchQueue.main) -// .sink { notification in -// guard let textField = notification.object as? UITextView else { return } -// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) -// attachmentService.description.value = text -// } -// .store(in: &cell.disposeBag) - return cell - } - } - } - -} - diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift deleted file mode 100644 index 814d79c0e..000000000 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusContentTableViewCell.swift +++ /dev/null @@ -1,172 +0,0 @@ -// -// ComposeStatusContentTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-28. -// - -import os.log -import UIKit -import Combine -import MetaTextKit -import UITextView_Placeholder -import MastodonAsset -import MastodonLocalization -import MastodonUI - -protocol ComposeStatusContentTableViewCellDelegate: AnyObject { - func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool -} - -final class ComposeStatusContentTableViewCell: UITableViewCell { - - let logger = Logger(subsystem: "ComposeStatusContentTableViewCell", category: "View") - - var disposeBag = Set() - weak var delegate: ComposeStatusContentTableViewCellDelegate? - - let statusView = StatusView() - - let statusContentWarningEditorView = StatusContentWarningEditorView() - - let textEditorViewContainerView = UIView() - - static let metaTextViewTag: Int = 333 - let metaText: MetaText = { - let metaText = MetaText() - metaText.textView.backgroundColor = .clear - metaText.textView.isScrollEnabled = false - metaText.textView.keyboardType = .twitter - metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment - metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset - metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) - metaText.textView.attributedPlaceholder = { - var attributes = metaText.textAttributes - attributes[.foregroundColor] = Asset.Colors.Label.secondary.color - return NSAttributedString( - string: L10n.Scene.Compose.contentInputPlaceholder, - attributes: attributes - ) - }() - metaText.paragraphStyle = { - let style = NSMutableParagraphStyle() - style.lineSpacing = 5 - style.paragraphSpacing = 0 - return style - }() - metaText.textAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), - .foregroundColor: Asset.Colors.Label.primary.color, - ] - metaText.linkAttributes = [ - .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), - .foregroundColor: Asset.Colors.brand.color, - ] - return metaText - }() - - // output - let contentWarningContent = PassthroughSubject() - - override func prepareForReuse() { - super.prepareForReuse() - - metaText.delegate = nil - metaText.textView.delegate = nil - } - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusContentTableViewCell { - - private func _init() { - selectionStyle = .none - layer.zPosition = 999 - backgroundColor = .clear - preservesSuperviewLayoutMargins = true - - let containerStackView = UIStackView() - containerStackView.axis = .vertical - containerStackView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(containerStackView) - NSLayoutConstraint.activate([ - containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), - containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - ]) - containerStackView.preservesSuperviewLayoutMargins = true - - containerStackView.addArrangedSubview(statusContentWarningEditorView) - statusContentWarningEditorView.setContentHuggingPriority(.required - 1, for: .vertical) - - let statusContainerView = UIView() - statusContainerView.preservesSuperviewLayoutMargins = true - containerStackView.addArrangedSubview(statusContainerView) - statusView.translatesAutoresizingMaskIntoConstraints = false - statusContainerView.addSubview(statusView) - NSLayoutConstraint.activate([ - statusView.topAnchor.constraint(equalTo: statusContainerView.topAnchor, constant: 20), - statusView.leadingAnchor.constraint(equalTo: statusContainerView.leadingAnchor), - statusView.trailingAnchor.constraint(equalTo: statusContainerView.trailingAnchor), - statusView.bottomAnchor.constraint(equalTo: statusContainerView.bottomAnchor), - ]) - statusView.setup(style: .composeStatusAuthor) - - containerStackView.addArrangedSubview(textEditorViewContainerView) - metaText.textView.translatesAutoresizingMaskIntoConstraints = false - textEditorViewContainerView.addSubview(metaText.textView) - NSLayoutConstraint.activate([ - metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor), - metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.leadingAnchor), - metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.trailingAnchor), - metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor), - metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh), - ]) - statusContentWarningEditorView.textView.delegate = self - } - -} - -// MARK: - UITextViewDelegate -extension ComposeStatusContentTableViewCell: UITextViewDelegate { - - func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { - return delegate?.composeStatusContentTableViewCell(self, textViewShouldBeginEditing: textView) ?? true - } - - func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { - switch textView { - case statusContentWarningEditorView.textView: - // disable input line break - guard text != "\n" else { return false } - return true - default: - assertionFailure() - return true - } - } - - func textViewDidChange(_ textView: UITextView) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): text: \(textView.text ?? "")") - guard textView === statusContentWarningEditorView.textView else { return } - // replace line break with space - // needs check input state to prevent break the IME - if textView.markedTextRange == nil { - textView.text = textView.text.replacingOccurrences(of: "\n", with: " ") - } - contentWarningContent.send(textView.text) - } - -} - diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift b/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift deleted file mode 100644 index f33a35c3e..000000000 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeStatusPollTableViewCell.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// ComposeStatusPollTableViewCell.swift -// Mastodon -// -// Created by MainasuK Cirno on 2021-6-29. -// - -import os.log -import UIKit -import Combine -import MastodonAsset -import MastodonLocalization - -protocol ComposeStatusPollTableViewCellDelegate: AnyObject { - func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) -} - -final class ComposeStatusPollTableViewCell: UITableViewCell { - - let logger = Logger(subsystem: "ComposeStatusPollTableViewCell", category: "UI") - - private(set) var dataSource: UICollectionViewDiffableDataSource! - var observations = Set() - - weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel? - weak var delegate: ComposeStatusPollTableViewCellDelegate? - weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate? - weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? - weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? - - private static func createLayout() -> UICollectionViewLayout { - let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) - let item = NSCollectionLayoutItem(layoutSize: itemSize) - let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) - let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) - let section = NSCollectionLayoutSection(group: group) - section.contentInsetsReference = .readableContent - return UICollectionViewCompositionalLayout(section: section) - } - - private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! - let collectionView: UICollectionView = { - let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout() - let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) - collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self)) - collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self)) - collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) - collectionView.backgroundColor = .clear - collectionView.alwaysBounceVertical = true - collectionView.isScrollEnabled = false - collectionView.dragInteractionEnabled = true - return collectionView - }() - let collectionViewHeightDidUpdate = PassthroughSubject() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - _init() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - _init() - } - -} - -extension ComposeStatusPollTableViewCell { - - private func _init() { - backgroundColor = .clear - contentView.backgroundColor = .clear - - collectionView.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(collectionView) - collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh) - NSLayoutConstraint.activate([ - collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), - collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - collectionViewHeightLayoutConstraint, - ]) - - collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in - guard let self = self else { return } - self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height - self.collectionViewHeightDidUpdate.send() - } - .store(in: &observations) - - self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ - weak self - ] collectionView, indexPath, item -> UICollectionViewCell? in - guard let self = self else { return UICollectionViewCell() } - - switch item { - case .pollOption(let attribute): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell - cell.pollOptionView.optionTextField.text = attribute.option.value - cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1) - cell.pollOption - .receive(on: DispatchQueue.main) - .assign(to: \.value, on: attribute.option) - .store(in: &cell.disposeBag) - cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate - if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel { - ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) - } - return cell - case .pollOptionAppendEntry: - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell - cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate - return cell - case .pollExpiresOption(let attribute): - let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell - cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) - attribute.expiresOption - .receive(on: DispatchQueue.main) - .sink { [weak cell] expiresOption in - guard let cell = cell else { return } - cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) - } - .store(in: &cell.disposeBag) - cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate - return cell - } - } - - collectionView.dragDelegate = self - collectionView.dropDelegate = self - } - -} - -// MARK: - UICollectionViewDragDelegate -extension ComposeStatusPollTableViewCell: UICollectionViewDragDelegate { - - func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - guard let item = dataSource.itemIdentifier(for: indexPath) else { return [] } - switch item { - case .pollOption: - let itemProvider = NSItemProvider(object: String(item.hashValue) as NSString) - let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = item - return [dragItem] - default: - return [] - } - } - - func collectionView(_ collectionView: UICollectionView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool { - // drag to app should be the same app - return true - } -} - -// MARK: - UICollectionViewDropDelegate -extension ComposeStatusPollTableViewCell: UICollectionViewDropDelegate { - // didUpdate - func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { - guard collectionView.hasActiveDrag, - let destinationIndexPath = destinationIndexPath, - let item = dataSource.itemIdentifier(for: destinationIndexPath) - else { - return UICollectionViewDropProposal(operation: .forbidden) - } - - switch item { - case .pollOption: - return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) - default: - return UICollectionViewDropProposal(operation: .cancel) - } - } - - // performDrop - func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { - guard let dropItem = coordinator.items.first, - let item = dropItem.dragItem.localObject as? ComposeStatusPollItem, - case .pollOption = item - else { return } - - guard coordinator.proposal.operation == .move else { return } - guard let destinationIndexPath = coordinator.destinationIndexPath, - let _ = collectionView.cellForItem(at: destinationIndexPath) as? ComposeStatusPollOptionCollectionViewCell - else { return } - - var snapshot = dataSource.snapshot() - guard destinationIndexPath.row < snapshot.itemIdentifiers.count else { return } - let anchorItem = snapshot.itemIdentifiers[destinationIndexPath.row] - snapshot.moveItem(item, afterItem: anchorItem) - dataSource.apply(snapshot) - - coordinator.drop(dropItem.dragItem, toItemAt: destinationIndexPath) - } -} - -extension ComposeStatusPollTableViewCell: UICollectionViewDelegate { - func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(originalIndexPath.debugDescription) -> \(proposedIndexPath.debugDescription)") - - guard let _ = collectionView.cellForItem(at: proposedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else { - return originalIndexPath - } - - return proposedIndexPath - } -} diff --git a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift index d258ff7b8..496c8191b 100644 --- a/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift +++ b/Mastodon/Scene/Compose/View/CustomEmojiPickerInputViewModel.swift @@ -8,6 +8,8 @@ import UIKit import Combine import MetaTextKit +import MastodonCore +import MastodonUI final class CustomEmojiPickerInputViewModel { diff --git a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift index cbf292e3b..476832b69 100644 --- a/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Community/DiscoveryCommunityViewModel+State.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension DiscoveryCommunityViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "DiscoveryCommunityViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: DiscoveryCommunityViewModel? @@ -29,8 +25,10 @@ extension DiscoveryCommunityViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? DiscoveryCommunityViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension DiscoveryCommunityViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift index 09c3c57bb..7c802cde3 100644 --- a/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/News/DiscoveryNewsViewModel+State.swift @@ -11,16 +11,12 @@ import GameplayKit import MastodonSDK extension DiscoveryNewsViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "DiscoveryNewsViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: DiscoveryNewsViewModel? init(viewModel: DiscoveryNewsViewModel) { @@ -29,8 +25,10 @@ extension DiscoveryNewsViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? DiscoveryNewsViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension DiscoveryNewsViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift index 9dc1e1f29..3ed245e99 100644 --- a/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift +++ b/Mastodon/Scene/Discovery/Posts/DiscoveryPostsViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonSDK import MastodonCore extension DiscoveryPostsViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "DiscoveryPostsViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: DiscoveryPostsViewModel? init(viewModel: DiscoveryPostsViewModel) { @@ -30,8 +26,10 @@ extension DiscoveryPostsViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? DiscoveryPostsViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension DiscoveryPostsViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift index a4818aa71..42079a91e 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewController.swift @@ -13,6 +13,7 @@ import GameplayKit import CoreData import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class HashtagTimelineViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -168,10 +169,10 @@ extension HashtagTimelineViewController { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let composeViewModel = ComposeViewModel( context: context, - composeKind: .hashtag(hashtag: viewModel.hashtag), - authContext: viewModel.authContext + authContext: viewModel.authContext, + kind: .hashtag(hashtag: viewModel.hashtag) ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } } diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift index 57d8c99dd..deb9de0a2 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel+State.swift @@ -11,7 +11,7 @@ import GameplayKit import CoreDataStack extension HashtagTimelineViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "HashtagTimelineViewModel.LoadOldestState", category: "StateMachine") @@ -28,10 +28,11 @@ extension HashtagTimelineViewModel { } override func didEnter(from previousState: GKState?) { - let previousState = previousState as? HashtagTimelineViewModel.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 ?? "")") - - viewModel?.loadOldestStateMachinePublisher.send(self) + super.didEnter(from: previousState) + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor diff --git a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift index aa57a3930..af4d2a01a 100644 --- a/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift +++ b/Mastodon/Scene/HashtagTimeline/HashtagTimelineViewModel.swift @@ -51,7 +51,6 @@ final class HashtagTimelineViewModel { stateMachine.enter(State.Initial.self) return stateMachine }() - lazy var loadOldestStateMachinePublisher = CurrentValueSubject(nil) init(context: AppContext, authContext: AuthContext, hashtag: String) { self.context = context diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift index 9412abad1..cabc655c9 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+Diffable.swift @@ -9,6 +9,7 @@ import os.log import UIKit import CoreData import CoreDataStack +import MastodonUI extension HomeTimelineViewModel { diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift index 2a0396f20..e88b5ed5f 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel+LoadOldestState.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension HomeTimelineViewModel { - class LoadOldestState: GKState, NamingState { + class LoadOldestState: GKState { let logger = Logger(subsystem: "HomeTimelineViewModel.LoadOldestState", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: HomeTimelineViewModel? @@ -29,10 +25,10 @@ extension HomeTimelineViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? HomeTimelineViewModel.LoadOldestState - 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 ?? "")") - viewModel?.loadOldestStateMachinePublisher.send(self) + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -41,7 +37,7 @@ extension HomeTimelineViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift index 4cdd1da55..edd431e52 100644 --- a/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift +++ b/Mastodon/Scene/HomeTimeline/HomeTimelineViewModel.swift @@ -16,6 +16,7 @@ import GameplayKit import AlamofireImage import DateToolsSwift import MastodonCore +import MastodonUI final class HomeTimelineViewModel: NSObject { diff --git a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift index 346692adc..ff23d8d6e 100644 --- a/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift +++ b/Mastodon/Scene/Notification/NotificationTimeline/NotificationTimelineViewModel+LoadOldestState.swift @@ -12,16 +12,12 @@ import MastodonSDK import os.log extension NotificationTimelineViewModel { - class LoadOldestState: GKState, NamingState { + class LoadOldestState: GKState { let logger = Logger(subsystem: "NotificationTimelineViewModel.LoadOldestState", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: NotificationTimelineViewModel? init(viewModel: NotificationTimelineViewModel) { @@ -30,8 +26,10 @@ extension NotificationTimelineViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? NotificationTimelineViewModel.LoadOldestState - 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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension NotificationTimelineViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift index 3b0ebca8c..b8dbf994e 100644 --- a/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift +++ b/Mastodon/Scene/Onboarding/PickServer/TableViewCell/PickServerLoaderTableViewCell.swift @@ -12,7 +12,7 @@ import MastodonCore import MastodonUI import MastodonLocalization -final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { let containerView: UIView = { let view = UIView() @@ -30,7 +30,7 @@ final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell { return label }() - override func _init() { + public override func _init() { super._init() diff --git a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift index 4e757cd1a..d50c62afe 100644 --- a/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift +++ b/Mastodon/Scene/Onboarding/PickServer/View/PickServerServerSectionTableHeaderView.swift @@ -9,6 +9,7 @@ import os.log import UIKit import Tabman import MastodonAsset +import MastodonUI import MastodonLocalization protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject { diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift index b91b90dad..5edec2618 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewController.swift @@ -12,6 +12,7 @@ import Combine import GameplayKit import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class BookmarkViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift index 8c9fd1150..e86ee92cc 100644 --- a/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift +++ b/Mastodon/Scene/Profile/Bookmark/BookmarkViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonSDK import MastodonCore extension BookmarkViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "BookmarkViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: BookmarkViewModel? init(viewModel: BookmarkViewModel) { @@ -30,8 +26,10 @@ extension BookmarkViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? BookmarkViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension BookmarkViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift index 0b113345e..c15adcf83 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewController.swift @@ -15,6 +15,7 @@ import Combine import GameplayKit import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class FavoriteViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { diff --git a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift index b583ec139..803a9d45e 100644 --- a/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift +++ b/Mastodon/Scene/Profile/Favorite/FavoriteViewModel+State.swift @@ -12,15 +12,11 @@ import MastodonCore import MastodonSDK extension FavoriteViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FavoriteViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: FavoriteViewModel? @@ -30,8 +26,10 @@ extension FavoriteViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? FavoriteViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension FavoriteViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift index 57e068c72..190fa27e5 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewController.swift @@ -10,6 +10,7 @@ import UIKit import GameplayKit import Combine import MastodonCore +import MastodonUI import MastodonLocalization final class FollowerListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift index fe336f39f..045def7b7 100644 --- a/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Follower/FollowerListViewModel+State.swift @@ -12,7 +12,7 @@ import MastodonSDK import MastodonCore extension FollowerListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FollowerListViewModel.State", category: "StateMachine") @@ -30,8 +30,10 @@ extension FollowerListViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? FollowerListViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +42,7 @@ extension FollowerListViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift index ccf0f1819..e16b600c2 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewController.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewController.swift @@ -11,6 +11,7 @@ import GameplayKit import Combine import MastodonLocalization import MastodonCore +import MastodonUI final class FollowingListViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift index d8c1f765a..723e66c8e 100644 --- a/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift +++ b/Mastodon/Scene/Profile/Following/FollowingListViewModel+State.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension FollowingListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "FollowingListViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: FollowingListViewModel? @@ -29,8 +25,10 @@ extension FollowingListViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? FollowingListViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension FollowingListViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/ProfileViewController.swift b/Mastodon/Scene/Profile/ProfileViewController.swift index 30c37473e..9dd06b22c 100644 --- a/Mastodon/Scene/Profile/ProfileViewController.swift +++ b/Mastodon/Scene/Profile/ProfileViewController.swift @@ -499,7 +499,7 @@ extension ProfileViewController { user: record ) guard let activityViewController = _activityViewController else { return } - self.coordinator.present( + _ = self.coordinator.present( scene: .activityViewController( activityViewController: activityViewController, sourceView: nil, @@ -514,13 +514,13 @@ extension ProfileViewController { @objc private func favoriteBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let favoriteViewModel = FavoriteViewModel(context: context, authContext: viewModel.authContext) - coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) + _ = coordinator.present(scene: .favorite(viewModel: favoriteViewModel), from: self, transition: .show) } @objc private func bookmarkBarButtonItemPressed(_ sender: UIBarButtonItem) { os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) let bookmarkViewModel = BookmarkViewModel(context: context, authContext: viewModel.authContext) - coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) + _ = coordinator.present(scene: .bookmark(viewModel: bookmarkViewModel), from: self, transition: .show) } @objc private func replyBarButtonItemPressed(_ sender: UIBarButtonItem) { @@ -528,10 +528,10 @@ extension ProfileViewController { guard let mastodonUser = viewModel.user else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .mention(user: .init(objectID: mastodonUser.objectID)), - authContext: viewModel.authContext + authContext: viewModel.authContext, + kind: .mention(user: mastodonUser.asRecrod) ) - coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) + _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) } @objc private func refreshControlValueChanged(_ sender: UIRefreshControl) { diff --git a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift index b87d4305e..4ed266c2e 100644 --- a/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift +++ b/Mastodon/Scene/Profile/Timeline/UserTimelineViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonCore import MastodonSDK extension UserTimelineViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "UserTimelineViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: UserTimelineViewModel? init(viewModel: UserTimelineViewModel) { @@ -30,8 +26,10 @@ extension UserTimelineViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? UserTimelineViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension UserTimelineViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift index 18c37c403..c7b3e20cd 100644 --- a/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift +++ b/Mastodon/Scene/Profile/UserLIst/UserListViewModel+State.swift @@ -11,15 +11,11 @@ import GameplayKit import MastodonSDK extension UserListViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "UserListViewModel.State", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: UserListViewModel? @@ -29,8 +25,10 @@ extension UserListViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? UserListViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +37,7 @@ extension UserListViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift index 7ed704434..d45c196cd 100644 --- a/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift +++ b/Mastodon/Scene/Report/ReportStatus/ReportStatusViewController.swift @@ -11,6 +11,7 @@ import Combine import CoreDataStack import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportStatusViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift index 8e44479d3..e644c29ea 100644 --- a/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift +++ b/Mastodon/Scene/Report/ReportSupplementary/ReportSupplementaryViewController.swift @@ -10,6 +10,7 @@ import UIKit import Combine import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization protocol ReportSupplementaryViewControllerDelegate: AnyObject { diff --git a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift index 31d7d9fde..83860046e 100644 --- a/Mastodon/Scene/Root/MainTab/MainTabBarController.swift +++ b/Mastodon/Scene/Root/MainTab/MainTabBarController.swift @@ -372,8 +372,8 @@ extension MainTabBarController { guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } @@ -741,8 +741,8 @@ extension MainTabBarController { guard let authContext = self.authContext else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) } diff --git a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift index 98006d4c0..70e1239b6 100644 --- a/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift +++ b/Mastodon/Scene/Root/Sidebar/SidebarViewController.swift @@ -207,8 +207,8 @@ extension SidebarViewController: UICollectionViewDelegate { case .compose: let composeViewModel = ComposeViewModel( context: context, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) _ = coordinator.present(scene: .compose(viewModel: composeViewModel), from: self, transition: .modal(animated: true, completion: nil)) default: diff --git a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift index b5deb777c..9b12e1af0 100644 --- a/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift +++ b/Mastodon/Scene/Search/SearchDetail/SearchResult/SearchResultViewModel+State.swift @@ -12,15 +12,12 @@ import MastodonSDK import MastodonCore extension SearchResultViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "SearchResultViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } weak var viewModel: SearchResultViewModel? init(viewModel: SearchResultViewModel) { @@ -29,8 +26,10 @@ extension SearchResultViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? SearchResultViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -39,7 +38,7 @@ extension SearchResultViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift index 6e3e73b8a..13c8311c7 100644 --- a/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift +++ b/Mastodon/Scene/SuggestionAccount/SuggestionAccountViewController.swift @@ -13,6 +13,7 @@ import OSLog import UIKit import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization class SuggestionAccountViewController: UIViewController, NeedsDependency { diff --git a/Mastodon/Scene/Thread/ThreadViewController.swift b/Mastodon/Scene/Thread/ThreadViewController.swift index fc158919b..6599f449e 100644 --- a/Mastodon/Scene/Thread/ThreadViewController.swift +++ b/Mastodon/Scene/Thread/ThreadViewController.swift @@ -13,6 +13,7 @@ import AVKit import MastodonMeta import MastodonAsset import MastodonCore +import MastodonUI import MastodonLocalization final class ThreadViewController: UIViewController, NeedsDependency, MediaPreviewableViewController { @@ -114,8 +115,8 @@ extension ThreadViewController { guard case let .root(threadContext) = viewModel.root else { return } let composeViewModel = ComposeViewModel( context: context, - composeKind: .reply(status: threadContext.status), - authContext: viewModel.authContext + authContext: viewModel.authContext, + kind: .reply(status: threadContext.status) ) _ = coordinator.present( scene: .compose(viewModel: composeViewModel), diff --git a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift index 7040818e9..834d478e6 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+Diffable.swift @@ -10,6 +10,7 @@ import Combine import CoreData import CoreDataStack import MastodonCore +import MastodonUI import MastodonSDK extension ThreadViewModel { diff --git a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift index 4917aacb5..050670be7 100644 --- a/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift +++ b/Mastodon/Scene/Thread/ThreadViewModel+LoadThreadState.swift @@ -13,15 +13,11 @@ import CoreDataStack import MastodonSDK extension ThreadViewModel { - class LoadThreadState: GKState, NamingState { + class LoadThreadState: GKState { let logger = Logger(subsystem: "ThreadViewModel.LoadThreadState", category: "StateMachine") let id = UUID() - - var name: String { - String(describing: Self.self) - } weak var viewModel: ThreadViewModel? @@ -31,8 +27,10 @@ extension ThreadViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? ThreadViewModel.LoadThreadState - 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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -41,7 +39,7 @@ extension ThreadViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Supporting Files/SceneDelegate.swift b/Mastodon/Supporting Files/SceneDelegate.swift index f368fa246..a2dd1625a 100644 --- a/Mastodon/Supporting Files/SceneDelegate.swift +++ b/Mastodon/Supporting Files/SceneDelegate.swift @@ -149,10 +149,10 @@ extension SceneDelegate { if let authContext = coordinator?.authContext { let composeViewModel = ComposeViewModel( context: AppContext.shared, - composeKind: .post, - authContext: authContext + authContext: authContext, + kind: .post ) - coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) + _ = coordinator?.present(scene: .compose(viewModel: composeViewModel), from: nil, transition: .modal(animated: true, completion: nil)) logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): present compose scene") } else { logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): not authenticated") diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift similarity index 95% rename from Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift index e85c8263e..78d7f2e81 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+API+Subscriptions+Policy.swift @@ -11,7 +11,7 @@ import MastodonAsset import MastodonLocalization extension Mastodon.API.Subscriptions.Policy { - var title: String { + public var title: String { switch self { case .all: return L10n.Scene.Settings.Section.Notifications.Trigger.anyone case .follower: return L10n.Scene.Settings.Section.Notifications.Trigger.follower diff --git a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift index 2441ebfd0..059f09420 100644 --- a/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Account.swift @@ -7,14 +7,15 @@ import Foundation import MastodonSDK +import MastodonMeta -extension Mastodon.Entity.Account { - public var displayNameWithFallback: String { - if displayName.isEmpty { - return username - } else { - return displayName - } +extension Mastodon.Entity.Account: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: Mastodon.Entity.Account, rhs: Mastodon.Entity.Account) -> Bool { + return lhs.id == rhs.id } } @@ -28,3 +29,21 @@ extension Mastodon.Entity.Account { return avatarImageURL() ?? URL(string: "https://\(domain)/avatars/original/missing.png")! } } + +extension Mastodon.Entity.Account { + public var displayNameWithFallback: String { + return !displayName.isEmpty ? displayName : username + } +} + +extension Mastodon.Entity.Account { + public var emojiMeta: MastodonContent.Emojis { + let isAnimated = !UserDefaults.shared.preferredStaticEmoji + + var dict = MastodonContent.Emojis() + for emoji in emojis ?? [] { + dict[emoji.shortcode] = isAnimated ? emoji.url : emoji.staticURL + } + return dict + } +} diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift similarity index 92% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift index b3771632c..1dbfbd24f 100644 --- a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift +++ b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error+Detail.swift @@ -36,7 +36,7 @@ extension Mastodon.Entity.Error.Detail: LocalizedError { extension Mastodon.Entity.Error.Detail { - enum Item: String { + public enum Item: String { case username case email case password @@ -82,32 +82,32 @@ extension Mastodon.Entity.Error.Detail { } } - var usernameErrorDescriptions: [String] { + public var usernameErrorDescriptions: [String] { guard let username = username, !username.isEmpty else { return [] } return username.map { Mastodon.Entity.Error.Detail.localizeError(item: .username, for: $0) } } - var emailErrorDescriptions: [String] { + public var emailErrorDescriptions: [String] { guard let email = email, !email.isEmpty else { return [] } return email.map { Mastodon.Entity.Error.Detail.localizeError(item: .email, for: $0) } } - var passwordErrorDescriptions: [String] { + public var passwordErrorDescriptions: [String] { guard let password = password, !password.isEmpty else { return [] } return password.map { Mastodon.Entity.Error.Detail.localizeError(item: .password, for: $0) } } - var agreementErrorDescriptions: [String] { + public var agreementErrorDescriptions: [String] { guard let agreement = agreement, !agreement.isEmpty else { return [] } return agreement.map { Mastodon.Entity.Error.Detail.localizeError(item: .agreement, for: $0) } } - var localeErrorDescriptions: [String] { + public var localeErrorDescriptions: [String] { guard let locale = locale, !locale.isEmpty else { return [] } return locale.map { Mastodon.Entity.Error.Detail.localizeError(item: .locale, for: $0) } } - var reasonErrorDescriptions: [String] { + public var reasonErrorDescriptions: [String] { guard let reason = reason, !reason.isEmpty else { return [] } return reason.map { Mastodon.Entity.Error.Detail.localizeError(item: .reason, for: $0) } } diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Error.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Error.swift diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Field.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Field.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Field.swift diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+History.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+History.swift diff --git a/Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift b/MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift similarity index 100% rename from Mastodon/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift rename to MastodonSDK/Sources/MastodonCore/Extension/MastodonSDK/Mastodon+Entity+Notification+Type.swift diff --git a/Mastodon/Diffiable/Compose/AutoCompleteItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteItem.swift similarity index 90% rename from Mastodon/Diffiable/Compose/AutoCompleteItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteItem.swift index ee296ba71..21bf9d759 100644 --- a/Mastodon/Diffiable/Compose/AutoCompleteItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteItem.swift @@ -8,7 +8,7 @@ import Foundation import MastodonSDK -enum AutoCompleteItem { +public enum AutoCompleteItem { case hashtag(tag: Mastodon.Entity.Tag) case hashtagV1(tag: String) case account(account: Mastodon.Entity.Account) @@ -17,7 +17,7 @@ enum AutoCompleteItem { } extension AutoCompleteItem: Equatable { - static func == (lhs: AutoCompleteItem, rhs: AutoCompleteItem) -> Bool { + public static func == (lhs: AutoCompleteItem, rhs: AutoCompleteItem) -> Bool { switch (lhs, rhs) { case (.hashtag(let tagLeft), hashtag(let tagRight)): return tagLeft.name == tagRight.name @@ -36,7 +36,7 @@ extension AutoCompleteItem: Equatable { } extension AutoCompleteItem: Hashable { - func hash(into hasher: inout Hasher) { + public func hash(into hasher: inout Hasher) { switch self { case .hashtag(let tag): hasher.combine(tag.name) diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift new file mode 100644 index 000000000..d7c9d07e9 --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/AutoCompleteSection.swift @@ -0,0 +1,16 @@ +// +// AutoCompleteSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-17. +// + +import UIKit +import MastodonSDK +import MastodonMeta +import MastodonAsset +import MastodonLocalization + +public enum AutoCompleteSection: Equatable, Hashable { + case main +} diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift similarity index 93% rename from Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift index 07ae4e5df..834e1da49 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentItem.swift @@ -6,7 +6,6 @@ // import Foundation -import MastodonCore enum ComposeStatusAttachmentItem { case attachment(attachmentService: MastodonAttachmentService) diff --git a/Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusAttachmentSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusAttachmentSection.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift similarity index 99% rename from Mastodon/Diffiable/Compose/ComposeStatusItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift index aad93c2d2..65650dcdc 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusItem.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusItem.swift @@ -8,7 +8,6 @@ import Foundation import Combine import CoreData -import MastodonCore import MastodonMeta import CoreDataStack diff --git a/Mastodon/Diffiable/Compose/ComposeStatusPollItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollItem.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusPollItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollItem.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusPollSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollSection.swift similarity index 100% rename from Mastodon/Diffiable/Compose/ComposeStatusPollSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusPollSection.swift diff --git a/Mastodon/Diffiable/Compose/ComposeStatusSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusSection.swift similarity index 54% rename from Mastodon/Diffiable/Compose/ComposeStatusSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusSection.swift index 7b4596267..12dc88053 100644 --- a/Mastodon/Diffiable/Compose/ComposeStatusSection.swift +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/ComposeStatusSection.swift @@ -13,7 +13,7 @@ import MetaTextKit import MastodonMeta import AlamofireImage -enum ComposeStatusSection: Equatable, Hashable { +public enum ComposeStatusSection: Equatable, Hashable { case replyTo case status case attachment @@ -21,20 +21,11 @@ enum ComposeStatusSection: Equatable, Hashable { } extension ComposeStatusSection { - public enum ComposeKind { - case post - case hashtag(hashtag: String) - case mention(user: ManagedObjectRecord) - case reply(status: ManagedObjectRecord) - } -} -extension ComposeStatusSection { - - static func configure( - cell: ComposeStatusContentTableViewCell, - attribute: ComposeStatusItem.ComposeStatusAttribute - ) { +// static func configure( +// cell: ComposeStatusContentTableViewCell, +// attribute: ComposeStatusItem.ComposeStatusAttribute +// ) { // cell.prepa // // set avatar // attribute.avatarURL @@ -62,18 +53,18 @@ extension ComposeStatusSection { // cell.statusView.usernameLabel.text = username.flatMap { "@" + $0 } ?? " " // } // .store(in: &cell.disposeBag) - } +// } } -protocol CustomEmojiReplaceableTextInput: UITextInput & UIResponder { +public protocol CustomEmojiReplaceableTextInput: UITextInput & UIResponder { var inputView: UIView? { get set } } -class CustomEmojiReplaceableTextInputReference { - weak var value: CustomEmojiReplaceableTextInput? +public class CustomEmojiReplaceableTextInputReference { + public weak var value: CustomEmojiReplaceableTextInput? - init(value: CustomEmojiReplaceableTextInput? = nil) { + public init(value: CustomEmojiReplaceableTextInput? = nil) { self.value = value } } @@ -83,21 +74,21 @@ extension UITextView: CustomEmojiReplaceableTextInput { } extension ComposeStatusSection { - static func configureCustomEmojiPicker( - viewModel: CustomEmojiPickerInputViewModel?, - customEmojiReplaceableTextInput: CustomEmojiReplaceableTextInput, - disposeBag: inout Set - ) { - guard let viewModel = viewModel else { return } - viewModel.isCustomEmojiComposing - .receive(on: DispatchQueue.main) - .sink { [weak viewModel] isCustomEmojiComposing in - guard let viewModel = viewModel else { return } - customEmojiReplaceableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil - customEmojiReplaceableTextInput.reloadInputViews() - viewModel.append(customEmojiReplaceableTextInput: customEmojiReplaceableTextInput) - } - .store(in: &disposeBag) - } +// static func configureCustomEmojiPicker( +// viewModel: CustomEmojiPickerInputViewModel?, +// customEmojiReplaceableTextInput: CustomEmojiReplaceableTextInput, +// disposeBag: inout Set +// ) { +// guard let viewModel = viewModel else { return } +// viewModel.isCustomEmojiComposing +// .receive(on: DispatchQueue.main) +// .sink { [weak viewModel] isCustomEmojiComposing in +// guard let viewModel = viewModel else { return } +// customEmojiReplaceableTextInput.inputView = isCustomEmojiComposing ? viewModel.customEmojiPickerInputView : nil +// customEmojiReplaceableTextInput.reloadInputViews() +// viewModel.append(customEmojiReplaceableTextInput: customEmojiReplaceableTextInput) +// } +// .store(in: &disposeBag) +// } } diff --git a/Mastodon/Diffiable/Compose/CustomEmojiPickerItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift similarity index 100% rename from Mastodon/Diffiable/Compose/CustomEmojiPickerItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerItem.swift diff --git a/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift new file mode 100644 index 000000000..5556a41ee --- /dev/null +++ b/MastodonSDK/Sources/MastodonCore/Model/Compose/CustomEmojiPickerSection.swift @@ -0,0 +1,12 @@ +// +// CustomEmojiPickerSection.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-3-24. +// + +import Foundation + +public enum CustomEmojiPickerSection: Equatable, Hashable { + case emoji(name: String) +} diff --git a/MastodonSDK/Sources/MastodonUI/Model/Poll/PollItem.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Model/Poll/PollItem.swift rename to MastodonSDK/Sources/MastodonCore/Model/Poll/PollItem.swift diff --git a/MastodonSDK/Sources/MastodonUI/Model/Poll/PollSection.swift b/MastodonSDK/Sources/MastodonCore/Model/Poll/PollSection.swift similarity index 100% rename from MastodonSDK/Sources/MastodonUI/Model/Poll/PollSection.swift rename to MastodonSDK/Sources/MastodonCore/Model/Poll/PollSection.swift diff --git a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift index f7e595b81..4abe9ba5f 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/API/APIService+Favorite.swift @@ -12,7 +12,6 @@ import MastodonSDK import CoreData import CoreDataStack import CommonOSLog -import MastodonCore extension APIService { diff --git a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift index afb4e63a9..48da254c6 100644 --- a/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift +++ b/MastodonSDK/Sources/MastodonCore/Service/AuthenticationService.swift @@ -79,7 +79,6 @@ extension AuthenticationService { public func activeMastodonUser(domain: String, userID: MastodonUser.ID) async throws -> Bool { var isActive = false - var _mastodonAuthentication: MastodonAuthentication? let managedObjectContext = backgroundManagedObjectContext @@ -91,7 +90,6 @@ extension AuthenticationService { return } mastodonAuthentication.update(activedAt: Date()) - _mastodonAuthentication = mastodonAuthentication isActive = true } diff --git a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift similarity index 94% rename from Mastodon/Diffiable/Compose/AutoCompleteSection.swift rename to MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift index 1260f398b..3022899f1 100644 --- a/Mastodon/Diffiable/Compose/AutoCompleteSection.swift +++ b/MastodonSDK/Sources/MastodonUI/DataSource/AutoCompleteSection+Diffable.swift @@ -1,25 +1,20 @@ // -// AutoCompleteSection.swift -// Mastodon +// AutoCompleteSection+Diffable.swift +// // -// Created by MainasuK Cirno on 2021-5-17. +// Created by MainasuK on 22/10/10. // import UIKit -import MastodonSDK -import MastodonMeta -import MastodonAsset -import MastodonLocalization import MastodonCore - -enum AutoCompleteSection: Equatable, Hashable { - case main -} +import MastodonSDK +import MastodonLocalization +import MastodonMeta extension AutoCompleteSection { - static func tableViewDiffableDataSource( - for tableView: UITableView + public static func tableViewDiffableDataSource( + tableView: UITableView ) -> UITableViewDiffableDataSource { UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in switch item { diff --git a/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift new file mode 100644 index 000000000..ca3658e95 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/DataSource/CustomEmojiPickerSection+Diffable.swift @@ -0,0 +1,59 @@ +// +// CustomEmojiPickerSection+Diffable.swift +// +// +// Created by MainasuK on 22/10/10. +// + +import Foundation +import MastodonCore + +extension CustomEmojiPickerSection { +// static func collectionViewDiffableDataSource( +// collectionView: UICollectionView, +// dependency: NeedsDependency +// ) -> UICollectionViewDiffableDataSource { +// let dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [weak dependency] collectionView, indexPath, item -> UICollectionViewCell? in +// guard let _ = dependency else { return nil } +// switch item { +// case .emoji(let attribute): +// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: CustomEmojiPickerItemCollectionViewCell.self), for: indexPath) as! CustomEmojiPickerItemCollectionViewCell +// let placeholder = UIImage.placeholder(size: CustomEmojiPickerItemCollectionViewCell.itemSize, color: .systemFill) +// .af.imageRounded(withCornerRadius: 4) +// +// let isAnimated = !UserDefaults.shared.preferredStaticEmoji +// let url = URL(string: isAnimated ? attribute.emoji.url : attribute.emoji.staticURL) +// cell.emojiImageView.sd_setImage( +// with: url, +// placeholderImage: placeholder, +// options: [], +// context: nil +// ) +// cell.accessibilityLabel = attribute.emoji.shortcode +// return cell +// } +// } +// +// dataSource.supplementaryViewProvider = { [weak dataSource] collectionView, kind, indexPath -> UICollectionReusableView? in +// guard let dataSource = dataSource else { return nil } +// let sections = dataSource.snapshot().sectionIdentifiers +// guard indexPath.section < sections.count else { return nil } +// let section = sections[indexPath.section] +// +// switch kind { +// case String(describing: CustomEmojiPickerHeaderCollectionReusableView.self): +// let header = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: String(describing: CustomEmojiPickerHeaderCollectionReusableView.self), for: indexPath) as! CustomEmojiPickerHeaderCollectionReusableView +// switch section { +// case .emoji(let name): +// header.titleLabel.text = name +// } +// return header +// default: +// assertionFailure() +// return nil +// } +// } +// +// return dataSource +// } +} diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift similarity index 98% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift index 16f65f702..aa21057d1 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewController.swift @@ -88,7 +88,7 @@ extension AutoCompleteViewController { ]) tableView.delegate = self - viewModel.setupDiffableDataSource(for: tableView) +// viewModel.setupDiffableDataSource(tableView: tableView) // bind to layout chevron viewModel.symbolBoundingRect diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift new file mode 100644 index 000000000..adbf6ac09 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+Diffable.swift @@ -0,0 +1,22 @@ +// +// AutoCompleteViewModel+Diffable.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-5-17. +// + +import UIKit + +extension AutoCompleteViewModel { + +// func setupDiffableDataSource( +// tableView: UITableView +// ) { +// diffableDataSource = AutoCompleteSection.tableViewDiffableDataSource(for: tableView) +// +// var snapshot = NSDiffableDataSourceSnapshot() +// snapshot.appendSections([.main]) +// diffableDataSource?.apply(snapshot) +// } + +} diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift similarity index 95% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift index 1d46fb214..b1f5f3187 100644 --- a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel+State.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel+State.swift @@ -12,16 +12,12 @@ import MastodonSDK import MastodonCore extension AutoCompleteViewModel { - class State: GKState, NamingState { + class State: GKState { let logger = Logger(subsystem: "AutoCompleteViewModel.State", category: "StateMachine") let id = UUID() - var name: String { - String(describing: Self.self) - } - weak var viewModel: AutoCompleteViewModel? init(viewModel: AutoCompleteViewModel) { @@ -30,8 +26,10 @@ extension AutoCompleteViewModel { override func didEnter(from previousState: GKState?) { super.didEnter(from: previousState) - let previousState = previousState as? AutoCompleteViewModel.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 ?? "")") + + let from = previousState.flatMap { String(describing: $0) } ?? "nil" + let to = String(describing: self) + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(from) -> \(to)") } @MainActor @@ -40,7 +38,7 @@ extension AutoCompleteViewModel { } deinit { - logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(self.name)") + logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [\(self.id.uuidString)] \(String(describing: self))") } } } diff --git a/Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift similarity index 100% rename from Mastodon/Scene/Compose/AutoComplete/AutoCompleteViewModel.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/AutoCompleteViewModel.swift diff --git a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/Cell/AutoCompleteTableViewCell.swift similarity index 95% rename from Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/Cell/AutoCompleteTableViewCell.swift index b7c8fcecc..c5db611d0 100644 --- a/Mastodon/Scene/Compose/AutoComplete/Cell/AutoCompleteTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/Cell/AutoCompleteTableViewCell.swift @@ -10,10 +10,8 @@ import FLAnimatedImage import MetaTextKit import MastodonAsset import MastodonLocalization -import MastodonUI - -final class AutoCompleteTableViewCell: UITableViewCell { +public final class AutoCompleteTableViewCell: UITableViewCell { static let avatarImageSize = CGSize(width: 42, height: 42) static let avatarImageCornerRadius: CGFloat = 4 @@ -51,17 +49,17 @@ final class AutoCompleteTableViewCell: UITableViewCell { let separatorLine = UIView.separatorLine - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) _init() } - override func setHighlighted(_ highlighted: Bool, animated: Bool) { + public override func setHighlighted(_ highlighted: Bool, animated: Bool) { super.setHighlighted(highlighted, animated: animated) // workaround for hitTest trigger highlighted issue diff --git a/Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift similarity index 100% rename from Mastodon/Scene/Compose/AutoComplete/View/AutoCompleteTopChevronView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/AutoComplete/View/AutoCompleteTopChevronView.swift diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift index 21161a0a6..886634d7e 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentView.swift @@ -8,11 +8,93 @@ import SwiftUI public struct ComposeContentView: View { + + @ObservedObject var viewModel: ComposeContentViewModel + + @State var contentOffsetDelta: CGFloat = .zero + public var body: some View { ScrollView { - VStack { - Text("Hello") - } - } - } + VStack(spacing: .zero) { + GeometryReader { geometry in + Color.clear.preference( + key: ScrollOffsetPreferenceKey.self, + value: geometry.frame(in: .named("scrollView")).origin + ) + }.frame(width: 0, height: 0) + .onPreferenceChange(ScrollOffsetPreferenceKey.self) { offset in + print("contentOffset: \(offset)") + } + VStack { + Text("Reply") + } + .frame(height: 100) + .frame(maxWidth: .infinity) + .background(Color.blue) + .background( + GeometryReader { geometry in + Color.clear.preference( + key: ViewFramePreferenceKey.self, + value: geometry.frame(in: .named("scrollView")) + ) + } + .onPreferenceChange(ViewFramePreferenceKey.self) { frame in + print("reply frame: \(frame)") + } + ) + VStack { + Text("Content") + } + .frame(maxWidth: .infinity) + .background(Color.orange) + } // end VStack + .offset(y: contentOffsetDelta) + } // end ScrollView + .coordinateSpace(name: "scrollView") + } // end body } + +private struct ScrollOffsetPreferenceKey: PreferenceKey { + static var defaultValue: CGPoint = .zero + + static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) { } +} + +private struct ViewFramePreferenceKey: PreferenceKey { + static var defaultValue: CGRect = .zero + + static func reduce(value: inout CGRect, nextValue: () -> CGRect) { } +} + +//struct ScrollView: View { +// let axes: Axis.Set +// let showsIndicators: Bool +// let offsetChanged: (CGPoint) -> Void +// let content: Content +// +// init( +// axes: Axis.Set = .vertical, +// showsIndicators: Bool = true, +// offsetChanged: @escaping (CGPoint) -> Void = { _ in }, +// @ViewBuilder content: () -> Content +// ) { +// self.axes = axes +// self.showsIndicators = showsIndicators +// self.offsetChanged = offsetChanged +// self.content = content() +// } +// +// var body: some View { +// SwiftUI.ScrollView(axes, showsIndicators: showsIndicators) { +// GeometryReader { geometry in +// Color.clear.preference( +// key: ScrollOffsetPreferenceKey.self, +// value: geometry.frame(in: .named("scrollView")).origin +// ) +// }.frame(width: 0, height: 0) +// content +// } +// .coordinateSpace(name: "scrollView") +// .onPreferenceChange(ScrollOffsetPreferenceKey.self, perform: offsetChanged) +// } +//} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift index 18604c4b3..240e756a1 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewController.swift @@ -7,18 +7,47 @@ import os.log import UIKit +import SwiftUI public final class ComposeContentViewController: UIViewController { let logger = Logger(subsystem: "ComposeContentViewController", category: "ViewController") + public var viewModel: ComposeContentViewModel! - + let tableView: ComposeTableView = { + let tableView = ComposeTableView() + tableView.alwaysBounceVertical = true + tableView.separatorStyle = .none + tableView.tableFooterView = UIView() + return tableView + }() + + deinit { + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) + } + } extension ComposeContentViewController { public override func viewDidLoad() { super.viewDidLoad() + 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.setupDataSource(tableView: tableView) + } } + +// MARK: - UITableViewDelegate +extension ComposeContentViewController: UITableViewDelegate { } + diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift new file mode 100644 index 000000000..e1ad561ef --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel+DataSource.swift @@ -0,0 +1,90 @@ +// +// ComposeContentViewModel+DataSource.swift +// +// +// Created by MainasuK on 22/10/10. +// + +import UIKit +import MastodonCore +import CoreDataStack + +extension ComposeContentViewModel { + + func setupDataSource( + tableView: UITableView + ) { + tableView.dataSource = self + + setupTableViewCell(tableView: tableView) + } + +} + +extension ComposeContentViewModel { + enum Section: CaseIterable { + case replyTo + case status + case attachment + case poll + } + + private func setupTableViewCell(tableView: UITableView) { + switch kind { + case .post: + break + case .reply(let status): + let cell = composeReplyToTableViewCell + // bind frame publisher +// cell.framePublisher +// .receive(on: DispatchQueue.main) +// .assign(to: \.repliedToCellFrame, on: self) +// .store(in: &cell.disposeBag) + + // set initial width + cell.statusView.frame.size.width = tableView.frame.width + + // configure status + context.managedObjectContext.performAndWait { + guard let replyTo = status.object(in: context.managedObjectContext) else { return } + cell.statusView.configure(status: replyTo) + } + case .hashtag(let hashtag): + break + case .mention(let user): + break + } + } +} + +extension ComposeContentViewModel: UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + return Section.allCases.count + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch Section.allCases[section] { + case .replyTo: + switch kind { + case .reply: return 1 + default: return 0 + } + case .status: return 1 + case .attachment: return 1 + case .poll: return 1 + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + switch Section.allCases[indexPath.section] { + case .replyTo: + return composeReplyToTableViewCell + case .status: + return UITableViewCell() + case .attachment: + return UITableViewCell() + case .poll: + return UITableViewCell() + } + } +} diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift index 9fa94c5e0..9f0eed2fe 100644 --- a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/ComposeContentViewModel.swift @@ -6,15 +6,41 @@ // import Foundation +import CoreDataStack import MastodonCore -final class ComposeContentViewModel: ObservableObject { +public final class ComposeContentViewModel: NSObject, ObservableObject { + + // tableViewCell + let composeReplyToTableViewCell = ComposeReplyToTableViewCell() // input let context: AppContext - - init(context: AppContext) { + let kind: Kind + + public init( + context: AppContext, + kind: Kind + ) { self.context = context + self.kind = kind + super.init() + // end init } } + +extension ComposeContentViewModel { + public enum Kind { + case post + case hashtag(hashtag: String) + case mention(user: ManagedObjectRecord) + case reply(status: ManagedObjectRecord) + } + + public enum ViewState { + case fold // snap to input + case expand // snap to reply + } +} + diff --git a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift similarity index 83% rename from Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift index b0fbb0194..773245cbf 100644 --- a/Mastodon/Scene/Compose/TableViewCell/ComposeRepliedToStatusContentTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeReplyToTableViewCell.swift @@ -1,5 +1,5 @@ // -// ComposeRepliedToStatusContentTableViewCell.swift +// ComposeReplyToTableViewCell.swift // Mastodon // // Created by MainasuK Cirno on 2021-6-28. @@ -7,15 +7,14 @@ import UIKit import Combine -import MastodonUI -final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell { +final class ComposeReplyToTableViewCell: UITableViewCell { var disposeBag = Set() let statusView = StatusView() - let framePublisher = PassthroughSubject() + @Published var framePublisher: CGRect = .zero override func prepareForReuse() { super.prepareForReuse() @@ -36,12 +35,12 @@ final class ComposeRepliedToStatusContentTableViewCell: UITableViewCell { override func layoutSubviews() { super.layoutSubviews() - framePublisher.send(bounds) + framePublisher = bounds } } -extension ComposeRepliedToStatusContentTableViewCell { +extension ComposeReplyToTableViewCell { private func _init() { selectionStyle = .none diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift new file mode 100644 index 000000000..42a851bf1 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusAttachmentTableViewCell.swift @@ -0,0 +1,172 @@ +// +// ComposeStatusAttachmentTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import UIKit +import SwiftUI +import Combine +import AlamofireImage +import MastodonAsset +import MastodonCore +import MastodonLocalization +import UIHostingConfigurationBackport + +//final class ComposeStatusAttachmentTableViewCell: UITableViewCell { +// +// private(set) var dataSource: UICollectionViewDiffableDataSource! +// weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate? +// var observations = Set() +// +// private static func createLayout() -> UICollectionViewLayout { +// let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) +// let item = NSCollectionLayoutItem(layoutSize: itemSize) +// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) +// let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) +// let section = NSCollectionLayoutSection(group: group) +// section.contentInsetsReference = .readableContent +// return UICollectionViewCompositionalLayout(section: section) +// } +// +// private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! +// let collectionView: UICollectionView = { +// let collectionViewLayout = ComposeStatusAttachmentTableViewCell.createLayout() +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) +// collectionView.register(ComposeStatusAttachmentCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self)) +// collectionView.backgroundColor = .clear +// collectionView.alwaysBounceVertical = true +// collectionView.isScrollEnabled = false +// return collectionView +// }() +// let collectionViewHeightDidUpdate = PassthroughSubject() +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension ComposeStatusAttachmentTableViewCell { +// +// private func _init() { +// backgroundColor = .clear +// contentView.backgroundColor = .clear +// +// collectionView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(collectionView) +// collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 200).priority(.defaultHigh) +// NSLayoutConstraint.activate([ +// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), +// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// collectionViewHeightLayoutConstraint, +// ]) +// +// collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in +// guard let self = self else { return } +// self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height +// self.collectionViewHeightDidUpdate.send() +// } +// .store(in: &observations) +// +// self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { +// [weak self] collectionView, indexPath, item -> UICollectionViewCell? in +// guard let _ = self else { return UICollectionViewCell() } +// switch item { +// case .attachment: +// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusAttachmentCollectionViewCell.self), for: indexPath) as! ComposeStatusAttachmentCollectionViewCell +// cell.contentConfiguration = UIHostingConfigurationBackport { +// HStack { +// Image(systemName: "star") +// Text("Favorites") +// Spacer() +// } +// } +//// cell.attachmentContainerView.descriptionTextView.text = attachmentService.description.value +//// cell.delegate = self.composeStatusAttachmentCollectionViewCellDelegate +//// attachmentService.thumbnailImage +//// .receive(on: DispatchQueue.main) +//// .sink { [weak cell] thumbnailImage in +//// guard let cell = cell else { return } +//// let size = cell.attachmentContainerView.previewImageView.frame.size != .zero ? cell.attachmentContainerView.previewImageView.frame.size : CGSize(width: 1, height: 1) +//// guard let image = thumbnailImage else { +//// let placeholder = UIImage.placeholder( +//// size: size, +//// color: ThemeService.shared.currentTheme.value.systemGroupedBackgroundColor +//// ) +//// .af.imageRounded( +//// withCornerRadius: AttachmentContainerView.containerViewCornerRadius +//// ) +//// cell.attachmentContainerView.previewImageView.image = placeholder +//// return +//// } +//// // cannot get correct size. set corner radius on layer +//// cell.attachmentContainerView.previewImageView.image = image +//// } +//// .store(in: &cell.disposeBag) +//// Publishers.CombineLatest( +//// attachmentService.uploadStateMachineSubject.eraseToAnyPublisher(), +//// attachmentService.error.eraseToAnyPublisher() +//// ) +//// .receive(on: DispatchQueue.main) +//// .sink { [weak cell, weak attachmentService] uploadState, error in +//// guard let cell = cell else { return } +//// guard let attachmentService = attachmentService else { return } +//// cell.attachmentContainerView.emptyStateView.isHidden = error == nil +//// cell.attachmentContainerView.descriptionBackgroundView.isHidden = error != nil +//// if let error = error { +//// cell.attachmentContainerView.activityIndicatorView.stopAnimating() +//// cell.attachmentContainerView.emptyStateView.label.text = error.localizedDescription +//// } else { +//// guard let uploadState = uploadState else { return } +//// switch uploadState { +//// case is MastodonAttachmentService.UploadState.Finish: +//// cell.attachmentContainerView.activityIndicatorView.stopAnimating() +//// case is MastodonAttachmentService.UploadState.Fail: +//// cell.attachmentContainerView.activityIndicatorView.stopAnimating() +//// // FIXME: not display +//// cell.attachmentContainerView.emptyStateView.label.text = { +//// if let file = attachmentService.file.value { +//// switch file { +//// case .jpeg, .png, .gif: +//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) +//// case .other: +//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.video) +//// } +//// } else { +//// return L10n.Scene.Compose.Attachment.attachmentBroken(L10n.Scene.Compose.Attachment.photo) +//// } +//// }() +//// default: +//// break +//// } +//// } +//// } +//// .store(in: &cell.disposeBag) +//// NotificationCenter.default.publisher( +//// for: UITextView.textDidChangeNotification, +//// object: cell.attachmentContainerView.descriptionTextView +//// ) +//// .receive(on: DispatchQueue.main) +//// .sink { notification in +//// guard let textField = notification.object as? UITextView else { return } +//// let text = textField.text?.trimmingCharacters(in: .whitespacesAndNewlines) +//// attachmentService.description.value = text +//// } +//// .store(in: &cell.disposeBag) +// return cell +// } +// } +// } +// +//} +// diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift new file mode 100644 index 000000000..e9b8cf068 --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusContentTableViewCell.swift @@ -0,0 +1,172 @@ +// +// ComposeStatusContentTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-28. +// + +import os.log +import UIKit +import Combine +import MetaTextKit +import UITextView_Placeholder +import MastodonAsset +import MastodonLocalization +import MastodonUI + +//protocol ComposeStatusContentTableViewCellDelegate: AnyObject { +// func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool +//} + +final class ComposeStatusContentTableViewCell: UITableViewCell { + +// let logger = Logger(subsystem: "ComposeStatusContentTableViewCell", category: "View") +// +// var disposeBag = Set() +// weak var delegate: ComposeStatusContentTableViewCellDelegate? +// +// let statusView = StatusView() +// +// let statusContentWarningEditorView = StatusContentWarningEditorView() +// +// let textEditorViewContainerView = UIView() +// +// static let metaTextViewTag: Int = 333 +// let metaText: MetaText = { +// let metaText = MetaText() +// metaText.textView.backgroundColor = .clear +// metaText.textView.isScrollEnabled = false +// metaText.textView.keyboardType = .twitter +// metaText.textView.textDragInteraction?.isEnabled = false // disable drag for link and attachment +// metaText.textView.textContainer.lineFragmentPadding = 10 // leading inset +// metaText.textView.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)) +// metaText.textView.attributedPlaceholder = { +// var attributes = metaText.textAttributes +// attributes[.foregroundColor] = Asset.Colors.Label.secondary.color +// return NSAttributedString( +// string: L10n.Scene.Compose.contentInputPlaceholder, +// attributes: attributes +// ) +// }() +// metaText.paragraphStyle = { +// let style = NSMutableParagraphStyle() +// style.lineSpacing = 5 +// style.paragraphSpacing = 0 +// return style +// }() +// metaText.textAttributes = [ +// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .regular)), +// .foregroundColor: Asset.Colors.Label.primary.color, +// ] +// metaText.linkAttributes = [ +// .font: UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .semibold)), +// .foregroundColor: Asset.Colors.brand.color, +// ] +// return metaText +// }() +// +// // output +// let contentWarningContent = PassthroughSubject() +// +// override func prepareForReuse() { +// super.prepareForReuse() +// +// metaText.delegate = nil +// metaText.textView.delegate = nil +// } +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } + +} + +extension ComposeStatusContentTableViewCell { + +// private func _init() { +// selectionStyle = .none +// layer.zPosition = 999 +// backgroundColor = .clear +// preservesSuperviewLayoutMargins = true +// +// let containerStackView = UIStackView() +// containerStackView.axis = .vertical +// containerStackView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(containerStackView) +// NSLayoutConstraint.activate([ +// containerStackView.topAnchor.constraint(equalTo: contentView.topAnchor), +// containerStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// containerStackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// containerStackView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// ]) +// containerStackView.preservesSuperviewLayoutMargins = true +// +// containerStackView.addArrangedSubview(statusContentWarningEditorView) +// statusContentWarningEditorView.setContentHuggingPriority(.required - 1, for: .vertical) +// +// let statusContainerView = UIView() +// statusContainerView.preservesSuperviewLayoutMargins = true +// containerStackView.addArrangedSubview(statusContainerView) +// statusView.translatesAutoresizingMaskIntoConstraints = false +// statusContainerView.addSubview(statusView) +// NSLayoutConstraint.activate([ +// statusView.topAnchor.constraint(equalTo: statusContainerView.topAnchor, constant: 20), +// statusView.leadingAnchor.constraint(equalTo: statusContainerView.leadingAnchor), +// statusView.trailingAnchor.constraint(equalTo: statusContainerView.trailingAnchor), +// statusView.bottomAnchor.constraint(equalTo: statusContainerView.bottomAnchor), +// ]) +// statusView.setup(style: .composeStatusAuthor) +// +// containerStackView.addArrangedSubview(textEditorViewContainerView) +// metaText.textView.translatesAutoresizingMaskIntoConstraints = false +// textEditorViewContainerView.addSubview(metaText.textView) +// NSLayoutConstraint.activate([ +// metaText.textView.topAnchor.constraint(equalTo: textEditorViewContainerView.topAnchor), +// metaText.textView.leadingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.leadingAnchor), +// metaText.textView.trailingAnchor.constraint(equalTo: textEditorViewContainerView.layoutMarginsGuide.trailingAnchor), +// metaText.textView.bottomAnchor.constraint(equalTo: textEditorViewContainerView.bottomAnchor), +// metaText.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 64).priority(.defaultHigh), +// ]) +// statusContentWarningEditorView.textView.delegate = self +// } + +} + +// MARK: - UITextViewDelegate +//extension ComposeStatusContentTableViewCell: UITextViewDelegate { +// +// func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { +// return delegate?.composeStatusContentTableViewCell(self, textViewShouldBeginEditing: textView) ?? true +// } +// +// func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { +// switch textView { +// case statusContentWarningEditorView.textView: +// // disable input line break +// guard text != "\n" else { return false } +// return true +// default: +// assertionFailure() +// return true +// } +// } +// +// func textViewDidChange(_ textView: UITextView) { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): text: \(textView.text ?? "")") +// guard textView === statusContentWarningEditorView.textView else { return } +// // replace line break with space +// // needs check input state to prevent break the IME +// if textView.markedTextRange == nil { +// textView.text = textView.text.replacingOccurrences(of: "\n", with: " ") +// } +// contentWarningContent.send(textView.text) +// } +// +//} + diff --git a/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift new file mode 100644 index 000000000..27b835a5a --- /dev/null +++ b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/TableViewCell/ComposeStatusPollTableViewCell.swift @@ -0,0 +1,209 @@ +// +// ComposeStatusPollTableViewCell.swift +// Mastodon +// +// Created by MainasuK Cirno on 2021-6-29. +// + +import os.log +import UIKit +import Combine +import MastodonAsset +import MastodonLocalization + +//protocol ComposeStatusPollTableViewCellDelegate: AnyObject { +// func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) +//} +// +//final class ComposeStatusPollTableViewCell: UITableViewCell { +// +// let logger = Logger(subsystem: "ComposeStatusPollTableViewCell", category: "UI") +// +// private(set) var dataSource: UICollectionViewDiffableDataSource! +// var observations = Set() +// +// weak var customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel? +// weak var delegate: ComposeStatusPollTableViewCellDelegate? +// weak var composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate? +// weak var composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate? +// weak var composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate? +// +// private static func createLayout() -> UICollectionViewLayout { +// let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) +// let item = NSCollectionLayoutItem(layoutSize: itemSize) +// let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44)) +// let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item]) +// let section = NSCollectionLayoutSection(group: group) +// section.contentInsetsReference = .readableContent +// return UICollectionViewCompositionalLayout(section: section) +// } +// +// private(set) var collectionViewHeightLayoutConstraint: NSLayoutConstraint! +// let collectionView: UICollectionView = { +// let collectionViewLayout = ComposeStatusPollTableViewCell.createLayout() +// let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout) +// collectionView.register(ComposeStatusPollOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self)) +// collectionView.register(ComposeStatusPollOptionAppendEntryCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self)) +// collectionView.register(ComposeStatusPollExpiresOptionCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self)) +// collectionView.backgroundColor = .clear +// collectionView.alwaysBounceVertical = true +// collectionView.isScrollEnabled = false +// collectionView.dragInteractionEnabled = true +// return collectionView +// }() +// let collectionViewHeightDidUpdate = PassthroughSubject() +// +// override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +// super.init(style: style, reuseIdentifier: reuseIdentifier) +// _init() +// } +// +// required init?(coder: NSCoder) { +// super.init(coder: coder) +// _init() +// } +// +//} +// +//extension ComposeStatusPollTableViewCell { +// +// private func _init() { +// backgroundColor = .clear +// contentView.backgroundColor = .clear +// +// collectionView.translatesAutoresizingMaskIntoConstraints = false +// contentView.addSubview(collectionView) +// collectionViewHeightLayoutConstraint = collectionView.heightAnchor.constraint(equalToConstant: 300).priority(.defaultHigh) +// NSLayoutConstraint.activate([ +// collectionView.topAnchor.constraint(equalTo: contentView.topAnchor), +// collectionView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), +// collectionView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), +// collectionView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), +// collectionViewHeightLayoutConstraint, +// ]) +// +// collectionView.observe(\.contentSize, options: [.initial, .new]) { [weak self] collectionView, _ in +// guard let self = self else { return } +// self.collectionViewHeightLayoutConstraint.constant = collectionView.contentSize.height +// self.collectionViewHeightDidUpdate.send() +// } +// .store(in: &observations) +// +// self.dataSource = UICollectionViewDiffableDataSource(collectionView: collectionView) { [ +// weak self +// ] collectionView, indexPath, item -> UICollectionViewCell? in +// guard let self = self else { return UICollectionViewCell() } +// +// switch item { +// case .pollOption(let attribute): +// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionCollectionViewCell +// cell.pollOptionView.optionTextField.text = attribute.option.value +// cell.pollOptionView.optionTextField.placeholder = L10n.Scene.Compose.Poll.optionNumber(indexPath.item + 1) +// cell.pollOption +// .receive(on: DispatchQueue.main) +// .assign(to: \.value, on: attribute.option) +// .store(in: &cell.disposeBag) +// cell.delegate = self.composeStatusPollOptionCollectionViewCellDelegate +// if let customEmojiPickerInputViewModel = self.customEmojiPickerInputViewModel { +// ComposeStatusSection.configureCustomEmojiPicker(viewModel: customEmojiPickerInputViewModel, customEmojiReplaceableTextInput: cell.pollOptionView.optionTextField, disposeBag: &cell.disposeBag) +// } +// return cell +// case .pollOptionAppendEntry: +// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollOptionAppendEntryCollectionViewCell.self), for: indexPath) as! ComposeStatusPollOptionAppendEntryCollectionViewCell +// cell.delegate = self.composeStatusPollOptionAppendEntryCollectionViewCellDelegate +// return cell +// case .pollExpiresOption(let attribute): +// let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: ComposeStatusPollExpiresOptionCollectionViewCell.self), for: indexPath) as! ComposeStatusPollExpiresOptionCollectionViewCell +// cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(attribute.expiresOption.value.title), for: .normal) +// attribute.expiresOption +// .receive(on: DispatchQueue.main) +// .sink { [weak cell] expiresOption in +// guard let cell = cell else { return } +// cell.durationButton.setTitle(L10n.Scene.Compose.Poll.durationTime(expiresOption.title), for: .normal) +// } +// .store(in: &cell.disposeBag) +// cell.delegate = self.composeStatusPollExpiresOptionCollectionViewCellDelegate +// return cell +// } +// } +// +// collectionView.dragDelegate = self +// collectionView.dropDelegate = self +// } +// +//} +// +//// MARK: - UICollectionViewDragDelegate +//extension ComposeStatusPollTableViewCell: UICollectionViewDragDelegate { +// +// func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { +// guard let item = dataSource.itemIdentifier(for: indexPath) else { return [] } +// switch item { +// case .pollOption: +// let itemProvider = NSItemProvider(object: String(item.hashValue) as NSString) +// let dragItem = UIDragItem(itemProvider: itemProvider) +// dragItem.localObject = item +// return [dragItem] +// default: +// return [] +// } +// } +// +// func collectionView(_ collectionView: UICollectionView, dragSessionIsRestrictedToDraggingApplication session: UIDragSession) -> Bool { +// // drag to app should be the same app +// return true +// } +//} +// +//// MARK: - UICollectionViewDropDelegate +//extension ComposeStatusPollTableViewCell: UICollectionViewDropDelegate { +// // didUpdate +// func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal { +// guard collectionView.hasActiveDrag, +// let destinationIndexPath = destinationIndexPath, +// let item = dataSource.itemIdentifier(for: destinationIndexPath) +// else { +// return UICollectionViewDropProposal(operation: .forbidden) +// } +// +// switch item { +// case .pollOption: +// return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) +// default: +// return UICollectionViewDropProposal(operation: .cancel) +// } +// } +// +// // performDrop +// func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) { +// guard let dropItem = coordinator.items.first, +// let item = dropItem.dragItem.localObject as? ComposeStatusPollItem, +// case .pollOption = item +// else { return } +// +// guard coordinator.proposal.operation == .move else { return } +// guard let destinationIndexPath = coordinator.destinationIndexPath, +// let _ = collectionView.cellForItem(at: destinationIndexPath) as? ComposeStatusPollOptionCollectionViewCell +// else { return } +// +// var snapshot = dataSource.snapshot() +// guard destinationIndexPath.row < snapshot.itemIdentifiers.count else { return } +// let anchorItem = snapshot.itemIdentifiers[destinationIndexPath.row] +// snapshot.moveItem(item, afterItem: anchorItem) +// dataSource.apply(snapshot) +// +// coordinator.drop(dropItem.dragItem, toItemAt: destinationIndexPath) +// } +//} +// +//extension ComposeStatusPollTableViewCell: UICollectionViewDelegate { +// func collectionView(_ collectionView: UICollectionView, targetIndexPathForMoveFromItemAt originalIndexPath: IndexPath, toProposedIndexPath proposedIndexPath: IndexPath) -> IndexPath { +// logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(originalIndexPath.debugDescription) -> \(proposedIndexPath.debugDescription)") +// +// guard let _ = collectionView.cellForItem(at: proposedIndexPath) as? ComposeStatusPollOptionCollectionViewCell else { +// return originalIndexPath +// } +// +// return proposedIndexPath +// } +//} diff --git a/Mastodon/Scene/Compose/View/ComposeTableView.swift b/MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeTableView.swift similarity index 100% rename from Mastodon/Scene/Compose/View/ComposeTableView.swift rename to MastodonSDK/Sources/MastodonUI/Scene/ComposeContent/View/ComposeTableView.swift diff --git a/Mastodon/Vender/ControlContainableScrollViews.swift b/MastodonSDK/Sources/MastodonUI/Vendor/ControlContainableScrollViews.swift similarity index 80% rename from Mastodon/Vender/ControlContainableScrollViews.swift rename to MastodonSDK/Sources/MastodonUI/Vendor/ControlContainableScrollViews.swift index 057527ce2..79bb71c6f 100644 --- a/Mastodon/Vender/ControlContainableScrollViews.swift +++ b/MastodonSDK/Sources/MastodonUI/Vendor/ControlContainableScrollViews.swift @@ -17,9 +17,9 @@ import UIKit // they feel broken. Feel free to add your own exceptions if you have custom // controls that require swiping or dragging to function. -final class ControlContainableScrollView: UIScrollView { +public final class ControlContainableScrollView: UIScrollView { - override func touchesShouldCancel(in view: UIView) -> Bool { + public override func touchesShouldCancel(in view: UIView) -> Bool { if view is UIControl && !(view is UITextInput) && !(view is UISlider) @@ -32,9 +32,9 @@ final class ControlContainableScrollView: UIScrollView { } -final class ControlContainableTableView: UITableView { +public final class ControlContainableTableView: UITableView { - override func touchesShouldCancel(in view: UIView) -> Bool { + public override func touchesShouldCancel(in view: UIView) -> Bool { if view is UIControl && !(view is UITextInput) && !(view is UISlider) @@ -47,9 +47,9 @@ final class ControlContainableTableView: UITableView { } -final class ControlContainableCollectionView: UICollectionView { +public final class ControlContainableCollectionView: UICollectionView { - override func touchesShouldCancel(in view: UIView) -> Bool { + public override func touchesShouldCancel(in view: UIView) -> Bool { if view is UIControl && !(view is UITextInput) && !(view is UISlider) diff --git a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift index 76897a46f..438baff7e 100644 --- a/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/MediaView+Configuration.swift @@ -9,6 +9,7 @@ import UIKit import Combine import CoreData +import CoreDataStack import Photos import AlamofireImage import MastodonCore @@ -178,3 +179,59 @@ extension MediaView.Configuration { } } + +extension MediaView { + public static func configuration(status: Status) -> [MediaView.Configuration] { + func videoInfo(from attachment: MastodonAttachment) -> MediaView.Configuration.VideoInfo { + MediaView.Configuration.VideoInfo( + aspectRadio: attachment.size, + assetURL: attachment.assetURL, + previewURL: attachment.previewURL, + durationMS: attachment.durationMS + ) + } + + let status = status.reblog ?? status + let attachments = status.attachments + let configurations = attachments.map { attachment -> MediaView.Configuration in + let configuration: MediaView.Configuration = { + switch attachment.kind { + case .image: + let info = MediaView.Configuration.ImageInfo( + aspectRadio: attachment.size, + assetURL: attachment.assetURL + ) + return .init( + info: .image(info: info), + blurhash: attachment.blurhash + ) + case .video: + let info = videoInfo(from: attachment) + return .init( + info: .video(info: info), + blurhash: attachment.blurhash + ) + case .gifv: + let info = videoInfo(from: attachment) + return .init( + info: .gif(info: info), + blurhash: attachment.blurhash + ) + case .audio: + let info = videoInfo(from: attachment) + return .init( + info: .video(info: info), + blurhash: attachment.blurhash + ) + } // end switch + }() + + configuration.load() + configuration.isReveal = status.isMediaSensitive ? status.isSensitiveToggled : true + + return configuration + } + + return configurations + } +} diff --git a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift similarity index 99% rename from Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift rename to MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift index bea08059d..353dfc097 100644 --- a/Mastodon/Scene/Share/View/Content/StatusView+Configuration.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Content/StatusView+Configuration.swift @@ -7,11 +7,9 @@ import UIKit import Combine -import MastodonUI import CoreDataStack import MastodonSDK import MastodonCore -import MastodonUI import MastodonLocalization import MastodonMeta import Meta @@ -125,11 +123,12 @@ extension StatusView { if let authenticationBox = viewModel.authContext?.mastodonAuthenticationBox { Just(inReplyToAccountID) .asyncMap { userID in - return try await AppContext.shared.apiService.accountInfo( + return try await Mastodon.API.Account.accountInfo( + session: .shared, domain: authenticationBox.domain, userID: userID, authorization: authenticationBox.userAuthorization - ) + ).singleOutput() } .sink { completion in // do nothing diff --git a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift b/MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift similarity index 95% rename from Mastodon/Scene/Share/View/Decoration/SawToothView.swift rename to MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift index f154a48e6..1a7a977e6 100644 --- a/Mastodon/Scene/Share/View/Decoration/SawToothView.swift +++ b/MastodonSDK/Sources/MastodonUI/View/Decoration/SawToothView.swift @@ -10,7 +10,7 @@ import UIKit import Combine import MastodonCore -final class SawToothView: UIView { +public final class SawToothView: UIView { static let widthUint = 8 var disposeBag = Set() @@ -41,7 +41,7 @@ final class SawToothView: UIView { setNeedsDisplay() } - override func draw(_ rect: CGRect) { + public override func draw(_ rect: CGRect) { let bezierPath = UIBezierPath() let bottomY = rect.height let topY = 0 diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift similarity index 81% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift index f6c4596ab..cc16e520d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineBottomLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineBottomLoaderTableViewCell.swift @@ -8,18 +8,17 @@ import UIKit import Combine import MastodonCore -import MastodonUI -final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class TimelineBottomLoaderTableViewCell: TimelineLoaderTableViewCell { - override func prepareForReuse() { + public override func prepareForReuse() { super.prepareForReuse() loadMoreLabel.isHidden = true loadMoreButton.isHidden = true } - override func _init() { + public override func _init() { super._init() activityIndicatorView.isHidden = false diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift similarity index 84% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift index f9d58da6a..6e396dc6d 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineLoaderTableViewCell.swift @@ -9,23 +9,22 @@ import UIKit import Combine import MastodonAsset import MastodonCore -import MastodonUI import MastodonLocalization -class TimelineLoaderTableViewCell: UITableViewCell { +open class TimelineLoaderTableViewCell: UITableViewCell { - static let buttonHeight: CGFloat = 44 - static let buttonMargin: CGFloat = 12 - static let cellHeight: CGFloat = buttonHeight + 2 * buttonMargin - static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) + public static let buttonHeight: CGFloat = 44 + public static let buttonMargin: CGFloat = 12 + public static let cellHeight: CGFloat = buttonHeight + 2 * buttonMargin + public static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium)) var disposeBag = Set() private var _disposeBag = Set() - let stackView = UIStackView() + public let stackView = UIStackView() - let loadMoreButton: UIButton = { + public let loadMoreButton: UIButton = { let button = HighlightDimmableButton() button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont button.setTitleColor(ThemeService.tintColor, for: .normal) @@ -34,49 +33,49 @@ class TimelineLoaderTableViewCell: UITableViewCell { return button }() - let loadMoreLabel: UILabel = { + public let loadMoreLabel: UILabel = { let label = UILabel() label.font = TimelineLoaderTableViewCell.labelFont return label }() - let activityIndicatorView: UIActivityIndicatorView = { + public let activityIndicatorView: UIActivityIndicatorView = { let activityIndicatorView = UIActivityIndicatorView(style: .medium) activityIndicatorView.tintColor = Asset.Colors.Label.secondary.color activityIndicatorView.hidesWhenStopped = true return activityIndicatorView }() - override func prepareForReuse() { + public override func prepareForReuse() { super.prepareForReuse() disposeBag.removeAll() } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + public override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) _init() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) _init() } - func startAnimating() { + public func startAnimating() { activityIndicatorView.startAnimating() self.loadMoreButton.isEnabled = false self.loadMoreLabel.textColor = Asset.Colors.Label.secondary.color self.loadMoreLabel.text = L10n.Common.Controls.Timeline.Loader.loadingMissingPosts } - func stopAnimating() { + public func stopAnimating() { activityIndicatorView.stopAnimating() self.loadMoreButton.isEnabled = true self.loadMoreLabel.textColor = ThemeService.tintColor self.loadMoreLabel.text = "" } - func _init() { + open func _init() { selectionStyle = .none backgroundColor = .clear diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift similarity index 90% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift index 406d2a7ec..19f2bbdaa 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell+ViewModel.swift @@ -10,7 +10,7 @@ import Combine import CoreDataStack extension TimelineMiddleLoaderTableViewCell { - class ViewModel { + public class ViewModel { var disposeBag = Set() @Published var isFetching = false @@ -18,7 +18,7 @@ extension TimelineMiddleLoaderTableViewCell { } extension TimelineMiddleLoaderTableViewCell.ViewModel { - func bind(cell: TimelineMiddleLoaderTableViewCell) { + public func bind(cell: TimelineMiddleLoaderTableViewCell) { $isFetching .sink { isFetching in if isFetching { @@ -33,7 +33,7 @@ extension TimelineMiddleLoaderTableViewCell.ViewModel { extension TimelineMiddleLoaderTableViewCell { - func configure( + public func configure( feed: Feed, delegate: TimelineMiddleLoaderTableViewCellDelegate? ) { diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift similarity index 93% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift index 462a22d48..7cb5f2b44 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineMiddleLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineMiddleLoaderTableViewCell.swift @@ -9,13 +9,12 @@ import Combine import CoreData import os.log import UIKit -import MastodonUI -protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { +public protocol TimelineMiddleLoaderTableViewCellDelegate: AnyObject { func timelineMiddleLoaderTableViewCell(_ cell: TimelineMiddleLoaderTableViewCell, loadMoreButtonDidPressed button: UIButton) } -final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { +public final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { weak var delegate: TimelineMiddleLoaderTableViewCellDelegate? @@ -28,7 +27,7 @@ final class TimelineMiddleLoaderTableViewCell: TimelineLoaderTableViewCell { let topSawToothView = SawToothView() let bottomSawToothView = SawToothView() - override func _init() { + public override func _init() { super._init() loadMoreButton.isHidden = false diff --git a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift similarity index 83% rename from Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift rename to MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift index 20c117eab..742614854 100644 --- a/Mastodon/Scene/Share/View/TableviewCell/TimelineTopLoaderTableViewCell.swift +++ b/MastodonSDK/Sources/MastodonUI/View/TableViewCell/TimelineTopLoaderTableViewCell.swift @@ -8,11 +8,9 @@ import UIKit import Combine import MastodonCore -import MastodonUI - -final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { - override func _init() { +public final class TimelineTopLoaderTableViewCell: TimelineLoaderTableViewCell { + public override func _init() { super._init() activityIndicatorView.isHidden = false diff --git a/ShareActionExtension/Scene/ComposeViewController.swift b/ShareActionExtension/Scene/ComposeViewController.swift new file mode 100644 index 000000000..c93c05147 --- /dev/null +++ b/ShareActionExtension/Scene/ComposeViewController.swift @@ -0,0 +1,327 @@ +// +// ComposeViewController.swift +// MastodonShareAction +// +// Created by MainasuK Cirno on 2021-7-16. +// + +import os.log +import UIKit +import Combine +import MastodonUI +import SwiftUI +import MastodonAsset +import MastodonLocalization +import MastodonCore +import MastodonUI + +class ComposeViewController: UIViewController { + + let logger = Logger(subsystem: "ComposeViewController", category: "ViewController") + + let context = AppContext() + + var disposeBag = Set() + private(set) lazy var viewModel = ComposeViewModel(context: context) + + let publishButton: UIButton = { + let button = RoundedEdgesButton(type: .custom) + button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) + button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color), for: .normal) + button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color.withAlphaComponent(0.5)), for: .highlighted) + button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) + button.setTitleColor(.white, for: .normal) + button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height + button.adjustsImageWhenHighlighted = false + return button + }() + + private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:))) + private(set) lazy var publishBarButtonItem: UIBarButtonItem = { + let barButtonItem = UIBarButtonItem(customView: publishButton) + publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) + return barButtonItem + }() + + let activityIndicatorBarButtonItem: UIBarButtonItem = { + let indicatorView = UIActivityIndicatorView(style: .medium) + let barButtonItem = UIBarButtonItem(customView: indicatorView) + indicatorView.startAnimating() + return barButtonItem + }() + + +// let viewSafeAreaDidChange = PassthroughSubject() +// let composeToolbarView = ComposeToolbarView() +// var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! +// let composeToolbarBackgroundView = UIView() +} + +extension ComposeViewController { + + override func viewDidLoad() { + super.viewDidLoad() + +// navigationController?.presentationController?.delegate = self +// +// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) +// ThemeService.shared.currentTheme +// .receive(on: DispatchQueue.main) +// .sink { [weak self] theme in +// guard let self = self else { return } +// self.setupBackgroundColor(theme: theme) +// } +// .store(in: &disposeBag) +// +// navigationItem.leftBarButtonItem = cancelBarButtonItem +// viewModel.isBusy +// .receive(on: DispatchQueue.main) +// .sink { [weak self] isBusy in +// guard let self = self else { return } +// self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem +// } +// .store(in: &disposeBag) +// +// let hostingViewController = UIHostingController( +// rootView: ComposeView().environmentObject(viewModel.composeViewModel) +// ) +// addChild(hostingViewController) +// view.addSubview(hostingViewController.view) +// hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(hostingViewController.view) +// NSLayoutConstraint.activate([ +// hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), +// hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), +// ]) +// hostingViewController.didMove(toParent: self) +// +// composeToolbarView.translatesAutoresizingMaskIntoConstraints = false +// view.addSubview(composeToolbarView) +// composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) +// NSLayoutConstraint.activate([ +// composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), +// composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), +// composeToolbarViewBottomLayoutConstraint, +// composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), +// ]) +// composeToolbarView.preservesSuperviewLayoutMargins = true +// composeToolbarView.delegate = self +// +// composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false +// view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) +// NSLayoutConstraint.activate([ +// composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), +// composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), +// composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), +// view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), +// ]) +// +// // FIXME: using iOS 15 toolbar for .keyboard placement +// let keyboardEventPublishers = Publishers.CombineLatest3( +// KeyboardResponderService.shared.isShow, +// KeyboardResponderService.shared.state, +// KeyboardResponderService.shared.endFrame +// ) +// +// Publishers.CombineLatest( +// keyboardEventPublishers, +// viewSafeAreaDidChange +// ) +// .sink(receiveValue: { [weak self] keyboardEvents, _ in +// guard let self = self else { return } +// +// let (isShow, state, endFrame) = keyboardEvents +// guard isShow, state == .dock else { +// UIView.animate(withDuration: 0.3) { +// self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom +// self.view.layoutIfNeeded() +// } +// return +// } +// // isShow AND dock state +// +// UIView.animate(withDuration: 0.3) { +// self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height +// self.view.layoutIfNeeded() +// } +// }) +// .store(in: &disposeBag) +// +// // bind visibility toolbar UI +// Publishers.CombineLatest( +// viewModel.selectedStatusVisibility, +// viewModel.traitCollectionDidChangePublisher +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] type, _ in +// guard let self = self else { return } +// let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) +// self.composeToolbarView.visibilityButton.setImage(image, for: .normal) +// self.composeToolbarView.activeVisibilityType.value = type +// } +// .store(in: &disposeBag) +// +// // bind counter +// viewModel.characterCount +// .receive(on: DispatchQueue.main) +// .sink { [weak self] characterCount in +// guard let self = self else { return } +// let count = ShareViewModel.composeContentLimit - characterCount +// self.composeToolbarView.characterCountLabel.text = "\(count)" +// switch count { +// case _ where count < 0: +// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) +// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color +// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count)) +// default: +// self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) +// self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color +// self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count) +// } +// } +// .store(in: &disposeBag) +// +// // bind valid +// viewModel.isValid +// .receive(on: DispatchQueue.main) +// .assign(to: \.isEnabled, on: publishButton) +// .store(in: &disposeBag) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + +// viewModel.viewDidAppear.value = true +// viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] +// +// viewModel.composeViewModel.viewDidAppear = true + } + + override func viewSafeAreaInsetsDidChange() { + super.viewSafeAreaInsetsDidChange() + +// viewSafeAreaDidChange.send() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + +// viewModel.traitCollectionDidChangePublisher.send() + } + +} + +//extension ComposeViewController { +// private func setupBackgroundColor(theme: Theme) { +// view.backgroundColor = theme.systemElevatedBackgroundColor +// viewModel.composeViewModel.backgroundColor = theme.systemElevatedBackgroundColor +// composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor +// +// let barAppearance = UINavigationBarAppearance() +// barAppearance.configureWithDefaultBackground() +// barAppearance.backgroundColor = theme.navigationBarBackgroundColor +// navigationItem.standardAppearance = barAppearance +// navigationItem.compactAppearance = barAppearance +// navigationItem.scrollEdgeAppearance = barAppearance +// } +// +// private func showDismissConfirmAlertController() { +// let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension +// let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in +// self.extensionContext?.cancelRequest(withError: ShareViewModel.ShareError.userCancelShare) +// } +// alertController.addAction(discardAction) +// let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil) +// alertController.addAction(okAction) +// self.present(alertController, animated: true, completion: nil) +// } +//} +// +extension ComposeViewController { + @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + +// showDismissConfirmAlertController() + } + + @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { + logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") + +// viewModel.isPublishing.value = true +// +// viewModel.publish() +// .delay(for: 2, scheduler: DispatchQueue.main) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] completion in +// guard let self = self else { return } +// self.viewModel.isPublishing.value = false +// +// switch completion { +// case .failure: +// let alertController = UIAlertController( +// title: L10n.Common.Alerts.PublishPostFailure.title, +// message: L10n.Common.Alerts.PublishPostFailure.message, +// preferredStyle: .actionSheet // can not use alert in extension +// ) +// let okAction = UIAlertAction( +// title: L10n.Common.Controls.Actions.ok, +// style: .cancel, +// handler: nil +// ) +// alertController.addAction(okAction) +// self.present(alertController, animated: true, completion: nil) +// case .finished: +// self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal) +// self.publishButton.isUserInteractionEnabled = false +// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in +// guard let self = self else { return } +// self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) +// } +// } +// } receiveValue: { response in +// // do nothing +// } +// .store(in: &disposeBag) + } +} + +//// MARK - ComposeToolbarViewDelegate +//extension ComposeViewController: ComposeToolbarViewDelegate { +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// +// withAnimation { +// viewModel.composeViewModel.isContentWarningComposing.toggle() +// } +// } +// +// func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// +// viewModel.selectedStatusVisibility.value = type +// } +// +//} +// +//// MARK: - UIAdaptivePresentationControllerDelegate +//extension ComposeViewController: UIAdaptivePresentationControllerDelegate { +// +// func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { +// return viewModel.shouldDismiss.value +// } +// +// func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// showDismissConfirmAlertController() +// +// } +// +// func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { +// os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) +// } +// +//} diff --git a/ShareActionExtension/Scene/ComposeViewModel.swift b/ShareActionExtension/Scene/ComposeViewModel.swift new file mode 100644 index 000000000..4470cfbe8 --- /dev/null +++ b/ShareActionExtension/Scene/ComposeViewModel.swift @@ -0,0 +1,417 @@ +// +// ComposeViewModel.swift +// MastodonShareAction +// +// Created by MainasuK Cirno on 2021-7-16. +// + +import os.log +import Foundation +import Combine +import CoreData +import CoreDataStack +import MastodonSDK +import MastodonUI +import SwiftUI +import UniformTypeIdentifiers +import MastodonAsset +import MastodonLocalization +import MastodonUI +import MastodonCore + +final class ComposeViewModel { + + let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel") + + var disposeBag = Set() + + static let composeContentLimit: Int = 500 + + // input + let context: AppContext + +// private var coreDataStack: CoreDataStack? +// var managedObjectContext: NSManagedObjectContext? +// var api: APIService? +// +// var inputItems = CurrentValueSubject<[NSExtensionItem], Never>([]) +// let viewDidAppear = CurrentValueSubject(false) +// let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit +// let selectedStatusVisibility = CurrentValueSubject(.public) +// +// // output +// let authentication = CurrentValueSubject?, Never>(nil) +// let isFetchAuthentication = CurrentValueSubject(true) +// let isPublishing = CurrentValueSubject(false) +// let isBusy = CurrentValueSubject(true) +// let isValid = CurrentValueSubject(false) +// let shouldDismiss = CurrentValueSubject(true) +// let composeViewModel = ComposeViewModel() +// let characterCount = CurrentValueSubject(0) + + init(context: AppContext) { + self.context = context + // end init + +// viewDidAppear.receive(on: DispatchQueue.main) +// .removeDuplicates() +// .sink { [weak self] viewDidAppear in +// guard let self = self else { return } +// guard viewDidAppear else { return } +// self.setupCoreData() +// } +// .store(in: &disposeBag) +// +// Publishers.CombineLatest( +// inputItems.removeDuplicates(), +// viewDidAppear.removeDuplicates() +// ) +// .receive(on: DispatchQueue.main) +// .sink { [weak self] inputItems, _ in +// guard let self = self else { return } +// self.parse(inputItems: inputItems) +// } +// .store(in: &disposeBag) +// +// // bind authentication loading state +// authentication +// .map { result in result == nil } +// .assign(to: \.value, on: isFetchAuthentication) +// .store(in: &disposeBag) +// +// // bind user locked state +// authentication +// .compactMap { result -> Bool? in +// guard let result = result else { return nil } +// switch result { +// case .success(let authentication): +// return authentication.user.locked +// case .failure: +// return nil +// } +// } +// .map { locked -> ComposeToolbarView.VisibilitySelectionType in +// locked ? .private : .public +// } +// .assign(to: \.value, on: selectedStatusVisibility) +// .store(in: &disposeBag) +// +// // bind author +// authentication +// .receive(on: DispatchQueue.main) +// .sink { [weak self] result in +// guard let self = self else { return } +// guard let result = result else { return } +// switch result { +// case .success(let authentication): +// self.composeViewModel.avatarImageURL = authentication.user.avatarImageURL() +// self.composeViewModel.authorName = authentication.user.displayNameWithFallback +// self.composeViewModel.authorUsername = "@" + authentication.user.username +// case .failure: +// self.composeViewModel.avatarImageURL = nil +// self.composeViewModel.authorName = " " +// self.composeViewModel.authorUsername = " " +// } +// } +// .store(in: &disposeBag) +// +// // bind authentication to compose view model +// authentication +// .map { result -> MastodonAuthentication? in +// guard let result = result else { return nil } +// switch result { +// case .success(let authentication): +// return authentication +// case .failure: +// return nil +// } +// } +// .assign(to: &composeViewModel.$authentication) +// +// // bind isBusy +// Publishers.CombineLatest( +// isFetchAuthentication, +// isPublishing +// ) +// .receive(on: DispatchQueue.main) +// .map { $0 || $1 } +// .assign(to: \.value, on: isBusy) +// .store(in: &disposeBag) +// +// // pass initial i18n string +// composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder +// composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder +// composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight +// +// // bind compose bar button item UI state +// let isComposeContentEmpty = composeViewModel.$statusContent +// .map { $0.isEmpty } +// +// isComposeContentEmpty +// .assign(to: \.value, on: shouldDismiss) +// .store(in: &disposeBag) +// +// let isComposeContentValid = composeViewModel.$characterCount +// .map { characterCount -> Bool in +// return characterCount <= ShareViewModel.composeContentLimit +// } +// let isMediaEmpty = composeViewModel.$attachmentViewModels +// .map { $0.isEmpty } +// let isMediaUploadAllSuccess = composeViewModel.$attachmentViewModels +// .map { viewModels in +// viewModels.allSatisfy { $0.uploadStateMachineSubject.value is StatusAttachmentViewModel.UploadState.Finish } +// } +// +// let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( +// isComposeContentEmpty, +// isComposeContentValid, +// isMediaEmpty, +// isMediaUploadAllSuccess +// ) +// .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in +// if isMediaEmpty { +// return isComposeContentValid && !isComposeContentEmpty +// } else { +// return isComposeContentValid && isMediaUploadAllSuccess +// } +// } +// .eraseToAnyPublisher() +// +// let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest( +// isComposeContentEmpty, +// isComposeContentValid +// ) +// .map { isComposeContentEmpty, isComposeContentValid -> Bool in +// return isComposeContentValid && !isComposeContentEmpty +// } +// .eraseToAnyPublisher() +// +// Publishers.CombineLatest( +// isPublishBarButtonItemEnabledPrecondition1, +// isPublishBarButtonItemEnabledPrecondition2 +// ) +// .map { $0 && $1 } +// .assign(to: \.value, on: isValid) +// .store(in: &disposeBag) +// +// // bind counter +// composeViewModel.$characterCount +// .assign(to: \.value, on: characterCount) +// .store(in: &disposeBag) +// +// // setup theme +// setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) +// ThemeService.shared.currentTheme +// .receive(on: DispatchQueue.main) +// .sink { [weak self] theme in +// guard let self = self else { return } +// self.setupBackgroundColor(theme: theme) +// } +// .store(in: &disposeBag) + } + + private func setupBackgroundColor(theme: Theme) { +// composeViewModel.contentWarningBackgroundColor = Color(theme.contentWarningOverlayBackgroundColor) + } + +} + +//extension ShareViewModel { +// enum ShareError: Error { +// case `internal`(error: Error) +// case userCancelShare +// case missingAuthentication +// } +//} + +extension ComposeViewModel { +// private func setupCoreData() { +// logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") +// DispatchQueue.global().async { +// let _coreDataStack = CoreDataStack() +// self.coreDataStack = _coreDataStack +// self.managedObjectContext = _coreDataStack.persistentContainer.viewContext +// +// _coreDataStack.didFinishLoad +// .receive(on: RunLoop.main) +// .sink { [weak self] didFinishLoad in +// guard let self = self else { return } +// guard didFinishLoad else { return } +// guard let managedObjectContext = self.managedObjectContext else { return } +// +// +// self.api = APIService(backgroundManagedObjectContext: _coreDataStack.newTaskContext()) +// +// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication…") +// managedObjectContext.perform { +// do { +// let request = MastodonAuthentication.sortedFetchRequest +// let authentications = try managedObjectContext.fetch(request) +// let authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first +// guard let activeAuthentication = authentication else { +// self.authentication.value = .failure(ShareError.missingAuthentication) +// return +// } +// self.authentication.value = .success(activeAuthentication) +// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication success \(activeAuthentication.userID)") +// } catch { +// self.authentication.value = .failure(ShareError.internal(error: error)) +// self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication fail \(error.localizedDescription)") +// assertionFailure(error.localizedDescription) +// } +// } +// } +// .store(in: &self.disposeBag) +// } +// } +} + +//extension ShareViewModel { +// func parse(inputItems: [NSExtensionItem]) { +// var itemProviders: [NSItemProvider] = [] +// +// for item in inputItems { +// itemProviders.append(contentsOf: item.attachments ?? []) +// } +// +// let _textProvider = itemProviders.first { provider in +// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: []) +// } +// +// let _urlProvider = itemProviders.first { provider in +// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: []) +// } +// +// let _movieProvider = itemProviders.first { provider in +// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) +// } +// +// let imageProviders = itemProviders.filter { provider in +// return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) +// } +// +// Task { @MainActor in +// async let text = ShareViewModel.loadText(textProvider: _textProvider) +// async let url = ShareViewModel.loadURL(textProvider: _urlProvider) +// +// let content = await [text, url] +// .compactMap { $0 } +// .joined(separator: " ") +// self.composeViewModel.statusContent = content +// } +// +// guard let api = self.api else { return } +// +// if let movieProvider = _movieProvider { +// composeViewModel.setupAttachmentViewModels([ +// StatusAttachmentViewModel(api: api, itemProvider: movieProvider) +// ]) +// } else if !imageProviders.isEmpty { +// let viewModels = imageProviders.map { provider in +// StatusAttachmentViewModel(api: api, itemProvider: provider) +// } +// composeViewModel.setupAttachmentViewModels(viewModels) +// } +// +// } +// +// private static func loadText(textProvider: NSItemProvider?) async -> String? { +// guard let textProvider = textProvider else { return nil } +// do { +// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier) +// guard let text = item as? String else { return nil } +// return text +// } catch { +// return nil +// } +// } +// +// private static func loadURL(textProvider: NSItemProvider?) async -> String? { +// guard let textProvider = textProvider else { return nil } +// do { +// let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier) +// guard let url = item as? URL else { return nil } +// return url.absoluteString +// } catch { +// return nil +// } +// } +// +//} +// +//extension ShareViewModel { +// func publish() -> AnyPublisher, Error> { +// guard let authentication = composeViewModel.authentication, +// let api = self.api +// else { +// return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() +// } +// let authenticationBox = MastodonAuthenticationBox( +// authenticationRecord: .init(objectID: authentication.objectID), +// domain: authentication.domain, +// userID: authentication.userID, +// appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), +// userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) +// ) +// +// let domain = authentication.domain +// let attachmentViewModels = composeViewModel.attachmentViewModels +// let mediaIDs = attachmentViewModels.compactMap { viewModel in +// viewModel.attachment.value?.id +// } +// let sensitive: Bool = composeViewModel.isContentWarningComposing +// let spoilerText: String? = { +// let text = composeViewModel.contentWarningContent +// guard !text.isEmpty else { return nil } +// return text +// }() +// let visibility = selectedStatusVisibility.value.visibility +// +// let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { +// var subscriptions: [AnyPublisher, Error>] = [] +// for attachmentViewModel in attachmentViewModels { +// guard let attachmentID = attachmentViewModel.attachment.value?.id else { continue } +// let description = attachmentViewModel.descriptionContent.trimmingCharacters(in: .whitespacesAndNewlines) +// guard !description.isEmpty else { continue } +// let query = Mastodon.API.Media.UpdateMediaQuery( +// file: nil, +// thumbnail: nil, +// description: description, +// focus: nil +// ) +// let subscription = api.updateMedia( +// domain: domain, +// attachmentID: attachmentID, +// query: query, +// mastodonAuthenticationBox: authenticationBox +// ) +// subscriptions.append(subscription) +// } +// return subscriptions +// }() +// +// let status = composeViewModel.statusContent +// +// return Publishers.MergeMany(updateMediaQuerySubscriptions) +// .collect() +// .asyncMap { attachments in +// let query = Mastodon.API.Statuses.PublishStatusQuery( +// status: status, +// mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, +// pollOptions: nil, +// pollExpiresIn: nil, +// inReplyToID: nil, +// sensitive: sensitive, +// spoilerText: spoilerText, +// visibility: visibility +// ) +// return try await api.publishStatus( +// domain: domain, +// idempotencyKey: nil, // FIXME: +// query: query, +// authenticationBox: authenticationBox +// ) +// } +// .eraseToAnyPublisher() +// } +//} diff --git a/ShareActionExtension/Scene/ShareViewController.swift b/ShareActionExtension/Scene/ShareViewController.swift deleted file mode 100644 index a142aa69b..000000000 --- a/ShareActionExtension/Scene/ShareViewController.swift +++ /dev/null @@ -1,325 +0,0 @@ -// -// ShareViewController.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import UIKit -import Combine -import MastodonUI -import SwiftUI -import MastodonAsset -import MastodonLocalization -import MastodonCore -import MastodonUI - -class ShareViewController: UIViewController { - - let logger = Logger(subsystem: "ShareViewController", category: "UI") - - var disposeBag = Set() - let viewModel = ShareViewModel() - - let publishButton: UIButton = { - let button = RoundedEdgesButton(type: .custom) - button.setTitle(L10n.Scene.Compose.composeAction, for: .normal) - button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold) - button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color), for: .normal) - button.setBackgroundImage(.placeholder(color: Asset.Colors.brand.color.withAlphaComponent(0.5)), for: .highlighted) - button.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled) - button.setTitleColor(.white, for: .normal) - button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16) // set 28pt height - button.adjustsImageWhenHighlighted = false - return button - }() - - private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ShareViewController.cancelBarButtonItemPressed(_:))) - private(set) lazy var publishBarButtonItem: UIBarButtonItem = { - let barButtonItem = UIBarButtonItem(customView: publishButton) - publishButton.addTarget(self, action: #selector(ShareViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside) - return barButtonItem - }() - - let activityIndicatorBarButtonItem: UIBarButtonItem = { - let indicatorView = UIActivityIndicatorView(style: .medium) - let barButtonItem = UIBarButtonItem(customView: indicatorView) - indicatorView.startAnimating() - return barButtonItem - }() - - - let viewSafeAreaDidChange = PassthroughSubject() - let composeToolbarView = ComposeToolbarView() - var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint! - let composeToolbarBackgroundView = UIView() -} - -extension ShareViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - navigationController?.presentationController?.delegate = self - - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) - - navigationItem.leftBarButtonItem = cancelBarButtonItem - viewModel.isBusy - .receive(on: DispatchQueue.main) - .sink { [weak self] isBusy in - guard let self = self else { return } - self.navigationItem.rightBarButtonItem = isBusy ? self.activityIndicatorBarButtonItem : self.publishBarButtonItem - } - .store(in: &disposeBag) - - let hostingViewController = UIHostingController( - rootView: ComposeView().environmentObject(viewModel.composeViewModel) - ) - addChild(hostingViewController) - view.addSubview(hostingViewController.view) - hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(hostingViewController.view) - NSLayoutConstraint.activate([ - hostingViewController.view.topAnchor.constraint(equalTo: view.topAnchor), - hostingViewController.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - hostingViewController.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - hostingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - hostingViewController.didMove(toParent: self) - - composeToolbarView.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(composeToolbarView) - composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor) - NSLayoutConstraint.activate([ - composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - composeToolbarViewBottomLayoutConstraint, - composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight), - ]) - composeToolbarView.preservesSuperviewLayoutMargins = true - composeToolbarView.delegate = self - - composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false - view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView) - NSLayoutConstraint.activate([ - composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor), - composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor), - composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor), - view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor), - ]) - - // FIXME: using iOS 15 toolbar for .keyboard placement - let keyboardEventPublishers = Publishers.CombineLatest3( - KeyboardResponderService.shared.isShow, - KeyboardResponderService.shared.state, - KeyboardResponderService.shared.endFrame - ) - - Publishers.CombineLatest( - keyboardEventPublishers, - viewSafeAreaDidChange - ) - .sink(receiveValue: { [weak self] keyboardEvents, _ in - guard let self = self else { return } - - let (isShow, state, endFrame) = keyboardEvents - guard isShow, state == .dock else { - UIView.animate(withDuration: 0.3) { - self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom - self.view.layoutIfNeeded() - } - return - } - // isShow AND dock state - - UIView.animate(withDuration: 0.3) { - self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height - self.view.layoutIfNeeded() - } - }) - .store(in: &disposeBag) - - // bind visibility toolbar UI - Publishers.CombineLatest( - viewModel.selectedStatusVisibility, - viewModel.traitCollectionDidChangePublisher - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] type, _ in - guard let self = self else { return } - let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle) - self.composeToolbarView.visibilityButton.setImage(image, for: .normal) - self.composeToolbarView.activeVisibilityType.value = type - } - .store(in: &disposeBag) - - // bind counter - viewModel.characterCount - .receive(on: DispatchQueue.main) - .sink { [weak self] characterCount in - guard let self = self else { return } - let count = ShareViewModel.composeContentLimit - characterCount - self.composeToolbarView.characterCountLabel.text = "\(count)" - switch count { - case _ where count < 0: - self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold) - self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.danger.color - self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count)) - default: - self.composeToolbarView.characterCountLabel.font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular) - self.composeToolbarView.characterCountLabel.textColor = Asset.Colors.Label.secondary.color - self.composeToolbarView.characterCountLabel.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count) - } - } - .store(in: &disposeBag) - - // bind valid - viewModel.isValid - .receive(on: DispatchQueue.main) - .assign(to: \.isEnabled, on: publishButton) - .store(in: &disposeBag) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - - viewModel.viewDidAppear.value = true - viewModel.inputItems.value = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? [] - - viewModel.composeViewModel.viewDidAppear = true - } - - override func viewSafeAreaInsetsDidChange() { - super.viewSafeAreaInsetsDidChange() - - viewSafeAreaDidChange.send() - } - - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - super.traitCollectionDidChange(previousTraitCollection) - - viewModel.traitCollectionDidChangePublisher.send() - } - -} - -extension ShareViewController { - private func setupBackgroundColor(theme: Theme) { - view.backgroundColor = theme.systemElevatedBackgroundColor - viewModel.composeViewModel.backgroundColor = theme.systemElevatedBackgroundColor - composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor - - let barAppearance = UINavigationBarAppearance() - barAppearance.configureWithDefaultBackground() - barAppearance.backgroundColor = theme.navigationBarBackgroundColor - navigationItem.standardAppearance = barAppearance - navigationItem.compactAppearance = barAppearance - navigationItem.scrollEdgeAppearance = barAppearance - } - - private func showDismissConfirmAlertController() { - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) // can not use alert in extension - let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { _ in - self.extensionContext?.cancelRequest(withError: ShareViewModel.ShareError.userCancelShare) - } - alertController.addAction(discardAction) - let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .cancel, handler: nil) - alertController.addAction(okAction) - self.present(alertController, animated: true, completion: nil) - } -} - -extension ShareViewController { - @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - - showDismissConfirmAlertController() - } - - @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - - viewModel.isPublishing.value = true - - viewModel.publish() - .delay(for: 2, scheduler: DispatchQueue.main) - .receive(on: DispatchQueue.main) - .sink { [weak self] completion in - guard let self = self else { return } - self.viewModel.isPublishing.value = false - - switch completion { - case .failure: - let alertController = UIAlertController( - title: L10n.Common.Alerts.PublishPostFailure.title, - message: L10n.Common.Alerts.PublishPostFailure.message, - preferredStyle: .actionSheet // can not use alert in extension - ) - let okAction = UIAlertAction( - title: L10n.Common.Controls.Actions.ok, - style: .cancel, - handler: nil - ) - alertController.addAction(okAction) - self.present(alertController, animated: true, completion: nil) - case .finished: - self.publishButton.setTitle(L10n.Common.Controls.Actions.done, for: .normal) - self.publishButton.isUserInteractionEnabled = false - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in - guard let self = self else { return } - self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) - } - } - } receiveValue: { response in - // do nothing - } - .store(in: &disposeBag) - } -} - -// MARK - ComposeToolbarViewDelegate -extension ShareViewController: ComposeToolbarViewDelegate { - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: UIButton) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - - withAnimation { - viewModel.composeViewModel.isContentWarningComposing.toggle() - } - } - - func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: UIButton, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - - viewModel.selectedStatusVisibility.value = type - } - -} - -// MARK: - UIAdaptivePresentationControllerDelegate -extension ShareViewController: UIAdaptivePresentationControllerDelegate { - - func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool { - return viewModel.shouldDismiss.value - } - - func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - showDismissConfirmAlertController() - - } - - func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { - os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function) - } - -} diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift deleted file mode 100644 index 63a7132f6..000000000 --- a/ShareActionExtension/Scene/ShareViewModel.swift +++ /dev/null @@ -1,412 +0,0 @@ -// -// ShareViewModel.swift -// MastodonShareAction -// -// Created by MainasuK Cirno on 2021-7-16. -// - -import os.log -import Foundation -import Combine -import CoreData -import CoreDataStack -import MastodonSDK -import MastodonUI -import SwiftUI -import UniformTypeIdentifiers -import MastodonAsset -import MastodonLocalization -import MastodonUI -import MastodonCore - -final class ShareViewModel { - - let logger = Logger(subsystem: "ShareViewModel", category: "logic") - - var disposeBag = Set() - - static let composeContentLimit: Int = 500 - - // input - private var coreDataStack: CoreDataStack? - var managedObjectContext: NSManagedObjectContext? - var api: APIService? - - var inputItems = CurrentValueSubject<[NSExtensionItem], Never>([]) - let viewDidAppear = CurrentValueSubject(false) - let traitCollectionDidChangePublisher = CurrentValueSubject(Void()) // use CurrentValueSubject to make initial event emit - let selectedStatusVisibility = CurrentValueSubject(.public) - - // output - let authentication = CurrentValueSubject?, Never>(nil) - let isFetchAuthentication = CurrentValueSubject(true) - let isPublishing = CurrentValueSubject(false) - let isBusy = CurrentValueSubject(true) - let isValid = CurrentValueSubject(false) - let shouldDismiss = CurrentValueSubject(true) - let composeViewModel = ComposeViewModel() - let characterCount = CurrentValueSubject(0) - - init() { - viewDidAppear.receive(on: DispatchQueue.main) - .removeDuplicates() - .sink { [weak self] viewDidAppear in - guard let self = self else { return } - guard viewDidAppear else { return } - self.setupCoreData() - } - .store(in: &disposeBag) - - Publishers.CombineLatest( - inputItems.removeDuplicates(), - viewDidAppear.removeDuplicates() - ) - .receive(on: DispatchQueue.main) - .sink { [weak self] inputItems, _ in - guard let self = self else { return } - self.parse(inputItems: inputItems) - } - .store(in: &disposeBag) - - // bind authentication loading state - authentication - .map { result in result == nil } - .assign(to: \.value, on: isFetchAuthentication) - .store(in: &disposeBag) - - // bind user locked state - authentication - .compactMap { result -> Bool? in - guard let result = result else { return nil } - switch result { - case .success(let authentication): - return authentication.user.locked - case .failure: - return nil - } - } - .map { locked -> ComposeToolbarView.VisibilitySelectionType in - locked ? .private : .public - } - .assign(to: \.value, on: selectedStatusVisibility) - .store(in: &disposeBag) - - // bind author - authentication - .receive(on: DispatchQueue.main) - .sink { [weak self] result in - guard let self = self else { return } - guard let result = result else { return } - switch result { - case .success(let authentication): - self.composeViewModel.avatarImageURL = authentication.user.avatarImageURL() - self.composeViewModel.authorName = authentication.user.displayNameWithFallback - self.composeViewModel.authorUsername = "@" + authentication.user.username - case .failure: - self.composeViewModel.avatarImageURL = nil - self.composeViewModel.authorName = " " - self.composeViewModel.authorUsername = " " - } - } - .store(in: &disposeBag) - - // bind authentication to compose view model - authentication - .map { result -> MastodonAuthentication? in - guard let result = result else { return nil } - switch result { - case .success(let authentication): - return authentication - case .failure: - return nil - } - } - .assign(to: &composeViewModel.$authentication) - - // bind isBusy - Publishers.CombineLatest( - isFetchAuthentication, - isPublishing - ) - .receive(on: DispatchQueue.main) - .map { $0 || $1 } - .assign(to: \.value, on: isBusy) - .store(in: &disposeBag) - - // pass initial i18n string - composeViewModel.statusPlaceholder = L10n.Scene.Compose.contentInputPlaceholder - composeViewModel.contentWarningPlaceholder = L10n.Scene.Compose.ContentWarning.placeholder - composeViewModel.toolbarHeight = ComposeToolbarView.toolbarHeight - - // bind compose bar button item UI state - let isComposeContentEmpty = composeViewModel.$statusContent - .map { $0.isEmpty } - - isComposeContentEmpty - .assign(to: \.value, on: shouldDismiss) - .store(in: &disposeBag) - - let isComposeContentValid = composeViewModel.$characterCount - .map { characterCount -> Bool in - return characterCount <= ShareViewModel.composeContentLimit - } - let isMediaEmpty = composeViewModel.$attachmentViewModels - .map { $0.isEmpty } - let isMediaUploadAllSuccess = composeViewModel.$attachmentViewModels - .map { viewModels in - viewModels.allSatisfy { $0.uploadStateMachineSubject.value is StatusAttachmentViewModel.UploadState.Finish } - } - - let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4( - isComposeContentEmpty, - isComposeContentValid, - isMediaEmpty, - isMediaUploadAllSuccess - ) - .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in - if isMediaEmpty { - return isComposeContentValid && !isComposeContentEmpty - } else { - return isComposeContentValid && isMediaUploadAllSuccess - } - } - .eraseToAnyPublisher() - - let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest( - isComposeContentEmpty, - isComposeContentValid - ) - .map { isComposeContentEmpty, isComposeContentValid -> Bool in - return isComposeContentValid && !isComposeContentEmpty - } - .eraseToAnyPublisher() - - Publishers.CombineLatest( - isPublishBarButtonItemEnabledPrecondition1, - isPublishBarButtonItemEnabledPrecondition2 - ) - .map { $0 && $1 } - .assign(to: \.value, on: isValid) - .store(in: &disposeBag) - - // bind counter - composeViewModel.$characterCount - .assign(to: \.value, on: characterCount) - .store(in: &disposeBag) - - // setup theme - setupBackgroundColor(theme: ThemeService.shared.currentTheme.value) - ThemeService.shared.currentTheme - .receive(on: DispatchQueue.main) - .sink { [weak self] theme in - guard let self = self else { return } - self.setupBackgroundColor(theme: theme) - } - .store(in: &disposeBag) - } - - private func setupBackgroundColor(theme: Theme) { - composeViewModel.contentWarningBackgroundColor = Color(theme.contentWarningOverlayBackgroundColor) - } - -} - -extension ShareViewModel { - enum ShareError: Error { - case `internal`(error: Error) - case userCancelShare - case missingAuthentication - } -} - -extension ShareViewModel { - private func setupCoreData() { - logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public)") - DispatchQueue.global().async { - let _coreDataStack = CoreDataStack() - self.coreDataStack = _coreDataStack - self.managedObjectContext = _coreDataStack.persistentContainer.viewContext - - _coreDataStack.didFinishLoad - .receive(on: RunLoop.main) - .sink { [weak self] didFinishLoad in - guard let self = self else { return } - guard didFinishLoad else { return } - guard let managedObjectContext = self.managedObjectContext else { return } - - - self.api = APIService(backgroundManagedObjectContext: _coreDataStack.newTaskContext()) - - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication…") - managedObjectContext.perform { - do { - let request = MastodonAuthentication.sortedFetchRequest - let authentications = try managedObjectContext.fetch(request) - let authentication = authentications.sorted(by: { $0.activedAt > $1.activedAt }).first - guard let activeAuthentication = authentication else { - self.authentication.value = .failure(ShareError.missingAuthentication) - return - } - self.authentication.value = .success(activeAuthentication) - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication success \(activeAuthentication.userID)") - } catch { - self.authentication.value = .failure(ShareError.internal(error: error)) - self.logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): fetch authentication fail \(error.localizedDescription)") - assertionFailure(error.localizedDescription) - } - } - } - .store(in: &self.disposeBag) - } - } -} - -extension ShareViewModel { - func parse(inputItems: [NSExtensionItem]) { - var itemProviders: [NSItemProvider] = [] - - for item in inputItems { - itemProviders.append(contentsOf: item.attachments ?? []) - } - - let _textProvider = itemProviders.first { provider in - return provider.hasRepresentationConforming(toTypeIdentifier: UTType.plainText.identifier, fileOptions: []) - } - - let _urlProvider = itemProviders.first { provider in - return provider.hasRepresentationConforming(toTypeIdentifier: UTType.url.identifier, fileOptions: []) - } - - let _movieProvider = itemProviders.first { provider in - return provider.hasRepresentationConforming(toTypeIdentifier: UTType.movie.identifier, fileOptions: []) - } - - let imageProviders = itemProviders.filter { provider in - return provider.hasRepresentationConforming(toTypeIdentifier: UTType.image.identifier, fileOptions: []) - } - - Task { @MainActor in - async let text = ShareViewModel.loadText(textProvider: _textProvider) - async let url = ShareViewModel.loadURL(textProvider: _urlProvider) - - let content = await [text, url] - .compactMap { $0 } - .joined(separator: " ") - self.composeViewModel.statusContent = content - } - - guard let api = self.api else { return } - - if let movieProvider = _movieProvider { - composeViewModel.setupAttachmentViewModels([ - StatusAttachmentViewModel(api: api, itemProvider: movieProvider) - ]) - } else if !imageProviders.isEmpty { - let viewModels = imageProviders.map { provider in - StatusAttachmentViewModel(api: api, itemProvider: provider) - } - composeViewModel.setupAttachmentViewModels(viewModels) - } - - } - - private static func loadText(textProvider: NSItemProvider?) async -> String? { - guard let textProvider = textProvider else { return nil } - do { - let item = try await textProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier) - guard let text = item as? String else { return nil } - return text - } catch { - return nil - } - } - - private static func loadURL(textProvider: NSItemProvider?) async -> String? { - guard let textProvider = textProvider else { return nil } - do { - let item = try await textProvider.loadItem(forTypeIdentifier: UTType.url.identifier) - guard let url = item as? URL else { return nil } - return url.absoluteString - } catch { - return nil - } - } - -} - -extension ShareViewModel { - func publish() -> AnyPublisher, Error> { - guard let authentication = composeViewModel.authentication, - let api = self.api - else { - return Fail(error: APIService.APIError.implicit(.authenticationMissing)).eraseToAnyPublisher() - } - let authenticationBox = MastodonAuthenticationBox( - authenticationRecord: .init(objectID: authentication.objectID), - domain: authentication.domain, - userID: authentication.userID, - appAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.appAccessToken), - userAuthorization: Mastodon.API.OAuth.Authorization(accessToken: authentication.userAccessToken) - ) - - let domain = authentication.domain - let attachmentViewModels = composeViewModel.attachmentViewModels - let mediaIDs = attachmentViewModels.compactMap { viewModel in - viewModel.attachment.value?.id - } - let sensitive: Bool = composeViewModel.isContentWarningComposing - let spoilerText: String? = { - let text = composeViewModel.contentWarningContent - guard !text.isEmpty else { return nil } - return text - }() - let visibility = selectedStatusVisibility.value.visibility - - let updateMediaQuerySubscriptions: [AnyPublisher, Error>] = { - var subscriptions: [AnyPublisher, Error>] = [] - for attachmentViewModel in attachmentViewModels { - guard let attachmentID = attachmentViewModel.attachment.value?.id else { continue } - let description = attachmentViewModel.descriptionContent.trimmingCharacters(in: .whitespacesAndNewlines) - guard !description.isEmpty else { continue } - let query = Mastodon.API.Media.UpdateMediaQuery( - file: nil, - thumbnail: nil, - description: description, - focus: nil - ) - let subscription = api.updateMedia( - domain: domain, - attachmentID: attachmentID, - query: query, - mastodonAuthenticationBox: authenticationBox - ) - subscriptions.append(subscription) - } - return subscriptions - }() - - let status = composeViewModel.statusContent - - return Publishers.MergeMany(updateMediaQuerySubscriptions) - .collect() - .asyncMap { attachments in - let query = Mastodon.API.Statuses.PublishStatusQuery( - status: status, - mediaIDs: mediaIDs.isEmpty ? nil : mediaIDs, - pollOptions: nil, - pollExpiresIn: nil, - inReplyToID: nil, - sensitive: sensitive, - spoilerText: spoilerText, - visibility: visibility - ) - return try await api.publishStatus( - domain: domain, - idempotencyKey: nil, // FIXME: - query: query, - authenticationBox: authenticationBox - ) - } - .eraseToAnyPublisher() - } -}