Add the ability to follow hashtags
Changelog-Added: Add the ability to follow hashtags
This commit is contained in:
@@ -144,6 +144,7 @@
|
|||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */; };
|
||||||
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
4C649881286E0EE300EAE2B3 /* secp256k1 in Frameworks */ = {isa = PBXBuildFile; productRef = 4C649880286E0EE300EAE2B3 /* secp256k1 */; };
|
||||||
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
4C687C212A5F7ED00092C550 /* DamusBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C202A5F7ED00092C550 /* DamusBackground.swift */; };
|
||||||
|
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */; };
|
||||||
4C687C272A6039500092C550 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C262A6039500092C550 /* TestData.swift */; };
|
4C687C272A6039500092C550 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C687C262A6039500092C550 /* TestData.swift */; };
|
||||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; };
|
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */; };
|
||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C75EFA327FA577B0006080F /* PostView.swift */; };
|
||||||
@@ -619,6 +620,7 @@
|
|||||||
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesView.swift; sourceTree = "<group>"; };
|
||||||
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
4C64987D286D082C00EAE2B3 /* DirectMessagesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessagesModel.swift; sourceTree = "<group>"; };
|
||||||
4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; };
|
4C687C202A5F7ED00092C550 /* DamusBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusBackground.swift; sourceTree = "<group>"; };
|
||||||
|
4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchHeaderView.swift; sourceTree = "<group>"; };
|
||||||
4C687C262A6039500092C550 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = "<group>"; };
|
4C687C262A6039500092C550 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = "<group>"; };
|
||||||
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = "<group>"; };
|
4C73C5132A4437C10062CAC0 /* ZapUserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapUserView.swift; sourceTree = "<group>"; };
|
||||||
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
4C75EFA327FA577B0006080F /* PostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostView.swift; sourceTree = "<group>"; };
|
||||||
@@ -1099,6 +1101,14 @@
|
|||||||
path = Notifications;
|
path = Notifications;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
4C687C2A2A6058450092C550 /* Search */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
4C687C232A5FA86D0092C550 /* SearchHeaderView.swift */,
|
||||||
|
);
|
||||||
|
path = Search;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
4C75EFA227FA576C0006080F /* Views */ = {
|
4C75EFA227FA576C0006080F /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1404,6 +1414,7 @@
|
|||||||
4CE4F9DF285287A000C00DD9 /* Components */ = {
|
4CE4F9DF285287A000C00DD9 /* Components */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
4C687C2A2A6058450092C550 /* Search */,
|
||||||
4C7D09702A0AEF4C00943473 /* Gradients */,
|
4C7D09702A0AEF4C00943473 /* Gradients */,
|
||||||
31D2E846295218AF006D67F8 /* Shimmer.swift */,
|
31D2E846295218AF006D67F8 /* Shimmer.swift */,
|
||||||
4CD7641A28A1641400B6928F /* EndBlock.swift */,
|
4CD7641A28A1641400B6928F /* EndBlock.swift */,
|
||||||
@@ -1946,6 +1957,7 @@
|
|||||||
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
|
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */,
|
||||||
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
4CC7AAF0297F11C700430951 /* SelectedEventView.swift in Sources */,
|
||||||
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
4CC7AAF8297F1CEE00430951 /* EventProfile.swift in Sources */,
|
||||||
|
4C687C242A5FA86D0092C550 /* SearchHeaderView.swift in Sources */,
|
||||||
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
64FBD06F296255C400D9D3B2 /* Theme.swift in Sources */,
|
||||||
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
|
4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */,
|
||||||
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
4C3EA64928FF597700C48A62 /* bech32.c in Sources */,
|
||||||
|
|||||||
134
damus/Components/Search/SearchHeaderView.swift
Normal file
134
damus/Components/Search/SearchHeaderView.swift
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
//
|
||||||
|
// SearchIconView.swift
|
||||||
|
// damus
|
||||||
|
//
|
||||||
|
// Created by William Casarin on 2023-07-12.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct SearchHeaderView: View {
|
||||||
|
let state: DamusState
|
||||||
|
let described: DescribedSearch
|
||||||
|
@State var is_following: Bool
|
||||||
|
|
||||||
|
init(state: DamusState, described: DescribedSearch) {
|
||||||
|
self.state = state
|
||||||
|
self.described = described
|
||||||
|
|
||||||
|
let is_following = (described.is_hashtag.map {
|
||||||
|
ht in is_following_hashtag(contacts: state.contacts.event, hashtag: ht)
|
||||||
|
}) ?? false
|
||||||
|
|
||||||
|
self._is_following = State(wrappedValue: is_following)
|
||||||
|
}
|
||||||
|
|
||||||
|
var Icon: some View {
|
||||||
|
ZStack {
|
||||||
|
Circle()
|
||||||
|
.fill(Color(red: 0xF8/255.0, green: 0xE7/255.0, blue: 0xF8/255.0))
|
||||||
|
.frame(width: 54, height: 54)
|
||||||
|
|
||||||
|
switch described {
|
||||||
|
case .hashtag:
|
||||||
|
Text("#")
|
||||||
|
.font(.largeTitle.bold())
|
||||||
|
.foregroundStyle(PinkGradient)
|
||||||
|
.mask(Text("#")
|
||||||
|
.font(.largeTitle.bold()))
|
||||||
|
|
||||||
|
case .unknown:
|
||||||
|
Image(systemName: "magnifyingglass")
|
||||||
|
.font(.title.bold())
|
||||||
|
.foregroundStyle(PinkGradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var SearchText: Text {
|
||||||
|
switch described {
|
||||||
|
case .hashtag(let ht):
|
||||||
|
Text(verbatim: "#" + ht)
|
||||||
|
case .unknown:
|
||||||
|
Text("Search")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func unfollow(_ hashtag: String) {
|
||||||
|
is_following = false
|
||||||
|
handle_unfollow(state: state, unfollow: .t(hashtag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func follow(_ hashtag: String) {
|
||||||
|
is_following = true
|
||||||
|
handle_follow(state: state, follow: .t(hashtag))
|
||||||
|
}
|
||||||
|
|
||||||
|
func FollowButton(_ ht: String) -> some View {
|
||||||
|
return Button(action: { follow(ht) }) {
|
||||||
|
Text("Follow hashtag")
|
||||||
|
.font(.footnote.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnfollowButton(_ ht: String) -> some View {
|
||||||
|
return Button(action: { unfollow(ht) }) {
|
||||||
|
Text("Unfollow hashtag")
|
||||||
|
.font(.footnote.bold())
|
||||||
|
}
|
||||||
|
.buttonStyle(GradientButtonStyle(padding: 10))
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(alignment: .center, spacing: 30) {
|
||||||
|
Icon
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10.0) {
|
||||||
|
SearchText
|
||||||
|
.foregroundStyle(DamusLogoGradient.gradient)
|
||||||
|
.font(.title.bold())
|
||||||
|
|
||||||
|
if state.is_privkey_user, case .hashtag(let ht) = described {
|
||||||
|
if is_following {
|
||||||
|
UnfollowButton(ht)
|
||||||
|
} else {
|
||||||
|
FollowButton(ht)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.followed)) { notif in
|
||||||
|
let ref = notif.object as! ReferencedId
|
||||||
|
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
||||||
|
self.is_following = true
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.unfollowed)) { notif in
|
||||||
|
let ref = notif.object as! ReferencedId
|
||||||
|
guard hashtag_matches_search(desc: self.described, ref: ref) else { return }
|
||||||
|
self.is_following = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hashtag_matches_search(desc: DescribedSearch, ref: ReferencedId) -> Bool {
|
||||||
|
guard let ht = desc.is_hashtag, ref.key == "t" && ref.ref_id == ht
|
||||||
|
else { return false }
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func is_following_hashtag(contacts: NostrEvent?, hashtag: String) -> Bool {
|
||||||
|
guard let contacts else { return false }
|
||||||
|
return is_already_following(contacts: contacts, follow: .t(hashtag))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
struct SearchHeaderView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
SearchHeaderView(state: test_damus_state(), described: .hashtag("damus"))
|
||||||
|
|
||||||
|
SearchHeaderView(state: test_damus_state(), described: .unknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -395,16 +395,22 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollow)) { notif in
|
.onReceive(handle_notify(.unfollow)) { notif in
|
||||||
guard let state = self.damus_state else {
|
guard let state = self.damus_state else { return }
|
||||||
return
|
guard let unfollow = handle_unfollow_notif(state: state, notif: notif) else { return }
|
||||||
}
|
}
|
||||||
handle_unfollow(state: state, notif: notif)
|
.onReceive(handle_notify(.unfollowed)) { notif in
|
||||||
|
guard let state = self.damus_state else { return }
|
||||||
|
let unfollow = notif.object as! ReferencedId
|
||||||
|
home.resubscribe(.unfollowing(unfollow))
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.follow)) { notif in
|
.onReceive(handle_notify(.follow)) { notif in
|
||||||
guard let state = self.damus_state else {
|
guard let state = self.damus_state else { return }
|
||||||
return
|
guard handle_follow_notif(state: state, notif: notif) else { return }
|
||||||
}
|
}
|
||||||
handle_follow(state: state, notif: notif)
|
.onReceive(handle_notify(.followed)) { notif in
|
||||||
|
guard let state = self.damus_state else { return }
|
||||||
|
let follow = notif.object as! ReferencedId
|
||||||
|
home.resubscribe(.following)
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.post)) { notif in
|
.onReceive(handle_notify(.post)) { notif in
|
||||||
guard let state = self.damus_state,
|
guard let state = self.damus_state,
|
||||||
@@ -879,47 +885,75 @@ func timeline_name(_ timeline: Timeline?) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_unfollow(state: DamusState, notif: Notification) {
|
@discardableResult
|
||||||
|
func handle_unfollow(state: DamusState, unfollow: ReferencedId) -> Bool {
|
||||||
guard let keypair = state.keypair.to_full() else {
|
guard let keypair = state.keypair.to_full() else {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = notif.object as! FollowTarget
|
|
||||||
let pk = target.pubkey
|
|
||||||
let old_contacts = state.contacts.event
|
let old_contacts = state.contacts.event
|
||||||
|
|
||||||
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: .p(pk))
|
guard let ev = unfollow_reference(postbox: state.postbox, our_contacts: old_contacts, keypair: keypair, unfollow: unfollow)
|
||||||
else { return }
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
notify(.unfollowed, pk)
|
notify(.unfollowed, unfollow)
|
||||||
|
|
||||||
state.contacts.event = ev
|
state.contacts.event = ev
|
||||||
state.contacts.remove_friend(pk)
|
|
||||||
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
|
if unfollow.key == "p" {
|
||||||
|
state.contacts.remove_friend(unfollow.ref_id)
|
||||||
|
state.user_search_cache.updateOwnContactsPetnames(id: state.pubkey, oldEvent: old_contacts, newEvent: ev)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_follow(state: DamusState, notif: Notification) {
|
func handle_unfollow_notif(state: DamusState, notif: Notification) -> ReferencedId? {
|
||||||
|
let target = notif.object as! FollowTarget
|
||||||
|
let pk = target.pubkey
|
||||||
|
|
||||||
|
let ref = ReferencedId.p(pk)
|
||||||
|
if handle_unfollow(state: state, unfollow: ref) {
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func handle_follow(state: DamusState, follow: ReferencedId) -> Bool {
|
||||||
guard let keypair = state.keypair.to_full() else {
|
guard let keypair = state.keypair.to_full() else {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let fnotify = notif.object as! FollowTarget
|
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: follow)
|
||||||
|
|
||||||
guard let ev = follow_reference(box: state.postbox, our_contacts: state.contacts.event, keypair: keypair, follow: .p(fnotify.pubkey))
|
|
||||||
else {
|
else {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
notify(.followed, fnotify.pubkey)
|
notify(.followed, follow)
|
||||||
|
|
||||||
state.contacts.event = ev
|
state.contacts.event = ev
|
||||||
|
if follow.key == "p" {
|
||||||
|
state.contacts.add_friend_pubkey(follow.ref_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
func handle_follow_notif(state: DamusState, notif: Notification) -> Bool {
|
||||||
|
let fnotify = notif.object as! FollowTarget
|
||||||
switch fnotify {
|
switch fnotify {
|
||||||
case .pubkey(let pk):
|
case .pubkey(let pk):
|
||||||
state.contacts.add_friend_pubkey(pk)
|
state.contacts.add_friend_pubkey(pk)
|
||||||
case .contact(let ev):
|
case .contact(let ev):
|
||||||
state.contacts.add_friend_contact(ev)
|
state.contacts.add_friend_contact(ev)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return handle_follow(state: state, follow: .p(fnotify.pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool {
|
func handle_post_notification(keypair: FullKeypair, postbox: PostBox, events: EventCache, notif: Notification) -> Bool {
|
||||||
|
|||||||
@@ -66,10 +66,19 @@ class Contacts {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func get_friend_list() -> [String] {
|
func get_friend_list() -> Set<String> {
|
||||||
return Array(friends)
|
return friends
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func get_followed_hashtags() -> Set<String> {
|
||||||
|
guard let ev = self.event else { return Set() }
|
||||||
|
return ev.tags.reduce(into: Set<String>(), { htags, tag in
|
||||||
|
if tag.count >= 2 && tag[0] == "t" && tag[1] != "" {
|
||||||
|
htags.insert(tag[1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func add_friend_pubkey(_ pubkey: String) {
|
func add_friend_pubkey(_ pubkey: String) {
|
||||||
friends.insert(pubkey)
|
friends.insert(pubkey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,40 @@ struct NewEventsBits: OptionSet {
|
|||||||
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
static let notifications: NewEventsBits = [.zaps, .likes, .reposts, .mentions]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum Resubscribe {
|
||||||
|
case following
|
||||||
|
case unfollowing(ReferencedId)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HomeResubFilter {
|
||||||
|
case pubkey(String)
|
||||||
|
case hashtag(String)
|
||||||
|
|
||||||
|
init?(from: ReferencedId) {
|
||||||
|
if from.key == "p" {
|
||||||
|
self = .pubkey(from.ref_id)
|
||||||
|
return
|
||||||
|
} else if from.key == "t" {
|
||||||
|
self = .hashtag(from.ref_id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filter(contacts: Contacts, ev: NostrEvent) -> Bool {
|
||||||
|
switch self {
|
||||||
|
case .pubkey(let pk):
|
||||||
|
return ev.pubkey == pk
|
||||||
|
case .hashtag(let ht):
|
||||||
|
if contacts.is_friend(ev.pubkey) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ev.references(id: ht, key: "t")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class HomeModel {
|
class HomeModel {
|
||||||
// Don't trigger a user notification for events older than a certain age
|
// Don't trigger a user notification for events older than a certain age
|
||||||
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
|
static let event_max_age_for_notification: TimeInterval = 12 * 60 * 60
|
||||||
@@ -36,6 +70,7 @@ class HomeModel {
|
|||||||
var done_init: Bool = false
|
var done_init: Bool = false
|
||||||
var incoming_dms: [NostrEvent] = []
|
var incoming_dms: [NostrEvent] = []
|
||||||
let dm_debouncer = Debouncer(interval: 0.5)
|
let dm_debouncer = Debouncer(interval: 0.5)
|
||||||
|
let resub_debouncer = Debouncer(interval: 3.0)
|
||||||
var should_debounce_dms = true
|
var should_debounce_dms = true
|
||||||
|
|
||||||
let home_subid = UUID().description
|
let home_subid = UUID().description
|
||||||
@@ -90,6 +125,31 @@ class HomeModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resubscribe(_ resubbing: Resubscribe) {
|
||||||
|
if self.should_debounce_dms {
|
||||||
|
// don't resub on initial load
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
print("hit resub debouncer")
|
||||||
|
|
||||||
|
resub_debouncer.debounce {
|
||||||
|
print("resub")
|
||||||
|
self.unsubscribe_to_home_filters()
|
||||||
|
|
||||||
|
switch resubbing {
|
||||||
|
case .following:
|
||||||
|
break
|
||||||
|
case .unfollowing(let r):
|
||||||
|
if let filter = HomeResubFilter(from: r) {
|
||||||
|
self.events.filter { ev in !filter.filter(contacts: self.damus_state.contacts, ev: ev) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.subscribe_to_home_filters()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
func process_event(sub_id: String, relay_id: String, ev: NostrEvent) {
|
||||||
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
if has_sub_id_event(sub_id: sub_id, ev_id: ev.id) {
|
||||||
return
|
return
|
||||||
@@ -382,8 +442,7 @@ class HomeModel {
|
|||||||
// TODO: since times should be based on events from a specific relay
|
// TODO: since times should be based on events from a specific relay
|
||||||
// perhaps we could mark this in the relay pool somehow
|
// perhaps we could mark this in the relay pool somehow
|
||||||
|
|
||||||
var friends = damus_state.contacts.get_friend_list()
|
let friends = get_friends()
|
||||||
friends.append(damus_state.pubkey)
|
|
||||||
|
|
||||||
var contacts_filter = NostrFilter(kinds: [.metadata])
|
var contacts_filter = NostrFilter(kinds: [.metadata])
|
||||||
contacts_filter.authors = friends
|
contacts_filter.authors = friends
|
||||||
@@ -405,18 +464,6 @@ class HomeModel {
|
|||||||
dms_filter.pubkeys = [ damus_state.pubkey ]
|
dms_filter.pubkeys = [ damus_state.pubkey ]
|
||||||
our_dms_filter.authors = [ damus_state.pubkey ]
|
our_dms_filter.authors = [ damus_state.pubkey ]
|
||||||
|
|
||||||
// TODO: separate likes?
|
|
||||||
var home_filter_kinds: [NostrKind] = [
|
|
||||||
.text, .longform, .boost
|
|
||||||
]
|
|
||||||
if !damus_state.settings.onlyzaps_mode {
|
|
||||||
home_filter_kinds.append(.like)
|
|
||||||
}
|
|
||||||
var home_filter = NostrFilter(kinds: home_filter_kinds)
|
|
||||||
// include our pubkey as well even if we're not technically a friend
|
|
||||||
home_filter.authors = friends
|
|
||||||
home_filter.limit = 500
|
|
||||||
|
|
||||||
var notifications_filter_kinds: [NostrKind] = [
|
var notifications_filter_kinds: [NostrKind] = [
|
||||||
.text,
|
.text,
|
||||||
.boost,
|
.boost,
|
||||||
@@ -429,33 +476,71 @@ class HomeModel {
|
|||||||
notifications_filter.pubkeys = [damus_state.pubkey]
|
notifications_filter.pubkeys = [damus_state.pubkey]
|
||||||
notifications_filter.limit = 500
|
notifications_filter.limit = 500
|
||||||
|
|
||||||
var home_filters = [home_filter]
|
|
||||||
var notifications_filters = [notifications_filter]
|
var notifications_filters = [notifications_filter]
|
||||||
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
|
var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter]
|
||||||
var dms_filters = [dms_filter, our_dms_filter]
|
var dms_filters = [dms_filter, our_dms_filter]
|
||||||
|
let last_of_kind = get_last_of_kind(relay_id: relay_id)
|
||||||
|
|
||||||
let last_of_kind = relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
|
||||||
|
|
||||||
home_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: home_filters)
|
|
||||||
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
contacts_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: contacts_filters)
|
||||||
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
notifications_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: notifications_filters)
|
||||||
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
dms_filters = update_filters_with_since(last_of_kind: last_of_kind, filters: dms_filters)
|
||||||
|
|
||||||
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
//print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters])
|
||||||
|
|
||||||
if let relay_id {
|
subscribe_to_home_filters(relay_id: relay_id)
|
||||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id])
|
|
||||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
|
let relay_ids = relay_id.map { [$0] }
|
||||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id])
|
|
||||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: [relay_id])
|
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: relay_ids)
|
||||||
} else {
|
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: relay_ids)
|
||||||
pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)))
|
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)), to: relay_ids)
|
||||||
pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)))
|
|
||||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)))
|
|
||||||
pool.send(.subscribe(.init(filters: dms_filters, sub_id: dms_subid)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func get_last_of_kind(relay_id: String?) -> [Int: NostrEvent] {
|
||||||
|
return relay_id.flatMap { last_event_of_kind[$0] } ?? [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsubscribe_to_home_filters() {
|
||||||
|
pool.send(.unsubscribe(home_subid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_friends() -> [String] {
|
||||||
|
var friends = damus_state.contacts.get_friend_list()
|
||||||
|
friends.insert(damus_state.pubkey)
|
||||||
|
return Array(friends)
|
||||||
|
}
|
||||||
|
|
||||||
|
func subscribe_to_home_filters(friends fs: [String]? = nil, relay_id: String? = nil) {
|
||||||
|
// TODO: separate likes?
|
||||||
|
let home_filter_kinds: [NostrKind] = [
|
||||||
|
.text, .longform, .boost
|
||||||
|
]
|
||||||
|
//if !damus_state.settings.onlyzaps_mode {
|
||||||
|
//home_filter_kinds.append(.like)
|
||||||
|
//}
|
||||||
|
|
||||||
|
let friends = fs ?? get_friends()
|
||||||
|
var home_filter = NostrFilter(kinds: home_filter_kinds)
|
||||||
|
// include our pubkey as well even if we're not technically a friend
|
||||||
|
home_filter.authors = friends
|
||||||
|
home_filter.limit = 500
|
||||||
|
|
||||||
|
var home_filters = [home_filter]
|
||||||
|
|
||||||
|
let followed_hashtags = Array(damus_state.contacts.get_followed_hashtags())
|
||||||
|
if followed_hashtags.count != 0 {
|
||||||
|
var hashtag_filter = NostrFilter.filter_hashtag(followed_hashtags)
|
||||||
|
hashtag_filter.limit = 100
|
||||||
|
home_filters.append(hashtag_filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
let relay_ids = relay_id.map { [$0] }
|
||||||
|
home_filters = update_filters_with_since(last_of_kind: get_last_of_kind(relay_id: relay_id), filters: home_filters)
|
||||||
|
let sub = NostrSubscribe(filters: home_filters, sub_id: home_subid)
|
||||||
|
|
||||||
|
pool.send(.subscribe(sub), to: relay_ids)
|
||||||
|
}
|
||||||
|
|
||||||
func handle_list_event(_ ev: NostrEvent) {
|
func handle_list_event(_ ev: NostrEvent) {
|
||||||
// we only care about our lists
|
// we only care about our lists
|
||||||
guard ev.pubkey == damus_state.pubkey else {
|
guard ev.pubkey == damus_state.pubkey else {
|
||||||
@@ -614,32 +699,34 @@ func add_contact_if_friend(contacts: Contacts, ev: NostrEvent) {
|
|||||||
|
|
||||||
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
func load_our_contacts(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) {
|
||||||
let contacts = state.contacts
|
let contacts = state.contacts
|
||||||
var new_pks = Set<String>()
|
var new_refs = Set<ReferencedId>()
|
||||||
// our contacts
|
// our contacts
|
||||||
for tag in ev.tags {
|
for tag in ev.tags {
|
||||||
if tag.count >= 2 && tag[0] == "p" {
|
guard let ref = tag_to_refid(tag) else { continue }
|
||||||
new_pks.insert(tag[1])
|
new_refs.insert(ref)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var old_pks = Set<String>()
|
var old_refs = Set<ReferencedId>()
|
||||||
// find removed contacts
|
// find removed contacts
|
||||||
if let old_ev = m_old_ev {
|
if let old_ev = m_old_ev {
|
||||||
for tag in old_ev.tags {
|
for tag in old_ev.tags {
|
||||||
if tag.count >= 2 && tag[0] == "p" {
|
guard let ref = tag_to_refid(tag) else { continue }
|
||||||
old_pks.insert(tag[1])
|
old_refs.insert(ref)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let diff = new_pks.symmetricDifference(old_pks)
|
let diff = new_refs.symmetricDifference(old_refs)
|
||||||
for pk in diff {
|
for ref in diff {
|
||||||
if new_pks.contains(pk) {
|
if new_refs.contains(ref) {
|
||||||
notify(.followed, pk)
|
notify(.followed, ref)
|
||||||
contacts.add_friend_pubkey(pk)
|
if ref.key == "p" {
|
||||||
|
contacts.add_friend_pubkey(ref.ref_id)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
notify(.unfollowed, pk)
|
notify(.unfollowed, ref)
|
||||||
contacts.remove_friend(pk)
|
if ref.key == "p" {
|
||||||
|
contacts.remove_friend(ref.ref_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,16 @@ struct FollowButtonView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.followed)) { notif in
|
.onReceive(handle_notify(.followed)) { notif in
|
||||||
let pk = notif.object as! String
|
let pk = notif.object as! ReferencedId
|
||||||
if pk != target.pubkey {
|
if pk.key == "p", pk.ref_id != target.pubkey {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.follow_state = .follows
|
self.follow_state = .follows
|
||||||
}
|
}
|
||||||
.onReceive(handle_notify(.unfollowed)) { notif in
|
.onReceive(handle_notify(.unfollowed)) { notif in
|
||||||
let pk = notif.object as! String
|
let pk = notif.object as! ReferencedId
|
||||||
if pk != target.pubkey {
|
if pk.key == "p", pk.ref_id != target.pubkey {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,32 +11,70 @@ struct SearchView: View {
|
|||||||
let appstate: DamusState
|
let appstate: DamusState
|
||||||
@ObservedObject var search: SearchModel
|
@ObservedObject var search: SearchModel
|
||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
|
|
||||||
|
let height: CGFloat = 250.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineView<AnyView>(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true })
|
TimelineView(events: search.events, loading: $search.loading, damus: appstate, show_friend_icon: true, filter: { _ in true }) {
|
||||||
.navigationBarTitle(describe_search(search.search))
|
ZStack(alignment: .leading) {
|
||||||
.onReceive(handle_notify(.switched_timeline)) { obj in
|
DamusBackground(maxHeight: height)
|
||||||
dismiss()
|
.mask(LinearGradient(gradient: Gradient(colors: [.black, .black, .black, .clear]), startPoint: .top, endPoint: .bottom))
|
||||||
}
|
SearchHeaderView(state: appstate, described: described_search)
|
||||||
.onAppear() {
|
.padding(.leading, 30)
|
||||||
search.subscribe()
|
.padding(.top, 100)
|
||||||
}
|
|
||||||
.onDisappear() {
|
|
||||||
search.unsubscribe()
|
|
||||||
}
|
|
||||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
|
||||||
search.filter_muted()
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.onReceive(handle_notify(.switched_timeline)) { obj in
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.onAppear() {
|
||||||
|
search.subscribe()
|
||||||
|
}
|
||||||
|
.onDisappear() {
|
||||||
|
search.unsubscribe()
|
||||||
|
}
|
||||||
|
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||||
|
search.filter_muted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var described_search: DescribedSearch {
|
||||||
|
return describe_search(search.search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func describe_search(_ filter: NostrFilter) -> String {
|
enum DescribedSearch {
|
||||||
if let hashtags = filter.hashtag {
|
case hashtag(String)
|
||||||
if hashtags.count >= 1 {
|
case unknown
|
||||||
return "#" + hashtags[0]
|
|
||||||
|
var is_hashtag: String? {
|
||||||
|
switch self {
|
||||||
|
case .hashtag(let ht):
|
||||||
|
return ht
|
||||||
|
case .unknown:
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Search"
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .hashtag(let s):
|
||||||
|
return "#" + s
|
||||||
|
case .unknown:
|
||||||
|
return "Search"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func describe_search(_ filter: NostrFilter) -> DescribedSearch {
|
||||||
|
if let hashtags = filter.hashtag {
|
||||||
|
if hashtags.count >= 1 {
|
||||||
|
return .hashtag(hashtags[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return .unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SearchView_Previews: PreviewProvider {
|
struct SearchView_Previews: PreviewProvider {
|
||||||
|
|||||||
Reference in New Issue
Block a user