Skip to content
This repository has been archived by the owner on Aug 11, 2024. It is now read-only.

Configurable sort order for notebook #1142

Merged
merged 6 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,7 @@ struct EntryListView: View {
// invisible marker to scroll back to
EmptyView().id(Self.resetScrollTargetId)

ForEach(entries.indices, id: \.self) { idx in
let entry = entries[idx]

ForEach(entries, id: \.self) { entry in
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We didn't need the index anymore, apparently

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entries should be Identifiable, so we shouldn't need the second argument at all. That would be preferable, since \.self will consider value equality, and we would prefer to compare ID equality.

Button(
action: {
notify(.requestDetail(entry))
Expand Down Expand Up @@ -134,6 +132,7 @@ struct EntryListView: View {
)
}
}
.id(entry.address)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes the animations

}

FabSpacerView()
Expand Down
80 changes: 77 additions & 3 deletions xcode/Subconscious/Shared/Components/Notebook/Notebook.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,16 @@ enum NotebookAction: Hashable {
/// Fail to get count of existing entries
case failEntryCount(String)

// List entries
// List recent entries
case listRecent
case setRecent([EntryStub])
case listRecentFailure(String)

// List liked entries
case listLiked
case setLiked([EntryStub])
case listLikedFailure(String)

// Delete entries
case confirmDelete(Slashlink?)
case setConfirmDeleteShowing(Bool)
Expand All @@ -155,6 +160,8 @@ enum NotebookAction: Hashable {
case succeedRefreshLikes(_ likes: [Slashlink])
case failRefreshLikes(_ error: String)

case setSortOrder(EntryListSortOrder)

/// Note lifecycle events.
/// `request`s are passed up to the app root
/// `succeed`s are passed down from the app root
Expand Down Expand Up @@ -352,6 +359,11 @@ struct NotebookSearchCursor: CursorProtocol {
}
}

enum EntryListSortOrder: CaseIterable {
case recentlyModified
case likedAndSwiped
}

// MARK: Model
/// Model containing state for the notebook tab.
struct NotebookModel: ModelProtocol {
Expand All @@ -377,8 +389,11 @@ struct NotebookModel: ModelProtocol {

/// Recent entries (nil means "hasn't been loaded from DB")
var recent: [EntryStub]? = nil
var liked: [EntryStub]? = nil
var likes: [Slashlink]? = nil

var sortOrder: EntryListSortOrder = .recentlyModified

var feed: [EntryStub]? = nil

// Note deletion action sheet
Expand Down Expand Up @@ -457,6 +472,12 @@ struct NotebookModel: ModelProtocol {
environment: environment,
count: count
)
case .setSortOrder(let order):
return setSortOrder(
state: state,
environment: environment,
order: order
)
case .failEntryCount(let error):
logger.warning("Failed to count entries: \(error)")
return Update(state: state)
Expand All @@ -475,6 +496,21 @@ struct NotebookModel: ModelProtocol {
"Failed to list recent entries: \(error)"
)
return Update(state: state)
case .listLiked:
return listLiked(
state: state,
environment: environment
)
case let .setLiked(entries):
var model = state
model.liked = entries
model.loadingStatus = .loaded
return Update(state: model).animation(.default)
case let .listLikedFailure(error):
logger.warning(
"Failed to list liked entries: \(error)"
)
return Update(state: state)
case let .confirmDelete(address):
return confirmDelete(
state: state,
Expand Down Expand Up @@ -673,7 +709,7 @@ struct NotebookModel: ModelProtocol {
.search(.refreshSuggestions),
.countEntries,
.refreshLikes,
.listRecent
model.sortOrder == .recentlyModified ? .listRecent : .listLiked
],
environment: environment
).animation(.default)
Expand Down Expand Up @@ -707,11 +743,30 @@ struct NotebookModel: ModelProtocol {
return Update(state: model).animation(.default)
}

/// Set entry count
static func setSortOrder(
state: NotebookModel,
environment: AppEnvironment,
order: EntryListSortOrder
) -> Update<NotebookModel> {
var model = state
model.sortOrder = order
return update(
state: model,
action: .refreshLists,
environment: environment
).animation(
.default
)
}

static func listRecent(
state: NotebookModel,
environment: AppEnvironment
) -> Update<NotebookModel> {
let fx: Fx<NotebookAction> = environment.data.listRecentMemosPublisher()
let fx: Fx<NotebookAction> = Future.detached {
return try await environment.data.listRecentMemos()
}
.map({ entries in
NotebookAction.setRecent(entries)
})
Expand All @@ -726,6 +781,25 @@ struct NotebookModel: ModelProtocol {
return Update(state: state, fx: fx).animation(.default)
}

static func listLiked(
state: NotebookModel,
environment: AppEnvironment
) -> Update<NotebookModel> {
let fx: Fx<NotebookAction> = Future.detached {
let liked = try await environment.data.likedAndSwiped()
return NotebookAction.setLiked(liked)
}
.catch({ error in
Just(
.listLikedFailure(
error.localizedDescription
)
)
})
.eraseToAnyPublisher()
return Update(state: state, fx: fx).animation(.default)
}

static func confirmDelete(
state: NotebookModel,
environment: AppEnvironment,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ struct NotebookNavigationView: View {
ScrollViewReader { proxy in
VStack(spacing: 0) {
EntryListView(
entries: store.state.recent,
entries: store.state.sortOrder == .recentlyModified
? store.state.recent
: store.state.liked,
likes: store.state.likes,
onRefresh: {
app.send(.syncAll)
Expand Down Expand Up @@ -113,14 +115,37 @@ struct NotebookNavigationView: View {
.navigationTitle("Notes")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
MainToolbar(app: app)

ToolbarItemGroup(placement: .principal) {
HStack {
Text("Notes").bold()
CountChip(count: store.state.entryCount)
}
}

ToolbarItem(placement: .primaryAction) {
Menu {
Picker("Sort Order", selection: store.binding(get: \.sortOrder, tag: NotebookAction.setSortOrder)) {
ForEach(EntryListSortOrder.allCases, id: \.self) { option in
switch option {
case .recentlyModified:
Label(
title: { Text(String(localized: "Recently Edited"))} ,
icon: { Image(systemName: "clock") }
)
case .likedAndSwiped:
Label(
title: { Text(String(localized: "Most Liked"))} ,
icon: { Image(systemName: "heart") }
)
}
}
}
} label: {
Image(systemName: "arrow.up.arrow.down")
}
}

MainToolbar(app: app)
}
.onReceive(store.actions) { action in
switch action {
Expand Down
23 changes: 23 additions & 0 deletions xcode/Subconscious/Shared/Services/DataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,29 @@ actor DataService {
return try self.database.listRecentMemos(owner: identity, includeDrafts: true)
}

func likedAndSwiped() async throws -> [EntryStub] {
let identity = try? await noosphere.identity()
let recent = try self.database.listLikedMemos(owner: identity, includeDrafts: true)

var liked: [EntryStub] = []
for memo in recent {
if let isLiked = try? await self.userLikes.isLikedByUs(address: memo.address),
isLiked {
liked.append(memo)
}
}

for memo in recent {
guard let isLiked = try? await self.userLikes.isLikedByUs(address: memo.address),
!isLiked else {
continue
}
liked.append(memo)
}

return liked
}

nonisolated func listRecentMemosPublisher() -> AnyPublisher<[EntryStub], Error> {
Future.detached {
try await self.listRecentMemos()
Expand Down
56 changes: 56 additions & 0 deletions xcode/Subconscious/Shared/Services/DatabaseService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,62 @@ final class DatabaseService {
})
}

/// List liked entries
func listLikedMemos(owner: Did?, includeDrafts: Bool = false) throws -> [EntryStub] {
guard self.state == .ready else {
throw DatabaseServiceError.notReady
}

var dids: [String] = []
if includeDrafts {
dids.append(Did.local.description)
}
if let owner = owner {
dids.append(owner.description)
}

let results = try database.execute(
sql: """
SELECT m.did, m.id, m.excerpt, m.headers, COUNT(a.id) AS choose_card_count
FROM memo m
LEFT JOIN activity a
ON (m.slashlink = a.metadata->>'$.address' OR m.id = a.metadata->>'$.address')
AND a.event = 'choose_card'
WHERE did IN (SELECT value FROM json_each(?))
AND substr(slug, 1, 1) != '_'
GROUP BY m.id
ORDER BY choose_card_count DESC,
modified DESC
LIMIT 1000;
""",
parameters: [
.json(dids, or: "[]")
]
)
return try results.compactMap({ row in
guard
let did = row.col(0)?.toString()?.toDid(),
let address = row.col(1)?
.toString()?
.toLink()?
.toSlashlink()?
.relativizeIfNeeded(did: owner)
else {
return nil
}

let excerpt = Subtext(markup: row.col(2)?.toString() ?? "")
let headers = try parseHeadersJson(json: row.col(3)?.toString() ?? "")

return EntryStub(
did: did,
address: address,
excerpt: excerpt,
headers: headers
)
})
}

/// List file fingerprints for all memos.
func listLocalMemoFingerprints() throws -> [FileFingerprint] {
guard self.state == .ready else {
Expand Down
38 changes: 38 additions & 0 deletions xcode/Subconscious/SubconsciousTests/Tests_DataService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,44 @@ final class Tests_DataService: XCTestCase {
XCTAssertNil(detail)
}

func testListLikedMemos() async throws {
let tmp = try TestUtilities.createTmpDir()
let environment = try await TestUtilities.createDataServiceEnvironment(
tmp: tmp
)

let memoA = Memo(
contentType: ContentType.subtext.rawValue,
created: Date.now,
modified: Date.now,
fileExtension: ContentType.subtext.fileExtension,
additionalHeaders: [],
body: "Test content"
)

let addressA = Slashlink("/a")!
try await environment.data.writeMemo(address: addressA, memo: memoA)

let memoB = Memo(
contentType: ContentType.subtext.rawValue,
created: Date.now,
modified: Date.now,
fileExtension: ContentType.subtext.fileExtension,
additionalHeaders: [],
body: "More content"
)

let addressB = Slashlink("/another/slug")!
try await environment.data.writeMemo(address: addressB, memo: memoB)

try await environment.userLikes.persistLike(for: addressB)

let liked = try await environment.data.likedAndSwiped()

XCTAssert(liked.count == 2)
XCTAssert(liked[0].address == addressB)
}

func testListRecentMemosExcludingHidden() async throws {
let tmp = try TestUtilities.createTmpDir()
let environment = try await TestUtilities.createDataServiceEnvironment(tmp: tmp)
Expand Down
Loading
Loading