feat: use search api to fetch tag info

This commit is contained in:
jk234ert 2021-04-02 10:21:51 +08:00
parent d548840bd9
commit b63a5ebe5f
13 changed files with 275 additions and 10 deletions

View File

@ -240,6 +240,9 @@
"placeholder": "Search hashtags and users",
"cancel": "Cancel"
}
},
"hashtag": {
"prompt": "%s people talking"
}
}
}

View File

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

View File

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

View File

@ -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!";

View File

@ -95,13 +95,21 @@ 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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -107,6 +107,7 @@ extension Mastodon.API {
public enum Search { }
public enum Trends { }
public enum Suggestions { }
public enum Notifications { }
}
extension Mastodon.API {

View File

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