chore: [WIP] restore the replyTo entry for compose

This commit is contained in:
CMK 2022-10-10 19:14:52 +08:00
parent 56f04db40f
commit 02e3ad9a16
105 changed files with 4011 additions and 4041 deletions

View File

@ -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 = "<group>"; };
2D206B9125F60EA700143C56 /* UIControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIControl.swift; sourceTree = "<group>"; };
2D24E1222626ED9D00A59D4F /* UIView+Gesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Gesture.swift"; sourceTree = "<group>"; };
2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = "<group>"; };
2D35237926256D920031AF25 /* NotificationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSection.swift; sourceTree = "<group>"; };
2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = "<group>"; };
2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = "<group>"; };
@ -588,12 +539,10 @@
2D571B2E26004EC000540450 /* NavigationBarProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBarProgressView.swift; sourceTree = "<group>"; };
2D59819A25E4A581000FB903 /* MastodonConfirmEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewController.swift; sourceTree = "<group>"; };
2D5981A025E4A593000FB903 /* MastodonConfirmEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonConfirmEmailViewModel.swift; sourceTree = "<group>"; };
2D5A3D0225CF8742002347D6 /* ControlContainableScrollViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlContainableScrollViews.swift; sourceTree = "<group>"; };
2D5A3D2725CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
2D5A3D3725CF8D9F002347D6 /* ScrollViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollViewContainer.swift; sourceTree = "<group>"; };
2D5A3D6125CFD9CB002347D6 /* HomeTimelineViewController+DebugAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HomeTimelineViewController+DebugAction.swift"; sourceTree = "<group>"; };
2D607AD726242FC500B70763 /* NotificationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewModel.swift; sourceTree = "<group>"; };
2D650FAA25ECDC9300851B58 /* Mastodon+Entity+Error+Detail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error+Detail.swift"; sourceTree = "<group>"; };
2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = "<group>"; };
2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
2D76319E25C1521200929FB9 /* StatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusSection.swift; sourceTree = "<group>"; };
@ -606,14 +555,10 @@
2D84350425FF858100EECE90 /* UIScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIScrollView.swift; sourceTree = "<group>"; };
2D939AB425EDD8A90076FA61 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonRegisterViewController+Avatar.swift"; sourceTree = "<group>"; };
2DA504682601ADE7008F4E6C /* SawToothView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SawToothView.swift; sourceTree = "<group>"; };
2DA7D04325CA52B200804E11 /* TimelineLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLoaderTableViewCell.swift; sourceTree = "<group>"; };
2DA7D04925CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineBottomLoaderTableViewCell.swift; sourceTree = "<group>"; };
2DA7D05025CA545E00804E11 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = "<group>"; };
2DAC9E37262FC2320062E1A6 /* SuggestionAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewController.swift; sourceTree = "<group>"; };
2DAC9E3D262FC2400062E1A6 /* SuggestionAccountViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountViewModel.swift; sourceTree = "<group>"; };
2DAC9E45262FC9FD0062E1A6 /* SuggestionAccountTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionAccountTableViewCell.swift; sourceTree = "<group>"; };
2DB72C8B262D764300CE6173 /* Mastodon+Entity+Notification+Type.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Notification+Type.swift"; sourceTree = "<group>"; };
2DCB73FC2615C13900EC03D4 /* SearchRecommendCollectionHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendCollectionHeader.swift; sourceTree = "<group>"; };
2DE0FACD2615F7AD00CDF649 /* RecommendAccountSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendAccountSection.swift; sourceTree = "<group>"; };
2DF123A625C3B0210020F248 /* ActiveLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveLabel.swift; sourceTree = "<group>"; };
@ -638,9 +583,6 @@
5D0393952612D266007FE196 /* WebViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewModel.swift; sourceTree = "<group>"; };
5DA732CB2629CEF500A92342 /* UIView+Remove.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Remove.swift"; sourceTree = "<group>"; };
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 = "<group>"; };
5DDDF1922617442700311060 /* Mastodon+Entity+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Account.swift"; sourceTree = "<group>"; };
5DDDF1982617447F00311060 /* Mastodon+Entity+Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Tag.swift"; sourceTree = "<group>"; };
5DDDF1A82617489F00311060 /* Mastodon+Entity+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+History.swift"; sourceTree = "<group>"; };
5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVPlayer.swift; sourceTree = "<group>"; };
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 = "<group>"; };
6213AF5928939C8400BCADB6 /* BookmarkViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkViewModel.swift; sourceTree = "<group>"; };
@ -684,8 +626,6 @@
DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdaptiveUserInterfaceStyleBarButtonItem.swift; sourceTree = "<group>"; };
DB03A792272A7E5700EE37C5 /* SidebarListHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListHeaderView.swift; sourceTree = "<group>"; };
DB03A794272A981400EE37C5 /* ContentSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSplitViewController.swift; sourceTree = "<group>"; };
DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeRepliedToStatusContentTableViewCell.swift; sourceTree = "<group>"; };
DB03F7F42689B782007B274C /* ComposeTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeTableView.swift; sourceTree = "<group>"; };
DB0617EA277EF3820030EE79 /* GradientBorderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientBorderView.swift; sourceTree = "<group>"; };
DB0617EC277F02C50030EE79 /* OnboardingNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingNavigationController.swift; sourceTree = "<group>"; };
DB0617EE277F12720030EE79 /* NavigationActionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationActionView.swift; sourceTree = "<group>"; };
@ -707,8 +647,6 @@
DB0F9D53283EB3C000379AF8 /* ProfileHeaderView+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+ViewModel.swift"; sourceTree = "<group>"; };
DB0F9D55283EB46200379AF8 /* ProfileHeaderView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProfileHeaderView+Configuration.swift"; sourceTree = "<group>"; };
DB0FCB67279507EF006C02E2 /* DataSourceFacade+Meta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Meta.swift"; sourceTree = "<group>"; };
DB0FCB6F27951368006C02E2 /* TimelineMiddleLoaderTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineMiddleLoaderTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DB0FCB7127952986006C02E2 /* NamingState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NamingState.swift; sourceTree = "<group>"; };
DB0FCB7327956939006C02E2 /* DataSourceFacade+Status.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceFacade+Status.swift"; sourceTree = "<group>"; };
DB0FCB75279571C5006C02E2 /* ThreadViewController+DataSourceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewController+DataSourceProvider.swift"; sourceTree = "<group>"; };
DB0FCB7727957678006C02E2 /* DataSourceProvider+UITableViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DataSourceProvider+UITableViewDelegate.swift"; sourceTree = "<group>"; };
@ -748,14 +686,6 @@
DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationViewModel.swift; sourceTree = "<group>"; };
DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollExpiresOptionCollectionViewCell.swift; sourceTree = "<group>"; };
DB336F3E278E668C0031E64B /* StatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
DB336F40278E68480031E64B /* StatusView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusView+Configuration.swift"; sourceTree = "<group>"; };
DB336F42278EB1680031E64B /* MediaView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MediaView+Configuration.swift"; sourceTree = "<group>"; };
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentTableViewCell.swift; sourceTree = "<group>"; };
DB36679E268ABAF20027D07F /* ComposeStatusAttachmentSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentSection.swift; sourceTree = "<group>"; };
DB3667A0268ABB2E0027D07F /* ComposeStatusAttachmentItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusAttachmentItem.swift; sourceTree = "<group>"; };
DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollTableViewCell.swift; sourceTree = "<group>"; };
DB3667A5268AE2620027D07F /* ComposeStatusPollSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollSection.swift; sourceTree = "<group>"; };
DB3667A7268AE2900027D07F /* ComposeStatusPollItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusPollItem.swift; sourceTree = "<group>"; };
DB3D0FED25BAA42200EAA174 /* MastodonSDK */ = {isa = PBXFileReference; lastKnownFileType = folder; path = MastodonSDK; sourceTree = "<group>"; };
DB3E6FDC2806A40F00B035AE /* DiscoveryHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewController.swift; sourceTree = "<group>"; };
DB3E6FDF2806A4ED00B035AE /* DiscoveryHashtagsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiscoveryHashtagsViewModel.swift; sourceTree = "<group>"; };
@ -789,8 +719,6 @@
DB427DF925BAA00100D1B89D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DB443CD32694627B00159B29 /* AppearanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceView.swift; sourceTree = "<group>"; };
DB44767A260B3B8C00B66B82 /* CustomEmojiPickerInputView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerInputView.swift; sourceTree = "<group>"; };
DB447680260B3ED600B66B82 /* CustomEmojiPickerSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerSection.swift; sourceTree = "<group>"; };
DB44768A260B3F2100B66B82 /* CustomEmojiPickerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItem.swift; sourceTree = "<group>"; };
DB447690260B406600B66B82 /* CustomEmojiPickerItemCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerItemCollectionViewCell.swift; sourceTree = "<group>"; };
DB447696260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiPickerHeaderCollectionReusableView.swift; sourceTree = "<group>"; };
DB4481B825EE289600BEFB67 /* UITableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = "<group>"; };
@ -893,8 +821,6 @@
DB64BA472851F29300ADF1B7 /* Account+Fetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Fetch.swift"; sourceTree = "<group>"; };
DB65C63627A2AF6C008BAC2E /* ReportItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportItem.swift; sourceTree = "<group>"; };
DB66728B25F9F8DC00D60309 /* ComposeViewModel+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComposeViewModel+DataSource.swift"; sourceTree = "<group>"; };
DB66729525F9F91600D60309 /* ComposeStatusSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusSection.swift; sourceTree = "<group>"; };
DB66729B25F9F91F00D60309 /* ComposeStatusItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusItem.swift; sourceTree = "<group>"; };
DB6746EA278ED8B0008A6B94 /* PollOptionView+Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PollOptionView+Configuration.swift"; sourceTree = "<group>"; };
DB6746EC278F45F0008A6B94 /* AutoGenerateProtocolRelayDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolRelayDelegate.swift; sourceTree = "<group>"; };
DB6746EF278F463B008A6B94 /* AutoGenerateProtocolDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoGenerateProtocolDelegate.swift; sourceTree = "<group>"; };
@ -922,14 +848,10 @@
DB6B74FD272FF59000C70B6E /* UserItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserItem.swift; sourceTree = "<group>"; };
DB6B74FF272FF73800C70B6E /* UserTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserTableViewCell.swift; sourceTree = "<group>"; };
DB6B750327300B4000C70B6E /* TimelineFooterTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFooterTableViewCell.swift; sourceTree = "<group>"; };
DB6C8C0E25F0A6AE00AAA452 /* Mastodon+Entity+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Error.swift"; sourceTree = "<group>"; };
DB6D1B43263691CF00ACB481 /* Mastodon+API+Subscriptions+Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+API+Subscriptions+Policy.swift"; sourceTree = "<group>"; };
DB6D9F3426351B7A008423CD /* NotificationService+Decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationService+Decrypt.swift"; sourceTree = "<group>"; };
DB6D9F7C26358ED4008423CD /* SettingsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
DB6D9F8326358EEC008423CD /* SettingsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsItem.swift; sourceTree = "<group>"; };
DB6D9F9626367249008423CD /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
DB6F5E34264E78E7009108F4 /* AutoCompleteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewController.swift; sourceTree = "<group>"; };
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTopChevronView.swift; sourceTree = "<group>"; };
DB72601B25E36A2100235243 /* MastodonServerRulesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewController.swift; sourceTree = "<group>"; };
DB72602625E36A6F00235243 /* MastodonServerRulesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonServerRulesViewModel.swift; sourceTree = "<group>"; };
DB7274F3273BB9B200577D95 /* ListBatchFetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListBatchFetchViewModel.swift; sourceTree = "<group>"; };
@ -976,7 +898,6 @@
DB938F0826240F3C00E5B6C1 /* RemoteThreadViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteThreadViewModel.swift; sourceTree = "<group>"; };
DB938F0E2624119800E5B6C1 /* ThreadViewModel+LoadThreadState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+LoadThreadState.swift"; sourceTree = "<group>"; };
DB938F1E2624382F00E5B6C1 /* ThreadViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ThreadViewModel+Diffable.swift"; sourceTree = "<group>"; };
DB938F3226243D6200E5B6C1 /* TimelineTopLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTopLoaderTableViewCell.swift; sourceTree = "<group>"; };
DB98EB4627B0DFAA0082E365 /* ReportStatusViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusViewModel+State.swift"; sourceTree = "<group>"; };
DB98EB4827B0F0CD0082E365 /* ReportStatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportStatusTableViewCell.swift; sourceTree = "<group>"; };
DB98EB4B27B0F2BC0082E365 /* ReportStatusTableViewCell+ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReportStatusTableViewCell+ViewModel.swift"; sourceTree = "<group>"; };
@ -1022,7 +943,6 @@
DBA94433265CBB5300C537E1 /* ProfileFieldSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldSection.swift; sourceTree = "<group>"; };
DBA94435265CBB7400C537E1 /* ProfileFieldItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldItem.swift; sourceTree = "<group>"; };
DBA9443D265CFA6400C537E1 /* ProfileFieldCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldCollectionViewCell.swift; sourceTree = "<group>"; };
DBA9443F265D137600C537E1 /* Mastodon+Entity+Field.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Entity+Field.swift"; sourceTree = "<group>"; };
DBABE3EB25ECAC4B00879EE5 /* WelcomeIllustrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeIllustrationView.swift; sourceTree = "<group>"; };
DBAE3FAE26172FC0004B8251 /* RemoteProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteProfileViewModel.swift; sourceTree = "<group>"; };
DBB3BA2926A81C020004F2D4 /* FLAnimatedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FLAnimatedImageView.swift; sourceTree = "<group>"; };
@ -1042,20 +962,13 @@
DBB8AB4526AECDE200F6D281 /* SendPostIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendPostIntentHandler.swift; sourceTree = "<group>"; };
DBB9759B262462E1004620BD /* ThreadMetaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadMetaView.swift; sourceTree = "<group>"; };
DBBC24A726A52F9000398BB9 /* ComposeToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeToolbarView.swift; sourceTree = "<group>"; };
DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeStatusContentTableViewCell.swift; sourceTree = "<group>"; };
DBBC24D626A54BCB00398BB9 /* MastodonRegex.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MastodonRegex.swift; sourceTree = "<group>"; };
DBBE1B4425F3474B0081417A /* MastodonPickServerAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonPickServerAppearance.swift; sourceTree = "<group>"; };
DBBF1DBE2652401B00E5B703 /* AutoCompleteViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteViewModel.swift; sourceTree = "<group>"; };
DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteTableViewCell.swift; sourceTree = "<group>"; };
DBBF1DC4265251C300E5B703 /* AutoCompleteViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+Diffable.swift"; sourceTree = "<group>"; };
DBBF1DC6265251D400E5B703 /* AutoCompleteViewModel+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AutoCompleteViewModel+State.swift"; sourceTree = "<group>"; };
DBBF1DC82652538500E5B703 /* AutoCompleteSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteSection.swift; sourceTree = "<group>"; };
DBBF1DCA2652539E00E5B703 /* AutoCompleteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCompleteItem.swift; sourceTree = "<group>"; };
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 = "<group>"; };
DBC6461426A170AB00B0E31B /* ComposeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeViewController.swift; sourceTree = "<group>"; };
DBC6461726A170AB00B0E31B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
DBC6461926A170AB00B0E31B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
DBC6462226A1712000B0E31B /* ShareViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShareViewModel.swift; sourceTree = "<group>"; };
DBC6462226A1712000B0E31B /* ComposeViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ComposeViewModel.swift; sourceTree = "<group>"; };
DBC7A671260C897100E57475 /* StatusContentWarningEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusContentWarningEditorView.swift; sourceTree = "<group>"; };
DBC9E3A3282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Intents.strings; sourceTree = "<group>"; };
DBC9E3A4282E13BB0063A4D9 /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -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 = "<group>";
};
2DA504672601ADBA008F4E6C /* Decoration */ = {
isa = PBXGroup;
children = (
2DA504682601ADE7008F4E6C /* SawToothView.swift */,
);
path = Decoration;
sourceTree = "<group>";
};
2DAC9E36262FC20B0062E1A6 /* SuggestionAccount */ = {
isa = PBXGroup;
children = (
@ -1638,17 +1532,6 @@
path = Onboarding;
sourceTree = "<group>";
};
DB03F7F1268990A2007B274C /* TableViewCell */ = {
isa = PBXGroup;
children = (
DB03F7F22689AEA3007B274C /* ComposeRepliedToStatusContentTableViewCell.swift */,
DBBC24AB26A53D9300398BB9 /* ComposeStatusContentTableViewCell.swift */,
DB36679C268AB91B0027D07F /* ComposeStatusAttachmentTableViewCell.swift */,
DB3667A3268AE2370027D07F /* ComposeStatusPollTableViewCell.swift */,
);
path = TableViewCell;
sourceTree = "<group>";
};
DB0617F727855B010030EE79 /* Notification */ = {
isa = PBXGroup;
children = (
@ -1808,13 +1691,6 @@
path = Discovery;
sourceTree = "<group>";
};
DB3E6FEA2806BD2500B035AE /* MastodonUI */ = {
isa = PBXGroup;
children = (
);
path = MastodonUI;
sourceTree = "<group>";
};
DB3E6FED2806D7FC00B035AE /* News */ = {
isa = PBXGroup;
children = (
@ -1934,23 +1810,6 @@
path = SearchResult;
sourceTree = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
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 = "<group>";
};
DBBF1DC02652402000E5B703 /* View */ = {
isa = PBXGroup;
children = (
DB6F5E37264E994A009108F4 /* AutoCompleteTopChevronView.swift */,
);
path = View;
sourceTree = "<group>";
};
DBBF1DC326524D3100E5B703 /* Cell */ = {
isa = PBXGroup;
children = (
DBBF1DC126524D2900E5B703 /* AutoCompleteTableViewCell.swift */,
);
path = Cell;
sourceTree = "<group>";
};
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 = "<group>";
@ -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;
};

View File

@ -112,12 +112,12 @@
<key>NotificationService.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>25</integer>
<integer>18</integer>
</dict>
<key>ShareActionExtension.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>24</integer>
<integer>17</integer>
</dict>
</dict>
<key>SuppressBuildableAutocreation</key>

View File

@ -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<CustomEmojiPickerSection, CustomEmojiPickerItem> {
let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(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
}
}

View File

@ -15,6 +15,7 @@ import MetaTextKit
import MastodonMeta
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
enum NotificationSection: Equatable, Hashable {

View File

@ -14,6 +14,7 @@ import UIKit
import os.log
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
enum ReportSection: Equatable, Hashable {

View File

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

View File

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

View File

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

View File

@ -1,12 +0,0 @@
//
// NamingState.swift
// Mastodon
//
// Created by MainasuK on 2022-1-17.
//
import Foundation
protocol NamingState {
var name: String { get }
}

View File

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

View File

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

View File

@ -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<AutoCompleteSection, AutoCompleteItem>()
snapshot.appendSections([.main])
diffableDataSource?.apply(snapshot)
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -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<CustomEmojiPickerSection, CustomEmojiPickerItem>()
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<CustomEmojiPickerSection, CustomEmojiPickerItem>()
// 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<ComposeStatusPollSection, ComposeStatusPollItem>()
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<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
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<ComposeStatusPollSection, ComposeStatusPollItem>()
// 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<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
// 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)
// }
//}

View File

@ -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<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, 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<Mastodon.Entity.Status> 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<Mastodon.Response.Content<Mastodon.Entity.Attachment>, Error>] = {
// var subscriptions: [AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Attachment>, 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<Mastodon.Entity.Status> 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
// }
// }
//
//}

View File

@ -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, Never>(Void()) // use CurrentValueSubject to make initial event emit
var isViewAppeared = false
// let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(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<ComposeStatusSection, ComposeStatusItem>?
var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
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<PublishState?, Never>(nil)
private(set) var publishDate = Date() // update it when enter Publishing state
// TODO: group post material into Hashable class
var idempotencyKey = CurrentValueSubject<String, Never>(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: "#<hashtag> "
// for mention: "@<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<ComposeStatusSection, ComposeStatusItem>?
// var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
// 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<PublishState?, Never>(nil)
// private(set) var publishDate = Date() // update it when enter Publishing state
//
// // TODO: group post material into Hashable class
// var idempotencyKey = CurrentValueSubject<String, Never>(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: "#<hashtag> "
// // for mention: "@<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
// }
//}

View File

@ -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<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>!
weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate?
var observations = Set<NSKeyValueObservation>()
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<Void, Never>()
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
}
}
}
}

View File

@ -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<AnyCancellable>()
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<String, Never>()
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 ?? "<nil>")")
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)
}
}

