From b61904bee98c1dda7abd23356effc37a4363a7b2 Mon Sep 17 00:00:00 2001 From: jack Date: Wed, 16 Jul 2025 15:54:40 +0200 Subject: [PATCH] Remove message retention and /save command - Delete MessageRetentionService.swift - Remove retentionEnabledChannels from ChatViewModel - Remove /save command handling - Remove retention UI elements from ContentView - Remove channelRetention message type from protocol - Update documentation and tests --- README.md | 2 - bitchat.xcodeproj/project.pbxproj | 6 - bitchat/Protocols/BitchatProtocol.swift | 2 - bitchat/Services/BluetoothMeshService.swift | 44 ---- .../Services/MessageRetentionService.swift | 222 ------------------ bitchat/ViewModels/ChatViewModel.swift | 205 +--------------- bitchat/Views/ContentView.swift | 25 -- bitchatTests/KeychainIntegrationTests.swift | 2 - .../NoiseIdentityPersistenceTests.swift | 26 +- 9 files changed, 2 insertions(+), 532 deletions(-) delete mode 100644 bitchat/Services/MessageRetentionService.swift diff --git a/README.md b/README.md index e6ad0747..2e3346cd 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,6 @@ This project is released into the public domain. See the [LICENSE](LICENSE) file - `/clear` - Clear chat messages - `/pass [password]` - Set/change channel password (owner only) - `/transfer @name` - Transfer channel ownership -- `/save` - Toggle message retention for channel (owner only) ### Getting Started @@ -89,7 +88,6 @@ This project is released into the public domain. See the [LICENSE](LICENSE) file ### Channel Features - **Password Protection**: Channel owners can set passwords with `/pass` -- **Message Retention**: Owners can enable mandatory message saving with `/save` - **@ Mentions**: Use `@nickname` to mention users (with autocomplete) - **Ownership Transfer**: Pass control to trusted users with `/transfer` diff --git a/bitchat.xcodeproj/project.pbxproj b/bitchat.xcodeproj/project.pbxproj index 57061424..abc6b911 100644 --- a/bitchat.xcodeproj/project.pbxproj +++ b/bitchat.xcodeproj/project.pbxproj @@ -72,9 +72,7 @@ 24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; }; 2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; }; 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; }; - 4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; }; 4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; }; - 4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; }; 5D95F2BFBE257A1225998389 /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; }; 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; }; 6DE056E1EE9850E9FBF50157 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; }; @@ -193,7 +191,6 @@ 527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = ""; }; 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = ""; }; 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.extensionkit-extension"; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetentionService.swift; sourceTree = ""; }; 6E2446380E7A44E49A35B664 /* IdentityModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityModels.swift; sourceTree = ""; }; 763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = ""; }; 8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = ""; }; @@ -367,7 +364,6 @@ D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */, 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */, 136696FC4436A02D98CE6A77 /* KeychainManager.swift */, - 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */, AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */, 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */, ); @@ -580,7 +576,6 @@ 31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */, 04B6BA572E203D6C0090FE39 /* FingerprintView.swift in Sources */, 04B6BA582E203D6C0090FE39 /* NoiseTestingView.swift in Sources */, - 4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */, C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */, 749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */, 0245710AEAA58AD0A1425234 /* OptimizedBloomFilter.swift in Sources */, @@ -617,7 +612,6 @@ 7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */, 04B6BA552E203D6C0090FE39 /* FingerprintView.swift in Sources */, 04B6BA562E203D6C0090FE39 /* NoiseTestingView.swift in Sources */, - 4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */, CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */, 61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */, 1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */, diff --git a/bitchat/Protocols/BitchatProtocol.swift b/bitchat/Protocols/BitchatProtocol.swift index 954feafa..677584d8 100644 --- a/bitchat/Protocols/BitchatProtocol.swift +++ b/bitchat/Protocols/BitchatProtocol.swift @@ -84,7 +84,6 @@ enum MessageType: UInt8 { case fragmentContinue = 0x06 case fragmentEnd = 0x07 case channelAnnounce = 0x08 // Announce password-protected channel status - case channelRetention = 0x09 // Announce channel retention status case deliveryAck = 0x0A // Acknowledge message received case deliveryStatusRequest = 0x0B // Request delivery status update case readReceipt = 0x0C // Message has been read/viewed @@ -112,7 +111,6 @@ enum MessageType: UInt8 { case .fragmentContinue: return "fragmentContinue" case .fragmentEnd: return "fragmentEnd" case .channelAnnounce: return "channelAnnounce" - case .channelRetention: return "channelRetention" case .deliveryAck: return "deliveryAck" case .deliveryStatusRequest: return "deliveryStatusRequest" case .readReceipt: return "readReceipt" diff --git a/bitchat/Services/BluetoothMeshService.swift b/bitchat/Services/BluetoothMeshService.swift index 39ec1180..25dc39c0 100644 --- a/bitchat/Services/BluetoothMeshService.swift +++ b/bitchat/Services/BluetoothMeshService.swift @@ -1104,27 +1104,6 @@ class BluetoothMeshService: NSObject { } } - func sendChannelRetentionAnnouncement(_ channel: String, enabled: Bool) { - messageQueue.async { [weak self] in - guard let self = self else { return } - - // Payload format: channel|enabled|creatorID - let enabledFlag = enabled ? "1" : "0" - let payload = "\(channel)|\(enabledFlag)|\(self.myPeerID)" - - let packet = BitchatPacket( - type: MessageType.channelRetention.rawValue, - senderID: Data(hexString: self.myPeerID) ?? Data(), - recipientID: SpecialRecipients.broadcast, - timestamp: UInt64(Date().timeIntervalSince1970 * 1000), - payload: Data(payload.utf8), - signature: nil, - ttl: 5 // Allow wider propagation for channel announcements - ) - - self.broadcastPacket(packet) - } - } func sendEncryptedChannelMessage(_ content: String, mentions: [String], channel: String, channelKey: SymmetricKey, messageID: String? = nil, timestamp: Date? = nil) { messageQueue.async { [weak self] in @@ -2282,29 +2261,6 @@ class BluetoothMeshService: NSObject { self.broadcastPacket(relayPacket) } - case .channelRetention: - if let payloadStr = String(data: packet.payload, encoding: .utf8) { - // Parse payload: channel|enabled|creatorID - let components = payloadStr.split(separator: "|").map(String.init) - if components.count >= 3 { - let channel = components[0] - let enabled = components[1] == "1" - let creatorID = components[2] - - - DispatchQueue.main.async { - self.delegate?.didReceiveChannelRetentionAnnouncement(channel, enabled: enabled, creatorID: creatorID) - } - - // Relay announcement - if packet.ttl > 1 { - var relayPacket = packet - relayPacket.ttl -= 1 - self.broadcastPacket(relayPacket) - } - } - } - case .readReceipt: // Handle read receipt if let recipientIDData = packet.recipientID, diff --git a/bitchat/Services/MessageRetentionService.swift b/bitchat/Services/MessageRetentionService.swift deleted file mode 100644 index 2cdeb419..00000000 --- a/bitchat/Services/MessageRetentionService.swift +++ /dev/null @@ -1,222 +0,0 @@ -// -// MessageRetentionService.swift -// bitchat -// -// This is free and unencumbered software released into the public domain. -// For more information, see -// - -import Foundation -import CryptoKit - -struct StoredMessage: Codable { - let id: String - let sender: String - let senderPeerID: String? - let content: String - let timestamp: Date - let channelTag: String? - let isPrivate: Bool - let recipientPeerID: String? -} - -class MessageRetentionService { - static let shared = MessageRetentionService() - - private let documentsDirectory: URL - private let messagesDirectory: URL - private let favoriteChannelsKey = "bitchat.favoriteChannels" - private let retentionDays = 7 // Messages retained for 7 days - private let encryptionKey: SymmetricKey - - private init() { - // Get documents directory with fallback to temp directory - if let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - documentsDirectory = docsDir - } else { - // Fallback to temporary directory if documents directory is not accessible - documentsDirectory = FileManager.default.temporaryDirectory - SecurityLogger.log("Using temporary directory for message retention", - category: SecurityLogger.security, level: .warning) - } - messagesDirectory = documentsDirectory.appendingPathComponent("Messages", isDirectory: true) - - // Create messages directory if it doesn't exist - try? FileManager.default.createDirectory(at: messagesDirectory, withIntermediateDirectories: true) - - // Generate or retrieve encryption key from keychain - let loadedKey: SymmetricKey - - // Try to load from keychain - if let keyData = KeychainManager.shared.getIdentityKey(forKey: "messageRetentionKey") { - loadedKey = SymmetricKey(data: keyData) - } - // Generate new key if needed - else { - loadedKey = SymmetricKey(size: .bits256) - let keyData = loadedKey.withUnsafeBytes { Data($0) } - // Save to keychain - _ = KeychainManager.shared.saveIdentityKey(keyData, forKey: "messageRetentionKey") - } - - // Now assign the final value - self.encryptionKey = loadedKey - - // Clean up old messages on init - cleanupOldMessages() - } - - // MARK: - Favorite Channels Management - - func getFavoriteChannels() -> Set { - let channels = UserDefaults.standard.stringArray(forKey: favoriteChannelsKey) ?? [] - return Set(channels) - } - - func toggleFavoriteChannel(_ channel: String) -> Bool { - var favorites = getFavoriteChannels() - if favorites.contains(channel) { - favorites.remove(channel) - // Clean up messages for this channel - deleteMessagesForChannel(channel) - } else { - favorites.insert(channel) - } - UserDefaults.standard.set(Array(favorites), forKey: favoriteChannelsKey) - return favorites.contains(channel) - } - - // MARK: - Message Storage - - func saveMessage(_ message: BitchatMessage, forChannel channel: String?) { - // Only save messages for favorite channels - guard let channel = channel ?? message.channel, - getFavoriteChannels().contains(channel) else { - return - } - - // Convert to StoredMessage - let storedMessage = StoredMessage( - id: message.id, - sender: message.sender, - senderPeerID: message.senderPeerID, - content: message.content, - timestamp: message.timestamp, - channelTag: message.channel, - isPrivate: message.isPrivate, - recipientPeerID: message.senderPeerID - ) - - // Encode message - guard let messageData = try? JSONEncoder().encode(storedMessage) else { return } - - // Encrypt message - guard let encryptedData = encrypt(messageData) else { return } - - // Save to file - let fileName = "\(channel)_\(message.timestamp.timeIntervalSince1970)_\(message.id).enc" - let fileURL = messagesDirectory.appendingPathComponent(fileName) - - try? encryptedData.write(to: fileURL) - } - - func loadMessagesForChannel(_ channel: String) -> [BitchatMessage] { - guard getFavoriteChannels().contains(channel) else { return [] } - - var messages: [BitchatMessage] = [] - - do { - let files = try FileManager.default.contentsOfDirectory(at: messagesDirectory, includingPropertiesForKeys: nil) - let channelFiles = files.filter { $0.lastPathComponent.hasPrefix("\(channel)_") } - - for fileURL in channelFiles { - if let encryptedData = try? Data(contentsOf: fileURL), - let decryptedData = decrypt(encryptedData), - let storedMessage = try? JSONDecoder().decode(StoredMessage.self, from: decryptedData) { - - let message = BitchatMessage( - sender: storedMessage.sender, - content: storedMessage.content, - timestamp: storedMessage.timestamp, - isRelay: false, - originalSender: nil, - isPrivate: storedMessage.isPrivate, - recipientNickname: nil, - senderPeerID: storedMessage.senderPeerID, - mentions: nil, - channel: storedMessage.channelTag - ) - - messages.append(message) - } - } - } catch { - } - - return messages.sorted { $0.timestamp < $1.timestamp } - } - - // MARK: - Encryption - - private func encrypt(_ data: Data) -> Data? { - do { - let sealedBox = try AES.GCM.seal(data, using: encryptionKey) - return sealedBox.combined - } catch { - return nil - } - } - - private func decrypt(_ data: Data) -> Data? { - do { - let sealedBox = try AES.GCM.SealedBox(combined: data) - return try AES.GCM.open(sealedBox, using: encryptionKey) - } catch { - return nil - } - } - - // MARK: - Cleanup - - private func cleanupOldMessages() { - let cutoffDate = Date().addingTimeInterval(-TimeInterval(retentionDays * 24 * 60 * 60)) - - do { - let files = try FileManager.default.contentsOfDirectory(at: messagesDirectory, includingPropertiesForKeys: [.creationDateKey]) - - for fileURL in files { - if let attributes = try? fileURL.resourceValues(forKeys: [.creationDateKey]), - let creationDate = attributes.creationDate, - creationDate < cutoffDate { - try? FileManager.default.removeItem(at: fileURL) - } - } - } catch { - } - } - - func deleteMessagesForChannel(_ channel: String) { - do { - let files = try FileManager.default.contentsOfDirectory(at: messagesDirectory, includingPropertiesForKeys: nil) - let channelFiles = files.filter { $0.lastPathComponent.hasPrefix("\(channel)_") } - - for fileURL in channelFiles { - try? FileManager.default.removeItem(at: fileURL) - } - } catch { - } - } - - func deleteAllStoredMessages() { - do { - let files = try FileManager.default.contentsOfDirectory(at: messagesDirectory, includingPropertiesForKeys: nil) - for fileURL in files { - try? FileManager.default.removeItem(at: fileURL) - } - } catch { - } - - // Clear favorite channels - UserDefaults.standard.removeObject(forKey: favoriteChannelsKey) - } -} diff --git a/bitchat/ViewModels/ChatViewModel.swift b/bitchat/ViewModels/ChatViewModel.swift index 7516c858..6b27676f 100644 --- a/bitchat/ViewModels/ChatViewModel.swift +++ b/bitchat/ViewModels/ChatViewModel.swift @@ -57,8 +57,6 @@ class ChatViewModel: ObservableObject { @Published var channelMetadata: [String: ChannelMetadata] = [:] // channel -> metadata from creator @Published var showPasswordPrompt: Bool = false @Published var passwordPromptChannel: String? = nil - @Published var savedChannels: Set = [] // Channels saved for message retention - @Published var retentionEnabledChannels: Set = [] // Channels where owner enabled retention for all members @Published var channelVerificationStatus: [String: ChannelVerificationStatus] = [:] // Track verification status var meshService = BluetoothMeshService() @@ -69,7 +67,6 @@ class ChatViewModel: ObservableObject { private let channelCreatorsKey = "bitchat.channelCreators" // private let channelPasswordsKey = "bitchat.channelPasswords" // Now using Keychain private let channelKeyCommitmentsKey = "bitchat.channelKeyCommitments" - private let retentionEnabledChannelsKey = "bitchat.retentionEnabledChannels" private var nicknameSaveTimer: Timer? @Published var favoritePeers: Set = [] // Now stores public key fingerprints instead of peer IDs @@ -96,8 +93,6 @@ class ChatViewModel: ObservableObject { loadChannelData() loadBlockedUsers() loadVerifiedFingerprints() - // Load saved channels state - savedChannels = MessageRetentionService.shared.getFavoriteChannels() meshService.delegate = self // Log startup info @@ -252,14 +247,6 @@ class ChatViewModel: ObservableObject { if channelMembers[channel] == nil { channelMembers[channel] = Set() } - - // Load saved messages if this channel has retention enabled - if retentionEnabledChannels.contains(channel) { - let savedMessages = MessageRetentionService.shared.loadMessagesForChannel(channel) - if !savedMessages.isEmpty { - channelMessages[channel] = savedMessages - } - } } } } @@ -291,11 +278,6 @@ class ChatViewModel: ObservableObject { channelKeyCommitments = savedCommitments } - // Load retention-enabled channels - if let savedRetentionChannels = userDefaults.stringArray(forKey: retentionEnabledChannelsKey) { - retentionEnabledChannels = Set(savedRetentionChannels) - } - // Load channel passwords from Keychain let savedPasswords = KeychainManager.shared.getAllChannelPasswords() channelPasswords = savedPasswords @@ -322,7 +304,6 @@ class ChatViewModel: ObservableObject { _ = KeychainManager.shared.saveChannelPassword(password, for: channel) } userDefaults.set(channelKeyCommitments, forKey: channelKeyCommitmentsKey) - userDefaults.set(Array(retentionEnabledChannels), forKey: retentionEnabledChannelsKey) // Force synchronize and add a small delay to ensure writes complete _ = userDefaults.synchronize() @@ -576,23 +557,6 @@ class ChatViewModel: ObservableObject { channelMessages[channelTag] = [] } - // Load saved messages if this is a favorite channel - if MessageRetentionService.shared.getFavoriteChannels().contains(channelTag) { - let savedMessages = MessageRetentionService.shared.loadMessagesForChannel(channelTag) - if !savedMessages.isEmpty { - // Merge saved messages with current messages, avoiding duplicates - var existingMessageIDs = Set(channelMessages[channelTag]?.map { $0.id } ?? []) - for savedMessage in savedMessages { - if !existingMessageIDs.contains(savedMessage.id) { - channelMessages[channelTag]?.append(savedMessage) - existingMessageIDs.insert(savedMessage.id) - } - } - // Sort by timestamp - channelMessages[channelTag]?.sort { $0.timestamp < $1.timestamp } - } - } - // Hide password prompt if it was showing showPasswordPrompt = false passwordPromptChannel = nil @@ -1335,11 +1299,6 @@ class ChatViewModel: ObservableObject { } channelMessages[channel]?.append(message) - // Save message if channel has retention enabled - if retentionEnabledChannels.contains(channel) { - MessageRetentionService.shared.saveMessage(message, forChannel: channel) - } - // Track ourselves as a channel member if channelMembers[channel] == nil { channelMembers[channel] = Set() @@ -1747,11 +1706,6 @@ class ChatViewModel: ObservableObject { verifiedFingerprints.removeAll() // Verified fingerprints are cleared when identity data is cleared below - // Clear all retained messages - MessageRetentionService.shared.deleteAllStoredMessages() - savedChannels.removeAll() - retentionEnabledChannels.removeAll() - // Clear message retry queue MessageRetryService.shared.clearRetryQueue() @@ -1760,7 +1714,6 @@ class ChatViewModel: ObservableObject { userDefaults.removeObject(forKey: passwordProtectedChannelsKey) userDefaults.removeObject(forKey: channelCreatorsKey) userDefaults.removeObject(forKey: channelKeyCommitmentsKey) - userDefaults.removeObject(forKey: retentionEnabledChannelsKey) // Reset nickname to anonymous nickname = "anon\(Int.random(in: 1000...9999))" @@ -2478,69 +2431,7 @@ extension ChatViewModel: BitchatDelegate { } func didReceiveChannelRetentionAnnouncement(_ channel: String, enabled: Bool, creatorID: String?) { - - // Only process if we're a member of this channel - guard joinedChannels.contains(channel) else { return } - - // Verify the announcement is from the channel owner - if let creatorID = creatorID, channelCreators[channel] != creatorID { - return - } - - // Update retention status - if enabled { - retentionEnabledChannels.insert(channel) - savedChannels.insert(channel) - // Ensure channel is in favorites if not already - if !MessageRetentionService.shared.getFavoriteChannels().contains(channel) { - _ = MessageRetentionService.shared.toggleFavoriteChannel(channel) - } - - // Show system message - let systemMessage = BitchatMessage( - sender: "system", - content: "channel owner enabled message retention for \(channel). all messages will be saved locally.", - timestamp: Date(), - isRelay: false - ) - if currentChannel == channel { - messages.append(systemMessage) - } else if var channelMsgs = channelMessages[channel] { - channelMsgs.append(systemMessage) - channelMessages[channel] = channelMsgs - } else { - channelMessages[channel] = [systemMessage] - } - } else { - retentionEnabledChannels.remove(channel) - savedChannels.remove(channel) - - // Delete all saved messages for this channel - MessageRetentionService.shared.deleteMessagesForChannel(channel) - // Remove from favorites if currently set - if MessageRetentionService.shared.getFavoriteChannels().contains(channel) { - _ = MessageRetentionService.shared.toggleFavoriteChannel(channel) - } - - // Show system message - let systemMessage = BitchatMessage( - sender: "system", - content: "channel owner disabled message retention for \(channel). all saved messages have been deleted.", - timestamp: Date(), - isRelay: false - ) - if currentChannel == channel { - messages.append(systemMessage) - } else if var channelMsgs = channelMessages[channel] { - channelMsgs.append(systemMessage) - channelMessages[channel] = channelMsgs - } else { - channelMessages[channel] = [systemMessage] - } - } - - // Persist retention status - userDefaults.set(Array(retentionEnabledChannels), forKey: retentionEnabledChannelsKey) + // This feature has been removed } private func handleCommand(_ command: String) { @@ -2698,9 +2589,6 @@ extension ChatViewModel: BitchatDelegate { if passwordProtectedChannels.contains(channel) { status += " 🔒" } - if retentionEnabledChannels.contains(channel) { - status += " 📌" - } let myFingerprint = getMyFingerprint() if channelCreators[channel] == meshService.myPeerID || channelCreators[channel] == myFingerprint { status += " (owner)" @@ -2789,92 +2677,6 @@ extension ChatViewModel: BitchatDelegate { // Clear main messages messages.removeAll() } - case "/save": - // Toggle retention for current channel (owner only) - guard let channel = currentChannel else { - let systemMessage = BitchatMessage( - sender: "system", - content: "you must be in a channel to toggle message retention.", - timestamp: Date(), - isRelay: false - ) - messages.append(systemMessage) - break - } - - // Check if user is the channel owner - let myFingerprint = getMyFingerprint() - guard channelCreators[channel] == meshService.myPeerID || channelCreators[channel] == myFingerprint else { - let systemMessage = BitchatMessage( - sender: "system", - content: "only the channel owner can toggle message retention.", - timestamp: Date(), - isRelay: false - ) - messages.append(systemMessage) - break - } - - // Toggle retention status - let isEnabling = !retentionEnabledChannels.contains(channel) - - if isEnabling { - // Enable retention for this channel - retentionEnabledChannels.insert(channel) - savedChannels.insert(channel) - _ = MessageRetentionService.shared.toggleFavoriteChannel(channel) // Enable if not already - - // Announce to all members that retention is enabled - meshService.sendChannelRetentionAnnouncement(channel, enabled: true) - - let systemMessage = BitchatMessage( - sender: "system", - content: "message retention enabled for channel \(channel). all members will save messages locally.", - timestamp: Date(), - isRelay: false - ) - messages.append(systemMessage) - - // Load any previously saved messages - let savedMessages = MessageRetentionService.shared.loadMessagesForChannel(channel) - if !savedMessages.isEmpty { - // Merge saved messages with current messages, avoiding duplicates - var existingMessageIDs = Set(channelMessages[channel]?.map { $0.id } ?? []) - for savedMessage in savedMessages { - if !existingMessageIDs.contains(savedMessage.id) { - if channelMessages[channel] == nil { - channelMessages[channel] = [] - } - channelMessages[channel]?.append(savedMessage) - existingMessageIDs.insert(savedMessage.id) - } - } - // Sort by timestamp - channelMessages[channel]?.sort { $0.timestamp < $1.timestamp } - } - } else { - // Disable retention for this channel - retentionEnabledChannels.remove(channel) - savedChannels.remove(channel) - - // Delete all saved messages for this channel - MessageRetentionService.shared.deleteMessagesForChannel(channel) - _ = MessageRetentionService.shared.toggleFavoriteChannel(channel) // Disable if enabled - - // Announce to all members that retention is disabled - meshService.sendChannelRetentionAnnouncement(channel, enabled: false) - - let systemMessage = BitchatMessage( - sender: "system", - content: "message retention disabled for channel \(channel). all saved messages will be deleted on all devices.", - timestamp: Date(), - isRelay: false - ) - messages.append(systemMessage) - } - - // Save the updated channel data - saveChannelData() case "/debug": // Debug command to check ownership info if let channel = currentChannel { @@ -3541,11 +3343,6 @@ extension ChatViewModel: BitchatDelegate { channelMessages[channel]?.sort { $0.timestamp < $1.timestamp } } - // Save message if channel has retention enabled - if retentionEnabledChannels.contains(channel) { - MessageRetentionService.shared.saveMessage(messageToAdd, forChannel: channel) - } - // Track channel members - only track the sender as a member if channelMembers[channel] == nil { channelMembers[channel] = Set() diff --git a/bitchat/Views/ContentView.swift b/bitchat/Views/ContentView.swift index 1868422b..17d880dc 100644 --- a/bitchat/Views/ContentView.swift +++ b/bitchat/Views/ContentView.swift @@ -380,7 +380,6 @@ struct ContentView: View { let channelCommandInfo: [(commands: [String], syntax: String?, description: String)] = [ (["/pass"], "[password]", "change channel password"), - (["/save"], nil, "save channel messages locally"), (["/transfer"], "", "transfer channel ownership") ] @@ -490,7 +489,6 @@ struct ContentView: View { // Add channel-specific commands if in a channel if viewModel.currentChannel != nil { commandDescriptions.append(("/pass", "change channel password")) - commandDescriptions.append(("/save", "save channel messages locally")) commandDescriptions.append(("/transfer", "transfer channel ownership")) } @@ -1192,29 +1190,6 @@ struct ContentView: View { Spacer() HStack(spacing: 8) { - // Show retention indicator for all users - if viewModel.retentionEnabledChannels.contains(currentChannel) { - Image(systemName: "bookmark.fill") - .font(.system(size: 16)) - .foregroundColor(Color.yellow) - .help("Messages in this channel are being saved locally") - .accessibilityLabel("Message retention enabled") - } - - // Save button - only for channel owner - if viewModel.isChannelOwner(currentChannel) { - Button(action: { - viewModel.sendMessage("/save") - }) { - Image(systemName: viewModel.retentionEnabledChannels.contains(currentChannel) ? "bookmark.slash" : "bookmark") - .font(.system(size: 16)) - .foregroundColor(textColor) - } - .buttonStyle(.plain) - .help(viewModel.retentionEnabledChannels.contains(currentChannel) ? "Disable message retention" : "Enable message retention") - .accessibilityLabel(viewModel.retentionEnabledChannels.contains(currentChannel) ? "Disable message retention" : "Enable message retention") - } - // Password button for channel creator only if viewModel.isChannelOwner(currentChannel) { Button(action: { diff --git a/bitchatTests/KeychainIntegrationTests.swift b/bitchatTests/KeychainIntegrationTests.swift index d806f535..e5562edd 100644 --- a/bitchatTests/KeychainIntegrationTests.swift +++ b/bitchatTests/KeychainIntegrationTests.swift @@ -97,12 +97,10 @@ class KeychainIntegrationTests: XCTestCase { func testNoPlaintextInUserDefaults() { // Create services to generate keys _ = NoiseEncryptionService() - _ = MessageRetentionService.shared // Check UserDefaults for any sensitive data let keysToCheck = [ "bitchat.noiseIdentityKey", - "bitchat.messageRetentionKey", "bitchat.channelPasswords", "bitchat.identityKey", "bitchat.staticKey" diff --git a/bitchatTests/NoiseIdentityPersistenceTests.swift b/bitchatTests/NoiseIdentityPersistenceTests.swift index 04e64471..c96be9b0 100644 --- a/bitchatTests/NoiseIdentityPersistenceTests.swift +++ b/bitchatTests/NoiseIdentityPersistenceTests.swift @@ -112,31 +112,7 @@ class NoiseIdentityPersistenceTests: XCTestCase { } // MARK: - Message Retention Key Tests - - func testMessageRetentionKeyPersistence() { - // Create first instance - _ = MessageRetentionService.shared - - // Get key from keychain - let keyData1 = KeychainManager.shared.getIdentityKey(forKey: "messageRetentionKey") - XCTAssertNotNil(keyData1, "Message retention key should be stored") - - // Simulate app restart by clearing the singleton - // (In real app, this would be a new process) - - // Get key again - let keyData2 = KeychainManager.shared.getIdentityKey(forKey: "messageRetentionKey") - XCTAssertEqual(keyData1, keyData2, "Message retention key should persist") - } - - func testMessageRetentionKeyNotInUserDefaults() { - // Ensure service is initialized - _ = MessageRetentionService.shared - - // Verify key is NOT in UserDefaults - let userDefaultsData = UserDefaults.standard.data(forKey: "bitchat.messageRetentionKey") - XCTAssertNil(userDefaultsData, "Message retention key should NOT be in UserDefaults") - } + // Message retention feature has been removed // MARK: - Keychain Service Name Tests