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

Commit

Permalink
Refresh backlinks regardless of whether editor content is stale (#859)
Browse files Browse the repository at this point in the history
Fixes #857

# Changes
- Backlinks are now loaded independently of the memo in both the viewer
and editor
- Backlinks are refreshed regardless of whether the editor content is
refresh on `.appear`
- `DataService.readMemoDetail` and `DataService.readEditorMemoDetail`
are both much simpler due to the introduction of
`DataService.readMemoBacklinks`
- Both the viewer an editor will refresh backlinks and transcludes (in
the case of the viewer) if indexing completes in the BG
- To accomplish this I had to pass `app` down to the viewer and editor
so they can replay select actions
- I'd already done this on the profile, so I refactored into a common
`.fromAppAction` pattern
  • Loading branch information
bfollington authored Sep 7, 2023
1 parent 7c83f52 commit fa508bc
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 102 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ struct DetailStackView<Root: View>: View {
switch detail {
case .editor(let description):
MemoEditorDetailView(
app: app,
description: description,
notify: Address.forward(
send: store.send,
Expand All @@ -35,6 +36,7 @@ struct DetailStackView<Root: View>: View {
)
case .viewer(let description):
MemoViewerDetailView(
app: app,
description: description,
notify: Address.forward(
send: store.send,
Expand Down
74 changes: 71 additions & 3 deletions xcode/Subconscious/Shared/Components/Detail/MemoEditorDetail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import Combine

// MARK: View
struct MemoEditorDetailView: View {
@ObservedObject var app: Store<AppModel>

/// Detail keeps a separate internal store for editor state that does not
/// need to be surfaced in higher level views.
///
Expand Down Expand Up @@ -196,6 +198,10 @@ struct MemoEditorDetailView: View {
) { action in
notify(action)
}
.onReceive(
app.actions.compactMap(MemoEditorDetailAction.fromAppAction),
perform: store.send
)
.sheet(
isPresented: Binding(
get: { store.state.isMetaSheetPresented },
Expand Down Expand Up @@ -305,6 +311,11 @@ enum MemoEditorDetailAction: Hashable, CustomLogStringConvertible {
/// Reload detail from source of truth
case refreshDetail
case refreshDetailIfStale

case refreshBacklinks
case succeedRefreshBacklinks(_ backlinks: [EntryStub])
case failRefreshBacklinks(_ error: String)

/// Unable to load detail
case failLoadDetail(String)
/// Set entry detail.
Expand Down Expand Up @@ -477,6 +488,21 @@ extension MemoEditorDetailAction {
}
}

/// React to actions from the root app store
extension MemoEditorDetailAction {
static func fromAppAction(
action: AppAction
) -> MemoEditorDetailAction? {
switch (action) {
case .succeedIndexOurSphere(_),
.succeedIndexPeer(_):
return .refreshBacklinks
case _:
return nil
}
}
}

// MARK: Cursors
/// Editor cursor
struct MemoEditorDetailSubtextTextCursor: CursorProtocol {
Expand Down Expand Up @@ -734,6 +760,20 @@ struct MemoEditorDetailModel: ModelProtocol {
state: state,
environment: environment
)

case .refreshBacklinks:
return refreshBacklinks(
state: state,
environment: environment
)
case .succeedRefreshBacklinks(let backlinks):
var model = state
model.backlinks = backlinks
return Update(state: model)
case .failRefreshBacklinks(let error):
logger.error("Failed to refresh backlinks: \(error)")
return Update(state: state)

case .requestDelete(let address):
return requestDelete(
state: state,
Expand Down Expand Up @@ -1164,8 +1204,14 @@ struct MemoEditorDetailModel: ModelProtocol {
Just(MemoEditorDetailAction.failLoadDetail(error.localizedDescription))
}).eraseToAnyPublisher()

let model = prepareLoadDetail(state)
return Update(state: model, fx: fx)
var model = prepareLoadDetail(state)
model.address = address

return update(
state: model,
action: .refreshBacklinks,
environment: environment
).mergeFx(fx)
}

/// Reload detail
Expand Down Expand Up @@ -1243,7 +1289,6 @@ struct MemoEditorDetailModel: ModelProtocol {
model.defaultAudience = detail.entry.address.toAudience()
model.headers = detail.entry.contents.wellKnownHeaders()
model.additionalHeaders = detail.entry.contents.additionalHeaders
model.backlinks = detail.backlinks
model.saveState = detail.saveState

let subtext = detail.entry.contents.body
Expand Down Expand Up @@ -1406,6 +1451,28 @@ struct MemoEditorDetailModel: ModelProtocol {
return Update(state: model)
}

static func refreshBacklinks(
state: MemoEditorDetailModel,
environment: AppEnvironment
) -> Update<MemoEditorDetailModel> {
guard let address = state.address else {
return Update(state: state)
}

let fx: Fx<MemoEditorDetailAction> = Future.detached {
try await environment.data.readMemoBacklinks(address: address)
}
.map { backlinks in
.succeedRefreshBacklinks(backlinks)
}
.recover { error in
.failRefreshBacklinks(error.localizedDescription)
}
.eraseToAnyPublisher()

return Update(state: state, fx: fx)
}

static func updateAudience(
state: MemoEditorDetailModel,
environment: AppEnvironment,
Expand Down Expand Up @@ -2032,6 +2099,7 @@ extension MemoEntry {
struct Detail_Previews: PreviewProvider {
static var previews: some View {
MemoEditorDetailView(
app: Store(state: AppModel(), environment: AppEnvironment()),
description: MemoEditorDetailDescription(
address: Slashlink("/nothing-is-lost-in-the-universe")!,
fallback: "Nothing is lost in the universe"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import ObservableStore
/// Display a read-only memo detail view.
/// Used for content from other spheres that we don't have write access to.
struct MemoViewerDetailView: View {
@ObservedObject var app: Store<AppModel>

@StateObject private var store = Store(
state: MemoViewerDetailModel(),
environment: AppEnvironment.default
Expand Down Expand Up @@ -70,6 +72,10 @@ struct MemoViewerDetailView: View {
let message = String.loggable(action)
MemoViewerDetailModel.logger.debug("[action] \(message)")
}
.onReceive(
app.actions.compactMap(MemoViewerDetailAction.fromAppAction),
perform: store.send
)
.sheet(
isPresented: Binding(
get: { store.state.isMetaSheetPresented },
Expand Down Expand Up @@ -231,11 +237,15 @@ struct MemoViewerDetailDescription: Hashable {
enum MemoViewerDetailAction: Hashable {
case metaSheet(MemoViewerDetailMetaSheetAction)
case appear(_ description: MemoViewerDetailDescription)
case setDetail(_ detail: MemoDetailResponse?)
case setDetail(_ entry: MemoEntry?)
case setDom(Subtext)
case failLoadDetail(_ message: String)
case presentMetaSheet(_ isPresented: Bool)

case refreshBacklinks
case succeedRefreshBacklinks(_ backlinks: [EntryStub])
case failRefreshBacklinks(_ error: String)

case fetchTranscludePreviews
case succeedFetchTranscludePreviews([Slashlink: EntryStub])
case failFetchTranscludePreviews(_ error: String)
Expand All @@ -244,6 +254,8 @@ enum MemoViewerDetailAction: Hashable {
case succeedFetchOwnerProfile(UserProfile)
case failFetchOwnerProfile(_ error: String)

case succeedIndexBackgroundSphere

/// Synonym for `.metaSheet(.setAddress(_))`
static func setMetaSheetAddress(_ address: Slashlink) -> Self {
.metaSheet(.setAddress(address))
Expand All @@ -261,6 +273,21 @@ extension MemoViewerDetailAction: CustomLogStringConvertible {
}
}

/// React to actions from the root app store
extension MemoViewerDetailAction {
static func fromAppAction(
_ action: AppAction
) -> MemoViewerDetailAction? {
switch (action) {
case .succeedIndexOurSphere(_),
.succeedIndexPeer(_):
return .succeedIndexBackgroundSphere
case _:
return nil
}
}
}

// MARK: Model
struct MemoViewerDetailModel: ModelProtocol {
typealias Action = MemoViewerDetailAction
Expand Down Expand Up @@ -304,11 +331,11 @@ struct MemoViewerDetailModel: ModelProtocol {
environment: environment,
description: description
)
case .setDetail(let response):
case .setDetail(let entry):
return setDetail(
state: state,
environment: environment,
response: response
entry: entry
)
case .setDom(let dom):
return setDom(
Expand All @@ -319,6 +346,20 @@ struct MemoViewerDetailModel: ModelProtocol {
case .failLoadDetail(let message):
logger.log("\(message)")
return Update(state: state)

case .refreshBacklinks:
return refreshBacklinks(
state: state,
environment: environment
)
case .succeedRefreshBacklinks(let backlinks):
var model = state
model.backlinks = backlinks
return Update(state: model)
case .failRefreshBacklinks(let error):
logger.error("Failed to refresh backlinks: \(error)")
return Update(state: state)

case .presentMetaSheet(let isPresented):
return presentMetaSheet(
state: state,
Expand Down Expand Up @@ -355,6 +396,11 @@ struct MemoViewerDetailModel: ModelProtocol {
case .failFetchOwnerProfile(let error):
logger.error("Failed to fetch owner: \(error)")
return Update(state: state)
case .succeedIndexBackgroundSphere:
return succeedIndexBackgroundSphere(
state: state,
environment: environment
)
}
}

Expand All @@ -377,7 +423,8 @@ struct MemoViewerDetailModel: ModelProtocol {
// Set meta sheet address as well
actions: [
.setMetaSheetAddress(description.address),
.fetchOwnerProfile
.fetchOwnerProfile,
.refreshBacklinks
],
environment: environment
).mergeFx(fx)
Expand All @@ -386,21 +433,20 @@ struct MemoViewerDetailModel: ModelProtocol {
static func setDetail(
state: Self,
environment: Environment,
response: MemoDetailResponse?
entry: MemoEntry?
) -> Update<Self> {
var model = state

// If no response, then mark not found
guard let response = response else {
guard let entry = entry else {
model.loadingState = .notFound
return Update(state: model)
}

model.loadingState = .loaded
let memo = response.entry.contents
model.address = response.entry.address
let memo = entry.contents
model.address = entry.address
model.title = memo.title()
model.backlinks = response.backlinks

let dom = memo.dom()

Expand All @@ -411,6 +457,28 @@ struct MemoViewerDetailModel: ModelProtocol {
).animation(.easeOut)
}

static func refreshBacklinks(
state: Self,
environment: Environment
) -> Update<Self> {
guard let address = state.address else {
return Update(state: state)
}

let fx: Fx<MemoViewerDetailAction> = Future.detached {
try await environment.data.readMemoBacklinks(address: address)
}
.map { backlinks in
.succeedRefreshBacklinks(backlinks)
}
.recover { error in
.failRefreshBacklinks(error.localizedDescription)
}
.eraseToAnyPublisher()

return Update(state: state, fx: fx)
}

static func setDom(
state: Self,
environment: Environment,
Expand Down Expand Up @@ -493,6 +561,21 @@ struct MemoViewerDetailModel: ModelProtocol {
model.isMetaSheetPresented = isPresented
return Update(state: model)
}

static func succeedIndexBackgroundSphere(
state: Self,
environment: Environment
) -> Update<Self> {
return update(
state: state,
actions: [
.refreshBacklinks,
.fetchTranscludePreviews
],
environment: environment
)
}

}

/// Meta sheet cursor
Expand Down
Loading

0 comments on commit fa508bc

Please sign in to comment.