feat: use search api to fetch tag info
This commit is contained in:
parent
d548840bd9
commit
b63a5ebe5f
|
@ -240,6 +240,9 @@
|
|||
"placeholder": "Search hashtags and users",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"hashtag": {
|
||||
"prompt": "%s people talking"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */; };
|
||||
0F2021FB2613262F000C64BF /* HashtagTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */; };
|
||||
0F202201261326E6000C64BF /* HashtagTimelineViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */; };
|
||||
0F20220726134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */; };
|
||||
|
@ -325,6 +326,7 @@
|
|||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineTitleView.swift; sourceTree = "<group>"; };
|
||||
0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewController.swift; sourceTree = "<group>"; };
|
||||
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HashtagTimelineViewModel.swift; sourceTree = "<group>"; };
|
||||
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HashtagTimelineViewModel+Diffable.swift"; sourceTree = "<group>"; };
|
||||
|
@ -652,9 +654,18 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0F1E2D102615C39800C38565 /* View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0F1E2D0A2615C39400C38565 /* HashtagTimelineTitleView.swift */,
|
||||
);
|
||||
path = View;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0F2021F5261325ED000C64BF /* HashtagTimeline */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0F1E2D102615C39800C38565 /* View */,
|
||||
0F2021FA2613262F000C64BF /* HashtagTimelineViewController.swift */,
|
||||
0F202200261326E6000C64BF /* HashtagTimelineViewModel.swift */,
|
||||
0F20220626134DA4000C64BF /* HashtagTimelineViewModel+Diffable.swift */,
|
||||
|
@ -1907,6 +1918,7 @@
|
|||
2D38F1F125CD477D00561493 /* HomeTimelineViewModel+LoadMiddleState.swift in Sources */,
|
||||
DB68A06325E905E000CFDF14 /* UIApplication.swift in Sources */,
|
||||
2D8434FB25FF46B300EECE90 /* HomeTimelineNavigationBarTitleView.swift in Sources */,
|
||||
0F1E2D0B2615C39400C38565 /* HashtagTimelineTitleView.swift in Sources */,
|
||||
DB9A488A26034D40008B817C /* ComposeViewModel+PublishState.swift in Sources */,
|
||||
DB45FAD725CA6C76005A8AC7 /* UIBarButtonItem.swift in Sources */,
|
||||
2DA504692601ADE7008F4E6C /* SawToothView.swift in Sources */,
|
||||
|
|
|
@ -249,6 +249,12 @@ internal enum L10n {
|
|||
internal static let title = L10n.tr("Localizable", "Scene.ConfirmEmail.OpenEmailApp.Title")
|
||||
}
|
||||
}
|
||||
internal enum Hashtag {
|
||||
/// %@ people talking
|
||||
internal static func prompt(_ p1: Any) -> String {
|
||||
return L10n.tr("Localizable", "Scene.Hashtag.Prompt", String(describing: p1))
|
||||
}
|
||||
}
|
||||
internal enum HomeTimeline {
|
||||
/// Home
|
||||
internal static let title = L10n.tr("Localizable", "Scene.HomeTimeline.Title")
|
||||
|
|
|
@ -80,6 +80,7 @@ uploaded to Mastodon.";
|
|||
"Scene.ConfirmEmail.Subtitle" = "We just sent an email to %@,
|
||||
tap the link to confirm your account.";
|
||||
"Scene.ConfirmEmail.Title" = "One last thing.";
|
||||
"Scene.Hashtag.Prompt" = "%@ people talking";
|
||||
"Scene.HomeTimeline.NavigationBarState.NewPosts" = "See new posts";
|
||||
"Scene.HomeTimeline.NavigationBarState.Offline" = "Offline";
|
||||
"Scene.HomeTimeline.NavigationBarState.Published" = "Published!";
|
||||
|
|
|
@ -96,12 +96,20 @@ extension HashtagTimelineViewController {
|
|||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
viewModel.hashtagEntity
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] tag in
|
||||
self?.updatePromptTitle()
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
viewModel.fetchTag()
|
||||
guard viewModel.loadLatestStateMachine.currentState is HashtagTimelineViewModel.LoadLatestState.Initial else { return }
|
||||
tableView.setContentOffset(CGPoint(x: 0, y: tableView.contentOffset.y - refreshControl.frame.size.height), animated: true)
|
||||
|
||||
refreshControl.beginRefreshing()
|
||||
refreshControl.sendActions(for: .valueChanged)
|
||||
}
|
||||
|
@ -123,6 +131,24 @@ extension HashtagTimelineViewController {
|
|||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
|
||||
private func updatePromptTitle() {
|
||||
guard let histories = viewModel.hashtagEntity.value?.history else {
|
||||
navigationItem.prompt = nil
|
||||
return
|
||||
}
|
||||
if histories.isEmpty {
|
||||
// No tag history, remove the prompt title
|
||||
navigationItem.prompt = nil
|
||||
} else {
|
||||
let sortedHistory = histories.sorted { (h1, h2) -> Bool in
|
||||
return h1.day > h2.day
|
||||
}
|
||||
if let accountsNumber = sortedHistory.first?.accounts {
|
||||
navigationItem.prompt = L10n.Scene.Hashtag.prompt(accountsNumber)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension HashtagTimelineViewController {
|
||||
|
|
|
@ -27,6 +27,7 @@ final class HashtagTimelineViewModel: NSObject {
|
|||
let fetchedResultsController: NSFetchedResultsController<Toot>
|
||||
let isFetchingLatestTimeline = CurrentValueSubject<Bool, Never>(false)
|
||||
let timelinePredicate = CurrentValueSubject<NSPredicate?, Never>(nil)
|
||||
let hashtagEntity = CurrentValueSubject<Mastodon.Entity.Tag?, Never>(nil)
|
||||
|
||||
weak var contentOffsetAdjustableTimelineViewControllerDelegate: ContentOffsetAdjustableTimelineViewControllerDelegate?
|
||||
weak var tableView: UITableView?
|
||||
|
@ -105,6 +106,24 @@ final class HashtagTimelineViewModel: NSObject {
|
|||
.store(in: &disposeBag)
|
||||
}
|
||||
|
||||
func fetchTag() {
|
||||
guard let activeMastodonAuthenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value else {
|
||||
return
|
||||
}
|
||||
let query = Mastodon.API.Search.Query(q: hashTag, type: .hashtags)
|
||||
context.apiService.search(domain: activeMastodonAuthenticationBox.domain, query: query, mastodonAuthenticationBox: activeMastodonAuthenticationBox)
|
||||
.sink { _ in
|
||||
|
||||
} receiveValue: { [weak self] response in
|
||||
let matchedTag = response.value.hashtags.first { tag -> Bool in
|
||||
return tag.name == self?.hashTag
|
||||
}
|
||||
self?.hashtagEntity.send(matchedTag)
|
||||
}
|
||||
.store(in: &disposeBag)
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s:", ((#file as NSString).lastPathComponent), #line, #function)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
//
|
||||
// HashtagTimelineTitleView.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by BradGao on 2021/4/1.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
final class HashtagTimelineTitleView: UIView {
|
||||
|
||||
let containerView = UIStackView()
|
||||
|
||||
let imageView = UIImageView()
|
||||
let button = RoundedEdgesButton()
|
||||
let label = UILabel()
|
||||
|
||||
// input
|
||||
private var blockingState: HomeTimelineNavigationBarTitleViewModel.State?
|
||||
weak var delegate: HomeTimelineNavigationBarTitleViewDelegate?
|
||||
|
||||
// output
|
||||
private(set) var state: HomeTimelineNavigationBarTitleViewModel.State = .logoImage
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
_init()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
_init()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension HomeTimelineNavigationBarTitleView {
|
||||
private func _init() {
|
||||
containerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
addSubview(containerView)
|
||||
NSLayoutConstraint.activate([
|
||||
containerView.topAnchor.constraint(equalTo: topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
containerView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
containerView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
])
|
||||
|
||||
containerView.addArrangedSubview(imageView)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addArrangedSubview(button)
|
||||
NSLayoutConstraint.activate([
|
||||
button.heightAnchor.constraint(equalToConstant: 24).priority(.defaultHigh)
|
||||
])
|
||||
containerView.addArrangedSubview(label)
|
||||
|
||||
configure(state: .logoImage)
|
||||
button.addTarget(self, action: #selector(HomeTimelineNavigationBarTitleView.buttonDidPressed(_:)), for: .touchUpInside)
|
||||
}
|
||||
}
|
|
@ -121,7 +121,7 @@ extension Mastodon.API.Favorites {
|
|||
case destroy
|
||||
}
|
||||
|
||||
public struct ListQuery: GetQuery,TimelineQueryType {
|
||||
public struct ListQuery: GetQuery,PagedQueryType {
|
||||
|
||||
public var limit: Int?
|
||||
public var minID: String?
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by BradGao on 2021/4/1.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
extension Mastodon.API.Notifications {
|
||||
static func notificationsEndpointURL(domain: String) -> URL {
|
||||
Mastodon.API.endpointV2URL(domain: domain).appendingPathComponent("notifications")
|
||||
}
|
||||
static func getNotificationEndpointURL(domain: String, notificationID: String) -> URL {
|
||||
notificationsEndpointURL(domain: domain).appendingPathComponent(notificationID)
|
||||
}
|
||||
|
||||
/// Get all notifications
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.1.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/notifications/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - query: `GetAllNotificationsQuery` with query parameters
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||
public static func getAll(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
query: GetAllNotificationsQuery,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<[Mastodon.Entity.Notification]>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: notificationsEndpointURL(domain: domain),
|
||||
query: query,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: [Mastodon.Entity.Notification].self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Get a single notification
|
||||
///
|
||||
/// - Since: 0.0.0
|
||||
/// - Version: 3.1.0
|
||||
/// # Last Update
|
||||
/// 2021/4/1
|
||||
/// # Reference
|
||||
/// [Document](https://docs.joinmastodon.org/methods/notifications/)
|
||||
/// - Parameters:
|
||||
/// - session: `URLSession`
|
||||
/// - domain: Mastodon instance domain. e.g. "example.com"
|
||||
/// - notificationID: ID of the notification.
|
||||
/// - authorization: User token
|
||||
/// - Returns: `AnyPublisher` contains `Token` nested in the response
|
||||
public static func get(
|
||||
session: URLSession,
|
||||
domain: String,
|
||||
notificationID: String,
|
||||
authorization: Mastodon.API.OAuth.Authorization?
|
||||
) -> AnyPublisher<Mastodon.Response.Content<Mastodon.Entity.Notification>, Error> {
|
||||
let request = Mastodon.API.get(
|
||||
url: getNotificationEndpointURL(domain: domain, notificationID: notificationID),
|
||||
query: nil,
|
||||
authorization: authorization
|
||||
)
|
||||
return session.dataTaskPublisher(for: request)
|
||||
.tryMap { data, response in
|
||||
let value = try Mastodon.API.decode(type: Mastodon.Entity.Notification.self, from: data, response: response)
|
||||
return Mastodon.Response.Content(value: value, response: response)
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public struct GetAllNotificationsQuery: Codable, PagedQueryType, GetQuery {
|
||||
public let maxID: Mastodon.Entity.Status.ID?
|
||||
public let sinceID: Mastodon.Entity.Status.ID?
|
||||
public let minID: Mastodon.Entity.Status.ID?
|
||||
public let limit: Int?
|
||||
public let excludeTypes: [String]?
|
||||
public let accountID: String?
|
||||
|
||||
public init(
|
||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||
sinceID: Mastodon.Entity.Status.ID? = nil,
|
||||
minID: Mastodon.Entity.Status.ID? = nil,
|
||||
limit: Int? = nil,
|
||||
excludeTypes: [String]? = nil,
|
||||
accountID: String? = nil
|
||||
) {
|
||||
self.maxID = maxID
|
||||
self.sinceID = sinceID
|
||||
self.minID = minID
|
||||
self.limit = limit
|
||||
self.excludeTypes = excludeTypes
|
||||
self.accountID = accountID
|
||||
}
|
||||
|
||||
var queryItems: [URLQueryItem]? {
|
||||
var items: [URLQueryItem] = []
|
||||
maxID.flatMap { items.append(URLQueryItem(name: "max_id", value: $0)) }
|
||||
sinceID.flatMap { items.append(URLQueryItem(name: "since_id", value: $0)) }
|
||||
minID.flatMap { items.append(URLQueryItem(name: "min_id", value: $0)) }
|
||||
limit.flatMap { items.append(URLQueryItem(name: "limit", value: String($0))) }
|
||||
if let excludeTypes = excludeTypes {
|
||||
excludeTypes.forEach {
|
||||
items.append(URLQueryItem(name: "exclude_types[]", value: $0))
|
||||
}
|
||||
}
|
||||
accountID.flatMap { items.append(URLQueryItem(name: "account_id", value: $0)) }
|
||||
guard !items.isEmpty else { return nil }
|
||||
return items
|
||||
}
|
||||
}
|
||||
}
|
|
@ -50,8 +50,21 @@ extension Mastodon.API.Search {
|
|||
}
|
||||
|
||||
extension Mastodon.API.Search {
|
||||
public enum SearchType: String, Codable {
|
||||
case ccounts, hashtags, statuses
|
||||
}
|
||||
|
||||
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?) {
|
||||
public init(q: String,
|
||||
type: SearchType? = nil,
|
||||
accountID: Mastodon.Entity.Account.ID? = nil,
|
||||
maxID: Mastodon.Entity.Status.ID? = nil,
|
||||
minID: Mastodon.Entity.Status.ID? = nil,
|
||||
excludeUnreviewed: Bool? = nil,
|
||||
resolve: Bool? = nil,
|
||||
limit: Int? = nil,
|
||||
offset: Int? = nil,
|
||||
following: Bool? = nil) {
|
||||
self.accountID = accountID
|
||||
self.maxID = maxID
|
||||
self.minID = minID
|
||||
|
@ -67,7 +80,7 @@ extension Mastodon.API.Search {
|
|||
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 type: SearchType?
|
||||
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.
|
||||
|
@ -80,7 +93,7 @@ extension Mastodon.API.Search {
|
|||
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)) }
|
||||
type.flatMap { items.append(URLQueryItem(name: "type", value: $0.rawValue)) }
|
||||
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)) }
|
||||
|
|
|
@ -121,14 +121,14 @@ extension Mastodon.API.Timeline {
|
|||
}
|
||||
}
|
||||
|
||||
public protocol TimelineQueryType {
|
||||
public protocol PagedQueryType {
|
||||
var maxID: Mastodon.Entity.Status.ID? { get }
|
||||
var sinceID: Mastodon.Entity.Status.ID? { get }
|
||||
}
|
||||
|
||||
extension Mastodon.API.Timeline {
|
||||
|
||||
public typealias TimelineQuery = TimelineQueryType
|
||||
public typealias TimelineQuery = PagedQueryType
|
||||
|
||||
public struct PublicTimelineQuery: Codable, TimelineQuery, GetQuery {
|
||||
|
||||
|
|
|
@ -107,6 +107,7 @@ extension Mastodon.API {
|
|||
public enum Search { }
|
||||
public enum Trends { }
|
||||
public enum Suggestions { }
|
||||
public enum Notifications { }
|
||||
}
|
||||
|
||||
extension Mastodon.API {
|
||||
|
|
|
@ -8,9 +8,9 @@
|
|||
import Foundation
|
||||
extension Mastodon.Entity {
|
||||
public struct SearchResult: Codable {
|
||||
let accounts: [Mastodon.Entity.Account]
|
||||
let statuses: [Mastodon.Entity.Status]
|
||||
let hashtags: [Mastodon.Entity.Tag]
|
||||
public let accounts: [Mastodon.Entity.Account]
|
||||
public let statuses: [Mastodon.Entity.Status]
|
||||
public let hashtags: [Mastodon.Entity.Tag]
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue