mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 09:05:20 +00:00
Merge pull request #34 from nelsoncampoos/fix/message-duplication-sync-improvements
Fix message duplication and improve sync performance in mesh network
This commit is contained in:
@@ -101,8 +101,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
private var advertisingTimer: Timer? // Timer for interval-based advertising
|
private var advertisingTimer: Timer? // Timer for interval-based advertising
|
||||||
|
|
||||||
// Timing randomization for privacy
|
// Timing randomization for privacy
|
||||||
private let minMessageDelay: TimeInterval = 0.05 // 50ms minimum
|
private let minMessageDelay: TimeInterval = 0.01 // 10ms minimum for faster sync
|
||||||
private let maxMessageDelay: TimeInterval = 0.5 // 500ms maximum
|
private let maxMessageDelay: TimeInterval = 0.1 // 100ms maximum for faster sync
|
||||||
|
|
||||||
// Fragment handling
|
// Fragment handling
|
||||||
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
private var incomingFragments: [String: [Int: Data]] = [:] // fragmentID -> [index: data]
|
||||||
@@ -372,7 +372,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send initial announces after services are ready
|
// Send initial announces after services are ready
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { [weak self] in
|
||||||
self?.sendBroadcastAnnounce()
|
self?.sendBroadcastAnnounce()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Send multiple times for reliability with jittered delays
|
// Send multiple times for reliability with jittered delays
|
||||||
for baseDelay in [0.5, 1.0, 2.0] {
|
for baseDelay in [0.2, 0.5, 1.0] {
|
||||||
let jitteredDelay = baseDelay + self.randomDelay()
|
let jitteredDelay = baseDelay + self.randomDelay()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + jitteredDelay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + jitteredDelay) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -495,10 +495,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
self.characteristic = characteristic
|
self.characteristic = characteristic
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMessage(_ content: String, mentions: [String] = [], channel: String? = nil, to recipientID: String? = nil) {
|
func sendMessage(_ content: String, mentions: [String] = [], channel: String? = nil, to recipientID: String? = nil, messageID: String? = nil, timestamp: Date? = nil) {
|
||||||
// Defensive check for empty content
|
// Defensive check for empty content
|
||||||
guard !content.isEmpty else { return }
|
guard !content.isEmpty else { return }
|
||||||
|
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@@ -506,9 +505,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
let senderNick = nickname?.nickname ?? self.myPeerID
|
let senderNick = nickname?.nickname ?? self.myPeerID
|
||||||
|
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
|
id: messageID,
|
||||||
sender: senderNick,
|
sender: senderNick,
|
||||||
content: content,
|
content: content,
|
||||||
timestamp: Date(),
|
timestamp: timestamp ?? Date(),
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -818,7 +818,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendEncryptedChannelMessage(_ content: String, mentions: [String], channel: String, channelKey: SymmetricKey) {
|
func sendEncryptedChannelMessage(_ content: String, mentions: [String], channel: String, channelKey: SymmetricKey, messageID: String? = nil, timestamp: Date? = nil) {
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
@@ -839,9 +839,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Create message with encrypted content
|
// Create message with encrypted content
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
|
id: messageID,
|
||||||
sender: senderNick,
|
sender: senderNick,
|
||||||
content: "", // Empty placeholder since actual content is encrypted
|
content: "", // Empty placeholder since actual content is encrypted
|
||||||
timestamp: Date(),
|
timestamp: timestamp ?? Date(),
|
||||||
isRelay: false,
|
isRelay: false,
|
||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: false,
|
isPrivate: false,
|
||||||
@@ -1221,7 +1222,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
|
|
||||||
// Send cached messages with slight delay between each
|
// Send cached messages with slight delay between each
|
||||||
for (index, storedMessage) in messagesToSend.enumerated() {
|
for (index, storedMessage) in messagesToSend.enumerated() {
|
||||||
let delay = Double(index) * 0.1 // 100ms between messages
|
let delay = Double(index) * 0.02 // 20ms between messages for faster sync
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak peripheral] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak peripheral] in
|
||||||
guard let peripheral = peripheral,
|
guard let peripheral = peripheral,
|
||||||
@@ -1275,8 +1276,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
private func broadcastPacket(_ packet: BitchatPacket) {
|
private func broadcastPacket(_ packet: BitchatPacket) {
|
||||||
guard let data = packet.toBinaryData() else {
|
guard let data = packet.toBinaryData() else {
|
||||||
// print("[ERROR] Failed to convert packet to binary data")
|
// print("[ERROR] Failed to convert packet to binary data")
|
||||||
// Add to retry queue if this is a message packet
|
// Add to retry queue if this is a message packet AND it's our own message
|
||||||
if packet.type == MessageType.message.rawValue,
|
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8),
|
||||||
|
senderID == self.myPeerID,
|
||||||
|
packet.type == MessageType.message.rawValue,
|
||||||
let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
||||||
MessageRetryService.shared.addMessageForRetry(
|
MessageRetryService.shared.addMessageForRetry(
|
||||||
content: message.content,
|
content: message.content,
|
||||||
@@ -1284,7 +1287,10 @@ class BluetoothMeshService: NSObject {
|
|||||||
channel: message.channel,
|
channel: message.channel,
|
||||||
isPrivate: message.isPrivate,
|
isPrivate: message.isPrivate,
|
||||||
recipientPeerID: nil,
|
recipientPeerID: nil,
|
||||||
recipientNickname: message.recipientNickname
|
recipientNickname: message.recipientNickname,
|
||||||
|
channelKey: nil,
|
||||||
|
originalMessageID: message.id,
|
||||||
|
originalTimestamp: message.timestamp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -1331,29 +1337,36 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no peers received the message, add to retry queue
|
// If no peers received the message, add to retry queue ONLY if it's our own message
|
||||||
if sentToPeripherals == 0 && sentToCentrals == 0 {
|
if sentToPeripherals == 0 && sentToCentrals == 0 {
|
||||||
if packet.type == MessageType.message.rawValue,
|
// Check if this packet originated from us
|
||||||
let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8),
|
||||||
// For encrypted channel messages, we need to preserve the channel key
|
senderID == self.myPeerID {
|
||||||
var channelKeyData: Data? = nil
|
// This is our own message that failed to send
|
||||||
if let channel = message.channel, message.isEncrypted {
|
if packet.type == MessageType.message.rawValue,
|
||||||
// This is an encrypted channel message
|
let message = BitchatMessage.fromBinaryPayload(packet.payload) {
|
||||||
if let viewModel = delegate as? ChatViewModel,
|
// For encrypted channel messages, we need to preserve the channel key
|
||||||
let channelKey = viewModel.channelKeys[channel] {
|
var channelKeyData: Data? = nil
|
||||||
channelKeyData = channelKey.withUnsafeBytes { Data($0) }
|
if let channel = message.channel, message.isEncrypted {
|
||||||
|
// This is an encrypted channel message
|
||||||
|
if let viewModel = delegate as? ChatViewModel,
|
||||||
|
let channelKey = viewModel.channelKeys[channel] {
|
||||||
|
channelKeyData = channelKey.withUnsafeBytes { Data($0) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
MessageRetryService.shared.addMessageForRetry(
|
MessageRetryService.shared.addMessageForRetry(
|
||||||
content: message.content,
|
content: message.content,
|
||||||
mentions: message.mentions,
|
mentions: message.mentions,
|
||||||
channel: message.channel,
|
channel: message.channel,
|
||||||
isPrivate: message.isPrivate,
|
isPrivate: message.isPrivate,
|
||||||
recipientPeerID: nil,
|
recipientPeerID: nil,
|
||||||
recipientNickname: message.recipientNickname,
|
recipientNickname: message.recipientNickname,
|
||||||
channelKey: channelKeyData
|
channelKey: channelKeyData,
|
||||||
)
|
originalMessageID: message.id,
|
||||||
|
originalTimestamp: message.timestamp
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1759,11 +1772,9 @@ class BluetoothMeshService: NSObject {
|
|||||||
// Send announce with our nickname immediately
|
// Send announce with our nickname immediately
|
||||||
self.sendAnnouncementToPeer(senderID)
|
self.sendAnnouncementToPeer(senderID)
|
||||||
|
|
||||||
// Delay sending cached messages to ensure connection is fully established
|
// Send cached messages immediately for faster sync
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
// Check if this peer has cached messages (especially for favorites)
|
||||||
// Check if this peer has cached messages (especially for favorites)
|
self.sendCachedMessages(to: senderID)
|
||||||
self?.sendCachedMessages(to: senderID)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import CryptoKit
|
|||||||
|
|
||||||
struct RetryableMessage {
|
struct RetryableMessage {
|
||||||
let id: String
|
let id: String
|
||||||
|
let originalMessageID: String?
|
||||||
|
let originalTimestamp: Date?
|
||||||
let content: String
|
let content: String
|
||||||
let mentions: [String]?
|
let mentions: [String]?
|
||||||
let channel: String?
|
let channel: String?
|
||||||
@@ -29,7 +31,7 @@ class MessageRetryService {
|
|||||||
|
|
||||||
private var retryQueue: [RetryableMessage] = []
|
private var retryQueue: [RetryableMessage] = []
|
||||||
private var retryTimer: Timer?
|
private var retryTimer: Timer?
|
||||||
private let retryInterval: TimeInterval = 5.0 // Retry every 5 seconds
|
private let retryInterval: TimeInterval = 2.0 // Retry every 2 seconds for faster sync
|
||||||
private let maxQueueSize = 50
|
private let maxQueueSize = 50
|
||||||
|
|
||||||
weak var meshService: BluetoothMeshService?
|
weak var meshService: BluetoothMeshService?
|
||||||
@@ -55,15 +57,34 @@ class MessageRetryService {
|
|||||||
isPrivate: Bool = false,
|
isPrivate: Bool = false,
|
||||||
recipientPeerID: String? = nil,
|
recipientPeerID: String? = nil,
|
||||||
recipientNickname: String? = nil,
|
recipientNickname: String? = nil,
|
||||||
channelKey: Data? = nil
|
channelKey: Data? = nil,
|
||||||
|
originalMessageID: String? = nil,
|
||||||
|
originalTimestamp: Date? = nil
|
||||||
) {
|
) {
|
||||||
|
// Don't queue empty or whitespace-only messages
|
||||||
|
guard !content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Don't queue if we're at capacity
|
// Don't queue if we're at capacity
|
||||||
guard retryQueue.count < maxQueueSize else {
|
guard retryQueue.count < maxQueueSize else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this message is already in the queue
|
||||||
|
if let messageID = originalMessageID {
|
||||||
|
let alreadyQueued = retryQueue.contains { msg in
|
||||||
|
msg.originalMessageID == messageID
|
||||||
|
}
|
||||||
|
if alreadyQueued {
|
||||||
|
return // Don't add duplicate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let retryMessage = RetryableMessage(
|
let retryMessage = RetryableMessage(
|
||||||
id: UUID().uuidString,
|
id: UUID().uuidString,
|
||||||
|
originalMessageID: originalMessageID,
|
||||||
|
originalTimestamp: originalTimestamp,
|
||||||
content: content,
|
content: content,
|
||||||
mentions: mentions,
|
mentions: mentions,
|
||||||
channel: channel,
|
channel: channel,
|
||||||
@@ -76,10 +97,17 @@ class MessageRetryService {
|
|||||||
)
|
)
|
||||||
|
|
||||||
retryQueue.append(retryMessage)
|
retryQueue.append(retryMessage)
|
||||||
|
|
||||||
|
// Sort the queue by original timestamp to maintain message order
|
||||||
|
retryQueue.sort { (msg1, msg2) in
|
||||||
|
let time1 = msg1.originalTimestamp ?? Date.distantPast
|
||||||
|
let time2 = msg2.originalTimestamp ?? Date.distantPast
|
||||||
|
return time1 < time2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func processRetryQueue() {
|
private func processRetryQueue() {
|
||||||
guard let meshService = meshService else { return }
|
guard meshService != nil else { return }
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
var messagesToRetry: [RetryableMessage] = []
|
var messagesToRetry: [RetryableMessage] = []
|
||||||
@@ -95,95 +123,123 @@ class MessageRetryService {
|
|||||||
|
|
||||||
retryQueue = updatedQueue
|
retryQueue = updatedQueue
|
||||||
|
|
||||||
for message in messagesToRetry {
|
// Sort messages by original timestamp to maintain order
|
||||||
|
messagesToRetry.sort { (msg1, msg2) in
|
||||||
|
let time1 = msg1.originalTimestamp ?? Date.distantPast
|
||||||
|
let time2 = msg2.originalTimestamp ?? Date.distantPast
|
||||||
|
return time1 < time2
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send messages with delay to maintain order
|
||||||
|
for (index, message) in messagesToRetry.enumerated() {
|
||||||
// Check if we should still retry
|
// Check if we should still retry
|
||||||
if message.retryCount >= message.maxRetries {
|
if message.retryCount >= message.maxRetries {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connectivity before retrying
|
// Add delay between messages to ensure proper ordering
|
||||||
let viewModel = meshService.delegate as? ChatViewModel
|
let delay = Double(index) * 0.05 // 50ms between messages
|
||||||
let connectedPeers = viewModel?.connectedPeers ?? []
|
|
||||||
|
|
||||||
if message.isPrivate {
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
// For private messages, check if recipient is connected
|
guard let self = self,
|
||||||
if let recipientID = message.recipientPeerID,
|
let meshService = self.meshService else { return }
|
||||||
connectedPeers.contains(recipientID) {
|
|
||||||
// Retry private message
|
// Check connectivity before retrying
|
||||||
meshService.sendPrivateMessage(
|
let viewModel = meshService.delegate as? ChatViewModel
|
||||||
message.content,
|
let connectedPeers = viewModel?.connectedPeers ?? []
|
||||||
to: recipientID,
|
|
||||||
recipientNickname: message.recipientNickname ?? "unknown"
|
if message.isPrivate {
|
||||||
)
|
// For private messages, check if recipient is connected
|
||||||
|
if let recipientID = message.recipientPeerID,
|
||||||
|
connectedPeers.contains(recipientID) {
|
||||||
|
// Retry private message
|
||||||
|
meshService.sendPrivateMessage(
|
||||||
|
message.content,
|
||||||
|
to: recipientID,
|
||||||
|
recipientNickname: message.recipientNickname ?? "unknown",
|
||||||
|
messageID: message.originalMessageID
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Recipient not connected, keep in queue with updated retry time
|
||||||
|
var updatedMessage = message
|
||||||
|
updatedMessage = RetryableMessage(
|
||||||
|
id: message.id,
|
||||||
|
originalMessageID: message.originalMessageID,
|
||||||
|
originalTimestamp: message.originalTimestamp,
|
||||||
|
content: message.content,
|
||||||
|
mentions: message.mentions,
|
||||||
|
channel: message.channel,
|
||||||
|
isPrivate: message.isPrivate,
|
||||||
|
recipientPeerID: message.recipientPeerID,
|
||||||
|
recipientNickname: message.recipientNickname,
|
||||||
|
channelKey: message.channelKey,
|
||||||
|
retryCount: message.retryCount + 1,
|
||||||
|
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
|
||||||
|
)
|
||||||
|
self.retryQueue.append(updatedMessage)
|
||||||
|
}
|
||||||
|
} else if let channel = message.channel, let channelKeyData = message.channelKey {
|
||||||
|
// For channel messages, check if we have peers in the channel
|
||||||
|
if !connectedPeers.isEmpty {
|
||||||
|
// Recreate SymmetricKey from data
|
||||||
|
let channelKey = SymmetricKey(data: channelKeyData)
|
||||||
|
meshService.sendEncryptedChannelMessage(
|
||||||
|
message.content,
|
||||||
|
mentions: message.mentions ?? [],
|
||||||
|
channel: channel,
|
||||||
|
channelKey: channelKey,
|
||||||
|
messageID: message.originalMessageID,
|
||||||
|
timestamp: message.originalTimestamp
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// No peers connected, keep in queue
|
||||||
|
var updatedMessage = message
|
||||||
|
updatedMessage = RetryableMessage(
|
||||||
|
id: message.id,
|
||||||
|
originalMessageID: message.originalMessageID,
|
||||||
|
originalTimestamp: message.originalTimestamp,
|
||||||
|
content: message.content,
|
||||||
|
mentions: message.mentions,
|
||||||
|
channel: message.channel,
|
||||||
|
isPrivate: message.isPrivate,
|
||||||
|
recipientPeerID: message.recipientPeerID,
|
||||||
|
recipientNickname: message.recipientNickname,
|
||||||
|
channelKey: message.channelKey,
|
||||||
|
retryCount: message.retryCount + 1,
|
||||||
|
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
|
||||||
|
)
|
||||||
|
self.retryQueue.append(updatedMessage)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Recipient not connected, keep in queue with updated retry time
|
// Regular message
|
||||||
var updatedMessage = message
|
if !connectedPeers.isEmpty {
|
||||||
updatedMessage = RetryableMessage(
|
meshService.sendMessage(
|
||||||
id: message.id,
|
message.content,
|
||||||
content: message.content,
|
mentions: message.mentions ?? [],
|
||||||
mentions: message.mentions,
|
channel: message.channel,
|
||||||
channel: message.channel,
|
to: nil,
|
||||||
isPrivate: message.isPrivate,
|
messageID: message.originalMessageID,
|
||||||
recipientPeerID: message.recipientPeerID,
|
timestamp: message.originalTimestamp
|
||||||
recipientNickname: message.recipientNickname,
|
)
|
||||||
channelKey: message.channelKey,
|
} else {
|
||||||
retryCount: message.retryCount + 1,
|
// No peers connected, keep in queue
|
||||||
nextRetryTime: Date().addingTimeInterval(retryInterval * Double(message.retryCount + 2))
|
var updatedMessage = message
|
||||||
)
|
updatedMessage = RetryableMessage(
|
||||||
retryQueue.append(updatedMessage)
|
id: message.id,
|
||||||
}
|
originalMessageID: message.originalMessageID,
|
||||||
} else if let channel = message.channel, let channelKeyData = message.channelKey {
|
originalTimestamp: message.originalTimestamp,
|
||||||
// For channel messages, check if we have peers in the channel
|
content: message.content,
|
||||||
if !connectedPeers.isEmpty {
|
mentions: message.mentions,
|
||||||
// Recreate SymmetricKey from data
|
channel: message.channel,
|
||||||
let channelKey = SymmetricKey(data: channelKeyData)
|
isPrivate: message.isPrivate,
|
||||||
meshService.sendEncryptedChannelMessage(
|
recipientPeerID: message.recipientPeerID,
|
||||||
message.content,
|
recipientNickname: message.recipientNickname,
|
||||||
mentions: message.mentions ?? [],
|
channelKey: message.channelKey,
|
||||||
channel: channel,
|
retryCount: message.retryCount + 1,
|
||||||
channelKey: channelKey
|
nextRetryTime: Date().addingTimeInterval(self.retryInterval * Double(message.retryCount + 2))
|
||||||
)
|
)
|
||||||
} else {
|
self.retryQueue.append(updatedMessage)
|
||||||
// No peers connected, keep in queue
|
}
|
||||||
var updatedMessage = message
|
|
||||||
updatedMessage = RetryableMessage(
|
|
||||||
id: message.id,
|
|
||||||
content: message.content,
|
|
||||||
mentions: message.mentions,
|
|
||||||
channel: message.channel,
|
|
||||||
isPrivate: message.isPrivate,
|
|
||||||
recipientPeerID: message.recipientPeerID,
|
|
||||||
recipientNickname: message.recipientNickname,
|
|
||||||
channelKey: message.channelKey,
|
|
||||||
retryCount: message.retryCount + 1,
|
|
||||||
nextRetryTime: Date().addingTimeInterval(retryInterval * Double(message.retryCount + 2))
|
|
||||||
)
|
|
||||||
retryQueue.append(updatedMessage)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Regular message
|
|
||||||
if !connectedPeers.isEmpty {
|
|
||||||
meshService.sendMessage(
|
|
||||||
message.content,
|
|
||||||
mentions: message.mentions ?? [],
|
|
||||||
channel: message.channel
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// No peers connected, keep in queue
|
|
||||||
var updatedMessage = message
|
|
||||||
updatedMessage = RetryableMessage(
|
|
||||||
id: message.id,
|
|
||||||
content: message.content,
|
|
||||||
mentions: message.mentions,
|
|
||||||
channel: message.channel,
|
|
||||||
isPrivate: message.isPrivate,
|
|
||||||
recipientPeerID: message.recipientPeerID,
|
|
||||||
recipientNickname: message.recipientNickname,
|
|
||||||
channelKey: message.channelKey,
|
|
||||||
retryCount: message.retryCount + 1,
|
|
||||||
nextRetryTime: Date().addingTimeInterval(retryInterval * Double(message.retryCount + 2))
|
|
||||||
)
|
|
||||||
retryQueue.append(updatedMessage)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2750,8 +2750,42 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
if channelMessages[channel] == nil {
|
if channelMessages[channel] == nil {
|
||||||
channelMessages[channel] = []
|
channelMessages[channel] = []
|
||||||
}
|
}
|
||||||
channelMessages[channel]?.append(finalMessage)
|
|
||||||
channelMessages[channel]?.sort { $0.timestamp < $1.timestamp }
|
// Check if this is our own message being echoed back
|
||||||
|
if finalMessage.sender != nickname && finalMessage.sender != "system" {
|
||||||
|
// Skip empty or whitespace-only messages
|
||||||
|
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
channelMessages[channel]?.append(finalMessage)
|
||||||
|
channelMessages[channel]?.sort { $0.timestamp < $1.timestamp }
|
||||||
|
}
|
||||||
|
} else if finalMessage.sender != "system" {
|
||||||
|
// Our own message - check if we already have it (by ID and content)
|
||||||
|
let messageExists = channelMessages[channel]?.contains { existingMsg in
|
||||||
|
// Check by ID first
|
||||||
|
if existingMsg.id == finalMessage.id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check by content and sender with time window (within 1 second)
|
||||||
|
if existingMsg.content == finalMessage.content &&
|
||||||
|
existingMsg.sender == finalMessage.sender {
|
||||||
|
let timeDiff = abs(existingMsg.timestamp.timeIntervalSince(finalMessage.timestamp))
|
||||||
|
return timeDiff < 1.0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
} ?? false
|
||||||
|
if !messageExists {
|
||||||
|
// This is a message we sent from another device or it's missing locally
|
||||||
|
// Skip empty or whitespace-only messages
|
||||||
|
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
channelMessages[channel]?.append(finalMessage)
|
||||||
|
channelMessages[channel]?.sort { $0.timestamp < $1.timestamp }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// System message - always add
|
||||||
|
channelMessages[channel]?.append(finalMessage)
|
||||||
|
channelMessages[channel]?.sort { $0.timestamp < $1.timestamp }
|
||||||
|
}
|
||||||
|
|
||||||
// Save message if channel has retention enabled
|
// Save message if channel has retention enabled
|
||||||
if retentionEnabledChannels.contains(channel) {
|
if retentionEnabledChannels.contains(channel) {
|
||||||
@@ -2767,8 +2801,8 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
} else {
|
} else {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update unread count if not currently viewing this channel
|
// Update unread count if not currently viewing this channel and it's not our own message
|
||||||
if currentChannel != channel {
|
if currentChannel != channel && finalMessage.sender != nickname {
|
||||||
unreadChannelMessages[channel] = (unreadChannelMessages[channel] ?? 0) + 1
|
unreadChannelMessages[channel] = (unreadChannelMessages[channel] ?? 0) + 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2782,9 +2816,10 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
(message.content.contains("🫂") || message.content.contains("🐟") ||
|
(message.content.contains("🫂") || message.content.contains("🐟") ||
|
||||||
message.content.contains("took a screenshot"))
|
message.content.contains("took a screenshot"))
|
||||||
|
|
||||||
|
let finalMessage: BitchatMessage
|
||||||
if isActionMessage {
|
if isActionMessage {
|
||||||
// Convert to system message
|
// Convert to system message
|
||||||
let systemMessage = BitchatMessage(
|
finalMessage = BitchatMessage(
|
||||||
sender: "system",
|
sender: "system",
|
||||||
content: String(message.content.dropFirst(2).dropLast(2)), // Remove * * wrapper
|
content: String(message.content.dropFirst(2).dropLast(2)), // Remove * * wrapper
|
||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
@@ -2796,12 +2831,46 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
mentions: message.mentions,
|
mentions: message.mentions,
|
||||||
channel: message.channel
|
channel: message.channel
|
||||||
)
|
)
|
||||||
messages.append(systemMessage)
|
|
||||||
} else {
|
} else {
|
||||||
messages.append(message)
|
finalMessage = message
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is our own message being echoed back
|
||||||
|
if finalMessage.sender != nickname && finalMessage.sender != "system" {
|
||||||
|
// Skip empty or whitespace-only messages
|
||||||
|
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
messages.append(finalMessage)
|
||||||
|
// Sort messages by timestamp to ensure proper ordering
|
||||||
|
messages.sort { $0.timestamp < $1.timestamp }
|
||||||
|
}
|
||||||
|
} else if finalMessage.sender != "system" {
|
||||||
|
// Our own message - check if we already have it (by ID and content)
|
||||||
|
let messageExists = messages.contains { existingMsg in
|
||||||
|
// Check by ID first
|
||||||
|
if existingMsg.id == finalMessage.id {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check by content and sender with time window (within 1 second)
|
||||||
|
if existingMsg.content == finalMessage.content &&
|
||||||
|
existingMsg.sender == finalMessage.sender {
|
||||||
|
let timeDiff = abs(existingMsg.timestamp.timeIntervalSince(finalMessage.timestamp))
|
||||||
|
return timeDiff < 1.0
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !messageExists {
|
||||||
|
// This is a message we sent from another device or it's missing locally
|
||||||
|
// Skip empty or whitespace-only messages
|
||||||
|
if !finalMessage.content.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||||
|
messages.append(finalMessage)
|
||||||
|
messages.sort { $0.timestamp < $1.timestamp }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// System message - always add
|
||||||
|
messages.append(finalMessage)
|
||||||
|
messages.sort { $0.timestamp < $1.timestamp }
|
||||||
}
|
}
|
||||||
// Sort messages by timestamp to ensure proper ordering
|
|
||||||
messages.sort { $0.timestamp < $1.timestamp }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we're mentioned
|
// Check if we're mentioned
|
||||||
|
|||||||
Reference in New Issue
Block a user