Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Made recent searches be stored per-account #684

Merged
merged 7 commits into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions Mlem/Models/Saved Account.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ struct SavedAccount: Identifiable, Codable, Equatable, Hashable {
let storedNickname: String?
let avatarUrl: URL?

var stableIdString: String {
assert(instanceLink.host() != nil, "nil instance link host!")
return "\(username)@\(instanceLink.host() ?? "unknown")"
}

init(
id: Int,
instanceLink: URL,
Expand Down
51 changes: 30 additions & 21 deletions Mlem/Models/Trackers/RecentSearchesTracker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,31 @@ class RecentSearchesTracker: ObservableObject {
var hasLoaded: Bool = false
@Published var recentSearches: [AnyContentModel] = .init()

func loadRecentSearches() async throws {
hasLoaded = true
let identifiers = persistenceRepository.loadRecentSearches()
for id in identifiers {
print(id.contentType, id.contentId)
switch id.contentType {
case .post:
break
case .community:
let community: CommunityModel = try await communityRepository.loadDetails(for: id.contentId)
recentSearches.append(AnyContentModel(community))
case .user:
let user = try await personRepository.loadDetails(for: id.contentId)
recentSearches.append(AnyContentModel(user))
/// clears recentSearches and loads new values based on the current account
func reloadRecentSearches(accountId: String?) async throws {
defer { hasLoaded = true }

recentSearches = .init()
if let accountId {
let identifiers = persistenceRepository.loadRecentSearches(for: accountId)

for id in identifiers {
print(id.contentType, id.contentId)
switch id.contentType {
case .post:
break
case .community:
let community: CommunityModel = try await communityRepository.loadDetails(for: id.contentId)
recentSearches.append(AnyContentModel(community))
case .user:
let user = try await personRepository.loadDetails(for: id.contentId)
recentSearches.append(AnyContentModel(user))
}
}
}
}

func addRecentSearch(_ item: AnyContentModel) {
func addRecentSearch(_ item: AnyContentModel, accountId: String?) {
// if the item is already in the recent list, move it to the top
if let index = recentSearches.firstIndex(of: item) {
recentSearches.remove(at: index)
Expand All @@ -49,17 +55,20 @@ class RecentSearchesTracker: ObservableObject {
recentSearches = recentSearches.dropLast(1)
}
}
saveRecentSearches()
saveRecentSearches(accountId: accountId)
}

func clearRecentSearches() {
func clearRecentSearches(accountId: String?) {
recentSearches.removeAll()
saveRecentSearches()
saveRecentSearches(accountId: accountId)
}

private func saveRecentSearches() {
Task(priority: .background) {
try await persistenceRepository.saveRecentSearches(recentSearches.map { $0.uid })
private func saveRecentSearches(accountId: String?) {
if let accountId {
print("saving searches for \(accountId)")
Task(priority: .background) {
try await persistenceRepository.saveRecentSearches(for: accountId, with: recentSearches.map(\.uid))
}
}
}
}
11 changes: 7 additions & 4 deletions Mlem/Repositories/PersistenceRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,15 @@ class PersistenceRepository {
try await save(value, to: Path.savedAccounts)
}

func loadRecentSearches() -> [ContentModelIdentifier] {
load([ContentModelIdentifier].self, from: Path.recentSearches) ?? []
func loadRecentSearches(for accountId: String) -> [ContentModelIdentifier] {
let searches = load([String: [ContentModelIdentifier]].self, from: Path.recentSearches) ?? [:]
return searches[accountId] ?? []
}

func saveRecentSearches(_ value: [ContentModelIdentifier]) async throws {
try await save(value, to: Path.recentSearches)
func saveRecentSearches(for accountId: String, with searches: [ContentModelIdentifier]) async throws {
var extant = load([String: [ContentModelIdentifier]].self, from: Path.recentSearches) ?? [:]
extant[accountId] = searches
try await save(extant, to: Path.recentSearches)
}

func loadFavoriteCommunities() -> [FavoriteCommunity] {
Expand Down
10 changes: 5 additions & 5 deletions Mlem/Views/Tabs/Search/RecentSearchesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import SwiftUI

struct RecentSearchesView: View {

@EnvironmentObject var appState: AppState
@EnvironmentObject var recentSearchesTracker: RecentSearchesTracker
@StateObject var contentTracker: ContentTracker<AnyContentModel> = .init()

Expand All @@ -17,8 +17,8 @@ struct RecentSearchesView: View {
if !recentSearchesTracker.recentSearches.isEmpty {
VStack(alignment: .leading, spacing: 0) {
headerView
.padding(.top, 15)
.padding(.bottom, 6)
.padding(.top, 15)
.padding(.bottom, 6)
Divider()
itemsView
}
Expand Down Expand Up @@ -47,7 +47,7 @@ struct RecentSearchesView: View {
Spacer()

Button {
recentSearchesTracker.clearRecentSearches()
recentSearchesTracker.clearRecentSearches(accountId: appState.currentActiveAccount?.stableIdString)
} label: {
Text("Clear")
.font(.subheadline)
Expand All @@ -68,7 +68,7 @@ struct RecentSearchesView: View {
}
}
.simultaneousGesture(TapGesture().onEnded {
recentSearchesTracker.addRecentSearch(contentModel)
recentSearchesTracker.addRecentSearch(contentModel, accountId: appState.currentActiveAccount?.stableIdString)
})
Divider()
}
Expand Down
5 changes: 3 additions & 2 deletions Mlem/Views/Tabs/Search/SearchResultListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import SwiftUI

struct SearchResultListView: View {
@EnvironmentObject var appState: AppState
@EnvironmentObject var recentSearchesTracker: RecentSearchesTracker
@EnvironmentObject var contentTracker: ContentTracker<AnyContentModel>

Expand All @@ -26,7 +27,7 @@ struct SearchResultListView: View {
}
}
.simultaneousGesture(TapGesture().onEnded {
recentSearchesTracker.addRecentSearch(contentModel)
recentSearchesTracker.addRecentSearch(contentModel, accountId: appState.currentActiveAccount?.stableIdString)
})
Divider()
.onAppear {
Expand Down Expand Up @@ -54,7 +55,7 @@ struct SearchResultListView: View {
} else if contentTracker.items.isEmpty {
Text("No results found.")
.foregroundStyle(.secondary)
} else if contentTracker.hasReachedEnd && contentTracker.items.count > 10 {
} else if contentTracker.hasReachedEnd, contentTracker.items.count > 10 {
HStack {
Image(systemName: "figure.climbing")
Text("I think I've found the bottom!")
Expand Down
24 changes: 11 additions & 13 deletions Mlem/Views/Tabs/Search/SearchView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
// Created by Jake Shirley on 7/5/23.
//

import Dependencies
import Combine
import Dependencies
import Foundation
import UIKit
import SwiftUI
import UIKit

private struct ViewOffsetKey: PreferenceKey {
typealias Value = CGFloat
Expand All @@ -27,6 +27,7 @@ struct SearchView: View {
}

// environment
@EnvironmentObject var appState: AppState
@EnvironmentObject private var recentSearchesTracker: RecentSearchesTracker
@StateObject var searchModel: SearchModel

Expand Down Expand Up @@ -54,19 +55,16 @@ struct SearchView: View {
page = .home
searchModel.searchText = ""
}

}
}
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.onAppear {
Task(priority: .background) {
if !recentSearchesTracker.hasLoaded {
do {
try await recentSearchesTracker.loadRecentSearches()
} catch {
print("Error while loading recent searches: \(error.localizedDescription)")
errorHandler.handle(error)
}
do {
try await recentSearchesTracker.reloadRecentSearches(accountId: appState.currentActiveAccount?.stableIdString)
} catch {
print("Error while loading recent searches: \(error.localizedDescription)")
errorHandler.handle(error)
}
}
}
Expand All @@ -93,8 +91,8 @@ struct SearchView: View {
.animation(.default, value: page)
}
.onChange(of: isSearching) { newValue in
if newValue && searchModel.searchText.isEmpty {
page = .recents
if newValue, searchModel.searchText.isEmpty {
page = .recents
}
}
.onChange(of: searchModel.searchText) { newValue in
Expand Down
1 change: 1 addition & 0 deletions MlemTests/Model/LemmyURLTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ final class LemmyURLTests: XCTestCase {
XCTAssertEqual(lemmyUrl?.url.absoluteString, validUrl)
}

// NOTE: this test fails on XCode 15+
func testHandlesUnencodedURL() throws {
let unencodedUrl = "https://matrix.to/#/#space:lemmy.world"
let lemmyUrl = LemmyURL(string: unencodedUrl)
Expand Down
22 changes: 14 additions & 8 deletions MlemTests/Persistence/PersistenceRepositoryTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -122,24 +122,30 @@
func testSaveRecentSearches() async throws {
let searches: [ContentModelIdentifier] = [.init(contentType: .user, contentId: 1), .init(contentType: .community, contentId: 2)]

try await repository.saveRecentSearches(searches) // write the examples to disk
let searchesFromDisk = try load([ContentModelIdentifier].self) // load them from the disk _without_ using the repository
try await repository.saveRecentSearches(for: "one@test", with: searches) // write the examples to disk
let searchesFromDisk = try load([Int: [ContentModelIdentifier]].self) // load them from the disk _without_ using the repository
EricBAndrews marked this conversation as resolved.
Show resolved Hide resolved

XCTAssertEqual(searches, searchesFromDisk) // confirm what was written to disk matches what we sent in
let expected: [Int: [ContentModelIdentifier]] = [1: searches]
XCTAssertEqual(expected, searchesFromDisk) // confirm what was written to disk matches what we sent in
}

func testLoadRecentSearchesWithValues() async throws {
let searches: [ContentModelIdentifier] = [.init(contentType: .user, contentId: 1), .init(contentType: .community, contentId: 2)]
let searches1: [ContentModelIdentifier] = [.init(contentType: .user, contentId: 1), .init(contentType: .community, contentId: 2)]
let searches2: [ContentModelIdentifier] = [.init(contentType: .user, contentId: 2), .init(contentType: .community, contentId: 3)]

try await repository.saveRecentSearches(for: "one@test", with: searches1)
try await repository.saveRecentSearches(for: "two@test", with: searches2)

try await repository.saveRecentSearches(searches) // write the example terms to the disk
let loadedSearches = repository.loadRecentSearches() // read them back
let loadedSearches1 = repository.loadRecentSearches(for: "one@test") // read them back
let loadedSearches2 = repository.loadRecentSearches(for: "two@test")

XCTAssertEqual(loadedSearches, searches) // assert we were given the same values back
XCTAssertEqual(loadedSearches1, searches1) // assert we were given the same values back
XCTAssertEqual(loadedSearches2, searches2)
}

func testLoadRecentSearchesWithoutValues() async throws {
XCTAssert(disk.isEmpty) // assert that our mock disk has nothing in it
let loadedSearches = repository.loadRecentSearches() // perform a load knowing the disk is empty
let loadedSearches = repository.loadRecentSearches(for: "one@test") // perform a load knowing the disk is empty
XCTAssert(loadedSearches.isEmpty) // assert we were returned an empty list
}

Expand Down Expand Up @@ -311,7 +317,7 @@
private func load<T: Decodable>(_ model: T.Type) throws -> T {
let key = disk.keys.first
let dataFromDisk = disk[key!]
return try JSONDecoder().decode(T.self, from: dataFromDisk!)

Check failure on line 320 in MlemTests/Persistence/PersistenceRepositoryTests.swift

View workflow job for this annotation

GitHub Actions / Xcode test results

Thrown Error

failed: caught error: "typeMismatch(Swift.Int, Swift.DecodingError.Context(codingPath: [_DictionaryCodingKey(stringValue: "one@test", intValue: nil)], debugDescription: "Expected Int key but found String key instead.", underlyingError: nil))"
}

var bundledMetadata: TimestampedValue<[InstanceMetadata]> {
Expand Down
Loading