Skip to content

Commit

Permalink
Made recent searches be stored per-account (#684)
Browse files Browse the repository at this point in the history
Commit:
b24e9ca [b24e9ca]
  • Loading branch information
boscojwho committed Oct 11, 2023
1 parent 2dcf7be commit f322e89
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 53 deletions.
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 @@ final class PersistenceRepositoryTests: XCTestCase {
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([String: [ContentModelIdentifier]].self) // load them from the disk _without_ using the repository

XCTAssertEqual(searches, searchesFromDisk) // confirm what was written to disk matches what we sent in
let expected: [String: [ContentModelIdentifier]] = ["one@test": 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

0 comments on commit f322e89

Please sign in to comment.