mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-25 09:41:00 +00:00
View favorites
This commit is contained in:
parent
238d83a9f7
commit
02cc1e3533
12 changed files with 51 additions and 22 deletions
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,8 @@ extension Timeline {
|
||||||
excludeReplies: excludeReplies,
|
excludeReplies: excludeReplies,
|
||||||
onlyMedia: onlyMedia,
|
onlyMedia: onlyMedia,
|
||||||
pinned: false)
|
pinned: false)
|
||||||
|
case .favorites:
|
||||||
|
return .favourites
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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([], +)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue