commit b1b032d9057ba91059a5bf6351283dec5791ac13
parent 7c805f7f2395c7f6d31b5cddc60f78d1b0b36e90
Author: Swift Coder <scoder1747@gmail.com>
Date: Sun, 20 Oct 2024 17:58:52 -0400
postview: add hashtag suggestions
Closes: #2604
Changelog-Changes: Add hashtag suggestions to post view
Signed-off-by: Swift Coder <scoder1747@gmail.com>
Diffstat:
4 files changed, 159 insertions(+), 23 deletions(-)
diff --git a/damus/Views/PostView.swift b/damus/Views/PostView.swift
@@ -401,7 +401,7 @@ struct PostView: View {
GeometryReader { (deviceSize: GeometryProxy) in
VStack(alignment: .leading, spacing: 0) {
let searching = get_searching_string(focusWordAttributes.0)
-
+ let searchingHashTag = get_searching_hashTag(focusWordAttributes.0)
TopBar
ScrollViewReader { scroller in
@@ -415,7 +415,7 @@ struct PostView: View {
.padding(.top, 5)
}
}
- .frame(maxHeight: searching == nil ? deviceSize.size.height : 70)
+ .frame(maxHeight: searching == nil && searchingHashTag == nil ? deviceSize.size.height : 70)
.onAppear {
scroll_to_event(scroller: scroller, id: "post", delay: 1.0, animate: true, anchor: .top)
}
@@ -426,7 +426,17 @@ struct PostView: View {
UserSearch(damus_state: damus_state, search: searching, focusWordAttributes: $focusWordAttributes, newCursorIndex: $newCursorIndex, post: $post)
.frame(maxHeight: .infinity)
.environmentObject(tagModel)
- } else {
+ // This else observes '#' for hash-tag suggestions and creates SuggestedHashtagsView
+ } else if let searchingHashTag {
+ SuggestedHashtagsView(damus_state: damus_state,
+ events: SearchHomeModel(damus_state: damus_state).events,
+ isFromPostView: true,
+ queryHashTag: searchingHashTag,
+ focusWordAttributes: $focusWordAttributes,
+ newCursorIndex: $newCursorIndex,
+ post: $post)
+ .environmentObject(tagModel)
+ } else {
Divider()
VStack(alignment: .leading) {
AttachmentBar
@@ -526,6 +536,17 @@ func get_searching_string(_ word: String?) -> String? {
return String(word.dropFirst())
}
+fileprivate func get_searching_hashTag(_ word: String?) -> String? {
+ guard let word,
+ word.count >= 2,
+ let first_char = word.first,
+ first_char == "#" else {
+ return nil
+ }
+
+ return String(word.dropFirst())
+}
+
struct PostView_Previews: PreviewProvider {
static var previews: some View {
PostView(action: .posting(.none), damus_state: test_damus_state)
diff --git a/damus/Views/SuggestedHashtagsView.swift b/damus/Views/SuggestedHashtagsView.swift
@@ -39,6 +39,7 @@ struct SuggestedHashtagsView: View {
.sorted(by: { a, b in
a.count > b.count
})
+ SuggestedHashtagsView.lastRefresh_hashtags = all_items // Collecting recent hash-tag data from Search-page
guard let item_limit else {
return all_items
}
@@ -46,10 +47,55 @@ struct SuggestedHashtagsView: View {
}
}
- init(damus_state: DamusState, suggested_hashtags: [String]? = nil, max_items item_limit: Int? = nil, events: EventHolder) {
+
+ static var lastRefresh_hashtags: [HashtagWithUserCount] = [] // Holds hash-tag data for PostView
+ var isFromPostView: Bool
+ var queryHashTag: String
+
+ var filteredSuggestedHashtags: [HashtagWithUserCount] {
+ let val = SuggestedHashtagsView.lastRefresh_hashtags.filter {$0.hashtag.hasPrefix(returnFirstWordOnly(hashTag: queryHashTag))}
+ if val.isEmpty {
+ if SuggestedHashtagsView.lastRefresh_hashtags.isEmpty {
+ // This is special case when user goes directly to PostView without opening Search-page previously.
+ var val = hashtags_with_count_to_display // retrieves default hash-tage values
+ // if not-found, put query hash tag at top
+ val.insert(HashtagWithUserCount(hashtag: returnFirstWordOnly(hashTag: queryHashTag), count: 0), at: 0)
+ return val
+ } else {
+ // if not-found, put query hash tag at top
+ var val = SuggestedHashtagsView.lastRefresh_hashtags
+ val.insert(HashtagWithUserCount(hashtag: returnFirstWordOnly(hashTag: queryHashTag), count: 0), at: 0)
+ return val
+ }
+ } else {
+ return val
+ }
+ }
+
+ @Binding var focusWordAttributes: (String?, NSRange?)
+ @Binding var newCursorIndex: Int?
+ @Binding var post: NSMutableAttributedString
+ @EnvironmentObject var tagModel: TagModel
+
+ init(damus_state: DamusState,
+ suggested_hashtags: [String]? = nil,
+ max_items item_limit: Int? = nil,
+ events: EventHolder,
+ isFromPostView: Bool = false,
+ queryHashTag: String = "",
+ focusWordAttributes: Binding<(String?, NSRange?)> = .constant((nil, nil)),
+ newCursorIndex: Binding<Int?> = .constant(nil),
+ post: Binding<NSMutableAttributedString> = .constant(NSMutableAttributedString(string: ""))) {
self.damus_state = damus_state
self.suggested_hashtags = suggested_hashtags ?? DEFAULT_SUGGESTED_HASHTAGS
self.item_limit = item_limit
+
+ self.isFromPostView = isFromPostView
+ self.queryHashTag = queryHashTag
+ self._focusWordAttributes = focusWordAttributes
+ self._newCursorIndex = newCursorIndex
+ self._post = post
+
_events = StateObject.init(wrappedValue: events)
}
@@ -59,24 +105,43 @@ struct SuggestedHashtagsView: View {
Image(systemName: "sparkles")
Text("Suggested hashtags", comment: "A label indicating that the items below it are suggested hashtags")
Spacer()
- Button(action: {
- withAnimation(.easeOut(duration: 0.2)) {
- show_suggested_hashtags.toggle()
- }
- }) {
- if show_suggested_hashtags {
- Image(systemName: "rectangle.compress.vertical")
- .foregroundStyle(PinkGradient)
- } else {
- Image(systemName: "rectangle.expand.vertical")
- .foregroundStyle(PinkGradient)
+ // Don't show suggestion expand/contract button when user is in PostView
+ if !isFromPostView {
+ Button(action: {
+ withAnimation(.easeOut(duration: 0.2)) {
+ show_suggested_hashtags.toggle()
+ }
+ }) {
+ if show_suggested_hashtags {
+ Image(systemName: "rectangle.compress.vertical")
+ .foregroundStyle(PinkGradient)
+ } else {
+ Image(systemName: "rectangle.expand.vertical")
+ .foregroundStyle(PinkGradient)
+ }
}
}
}
.foregroundColor(.secondary)
.padding(.vertical, 10)
- if show_suggested_hashtags {
+ if isFromPostView {
+ ScrollView {
+ LazyVStack {
+ ForEach(filteredSuggestedHashtags,
+ id: \.self) { hashtag_with_count in
+ SuggestedHashtagView(damus_state: damus_state,
+ hashtag: hashtag_with_count.hashtag,
+ count: hashtag_with_count.count,
+ isFromPostView: true,
+ focusWordAttributes: $focusWordAttributes,
+ newCursorIndex: $newCursorIndex,
+ post: $post)
+ .environmentObject(tagModel)
+ }
+ }
+ }
+ } else if show_suggested_hashtags {
ForEach(hashtags_with_count_to_display,
id: \.self) { hashtag_with_count in
SuggestedHashtagView(damus_state: damus_state, hashtag: hashtag_with_count.hashtag, count: hashtag_with_count.count)
@@ -91,10 +156,26 @@ struct SuggestedHashtagsView: View {
let hashtag: String
let count: Int
- init(damus_state: DamusState, hashtag: String, count: Int) {
+ let isFromPostView: Bool
+ @Binding var focusWordAttributes: (String?, NSRange?)
+ @Binding var newCursorIndex: Int?
+ @Binding var post: NSMutableAttributedString
+ @EnvironmentObject var tagModel: TagModel
+
+ init(damus_state: DamusState,
+ hashtag: String,
+ count: Int,
+ isFromPostView: Bool = false,
+ focusWordAttributes: Binding<(String?, NSRange?)> = .constant((nil, nil)),
+ newCursorIndex: Binding<Int?> = .constant(nil),
+ post: Binding<NSMutableAttributedString> = .constant(NSMutableAttributedString(string: ""))) {
self.damus_state = damus_state
self.hashtag = hashtag
self.count = count
+ self.isFromPostView = isFromPostView
+ self._focusWordAttributes = focusWordAttributes
+ self._newCursorIndex = newCursorIndex
+ self._post = post
}
var body: some View {
@@ -105,18 +186,48 @@ struct SuggestedHashtagsView: View {
Text(verbatim: "#\(hashtag)")
.bold()
- let pluralizedString = pluralizedString(key: "users_talking_about_it", count: self.count)
- Text(pluralizedString)
- .foregroundStyle(.secondary)
+ // Don't show user-talking label from PostView when the count is 0
+ if isFromPostView {
+ if count != 0 {
+ let pluralizedString = pluralizedString(key: "users_talking_about_it", count: self.count)
+ Text(pluralizedString)
+ .foregroundStyle(.secondary)
+ }
+ } else {
+ let pluralizedString = pluralizedString(key: "users_talking_about_it", count: self.count)
+ Text(pluralizedString)
+ .foregroundStyle(.secondary)
+ }
}
Spacer()
}
+ .contentShape(Rectangle()) // make the entire row/rectangle tappable
.onTapGesture {
- let search_model = SearchModel(state: damus_state, search: NostrFilter.init(hashtag: [hashtag]))
- damus_state.nav.push(route: Route.Search(search: search_model))
+ if isFromPostView {
+ let hashTag = NSMutableAttributedString(string: "#\(returnFirstWordOnly(hashTag: hashtag))",
+ attributes: [
+ NSAttributedString.Key.foregroundColor: UIColor.black,
+ NSAttributedString.Key.link: "#\(hashtag)"
+ ])
+ appendHashTag(withTag: hashTag)
+ } else {
+ let search_model = SearchModel(state: damus_state, search: NostrFilter.init(hashtag: [hashtag]))
+ damus_state.nav.push(route: Route.Search(search: search_model))
+ }
}
}
+
+ // Current working-code similar to UserSearch/appendUserTag
+ private func appendHashTag(withTag tag: NSMutableAttributedString) {
+ guard let wordRange = focusWordAttributes.1 else { return }
+ let appended = append_user_tag(tag: tag, post: post, word_range: wordRange)
+ self.post = appended.post
+ // adjust cursor position appropriately: ('diff' used in TextViewWrapper / updateUIView after below update of 'post')
+ tagModel.diff = appended.tag.length - wordRange.length
+ focusWordAttributes = (nil, nil)
+ newCursorIndex = wordRange.location + appended.tag.length
+ }
}
func users_talking_about(hashtag: Hashtag) -> Int {
@@ -147,3 +258,6 @@ struct SuggestedHashtagsView_Previews: PreviewProvider {
}
}
+fileprivate func returnFirstWordOnly(hashTag: String) -> String {
+ return hashTag.components(separatedBy: " ").first?.lowercased() ?? ""
+}
diff --git a/damus/Views/TextViewWrapper.swift b/damus/Views/TextViewWrapper.swift
@@ -93,7 +93,7 @@ struct TextViewWrapper: UIViewRepresentable {
let updateCursorPosition: ((Int) -> Void)
let initialTextSuffix: String?
var initialTextSuffixWasAdded: Bool = false
- static let ESCAPE_SEQUENCES = ["\n", "@", " ", ", ", ". ", "! ", "? ", "; "]
+ static let ESCAPE_SEQUENCES = ["\n", "@", " ", ", ", ". ", "! ", "? ", "; ", "#"]
init(attributedText: Binding<NSMutableAttributedString>,
getFocusWordForMention: ((String?, NSRange?) -> Void)?,
diff --git a/damus/damusApp.swift b/damus/damusApp.swift
@@ -44,6 +44,7 @@ struct MainView: View {
.onReceive(handle_notify(.logout)) { () in
try? clear_keypair()
keypair = nil
+ SuggestedHashtagsView.lastRefresh_hashtags.removeAll()
// We need to disconnect and reconnect to all relays when the user signs out
// This is to conform to NIP-42 and ensure we aren't persisting old connections
notify(.disconnect_relays)