Compare commits

..

1 Commits

Author SHA1 Message Date
685eb3e277 Fix missing profile zap notification text
Changelog-Fixed: Fix missing profile zap notification text
2023-06-27 07:06:08 -04:00
64 changed files with 579 additions and 1332 deletions

View File

@@ -94,34 +94,12 @@ damus implements the following [Nostr Implementation Possibilities][nips]
Contributors welcome! Start by examining known issues: https://github.com/damus-io/damus/issues.
### Mailing lists
We have a few mailing lists that anyone can join to get involved in damus development:
- [dev][dev-list] - development discussions
- [patches][patches-list] - code submission and review
- [product][product-list] - product discussions
- [design][design-list] - design discussions
[dev-list]: https://damus.io/list/dev
[patches-list]: https://damus.io/list/patches
[product-list]: https://damus.io/list/product
[design-list]: https://damus.io/list/design
### Code
[Email patches][git-send-email] to patches@damus.io are preferred, but I accept PRs on GitHub as well. Patches sent via email may include a bolt11 lightning invoice, choosing the price you think the patch is worth, and I will pay it once the patch is accepted and if I think the price isn't unreasonable. You can also send an any-amount invoice and I will pay what I think it's worth if you prefer not to choose. You can include the bolt11 in the commit body or email so that it can be paid once it is applied.
[Email patches][git-send-email] to jb55@jb55.com are preferred, but I accept PRs on GitHub as well. 50,000 sats will be rewarded for patches sent via email or [nostr][patchstr]
Recommended settings when submitting code via email:
```
$ git config sendemail.to "patches@damus.io"
$ git config format.subjectPrefix "PATCH damus"
$ git config --global sendemail.annotate yes
$ git config format.signOff yes
```
[git-send-email]: http://git-send-email.io
[patchstr]: https://patchstr.com
### Privacy
Your internet protocol (IP) address is exposed to the relays you connect to, and third party media hosters (e.g. nostr.build, imgur.com, giphy.com, youtube.com etc.) that render on Damus. If you want to improve your privacy, consider utilizing a service that masks your IP address (e.g. a VPN) from trackers online.

View File

@@ -20,11 +20,7 @@
3A4325A82961E11400BFCD9D /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 3A4325AA2961E11400BFCD9D /* Localizable.stringsdict */; };
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A4647CE2A413ADC00386AD8 /* CondensedProfilePicturesView.swift */; };
3A48E7B029DFBE9D006E787E /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; };
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E47C42A4A6CF400C0D090 /* Trie.swift */; };
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A5E47C62A4A76C800C0D090 /* TrieTests.swift */; };
3A8CC6CC2A2CFEF900940F5F /* StringUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */; };
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */; };
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */; };
3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FC297E3CFF0090C62D /* RepostsModel.swift */; };
3AA247FF297E3D900090C62D /* RepostsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA247FE297E3D900090C62D /* RepostsView.swift */; };
3AA24802297E3DC20090C62D /* RepostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AA24801297E3DC20090C62D /* RepostView.swift */; };
@@ -308,7 +304,6 @@
9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
DD597CBD2963D85A00C64D32 /* MarkdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */; };
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; };
E990020F2955F837003BBC5A /* EditMetadataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E990020E2955F837003BBC5A /* EditMetadataView.swift */; };
@@ -381,8 +376,6 @@
3A5CAE1D298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A5CAE1E298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-CN"; path = "zh-CN.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A5CAE1F298DC0DB00B5334F /* zh-CN */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "zh-CN"; path = "zh-CN.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
3A5E47C42A4A6CF400C0D090 /* Trie.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Trie.swift; sourceTree = "<group>"; };
3A5E47C62A4A76C800C0D090 /* TrieTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrieTests.swift; sourceTree = "<group>"; };
3A66D927299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = "<group>"; };
3A66D928299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = "<group>"; };
3A66D929299472FA008B44F4 /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ja; path = ja.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
@@ -396,8 +389,6 @@
3A8624DA299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = "<group>"; };
3A8624DB299E82BE00BD8BE9 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = cs; path = cs.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtil.swift; sourceTree = "<group>"; };
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UserSearchCache.swift; sourceTree = "<group>"; };
3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSearchCacheTests.swift; sourceTree = "<group>"; };
3A929C20297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A929C21297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "it-IT"; path = "it-IT.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A929C22297F2CF80090925E /* it-IT */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "it-IT"; path = "it-IT.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -762,7 +753,6 @@
9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; };
DD597CBC2963D85A00C64D32 /* MarkdownTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarkdownTests.swift; sourceTree = "<group>"; };
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
E990020E2955F837003BBC5A /* EditMetadataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditMetadataView.swift; sourceTree = "<group>"; };
@@ -929,8 +919,6 @@
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
3A5E47C42A4A6CF400C0D090 /* Trie.swift */,
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1167,7 +1155,6 @@
50B5685229F97CB400A23243 /* CredentialHandler.swift */,
4C7D09582A05BEAD00943473 /* KeyboardVisible.swift */,
3A8CC6CB2A2CFEF900940F5F /* StringUtil.swift */,
D2277EE92A089BD5006C3807 /* Router.swift */,
);
path = Util;
sourceTree = "<group>";
@@ -1320,9 +1307,7 @@
4CE6DEE427F7A08100C66700 /* Products */,
4CEE2AE62804F57B00AB5EEF /* Frameworks */,
);
indentWidth = 4;
sourceTree = "<group>";
tabWidth = 4;
};
4CE6DEE427F7A08100C66700 /* Products */ = {
isa = PBXGroup;
@@ -1389,8 +1374,6 @@
4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */,
501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */,
3AFBF3FC29FDA7CC00E79C7C /* CustomZapViewTests.swift */,
3A5E47C62A4A76C800C0D090 /* TrieTests.swift */,
3A90B1822A4EA3C600000D94 /* UserSearchCacheTests.swift */,
);
path = damusTests;
sourceTree = "<group>";
@@ -1743,7 +1726,6 @@
4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */,
4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */,
4C8D1A6F29F31E5000ACDF75 /* FriendsButton.swift in Sources */,
3A5E47C52A4A6CF400C0D090 /* Trie.swift in Sources */,
4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */,
4C75EFA627FF87A20006080F /* Nostr.swift in Sources */,
4CB883A62975F83C00DC99E7 /* LNUrlPayRequest.swift in Sources */,
@@ -1879,8 +1861,6 @@
5C6E1DAD2A193EC2008FC15A /* GradientButtonStyle.swift in Sources */,
4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */,
3A4647CF2A413ADC00386AD8 /* CondensedProfilePicturesView.swift in Sources */,
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */,
3A90B1812A4EA3AF00000D94 /* UserSearchCache.swift in Sources */,
4C5C7E6A284EDE2E00A22DF5 /* SearchResultsView.swift in Sources */,
4CE1399429F0669900AC6A0B /* BigButton.swift in Sources */,
7C60CAEF298471A1009C80D6 /* CoreSVG.swift in Sources */,
@@ -1972,7 +1952,6 @@
buildActionMask = 2147483647;
files = (
5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */,
3A90B1832A4EA3C600000D94 /* UserSearchCacheTests.swift in Sources */,
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */,
4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */,
3A30410129AB12AA008A0F29 /* EventGroupViewTests.swift in Sources */,
@@ -1987,7 +1966,6 @@
4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */,
4CB8839A297322D200DC99E7 /* DMTests.swift in Sources */,
F944F56E29EA9CCC0067B3BF /* DamusParseContentTests.swift in Sources */,
3A5E47C72A4A76C800C0D090 /* TrieTests.swift in Sources */,
4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */,
4C363AA02828A8DD006E126D /* LikeTests.swift in Sources */,
4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */,
@@ -2276,7 +2254,6 @@
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -2325,7 +2302,6 @@
INFOPLIST_KEY_UILaunchStoryboardName = Launch.storyboard;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -2356,7 +2332,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2376,7 +2352,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = XK7H4JAB3D;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.3;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.jb55.damusTests;
PRODUCT_NAME = "$(TARGET_NAME)";

View File

@@ -11,10 +11,21 @@ struct UserViewRow: View {
let damus_state: DamusState
let pubkey: String
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: pubkey)
UserView(damus_state: damus_state, pubkey: pubkey)
.contentShape(Rectangle())
.background(.clear)
.background(
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
)
.onTapGesture {
navigating = true
}
}
}

View File

