Notes UI prepared and search memos

- searching in memos done (SDK changes adopted)
- notes UI and flow implemented and prepared, storage is needed to enjoy full experience
This commit is contained in:
Lukas Korba 2025-01-28 14:43:27 +01:00
parent 67576e5e53
commit c4e7ef9569
9 changed files with 111 additions and 32 deletions

View File

@ -73,4 +73,7 @@ public struct SDKSynchronizerClient {
public var addProofsToPCZT: (Pczt) async throws -> Pczt
public var createTransactionFromPCZT: (Pczt, Pczt) async throws -> CreateProposedTransactionsResult
public var urEncoderForPCZT: (Pczt) -> UREncoder?
// Search
public var fetchTxidsWithMemoContaining: (String) async throws -> [Data]
}

View File

@ -301,6 +301,9 @@ extension SDKSynchronizerClient: DependencyKey {
let encoder = try? keystoneSDK.generateZcashPczt(pczt_hex: pczt)
return encoder
},
fetchTxidsWithMemoContaining: { searchTerm in
try await synchronizer.fetchTxidsWithMemoContaining(searchTerm: searchTerm)
}
)
}

View File

@ -44,7 +44,8 @@ extension SDKSynchronizerClient: TestDependencyKey {
createPCZTFromProposal: unimplemented("\(Self.self).createPCZTFromProposal", placeholder: Pczt()),
addProofsToPCZT: unimplemented("\(Self.self).addProofsToPCZT", placeholder: Pczt()),
createTransactionFromPCZT: unimplemented("\(Self.self).createTransactionFromPCZT", placeholder: .success(txIds: [])),
urEncoderForPCZT: unimplemented("\(Self.self).urEncoderForPCZT", placeholder: nil)
urEncoderForPCZT: unimplemented("\(Self.self).urEncoderForPCZT", placeholder: nil),
fetchTxidsWithMemoContaining: unimplemented("\(Self.self).fetchTxidsWithMemoContaining", placeholder: [])
)
}
@ -79,7 +80,8 @@ extension SDKSynchronizerClient {
createPCZTFromProposal: { _, _ in Pczt() },
addProofsToPCZT: { _ in Pczt() },
createTransactionFromPCZT: { _, _ in .success(txIds: []) },
urEncoderForPCZT: { _ in nil }
urEncoderForPCZT: { _ in nil },
fetchTxidsWithMemoContaining: { _ in [] }
)
public static let mock = Self.mocked()
@ -185,7 +187,8 @@ extension SDKSynchronizerClient {
createPCZTFromProposal: @escaping (AccountUUID, Proposal) async throws -> Pczt = { _, _ in Pczt() },
addProofsToPCZT: @escaping (Data) async throws -> Pczt = { _ in Pczt() },
createTransactionFromPCZT: @escaping (Pczt, Pczt) async throws -> CreateProposedTransactionsResult = { _, _ in .success(txIds: []) },
urEncoderForPCZT: @escaping (Pczt) -> UREncoder? = { _ in nil}
urEncoderForPCZT: @escaping (Pczt) -> UREncoder? = { _ in nil },
fetchTxidsWithMemoContaining: @escaping (String) async throws -> [Data] = { _ in [] }
) -> SDKSynchronizerClient {
SDKSynchronizerClient(
stateStream: stateStream,
@ -217,7 +220,8 @@ extension SDKSynchronizerClient {
createPCZTFromProposal: createPCZTFromProposal,
addProofsToPCZT: addProofsToPCZT,
createTransactionFromPCZT: createTransactionFromPCZT,
urEncoderForPCZT: urEncoderForPCZT
urEncoderForPCZT: urEncoderForPCZT,
fetchTxidsWithMemoContaining: fetchTxidsWithMemoContaining
)
}
}

View File

