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

MOB-1869 - Added section with suggested profiles #423

Merged
merged 23 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d44b4c5
Refactoring
Oleg-Pecheneg Mar 7, 2024
fc31840
Created scroll content for suggested profiles
Oleg-Pecheneg Mar 7, 2024
2dd6cb6
Implemented UI for suggested profiles
Oleg-Pecheneg Mar 7, 2024
f82f642
Refactoring
Oleg-Pecheneg Mar 7, 2024
b09dcca
Added section header
Oleg-Pecheneg Mar 7, 2024
19c48f4
Added page control
Oleg-Pecheneg Mar 7, 2024
ffab245
Refactoring
Oleg-Pecheneg Mar 7, 2024
aa2a662
Adjusted UI
Oleg-Pecheneg Mar 7, 2024
6d4e4fb
Added following state to suggestion structure
Oleg-Pecheneg Mar 7, 2024
eaddc49
Created suggested prfoiles list view
Oleg-Pecheneg Mar 7, 2024
0e67101
Fixed nav bar and tab bar on explore screen when navigate
Oleg-Pecheneg Mar 7, 2024
04b5d55
Created serialized profile suggestion
Oleg-Pecheneg Mar 7, 2024
be46269
Fixed cover image storing in follower tile view
Oleg-Pecheneg Mar 7, 2024
ed79201
Load more suggestions if available
Oleg-Pecheneg Mar 7, 2024
ea1a232
UX improvements
Oleg-Pecheneg Mar 7, 2024
9137878
Refactoring
Oleg-Pecheneg Mar 7, 2024
e62102a
Added follow actions publisher tp domain profiles service.
Oleg-Pecheneg Mar 7, 2024
ff5eb2a
Fixed tests
Oleg-Pecheneg Mar 7, 2024
a86219d
Refactoring
Oleg-Pecheneg Mar 7, 2024
fb065c4
Updated follow actions publisher
Oleg-Pecheneg Mar 7, 2024
21c6b1d
Update following state in suggested profiles array
Oleg-Pecheneg Mar 7, 2024
d2ba6a4
Test suggested profiles loaded and status updated on action
Oleg-Pecheneg Mar 7, 2024
a639f82
testSuggestedProfileFollowStatusUpdatedFromProfilesService
Oleg-Pecheneg Mar 7, 2024
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 @@ -7,7 +7,7 @@

import Foundation