@@ -106,7 +106,7 @@ struct ZapButton: View {
var body: some View {
HStack(spacing: 4) {
if !damus_state.settings.nozaps || zaps.zap_total > 0 {
if zaps.zap_total > 0 {
Button(action: {
}, label: {
Image(zap_img)
@@ -116,9 +116,7 @@ struct ZapButton: View {
.aspectRatio(contentMode: .fit)
.frame(width:20, height: 20)
})
}
if zaps.zap_total > 0 {
Text(verbatim: format_msats_abbrev(zaps.zap_total))
.font(.footnote)
.foregroundColor(zap_color)
@@ -126,14 +124,21 @@ struct ZapButton: View {
}
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
.simultaneousGesture(LongPressGesture().onEnded {_ in
guard !damus_state.settings.nozaps else { return }
// when we don't have nozaps mode enable, long press shows the zap customizer
if !damus_state.settings.nozaps {
//present_sheet(.zap(target: target, lnurl: lnurl))
}
present_sheet(.zap(target: target, lnurl: lnurl))
// long press does nothing in nozaps mode
})
.highPriorityGesture(TapGesture().onEnded {
guard !damus_state.settings.nozaps else { return }
tap()
// when we have appstore mode on, only show the zap customizer as "user zaps"
if damus_state.settings.nozaps {
//present_sheet(.zap(target: target, lnurl: lnurl))
} else {
// otherwise we restore the original behavior of one-tap zaps
tap()
}
})
}
}

View File

@@ -82,6 +82,14 @@ struct ContentView: View {
@State var damus_state: DamusState? = nil
@SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home
@State var is_deleted_account: Bool = false
@State var active_profile: String? = nil
@State var active_search: NostrFilter? = nil
@State var active_event: NostrEvent? = nil
@State var profile_open: Bool = false
@State var thread_open: Bool = false
@State var search_open: Bool = false
@State var wallet_open: Bool = false
@State var active_nwc: WalletConnectURL? = nil
@State var muting: String? = nil
@State var confirm_mute: Bool = false
@State var user_muted_confirm: Bool = false
@@ -89,7 +97,6 @@ struct ContentView: View {
@SceneStorage("ContentView.filter_state") var filter_state : FilterState = .posts_and_replies
@State private var isSideBarOpened = false
var home: HomeModel = HomeModel()
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
let sub_id = UUID().description
@@ -121,7 +128,7 @@ struct ContentView: View {
if privkey != nil {
PostButtonContainer(is_left_handed: damus_state?.settings.left_handed ?? false) {
self.active_sheet = .post(.posting(.none))
self.active_sheet = .post(.posting)
}
}
}
@@ -148,7 +155,10 @@ struct ContentView: View {
}
func popToRoot() {
navigationCoordinator.popToRoot()
profile_open = false
thread_open = false
search_open = false
wallet_open = false
isSideBarOpened = false
}
@@ -159,6 +169,21 @@ struct ContentView: View {
func MainContent(damus: DamusState) -> some View {
VStack {
NavigationLink(destination: WalletView(damus_state: damus, model: damus_state!.wallet), isActive: $wallet_open) {
EmptyView()
}
NavigationLink(destination: MaybeProfileView, isActive: $profile_open) {
EmptyView()
}
if let active_event {
let thread = ThreadModel(event: active_event, damus_state: damus_state!)
NavigationLink(destination: ThreadView(state: damus_state!, thread: thread), isActive: $thread_open) {
EmptyView()
}
}
NavigationLink(destination: MaybeSearchView, isActive: $search_open) {
EmptyView()
}
switch selected_timeline {
case .search:
if #available(iOS 16.0, *) {
@@ -200,6 +225,28 @@ struct ContentView: View {
}
}
var MaybeSearchView: some View {
Group {
if let search = self.active_search {
SearchView(appstate: damus_state!, search: SearchModel(state: damus_state!, search: search))
} else {
EmptyView()
}
}
}
var MaybeProfileView: some View {
Group {
if let pk = self.active_profile {
let profile_model = ProfileModel(pubkey: pk, damus: damus_state!)
let followers = FollowersModel(damus_state: damus_state!, target: pk)
ProfileView(damus_state: damus_state!, profile: profile_model, followers: followers)
} else {
EmptyView()
}
}
}
func MaybeReportView(target: ReportTarget) -> some View {
Group {
if let damus_state {
@@ -215,30 +262,32 @@ struct ContentView: View {
}
func open_event(ev: NostrEvent) {
let thread = ThreadModel(event: ev, damus_state: damus_state!)
navigationCoordinator.push(route: Route.Thread(thread: thread))
popToRoot()
self.active_event = ev
self.thread_open = true
}
func open_wallet(nwc: WalletConnectURL) {
self.damus_state!.wallet.new(nwc)
navigationCoordinator.push(route: Route.Wallet(wallet: damus_state!.wallet))
self.wallet_open = true
}
func open_profile(id: String) {
let profile_model = ProfileModel(pubkey: id, damus: damus_state!)
let followers = FollowersModel(damus_state: damus_state!, target: id)
navigationCoordinator.push(route: Route.Profile(profile: profile_model, followers: followers))
popToRoot()
self.active_profile = id
self.profile_open = true
}
func open_search(filt: NostrFilter) {
let search = SearchModel(state: damus_state!, search: filt)
navigationCoordinator.push(route: Route.Search(search: search))
popToRoot()
self.active_search = filt
self.search_open = true
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
if let damus = self.damus_state {
NavigationStack(path: $navigationCoordinator.path) {
NavigationView {
TabView { // Prevents navbar appearance change on scroll
MainContent(damus: damus)
.toolbar() {
@@ -278,12 +327,6 @@ struct ContentView: View {
.overlay(
SideMenuView(damus_state: damus, isSidebarVisible: $isSideBarOpened.animation())
)
.navigationDestination(for: Route.self) { route in
route.view(navigationCordinator: navigationCoordinator, damusState: damus_state!)
}
.onReceive(handle_notify(.switched_timeline)) { _ in
navigationCoordinator.popToRoot()
}
}
.navigationViewStyle(.stack)
@@ -477,8 +520,8 @@ struct ContentView: View {
switch local.type {
case .dm:
selected_timeline = .dms
damus_state.dms.set_active_dm(target.pubkey)
navigationCoordinator.push(route: Route.DMChat(dms: damus_state.dms.active_model))
damus_state.dms.open_dm_by_pk(target.pubkey)
case .like: fallthrough
case .zap: fallthrough
case .mention: fallthrough
@@ -624,14 +667,13 @@ struct ContentView: View {
let nwc = WalletConnectURL(str: nwc_str) {
try? pool.add_relay(.nwc(url: nwc.relay))
}
let user_search_cache = UserSearchCache()
self.damus_state = DamusState(pool: pool,
keypair: keypair,
likes: EventCounter(our_pubkey: pubkey),
boosts: EventCounter(our_pubkey: pubkey),
contacts: Contacts(our_pubkey: pubkey),
profiles: Profiles(user_search_cache: user_search_cache),
profiles: Profiles(),
dms: home.dms,
previews: PreviewCache(),
zaps: Zaps(our_pubkey: pubkey),
@@ -646,9 +688,7 @@ struct ContentView: View {
bootstrap_relays: bootstrap_relays,
replies: ReplyCounter(our_pubkey: pubkey),
muted_threads: MutedThreadsManager(keypair: keypair),
wallet: WalletModel(settings: settings),
nav: self.navigationCoordinator,
user_search_cache: user_search_cache
wallet: WalletModel(settings: settings)
)
home.damus_state = self.damus_state!
@@ -878,18 +918,17 @@ func handle_unfollow(state: DamusState, notif: Notification) {
let target = notif.object as! FollowTarget
let pk = target.pubkey
let old_contacts = state.contacts.event
if let ev = unfollow_user(postbox: state.postbox,
our_contacts: old_contacts,
our_contacts: state.contacts.event,
pubkey: state.pubkey,
privkey: privkey,
unfollow: pk) {
notify(.unfollowed, pk)
state.contacts.event = ev
state.contacts.remove_friend(pk)
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
//friend_events = friend_events.filter { $0.pubkey != pk }
}
}

View File

@@ -30,8 +30,6 @@ struct DamusState {
let replies: ReplyCounter
let muted_threads: MutedThreadsManager
let wallet: WalletModel
let nav: NavigationCoordinator
let user_search_cache: UserSearchCache
@discardableResult
func add_zap(zap: Zapping) -> Bool {
@@ -59,6 +57,5 @@ struct DamusState {
}
static var empty: DamusState {
let user_search_cache = UserSearchCache()
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(user_search_cache: user_search_cache), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator(), user_search_cache: user_search_cache) }
return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: ""), muted_threads: MutedThreadsManager(keypair: Keypair(pubkey: "", privkey: nil)), wallet: WalletModel(settings: UserSettingsStore())) }
}

View File

@@ -30,6 +30,16 @@ class DirectMessagesModel: ObservableObject {
self.active_model = model
}
func open_dm_by_pk(_ pubkey: String) {
self.set_active_dm(pubkey)
self.open_dm = true
}
func open_dm_by_model(_ model: DirectMessageModel) {
self.set_active_dm_model(model)
self.open_dm = true
}
func set_active_dm(_ pubkey: String) {
for model in self.dms where model.pubkey == pubkey {
self.set_active_dm_model(model)

View File

@@ -612,8 +612,7 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
contacts.add_friend_contact(ev)
}
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
let contacts = state.contacts
func load_our_contacts(contacts: Contacts, m_old_ev: NostrEvent?, ev: NostrEvent) {
var new_pks = Set<String>()
// our contacts
for tag in ev.tags {
@@ -642,8 +641,6 @@ func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent)
contacts.remove_friend(pk)
}
}
state.user_search_cache.updateOwnContactsPetnames(id: contacts.our_pubkey, oldEvent: m_old_ev, newEvent: ev)
}
@@ -704,9 +701,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P
}
var old_nip05: String? = nil
let mprof = profiles.lookup_with_timestamp(id: ev.pubkey)
if let mprof {
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
old_nip05 = mprof.profile.nip05
if mprof.event.created_at > ev.created_at {
// skip if we already have an newer profile
@@ -757,7 +752,7 @@ func guard_valid_event(events: EventCache, ev: NostrEvent, callback: @escaping (
switch validated {
case .unknown:
Task.detached(priority: .medium) {
Task {
let result = validate_event(ev: ev)
DispatchQueue.main.async {
@@ -815,7 +810,7 @@ func load_our_stuff(state: DamusState, ev: NostrEvent) {
let m_old_ev = state.contacts.event
state.contacts.event = ev
load_our_contacts(state: state, m_old_ev: m_old_ev, ev: ev)
load_our_contacts(contacts: state.contacts, m_old_ev: m_old_ev, ev: ev)
load_our_relays(state: state, m_old_ev: m_old_ev, ev: ev)
}
@@ -863,7 +858,6 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
if changed {
save_bootstrap_relays(pubkey: state.pubkey, relays: Array(new))
state.pool.connect()
notify(.relays_changed, ())
}
}

View File

@@ -1,129 +0,0 @@
//
// Trie.swift
// damus
//
// Created by Terry Yiu on 6/26/23.
//
import Foundation
/// Tree data structure of all the substring permutations of a collection of strings optimized for searching for values of type V.
///
/// Each node in the tree can have child nodes.
/// Each node represents a single character in substrings, and each of its child nodes represent the subsequent character in those substrings.
///
/// A node that has no children mean that there are no substrings with any additional characters beyond the branch of letters leading up to that node.
///
/// A node that has values mean that there are strings that end in the character represented by the node and contain the substring represented by the branch of letters leading up to that node.
///
/// https://en.wikipedia.org/wiki/Trie
class Trie<V: Hashable> {
private var children: [Character : Trie] = [:]
/// Separate exact matches from strict substrings so that exact matches appear first in returned results.
private var exactMatchValues = Set<V>()
private var substringMatchValues = Set<V>()
private var parent: Trie? = nil
}
extension Trie {
var hasChildren: Bool {
return !self.children.isEmpty
}
var hasValues: Bool {
return !self.exactMatchValues.isEmpty || !self.substringMatchValues.isEmpty
}
/// Finds the branch that matches the specified key and returns the values from all of its descendant nodes.
func find(key: String) -> [V] {
var currentNode = self
// Find branch with matching prefix.
for char in key {
if let child = currentNode.children[char] {
currentNode = child
} else {
return []
}
}
// Perform breadth-first search from matching branch and collect values from all descendants.
var exactMatches = Set<V>()
var substringMatches = Set<V>()
var queue = [currentNode]
while !queue.isEmpty {
let node = queue.removeFirst()
exactMatches.formUnion(node.exactMatchValues)
substringMatches.formUnion(node.substringMatchValues)
queue.append(contentsOf: node.children.values)
}
return Array(exactMatches) + substringMatches
}
/// Inserts value of type V into this trie for the specified key. This function stores all substring endings of the key, not only the key itself.
/// Runtime performance is O(n^2) and storage cost is O(n), where n is the number of characters in the key.
func insert(key: String, value: V) {
// Create root branches for each character of the key to enable substring searches instead of only just prefix searches.
// Hence the nested loop.
for i in 0..<key.count {
var currentNode = self
// Find branch with matching prefix.
for char in key[key.index(key.startIndex, offsetBy: i)...] {
if let child = currentNode.children[char] {
currentNode = child
} else {
let child = Trie()
child.parent = currentNode
currentNode.children[char] = child
currentNode = child
}
}
if i == 0 {
currentNode.exactMatchValues.insert(value)
} else {
currentNode.substringMatchValues.insert(value)
}
}
}
/// Removes value of type V from this trie for the specified key.
func remove(key: String, value: V) {
for i in 0..<key.count {
var currentNode = self
var foundLeafNode = true
// Find branch with matching prefix.
for j in i..<key.count {
let char = key[key.index(key.startIndex, offsetBy: j)]
if let child = currentNode.children[char] {
currentNode = child
} else {
foundLeafNode = false
break
}
}
if foundLeafNode {
currentNode.exactMatchValues.remove(value)
currentNode.substringMatchValues.remove(value)
// Clean up the tree if this leaf node no longer holds values or children.
for j in (i..<key.count).reversed() {
if let parent = currentNode.parent, !currentNode.hasValues && !currentNode.hasChildren {
currentNode = parent
let char = key[key.index(key.startIndex, offsetBy: j)]
currentNode.children.removeValue(forKey: char)
}
}
}
}
}
}

View File

@@ -1,107 +0,0 @@
//
// UserSearchCache.swift
// damus
//
// Created by Terry Yiu on 6/27/23.
//
import Foundation
/// Cache of searchable users by name, display_name, NIP-05 identifier, or own contact list petname.
/// Optimized for fast searches of substrings by using a Trie.
/// Optimal for performing user searches that could be initiated by typing quickly on a keyboard into a text input field.
class UserSearchCache {
private let trie = Trie<String>()
func search(key: String) -> [String] {
let results = trie.find(key: key)
return results
}
/// Computes the differences between an old profile, if it exists, and a new profile, and updates the user search cache accordingly.
func updateProfile(id: String, profiles: Profiles, oldProfile: Profile?, newProfile: Profile) {
// Remove searchable keys tied to the old profile if they differ from the new profile
// to keep the trie clean without empty nodes while avoiding excessive graph searching.
if let oldProfile {
if let oldName = oldProfile.name, newProfile.name?.caseInsensitiveCompare(oldName) != .orderedSame {
trie.remove(key: oldName.lowercased(), value: id)
}
if let oldDisplayName = oldProfile.display_name, newProfile.display_name?.caseInsensitiveCompare(oldDisplayName) != .orderedSame {
trie.remove(key: oldDisplayName.lowercased(), value: id)
}
if let oldNip05 = oldProfile.nip05, newProfile.nip05?.caseInsensitiveCompare(oldNip05) != .orderedSame {
trie.remove(key: oldNip05.lowercased(), value: id)
}
}
addProfile(id: id, profiles: profiles, profile: newProfile)
}
/// Adds a profile to the user search cache.
private func addProfile(id: String, profiles: Profiles, profile: Profile) {
// Searchable by name.
if let name = profile.name {
trie.insert(key: name.lowercased(), value: id)
}
// Searchable by display name.
if let displayName = profile.display_name {
trie.insert(key: displayName.lowercased(), value: id)
}
// Searchable by NIP-05 identifier.
if let nip05 = profiles.is_validated(id) {
trie.insert(key: "\(nip05.username.lowercased())@\(nip05.host.lowercased())", value: id)
}
}
/// Computes the diffences between an old contacts event and a new contacts event for our own user, and updates the search cache accordingly.
func updateOwnContactsPetnames(id: String, oldEvent: NostrEvent?, newEvent: NostrEvent) {
guard newEvent.known_kind == .contacts && newEvent.pubkey == id else {
return
}
var petnames: [String: String] = [:]
// Gets all petnames from our new contacts list.
newEvent.tags.forEach { tag in
guard tag.count >= 4 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
let petname = tag[3]
petnames[pubkey] = petname
}
// Compute the diff with the old contacts list, if it exists,
// mark the ones that are the same to not be removed from the user search cache,
// and remove the old ones that are different from the user search cache.
if let oldEvent, oldEvent.known_kind == .contacts && oldEvent.pubkey == id {
oldEvent.tags.forEach { tag in
guard tag.count >= 4 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
let oldPetname = tag[3]
if let newPetname = petnames[pubkey] {
if newPetname.caseInsensitiveCompare(oldPetname) == .orderedSame {
petnames.removeValue(forKey: pubkey)
} else {
trie.remove(key: oldPetname, value: pubkey)
}
} else {
trie.remove(key: oldPetname, value: pubkey)
}
}
}
// Add the new petnames to the user search cache.
for (pubkey, petname) in petnames {
trie.insert(key: petname, value: pubkey)
}
}
}

View File

@@ -23,12 +23,6 @@ class Profiles {
var zappers: [String: String] = [:]
private let database = ProfileDatabase()
let user_search_cache: UserSearchCache
init(user_search_cache: UserSearchCache) {
self.user_search_cache = user_search_cache
}
func is_validated(_ pk: String) -> NIP05? {
validated[pk]
@@ -46,9 +40,7 @@ class Profiles {
func add(id: String, profile: TimestampedProfile) {
queue.async(flags: .barrier) {
let old_timestamped_profile = self.profiles[id]
self.profiles[id] = profile
self.user_search_cache.updateProfile(id: id, profiles: self, oldProfile: old_timestamped_profile?.profile, newProfile: profile.profile)
}
Task {

View File

@@ -1,276 +0,0 @@
//
// Router.swift
// damus
//
// Created by Scott Penrose on 5/7/23.
//
import SwiftUI
enum Route: Hashable {
case ProfileByKey(pubkey: String)
case Profile(profile: ProfileModel, followers: FollowersModel)
case Followers(followers: FollowersModel)
case Relay(relay: String, showActionButtons: Binding<Bool>)
case RelayDetail(relay: String, metadata: RelayMetadata)
case Following(following: FollowingModel)
case MuteList(users: [String])
case RelayConfig
case Bookmarks
case Config
case EditMetadata
case DMChat(dms: DirectMessageModel)
case UserRelays(relays: [String])
case KeySettings(keypair: Keypair)
case AppearanceSettings(settings: UserSettingsStore)
case NotificationSettings(settings: UserSettingsStore)
case ZapSettings(settings: UserSettingsStore)
case TranslationSettings(settings: UserSettingsStore)
case SearchSettings(settings: UserSettingsStore)
case Thread(thread: ThreadModel)
case Reposts(reposts: RepostsModel)
case Reactions(reactions: ReactionsModel)
case Zaps(target: ZapTarget)
case Search(search: SearchModel)
case EULA
case Login
case CreateAccount
case SaveKeys(account: CreateAccountModel)
case Wallet(wallet: WalletModel)
case WalletScanner(result: Binding<WalletScanResult>)
case FollowersYouKnow(friendedFollowers: [String], followers: FollowersModel)
@ViewBuilder
func view(navigationCordinator: NavigationCoordinator, damusState: DamusState) -> some View {
switch self {
case .ProfileByKey(let pubkey):
ProfileView(damus_state: damusState, pubkey: pubkey)
case .Profile(let profile, let followers):
ProfileView(damus_state: damusState, profile: profile, followers: followers)
case .Followers(let followers):
FollowersView(damus_state: damusState, followers: followers)
case .Relay(let relay, let showActionButtons):
RelayView(state: damusState, relay: relay, showActionButtons: showActionButtons)
case .RelayDetail(let relay, let metadata):
RelayDetailView(state: damusState, relay: relay, nip11: metadata)
case .Following(let following):
FollowingView(damus_state: damusState, following: following)
case .MuteList(let users):
MutelistView(damus_state: damusState, users: users)
case .RelayConfig:
RelayConfigView(state: damusState)
case .Bookmarks:
BookmarksView(state: damusState)
case .Config:
ConfigView(state: damusState)
case .EditMetadata:
EditMetadataView(damus_state: damusState)
case .DMChat(let dms):
DMChatView(damus_state: damusState, dms: dms)
case .UserRelays(let relays):
UserRelaysView(state: damusState, relays: relays)
case .KeySettings(let keypair):
KeySettingsView(keypair: keypair)
case .AppearanceSettings(let settings):
AppearanceSettingsView(settings: settings)
case .NotificationSettings(let settings):
NotificationSettingsView(settings: settings)
case .ZapSettings(let settings):
ZapSettingsView(settings: settings)
case .TranslationSettings(let settings):
NotificationSettingsView(settings: settings)
case .SearchSettings(let settings):
SearchSettingsView(settings: settings)
case .Thread(let thread):
ThreadView(state: damusState, thread: thread)
case .Reposts(let reposts):
RepostsView(damus_state: damusState, model: reposts)
case .Reactions(let reactions):
ReactionsView(damus_state: damusState, model: reactions)
case .Zaps(let target):
ZapsView(state: damusState, target: target)
case .Search(let search):
SearchView(appstate: damusState, search: search)
case .EULA:
EULAView(nav: navigationCordinator)
case .Login:
LoginView(nav: navigationCordinator)
case .CreateAccount:
CreateAccountView(nav: navigationCordinator)
case .SaveKeys(let account):
SaveKeysView(account: account)
case .Wallet(let walletModel):
WalletView(damus_state: damusState, model: walletModel)
case .WalletScanner(let walletScanResult):
WalletScannerView(result: walletScanResult)
case .FollowersYouKnow(let friendedFollowers, let followers):
FollowersYouKnowView(damus_state: damusState, friended_followers: friendedFollowers, followers: followers)
}
}
static func == (lhs: Route, rhs: Route) -> Bool {
switch (lhs, rhs) {
case (.ProfileByKey (let lhs_pubkey), .ProfileByKey(let rhs_pubkey)):
return lhs_pubkey == rhs_pubkey
case (.Profile (let lhs_profile, _), .Profile(let rhs_profile, _)):
return lhs_profile == rhs_profile
case (.Followers (_), .Followers (_)):
return true
case (.Relay (let lhs_relay, _), .Relay (let rhs_relay, _)):
return lhs_relay == rhs_relay
case (.RelayDetail(let lhs_relay, _), .RelayDetail(let rhs_relay, _)):
return lhs_relay == rhs_relay
case (.Following(_), .Following(_)):
return true
case (.MuteList(let lhs_users), .MuteList(let rhs_users)):
return lhs_users == rhs_users
case (.RelayConfig, .RelayConfig):
return true
case (.Bookmarks, .Bookmarks):
return true
case (.Config, .Config):
return true
case (.EditMetadata, .EditMetadata):
return true
case (.DMChat(let lhs_dms), .DMChat(let rhs_dms)):
return lhs_dms.our_pubkey == rhs_dms.our_pubkey
case (.UserRelays(let lhs_relays), .UserRelays(let rhs_relays)):
return lhs_relays == rhs_relays
case (.KeySettings(let lhs_keypair), .KeySettings(let rhs_keypair)):
return lhs_keypair.pubkey == rhs_keypair.pubkey
case (.AppearanceSettings(_), .AppearanceSettings(_)):
return true
case (.NotificationSettings(_), .NotificationSettings(_)):
return true
case (.ZapSettings(_), .ZapSettings(_)):
return true
case (.TranslationSettings(_), .TranslationSettings(_)):
return true
case (.SearchSettings(_), .SearchSettings(_)):
return true
case (.Thread(let lhs_threadModel), .Thread(thread: let rhs_threadModel)):
return lhs_threadModel.event.id == rhs_threadModel.event.id
case (.Reposts(let lhs_reposts), .Reposts(let rhs_reposts)):
return lhs_reposts.target == rhs_reposts.target
case (.Reactions(let lhs_reactions), .Reactions(let rhs_reactions)):
return lhs_reactions.target == rhs_reactions.target
case (.Zaps(let lhs_target), .Zaps(let rhs_target)):
return lhs_target == rhs_target
case (.Search(let lhs_search), .Search(let rhs_search)):
return lhs_search.sub_id == rhs_search.sub_id && lhs_search.profiles_subid == rhs_search.profiles_subid
case (.EULA, .EULA):
return true
case (.Login, .Login):
return true
case (.CreateAccount, .CreateAccount):
return true
case (.SaveKeys(let lhs_account), .SaveKeys(let rhs_account)):
return lhs_account.pubkey == rhs_account.pubkey
case (.Wallet(_), .Wallet(_)):
return true
case (.WalletScanner(_), .WalletScanner(_)):
return true
case (.FollowersYouKnow(_, _), .FollowersYouKnow(_, _)):
return true
default:
return false
}
}
func hash(into hasher: inout Hasher) {
switch self {
case .ProfileByKey(let pubkey):
hasher.combine("profilebykey")
hasher.combine(pubkey)
case .Profile(let profile, _):
hasher.combine("profile")
hasher.combine(profile.pubkey)
case .Followers(_):
hasher.combine("followers")
case .Relay(let relay, _):
hasher.combine("relay")
hasher.combine(relay)
case .RelayDetail(let relay, _):
hasher.combine("relayDetail")
hasher.combine(relay)
case .Following(_):
hasher.combine("following")
case .MuteList(let users):
hasher.combine("muteList")
hasher.combine(users)
case .RelayConfig:
hasher.combine("relayConfig")
case .Bookmarks:
hasher.combine("bookmarks")
case .Config:
hasher.combine("config")
case .EditMetadata:
hasher.combine("editMetadata")
case .DMChat(let dms):
hasher.combine("dms")
hasher.combine(dms.our_pubkey)
case .UserRelays(let relays):
hasher.combine("userRelays")
hasher.combine(relays)
case .KeySettings(let keypair):
hasher.combine("keySettings")
hasher.combine(keypair.pubkey)
case .AppearanceSettings(_):
hasher.combine("appearanceSettings")
case .NotificationSettings(_):
hasher.combine("notificationSettings")
case .ZapSettings(_):
hasher.combine("zapSettings")
case .TranslationSettings(_):
hasher.combine("translationSettings")
case .SearchSettings(_):
hasher.combine("searchSettings")
case .Thread(let threadModel):
hasher.combine("thread")
hasher.combine(threadModel.event.id)
case .Reposts(let reposts):
hasher.combine("reposts")
hasher.combine(reposts.target)
case .Zaps(let target):
hasher.combine("zaps")
hasher.combine(target.id)
hasher.combine(target.pubkey)
case .Reactions(let reactions):
hasher.combine("reactions")
hasher.combine(reactions.target)
case .Search(let search):
hasher.combine("search")
hasher.combine(search.sub_id)
hasher.combine(search.profiles_subid)
case .EULA:
hasher.combine("eula")
case .Login:
hasher.combine("login")
case .CreateAccount:
hasher.combine("createAccount")
case .SaveKeys(let account):
hasher.combine("saveKeys")
hasher.combine(account.pubkey)
case .Wallet(_):
hasher.combine("wallet")
case .WalletScanner(_):
hasher.combine("walletScanner")
case .FollowersYouKnow(let friendedFollowers, let followers):
hasher.combine("followersYouKnow")
hasher.combine(friendedFollowers)
hasher.combine(followers.sub_id)
}
}
}
class NavigationCoordinator: ObservableObject {
@Published var path = [Route]()
func push(route: Route) {
path.append(route)
}
func popToRoot() {
path = []
}
}

View File

@@ -25,7 +25,7 @@ struct EventDetailBar: View {
var body: some View {
HStack {
if bar.boosts > 0 {
NavigationLink(value: Route.Reposts(reposts: RepostsModel(state: state, target: target))) {
NavigationLink(destination: RepostsView(damus_state: state, model: RepostsModel(state: state, target: target))) {
let noun = Text(verbatim: repostsCountString(bar.boosts)).foregroundColor(.gray)
Text("\(Text(verbatim: bar.boosts.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reposts. In source English, the first variable is the number of reposts, and the second variable is 'Repost' or 'Reposts'.")
}
@@ -33,7 +33,7 @@ struct EventDetailBar: View {
}
if bar.likes > 0 && !state.settings.onlyzaps_mode {
NavigationLink(value: Route.Reactions(reactions: ReactionsModel(state: state, target: target))) {
NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) {
let noun = Text(verbatim: reactionsCountString(bar.likes)).foregroundColor(.gray)
Text("\(Text(verbatim: bar.likes.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.")
}
@@ -41,7 +41,8 @@ struct EventDetailBar: View {
}
if bar.zaps > 0 {
NavigationLink(value: Route.Zaps(target: .note(id: target, author: target_pk))) {
let dst = ZapsView(state: state, target: .note(id: target, author: target_pk))
NavigationLink(destination: dst) {
let noun = Text(verbatim: zapsCountString(bar.zaps)).foregroundColor(.gray)
Text("\(Text(verbatim: bar.zaps.formatted()).font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many zap payments there are on a post. In source English, the first variable is the number of zap payments, and the second variable is 'Zap' or 'Zaps'.")
}

View File

@@ -38,6 +38,7 @@ struct BookmarksView: View {
} else {
ScrollView {
InnerTimelineView(events: EventHolder(events: bookmarks, incoming: []), damus: state, filter: noneFilter)
}
}
}

View File

@@ -18,16 +18,16 @@ struct ConfigView: View {
@State var delete_account_warning: Bool = false
@State var confirm_delete_account: Bool = false
@State var delete_text: String = ""
@ObservedObject var settings: UserSettingsStore
private let DELETE_KEYWORD = "DELETE"
init(state: DamusState) {
self.state = state
_settings = ObservedObject(initialValue: state.settings)
}
func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
@@ -36,30 +36,31 @@ struct ConfigView: View {
ZStack(alignment: .leading) {
Form {
Section {
NavigationLink(value: Route.KeySettings(keypair: state.keypair)) {
NavigationLink(destination: KeySettingsView(keypair: state.keypair)) {
IconLabel(NSLocalizedString("Keys", comment: "Settings section for managing keys"), img_name: "key", color: .purple)
}
NavigationLink(value: Route.AppearanceSettings(settings: settings)) {
NavigationLink(destination: AppearanceSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "eye", color: .red)
}
NavigationLink(value: Route.SearchSettings(settings: settings)) {
NavigationLink(destination: SearchSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Search/Universe", comment: "Section header for search/universe settings"), img_name: "magnifyingglass", color: .red)
}
NavigationLink(value: Route.NotificationSettings(settings: settings)) {
NavigationLink(destination: NotificationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Notifications", comment: "Section header for Damus notifications"), img_name: "notification-bell-on", color: .blue)
}
NavigationLink(value: Route.ZapSettings(settings: settings)) {
NavigationLink(destination: ZapSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Zaps", comment: "Section header for zap settings"), img_name: "zap.fill", color: .orange)
}
NavigationLink(value: Route.TranslationSettings(settings: settings)) {
NavigationLink(destination: TranslationSettingsView(settings: settings)) {
IconLabel(NSLocalizedString("Translation", comment: "Section header for text and appearance settings"), img_name: "globe", color: .green)
}
}
Section(NSLocalizedString("Sign Out", comment: "Section title for signing out")) {
Button(action: {
@@ -115,11 +116,11 @@ struct ConfigView: View {
guard let full_kp = state.keypair.to_full() else {
return
}
guard delete_text == DELETE_KEYWORD else {
return
}
let ev = created_deleted_account_profile(keypair: full_kp)
state.postbox.send(ev)
notify(.logout, ())
@@ -163,7 +164,7 @@ func handle_string_amount(new_value: String) -> Int? {
guard let amt = NumberFormatter().number(from: filtered) as? Int else {
return nil
}
return amt
}

View File

@@ -10,7 +10,8 @@ import SwiftUI
struct CreateAccountView: View {
@StateObject var account: CreateAccountModel = CreateAccountModel()
@StateObject var profileUploadViewModel = ProfileUploadingViewModel()
var nav: NavigationCoordinator
@State var is_done: Bool = false
func SignupForm<FormContent: View>(@ViewBuilder content: () -> FormContent) -> some View {
return VStack(alignment: .leading, spacing: 10.0, content: content)
@@ -24,6 +25,10 @@ struct CreateAccountView: View {
var body: some View {
ZStack(alignment: .top) {
NavigationLink(destination: SaveKeysView(account: account), isActive: $is_done) {
EmptyView()
}
VStack {
VStack(alignment: .center) {
ProfilePictureSelector(pubkey: account.pubkey, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:))
@@ -58,7 +63,7 @@ struct CreateAccountView: View {
.padding(.top, 10)
Button(action: {
nav.push(route: Route.SaveKeys(account: account))
self.is_done = true
}) {
HStack {
Text("Create account now", comment: "Button to create account.")
@@ -130,7 +135,7 @@ extension View {
struct CreateAccountView_Previews: PreviewProvider {
static var previews: some View {
let model = CreateAccountModel(real: "", nick: "jb55", about: "")
return CreateAccountView(account: model, nav: .init())
return CreateAccountView(account: model)
}
}

View File

@@ -61,7 +61,8 @@ struct DMChatView: View, KeyboardReadable {
var Header: some View {
let profile = damus_state.profiles.lookup(id: pubkey)
return NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
let profile_page = ProfileView(damus_state: damus_state, pubkey: pubkey)
return NavigationLink(destination: profile_page) {
HStack {
ProfilePicView(pubkey: pubkey, size: 24, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)

View File

@@ -18,9 +18,13 @@ struct DirectMessagesView: View {
@State var dm_type: DMType = .friend
@ObservedObject var model: DirectMessagesModel
@ObservedObject var settings: UserSettingsStore
func MainContent(requests: Bool) -> some View {
ScrollView {
let chat = DMChatView(damus_state: damus_state, dms: model.active_model)
NavigationLink(destination: chat, isActive: $model.open_dm) {
EmptyView()
}
LazyVStack(spacing: 0) {
if model.dms.isEmpty, !model.loading {
EmptyTimelineView()
@@ -50,8 +54,7 @@ struct DirectMessagesView: View {
if ok, let ev = model.events.last {
EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options)
.onTapGesture {
self.model.set_active_dm_model(model)
damus_state.nav.push(route: Route.DMChat(dms: self.model.active_model))
self.model.open_dm_by_model(model)
}
Divider()

View File

@@ -56,13 +56,18 @@ By using our Application, you signify your acceptance of this EULA. If you do no
"""
struct EULAView: View {
@State private var login = false
@State var accepted = false
@Environment(\.colorScheme) var colorScheme
@Environment(\.dismiss) var dismiss
var nav: NavigationCoordinator
var body: some View {
ZStack {
ScrollView {
NavigationLink(destination: LoginView(accepted: $accepted), isActive: $login) {
EmptyView()
}
Text(Markdown.parse(content: eula))
.padding()
}
@@ -91,7 +96,8 @@ struct EULAView: View {
}
Button(action: {
nav.push(route: Route.Login)
accepted = true
login.toggle()
}) {
HStack {
Text("Accept", comment: "Button to accept the end user license agreement before being allowed into the app.")
@@ -120,6 +126,6 @@ struct EULAView: View {
struct EULAView_Previews: PreviewProvider {
static var previews: some View {
EULAView(nav: .init())
EULAView()
}
}

View File

@@ -72,7 +72,8 @@ struct BuilderEventView: View {
if let event {
let ev = event.get_inner_event(cache: damus.events) ?? event
let thread = ThreadModel(event: ev, damus_state: damus)
NavigationLink(value: Route.Thread(thread: thread)) {
let dest = ThreadView(state: damus, thread: thread)
NavigationLink(destination: dest) {
EventView(damus: damus, event: event, options: .embedded)
.padding([.top, .bottom], 8)
}.buttonStyle(.plain)

View File

@@ -37,7 +37,7 @@ struct EventProfile: View {
var body: some View {
HStack(alignment: .center) {
VStack {
NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
NavigationLink(destination: ProfileView(damus_state: damus_state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: pfp_size, highlight: .none, profiles: damus_state.profiles, disable_animation: disable_animation)
}
}

View File

@@ -12,13 +12,16 @@ struct FollowUserView: View {
let damus_state: DamusState
static let markdown = Markdown()
@State var navigating: Bool = false
var body: some View {
let dest = ProfileView(damus_state: damus_state, pubkey: target.pubkey)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
HStack {
UserViewRow(damus_state: damus_state, pubkey: target.pubkey)
.onTapGesture {
damus_state.nav.push(route: Route.ProfileByKey(pubkey: target.pubkey))
}
FollowButtonView(target: target, follows_you: false, follow_state: damus_state.contacts.follow_state(target.pubkey))
}
@@ -29,7 +32,8 @@ struct FollowUserView: View {
struct FollowersYouKnowView: View {
let damus_state: DamusState
let friended_followers: [String]
@ObservedObject var followers: FollowersModel
@EnvironmentObject var followers: FollowersModel
var body: some View {
ScrollView {
@@ -46,7 +50,8 @@ struct FollowersYouKnowView: View {
struct FollowersView: View {
let damus_state: DamusState
@ObservedObject var followers: FollowersModel
@EnvironmentObject var followers: FollowersModel
var body: some View {
ScrollView {
@@ -71,7 +76,6 @@ struct FollowingView: View {
let damus_state: DamusState
let following: FollowingModel
var body: some View {
ScrollView {

View File

@@ -33,11 +33,13 @@ enum ParsedKey {
}
struct LoginView: View {
@State private var create_account = false
@State var key: String = ""
@State var is_pubkey: Bool = false
@State var error: String? = nil
@State private var credential_handler = CredentialHandler()
var nav: NavigationCoordinator
@Binding var accepted: Bool
func get_error(parsed_key: ParsedKey?) -> String? {
if self.error != nil {
@@ -53,6 +55,12 @@ struct LoginView: View {
var body: some View {
ZStack(alignment: .top) {
if accepted {
NavigationLink(destination: CreateAccountView(), isActive: $create_account) {
EmptyView()
}
}
VStack {
SignInHeader()
.padding(.top, 100)
@@ -99,7 +107,7 @@ struct LoginView: View {
.padding(.top, 10)
}
CreateAccountPrompt(nav: nav)
CreateAccountPrompt(create_account: $create_account)
.padding(.top, 10)
Spacer()
@@ -329,14 +337,14 @@ struct SignInEntry: View {
}
struct CreateAccountPrompt: View {
var nav: NavigationCoordinator
@Binding var create_account: Bool
var body: some View {
HStack {
Text("New to Nostr?", comment: "Ask the user if they are new to Nostr")
.foregroundColor(Color("DamusMediumGrey"))
Button(NSLocalizedString("Create account", comment: "Button to navigate to create account view.")) {
nav.push(route: Route.CreateAccount)
create_account.toggle()
}
Spacer()
@@ -350,8 +358,8 @@ struct LoginView_Previews: PreviewProvider {
let pubkey = "npub18m76awca3y37hkvuneavuw6pjj4525fw90necxmadrvjg0sdy6qsngq955"
let bech32_pubkey = "KeyInput"
Group {
LoginView(key: pubkey, nav: .init())
LoginView(key: bech32_pubkey, nav: .init())
LoginView(key: pubkey, accepted: .constant(true))
LoginView(key: bech32_pubkey, accepted: .constant(true))
}
}
}

View File

@@ -192,7 +192,7 @@ struct EventGroupView: View {
let state: DamusState
let event: NostrEvent?
let group: EventGroupType
func GroupDescription(_ pubkeys: [String]) -> some View {
Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event, pubkeys: pubkeys))")
}
@@ -240,7 +240,8 @@ struct EventGroupView: View {
if let event {
let thread = ThreadModel(event: event, damus_state: state)
NavigationLink(value: Route.Thread(thread: thread)) {
let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest) {
VStack(alignment: .leading) {
GroupDescription(unique_pubkeys)
EventBody(damus_state: state, event: event, size: .normal, options: [.truncate_content])

View File

@@ -59,7 +59,7 @@ struct NotificationItemView: View {
EventGroupView(state: state, event: ev, group: .reaction(evgrp))
case .reply(let ev):
NavigationLink(value: Route.Thread(thread: ThreadModel(event: ev, damus_state: state))) {
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev, options: options)
}
.buttonStyle(.plain)

View File

@@ -10,13 +10,20 @@ import SwiftUI
struct ProfilePicturesView: View {
let state: DamusState
let pubkeys: [String]
@State var nav_target: String? = nil
@State var navigating: Bool = false
var body: some View {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: nav_target ?? ""), isActive: $navigating) {
EmptyView()
}
HStack {
ForEach(pubkeys.prefix(8), id: \.self) { pubkey in
ProfilePicView(pubkey: pubkey, size: 32.0, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
.onTapGesture {
state.nav.push(route: Route.ProfileByKey(pubkey: pubkey))
nav_target = pubkey
navigating = true
}
}
}

View File

@@ -19,15 +19,10 @@ class TagModel: ObservableObject {
var diff = 0
}
enum PostTarget {
case none
case user(String)
}
enum PostAction {
case replying_to(NostrEvent)
case quoting(NostrEvent)
case posting(PostTarget)
case posting
var ev: NostrEvent? {
switch self {
@@ -84,27 +79,13 @@ struct PostView: View {
post.enumerateAttributes(in: NSRange(location: 0, length: post.length), options: []) { attributes, range, stop in
if let link = attributes[.link] as? String {
let normalized_link: String
if link.hasPrefix("damus:nostr:") {
// Replace damus:nostr: URI prefix with nostr: since the former is for internal navigation and not meant to be posted.
normalized_link = String(link.dropFirst(6))
} else {
normalized_link = link
}
// Add zero-width space in case text preceding the mention is not a whitespace.
// In the case where the character preceding the mention is a whitespace, the added zero-width space will be stripped out.
post.replaceCharacters(in: range, with: "\u{200B}\(normalized_link)\u{200B}")
post.replaceCharacters(in: range, with: link)
}
}
var content = self.post.string
// If two zero-width spaces are next to each other, normalize it to just one zero-width space.
.replacingOccurrences(of: "\u{200B}\u{200B}", with: "\u{200B}")
// If zero-width space is next to an actual whitespace, remove the zero-width space.
.replacingOccurrences(of: " \u{200B}", with: " ")
.replacingOccurrences(of: "\u{200B} ", with: " ")
.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
.replacingOccurrences(of: "\u{200B}", with: "") // these characters are added when adding mentions.
let imagesString = uploadedMedias.map { $0.uploadedURL.absoluteString }.joined(separator: " ")
@@ -130,14 +111,6 @@ struct PostView: View {
var is_post_empty: Bool {
return post.string.allSatisfy { $0.isWhitespace } && uploadedMedias.isEmpty
}
var uploading_disabled: Bool {
return image_upload.progress != nil
}
var posting_disabled: Bool {
return is_post_empty || uploading_disabled
}
var ImageButton: some View {
Button(action: {
@@ -162,7 +135,7 @@ struct PostView: View {
ImageButton
CameraButton
}
.disabled(uploading_disabled)
.disabled(image_upload.progress != nil)
}
var PostButton: some View {
@@ -173,29 +146,18 @@ struct PostView: View {
self.send_post()
}
}
.disabled(posting_disabled)
.disabled(is_post_empty)
.font(.system(size: 14, weight: .bold))
.frame(width: 80, height: 30)
.foregroundColor(.white)
.background(LINEAR_GRADIENT)
.opacity(posting_disabled ? 0.5 : 1.0)
.opacity(is_post_empty ? 0.5 : 1.0)
.clipShape(Capsule())
}
func isEmpty() -> Bool {
return self.uploadedMedias.count == 0 &&
self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines) ==
initialString().mutableString.trimmingCharacters(in: .whitespacesAndNewlines)
}
func initialString() -> NSMutableAttributedString {
guard case .posting(let target) = action,
case .user(let pubkey) = target else {
return .init(string: "")
}
let profile = damus_state.profiles.lookup(id: pubkey)
return user_tag_attr_string(profile: profile, pubkey: pubkey)
var isEmpty: Bool {
self.uploadedMedias.count == 0 &&
self.post.mutableString.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
func clear_draft() {
@@ -210,17 +172,15 @@ struct PostView: View {
}
func load_draft() -> Bool {
func load_draft() {
guard let draft = load_draft_for_post(drafts: self.damus_state.drafts, action: self.action) else {
self.post = NSMutableAttributedString("")
self.uploadedMedias = []
return false
return
}
self.uploadedMedias = draft.media
self.post = draft.content
return true
}
func post_changed(post: NSMutableAttributedString, media: [UploadedMedia]) {
@@ -361,11 +321,6 @@ struct PostView: View {
.padding(.horizontal)
}
func fill_target_content(target: PostTarget) {
self.post = initialString()
self.tagModel.diff = post.string.count
}
var body: some View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
@@ -435,7 +390,7 @@ struct PostView: View {
}
}
.onAppear() {
let loaded_draft = load_draft()
load_draft()
switch action {
case .replying_to(let replying_to):
@@ -444,10 +399,8 @@ struct PostView: View {
case .quoting(let quoting):
references = gather_quote_ids(our_pubkey: damus_state.pubkey, from: quoting)
originalReferences = references
case .posting(let target):
guard !loaded_draft else { break }
fill_target_content(target: target)
case .posting:
break
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@@ -455,7 +408,7 @@ struct PostView: View {
}
}
.onDisappear {
if isEmpty() {
if isEmpty {
clear_draft()
}
}
@@ -495,7 +448,7 @@ func get_searching_string(_ word: String?) -> String? {
struct PostView_Previews: PreviewProvider {
static var previews: some View {
PostView(action: .posting(.none), damus_state: test_damus_state())
PostView(action: .posting, damus_state: test_damus_state())
}
}

View File

@@ -8,6 +8,7 @@
import SwiftUI
struct SearchedUser: Identifiable {
let petname: String?
let profile: Profile?
let pubkey: String
@@ -27,14 +28,18 @@ struct UserSearch: View {
@EnvironmentObject var tagModel: TagModel
var users: [SearchedUser] {
return search_profiles(profiles: damus_state.profiles, search: search)
guard let contacts = damus_state.contacts.event else {
return search_profiles(profiles: damus_state.profiles, search: search)
}
return search_users_for_autocomplete(profiles: damus_state.profiles, tags: contacts.tags, search: search)
}
func on_user_tapped(user: SearchedUser) {
guard let pk = bech32_pubkey(user.pubkey) else {
return
}
let tagAttributedString = user_tag_attr_string(profile: user.profile, pubkey: pk)
let tagAttributedString = createUserTag(for: user, with: pk)
appendUserTag(withTag: tagAttributedString)
}
@@ -52,6 +57,26 @@ struct UserSearch: View {
newCursorIndex = wordRange.location + tagAttributedString.string.count
}
private func createUserTag(for user: SearchedUser, with pk: String) -> NSMutableAttributedString {
let name = Profile.displayName(profile: user.profile, pubkey: pk).username.truncate(maxLength: 50)
let tagString = "@\(name)\u{200B} "
let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "nostr:\(pk)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
return tagAttributedString
}
private func appendUserTag(_ tagAttributedString: NSMutableAttributedString) {
let mutableString = NSMutableAttributedString()
mutableString.append(post)
mutableString.append(tagAttributedString)
post = mutableString
}
var body: some View {
VStack(spacing: 0) {
Divider()
@@ -94,17 +119,52 @@ struct UserSearch_Previews: PreviewProvider {
}
}
func user_tag_attr_string(profile: Profile?, pubkey: String) -> NSMutableAttributedString {
let display_name = Profile.displayName(profile: profile, pubkey: pubkey)
let name = display_name.username.truncate(maxLength: 50)
let tagString = "@\(name)\u{200B} "
let tagAttributedString = NSMutableAttributedString(string: tagString,
attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18.0),
NSAttributedString.Key.link: "nostr:\(pubkey)"])
tagAttributedString.removeAttribute(.link, range: NSRange(location: tagAttributedString.length - 2, length: 2))
tagAttributedString.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.label], range: NSRange(location: tagAttributedString.length - 2, length: 2))
func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search _search: String) -> [SearchedUser] {
var seen_user = Set<String>()
let search = _search.lowercased()
return tagAttributedString
var matches = tags.reduce(into: Array<SearchedUser>()) { arr, tag in
guard tag.count >= 2 && tag[0] == "p" else {
return
}
let pubkey = tag[1]
guard !seen_user.contains(pubkey) else {
return
}
seen_user.insert(pubkey)
var petname: String? = nil
if tag.count >= 4 {
petname = tag[3]
}
let profile = profiles.lookup(id: pubkey)
guard ((petname?.lowercased().hasPrefix(search) ?? false) ||
(profile?.name?.lowercased().hasPrefix(search) ?? false) ||
(profile?.display_name?.lowercased().hasPrefix(search) ?? false)) else {
return
}
let searched_user = SearchedUser(petname: petname, profile: profile, pubkey: pubkey)
arr.append(searched_user)
}
// search profile cache as well
for tup in profiles.enumerated() {
let pk = tup.element.key
let prof = tup.element.value.profile
guard !seen_user.contains(pk) else {
continue
}
if let match = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: search) {
matches.append(match)
}
}
return matches
}

View File

@@ -28,7 +28,7 @@ struct MaybeAnonPfpView: View {
.font(.largeTitle)
.frame(width: size, height: size)
} else {
NavigationLink(value: Route.ProfileByKey(pubkey: pubkey)) {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) {
ProfilePicView(pubkey: pubkey, size: size, highlight: .none, profiles: state.profiles, disable_animation: state.settings.disable_animation)
}
}

View File

@@ -162,8 +162,7 @@ func get_profile_url(picture: String?, pubkey: String, profiles: Profiles) -> UR
}
func make_preview_profiles(_ pubkey: String) -> Profiles {
let user_search_cache = UserSearchCache()
let profiles = Profiles(user_search_cache: user_search_cache)
let profiles = Profiles()
let picture = "http://cdn.jb55.com/img/red-me.jpg"
let profile = Profile(name: "jb55", display_name: "William Casarin", about: "It's me", picture: picture, banner: "", website: "https://jb55.com", lud06: nil, lud16: nil, nip05: "jb55.com", damus_donation: nil)
let ts_profile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)

View File

@@ -73,11 +73,11 @@ func followedByString(_ friend_intersection: [String], profiles: Profiles, local
struct EditButton: View {
let damus_state: DamusState
@Environment(\.colorScheme) var colorScheme
var body: some View {
NavigationLink(value: Route.EditMetadata) {
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
Text("Edit", comment: "Button to edit user's profile.")
.frame(height: 30)
.padding(.horizontal,25)
@@ -92,11 +92,11 @@ struct EditButton: View {
.lineLimit(1)
}
}
func fillColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
func borderColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
@@ -104,11 +104,11 @@ struct EditButton: View {
struct VisualEffectView: UIViewRepresentable {
var effect: UIVisualEffect?
func makeUIView(context: UIViewRepresentableContext<Self>) -> UIVisualEffectView {
UIVisualEffectView()
}
func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext<Self>) {
uiView.effect = effect
}
@@ -120,52 +120,52 @@ struct ProfileView: View {
let bannerHeight: CGFloat = 150.0
static let markdown = Markdown()
@State var is_zoomed: Bool = false
@State var show_share_sheet: Bool = false
@State var show_qr_code: Bool = false
@State var action_sheet_presented: Bool = false
@State var filter_state : FilterState = .posts
@State var yOffset: CGFloat = 0
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
@StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: profile)
self._followers = StateObject(wrappedValue: followers)
}
init(damus_state: DamusState, pubkey: String) {
self.damus_state = damus_state
self._profile = StateObject(wrappedValue: ProfileModel(pubkey: pubkey, damus: damus_state))
self._followers = StateObject(wrappedValue: FollowersModel(damus_state: damus_state, target: pubkey))
}
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) var colorScheme
@Environment(\.presentationMode) var presentationMode
func imageBorderColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
}
func bannerBlurViewOpacity() -> Double {
let progress = -(yOffset + navbarHeight) / 100
return Double(-yOffset > navbarHeight ? progress : 0)
}
var bannerSection: some View {
GeometryReader { proxy -> AnyView in
let minY = proxy.frame(in: .global).minY
DispatchQueue.main.async {
self.yOffset = minY
}
return AnyView(
VStack(spacing: 0) {
ZStack {
@@ -173,10 +173,10 @@ struct ProfileView: View {
.aspectRatio(contentMode: .fill)
.frame(width: proxy.size.width, height: minY > 0 ? bannerHeight + minY : bannerHeight)
.clipped()
VisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterial)).opacity(bannerBlurViewOpacity())
}
Divider().opacity(bannerBlurViewOpacity())
}
.frame(height: minY > 0 ? bannerHeight + minY : nil)
@@ -187,11 +187,11 @@ struct ProfileView: View {
.frame(height: bannerHeight)
.allowsHitTesting(false)
}
var navbarHeight: CGFloat {
return 100.0 - (Theme.safeAreaInsets?.top ?? 0)
}
@ViewBuilder
func navImage(img: String) -> some View {
Image(img)
@@ -199,7 +199,7 @@ struct ProfileView: View {
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
var navBackButton: some View {
Button {
presentationMode.wrappedValue.dismiss()
@@ -207,7 +207,7 @@ struct ProfileView: View {
navImage(img: "chevron-left")
}
}
var navActionSheetButton: some View {
Button(action: {
action_sheet_presented = true
@@ -218,7 +218,7 @@ struct ProfileView: View {
Button(NSLocalizedString("Share", comment: "Button to share the link to a profile.")) {
show_share_sheet = true
}
Button(NSLocalizedString("QR Code", comment: "Button to view profile's qr code.")) {
show_qr_code = true
}
@@ -238,7 +238,7 @@ struct ProfileView: View {
else {
return
}
guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: profile.pubkey) else {
return
}
@@ -254,7 +254,7 @@ struct ProfileView: View {
}
}
}
var customNavbar: some View {
HStack {
navBackButton
@@ -265,7 +265,7 @@ struct ProfileView: View {
.padding(.horizontal)
.accentColor(DamusColors.white)
}
func lnButton(lnurl: String, profile: Profile) -> some View {
let button_img = profile.reactions == false ? "zap.fill" : "zap"
return Button(action: {
@@ -278,7 +278,7 @@ struct ProfileView: View {
if profile.reactions == false {
Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.")
}
if let addr = profile.lud16 {
Button {
UIPasteboard.general.string = addr
@@ -293,30 +293,31 @@ struct ProfileView: View {
}
}
}
}
.cornerRadius(24)
}
var dmButton: some View {
let dm_model = damus_state.dms.lookup_or_create(profile.pubkey)
return NavigationLink(value: Route.DMChat(dms: dm_model)) {
let dmview = DMChatView(damus_state: damus_state, dms: dm_model)
return NavigationLink(destination: dmview) {
Image("messages")
.profile_button_style(scheme: colorScheme)
}
}
func actionSection(profile_data: Profile?) -> some View {
return Group {
if let profile = profile_data {
if let lnurl = profile.lnurl, lnurl != "" {
lnButton(lnurl: lnurl, profile: profile)
}
}
dmButton
if profile.pubkey != damus_state.pubkey {
FollowButtonView(
target: profile.get_follow_target(),
@@ -324,26 +325,26 @@ struct ProfileView: View {
follow_state: damus_state.contacts.follow_state(profile.pubkey)
)
} else if damus_state.keypair.privkey != nil {
NavigationLink(value: Route.EditMetadata) {
NavigationLink(destination: EditMetadataView(damus_state: damus_state)) {
EditButton(damus_state: damus_state)
}
}
}
}
func pfpOffset() -> CGFloat {
let progress = -yOffset / navbarHeight
let offset = (pfp_size / 4.0) * (progress < 1.0 ? progress : 1)
return offset > 0 ? offset : 0
}
func pfpScale() -> CGFloat {
let progress = -yOffset / navbarHeight
let scale = 1.0 - (0.5 * (progress < 1.0 ? progress : 1))
return scale < 1 ? scale : 1
}
func nameSection(profile_data: Profile?) -> some View {
return Group {
HStack(alignment: .center) {
@@ -357,17 +358,17 @@ struct ProfileView: View {
.fullScreenCover(isPresented: $is_zoomed) {
ProfilePicImageView(pubkey: profile.pubkey, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
}
Spacer()
actionSection(profile_data: profile_data)
}
let follows_you = profile.pubkey != damus_state.pubkey && profile.follows(pubkey: damus_state.pubkey)
ProfileNameView(pubkey: profile.pubkey, profile: profile_data, follows_you: follows_you, damus: damus_state)
}
}
var followersCount: some View {
HStack {
if followers.count == nil {
@@ -384,26 +385,26 @@ struct ProfileView: View {
}
}
}
var aboutSection: some View {
VStack(alignment: .leading, spacing: 8.0) {
let profile_data = damus_state.profiles.lookup(id: profile.pubkey)
nameSection(profile_data: profile_data)
if let about = profile_data?.about {
AboutView(state: damus_state, about: about)
}
if let url = profile_data?.website_url {
WebsiteLink(url: url)
}
HStack {
if let contact = profile.contacts {
let contacts = contact.referenced_pubkeys.map { $0.ref_id }
let following_model = FollowingModel(damus_state: damus_state, contacts: contacts)
NavigationLink(value: Route.Following(following: following_model)) {
NavigationLink(destination: FollowingView(damus_state: damus_state, following: following_model)) {
HStack {
let noun_text = Text(verbatim: "\(followingCountString(profile.following))").font(.subheadline).foregroundColor(.gray)
Text("\(Text(verbatim: profile.following.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many profiles a user is following. In source English, the first variable is the number of profiles being followed, and the second variable is 'Following'.")
@@ -411,9 +412,10 @@ struct ProfileView: View {
}
.buttonStyle(PlainButtonStyle())
}
let fview = FollowersView(damus_state: damus_state)
.environmentObject(followers)
if followers.contacts != nil {
NavigationLink(value: Route.Followers(followers: followers)) {
NavigationLink(destination: fview) {
followersCount
}
.buttonStyle(PlainButtonStyle())
@@ -425,18 +427,18 @@ struct ProfileView: View {
followers.subscribe()
}
}
if let relays = profile.relays {
// Only open relay config view if the user is logged in with private key and they are looking at their own profile.
let noun_text = Text(verbatim: relaysCountString(relays.keys.count)).font(.subheadline).foregroundColor(.gray)
let relay_text = Text("\(Text(verbatim: relays.keys.count.formatted()).font(.subheadline.weight(.medium))) \(noun_text)", comment: "Sentence composed of 2 variables to describe how many relay servers a user is connected. In source English, the first variable is the number of relay servers, and the second variable is 'Relay' or 'Relays'.")
if profile.pubkey == damus_state.pubkey && damus_state.is_privkey_user {
NavigationLink(value: Route.RelayConfig) {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
relay_text
}
.buttonStyle(PlainButtonStyle())
} else {
NavigationLink(value: Route.UserRelays(relays: Array(relays.keys).sorted())) {
NavigationLink(destination: UserRelaysView(state: damus_state, relays: Array(relays.keys).sorted())) {
relay_text
}
.buttonStyle(PlainButtonStyle())
@@ -449,7 +451,7 @@ struct ProfileView: View {
if !friended_followers.isEmpty {
Spacer()
NavigationLink(value: Route.FollowersYouKnow(friendedFollowers: friended_followers, followers: followers)) {
NavigationLink(destination: FollowersYouKnowView(damus_state: damus_state, friended_followers: friended_followers)) {
HStack {
CondensedProfilePicturesView(state: damus_state, pubkeys: friended_followers, maxPictures: 3)
Text(followedByString(friended_followers, profiles: damus_state.profiles))
@@ -462,71 +464,63 @@ struct ProfileView: View {
}
.padding(.horizontal)
}
var body: some View {
ZStack {
ScrollView(.vertical) {
VStack(spacing: 0) {
bannerSection
.zIndex(1)
ScrollView(.vertical) {
VStack(spacing: 0) {
bannerSection
.zIndex(1)
VStack() {
aboutSection
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Notes", comment: "Label for filter for seeing only your notes (instead of notes and replies).").tag(FilterState.posts)
Text("Notes & Replies", comment: "Label for filter for seeing your notes and replies (instead of only your notes).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
VStack() {
aboutSection
VStack(spacing: 0) {
CustomPicker(selection: $filter_state, content: {
Text("Notes", comment: "Label for filter for seeing only your notes (instead of notes and replies).").tag(FilterState.posts)
Text("Notes & Replies", comment: "Label for filter for seeing your notes and replies (instead of only your notes).").tag(FilterState.posts_and_replies)
})
Divider()
.frame(height: 1)
}
.background(colorScheme == .dark ? Color.black : Color.white)
if filter_state == FilterState.posts {
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts.filter)
}
if filter_state == FilterState.posts_and_replies {
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts_and_replies.filter)
}
if filter_state == FilterState.posts {
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts.filter)
}
.padding(.horizontal, Theme.safeAreaInsets?.left)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
}
}
.ignoresSafeArea()
.navigationTitle("")
.navigationBarHidden(true)
.overlay(customNavbar, alignment: .top)
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onAppear() {
profile.subscribe()
//followers.subscribe()
}
.onDisappear {
profile.unsubscribe()
followers.unsubscribe()
// our profilemodel needs a bit more help
}
.sheet(isPresented: $show_share_sheet) {
if let npub = bech32_pubkey(profile.pubkey) {
if let url = URL(string: "https://damus.io/" + npub) {
ShareSheet(activityItems: [url])
if filter_state == FilterState.posts_and_replies {
InnerTimelineView(events: profile.events, damus: damus_state, filter: FilterState.posts_and_replies.filter)
}
}
.padding(.horizontal, Theme.safeAreaInsets?.left)
.zIndex(-yOffset > navbarHeight ? 0 : 1)
}
.fullScreenCover(isPresented: $show_qr_code) {
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
}
if damus_state.is_privkey_user {
PostButtonContainer(is_left_handed: damus_state.settings.left_handed) {
notify(.compose, PostAction.posting(.user(profile.pubkey)))
}
.ignoresSafeArea()
.navigationTitle("")
.navigationBarHidden(true)
.overlay(customNavbar, alignment: .top)
.onReceive(handle_notify(.switched_timeline)) { _ in
dismiss()
}
.onAppear() {
profile.subscribe()
//followers.subscribe()
}
.onDisappear {
profile.unsubscribe()
followers.unsubscribe()
// our profilemodel needs a bit more help
}
.sheet(isPresented: $show_share_sheet) {
if let npub = bech32_pubkey(profile.pubkey) {
if let url = URL(string: "https://damus.io/" + npub) {
ShareSheet(activityItems: [url])
}
}
}
.fullScreenCover(isPresented: $show_qr_code) {
QRCodeView(damus_state: damus_state, pubkey: profile.pubkey)
}
}
}
@@ -540,7 +534,7 @@ struct ProfileView_Previews: PreviewProvider {
func test_damus_state() -> DamusState {
let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let damus = DamusState.empty
let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil)
let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event)
damus.profiles.add(id: pubkey, profile: tsprof)
@@ -549,15 +543,15 @@ func test_damus_state() -> DamusState {
struct KeyView: View {
let pubkey: String
@Environment(\.colorScheme) var colorScheme
@State private var isCopied = false
func keyColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
private func copyPubkey(_ pubkey: String) {
UIPasteboard.general.string = pubkey
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
@@ -570,10 +564,10 @@ struct KeyView: View {
}
}
}
var body: some View {
let bech32 = bech32_pubkey(pubkey) ?? pubkey
HStack {
Text(verbatim: "\(abbrev_pubkey(bech32, amount: 16))")
.font(.footnote)
@@ -581,7 +575,7 @@ struct KeyView: View {
.padding(5)
.padding([.leading, .trailing], 5)
.background(RoundedRectangle(cornerRadius: 11).foregroundColor(DamusColors.adaptableGrey))
if isCopied != true {
Button {
copyPubkey(bech32)

View File

@@ -47,11 +47,14 @@ struct QRCodeView: View {
@Environment(\.presentationMode) var presentationMode
@State private var selectedTab = 0
@State var scanResult: ProfileScanResult? = nil
@State var showProfileView: Bool = false
@State var profile: Profile? = nil
@State var error: String? = nil
@State private var outerTrimEnd: CGFloat = 0
var animationDuration: Double = 0.5
let generator = UIImpactFeedbackGenerator(style: .light)
@@ -206,6 +209,13 @@ struct QRCodeView: View {
Spacer()
if let scanResult {
let dst = ProfileView(damus_state: damus_state, pubkey: scanResult.pubkey)
NavigationLink(destination: dst, isActive: $showProfileView) {
EmptyView()
}
}
Spacer()
Button(action: {
@@ -261,10 +271,9 @@ struct QRCodeView: View {
func show_profile_after_delay() {
DispatchQueue.main.asyncAfter(deadline: .now() + animationDuration) {
if let scanResult {
damus_state.nav.push(route: Route.ProfileByKey(pubkey: scanResult.pubkey))
}
showProfileView = true
}
}
func cameraAnimate(completion: @escaping () -> Void) {

View File

@@ -10,6 +10,9 @@ import SwiftUI
struct RelayFilterView: View {
let state: DamusState
let timeline: Timeline
//@State var relays: [RelayDescriptor]
//@EnvironmentObject var user_settings: UserSettingsStore
//@State var relays: [RelayDescriptor]
init(state: DamusState, timeline: Timeline) {
self.state = state

View File

@@ -42,7 +42,9 @@ struct RecommendedRelayView: View {
Text(relay).layoutPriority(1)
if let meta = damus.relay_metadata.lookup(relay_id: relay) {
NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta)){
NavigationLink ( destination:
RelayDetailView(state: damus, relay: relay, nip11: meta)
){
EmptyView()
}
.opacity(0.0)

View File

@@ -72,9 +72,7 @@ struct RelayDetailView: View {
if let pubkey = nip11.pubkey {
Section(NSLocalizedString("Admin", comment: "Label to display relay contact user.")) {
NavigationLink(value: Route.ProfileByKey(pubkey: pubkey), label: {
UserViewRow(damus_state: state, pubkey: pubkey)
})
UserViewRow(damus_state: state, pubkey: pubkey)
}
}
if let relay_connection {

View File

@@ -30,9 +30,8 @@ struct RelayView: View {
if let meta = state.relay_metadata.lookup(relay_id: relay) {
Text(relay)
.background(
NavigationLink(value: Route.RelayDetail(relay: relay, metadata: meta), label: {
EmptyView()
}).opacity(0.0).disabled(showActionButtons)
NavigationLink("", destination: RelayDetailView(state: state, relay: relay, nip11: meta)).opacity(0.0)
.disabled(showActionButtons)
)
Spacer()

View File

@@ -14,7 +14,7 @@ struct SignalView: View {
var body: some View {
Group {
if signal.signal != signal.max_signal {
NavigationLink(value: Route.RelayConfig) {
NavigationLink(destination: RelayConfigView(state: state)) {
Text("\(signal.signal)/\(signal.max_signal)", comment: "Fraction of how many of the user's relay servers that are operational.")
.font(.callout)
.foregroundColor(.gray)

View File

@@ -16,8 +16,9 @@ struct RepostedEvent: View {
var body: some View {
VStack(alignment: .leading) {
let prof = damus.profiles.lookup(id: event.pubkey)
let booster_profile = ProfileView(damus_state: damus, pubkey: event.pubkey)
NavigationLink(value: Route.ProfileByKey(pubkey: event.pubkey)) {
NavigationLink(destination: booster_profile) {
Reposted(damus: damus, pubkey: event.pubkey, profile: prof)
.padding(.horizontal)
}

View File

@@ -100,12 +100,14 @@ struct SearchingEventView: View {
.progressViewStyle(.circular)
}
case .found(let ev):
NavigationLink(value: Route.Thread(thread: ThreadModel(event: ev, damus_state: state))) {
NavigationLink(destination: ThreadView(state: state, thread: ThreadModel(event: ev, damus_state: state))) {
EventView(damus: state, event: ev)
}
.buttonStyle(PlainButtonStyle())
case .found_profile(let pk):
NavigationLink(value: Route.ProfileByKey(pubkey: pk)) {
NavigationLink(destination: ProfileView(damus_state: state, pubkey: pk)) {
FollowUserView(target: .pubkey(pk), damus_state: state)
}
.buttonStyle(PlainButtonStyle())

View File

@@ -126,6 +126,9 @@ struct SearchHomeView: View {
struct SearchHomeView_Previews: PreviewProvider {
static var previews: some View {
let state = test_damus_state()
SearchHomeView(damus_state: state, model: SearchHomeModel(damus_state: state))
SearchHomeView(
damus_state: state,
model: SearchHomeModel(damus_state: state)
)
}
}

View File

@@ -44,7 +44,8 @@ struct InnerSearchResults: View {
func HashtagSearch(_ ht: String) -> some View {
let search_model = SearchModel(state: damus_state, search: .filter_hashtag([ht]))
return NavigationLink(value: Route.Search(search: search_model)) {
let dst = SearchView(appstate: damus_state, search: search_model)
return NavigationLink(destination: dst) {
Text("Search hashtag: #\(ht)", comment: "Navigation link to search hashtag.")
}
}
@@ -180,21 +181,33 @@ func make_hashtagable(_ str: String) -> String {
}
func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] {
// Search by hex pubkey.
if search.count == 64 && hex_decode(search) != nil, let profile = profiles.lookup(id: search) {
return [SearchedUser(profile: profile, pubkey: search)]
}
// Search by npub pubkey.
if search.starts(with: "npub"), let bech32_key = decode_bech32_key(search), case Bech32Key.pub(let hex) = bech32_key, let profile = profiles.lookup(id: hex) {
return [SearchedUser(profile: profile, pubkey: hex)]
}
let new = search.lowercased()
let matched_pubkeys = profiles.user_search_cache.search(key: new)
return matched_pubkeys
.map { ($0, profiles.lookup(id: $0)) }
.filter { $1 != nil }
.map { SearchedUser(profile: $1, pubkey: $0) }
return profiles.enumerated().reduce(into: []) { acc, els in
let pk = els.element.key
let prof = els.element.value.profile
if let searched = profile_search_matches(profiles: profiles, profile: prof, pubkey: pk, search: new) {
acc.append(searched)
}
}
}
func profile_search_matches(profiles: Profiles, profile prof: Profile, pubkey pk: String, search new: String) -> SearchedUser? {
let lowname = prof.name.map { $0.lowercased() }
let lownip05 = profiles.is_validated(pk).map { $0.host.lowercased() }
let lowdisp = prof.display_name.map { $0.lowercased() }
let ok = new.count == 1 ?
((lowname?.starts(with: new) ?? false) ||
(lownip05?.starts(with: new) ?? false) ||
(lowdisp?.starts(with: new) ?? false)) : (pk.starts(with: new) || String(new.dropFirst()) == pk
|| lowname?.contains(new) ?? false
|| lownip05?.contains(new) ?? false
|| lowdisp?.contains(new) ?? false)
if ok {
return SearchedUser(petname: nil, profile: prof, pubkey: pk)
}
return nil
}

View File

@@ -17,12 +17,16 @@ func hex_col(r: UInt8, g: UInt8, b: UInt8) -> Color {
struct SetupView: View {
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
@State private var eula = false
var body: some View {
NavigationStack(path: $navigationCoordinator.path) {
NavigationView {
ZStack {
VStack(alignment: .center) {
NavigationLink(destination: EULAView(), isActive: $eula) {
EmptyView()
}
Spacer()
Image("logo-nobg")
@@ -49,7 +53,7 @@ struct SetupView: View {
Spacer()
Button(action: {
navigationCoordinator.push(route: Route.EULA)
eula.toggle()
}) {
HStack {
Text("Let's get started!", comment: "Button to continue to login page.")
@@ -68,9 +72,6 @@ struct SetupView: View {
.ignoresSafeArea(),
alignment: .top
)
.navigationDestination(for: Route.self) { route in
route.view(navigationCordinator: navigationCoordinator, damusState: DamusState.empty)
}
}
.navigationBarTitleDisplayMode(.inline)
.navigationViewStyle(StackNavigationViewStyle())

View File

@@ -11,22 +11,23 @@ struct SideMenuView: View {
let damus_state: DamusState
@Binding var isSidebarVisible: Bool
@State var confirm_logout: Bool = false
@State private var showQRCode = false
@Environment(\.colorScheme) var colorScheme
var sideBarWidth = min(UIScreen.main.bounds.size.width * 0.65, 400.0)
let verticalSpacing: CGFloat = 20
let padding: CGFloat = 30
func fillColor() -> Color {
colorScheme == .light ? DamusColors.white : DamusColors.black
}
func textColor() -> Color {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
var body: some View {
ZStack {
GeometryReader { _ in
@@ -41,20 +42,20 @@ struct SideMenuView: View {
content
}
}
func SidemenuItems(profile_model: ProfileModel, followers: FollowersModel) -> some View {
return VStack(spacing: verticalSpacing) {
NavigationLink(value: Route.Profile(profile: profile_model, followers: followers)) {
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
navLabel(title: NSLocalizedString("Profile", comment: "Sidebar menu label for Profile view."), img: "user")
}
NavigationLink(value: Route.Wallet(wallet: damus_state.wallet)) {
NavigationLink(destination: WalletView(damus_state: damus_state, model: damus_state.wallet)) {
navLabel(title: NSLocalizedString("Wallet", comment: "Sidebar menu label for Wallet view."), img: "wallet")
/*
HStack {
Image("wallet")
.tint(DamusColors.adaptableBlack)
Text(NSLocalizedString("wallet", comment: "Sidebar menu label for Wallet view."))
.font(.title2)
.foregroundColor(textColor())
@@ -62,35 +63,36 @@ struct SideMenuView: View {
.dynamicTypeSize(.xSmall)
}*/
}
NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) {
NavigationLink(destination: MutelistView(damus_state: damus_state, users: get_mutelist_users(damus_state.contacts.mutelist) )) {
navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute")
}
NavigationLink(value: Route.RelayConfig) {
NavigationLink(destination: RelayConfigView(state: damus_state)) {
navLabel(title: NSLocalizedString("Relays", comment: "Sidebar menu label for Relays view."), img: "world-relays")
}
NavigationLink(value: Route.Bookmarks) {
NavigationLink(destination: BookmarksView(state: damus_state)) {
navLabel(title: NSLocalizedString("Bookmarks", comment: "Sidebar menu label for Bookmarks view."), img: "bookmark")
}
NavigationLink(value: Route.Config) {
NavigationLink(destination: ConfigView(state: damus_state)) {
navLabel(title: NSLocalizedString("Settings", comment: "Sidebar menu label for accessing the app settings"), img: "settings")
}
}
}
var MainSidemenu: some View {
VStack(alignment: .leading, spacing: 0) {
let profile = damus_state.profiles.lookup(id: damus_state.pubkey)
let followers = FollowersModel(damus_state: damus_state, target: damus_state.pubkey)
let profile_model = ProfileModel(pubkey: damus_state.pubkey, damus: damus_state)
NavigationLink(value: Route.Profile(profile: profile_model, followers: followers), label: {
NavigationLink(destination: ProfileView(damus_state: damus_state, profile: profile_model, followers: followers)) {
HStack {
ProfilePicView(pubkey: damus_state.pubkey, size: 60, highlight: .none, profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation)
VStack(alignment: .leading) {
if let display_name = profile?.display_name {
Text(display_name)
@@ -107,10 +109,10 @@ struct SideMenuView: View {
}
}
.padding(.bottom, verticalSpacing)
})
}
Divider()
ScrollView {
SidemenuItems(profile_model: profile_model, followers: followers)
.labelStyle(SideMenuLabelStyle())
@@ -118,21 +120,21 @@ struct SideMenuView: View {
}
}
}
var content: some View {
HStack(alignment: .top) {
ZStack(alignment: .top) {
fillColor()
.ignoresSafeArea()
VStack(alignment: .leading, spacing: 0) {
MainSidemenu
.simultaneousGesture(TapGesture().onEnded {
isSidebarVisible = false
})
Divider()
HStack() {
Button(action: {
//ConfigView(state: damus_state)
@@ -148,9 +150,9 @@ struct SideMenuView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall)
})
Spacer()
Button(action: {
showQRCode.toggle()
}, label: {
@@ -184,20 +186,20 @@ struct SideMenuView: View {
Spacer()
}
}
@ViewBuilder
func navLabel(title: String, img: String) -> some View {
Image(img)
.tint(DamusColors.adaptableBlack)
Text(title)
.font(.title2)
.foregroundColor(textColor())
.frame(maxWidth: .infinity, alignment: .leading)
.dynamicTypeSize(.xSmall)
}
struct SideMenuLabelStyle: LabelStyle {
func makeBody(configuration: Configuration) -> some View {
HStack(alignment: .center, spacing: 8) {

View File

@@ -70,21 +70,17 @@ struct TextViewWrapper: UIViewRepresentable {
}
private func processFocusedWordForMention(textView: UITextView) {
var val: (String?, NSRange?) = (nil, nil)
guard let selectedRange = textView.selectedTextRange else { return }
let wordRange = textView.tokenizer.rangeEnclosingPosition(selectedRange.start, with: .word, inDirection: .init(rawValue: UITextLayoutDirection.left.rawValue))
if let wordRange,
let startPosition = textView.position(from: wordRange.start, offset: -1),
let cursorPosition = textView.position(from: selectedRange.start, offset: 0)
{
let word = textView.text(in: textView.textRange(from: startPosition, to: cursorPosition)!)
val = (word, convertToNSRange(startPosition, cursorPosition, textView))
if let selectedRange = textView.selectedTextRange {
var val: (String?, NSRange?)
if let wordRange = textView.tokenizer.rangeEnclosingPosition(selectedRange.start, with: .word, inDirection: .init(rawValue: UITextLayoutDirection.left.rawValue)) {
if let startPosition = textView.position(from: wordRange.start, offset: -1),
let cursorPosition = textView.position(from: selectedRange.start, offset: 0) {
let word = textView.text(in: textView.textRange(from: startPosition, to: cursorPosition)!)
val = (word, convertToNSRange(startPosition, cursorPosition, textView))
}
}
getFocusWordForMention?(val.0, val.1)
}
getFocusWordForMention?(val.0, val.1)
}
private func convertToNSRange( _ startPosition: UITextPosition, _ endPosition: UITextPosition, _ textView: UITextView) -> NSRange? {

View File

@@ -12,6 +12,8 @@ struct InnerTimelineView: View {
@ObservedObject var events: EventHolder
let state: DamusState
let filter: (NostrEvent) -> Bool
@State var nav_target: NostrEvent
@State var navigating: Bool = false
static var count: Int = 0
@@ -21,6 +23,8 @@ struct InnerTimelineView: View {
self.filter = filter
print("rendering InnerTimelineView \(InnerTimelineView.count)")
InnerTimelineView.count += 1
// dummy event to avoid MaybeThreadView
self._nav_target = State(initialValue: test_event)
}
var event_options: EventViewOptions {
@@ -32,6 +36,11 @@ struct InnerTimelineView: View {
}
var body: some View {
let thread = ThreadModel(event: nav_target, damus_state: state)
let dest = ThreadView(state: state, thread: thread)
NavigationLink(destination: dest, isActive: $navigating) {
EmptyView()
}
LazyVStack(spacing: 0) {
let events = self.events.events
if events.isEmpty {
@@ -44,9 +53,8 @@ struct InnerTimelineView: View {
let ind = tup.1
EventView(damus: state, event: ev, options: event_options)
.onTapGesture {
let event = ev.get_inner_event(cache: state.events) ?? ev
let thread = ThreadModel(event: event, damus_state: state)
state.nav.push(route: Route.Thread(thread: thread))
nav_target = ev.get_inner_event(cache: state.events) ?? ev
navigating = true
}
.padding(.top, 7)
.onAppear {

View File

@@ -14,7 +14,6 @@ struct ConnectWalletView: View {
@State var scanning: Bool = false
@State var error: String? = nil
@State var wallet_scan_result: WalletScanResult = .scanning
var nav: NavigationCoordinator
var body: some View {
MainContent
@@ -64,13 +63,17 @@ struct ConnectWalletView: View {
}
var ConnectWallet: some View {
VStack {
VStack {
NavigationLink(destination: WalletScannerView(result: $wallet_scan_result), isActive: $scanning) {
EmptyView()
}
AlbyButton() {
openURL(URL(string:"https://nwc.getalby.com/apps/new?c=Damus")!)
}
BigButton(NSLocalizedString("Attach Wallet", comment: "Text for button to attach Nostr Wallet Connect lightning wallet.")) {
nav.push(route: Route.WalletScanner(result: $wallet_scan_result))
scanning = true
}
if let err = self.error {
@@ -96,6 +99,6 @@ struct ConnectWalletView: View {
struct ConnectWalletView_Previews: PreviewProvider {
static var previews: some View {
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()), nav: .init())
ConnectWalletView(model: WalletModel(settings: UserSettingsStore()))
}
}

View File

@@ -155,9 +155,9 @@ struct WalletView: View {
var body: some View {
switch model.connect_state {
case .new:
ConnectWalletView(model: model, nav: damus_state.nav)
ConnectWalletView(model: model)
case .none:
ConnectWalletView(model: model, nav: damus_state.nav)
ConnectWalletView(model: model)
case .existing(let nwc):
MainWalletView(nwc: nwc)
.onAppear() {

View File

@@ -20,24 +20,6 @@
<string>... %d otras notas...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Seguido por %2$@, %3$@, %4$@ y %1$d persona más</string>
<key>many</key>
<string>Seguido por %2$@, %3$@, %4$@ y %1$d personas más</string>
<key>other</key>
<string>Seguido por %2$@, %3$@, %4$@ y %1$d personas más</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -85,14 +67,14 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más reaccionaron a una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d persona más reaccionaron a una publicación en la que te etiquetaron</string>
<key>many</key>
<string>%2$@ y %1$d personas más reaccionaron a una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d personas más reaccionaron a una publicación en la que te etiquetaron</string>
<key>other</key>
<string>%2$@ y %1$d personas más reaccionaron a una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d personas más reaccionaron a una publicación en la que te etiquetaron</string>
</dict>
</dict>
<key>reacted_your_note_3</key>
<key>reacted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
@@ -103,11 +85,11 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más reaccionaron a tu nota</string>
<string>%2$@ y %1$d persona más reaccionaron a tu publicación</string>
<key>many</key>
<string>%2$@ y %1$d personas más reaccionaron a tu nota</string>
<string>%2$@ y %1$d personas más reaccionaron a tu publicación</string>
<key>other</key>
<string>%2$@ y %1$d personas más reaccionaron a tu nota</string>
<string>%2$@ y %1$d personas más reaccionaron a tu publicación</string>
</dict>
</dict>
<key>reacted_your_profile_3</key>
@@ -193,14 +175,14 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más republicaron una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d persona más republicaron una publicación en la que te etiquetaron</string>
<key>many</key>
<string>%2$@ y %1$d personas más republicaron una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d personas más republicaron una publicación en la que te etiquetaron</string>
<key>other</key>
<string>%2$@ y %1$d personas más republicaron una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d personas más republicaron una publicación en la que te etiquetaron</string>
</dict>
</dict>
<key>reposted_your_note_3</key>
<key>reposted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
@@ -211,11 +193,11 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más republicaron tu nota</string>
<string>%2$@ y %1$d persona más republicaron tu publicación</string>
<key>many</key>
<string>%2$@ y %1$d personas más republicaron tu nota</string>
<string>%2$@ y %1$d personas más republicaron tu publicación</string>
<key>other</key>
<string>%2$@ y %1$d personas más republicaron tu nota</string>
<string>%2$@ y %1$d personas más republicaron tu publicación</string>
</dict>
</dict>
<key>reposted_your_profile_3</key>
@@ -319,11 +301,11 @@
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>Recibiste %2$@ sat de %3$@: &quot;%4$@&quot;</string>
<string>Recibiste %2$@ sat de %3$@: "%4$@"</string>
<key>many</key>
<string>Recibiste %2$@ sats de %3$@: &quot;%4$@&quot;</string>
<string>Recibiste %2$@ sats de %3$@: "%4$@"</string>
<key>other</key>
<string>Recibiste %2$@ sats de %3$@: &quot;%4$@&quot;</string>
<string>Recibiste %2$@ sats de %3$@: "%4$@"</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
@@ -337,14 +319,14 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más zapearon una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d persona más zapearon una publicación en la que te etiquetaron</string>
<key>many</key>
<string>%2$@ y %1$d personas más zapearon una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d personas más zapearon una publicación en la que te etiquetaron</string>
<key>other</key>
<string>%2$@ y %1$d personas más zapearon una nota en la que te etiquetaron</string>
<string>%2$@ y %1$d personas más zapearon una publicación en la que te etiquetaron</string>
</dict>
</dict>
<key>zapped_your_note_3</key>
<key>zapped_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
@@ -355,11 +337,11 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más zapearon tu nota</string>
<string>%2$@ y %1$d persona más zapearon tu publicación</string>
<key>many</key>
<string>%2$@ y %1$d personas más zapearon tu nota</string>
<string>%2$@ y %1$d personas más zapearon tu publicación</string>
<key>other</key>
<string>%2$@ y %1$d personas más zapearon tu nota</string>
<string>%2$@ y %1$d personas más zapearon tu publicación</string>
</dict>
</dict>
<key>zapped_your_profile_3</key>
@@ -373,11 +355,11 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ y %1$d persona más te zapearon</string>
<string>%2$@ y %1$d persona más zapearon tu perfil</string>
<key>many</key>
<string>%2$@ y %1$d personas más te zapearon</string>
<string>%2$@ y %1$d personas más zapearon tu perfil</string>
<key>other</key>
<string>%2$@ y %1$d personas más te zapearon</string>
<string>%2$@ y %1$d personas más zapearon tu perfil</string>
</dict>
</dict>
<key>zaps_count</key>

View File

@@ -20,24 +20,6 @@
<string>... %d otras notas ...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Seguido por %2$@, %3$@, %4$@ y %1$d más</string>
<key>many</key>
<string>Seguido por %2$@, %3$@, %4$@ y %1$d más</string>
<key>other</key>
<string>Seguido por %2$@, %3$@, %4$@ y %1$d más</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -175,11 +157,11 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Respondiendo a %2$@, %3$@ y %1$d más</string>
<string>Respondiendo a %2$@, %3$@ &amp; %1$d más</string>
<key>many</key>
<string>Respondiendo a %2$@, %3$@ y %1$d más</string>
<string>Respondiendo a %2$@, %3$@ &amp; %1$d más</string>
<key>other</key>
<string>Respondiendo a %2$@, %3$@ y %1$d más</string>
<string>Respondiendo a %2$@, %3$@ &amp; %1$d más</string>
</dict>
</dict>
<key>reposted_tagged_in_3</key>

View File

@@ -18,22 +18,6 @@
<string>... %d további bejegyzések ...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>Téged követ %2$@, %3$@, %4$@ &amp; %1$d más</string>
<key>other</key>
<string>Téged követ %2$@, %3$@, %4$@ &amp; %1$d mások</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -82,7 +66,7 @@
<string>%2$@ és %1$d mások reagáltak egy bejegyzésre amiben meg voltál jelölve</string>
</dict>
</dict>
<key>reacted_your_note_3</key>
<key>reacted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REACTED@</string>
@@ -178,7 +162,7 @@
<string>%2$@ és %1$d mások megosztották azt a bejegyzést amiben meg voltál jelölve</string>
</dict>
</dict>
<key>reposted_your_note_3</key>
<key>reposted_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@REPOSTED@</string>
@@ -285,9 +269,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>@</string>
<key>one</key>
<string>%2$@ sat kaptál %3$@: &quot;%4$@&quot;-tól</string>
<string>%2$@ sat kaptál %3$@: "%4$@"-tól</string>
<key>other</key>
<string>%2$@ sats kaptál %3$@: &quot;%4$@&quot;-tól</string>
<string>%2$@ sats kaptál %3$@: "%4$@"-tól</string>
</dict>
</dict>
<key>zapped_tagged_in_3</key>
@@ -306,7 +290,7 @@
<string>%2$@ és %1$d mások Zap-eltek egy bejegyzést amiben meg voltál jelölve</string>
</dict>
</dict>
<key>zapped_your_note_3</key>
<key>zapped_your_post_3</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@ZAPPED@</string>
@@ -333,9 +317,9 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>one</key>
<string>%2$@ és %1$d más Zap-elt téged</string>
<string>%2$@ és %1$d más Zap-elte a profilodat</string>
<key>other</key>
<string>%2$@ és %1$d mások Zap-elt téged</string>
<string>%2$@ és %1$d mások Zap-elték a profilodat</string>
</dict>
</dict>
<key>zaps_count</key>

Binary file not shown.

View File

@@ -16,20 +16,6 @@
<string>... 他%d件の投稿 ...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string> %2$@、%3$@、%4$@他%1$d人にフォローされています</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@@ -16,20 +16,6 @@
<string>... %d 条更多笔记...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>被 %2$@、%3$@、%4$@ 和 %1$d 个其他用户关注</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
@@ -139,7 +125,7 @@
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>正在回复%2$@, %3$@ %1$d 个其他用户</string>
<string>正在回复%2$@, %3$@ &amp; %1$d 个其他用户</string>
</dict>
</dict>
<key>reposted_tagged_in_3</key>

View File

@@ -16,20 +16,6 @@
<string>...還有%d 条筆記...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>被 %2$@、%3$@、%4$@ 和 %1$d 個其他用戶關注</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@@ -16,20 +16,6 @@
<string>...還有%d 条筆記...</string>
</dict>
</dict>
<key>followed_by_three_and_others</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@OTHERS@</string>
<key>OTHERS</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>other</key>
<string>被 %2$@、%3$@、%4$@ 和 %1$d 個其他用戶關注</string>
</dict>
</dict>
<key>followers_count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>

View File

@@ -1,79 +0,0 @@
//
// TrieTests.swift
// damusTests
//
// Created by Terry Yiu on 6/26/23.
//
import XCTest
@testable import damus
final class TrieTests: XCTestCase {
func testFind() throws {
let trie = Trie<String>()
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
keys.forEach {
trie.insert(key: $0, value: $0)
}
let allResults = trie.find(key: "")
XCTAssertEqual(Set(allResults), Set(["foobar", "food", "foo", "somethingelse", "duplicate"]))
let fooResults = trie.find(key: "foo")
XCTAssertEqual(fooResults.first, "foo")
XCTAssertEqual(Set(fooResults), Set(["foobar", "food", "foo"]))
let foodResults = trie.find(key: "food")
XCTAssertEqual(foodResults, ["food"])
let ooResults = trie.find(key: "oo")
XCTAssertEqual(Set(ooResults), Set(["foobar", "food", "foo"]))
let aResults = trie.find(key: "a")
XCTAssertEqual(Set(aResults), Set(["foobar", "duplicate"]))
let notFoundResults = trie.find(key: "notfound")
XCTAssertEqual(notFoundResults, [])
// Sanity check that the root node has children.
XCTAssertTrue(trie.hasChildren)
// Sanity check that the root node has no values.
XCTAssertFalse(trie.hasValues)
}
func testRemove() {
let trie = Trie<String>()
let keys = ["foobar", "food", "foo", "somethingelse", "duplicate", "duplicate"]
keys.forEach {
trie.insert(key: $0, value: $0)
}
keys.forEach {
trie.remove(key: $0, value: $0)
}
let allResults = trie.find(key: "")
XCTAssertTrue(allResults.isEmpty)
let fooResults = trie.find(key: "foo")
XCTAssertTrue(fooResults.isEmpty)
let foodResults = trie.find(key: "food")
XCTAssertTrue(foodResults.isEmpty)
let ooResults = trie.find(key: "oo")
XCTAssertTrue(ooResults.isEmpty)
let aResults = trie.find(key: "a")
XCTAssertTrue(aResults.isEmpty)
// Verify that removal of values from all the keys that were inserted in the trie previously also resulted in the cleanup of the trie.
XCTAssertFalse(trie.hasChildren)
XCTAssertFalse(trie.hasValues)
}
}

View File

@@ -1,141 +0,0 @@
//
// UserSearchCacheTests.swift
// damusTests
//
// Created by Terry Yiu on 6/30/23.
//
import XCTest
@testable import damus
final class UserSearchCacheTests: XCTestCase {
var keypair: Keypair? = nil
let damusState = DamusState.empty
let nip05 = "_@somedomain.com"
override func setUpWithError() throws {
keypair = try XCTUnwrap(generate_new_keypair())
if let keypair {
let pubkey = keypair.pubkey
damusState.profiles.validated[pubkey] = NIP05.parse(nip05)
let profile = Profile(name: "tyiu", display_name: "Terry Yiu", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nip05, damus_donation: nil)
let timestampedProfile = TimestampedProfile(profile: profile, timestamp: 0, event: test_event)
damusState.profiles.add(id: pubkey, profile: timestampedProfile)
// Lookup to synchronize access on profiles dictionary to avoid race conditions.
let _ = damusState.profiles.lookup(id: pubkey)
}
}
override func tearDown() {
keypair = nil
}
func testSearch() throws {
let keypair = try XCTUnwrap(keypair)
XCTAssertEqual(damusState.user_search_cache.search(key: "tyiu"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "ty"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "terry yiu"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "rry"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "somedomain"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "dom"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "i"), [keypair.pubkey])
}
func testUpdateProfile() throws {
let keypair = try XCTUnwrap(keypair)
let newNip05 = "_@other.xyz"
damusState.profiles.validated[keypair.pubkey] = NIP05.parse(newNip05)
let newProfile = Profile(name: "whoami", display_name: "T-DAWG", about: nil, picture: nil, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: newNip05, damus_donation: nil)
let newTimestampedProfile = TimestampedProfile(profile: newProfile, timestamp: 1000, event: test_event)
damusState.profiles.add(id: keypair.pubkey, profile: newTimestampedProfile)
// Lookup to synchronize access on profiles dictionary to avoid race conditions.
let _ = damusState.profiles.lookup(id: keypair.pubkey)
// Old profile attributes are removed from cache.
XCTAssertEqual(damusState.user_search_cache.search(key: "tyiu"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "ty"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "Terry Yiu"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "rry"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "somedomain"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "dom"), [])
// New profile attributes are added to cache.
XCTAssertEqual(damusState.user_search_cache.search(key: "whoami"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "hoa"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "t-dawg"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "daw"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "other"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "xyz"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "the"), [keypair.pubkey])
XCTAssertEqual(damusState.user_search_cache.search(key: "y"), [keypair.pubkey])
}
func testUpdateOwnContactsPetnames() throws {
let keypair = try XCTUnwrap(keypair)
let damus = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681"
let jb55 = "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"
var pubkeysToPetnames = [String: String]()
pubkeysToPetnames[damus] = "damus"
pubkeysToPetnames[jb55] = "jb55"
let contactsEvent = try createContactsEventWithPetnames(pubkeysToPetnames: pubkeysToPetnames)
// Initial own contacts event caching on searchable petnames.
damusState.user_search_cache.updateOwnContactsPetnames(id: keypair.pubkey, oldEvent: nil, newEvent: contactsEvent)
XCTAssertEqual(damusState.user_search_cache.search(key: "damus"), [damus])
XCTAssertEqual(damusState.user_search_cache.search(key: "jb55"), [jb55])
XCTAssertEqual(damusState.user_search_cache.search(key: "5"), [jb55])
// Replace one of the petnames and verify if the cache updates accordingly.
pubkeysToPetnames.removeValue(forKey: jb55)
pubkeysToPetnames[jb55] = "bill"
let newContactsEvent = try createContactsEventWithPetnames(pubkeysToPetnames: pubkeysToPetnames)
damusState.user_search_cache.updateOwnContactsPetnames(id: keypair.pubkey, oldEvent: contactsEvent, newEvent: newContactsEvent)
XCTAssertEqual(damusState.user_search_cache.search(key: "damus"), [damus])
XCTAssertEqual(damusState.user_search_cache.search(key: "jb55"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "5"), [])
XCTAssertEqual(damusState.user_search_cache.search(key: "bill"), [jb55])
XCTAssertEqual(damusState.user_search_cache.search(key: "l"), [jb55])
}
private func createContactsEventWithPetnames(pubkeysToPetnames: [String: String]) throws -> NostrEvent {
let keypair = try XCTUnwrap(keypair)
let privkey = try XCTUnwrap(keypair.privkey)
let bootstrapRelays = load_bootstrap_relays(pubkey: keypair.pubkey)
let relayInfo = RelayInfo(read: true, write: true)
var relays: [String: RelayInfo] = [:]
for relay in bootstrapRelays {
relays[relay] = relayInfo
}
let relayJson = encode_json(relays)!
let tags = pubkeysToPetnames.enumerated().map {
["p", $0.element.key, "", $0.element.value]
}
let ev = NostrEvent(content: relayJson,
pubkey: keypair.pubkey,
kind: NostrKind.contacts.rawValue,
tags: tags)
ev.calculate_id()
ev.sign(privkey: privkey)
return ev
}
}

View File

@@ -69,7 +69,7 @@ final class ZapTests: XCTestCase {
XCTAssertEqual(zap.target, ZapTarget.profile("32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245"))
XCTAssertEqual(zap_notification_title(zap), "Zap")
XCTAssertEqual(zap_notification_body(profiles: Profiles(user_search_cache: UserSearchCache()), zap: zap), "You received 1k sats from 107jk7ht:2qlu3nfm")
XCTAssertEqual(zap_notification_body(profiles: Profiles(), zap: zap), "You received 1k sats from 107jk7ht:2qlu3nfm")
}
}