View File

@ -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<ComposeStatusPollSection, ComposeStatusPollItem>!
var observations = Set<NSKeyValueObservation>()
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<Void, Never>()
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
}
}

View File

@ -8,6 +8,8 @@
import UIKit
import Combine
import MetaTextKit
import MastodonCore
import MastodonUI
final class CustomEmojiPickerInputViewModel {

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ?? "<nil>")")
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

View File

@ -51,7 +51,6 @@ final class HashtagTimelineViewModel {
stateMachine.enter(State.Initial.self)
return stateMachine
}()
lazy var loadOldestStateMachinePublisher = CurrentValueSubject<State?, Never>(nil)
init(context: AppContext, authContext: AuthContext, hashtag: String) {
self.context = context

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import CoreData
import CoreDataStack
import MastodonUI
extension HomeTimelineViewModel {

View File

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

View File

@ -16,6 +16,7 @@ import GameplayKit
import AlamofireImage
import DateToolsSwift
import MastodonCore
import MastodonUI
final class HomeTimelineViewModel: NSObject {

View File

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

View File

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

View File

@ -9,6 +9,7 @@ import os.log
import UIKit
import Tabman
import MastodonAsset
import MastodonUI
import MastodonLocalization
protocol PickServerServerSectionTableHeaderViewDelegate: AnyObject {

View File

@ -12,6 +12,7 @@ import Combine
import GameplayKit
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
final class BookmarkViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {

View File

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

View File

@ -15,6 +15,7 @@ import Combine
import GameplayKit
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
final class FavoriteViewController: UIViewController, NeedsDependency, MediaPreviewableViewController {

View File

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

View File

@ -10,6 +10,7 @@ import UIKit
import GameplayKit
import Combine
import MastodonCore
import MastodonUI
import MastodonLocalization
final class FollowerListViewController: UIViewController, NeedsDependency {

View File

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

View File

@ -11,6 +11,7 @@ import GameplayKit
import Combine
import MastodonLocalization
import MastodonCore
import MastodonUI
final class FollowingListViewController: UIViewController, NeedsDependency {

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import Combine
import CoreDataStack
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
protocol ReportStatusViewControllerDelegate: AnyObject {

View File

@ -10,6 +10,7 @@ import UIKit
import Combine
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
protocol ReportSupplementaryViewControllerDelegate: AnyObject {

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import OSLog
import UIKit
import MastodonAsset
import MastodonCore
import MastodonUI
import MastodonLocalization
class SuggestionAccountViewController: UIViewController, NeedsDependency {

View File

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

View File

@ -10,6 +10,7 @@ import Combine
import CoreData
import CoreDataStack
import MastodonCore
import MastodonUI
import MastodonSDK
extension ThreadViewModel {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@
//
import Foundation
import MastodonCore
enum ComposeStatusAttachmentItem {
case attachment(attachmentService: MastodonAttachmentService)

View File

@ -8,7 +8,6 @@
import Foundation
import Combine
import CoreData
import MastodonCore
import MastodonMeta
import CoreDataStack

View File

@ -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<MastodonUser>)
case reply(status: ManagedObjectRecord<Status>)
}
}
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<AnyCancellable>
) {
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<AnyCancellable>
// ) {
// 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)
// }
}

View File

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

View File

@ -12,7 +12,6 @@ import MastodonSDK
import CoreData
import CoreDataStack
import CommonOSLog
import MastodonCore
extension APIService {

View File

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

View File

@ -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<AutoCompleteSection, AutoCompleteItem> {
UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, item in
switch item {

View File

@ -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<CustomEmojiPickerSection, CustomEmojiPickerItem> {
// let dataSource = UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>(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
// }
}

View File

@ -88,7 +88,7 @@ extension AutoCompleteViewController {
])
tableView.delegate = self
viewModel.setupDiffableDataSource(for: tableView)
// viewModel.setupDiffableDataSource(tableView: tableView)
// bind to layout chevron
viewModel.symbolBoundingRect

View File

@ -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<AutoCompleteSection, AutoCompleteItem>()
// snapshot.appendSections([.main])
// diffableDataSource?.apply(snapshot)
// }
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<MastodonUser>)
case reply(status: ManagedObjectRecord<Status>)
}
public enum ViewState {
case fold // snap to input
case expand // snap to reply
}
}

View File

@ -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<AnyCancellable>()
let statusView = StatusView()
let framePublisher = PassthroughSubject<CGRect, Never>()
@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

View File

@ -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<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>!
// weak var composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate?
// var observations = Set<NSKeyValueObservation>()
//
// 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<Void, Never>()
//
// 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
// }
// }
// }
//
//}
//

View File

@ -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<AnyCancellable>()
// 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<String, Never>()
//
// 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 ?? "<nil>")")
// 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)
// }
//
//}

View File

@ -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<ComposeStatusPollSection, ComposeStatusPollItem>!
// var observations = Set<NSKeyValueObservation>()
//
// 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<Void, Never>()
//
// 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
// }
//}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<AnyCancellable>()
private var _disposeBag = Set<AnyCancellable>()
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

View File

@ -10,7 +10,7 @@ import Combine
import CoreDataStack
extension TimelineMiddleLoaderTableViewCell {
class ViewModel {
public class ViewModel {
var disposeBag = Set<AnyCancellable>()
@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?
) {

View File

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

Some files were not shown because too many files have changed in this diff Show More