final class PreviewPublicDomainProfileDisplayInfoStorageService: PublicDomainProfileDisplayInfoStorageServiceProtocol {
final class PreviewPublicDomainProfileDisplayInfoStorageService: DomainProfileDisplayInfoStorageServiceProtocol {
func store(profile: DomainProfileDisplayInfo) {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ extension NetworkService {
}
}

extension NetworkService: PublicDomainProfileNetworkServiceProtocol {
extension NetworkService: DomainProfileNetworkServiceProtocol {
public func searchForDomainsWith(name: String,
shouldBeSetAsRR: Bool) async throws -> [SearchDomainProfile] {
var result = [SearchDomainProfile]()
Expand Down Expand Up @@ -244,6 +244,10 @@ extension NetworkService: PublicDomainProfileNetworkServiceProtocol {
func unfollow(_ domainNameToUnfollow: String, by domain: DomainItem) async throws {

}

func getProfileSuggestions(for domainName: DomainName) async throws -> SerializedDomainProfileSuggestionsResponse {
MockEntitiesFabric.ProfileSuggestions.createSerializedSuggestionsForPreview()
}
}

extension NetworkService {
Expand Down
126 changes: 109 additions & 17 deletions unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ final class GeneralAppContext: AppContextProtocol {
walletConnectServiceV2: walletConnectServiceV2,
walletNFTsService: walletNFTsService)

domainProfilesService = DomainProfilesService(storage: PublicDomainProfileDisplayInfoStorageService(),
domainProfilesService = DomainProfilesService(storage: DomainProfileDisplayInfoCoreDataStorage(),
walletsDataService: walletsDataService)

// WC requests
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ final class MockContext: AppContextProtocol {
private(set) lazy var hotFeatureSuggestionsService: HotFeatureSuggestionsServiceProtocol = HotFeatureSuggestionsService(fetcher: DefaultHotFeaturesSuggestionsFetcher())
private(set) lazy var walletNFTsService: WalletNFTsServiceProtocol = WalletNFTsService()
private(set) lazy var walletsDataService: WalletsDataServiceProtocol = PreviewWalletsDataService()
private(set) lazy var domainProfilesService: DomainProfilesServiceProtocol = DomainProfilesService(storage: PublicDomainProfileDisplayInfoStorageService(),
private(set) lazy var domainProfilesService: DomainProfilesServiceProtocol = DomainProfilesService(storage: DomainProfileDisplayInfoCoreDataStorage(),
walletsDataService: walletsDataService)

var persistedProfileSignaturesStorage: PersistedSignaturesStorageProtocol = MockPersistedSignaturesStorage()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//
// DomainProfileSuggestion.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 07.03.2024.
//

import SwiftUI

struct DomainProfileSuggestion: Hashable, Identifiable {
var id: String { domain }

let address: String
let reasons: [String]
let score: Int
let domain: String
let imageUrl: URL?
let imageType: DomainProfileImageType?
var isFollowing: Bool = false

var classifiedReasons: [Reason] { reasons.compactMap { Reason(rawValue: $0) } }

func getReasonToShow() -> Reason? {
classifiedReasons.first
}

enum Reason: String {
case nftCollection = "Holds the same NFT collection"
case poap = "Holds the same POAP"
case transaction = "Shared a transaction"
case lensFollows = "Lens follows in common"
case farcasterFollows = "Farcaster follows in common"

var title: String {
rawValue
}

var icon: Image {
switch self {
case .nftCollection:
return .cryptoFaceIcon
case .poap:
return .cryptoPOAPIcon
case .transaction:
return .cryptoTransactionIcon
case .lensFollows:
return .lensIcon
case .farcasterFollows:
return .farcasterIcon
}
}
}
}

extension DomainProfileSuggestion {
init(serializedProfile: SerializedDomainProfileSuggestion) {
self.address = serializedProfile.address
self.reasons = serializedProfile.reasons
self.score = serializedProfile.score
self.domain = serializedProfile.domain
self.imageUrl = URL(string: serializedProfile.imageUrl ?? "")
self.imageType = serializedProfile.imageType
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,40 @@ extension MockEntitiesFabric {
DomainProfileSocialAccount.typesFrom(accounts: DomainProfile.createSocialAccounts())
}
}

enum ProfileSuggestions {
static func createSuggestionsForPreview() -> [DomainProfileSuggestion] {
createSerializedSuggestionsForPreview().map { DomainProfileSuggestion(serializedProfile: $0) }
}

static func createSuggestion(domain: String = "oleg.x",
withImage: Bool = true,
imageType: DomainProfileImageType = .offChain,
reasons: [DomainProfileSuggestion.Reason] = [.nftCollection]) -> DomainProfileSuggestion {
DomainProfileSuggestion(serializedProfile: createSerializedSuggestion(domain: domain,
withImage: withImage,
imageType: imageType,
reasons: reasons))
}

static func createSerializedSuggestionsForPreview() -> [SerializedDomainProfileSuggestion] {
[createSerializedSuggestion(domain: "normal_nft_collection.x", imageType: .offChain, reasons: [.nftCollection]),
createSerializedSuggestion(domain: "normal_on_chain_poap_with_vaery_long_name_that_cant_fit_screen_size.x", imageType: .onChain, reasons: [.poap]),
createSerializedSuggestion(domain: "no_ava_transaction.x", withImage: false, reasons: [.transaction]),
createSerializedSuggestion(domain: "lens_follows.x", withImage: false, reasons: [.lensFollows]),
createSerializedSuggestion(domain: "farcaster_follows.x", reasons: [.farcasterFollows])]
}

static func createSerializedSuggestion(domain: String = "oleg.x",
withImage: Bool = true,
imageType: DomainProfileImageType = .offChain,
reasons: [DomainProfileSuggestion.Reason] = [.nftCollection]) -> SerializedDomainProfileSuggestion {
SerializedDomainProfileSuggestion(address: "0x1",
reasons: reasons.map { $0.rawValue },
score: 10,
domain: domain,
imageUrl: withImage ? ImageURLs.aiAvatar.rawValue : nil,
imageType: imageType)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ extension MockEntitiesFabric {
enum Explore {
@MainActor
static func createViewModel() -> HomeExploreViewModel {
.init(router: Home.createHomeTabRouter())
createViewModelUsing(Home.createHomeTabRouter())
}

@MainActor
static func createViewModelUsing(_ router: HomeTabRouter) -> HomeExploreViewModel {
HomeExploreViewModel(router: router)
}

static func createFollowersProfiles() -> [SerializedPublicDomainProfile] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ extension MockEntitiesFabric {
} else {
profile = Profile.createWalletProfile()
}
return HomeTabRouter(profile: profile)
return createHomeTabRouterUsing(profile: profile)
}

@MainActor
static func createHomeTabRouterUsing(profile: UserProfile,
userProfileService: UserProfileServiceProtocol = appContext.userProfileService) -> HomeTabRouter {
HomeTabRouter(profile: profile,
userProfileService: userProfileService)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1129,6 +1129,7 @@ extension String {
static let exploreEmptyNoFollowingTitle = "EXPLORE_EMPTY_NO_FOLLOWING_TITLE"
static let exploreEmptyNoFollowingSubtitle = "EXPLORE_EMPTY_NO_FOLLOWING_SUBTITLE"
static let exploreEmptyNoFollowingActionTitle = "EXPLORE_EMPTY_NO_FOLLOWING_ACTION_TITLE"
static let suggestedForYou = "SUGGESTED_FOR_YOU"

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,8 @@ import SwiftUI
struct PublicProfileView: View, ViewAnalyticsLogger {

@MainActor
static func instantiate(domain: PublicDomainDisplayInfo,
wallet: WalletEntity,
viewingDomain: DomainDisplayInfo?,
preRequestedAction: PreRequestedProfileAction?,
delegate: PublicProfileViewDelegate? = nil) -> UIViewController {
let view = PublicProfileView(domain: domain,
wallet: wallet,
viewingDomain: viewingDomain,
preRequestedAction: preRequestedAction,
delegate: delegate)
static func instantiate(configuration: PublicProfileViewConfiguration) -> UIViewController {
let view = PublicProfileView(configuration: configuration)
let vc = UIHostingController(rootView: view)
return vc
}
Expand Down Expand Up @@ -77,17 +69,9 @@ struct PublicProfileView: View, ViewAnalyticsLogger {
})
}

init(domain: PublicDomainDisplayInfo,
wallet: WalletEntity,
viewingDomain: DomainDisplayInfo?,
preRequestedAction: PreRequestedProfileAction?,
delegate: PublicProfileViewDelegate? = nil) {
_viewModel = StateObject(wrappedValue: PublicProfileViewModel(domain: domain,
wallet: wallet,
viewingDomain: viewingDomain,
preRequestedAction: preRequestedAction,
delegate: delegate))
self.delegate = delegate
init(configuration: PublicProfileViewConfiguration) {
_viewModel = StateObject(wrappedValue: PublicProfileViewModel(configuration: configuration))
self.delegate = configuration.delegate
}
}

Expand Down Expand Up @@ -800,8 +784,7 @@ private extension PublicProfileView {

@available(iOS 17, *)
#Preview {
PublicProfileView(domain: .init(walletAddress: "0x123", name: "gounstoppable.polygon"),
wallet: MockEntitiesFabric.Wallet.mockEntities()[0],
viewingDomain: MockEntitiesFabric.Domains.mockDomainDisplayInfo(),
preRequestedAction: nil)
PublicProfileView(configuration: PublicProfileViewConfiguration(domain: .init(walletAddress: "0x123", name: "gounstoppable.polygon"),
wallet: MockEntitiesFabric.Wallet.mockEntities()[0],
viewingDomain: MockEntitiesFabric.Domains.mockDomainDisplayInfo()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ extension Array where Element == DomainProfileFollowerDisplayInfo {
}
}

struct PublicProfileViewConfiguration: Identifiable {

var id: String { domain.name }

let domain: PublicDomainDisplayInfo
let wallet: WalletEntity
var viewingDomain: DomainDisplayInfo? = nil
var preRequestedAction: PreRequestedProfileAction? = nil
var delegate: PublicProfileViewDelegate? = nil
}

extension PublicProfileView {

struct FollowersDisplayInfo {
Expand All @@ -29,7 +40,7 @@ extension PublicProfileView {
case failedToLoadFollowerInfo
case failedToFindDomain
}

@MainActor
final class PublicProfileViewModel: ObservableObject, ProfileImageLoader, ViewErrorHolder {

Expand All @@ -55,15 +66,11 @@ extension PublicProfileView {
private var badgesInfo: BadgesInfo?
private var preRequestedAction: PreRequestedProfileAction?

init(domain: PublicDomainDisplayInfo,
wallet: WalletEntity,
viewingDomain: DomainDisplayInfo?,
preRequestedAction: PreRequestedProfileAction?,
delegate: PublicProfileViewDelegate?) {
self.domain = domain
self.viewingDomain = viewingDomain ?? wallet.getDomainToViewPublicProfile()
self.preRequestedAction = preRequestedAction
self.delegate = delegate
init(configuration: PublicProfileViewConfiguration) {
self.domain = configuration.domain
self.viewingDomain = configuration.viewingDomain ?? configuration.wallet.getDomainToViewPublicProfile()
self.preRequestedAction = configuration.preRequestedAction
self.delegate = configuration.delegate
self.appearTime = Date()
loadAllProfileData()
loadViewingDomainData()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,40 @@ extension HomeExplore {
}
}
}

// MARK: - Open methods
extension HomeExplore {
struct DomainProfileSuggestionSectionsBuilder {

let sections: [Section]

init(profiles: [DomainProfileSuggestion]) {
let numOfProfilesInSection = 3
let maxNumOfSections = 3
let maxNumOfProfiles = numOfProfilesInSection * maxNumOfSections

var profilesToTake = Array(profiles.prefix(maxNumOfProfiles))
var sections: [Section] = []

let numOfSections = Double(profilesToTake.count) / Double(numOfProfilesInSection)
let numOfSectionsRounded = Int(ceil(numOfSections))
for _ in 0..<numOfSectionsRounded {
let sectionProfiles = Array(profilesToTake.prefix(numOfProfilesInSection))
let section = Section(profiles: sectionProfiles)
sections.append(section)
profilesToTake = Array(profilesToTake.dropFirst(numOfProfilesInSection))
}

self.sections = sections
}

func getProfilesMatrix() -> [[DomainProfileSuggestion]] {
sections.map { $0.profiles }
}

struct Section {
let profiles: [DomainProfileSuggestion]
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@
import SwiftUI

enum HomeExploreNavigationDestination: Hashable {

case suggestionsList
}

struct HomeExploreLinkNavigationDestination {

@ViewBuilder
static func viewFor(navigationDestination: HomeExploreNavigationDestination,
tabRouter: HomeTabRouter) -> some View {

switch navigationDestination {
case .suggestionsList:
HomeExploreSuggestedProfilesListView()
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// HomeExploreSuggestedProfilesListView.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 07.03.2024.
//

import SwiftUI

struct HomeExploreSuggestedProfilesListView: View {

@EnvironmentObject var viewModel: HomeExploreViewModel

var body: some View {
List {
ForEach(viewModel.suggestedProfiles) { profile in
HomeExploreSuggestedProfileRowView(profileSuggestion: profile)
}
}
.environmentObject(viewModel)
.listStyle(.plain)
.navigationTitle(String.Constants.suggestedForYou.localized())
.navigationBarTitleDisplayMode(.inline)
}
}

#Preview {
NavigationStack {
HomeExploreSuggestedProfilesListView()
}
.environmentObject(MockEntitiesFabric.Explore.createViewModel())
}
Loading