View favorites

This commit is contained in:
Justin Mazzocchi 2020-11-30 19:07:38 -08:00
parent 238d83a9f7
commit 02cc1e3533
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
12 changed files with 51 additions and 22 deletions

View file

@ -44,7 +44,7 @@ extension TimelineRecord {
id = timeline.id id = timeline.id
switch timeline { switch timeline {
case .home, .local, .federated: case .home, .local, .federated, .favorites:
listId = nil listId = nil
listTitle = nil listTitle = nil
tag = nil tag = nil

View file

@ -10,6 +10,7 @@ public enum Timeline: Hashable {
case list(List) case list(List)
case tag(String) case tag(String)
case profile(accountId: Account.Id, profileCollection: ProfileCollection) case profile(accountId: Account.Id, profileCollection: ProfileCollection)
case favorites
} }
public extension Timeline { public extension Timeline {
@ -18,7 +19,7 @@ public extension Timeline {
static let unauthenticatedDefaults: [Timeline] = [.local, .federated] static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated] static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
var filterContext: Filter.Context { var filterContext: Filter.Context? {
switch self { switch self {
case .home, .list: case .home, .list:
return .home return .home
@ -26,6 +27,8 @@ public extension Timeline {
return .public return .public
case .profile: case .profile:
return .account return .account
default:
return nil
} }
} }
} }
@ -45,6 +48,8 @@ extension Timeline: Identifiable {
return "tag-".appending(tag).lowercased() return "tag-".appending(tag).lowercased()
case let .profile(accountId, profileCollection): case let .profile(accountId, profileCollection):
return "profile-\(accountId)-\(profileCollection)" return "profile-\(accountId)-\(profileCollection)"
case .favorites:
return "favorites"
} }
} }
} }

View file

@ -24,6 +24,8 @@ extension Timeline {
self = .tag(tag) self = .tag(tag)
case (_, _, _, _, .some(let accountId), .some(let profileCollection)): case (_, _, _, _, .some(let accountId), .some(let profileCollection)):
self = .profile(accountId: accountId, profileCollection: profileCollection) self = .profile(accountId: accountId, profileCollection: profileCollection)
case (Timeline.favorites.id, _, _, _, _, _):
self = .favorites
default: default:
return nil return nil
} }

View file

@ -25,6 +25,7 @@
"attachment.sensitive-content" = "Sensitive content"; "attachment.sensitive-content" = "Sensitive content";
"attachment.media-hidden" = "Media hidden"; "attachment.media-hidden" = "Media hidden";
"cancel" = "Cancel"; "cancel" = "Cancel";
"favorites" = "Favorites";
"registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue"; "registration.review-terms-of-use-and-privacy-policy-%@" = "Please review %@'s Terms of Use and Privacy Policy to continue";
"registration.username" = "Username"; "registration.username" = "Username";
"registration.email" = "Email"; "registration.email" = "Email";

View file

@ -38,7 +38,9 @@ extension Array where Element == Filter {
// swiftlint:disable line_length // swiftlint:disable line_length
// Adapted from https://github.com/tootsuite/mastodon/blob/bf477cee9f31036ebf3d164ddec1cebef5375513/app/javascript/mastodon/selectors/index.js#L43 // Adapted from https://github.com/tootsuite/mastodon/blob/bf477cee9f31036ebf3d164ddec1cebef5375513/app/javascript/mastodon/selectors/index.js#L43
// swiftlint:enable line_length // swiftlint:enable line_length
public func regularExpression(context: Filter.Context) -> String? { public func regularExpression(context: Filter.Context?) -> String? {
guard let context = context else { return nil }
let inContext = filter { $0.context.contains(context) } let inContext = filter { $0.context.contains(context) }
guard !inContext.isEmpty else { return nil } guard !inContext.isEmpty else { return nil }

View file

@ -10,6 +10,7 @@ public enum StatusesEndpoint {
case timelinesHome case timelinesHome
case timelinesList(id: List.Id) case timelinesList(id: List.Id)
case accountsStatuses(id: Account.Id, excludeReplies: Bool, onlyMedia: Bool, pinned: Bool) case accountsStatuses(id: Account.Id, excludeReplies: Bool, onlyMedia: Bool, pinned: Bool)
case favourites
} }
extension StatusesEndpoint: Endpoint { extension StatusesEndpoint: Endpoint {
@ -21,6 +22,8 @@ extension StatusesEndpoint: Endpoint {
return defaultContext + ["timelines"] return defaultContext + ["timelines"]
case .accountsStatuses: case .accountsStatuses:
return defaultContext + ["accounts"] return defaultContext + ["accounts"]
case .favourites:
return defaultContext
} }
} }
@ -36,6 +39,8 @@ extension StatusesEndpoint: Endpoint {
return ["list", id] return ["list", id]
case let .accountsStatuses(id, _, _, _): case let .accountsStatuses(id, _, _, _):
return [id, "statuses"] return [id, "statuses"]
case .favourites:
return ["favourites"]
} }
} }

View file

@ -39,6 +39,8 @@ extension Timeline {
excludeReplies: excludeReplies, excludeReplies: excludeReplies,
onlyMedia: onlyMedia, onlyMedia: onlyMedia,
pinned: false) pinned: false)
case .favorites:
return .favourites
} }
} }
} }

View file

