mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 08:25:21 +00:00
Fix header bouncing and add private message notifications
- Fix header bar height to prevent bouncing when switching views - Add in-app notification banner for incoming private messages - Show orange notification that slides down from top - Tap notification to jump directly to private chat - Use medium haptic feedback for private messages (iOS) - Auto-dismiss notifications after 3 seconds
This commit is contained in:
@@ -20,11 +20,13 @@ class ChatViewModel: ObservableObject {
|
|||||||
@Published var privateChats: [String: [BitchatMessage]] = [:] // peerID -> messages
|
@Published var privateChats: [String: [BitchatMessage]] = [:] // peerID -> messages
|
||||||
@Published var selectedPrivateChatPeer: String? = nil
|
@Published var selectedPrivateChatPeer: String? = nil
|
||||||
@Published var unreadPrivateMessages: Set<String> = []
|
@Published var unreadPrivateMessages: Set<String> = []
|
||||||
|
@Published var privateMessageNotification: (sender: String, message: String)? = nil
|
||||||
|
|
||||||
let meshService = BluetoothMeshService()
|
let meshService = BluetoothMeshService()
|
||||||
private let userDefaults = UserDefaults.standard
|
private let userDefaults = UserDefaults.standard
|
||||||
private let nicknameKey = "bitchat.nickname"
|
private let nicknameKey = "bitchat.nickname"
|
||||||
private var nicknameSaveTimer: Timer?
|
private var nicknameSaveTimer: Timer?
|
||||||
|
private var notificationTimer: Timer?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadNickname()
|
loadNickname()
|
||||||
@@ -101,6 +103,17 @@ class ChatViewModel: ObservableObject {
|
|||||||
return nicknames.first(where: { $0.value == nickname })?.key
|
return nicknames.first(where: { $0.value == nickname })?.key
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showPrivateMessageNotification(from sender: String, content: String) {
|
||||||
|
// Show notification
|
||||||
|
privateMessageNotification = (sender: sender, message: content)
|
||||||
|
|
||||||
|
// Auto-dismiss after 3 seconds
|
||||||
|
notificationTimer?.invalidate()
|
||||||
|
notificationTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: false) { [weak self] _ in
|
||||||
|
self?.privateMessageNotification = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func formatTimestamp(_ date: Date) -> String {
|
func formatTimestamp(_ date: Date) -> String {
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
@@ -173,6 +186,9 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
if selectedPrivateChatPeer != peerID {
|
if selectedPrivateChatPeer != peerID {
|
||||||
unreadPrivateMessages.insert(peerID)
|
unreadPrivateMessages.insert(peerID)
|
||||||
print("[DEBUG] Added unread message indicator for peer: \(peerID)")
|
print("[DEBUG] Added unread message indicator for peer: \(peerID)")
|
||||||
|
|
||||||
|
// Show notification banner
|
||||||
|
showPrivateMessageNotification(from: message.sender, content: message.content)
|
||||||
}
|
}
|
||||||
} else if message.sender == nickname {
|
} else if message.sender == nickname {
|
||||||
// Our own message - find recipient by nickname
|
// Our own message - find recipient by nickname
|
||||||
@@ -190,9 +206,16 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
// Haptic feedback for new messages
|
// Different haptic feedback for private vs public messages
|
||||||
|
if message.isPrivate && message.sender != nickname {
|
||||||
|
// Medium haptic for private messages
|
||||||
|
let impactFeedback = UIImpactFeedbackGenerator(style: .medium)
|
||||||
|
impactFeedback.impactOccurred()
|
||||||
|
} else {
|
||||||
|
// Light haptic for public messages
|
||||||
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
|
let impactFeedback = UIImpactFeedbackGenerator(style: .light)
|
||||||
impactFeedback.impactOccurred()
|
impactFeedback.impactOccurred()
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
ZStack {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
headerView
|
headerView
|
||||||
Divider()
|
Divider()
|
||||||
@@ -29,6 +30,40 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
// Private message notification overlay
|
||||||
|
if let notification = viewModel.privateMessageNotification {
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Message from \(notification.sender)")
|
||||||
|
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
||||||
|
.foregroundColor(.white)
|
||||||
|
Text(notification.message)
|
||||||
|
.font(.system(size: 12, design: .monospaced))
|
||||||
|
.foregroundColor(.white.opacity(0.9))
|
||||||
|
.lineLimit(2)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.background(Color.orange)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.shadow(radius: 4)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.onTapGesture {
|
||||||
|
if let peerID = viewModel.getPeerIDForNickname(notification.sender) {
|
||||||
|
viewModel.startPrivateChat(with: peerID)
|
||||||
|
viewModel.privateMessageNotification = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.top, 60)
|
||||||
|
.transition(.move(edge: .top).combined(with: .opacity))
|
||||||
|
.animation(.easeInOut, value: viewModel.privateMessageNotification)
|
||||||
|
}
|
||||||
|
}
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
.frame(minWidth: 600, minHeight: 400)
|
.frame(minWidth: 600, minHeight: 400)
|
||||||
#endif
|
#endif
|
||||||
@@ -39,7 +74,6 @@ struct ContentView: View {
|
|||||||
if let privatePeerID = viewModel.selectedPrivateChatPeer,
|
if let privatePeerID = viewModel.selectedPrivateChatPeer,
|
||||||
let privatePeerNick = viewModel.meshService.getPeerNicknames()[privatePeerID] {
|
let privatePeerNick = viewModel.meshService.getPeerNicknames()[privatePeerID] {
|
||||||
// Private chat header
|
// Private chat header
|
||||||
HStack {
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
viewModel.endPrivateChat()
|
viewModel.endPrivateChat()
|
||||||
}) {
|
}) {
|
||||||
@@ -70,7 +104,6 @@ struct ContentView: View {
|
|||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
}
|
}
|
||||||
.opacity(0)
|
.opacity(0)
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Public chat header
|
// Public chat header
|
||||||
Text("bitchat")
|
Text("bitchat")
|
||||||
@@ -103,8 +136,8 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(height: 44) // Fixed height to prevent bouncing
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
.padding(.vertical, 8)
|
|
||||||
.background(backgroundColor.opacity(0.95))
|
.background(backgroundColor.opacity(0.95))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user