mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 14:45:22 +00:00
Fix/geohash work (#475)
* Scroll UX: auto-scroll only when last item visible; preserve user position when scrolled up for mesh/geohash/DM; reduce blanking after very long messages * iOS: re-enable keyboard autocomplete and default capitalization for message input * Styling: stop blue/underline styling for #hashtags; render as normal text color (self=orange, others=green) * Geo UI: ensure self shows teleported (face.dashed) if either per-session tag or manager flag is true * Geo teleported: publish UI updates by assigning @Published Set instead of in-place insert; update on tag receipt and channel switch --------- Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
@@ -1179,8 +1179,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
self.recordGeoParticipant(pubkeyHex: id.publicKeyHex)
|
self.recordGeoParticipant(pubkeyHex: id.publicKeyHex)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if LocationChannelManager.shared.teleported {
|
if LocationChannelManager.shared.teleported {
|
||||||
teleportedGeo.insert(id.publicKeyHex.lowercased())
|
let key = id.publicKeyHex.lowercased()
|
||||||
objectWillChange.send()
|
teleportedGeo = teleportedGeo.union([key])
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -1198,9 +1198,8 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
// Track teleport tag for participants
|
// Track teleport tag for participants
|
||||||
if let teleTag = event.tags.first(where: { $0.first == "t" }), teleTag.count >= 2, teleTag[1] == "teleport" {
|
if let teleTag = event.tags.first(where: { $0.first == "t" }), teleTag.count >= 2, teleTag[1] == "teleport" {
|
||||||
let key = event.pubkey.lowercased()
|
let key = event.pubkey.lowercased()
|
||||||
if !self.teleportedGeo.contains(key) {
|
Task { @MainActor in
|
||||||
self.teleportedGeo.insert(key)
|
self.teleportedGeo = self.teleportedGeo.union([key])
|
||||||
DispatchQueue.main.async { [weak self] in self?.objectWillChange.send() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Skip our own events (we already locally echoed)
|
// Skip our own events (we already locally echoed)
|
||||||
@@ -2826,17 +2825,25 @@ class ChatViewModel: ObservableObject, BitchatDelegate {
|
|||||||
result.append(AttributedString(mSuffix).mergingAttributes(light))
|
result.append(AttributedString(mSuffix).mergingAttributes(light))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
var matchStyle = AttributeContainer()
|
// Style non-mention matches
|
||||||
matchStyle.font = .system(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
|
|
||||||
// Self messages use orange for all tokens; others keep blue for links/hashtags
|
|
||||||
if type == "hashtag" {
|
if type == "hashtag" {
|
||||||
matchStyle.foregroundColor = isSelf ? .orange : .blue
|
// Do NOT special-style hashtags: render like normal content (no blue, no underline)
|
||||||
matchStyle.underlineStyle = .single
|
var tagStyle = AttributeContainer()
|
||||||
} else if type == "url" {
|
tagStyle.font = isSelf
|
||||||
matchStyle.foregroundColor = isSelf ? .orange : .blue
|
? .system(size: 14, weight: .bold, design: .monospaced)
|
||||||
matchStyle.underlineStyle = .single
|
: .system(size: 14, design: .monospaced)
|
||||||
|
tagStyle.foregroundColor = baseColor
|
||||||
|
result.append(AttributedString(matchText).mergingAttributes(tagStyle))
|
||||||
|
} else {
|
||||||
|
// Keep URL styling (blue + underline for non-self, orange for self)
|
||||||
|
var matchStyle = AttributeContainer()
|
||||||
|
matchStyle.font = .system(size: 14, weight: isSelf ? .bold : .semibold, design: .monospaced)
|
||||||
|
if type == "url" {
|
||||||
|
matchStyle.foregroundColor = isSelf ? .orange : .blue
|
||||||
|
matchStyle.underlineStyle = .single
|
||||||
|
}
|
||||||
|
result.append(AttributedString(matchText).mergingAttributes(matchStyle))
|
||||||
}
|
}
|
||||||
result.append(AttributedString(matchText).mergingAttributes(matchStyle))
|
|
||||||
}
|
}
|
||||||
lastEnd = nsRange.upperBound
|
lastEnd = nsRange.upperBound
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private func messagesView(privatePeer: String?) -> some View {
|
private func messagesView(privatePeer: String?) -> some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
|
@State var isAtBottom: Bool = true
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(alignment: .leading, spacing: 0) {
|
LazyVStack(alignment: .leading, spacing: 0) {
|
||||||
// Extract messages based on context (private or public chat)
|
// Extract messages based on context (private or public chat)
|
||||||
@@ -325,6 +326,17 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.id(message.id)
|
.id(message.id)
|
||||||
|
.onAppear {
|
||||||
|
// Track if last item is visible to enable auto-scroll only when near bottom
|
||||||
|
if message.id == windowedMessages.last?.id {
|
||||||
|
isAtBottom = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
if message.id == windowedMessages.last?.id {
|
||||||
|
isAtBottom = false
|
||||||
|
}
|
||||||
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
// Only show actions for messages from other users (not system or self)
|
// Only show actions for messages from other users (not system or self)
|
||||||
@@ -347,6 +359,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: viewModel.messages.count) { _ in
|
.onChange(of: viewModel.messages.count) { _ in
|
||||||
if privatePeer == nil && !viewModel.messages.isEmpty {
|
if privatePeer == nil && !viewModel.messages.isEmpty {
|
||||||
|
// Only autoscroll when user is at/near bottom
|
||||||
|
guard isAtBottom else { return }
|
||||||
// Throttle scroll animations to prevent excessive UI updates
|
// Throttle scroll animations to prevent excessive UI updates
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
||||||
@@ -367,6 +381,8 @@ struct ContentView: View {
|
|||||||
if let peerID = privatePeer,
|
if let peerID = privatePeer,
|
||||||
let messages = viewModel.privateChats[peerID],
|
let messages = viewModel.privateChats[peerID],
|
||||||
!messages.isEmpty {
|
!messages.isEmpty {
|
||||||
|
// Only autoscroll when user is at/near bottom
|
||||||
|
guard isAtBottom else { return }
|
||||||
// Same throttling for private chats
|
// Same throttling for private chats
|
||||||
let now = Date()
|
let now = Date()
|
||||||
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
if now.timeIntervalSince(lastScrollTime) > 0.5 {
|
||||||
@@ -508,10 +524,7 @@ struct ContentView: View {
|
|||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.focused($isTextFieldFocused)
|
.focused($isTextFieldFocused)
|
||||||
.padding(.leading, 12)
|
.padding(.leading, 12)
|
||||||
.autocorrectionDisabled(true)
|
// iOS keyboard autocomplete and capitalization enabled by default
|
||||||
#if os(iOS)
|
|
||||||
.textInputAutocapitalization(.never)
|
|
||||||
#endif
|
|
||||||
.onChange(of: messageText) { newValue in
|
.onChange(of: messageText) { newValue in
|
||||||
// Cancel previous debounce timer
|
// Cancel previous debounce timer
|
||||||
autocompleteDebounceTimer?.invalidate()
|
autocompleteDebounceTimer?.invalidate()
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ struct GeohashPeopleList: View {
|
|||||||
// For the local user, use a different face icon when teleported
|
// For the local user, use a different face icon when teleported
|
||||||
let isMe = (person.id == myHex)
|
let isMe = (person.id == myHex)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
let teleported = isMe ? LocationChannelManager.shared.teleported : viewModel.teleportedGeo.contains(person.id.lowercased())
|
// Consider either the per-session tag (for any peer) or the manager flag for self
|
||||||
|
let teleported = viewModel.teleportedGeo.contains(person.id.lowercased()) || (isMe && LocationChannelManager.shared.teleported)
|
||||||
#else
|
#else
|
||||||
let teleported = false
|
let teleported = false
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user