@ -15,27 +15,15 @@ public struct TimelineService {
private let timeline: Timeline private let timeline: Timeline
private let mastodonAPIClient: MastodonAPIClient private let mastodonAPIClient: MastodonAPIClient
private let contentDatabase: ContentDatabase private let contentDatabase: ContentDatabase
private let nextPageMaxIdSubject: CurrentValueSubject<String, Never> private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) { init(timeline: Timeline, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
self.timeline = timeline self.timeline = timeline
self.mastodonAPIClient = mastodonAPIClient self.mastodonAPIClient = mastodonAPIClient
self.contentDatabase = contentDatabase self.contentDatabase = contentDatabase
let nextPageMaxIdSubject = CurrentValueSubject<String, Never>(String(Int.max))
self.nextPageMaxIdSubject = nextPageMaxIdSubject
sections = contentDatabase.timelinePublisher(timeline) sections = contentDatabase.timelinePublisher(timeline)
.handleEvents(receiveOutput: {
guard case let .status(status, _) = $0.last?.last,
status.id < nextPageMaxIdSubject.value
else { return }
nextPageMaxIdSubject.send(status.id)
})
.eraseToAnyPublisher()
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase) navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
nextPageMaxId = nextPageMaxIdSubject.dropFirst().eraseToAnyPublisher() nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
if case let .tag(tag) = timeline { if case let .tag(tag) = timeline {
title = Just("#".appending(tag)).eraseToAnyPublisher() title = Just("#".appending(tag)).eraseToAnyPublisher()
@ -58,9 +46,9 @@ extension TimelineService: CollectionService {
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> { public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
mastodonAPIClient.pagedRequest(timeline.endpoint, maxId: maxId, minId: minId) mastodonAPIClient.pagedRequest(timeline.endpoint, maxId: maxId, minId: minId)
.handleEvents(receiveOutput: { .handleEvents(receiveOutput: {
guard let maxId = $0.info.maxId, maxId < nextPageMaxIdSubject.value else { return } if let maxId = $0.info.maxId {
nextPageMaxIdSubject.send(maxId)
nextPageMaxIdSubject.send(maxId) }
}) })
.flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) } .flatMap { contentDatabase.insert(statuses: $0.result, timeline: timeline) }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View file

@ -98,7 +98,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
.eraseToAnyPublisher() .eraseToAnyPublisher()
self.hasRequestedUsingMarker = true self.hasRequestedUsingMarker = true
} else { } else {
publisher = collectionService.request(maxId: maxId, minId: minId) publisher = collectionService.request(maxId: realMaxId(maxId: maxId), minId: minId)
} }
publisher publisher
@ -294,6 +294,17 @@ private extension CollectionItemsViewModel {
viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) } viewModelCache = viewModelCache.filter { itemsSet.contains($0.key) }
} }
func realMaxId(maxId: String?) -> String? {
guard let maxId = maxId else { return nil }
guard let markerTimeline = collectionService.markerTimeline,
identification.appPreferences.positionBehavior(markerTimeline: markerTimeline) == .rememberPosition,
let lastItemId = items.value.last?.last?.itemId
else { return maxId }
return min(maxId, lastItemId)
}
func idForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItem.Id? { func idForScrollPositionMaintenance(newItems: [[CollectionItem]]) -> CollectionItem.Id? {
let flatItems = items.value.reduce([], +) let flatItems = items.value.reduce([], +)
let flatNewItems = newItems.reduce([], +) let flatNewItems = newItems.reduce([], +)

View file

@ -95,7 +95,7 @@ public extension NavigationViewModel {
switch timeline { switch timeline {
case .home, .list: case .home, .list:
return identification.identity.handle return identification.identity.handle
case .local, .federated, .tag, .profile: case .local, .federated, .tag, .profile, .favorites:
return identification.identity.instance?.uri ?? "" return identification.identity.instance?.uri ?? ""
} }
} }
@ -140,6 +140,12 @@ public extension NavigationViewModel {
case notifications case notifications
case messages case messages
} }
func favoritesViewModel() -> CollectionViewModel {
CollectionItemsViewModel(
collectionService: identification.service.service(timeline: .favorites),
identification: identification)
}
} }
extension NavigationViewModel.Tab: Identifiable { extension NavigationViewModel.Tab: Identifiable {

View file

@ -56,6 +56,10 @@ struct SecondaryNavigationView: View {
NavigationLink(destination: ListsView(viewModel: .init(identification: viewModel.identification))) { NavigationLink(destination: ListsView(viewModel: .init(identification: viewModel.identification))) {
Label("secondary-navigation.lists", systemImage: "scroll") Label("secondary-navigation.lists", systemImage: "scroll")
} }
NavigationLink(destination: TableView(viewModel: viewModel.favoritesViewModel())
.navigationTitle(Text("favorites"))) {
Label("favorites", systemImage: "star.fill")
}
} }
Section { Section {
NavigationLink( NavigationLink(

View file

@ -159,6 +159,8 @@ private extension Timeline {
return "#" + tag return "#" + tag
case .profile: case .profile:
return "" return ""
case .favorites:
return NSLocalizedString("favorites", comment: "")
} }
} }
@ -170,6 +172,7 @@ private extension Timeline {
case .list: return "scroll" case .list: return "scroll"
case .tag: return "number" case .tag: return "number"
case .profile: return "person" case .profile: return "person"
case .favorites: return "star.fill"
} }
} }
} }