mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 02:25:20 +00:00
PeerID 27/n: ContentView and MeshPeerList (#835)
This commit is contained in:
@@ -80,7 +80,7 @@ struct ContentView: View {
|
|||||||
// Timer-based refresh removed; use LocationChannelManager live updates instead
|
// Timer-based refresh removed; use LocationChannelManager live updates instead
|
||||||
// Window sizes for rendering (infinite scroll up)
|
// Window sizes for rendering (infinite scroll up)
|
||||||
@State private var windowCountPublic: Int = 300
|
@State private var windowCountPublic: Int = 300
|
||||||
@State private var windowCountPrivate: [String: Int] = [:]
|
@State private var windowCountPrivate: [PeerID: Int] = [:]
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
|
|
||||||
private struct PrivateHeaderContext {
|
private struct PrivateHeaderContext {
|
||||||
let headerPeerID: String
|
let headerPeerID: PeerID
|
||||||
let peer: BitchatPeer?
|
let peer: BitchatPeer?
|
||||||
let displayName: String
|
let displayName: String
|
||||||
let isNostrAvailable: Bool
|
let isNostrAvailable: Bool
|
||||||
@@ -328,9 +328,9 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// MARK: - Message List View
|
// MARK: - Message List View
|
||||||
|
|
||||||
private func messagesView(privatePeer: String?, isAtBottom: Binding<Bool>) -> some View {
|
private func messagesView(privatePeer: PeerID?, isAtBottom: Binding<Bool>) -> some View {
|
||||||
let messages: [BitchatMessage] = {
|
let messages: [BitchatMessage] = {
|
||||||
if let peerID = PeerID(str: privatePeer) {
|
if let peerID = privatePeer {
|
||||||
return viewModel.getPrivateChatMessages(for: peerID)
|
return viewModel.getPrivateChatMessages(for: peerID)
|
||||||
}
|
}
|
||||||
return viewModel.messages
|
return viewModel.messages
|
||||||
@@ -470,7 +470,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onChange(of: viewModel.privateChats) { _ in
|
.onChange(of: viewModel.privateChats) { _ in
|
||||||
if let peerID = privatePeer,
|
if let peerID = privatePeer,
|
||||||
let messages = viewModel.privateChats[PeerID(str: peerID)],
|
let messages = viewModel.privateChats[peerID],
|
||||||
!messages.isEmpty {
|
!messages.isEmpty {
|
||||||
// If the newest private message is from me, always scroll
|
// If the newest private message is from me, always scroll
|
||||||
let lastMsg = messages.last!
|
let lastMsg = messages.last!
|
||||||
@@ -525,7 +525,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
// Also check when view appears
|
// Also check when view appears
|
||||||
if let peerID = PeerID(str: privatePeer) {
|
if let peerID = privatePeer {
|
||||||
// Try multiple times to ensure read receipts are sent
|
// Try multiple times to ensure read receipts are sent
|
||||||
viewModel.markPrivateMessagesAsRead(from: peerID)
|
viewModel.markPrivateMessagesAsRead(from: peerID)
|
||||||
|
|
||||||
@@ -826,10 +826,10 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func scrollToBottom(on proxy: ScrollViewProxy,
|
private func scrollToBottom(on proxy: ScrollViewProxy,
|
||||||
privatePeer: String?,
|
privatePeer: PeerID?,
|
||||||
isAtBottom: Binding<Bool>) {
|
isAtBottom: Binding<Bool>) {
|
||||||
let targetID: String? = {
|
let targetID: String? = {
|
||||||
if let peer = PeerID(str: privatePeer),
|
if let peer = privatePeer,
|
||||||
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
|
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
|
||||||
return "dm:\(peer)|\(last)"
|
return "dm:\(peer)|\(last)"
|
||||||
}
|
}
|
||||||
@@ -855,7 +855,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) {
|
||||||
let secondTarget: String? = {
|
let secondTarget: String? = {
|
||||||
if let peer = PeerID(str: privatePeer),
|
if let peer = privatePeer,
|
||||||
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
|
let last = viewModel.getPrivateChatMessages(for: peer).suffix(300).last?.id {
|
||||||
return "dm:\(peer)|\(last)"
|
return "dm:\(peer)|\(last)"
|
||||||
}
|
}
|
||||||
@@ -992,14 +992,14 @@ struct ContentView: View {
|
|||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
secondaryTextColor: secondaryTextColor,
|
secondaryTextColor: secondaryTextColor,
|
||||||
onTapPeer: { peerID in
|
onTapPeer: { peerID in
|
||||||
viewModel.startPrivateChat(with: PeerID(str: peerID))
|
viewModel.startPrivateChat(with: peerID)
|
||||||
showSidebar = true
|
showSidebar = true
|
||||||
},
|
},
|
||||||
onToggleFavorite: { peerID in
|
onToggleFavorite: { peerID in
|
||||||
viewModel.toggleFavorite(peerID: PeerID(str: peerID))
|
viewModel.toggleFavorite(peerID: peerID)
|
||||||
},
|
},
|
||||||
onShowFingerprint: { peerID in
|
onShowFingerprint: { peerID in
|
||||||
viewModel.showFingerprint(for: PeerID(str: peerID))
|
viewModel.showFingerprint(for: peerID)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1014,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
private var privateChatSheetView: some View {
|
private var privateChatSheetView: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if let privatePeerID = viewModel.selectedPrivateChatPeer?.id {
|
if let privatePeerID = viewModel.selectedPrivateChatPeer {
|
||||||
let headerContext = makePrivateHeaderContext(for: privatePeerID)
|
let headerContext = makePrivateHeaderContext(for: privatePeerID)
|
||||||
|
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
@@ -1038,12 +1038,11 @@ struct ContentView: View {
|
|||||||
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
privateHeaderInfo(context: headerContext, privatePeerID: privatePeerID)
|
privateHeaderInfo(context: headerContext, privatePeerID: privatePeerID)
|
||||||
let peerID = PeerID(str: headerContext.headerPeerID)
|
let isFavorite = viewModel.isFavorite(peerID: headerContext.headerPeerID)
|
||||||
let isFavorite = viewModel.isFavorite(peerID: peerID)
|
|
||||||
|
|
||||||
if !privatePeerID.hasPrefix("nostr_") {
|
if !privatePeerID.isGeoDM {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.toggleFavorite(peerID: peerID)
|
viewModel.toggleFavorite(peerID: headerContext.headerPeerID)
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: isFavorite ? "star.fill" : "star")
|
Image(systemName: isFavorite ? "star.fill" : "star")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
@@ -1082,7 +1081,7 @@ struct ContentView: View {
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
messagesView(privatePeer: viewModel.selectedPrivateChatPeer?.id, isAtBottom: $isAtBottomPrivate)
|
messagesView(privatePeer: viewModel.selectedPrivateChatPeer, isAtBottom: $isAtBottomPrivate)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
Divider()
|
Divider()
|
||||||
@@ -1104,9 +1103,9 @@ struct ContentView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func privateHeaderInfo(context: PrivateHeaderContext, privatePeerID: String) -> some View {
|
private func privateHeaderInfo(context: PrivateHeaderContext, privatePeerID: PeerID) -> some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.showFingerprint(for: PeerID(str: context.headerPeerID))
|
viewModel.showFingerprint(for: context.headerPeerID)
|
||||||
}) {
|
}) {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
if let connectionState = context.peer?.connectionState {
|
if let connectionState = context.peer?.connectionState {
|
||||||
@@ -1129,7 +1128,7 @@ struct ContentView: View {
|
|||||||
case .offline:
|
case .offline:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
} else if viewModel.meshService.isPeerReachable(PeerID(str: context.headerPeerID)) {
|
} else if viewModel.meshService.isPeerReachable(context.headerPeerID) {
|
||||||
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
Image(systemName: "point.3.filled.connected.trianglepath.dotted")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
@@ -1139,7 +1138,7 @@ struct ContentView: View {
|
|||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
.foregroundColor(.purple)
|
.foregroundColor(.purple)
|
||||||
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator"))
|
.accessibilityLabel(String(localized: "content.accessibility.available_nostr", comment: "Accessibility label for Nostr-available peer indicator"))
|
||||||
} else if viewModel.meshService.isPeerConnected(PeerID(str: context.headerPeerID)) || viewModel.connectedPeers.contains(PeerID(str: context.headerPeerID)) {
|
} else if viewModel.meshService.isPeerConnected(context.headerPeerID) || viewModel.connectedPeers.contains(context.headerPeerID) {
|
||||||
Image(systemName: "dot.radiowaves.left.and.right")
|
Image(systemName: "dot.radiowaves.left.and.right")
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
@@ -1150,14 +1149,14 @@ struct ContentView: View {
|
|||||||
.font(.bitchatSystem(size: 16, weight: .medium, design: .monospaced))
|
.font(.bitchatSystem(size: 16, weight: .medium, design: .monospaced))
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
if !privatePeerID.hasPrefix("nostr_") {
|
if !privatePeerID.isGeoDM {
|
||||||
let statusPeerID: String = {
|
let statusPeerID: PeerID = {
|
||||||
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) {
|
if privatePeerID.id.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID.id) {
|
||||||
return short.id
|
return short
|
||||||
}
|
}
|
||||||
return context.headerPeerID
|
return context.headerPeerID
|
||||||
}()
|
}()
|
||||||
let encryptionStatus = viewModel.getEncryptionStatus(for: PeerID(str: statusPeerID))
|
let encryptionStatus = viewModel.getEncryptionStatus(for: statusPeerID)
|
||||||
if let icon = encryptionStatus.icon {
|
if let icon = encryptionStatus.icon {
|
||||||
Image(systemName: icon)
|
Image(systemName: icon)
|
||||||
.font(.bitchatSystem(size: 14))
|
.font(.bitchatSystem(size: 14))
|
||||||
@@ -1189,33 +1188,33 @@ struct ContentView: View {
|
|||||||
.frame(height: headerHeight)
|
.frame(height: headerHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func makePrivateHeaderContext(for privatePeerID: String) -> PrivateHeaderContext {
|
private func makePrivateHeaderContext(for privatePeerID: PeerID) -> PrivateHeaderContext {
|
||||||
let headerPeerID: String = {
|
let headerPeerID: PeerID = {
|
||||||
if privatePeerID.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID) {
|
if privatePeerID.id.count == 64, let short = viewModel.getShortIDForNoiseKey(privatePeerID.id) {
|
||||||
return short.id
|
return short
|
||||||
}
|
}
|
||||||
return privatePeerID
|
return privatePeerID
|
||||||
}()
|
}()
|
||||||
|
|
||||||
let peer = viewModel.getPeer(byID: PeerID(str: headerPeerID))
|
let peer = viewModel.getPeer(byID: headerPeerID)
|
||||||
|
|
||||||
let displayName: String = {
|
let displayName: String = {
|
||||||
if privatePeerID.hasPrefix("nostr_"), case .location(let ch) = locationManager.selectedChannel {
|
if privatePeerID.isGeoDM, case .location(let ch) = locationManager.selectedChannel {
|
||||||
let disp = viewModel.geohashDisplayName(for: PeerID(str: privatePeerID))
|
let disp = viewModel.geohashDisplayName(for: privatePeerID)
|
||||||
return "#\(ch.geohash)/@\(disp)"
|
return "#\(ch.geohash)/@\(disp)"
|
||||||
}
|
}
|
||||||
if let name = peer?.displayName { return name }
|
if let name = peer?.displayName { return name }
|
||||||
if let name = viewModel.meshService.peerNickname(peerID: PeerID(str: headerPeerID)) { return name }
|
if let name = viewModel.meshService.peerNickname(peerID: headerPeerID) { return name }
|
||||||
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID) ?? Data()),
|
if let fav = FavoritesPersistenceService.shared.getFavoriteStatus(for: Data(hexString: headerPeerID.id) ?? Data()),
|
||||||
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
!fav.peerNickname.isEmpty { return fav.peerNickname }
|
||||||
if headerPeerID.count == 16 {
|
if headerPeerID.id.count == 16 {
|
||||||
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(PeerID(str: headerPeerID))
|
let candidates = viewModel.identityManager.getCryptoIdentitiesByPeerIDPrefix(headerPeerID)
|
||||||
if let id = candidates.first,
|
if let id = candidates.first,
|
||||||
let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) {
|
let social = viewModel.identityManager.getSocialIdentity(for: id.fingerprint) {
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
||||||
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
if !social.claimedNickname.isEmpty { return social.claimedNickname }
|
||||||
}
|
}
|
||||||
} else if headerPeerID.count == 64, let keyData = Data(hexString: headerPeerID) {
|
} else if let keyData = headerPeerID.noiseKey {
|
||||||
let fp = keyData.sha256Fingerprint()
|
let fp = keyData.sha256Fingerprint()
|
||||||
if let social = viewModel.identityManager.getSocialIdentity(for: fp) {
|
if let social = viewModel.identityManager.getSocialIdentity(for: fp) {
|
||||||
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
if let pet = social.localPetname, !pet.isEmpty { return pet }
|
||||||
@@ -1227,7 +1226,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
let isNostrAvailable: Bool = {
|
let isNostrAvailable: Bool = {
|
||||||
guard let connectionState = peer?.connectionState else {
|
guard let connectionState = peer?.connectionState else {
|
||||||
if let noiseKey = Data(hexString: headerPeerID),
|
if let noiseKey = Data(hexString: headerPeerID.id),
|
||||||
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
let favoriteStatus = FavoritesPersistenceService.shared.getFavoriteStatus(for: noiseKey),
|
||||||
favoriteStatus.isMutual {
|
favoriteStatus.isMutual {
|
||||||
return true
|
return true
|
||||||
@@ -1746,7 +1745,7 @@ private extension ContentView {
|
|||||||
|
|
||||||
private func expandWindow(ifNeededFor message: BitchatMessage,
|
private func expandWindow(ifNeededFor message: BitchatMessage,
|
||||||
allMessages: [BitchatMessage],
|
allMessages: [BitchatMessage],
|
||||||
privatePeer: String?,
|
privatePeer: PeerID?,
|
||||||
proxy: ScrollViewProxy) {
|
proxy: ScrollViewProxy) {
|
||||||
let step = TransportConfig.uiWindowStepCount
|
let step = TransportConfig.uiWindowStepCount
|
||||||
let contextKey: String = {
|
let contextKey: String = {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ struct MeshPeerList: View {
|
|||||||
@ObservedObject var viewModel: ChatViewModel
|
@ObservedObject var viewModel: ChatViewModel
|
||||||
let textColor: Color
|
let textColor: Color
|
||||||
let secondaryTextColor: Color
|
let secondaryTextColor: Color
|
||||||
let onTapPeer: (String) -> Void
|
let onTapPeer: (PeerID) -> Void
|
||||||
let onToggleFavorite: (String) -> Void
|
let onToggleFavorite: (PeerID) -> Void
|
||||||
let onShowFingerprint: (String) -> Void
|
let onShowFingerprint: (PeerID) -> Void
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
|
|
||||||
@State private var orderedIDs: [String] = []
|
@State private var orderedIDs: [String] = []
|
||||||
@@ -130,7 +130,7 @@ struct MeshPeerList: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !isMe {
|
if !isMe {
|
||||||
Button(action: { onToggleFavorite(peer.peerID.id) }) {
|
Button(action: { onToggleFavorite(peer.peerID) }) {
|
||||||
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
Image(systemName: (peer.favoriteStatus?.isFavorite ?? false) ? "star.fill" : "star")
|
||||||
.font(.bitchatSystem(size: 12))
|
.font(.bitchatSystem(size: 12))
|
||||||
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
|
.foregroundColor((peer.favoriteStatus?.isFavorite ?? false) ? .yellow : secondaryTextColor)
|
||||||
@@ -142,8 +142,8 @@ struct MeshPeerList: View {
|
|||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
.padding(.top, idx == 0 ? 10 : 0)
|
.padding(.top, idx == 0 ? 10 : 0)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture { if !isMe { onTapPeer(peer.peerID.id) } }
|
.onTapGesture { if !isMe { onTapPeer(peer.peerID) } }
|
||||||
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID.id) } }
|
.onTapGesture(count: 2) { if !isMe { onShowFingerprint(peer.peerID) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Seed and update order outside result builder
|
// Seed and update order outside result builder
|
||||||
|
|||||||
Reference in New Issue
Block a user