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

Implement peer discovery #1117

Merged
merged 12 commits into from
Feb 22, 2024
7 changes: 7 additions & 0 deletions xcode/Subconscious/Shared/Components/AppTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ enum AppTab: String, Hashable {
case deck
case notebook
case profile
case discover
}

struct AppTabView: View {
Expand All @@ -33,6 +34,12 @@ struct AppTabView: View {
}
.tag(AppTab.deck)

DiscoverView(app: store)
.tabItem {
Label("Discover", systemImage: "globe")
}
.tag(AppTab.discover)

NotebookView(app: store)
.tabItem {
Label("Notes", systemImage: "folder")
Expand Down
16 changes: 12 additions & 4 deletions xcode/Subconscious/Shared/Components/AppView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ enum AppAction: Hashable {
// Addressbook Management Actions
case followPeer(identity: Did, petname: Petname)
case failFollowPeer(error: String)
case succeedFollowPeer(_ petname: Petname)
case succeedFollowPeer(_ identity: Did, _ petname: Petname)

case renamePeer(from: Petname, to: Petname)
case failRenamePeer(error: String)
Expand Down Expand Up @@ -336,6 +336,7 @@ enum AppAction: Hashable {
case requestNotebookRoot
case requestProfileRoot
case requestDeckRoot
case requestDiscoverRoot

/// Used as a notification that recovery completed
case succeedRecoverOurSphere
Expand Down Expand Up @@ -1141,10 +1142,11 @@ struct AppModel: ModelProtocol {
environment: environment,
error: error
)
case .succeedFollowPeer(let petname):
case let .succeedFollowPeer(did, petname):
return succeedFollowPeer(
state: state,
environment: environment,
identity: did,
petname: petname
)
case .renamePeer(let from, let to):
Expand Down Expand Up @@ -1213,7 +1215,10 @@ struct AppModel: ModelProtocol {
environment: environment,
tab: tab
)
case .requestNotebookRoot, .requestProfileRoot, .requestDeckRoot:
case .requestNotebookRoot,
.requestProfileRoot,
.requestDeckRoot,
.requestDiscoverRoot:
return Update(state: state)
case .checkRecoveryStatus:
return checkRecoveryStatus(
Expand Down Expand Up @@ -2738,7 +2743,7 @@ struct AppModel: ModelProtocol {
preventOverwrite: true
)
.map({ _ in
.succeedFollowPeer(petname)
.succeedFollowPeer(identity, petname)
})
.recover { error in
.failFollowPeer(
Expand Down Expand Up @@ -2766,6 +2771,7 @@ struct AppModel: ModelProtocol {
static func succeedFollowPeer(
state: AppModel,
environment: AppEnvironment,
identity: Did,
petname: Petname
) -> Update<AppModel> {
logger.log(
Expand Down Expand Up @@ -2959,6 +2965,8 @@ struct AppModel: ModelProtocol {
return AppAction.requestDeckRoot
case .notebook:
return AppAction.requestNotebookRoot
case .discover:
return AppAction.requestDiscoverRoot
case .profile:
return AppAction.requestProfileRoot
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import SwiftUI
import ObservableStore
import CodeScanner
import os
import Combine

struct FollowNewUserFormSheetView: View {
var store: ViewStore<FollowNewUserFormSheetModel>
Expand Down Expand Up @@ -64,6 +65,28 @@ struct FollowNewUserFormSheetView: View {
)
}

Section(header: Text("Suggestions")) {
ForEach(store.state.suggestions, id: \.identity) { suggestion in
Button(
action: {
store.send(.requestFollowSuggestion(suggestion))
},
label: {
HStack(spacing: AppTheme.unit2) {
ProfilePic(
pfp: .generated(suggestion.identity),
size: .small
)

Text("\(suggestion.name.description)")
.italic()
.fontWeight(.medium)
}
}
)
}
}

if let did = store.state.did {
Section(header: Text("Your DID")) {
DidView(did: did)
Expand All @@ -73,6 +96,7 @@ struct FollowNewUserFormSheetView: View {
ShareableDidQrCodeView(did: did, color: Color.gray)
}
}

}
.navigationTitle("Follow User")
.toolbar {
Expand Down Expand Up @@ -132,6 +156,12 @@ enum FollowNewUserFormSheetAction: Equatable {

case attemptFollow
case dismissSheet

case requestFollowSuggestion(_ neighbor: NeighborRecord)

case refreshSuggestions
case succeedRefreshSuggestions(_ suggestions: [NeighborRecord])
case failRefreshSuggestions(_ error: String)
}

typealias FollowNewUserFormSheetEnvironment = AppEnvironment
Expand Down Expand Up @@ -195,6 +225,7 @@ struct FollowNewUserFormSheetModel: ModelProtocol {

var did: Did? = nil
var isQrCodeScannerPresented = false
var suggestions: [NeighborRecord] = []

var form: FollowUserFormModel = FollowUserFormModel()

Expand All @@ -216,7 +247,11 @@ struct FollowNewUserFormSheetModel: ModelProtocol {
case .populate(let did):
var model = state
model.did = did
return Update(state: model)
return update(
state: model,
action: .refreshSuggestions,
environment: environment
)
case .presentQRCodeScanner(let isPresented):
var model = state
model.failQRCodeScanErrorMessage = nil
Expand All @@ -227,7 +262,6 @@ struct FollowNewUserFormSheetModel: ModelProtocol {
return update(
state: state,
actions: [
.form(.didField(.reset)),
.form(.didField(.setValue(input: content)))
],
environment: environment
Expand Down Expand Up @@ -273,6 +307,55 @@ struct FollowNewUserFormSheetModel: ModelProtocol {
case .attemptFollow:
// Handled by FollowNewUserFormSheetCursor.tag
return Update(state: state)
case .refreshSuggestions:
return refreshSuggestions(
state: state,
environment: environment
)
case let .succeedRefreshSuggestions(suggestions):
return succeedRefreshSuggestions(
state: state,
environment: environment,
suggestions: suggestions
)
case let .failRefreshSuggestions(error):
logger.warning("Failed to refresh suggestions: \(error)")
return Update(state: state)
case let .requestFollowSuggestion(neighbor):
return update(
state: state,
actions: [
.form(.didField(.setValue(input: neighbor.identity.description))),
.form(.petnameField(.setValue(input: neighbor.name.description)))
],
gordonbrander marked this conversation as resolved.
Show resolved Hide resolved
environment: environment
)
}
}

static func refreshSuggestions(
state: Self,
environment: Environment
) -> Update<Self> {
let fx: Fx<Action> = Future.detached {
let suggestions = try environment.database.listNeighbors()
return .succeedRefreshSuggestions(suggestions)
}
.recover { error in
.failRefreshSuggestions(error.localizedDescription)
}
.eraseToAnyPublisher()

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

static func succeedRefreshSuggestions(
state: Self,
environment: Environment,
suggestions: [NeighborRecord]
) -> Update<Self> {
var model = state
model.suggestions = suggestions
return Update(state: model)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ extension UserProfileDetailAction {
return .succeedMoveEntry(from: from, to: to)
case let .succeedUpdateAudience(receipt):
return .succeedUpdateAudience(receipt)
case let .succeedFollowPeer(petname):
case let .succeedFollowPeer(_, petname):
return .succeedFollow(petname)
case let .succeedUnfollowPeer(identity, petname):
return .succeedUnfollow(identity: identity, petname: petname)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// DiscoverActionButtonStyle.swift
// Subconscious (iOS)
//
// Created by Ben Follington on 20/2/2024.
//

import SwiftUI

/// A rounded rectanglular row button style designed for the header
/// This may outgrow its narrow role and the name should be updated if it it used elsewhere
struct DiscoverActionButtonStyle: ButtonStyle {
private func foregroundColor(_ configuration: Configuration) -> Color {
configuration.role == .destructive ?
Color.red :
Color.accentColor
}

private var defaultBackgroundColor: Color {
Color.primaryButtonBackground.opacity(0.5)
}

private var pressedBackgroundColor: Color {
Color.primaryButtonBackgroundPressed
}

private func backgroundColor(_ configuration: Configuration) -> Color {
configuration.isPressed
? pressedBackgroundColor
: defaultBackgroundColor
}

func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(.vertical, AppTheme.unit)
.padding(.horizontal, AppTheme.unit2)
.bold()
.foregroundColor(foregroundColor(configuration))
.background(backgroundColor(configuration))
.cornerRadius(AppTheme.cornerRadius)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
//
// DiscoverNavigationView.swift
// Subconscious (iOS)
//
// Created by Ben Follington on 19/2/2024.
//

import Foundation
import ObservableStore
import SwiftUI

struct DiscoverNavigationView: View {
@ObservedObject var app: Store<AppModel>
@ObservedObject var store: Store<DiscoverModel>
@Environment(\.colorScheme) var colorScheme

var detailStack: ViewStore<DetailStackModel> {
store.viewStore(
get: DiscoverDetailStackCursor.get,
tag: DiscoverDetailStackCursor.tag
)
}

var body: some View {
DetailStackView(app: app, store: detailStack) {
ScrollView {
VStack {
switch store.state.loadingStatus {
case .loading:
Spacer()
ProgressView()
Spacer()
case .loaded:
VStack(alignment: .leading) {
ForEach(store.state.suggestions) { suggestion in
DiscoverUserView(
suggestion: suggestion,
action: { address in
detailStack.send(
.pushDetail(
.profile(
UserProfileDetailDescription(
address: address
)
)
)
)
},
pendingFollow: store.state.pendingFollows.contains(where: { pending in
pending == suggestion.neighbor
}),
onFollow: { neighbor in
store.send(.requestFollowNeighbor(neighbor))
},
onUnfollow: { neighbor in
store.send(.requestUnfollowNeighbor(neighbor))
}
)
}
}
.padding(AppTheme.padding)
.frame(maxWidth: .infinity, maxHeight: .infinity)
case .notFound:
NotFoundView()
}
}
}
.ignoresSafeArea(.keyboard, edges: .bottom)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.navigationTitle("Discover")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
MainToolbar(
app: app
)
}
}
}
}

Loading
Loading