postview: add hashtag suggestions
Closes: #2604 Changelog-Changes: Add hashtag suggestions to post view Signed-off-by: Swift Coder <scoder1747@gmail.com>
This commit is contained in:
committed by
William Casarin
parent
7c805f7f23
commit
b1b032d905
@@ -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)
|
||||
|
||||
@@ -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() ?? ""
|
||||
}
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user