@ -728,7 +728,7 @@ public struct Tabs {
state.transactionDetailsState.userMetadataRequest = true
return .none
case .transactionDetails(.addNoteTapped(let txId)):
case .transactionDetails(.addNoteTapped(let txId)), .transactionDetails(.saveNoteTapped(let txId)):
state.transactionDetailsState.userMetadataRequest = false
if let index = state.homeState.transactionListState.transactionList.index(id: txId) {
state.homeState.transactionListState.transactionList[index].userMetadata = state.transactionDetailsState.userMetadata
@ -740,6 +740,18 @@ public struct Tabs {
}
return .none
case .transactionDetails(.deleteNoteTapped(let txId)):
state.transactionDetailsState.userMetadataRequest = false
if let index = state.homeState.transactionListState.transactionList.index(id: txId) {
state.homeState.transactionListState.transactionList[index].userMetadata = ""
}
if let index = state.transactionsManagerState.transactionList.index(id: txId) {
state.transactionsManagerState.transactionList[index].userMetadata = ""
state.transactionDetailsState.transaction.userMetadata = ""
state.transactionDetailsState.userMetadata = ""
}
return .none
case .transactionDetails(.bookmarkTapped(let txId)):
if let index = state.transactionsManagerState.transactionList.index(id: txId) {
state.transactionsManagerState.transactionList[index].bookmarked.toggle()

View File

@ -41,6 +41,7 @@ public struct TransactionDetails {
@Shared(.appStorage(.sensitiveContent)) var isSensitiveContentHidden = false
public var messageStates: [MessageState] = []
public var userMetadata = ""
public var userMetadataOrigin = ""
@Shared(.inMemory(.toast)) public var toast: Toast.Edge? = nil
public var transaction: TransactionState
public var userMetadataRequest = false
@ -70,6 +71,7 @@ public struct TransactionDetails {
case addressTapped
case binding(BindingAction<TransactionDetails.State>)
case bookmarkTapped(String)
case deleteNoteTapped(String)
case fetchedABContacts(AddressBookContacts)
case memosLoaded([Memo])
case messageTapped(Int)
@ -77,6 +79,7 @@ public struct TransactionDetails {
case onAppear
case resolveMemos
case saveAddressTapped
case saveNoteTapped(String)
case sendAgainTapped
case sentToRowTapped
case transactionIdTapped
@ -124,7 +127,17 @@ public struct TransactionDetails {
case .binding:
return .none
case .deleteNoteTapped:
state.userMetadata = ""
state.transaction.userMetadata = ""
return .none
case .saveNoteTapped:
state.transaction.userMetadata = state.userMetadata
state.userMetadataOrigin = ""
return .none
case .addNoteTapped:
state.transaction.userMetadata = state.userMetadata
return .none
@ -160,6 +173,7 @@ public struct TransactionDetails {
return .none
case .noteButtonTapped:
state.userMetadataOrigin = state.userMetadata
return .none
case .bookmarkTapped:

View File

@ -116,10 +116,9 @@ public struct TransactionDetailsView: View {
type: .tertiary
) {
store.send(.noteButtonTapped)
isUserMetadataFocused = true
}
if store.transaction.isSentTransaction {
if store.transaction.isSentTransaction && !store.transaction.isShieldingTransaction {
if store.alias == nil {
ZashiButton(
"Save address"
@ -150,7 +149,7 @@ public struct TransactionDetailsView: View {
store.send(.onAppear)
}
.sheet(isPresented: $store.userMetadataRequest) {
userMetadataContent()
userMetadataContent(!store.transaction.userMetadata.isEmpty)
}
}
.navigationBarTitleDisplayMode(.inline)

View File

@ -11,33 +11,36 @@ import Generated
import UIComponents
extension TransactionDetailsView {
@ViewBuilder func userMetadataContent() -> some View {
@ViewBuilder func userMetadataContent(_ isEditMode: Bool) -> some View {
WithPerceptionTracking {
if #available(iOS 16.0, *) {
mainBodyUM()
mainBodyUM(isEditMode: isEditMode)
.presentationDetents([.height(filtersSheetHeight)])
.presentationDragIndicator(.visible)
} else {
mainBodyUM(stickToBottom: true)
mainBodyUM(isEditMode: isEditMode, stickToBottom: true)
}
}
}
@ViewBuilder func mainBodyUM(stickToBottom: Bool = false) -> some View {
@ViewBuilder func mainBodyUM(isEditMode: Bool, stickToBottom: Bool = false) -> some View {
VStack(alignment: .leading, spacing: 0) {
if stickToBottom {
Spacer()
}
Text("Add a note")
.zFont(.semiBold, size: 20, style: Design.Text.primary)
.padding(.top, 32)
.padding(.bottom, 16)
Text(isEditMode
? "Edit a note"
: "Add a note"
)
.zFont(.semiBold, size: 20, style: Design.Text.primary)
.padding(.top, 32)
.padding(.bottom, 16)
VStack(alignment: .leading, spacing: 6) {
TextEditor(text: $store.userMetadata)
.focused($isUserMetadataFocused)
.font(.custom(FontFamily.Inter.regular.name, size: 16))
.font(.custom(FontFamily.Inter.medium.name, size: 16))
.frame(height: 122)
.padding(.horizontal, 10)
.padding(.top, 2)
@ -54,7 +57,7 @@ extension TransactionDetailsView {
.onTapGesture {
isUserMetadataFocused = true
}
Spacer()
}
.padding(.top, 10)
@ -66,19 +69,40 @@ extension TransactionDetailsView {
EmptyView()
}
}
Text("\(store.userMetadata.count)/\(TransactionDetails.State.Constants.userMetadataMaxLength) characters")
.zFont(size: 14, style: Design.Inputs.Default.hint)
}
.padding(.bottom, 32)
ZashiButton(
"Add note",
type: .secondary
) {
store.send(.addNoteTapped(store.transaction.id))
if isEditMode {
HStack(spacing: 8) {
ZashiButton(
"Delete note",
type: .destructive1
) {
store.send(.deleteNoteTapped(store.transaction.id))
}
ZashiButton(
"Save note",
type: .secondary
) {
store.send(.saveNoteTapped(store.transaction.id))
}
.disabled(store.userMetadata == store.userMetadataOrigin)
}
.padding(.bottom, 24)
} else {
ZashiButton(
"Add note",
type: .secondary
) {
store.send(.addNoteTapped(store.transaction.id))
}
.disabled(store.userMetadata.isEmpty)
.padding(.bottom, 24)
}
.disabled(store.userMetadata.isEmpty)
.padding(.bottom, 24)
}
.screenHorizontalPadding()
.background {

View File

@ -74,6 +74,7 @@ public struct TransactionsManager {
}
public enum Action: BindableAction, Equatable {
case asynchronousMemoSearchResult([String])
case applyFiltersTapped
case binding(BindingAction<TransactionsManager.State>)
case eraseSearchTermTapped
@ -270,17 +271,36 @@ public struct TransactionsManager {
if !state.searchTerm.isEmpty && state.searchTerm.count >= 2 {
state.searchedTransactionsList.removeAll()
// synchronous search
state.transactionList.forEach { transaction in
if checkSearchTerm(state.searchTerm, transaction: transaction, addressBookContacts: state.addressBookContacts) {
state.searchedTransactionsList.append(transaction)
}
}
// asynchronous search
return .run { [searchTerm = state.searchTerm] send in
let txids = try? await sdkSynchronizer.fetchTxidsWithMemoContaining(searchTerm).map {
$0.toHexStringTxId()
}
if let txids {
await send(.asynchronousMemoSearchResult(txids))
} else {
await send(.updateTransactionsAccordingToFilters)
}
}
} else {
state.searchedTransactionsList = state.transactionList
}
return .send(.updateTransactionsAccordingToFilters)
case .asynchronousMemoSearchResult(let txids):
let results = state.transactionList.filter { txids.contains($0.id) }
state.searchedTransactionsList.append(contentsOf: results)
return .send(.updateTransactionsAccordingToFilters)
case .updateTransactionsAccordingToFilters:
// modify the initial list of all transactions according to active filters
if !state.activeFilters.isEmpty {
@ -423,11 +443,11 @@ extension TransactionsManager {
}
}
// fulsearch amounts
// fullsearch amounts
if input.contains(searchTerm) {
return true
}
return false
}

View File

@ -2514,7 +2514,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
@ -2545,7 +2545,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;
@ -2575,7 +2575,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "zashi-internal.entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8;
CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_ASSET_PATHS = "\"secant/Preview Content\"";
DEVELOPMENT_TEAM = RLPRR8CPQG;
ENABLE_BITCODE = NO;