diff --git a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents index c9ebac45..3c73a77b 100644 --- a/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents +++ b/CoreDataStack/CoreData.xcdatamodeld/CoreData.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -35,11 +35,11 @@ - + - + diff --git a/CoreDataStack/Entity/History.swift b/CoreDataStack/Entity/History.swift index 552e2a40..11487929 100644 --- a/CoreDataStack/Entity/History.swift +++ b/CoreDataStack/Entity/History.swift @@ -14,8 +14,8 @@ public final class History: NSManagedObject { @NSManaged public private(set) var createAt: Date @NSManaged public private(set) var day: Date - @NSManaged public private(set) var uses: Int - @NSManaged public private(set) var accounts: Int + @NSManaged public private(set) var uses: String + @NSManaged public private(set) var accounts: String // many-to-one relationship @NSManaged public private(set) var tag: Tag @@ -43,10 +43,10 @@ public extension History { public extension History { struct Property { public let day: Date - public let uses: Int - public let accounts: Int + public let uses: String + public let accounts: String - public init(day: Date, uses: Int, accounts: Int) { + public init(day: Date, uses: String, accounts: String) { self.day = day self.uses = uses self.accounts = accounts diff --git a/Localization/app.json b/Localization/app.json index c307b0ab..e2d64db0 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -234,6 +234,12 @@ "private": "Followers only", "direct": "Only people I mention" } + }, + "search": { + "searchBar": { + "placeholder": "Search hashtags and users", + "cancel": "Cancel" + } } } } diff --git a/Mastodon.xcodeproj/project.pbxproj b/Mastodon.xcodeproj/project.pbxproj index 61204352..5179c387 100644 --- a/Mastodon.xcodeproj/project.pbxproj +++ b/Mastodon.xcodeproj/project.pbxproj @@ -30,6 +30,10 @@ 2D32EAAC25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */; }; 2D32EABA25CB9B0500C9ED86 /* UIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAB925CB9B0500C9ED86 /* UIView.swift */; }; 2D32EADA25CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */; }; + 2D34D9CB261489930081BFC0 /* SearchViewController+recomendView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */; }; + 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */; }; + 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9DA261494120081BFC0 /* APIService+Search.swift */; }; + 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */; }; 2D364F7225E66D7500204FDC /* MastodonResendEmailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */; }; 2D364F7825E66D8300204FDC /* MastodonResendEmailViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */; }; 2D38F1C625CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */; }; @@ -42,7 +46,7 @@ 2D38F1FE25CD481700561493 /* StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F1FD25CD481700561493 /* StatusProvider.swift */; }; 2D38F20825CD491300561493 /* DisposeBagCollectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D38F20725CD491300561493 /* DisposeBagCollectable.swift */; }; 2D3F9E0425DFA133004262D9 /* UITapGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */; }; - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */; }; + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 2D42FF6025C8177C004A627A /* ActiveLabel */; }; 2D42FF6B25C817D2004A627A /* TootContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF6A25C817D2004A627A /* TootContent.swift */; }; 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF7D25C82218004A627A /* ActionToolBarContainer.swift */; }; 2D42FF8525C8224F004A627A /* HitTestExpandedButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D42FF8425C8224F004A627A /* HitTestExpandedButton.swift */; }; @@ -52,18 +56,19 @@ 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 */; }; - 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */; }; + 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */ = {isa = PBXBuildFile; productRef = 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */; }; 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 */; }; 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335725C188A000CAE157 /* APIService+Persist+Status.swift */; }; 2D61335E25C1894B00CAE157 /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D61335D25C1894B00CAE157 /* APIService.swift */; }; - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */; }; + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */; }; 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 */; }; 2D69CFF425CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */; }; 2D69D00A25CAA00300C3A1B2 /* APIService+CoreData+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.swift */; }; + 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */; }; 2D76316525C14BD100929FB9 /* PublicTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */; }; 2D76316B25C14D4C00929FB9 /* PublicTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */; }; 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */; }; @@ -81,7 +86,7 @@ 2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F0D25C7E9C9004F19B8 /* History.swift */; }; 2D927F1425C7EDD9004F19B8 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D927F1325C7EDD9004F19B8 /* Emoji.swift */; }; 2D939AB525EDD8A90076FA61 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AB425EDD8A90076FA61 /* String.swift */; }; - 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */; }; + 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */ = {isa = PBXBuildFile; productRef = 2D939AC725EE14620076FA61 /* CropViewController */; }; 2D939AE825EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D939AE725EE1CF80076FA61 /* MastodonRegisterViewController+Avatar.swift */; }; 2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA504682601ADE7008F4E6C /* SawToothView.swift */; }; 2DA6054725F716A2006356F9 /* PlaybackState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DA6054625F716A2006356F9 /* PlaybackState.swift */; }; @@ -95,7 +100,8 @@ 2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */; }; 2DF75BB925D1474100694EC8 /* ManagedObjectObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */; }; 2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */; }; - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */; }; + 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */; }; + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */ = {isa = PBXBuildFile; productRef = 5D526FE125BE9AC400460CB9 /* MastodonSDK */; }; 5DF1054125F886D400D6C0D4 /* ViedeoPlaybackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054025F886D400D6C0D4 /* ViedeoPlaybackService.swift */; }; 5DF1054725F8870E00D6C0D4 /* VideoPlayerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1054625F8870E00D6C0D4 /* VideoPlayerViewModel.swift */; }; 5DF1056425F887CB00D6C0D4 /* AVPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */; }; @@ -108,7 +114,7 @@ DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A025C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift */; }; DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140A725C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift */; }; DB0140AE25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140AD25C40C7300F9F3CF /* MastodonPinBasedAuthenticationViewModel.swift */; }; - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */; }; + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */ = {isa = PBXBuildFile; productRef = DB0140BC25C40D7500F9F3CF /* CommonOSLog */; }; DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0140CE25C42AEE00F9F3CF /* OSLog.swift */; }; DB084B5725CBC56C00F898ED /* Toot.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB084B5625CBC56C00F898ED /* Toot.swift */; }; DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */; }; @@ -126,7 +132,7 @@ DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2B3AE825E38850007045F9 /* UIViewPreview.swift */; }; DB2F073525E8ECF000957B2D /* AuthenticationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2F073325E8ECF000957B2D /* AuthenticationViewModel.swift */; }; DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB2FF50F260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift */; }; - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */; }; + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */ = {isa = PBXBuildFile; productRef = DB3D0FF225BAA61700EAA174 /* AlamofireImage */; }; DB3D100D25BAA75E00EAA174 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DB3D100F25BAA75E00EAA174 /* Localizable.strings */; }; DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD525BAA00100D1B89D /* AppDelegate.swift */; }; DB427DD825BAA00100D1B89D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB427DD725BAA00100D1B89D /* SceneDelegate.swift */; }; @@ -163,7 +169,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB49A63C25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift */; }; DB5086A525CC0B7000C2C187 /* AvatarBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086A425CC0B7000C2C187 /* AvatarBarButtonItem.swift */; }; DB5086AB25CC0BBB00C2C187 /* AvatarConfigurableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086AA25CC0BBB00C2C187 /* AvatarConfigurableView.swift */; }; - DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */; }; + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DB5086B725CC0D6400C2C187 /* Kingfisher */; }; DB5086BE25CC0D9900C2C187 /* SplashPreference.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB5086BD25CC0D9900C2C187 /* SplashPreference.swift */; }; DB55D33025FB630A0002F825 /* TwitterTextEditor+String.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB55D32F25FB630A0002F825 /* TwitterTextEditor+String.swift */; }; DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB59F0FD25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift */; }; @@ -229,7 +235,7 @@ DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB98339B25C96DE600AD9700 /* APIService+Account.swift */; }; DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A485B2603010E008B817C /* PHPickerResultLoader.swift */; }; DB9A486C26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A486B26032AC1008B817C /* AttachmentContainerView+EmptyStateView.swift */; }; - DB9A487E2603456B008B817C /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* SwiftPackageProductDependency */; }; + DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */ = {isa = PBXBuildFile; productRef = DB9A487D2603456B008B817C /* UITextView+Placeholder */; }; DB9A488426034BD7008B817C /* APIService+Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488326034BD7008B817C /* APIService+Status.swift */; }; DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488926034D40008B817C /* ComposeViewModel+PublishState.swift */; }; DB9A489026035963008B817C /* APIService+Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9A488F26035963008B817C /* APIService+Media.swift */; }; @@ -252,8 +258,8 @@ DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; }; DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; }; DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; }; - DBE64A8B260C49D200E6359A /* BuildFile in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* SwiftPackageProductDependency */; }; - DBE64A8C260C49D200E6359A /* BuildFile in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* SwiftPackageProductDependency */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; + DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; }; + DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */ = {isa = PBXBuildFile; productRef = DBE64A8A260C49D200E6359A /* TwitterTextEditor */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -301,7 +307,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - DBE64A8C260C49D200E6359A /* BuildFile in Embed Frameworks */, + DBE64A8C260C49D200E6359A /* TwitterTextEditor in Embed Frameworks */, DB89BA0425C10FD0008580ED /* CoreDataStack.framework in Embed Frameworks */, ); name = "Embed Frameworks"; @@ -332,6 +338,10 @@ 2D32EAAB25CB96DC00C9ED86 /* TimelineMiddleLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineMiddleLoaderTableViewCell.swift; sourceTree = ""; }; 2D32EAB925CB9B0500C9ED86 /* UIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIView.swift; sourceTree = ""; }; 2D32EAD925CBCC3300C9ED86 /* PublicTimelineViewModel+LoadMiddleState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewModel+LoadMiddleState.swift"; sourceTree = ""; }; + 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SearchViewController+recomendView.swift"; sourceTree = ""; }; + 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Recommend.swift"; sourceTree = ""; }; + 2D34D9DA261494120081BFC0 /* APIService+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Search.swift"; sourceTree = ""; }; + 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchRecommendTagsCollectionViewCell.swift; sourceTree = ""; }; 2D364F7125E66D7500204FDC /* MastodonResendEmailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewController.swift; sourceTree = ""; }; 2D364F7725E66D8300204FDC /* MastodonResendEmailViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonResendEmailViewModel.swift; sourceTree = ""; }; 2D38F1C525CD37F400561493 /* ContentOffsetAdjustableTimelineViewControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentOffsetAdjustableTimelineViewControllerDelegate.swift; sourceTree = ""; }; @@ -363,6 +373,7 @@ 2D694A7325F9EB4E0038ADDC /* ContentWarningOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentWarningOverlayView.swift; sourceTree = ""; }; 2D69CFF325CA9E2200C3A1B2 /* LoadMoreConfigurableTableViewContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadMoreConfigurableTableViewContainer.swift; sourceTree = ""; }; 2D69D00925CAA00300C3A1B2 /* APIService+CoreData+Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "APIService+CoreData+Status.swift"; sourceTree = ""; }; + 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = ""; }; 2D76316425C14BD100929FB9 /* PublicTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewController.swift; sourceTree = ""; }; 2D76316A25C14D4C00929FB9 /* PublicTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineViewModel.swift; sourceTree = ""; }; 2D76317C25C14DF400929FB9 /* PublicTimelineViewController+StatusProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PublicTimelineViewController+StatusProvider.swift"; sourceTree = ""; }; @@ -394,6 +405,7 @@ 2DF75BA625D10E1000694EC8 /* APIService+Favorite.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Favorite.swift"; sourceTree = ""; }; 2DF75BB825D1474100694EC8 /* ManagedObjectObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectObserver.swift; sourceTree = ""; }; 2DF75BC625D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ManagedObjectContextObjectsDidChange.swift; sourceTree = ""; }; + 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+Constraint.swift"; sourceTree = ""; }; 2E1F6A67FDF9771D3E064FDC /* Pods-Mastodon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Mastodon.debug.xcconfig"; path = "Target Support Files/Pods-Mastodon/Pods-Mastodon.debug.xcconfig"; sourceTree = ""; }; 3C030226D3C73DCC23D67452 /* Pods_Mastodon_MastodonUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Mastodon_MastodonUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 452147B2903DF38070FE56A2 /* Pods_MastodonTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MastodonTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -572,17 +584,17 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DB0140BD25C40D7500F9F3CF /* BuildFile in Frameworks */, + DB0140BD25C40D7500F9F3CF /* CommonOSLog in Frameworks */, DB89BA0325C10FD0008580ED /* CoreDataStack.framework in Frameworks */, - 2D42FF6125C8177C004A627A /* BuildFile in Frameworks */, - DB9A487E2603456B008B817C /* BuildFile in Frameworks */, - 2D939AC825EE14620076FA61 /* BuildFile in Frameworks */, - 5D526FE225BE9AC400460CB9 /* BuildFile in Frameworks */, - DB5086B825CC0D6400C2C187 /* BuildFile in Frameworks */, - 2D61336925C18A4F00CAE157 /* BuildFile in Frameworks */, - DB3D0FF325BAA61700EAA174 /* BuildFile in Frameworks */, - DBE64A8B260C49D200E6359A /* BuildFile in Frameworks */, - 2D5981BA25E4D7F8000FB903 /* BuildFile in Frameworks */, + 2D42FF6125C8177C004A627A /* ActiveLabel in Frameworks */, + DB9A487E2603456B008B817C /* UITextView+Placeholder in Frameworks */, + 2D939AC825EE14620076FA61 /* CropViewController in Frameworks */, + 5D526FE225BE9AC400460CB9 /* MastodonSDK in Frameworks */, + DB5086B825CC0D6400C2C187 /* Kingfisher in Frameworks */, + 2D61336925C18A4F00CAE157 /* AlamofireNetworkActivityIndicator in Frameworks */, + DB3D0FF325BAA61700EAA174 /* AlamofireImage in Frameworks */, + DBE64A8B260C49D200E6359A /* TwitterTextEditor in Frameworks */, + 2D5981BA25E4D7F8000FB903 /* ThirdPartyMailer in Frameworks */, 87FFDA5D898A5C42ADCB35E7 /* Pods_Mastodon.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -698,6 +710,14 @@ path = Content; sourceTree = ""; }; + 2D34D9E026149C550081BFC0 /* CollectionViewCell */ = { + isa = PBXGroup; + children = ( + 2D34D9E126149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift */, + ); + path = CollectionViewCell; + sourceTree = ""; + }; 2D364F7025E66D5B00204FDC /* ResendEmail */ = { isa = PBXGroup; children = ( @@ -1097,6 +1117,8 @@ DB49A62A25FF36C700B98345 /* APIService+CustomEmoji.swift */, DB9A488326034BD7008B817C /* APIService+Status.swift */, DB9A488F26035963008B817C /* APIService+Media.swift */, + 2D34D9D026148D9E0081BFC0 /* APIService+Recommend.swift */, + 2D34D9DA261494120081BFC0 /* APIService+Search.swift */, ); path = APIService; sourceTree = ""; @@ -1342,6 +1364,7 @@ 5DF1056325F887CB00D6C0D4 /* AVPlayer.swift */, DB47229625F9EFAD00DA7F53 /* NSManagedObjectContext.swift */, 2D32EAB925CB9B0500C9ED86 /* UIView.swift */, + 2DFF41882614A4DC00F776A4 /* UIView+Constraint.swift */, DB8AF55C25C138B7002E6C99 /* UIViewController.swift */, 2D3F9E0325DFA133004262D9 /* UITapGestureRecognizer.swift */, 2D84350425FF858100EECE90 /* UIScrollView.swift */, @@ -1382,6 +1405,9 @@ isa = PBXGroup; children = ( DB9D6BE825E4F5340051B173 /* SearchViewController.swift */, + 2D34D9CA261489930081BFC0 /* SearchViewController+recomendView.swift */, + 2D6DE3FF26141DF600A63F6A /* SearchViewModel.swift */, + 2D34D9E026149C550081BFC0 /* CollectionViewCell */, ); path = Search; sourceTree = ""; @@ -1492,16 +1518,16 @@ ); name = Mastodon; packageProductDependencies = ( - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */, - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */, - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */, - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */, - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */, - DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */, - 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */, - 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */, - DB9A487D2603456B008B817C /* SwiftPackageProductDependency */, - DBE64A8A260C49D200E6359A /* SwiftPackageProductDependency */, + DB3D0FF225BAA61700EAA174 /* AlamofireImage */, + 5D526FE125BE9AC400460CB9 /* MastodonSDK */, + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */, + 2D42FF6025C8177C004A627A /* ActiveLabel */, + DB0140BC25C40D7500F9F3CF /* CommonOSLog */, + DB5086B725CC0D6400C2C187 /* Kingfisher */, + 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */, + 2D939AC725EE14620076FA61 /* CropViewController */, + DB9A487D2603456B008B817C /* UITextView+Placeholder */, + DBE64A8A260C49D200E6359A /* TwitterTextEditor */, ); productName = Mastodon; productReference = DB427DD225BAA00100D1B89D /* Mastodon.app */; @@ -1624,15 +1650,15 @@ ); mainGroup = DB427DC925BAA00100D1B89D; packageReferences = ( - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */, - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */, - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */, - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */, - DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */, - 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */, - 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */, - DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */, - DBE64A89260C49D200E6359A /* RemoteSwiftPackageReference */, + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */, + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */, + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */, + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */, + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */, + 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */, + 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */, + DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */, + DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */, ); productRefGroup = DB427DD325BAA00100D1B89D /* Products */; projectDirPath = ""; @@ -1863,6 +1889,7 @@ DB59F0FE25EF5D96001F1DAB /* StatusProvider+UITableViewDelegate.swift in Sources */, DB68586425E619B700F0A850 /* NSKeyValueObservation.swift in Sources */, 2D61335825C188A000CAE157 /* APIService+Persist+Status.swift in Sources */, + 2D34D9DB261494120081BFC0 /* APIService+Search.swift in Sources */, DB45FAE325CA7181005A8AC7 /* MastodonUser.swift in Sources */, DB2FF510260B113300ADA9FE /* ComposeStatusPollExpiresOptionCollectionViewCell.swift in Sources */, DB0AC6FC25CD02E600D75117 /* APIService+Instance.swift in Sources */, @@ -1923,6 +1950,7 @@ DBC7A672260C897100E57475 /* StatusContentWarningEditorView.swift in Sources */, DB59F10E25EF724F001F1DAB /* APIService+Poll.swift in Sources */, DB47229725F9EFAD00DA7F53 /* NSManagedObjectContext.swift in Sources */, + 2D34D9D126148D9E0081BFC0 /* APIService+Recommend.swift in Sources */, 2D42FF7E25C82218004A627A /* ActionToolBarContainer.swift in Sources */, DB221B16260C395900AEFE46 /* CustomEmojiPickerInputViewModel.swift in Sources */, DB0140A125C40C0600F9F3CF /* MastodonPinBasedAuthenticationViewController.swift in Sources */, @@ -1953,6 +1981,7 @@ DB49A63D25FF609300B98345 /* PlayerContainerView+MediaTypeIndicotorView.swift in Sources */, DB0140CF25C42AEE00F9F3CF /* OSLog.swift in Sources */, DB44384F25E8C1FA008912A2 /* CALayer.swift in Sources */, + 2D34D9CB261489930081BFC0 /* SearchViewController+recomendView.swift in Sources */, 2D206B8625F5FB0900143C56 /* Double.swift in Sources */, DB9A485C2603010E008B817C /* PHPickerResultLoader.swift in Sources */, 2D76319F25C1521200929FB9 /* StatusSection.swift in Sources */, @@ -1971,6 +2000,7 @@ DB98338725C945ED00AD9700 /* Strings.swift in Sources */, DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */, DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */, + 2DFF41892614A4DC00F776A4 /* UIView+Constraint.swift in Sources */, 2D5A3D0325CF8742002347D6 /* ControlContainableScrollViews.swift in Sources */, DB98336B25C9420100AD9700 /* APIService+App.swift in Sources */, DBA0A11325FB3FC10079C110 /* ComposeToolbarView.swift in Sources */, @@ -1980,6 +2010,7 @@ DB447697260B439000B66B82 /* CustomEmojiPickerHeaderCollectionReusableView.swift in Sources */, DB45FAF925CA80A2005A8AC7 /* APIService+CoreData+MastodonAuthentication.swift in Sources */, 2D04F42525C255B9003F936F /* APIService+PublicTimeline.swift in Sources */, + 2D34D9E226149C920081BFC0 /* SearchRecommendTagsCollectionViewCell.swift in Sources */, 2D76317D25C14DF500929FB9 /* PublicTimelineViewController+StatusProvider.swift in Sources */, 2D206B8025F5F45E00143C56 /* UIImage.swift in Sources */, 2D5A3D2825CF8BC9002347D6 /* HomeTimelineViewModel+Diffable.swift in Sources */, @@ -2011,6 +2042,7 @@ 0FB3D31E25E534C700AAD544 /* PickServerCategoryCollectionViewCell.swift in Sources */, 5DF1057925F88A1D00D6C0D4 /* PlayerContainerView.swift in Sources */, DB45FB0F25CA87D0005A8AC7 /* AuthenticationService.swift in Sources */, + 2D6DE40026141DF600A63F6A /* SearchViewModel.swift in Sources */, DBCCC71E25F73297007E1AB6 /* APIService+Reblog.swift in Sources */, DB789A2B25F9F7AB0071ACA0 /* ComposeRepliedToTootContentCollectionViewCell.swift in Sources */, ); @@ -2543,7 +2575,7 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */ = { + 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TwidereProject/ActiveLabel.swift"; requirement = { @@ -2551,7 +2583,7 @@ minimumVersion = 4.0.0; }; }; - 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */ = { + 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/vtourraine/ThirdPartyMailer.git"; requirement = { @@ -2559,7 +2591,7 @@ minimumVersion = 1.7.1; }; }; - 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */ = { + 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireNetworkActivityIndicator"; requirement = { @@ -2567,7 +2599,7 @@ minimumVersion = 3.1.0; }; }; - 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */ = { + 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/TimOliver/TOCropViewController.git"; requirement = { @@ -2575,7 +2607,7 @@ minimumVersion = 2.6.0; }; }; - DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */ = { + DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/CommonOSLog"; requirement = { @@ -2583,7 +2615,7 @@ minimumVersion = 0.1.1; }; }; - DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */ = { + DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/AlamofireImage.git"; requirement = { @@ -2591,7 +2623,7 @@ minimumVersion = 4.1.0; }; }; - DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */ = { + DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { @@ -2599,7 +2631,7 @@ minimumVersion = 6.1.0; }; }; - DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */ = { + DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/UITextView-Placeholder"; requirement = { @@ -2607,7 +2639,7 @@ minimumVersion = 1.4.1; }; }; - DBE64A89260C49D200E6359A /* RemoteSwiftPackageReference */ = { + DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/MainasuK/TwitterTextEditor"; requirement = { @@ -2618,53 +2650,53 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 2D42FF6025C8177C004A627A /* SwiftPackageProductDependency */ = { + 2D42FF6025C8177C004A627A /* ActiveLabel */ = { isa = XCSwiftPackageProductDependency; - package = 2D42FF5F25C8177C004A627A /* RemoteSwiftPackageReference */; + package = 2D42FF5F25C8177C004A627A /* XCRemoteSwiftPackageReference "ActiveLabel" */; productName = ActiveLabel; }; - 2D5981B925E4D7F8000FB903 /* SwiftPackageProductDependency */ = { + 2D5981B925E4D7F8000FB903 /* ThirdPartyMailer */ = { isa = XCSwiftPackageProductDependency; - package = 2D5981B825E4D7F8000FB903 /* RemoteSwiftPackageReference */; + package = 2D5981B825E4D7F8000FB903 /* XCRemoteSwiftPackageReference "ThirdPartyMailer" */; productName = ThirdPartyMailer; }; - 2D61336825C18A4F00CAE157 /* SwiftPackageProductDependency */ = { + 2D61336825C18A4F00CAE157 /* AlamofireNetworkActivityIndicator */ = { isa = XCSwiftPackageProductDependency; - package = 2D61336725C18A4F00CAE157 /* RemoteSwiftPackageReference */; + package = 2D61336725C18A4F00CAE157 /* XCRemoteSwiftPackageReference "AlamofireNetworkActivityIndicator" */; productName = AlamofireNetworkActivityIndicator; }; - 2D939AC725EE14620076FA61 /* SwiftPackageProductDependency */ = { + 2D939AC725EE14620076FA61 /* CropViewController */ = { isa = XCSwiftPackageProductDependency; - package = 2D939AC625EE14620076FA61 /* RemoteSwiftPackageReference */; + package = 2D939AC625EE14620076FA61 /* XCRemoteSwiftPackageReference "TOCropViewController" */; productName = CropViewController; }; - 5D526FE125BE9AC400460CB9 /* SwiftPackageProductDependency */ = { + 5D526FE125BE9AC400460CB9 /* MastodonSDK */ = { isa = XCSwiftPackageProductDependency; productName = MastodonSDK; }; - DB0140BC25C40D7500F9F3CF /* SwiftPackageProductDependency */ = { + DB0140BC25C40D7500F9F3CF /* CommonOSLog */ = { isa = XCSwiftPackageProductDependency; - package = DB0140BB25C40D7500F9F3CF /* RemoteSwiftPackageReference */; + package = DB0140BB25C40D7500F9F3CF /* XCRemoteSwiftPackageReference "CommonOSLog" */; productName = CommonOSLog; }; - DB3D0FF225BAA61700EAA174 /* SwiftPackageProductDependency */ = { + DB3D0FF225BAA61700EAA174 /* AlamofireImage */ = { isa = XCSwiftPackageProductDependency; - package = DB3D0FF125BAA61700EAA174 /* RemoteSwiftPackageReference */; + package = DB3D0FF125BAA61700EAA174 /* XCRemoteSwiftPackageReference "AlamofireImage" */; productName = AlamofireImage; }; - DB5086B725CC0D6400C2C187 /* SwiftPackageProductDependency */ = { + DB5086B725CC0D6400C2C187 /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; - package = DB5086B625CC0D6400C2C187 /* RemoteSwiftPackageReference */; + package = DB5086B625CC0D6400C2C187 /* XCRemoteSwiftPackageReference "Kingfisher" */; productName = Kingfisher; }; - DB9A487D2603456B008B817C /* SwiftPackageProductDependency */ = { + DB9A487D2603456B008B817C /* UITextView+Placeholder */ = { isa = XCSwiftPackageProductDependency; - package = DB9A487C2603456B008B817C /* RemoteSwiftPackageReference */; + package = DB9A487C2603456B008B817C /* XCRemoteSwiftPackageReference "UITextView-Placeholder" */; productName = "UITextView+Placeholder"; }; - DBE64A8A260C49D200E6359A /* SwiftPackageProductDependency */ = { + DBE64A8A260C49D200E6359A /* TwitterTextEditor */ = { isa = XCSwiftPackageProductDependency; - package = DBE64A89260C49D200E6359A /* RemoteSwiftPackageReference */; + package = DBE64A89260C49D200E6359A /* XCRemoteSwiftPackageReference "TwitterTextEditor" */; productName = TwitterTextEditor; }; /* End XCSwiftPackageProductDependency section */ diff --git a/Mastodon/Extension/UIView+Constraint.swift b/Mastodon/Extension/UIView+Constraint.swift new file mode 100644 index 00000000..42d3bfd9 --- /dev/null +++ b/Mastodon/Extension/UIView+Constraint.swift @@ -0,0 +1,271 @@ +// +// UIView+Constraint.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import UIKit + +enum Dimension { + case width + case height + + var layoutAttribute: NSLayoutConstraint.Attribute { + switch self { + case .width: + return .width + case .height: + return .height + } + } + +} + +extension UIView { + + func constrain(toSuperviewEdges: UIEdgeInsets?) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return} + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + NSLayoutConstraint(item: self, + attribute: .leading, + relatedBy: .equal, + toItem: view, + attribute: .leading, + multiplier: 1.0, + constant: toSuperviewEdges?.left ?? 0.0), + NSLayoutConstraint(item: self, + attribute: .top, + relatedBy: .equal, + toItem: view, + attribute: .top, + multiplier: 1.0, + constant: toSuperviewEdges?.top ?? 0.0), + NSLayoutConstraint(item: self, + attribute: .trailing, + relatedBy: .equal, + toItem: view, + attribute: .trailing, + multiplier: 1.0, + constant: toSuperviewEdges?.right ?? 0.0), + NSLayoutConstraint(item: self, + attribute: .bottom, + relatedBy: .equal, + toItem: view, + attribute: .bottom, + multiplier: 1.0, + constant: toSuperviewEdges?.bottom ?? 0.0) + ]) + } + + func constrain(_ constraints: [NSLayoutConstraint?]) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate(constraints.compactMap { $0 }) + } + + func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView, constant: CGFloat?) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil} + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: constant ?? 0.0) + } + + func constraint(_ attribute: NSLayoutConstraint.Attribute, toView: UIView) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil} + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: attribute, relatedBy: .equal, toItem: toView, attribute: attribute, multiplier: 1.0, constant: 0.0) + } + + func constraint(_ dimension: Dimension, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, + attribute: dimension.layoutAttribute, + relatedBy: .equal, + toItem: nil, + attribute: .notAnAttribute, + multiplier: 1.0, + constant: constant) + } + + func constraint(toBottom: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: toBottom, attribute: .bottom, multiplier: 1.0, constant: constant) + } + + func pinToBottom(to: UIView, height: CGFloat) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.width, toView: to), + constraint(toBottom: to, constant: 0.0), + constraint(.height, constant: height) + ]) + } + + func constraint(toTop: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .bottom, relatedBy: .equal, toItem: toTop, attribute: .top, multiplier: 1.0, constant: constant) + } + + func constraint(toTrailing: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .leading, relatedBy: .equal, toItem: toTrailing, attribute: .trailing, multiplier: 1.0, constant: constant) + } + + func constraint(toLeading: UIView, constant: CGFloat) -> NSLayoutConstraint? { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return nil } + translatesAutoresizingMaskIntoConstraints = false + return NSLayoutConstraint(item: self, attribute: .trailing, relatedBy: .equal, toItem: toLeading, attribute: .leading, multiplier: 1.0, constant: constant) + } + + func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat, topLayoutGuide: UILayoutSupport) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + NSLayoutConstraint(item: self, attribute: .top, relatedBy: .equal, toItem: topLayoutGuide, attribute: .bottom, multiplier: 1.0, constant: topPadding), + constraint(.trailing, toView: view, constant: -sidePadding) + ]) + } + + func constrainTopCorners(sidePadding: CGFloat, topPadding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + constraint(.top, toView: view, constant: topPadding), + constraint(.trailing, toView: view, constant: -sidePadding) + ]) + } + + func constrainTopCorners(height: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.leading, toView: view), + constraint(.top, toView: view), + constraint(.trailing, toView: view), + constraint(.height, constant: height) + ]) + } + + func constrainBottomCorners(sidePadding: CGFloat, bottomPadding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.leading, toView: view, constant: sidePadding), + constraint(.bottom, toView: view, constant: -bottomPadding), + constraint(.trailing, toView: view, constant: -sidePadding) + ]) + } + + func constrainBottomCorners(height: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.leading, toView: view), + constraint(.bottom, toView: view), + constraint(.trailing, toView: view), + constraint(.height, constant: height) + ]) + } + + func constrainLeadingCorners() { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.top, toView: view), + constraint(.leading, toView: view), + constraint(.bottom, toView: view) + ]) + } + + func constrainTrailingCorners() { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.top, toView: view), + constraint(.trailing, toView: view), + constraint(.bottom, toView: view) + ]) + } + + func constrainToCenter() { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.centerX, toView: view), + constraint(.centerY, toView: view) + ]) + } + + func pinTo(viewAbove: UIView, padding: CGFloat = 0.0, height: CGFloat? = nil) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + constraint(.width, toView: viewAbove), + constraint(toBottom: viewAbove, constant: padding), + self.centerXAnchor.constraint(equalTo: viewAbove.centerXAnchor), + height != nil ? constraint(.height, constant: height!) : nil + ]) + } + + func pin(toSize: CGSize) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + widthAnchor.constraint(equalToConstant: toSize.width), + heightAnchor.constraint(equalToConstant: toSize.height)]) + } + + func pinTopLeft(padding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: padding), + topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) + } + + func pinTopRight(padding: CGFloat) { + guard let view = superview else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + view.trailingAnchor.constraint(equalTo: trailingAnchor, constant: padding), + topAnchor.constraint(equalTo: view.topAnchor, constant: padding)]) + } + + func pinTopLeft(toView: UIView, topPadding: CGFloat) { + guard superview != nil else { assert(false, "Superview cannot be nil when adding contraints"); return } + translatesAutoresizingMaskIntoConstraints = false + constrain([ + leadingAnchor.constraint(equalTo: toView.leadingAnchor), + topAnchor.constraint(equalTo: toView.bottomAnchor, constant: topPadding)]) + } + + /// Cross-fades between two views by animating their alpha then setting one or the other hidden. + /// - parameters: + /// - lhs: left view + /// - rhs: right view + /// - toRight: fade to the right view if true, fade to the left view if false + /// - duration: animation duration + /// + static func crossfade(_ lhs: UIView, _ rhs: UIView, toRight: Bool, duration: TimeInterval) { + lhs.alpha = toRight ? 1.0 : 0.0 + rhs.alpha = toRight ? 0.0 : 1.0 + lhs.isHidden = false + rhs.isHidden = false + + UIView.animate(withDuration: duration, animations: { + lhs.alpha = toRight ? 0.0 : 1.0 + rhs.alpha = toRight ? 1.0 : 0.0 + }, completion: { _ in + lhs.isHidden = toRight + rhs.isHidden = !toRight + }) + } +} diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 4ae071e5..1d2bd87a 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -369,6 +369,14 @@ internal enum L10n { } } } + internal enum Search { + internal enum Searchbar { + /// Cancel + internal static let cancel = L10n.tr("Localizable", "Scene.Search.Searchbar.Cancel") + /// Search hashtags and users + internal static let placeholder = L10n.tr("Localizable", "Scene.Search.Searchbar.Placeholder") + } + } internal enum ServerPicker { /// Pick a Server,\nany server. internal static let title = L10n.tr("Localizable", "Scene.ServerPicker.Title") diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index f68b8ecb..aecd9675 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -115,6 +115,8 @@ tap the link to confirm your account."; "Scene.Register.Input.Username.DuplicatePrompt" = "This username is taken."; "Scene.Register.Input.Username.Placeholder" = "username"; "Scene.Register.Title" = "Tell us about you."; +"Scene.Search.Searchbar.Cancel" = "Cancel"; +"Scene.Search.Searchbar.Placeholder" = "Search hashtags and users"; "Scene.ServerPicker.Button.Category.All" = "All"; "Scene.ServerPicker.Button.SeeLess" = "See Less"; "Scene.ServerPicker.Button.SeeMore" = "See More"; diff --git a/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift new file mode 100644 index 00000000..108f2b6a --- /dev/null +++ b/Mastodon/Scene/Search/CollectionViewCell/SearchRecommendTagsCollectionViewCell.swift @@ -0,0 +1,76 @@ +// +// SearchRecommendTagsCollectionViewCell.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +import UIKit + +class SearchRecommendTagsCollectionViewCell: UICollectionViewCell { + let backgroundImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + let hashTagTitleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .preferredFont(forTextStyle: .caption1) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let peopleLabel: UILabel = { + let label = UILabel() + label.textColor = .white + label.font = .preferredFont(forTextStyle: .body) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + let flameIconView: UIImageView = { + let imageView = UIImageView() + let image = UIImage(systemName: "flame.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold))!.withRenderingMode(.alwaysTemplate) + imageView.image = image + imageView.tintColor = .white + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + override func prepareForReuse() { + super.prepareForReuse() + } + + override init(frame: CGRect) { + super.init(frame: .zero) + configure() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + configure() + } +} + +extension SearchRecommendTagsCollectionViewCell { + private func configure() { + contentView.addSubview(backgroundImageView) + backgroundImageView.constrain(toSuperviewEdges: nil) + + contentView.addSubview(hashTagTitleLabel) + hashTagTitleLabel.pinTopLeft(padding: 16) + + contentView.addSubview(peopleLabel) + peopleLabel.constrain([ + peopleLabel.constraint(toTop: contentView, constant: 46), + peopleLabel.constraint(toLeading: contentView, constant: 16) + ]) + + contentView.addSubview(flameIconView) + flameIconView.pinTopRight(padding: 16) + + } +} diff --git a/Mastodon/Scene/Search/SearchViewController+recomendView.swift b/Mastodon/Scene/Search/SearchViewController+recomendView.swift new file mode 100644 index 00000000..b498aa60 --- /dev/null +++ b/Mastodon/Scene/Search/SearchViewController+recomendView.swift @@ -0,0 +1,49 @@ +// +// SearchViewController+recommendView.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +import UIKit + + +extension SearchViewController { + func setuprecommendView() { + recommendView.register(SearchRecommendTagsCollectionViewCell.self, forCellWithReuseIdentifier: String(describing: SearchRecommendTagsCollectionViewCell.self)) + recommendView.dataSource = self + recommendView.delegate = self + } + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + recommendView.collectionViewLayout.invalidateLayout() + } +} + +extension SearchViewController: UICollectionViewDelegate { + +} + +extension SearchViewController: UICollectionViewDataSource { + func numberOfSections(in collectionView: UICollectionView) -> Int { + return (self.viewModel.recommendAccounts.isEmpty ? 0 : 1) + (self.viewModel.recommendHashTags.isEmpty ? 0 : 1) + } + + func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + switch section { + case 0: + return viewModel.recommendHashTags.count + case 1: + return viewModel.recommendAccounts.count + default: + return 0 + } + } + + func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + return UICollectionViewCell() + } + + +} diff --git a/Mastodon/Scene/Search/SearchViewController.swift b/Mastodon/Scene/Search/SearchViewController.swift index 084e7b23..f76f596c 100644 --- a/Mastodon/Scene/Search/SearchViewController.swift +++ b/Mastodon/Scene/Search/SearchViewController.swift @@ -2,23 +2,80 @@ // SearchViewController.swift // Mastodon // -// Created by MainasuK Cirno on 2021-2-23. +// Created by sxiaojian on 2021/3/31. // import UIKit +import Combine final class SearchViewController: UIViewController, NeedsDependency { weak var context: AppContext! { willSet { precondition(!isViewLoaded) } } weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } } + var disposeBag = Set() + private(set) lazy var viewModel = SearchViewModel(context: context) + + let searchBar: UISearchBar = { + let searchBar = UISearchBar() + searchBar.placeholder = L10n.Scene.Search.Searchbar.placeholder + searchBar.tintColor = Asset.Colors.buttonDefault.color + searchBar.translatesAutoresizingMaskIntoConstraints = false + let micImage = UIImage(systemName: "mic.fill") + searchBar.setImage(micImage, for: .bookmark, state: .normal) + searchBar.showsBookmarkButton = true + return searchBar + }() + + let recommendView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + let view = ControlContainableCollectionView(frame: .zero, collectionViewLayout: flowLayout) + view.backgroundColor = .clear + view.showsHorizontalScrollIndicator = false + view.showsVerticalScrollIndicator = false + view.layer.masksToBounds = false + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() } extension SearchViewController { override func viewDidLoad() { super.viewDidLoad() - + searchBar.delegate = self + navigationItem.titleView = searchBar + navigationItem.hidesBackButton = true + viewModel.requestRecommendData() } } + +extension SearchViewController: UISearchBarDelegate { + func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(true, animated: true) + } + + func searchBarTextDidEndEditing(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(false, animated: true) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + searchBar.setShowsCancelButton(false, animated: true) + searchBar.text = "" + searchBar.resignFirstResponder() + } + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + viewModel.searchText.send(searchText) + } + + func searchBarBookmarkButtonClicked(_ searchBar: UISearchBar) { + + } +} + +extension SearchViewController { + +} diff --git a/Mastodon/Scene/Search/SearchViewModel.swift b/Mastodon/Scene/Search/SearchViewModel.swift new file mode 100644 index 00000000..679a2cfa --- /dev/null +++ b/Mastodon/Scene/Search/SearchViewModel.swift @@ -0,0 +1,54 @@ +// +// SearchViewModel.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +import Combine +import MastodonSDK +import UIKit +import OSLog + +final class SearchViewModel { + + var disposeBag = Set() + + // input + let context: AppContext + + // output + let searchText = CurrentValueSubject("") + + var recommendHashTags = [Mastodon.Entity.Tag]() + var recommendAccounts = [Mastodon.Entity.Account]() + + init(context: AppContext) { + self.context = context + } + + func requestRecommendData() { + guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else { + return + } + let trendsAPI = context.apiService.recommendTrends(domain: activeMastodonAuthenticationBox.domain, query: Mastodon.API.Trends.Query(limit: 5)) + + let accountsAPI = context.apiService.recommendAccount(domain: activeMastodonAuthenticationBox.domain, query: nil, mastodonAuthenticationBox: activeMastodonAuthenticationBox) + Publishers.Zip(trendsAPI,accountsAPI) + .sink { completion in + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: zip request fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: zip request success", ((#file as NSString).lastPathComponent), #line, #function) + break + } + } receiveValue: { [weak self] (tags, accounts) in + guard let self = self else { return } + self.recommendAccounts = accounts.value + self.recommendHashTags = tags.value + } + .store(in: &disposeBag) + } +} diff --git a/Mastodon/Service/APIService/APIService+Recommend.swift b/Mastodon/Service/APIService/APIService+Recommend.swift new file mode 100644 index 00000000..bf6db017 --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Recommend.swift @@ -0,0 +1,30 @@ +// +// APIService+Recommend.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +import MastodonSDK +import Combine + +extension APIService { + + func recommendAccount( + domain: String, + query: Mastodon.API.Suggestions.Query?, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + + return Mastodon.API.Suggestions.get(session: session, domain: domain, query: query, authorization: authorization) + } + + func recommendTrends( + domain: String, + query: Mastodon.API.Trends.Query? + ) -> AnyPublisher, Error> { + return Mastodon.API.Trends.get(session: session, domain: domain, query: query) + } +} diff --git a/Mastodon/Service/APIService/APIService+Search.swift b/Mastodon/Service/APIService/APIService+Search.swift new file mode 100644 index 00000000..ba40aa5d --- /dev/null +++ b/Mastodon/Service/APIService/APIService+Search.swift @@ -0,0 +1,23 @@ +// +// APIService+Search.swift +// Mastodon +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +import MastodonSDK +import Combine + +extension APIService { + + func search( + domain: String, + query: Mastodon.API.Search.Query, + mastodonAuthenticationBox: AuthenticationService.MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + + return Mastodon.API.Search.search(session: session, domain: domain, query: query, authorization: authorization) + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift new file mode 100644 index 00000000..8f266437 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Search.swift @@ -0,0 +1,95 @@ +// +// Mastodon+API+Search.swift +// +// +// Created by sxiaojian on 2021/3/31. +// + +import Combine +import Foundation + +extension Mastodon.API.Search { + static func searchURL(domain: String) -> URL { + Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("search") + } + + /// Search results + /// + /// Search for content in accounts, statuses and hashtags. + /// + /// Version history: + /// 2.4.1 - added, limit hardcoded to 5 + /// 2.8.0 - add type, limit, offset, min_id, max_id, account_id + /// 3.0.0 - add exclude_unreviewed param + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/search/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: search query + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Accounts,Hashtags,Status` nested in the response + public static func search( + session: URLSession, + domain: String, + query: Mastodon.API.Search.Query, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: searchURL(domain: domain), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.SearchResult.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } +} + +extension Mastodon.API.Search { + public struct Query: Codable, GetQuery { + public init(accountID: Mastodon.Entity.Account.ID?, maxID: Mastodon.Entity.Status.ID?, minID: Mastodon.Entity.Status.ID?, type: String?, excludeUnreviewed: Bool?, q: String, resolve: Bool?, limit: Int?, offset: Int?, following: Bool?) { + self.accountID = accountID + self.maxID = maxID + self.minID = minID + self.type = type + self.excludeUnreviewed = excludeUnreviewed + self.q = q + self.resolve = resolve + self.limit = limit + self.offset = offset + self.following = following + } + + public let accountID: Mastodon.Entity.Account.ID? + public let maxID: Mastodon.Entity.Status.ID? + public let minID: Mastodon.Entity.Status.ID? + public let type: String? + public let excludeUnreviewed: Bool? // Filter out unreviewed tags? Defaults to false. Use true when trying to find trending tags. + public let q: String + public let resolve: Bool? // Attempt WebFinger lookup. Defaults to false. + public let limit: Int? // Maximum number of results to load, per type. Defaults to 20. Max 40. + public let offset: Int? // Offset in search results. Used for pagination. Defaults to 0. + public let following: Bool? // Only include accounts that the user is following. Defaults to false. + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) } + maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) } + minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) } + type.flatMap { items.append(URLQueryItem(name: "type", value: $0)) } + excludeUnreviewed.flatMap { items.append(URLQueryItem(name: "exclude_unreviewed", value: $0.queryItemValue)) } + items.append(URLQueryItem(name: "q", value: q)) + resolve.flatMap { items.append(URLQueryItem(name: "resolve", value: $0.queryItemValue)) } + + limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + offset.flatMap { items.append(URLQueryItem(name: "offset", value: String($0))) } + following.flatMap { items.append(URLQueryItem(name: "following", value: $0.queryItemValue)) } + guard !items.isEmpty else { return nil } + return items + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift new file mode 100644 index 00000000..55808964 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Suggestions.swift @@ -0,0 +1,65 @@ +// +// Mastodon+API+Suggestions.swift +// +// +// Created by sxiaojian on 2021/3/31. +// + +import Combine +import Foundation + +extension Mastodon.API.Suggestions { + static func suggestionsURL(domain: String) -> URL { + Mastodon.API.endpointURL(domain: domain).appendingPathComponent("suggestions") + } + + /// Follow suggestions + /// + /// Server-generated suggestions on who to follow, based on previous positive interactions. + /// + /// Version history: + /// 2.4.3 - added + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/accounts/suggestions/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: query + /// - authorization: User token. + /// - Returns: `AnyPublisher` contains `Accounts` nested in the response + public static func get( + session: URLSession, + domain: String, + query: Mastodon.API.Suggestions.Query?, + authorization: Mastodon.API.OAuth.Authorization + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: suggestionsURL(domain: domain), + query: query, + authorization: authorization + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [Mastodon.Entity.Account].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } +} + +extension Mastodon.API.Suggestions { + public struct Query: Codable, GetQuery { + public init(limit: Int?) { + self.limit = limit + } + + public let limit: Int? // Maximum number of results to return. Defaults to 40. + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + guard !items.isEmpty else { return nil } + return items + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift new file mode 100644 index 00000000..385e3d75 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Trends.swift @@ -0,0 +1,64 @@ +// +// Mastodon+API+Trends.swift +// +// +// Created by sxiaojian on 2021/3/31. +// + +import Combine +import Foundation + +extension Mastodon.API.Trends { + static func trendsURL(domain: String) -> URL { + Mastodon.API.endpointURL(domain: domain).appendingPathComponent("trends") + } + + /// Trending tags + /// + /// Tags that are being used more frequently within the past week. + /// + /// Version history: + /// 3.0.0 - added + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/instance/trends/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - query: query + /// - Returns: `AnyPublisher` contains `Hashtags` nested in the response + + public static func get( + session: URLSession, + domain: String, + query: Mastodon.API.Trends.Query? + ) -> AnyPublisher, Error> { + let request = Mastodon.API.get( + url: trendsURL(domain: domain), + query: query, + authorization: nil + ) + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: [Mastodon.Entity.Tag].self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } +} + +extension Mastodon.API.Trends { + public struct Query: Codable, GetQuery { + public init(limit: Int?) { + self.limit = limit + } + + public let limit: Int? // Maximum number of results to return. Defaults to 10. + + var queryItems: [URLQueryItem]? { + var items: [URLQueryItem] = [] + limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) } + guard !items.isEmpty else { return nil } + return items + } + } +} diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift index b86cc50e..ac960e71 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API.swift @@ -50,6 +50,9 @@ extension Mastodon.API { if let date = fullDatePreciseISO8601Formatter.date(from: string) { return date } + if let timestamp = TimeInterval(string) { + return Date(timeIntervalSince1970: timestamp) + } } catch { // do nothing } @@ -101,6 +104,9 @@ extension Mastodon.API { public enum Reblog { } public enum Statuses { } public enum Timeline { } + public enum Search { } + public enum Trends { } + public enum Suggestions { } } extension Mastodon.API { diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift index 9e2f9efb..9bf1a3a2 100644 --- a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+History.swift @@ -19,7 +19,7 @@ extension Mastodon.Entity { public struct History: Codable { /// UNIX timestamp on midnight of the given day public let day: Date - public let uses: Int - public let accounts: Int + public let uses: String + public let accounts: String } } diff --git a/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift new file mode 100644 index 00000000..f06f1a54 --- /dev/null +++ b/MastodonSDK/Sources/MastodonSDK/Entity/Mastodon+Entity+SearchResult.swift @@ -0,0 +1,16 @@ +// +// File.swift +// +// +// Created by sxiaojian on 2021/3/31. +// + +import Foundation +extension Mastodon.Entity { + public struct SearchResult: Codable { + let accounts: [Mastodon.Entity.Account] + let statuses: [Mastodon.Entity.Status] + let hashtags: [Mastodon.Entity.Tag] + } + +}