mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 19:25:20 +00:00
Fix delivery confirmation for private messages
- Pass message ID from ChatViewModel to BluetoothMeshService - Preserve original message ID when creating ACK messages - Move delivery tracking to ChatViewModel for consistent IDs - Update project.yml to match current project structure
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 63;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
@@ -28,6 +28,7 @@
|
|||||||
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; };
|
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F149C43D1915831B60FE09 /* CompressionUtil.swift */; };
|
||||||
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
7DD72D928FF9DD3CA81B46B0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3A69677D382F1C3D5ED03F7D /* Assets.xcassets */; };
|
||||||
846E2B446E36639159704730 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; };
|
846E2B446E36639159704730 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; };
|
||||||
|
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */; };
|
||||||
8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
8F0BFC2D2B2A5E7B70C3B485 /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
||||||
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 136696FC4436A02D98CE6A77 /* KeychainManager.swift */; };
|
||||||
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
923027D6F2F417AFA2488127 /* BitchatProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */; };
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
C0A80BA73EC1A372B9338E3C /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; };
|
C0A80BA73EC1A372B9338E3C /* BatteryOptimizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED176FF3B274E35C2D827894 /* BatteryOptimizer.swift */; };
|
||||||
C91FDE97070433E6CFE95C55 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; };
|
C91FDE97070433E6CFE95C55 /* BloomFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */; };
|
||||||
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; };
|
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; };
|
||||||
|
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */; };
|
||||||
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
CDAD6629EB69916B95C80DAF /* BinaryProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */; };
|
||||||
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; };
|
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */; };
|
||||||
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_macOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryTracker.swift; sourceTree = "<group>"; };
|
||||||
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
|
136696FC4436A02D98CE6A77 /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
|
||||||
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = "<group>"; };
|
1EB3A8FE16333ED12FCB8ACB /* BloomFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BloomFilterTests.swift; sourceTree = "<group>"; };
|
||||||
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
|
229F17B68CFF7AB1BC91C847 /* BitchatProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatProtocol.swift; sourceTree = "<group>"; };
|
||||||
@@ -81,9 +84,9 @@
|
|||||||
67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetentionService.swift; sourceTree = "<group>"; };
|
67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetentionService.swift; sourceTree = "<group>"; };
|
||||||
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
6DC1563390A15C042D059CF9 /* EncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptionService.swift; sourceTree = "<group>"; };
|
||||||
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = "<group>"; };
|
||||||
7EEBDA723E1CFD88758DA4AC /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = "<group>"; };
|
8DE9CDF66D4E52D268851048 /* MessagePaddingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagePaddingTests.swift; sourceTree = "<group>"; };
|
||||||
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
8F3A7C058C2C8E1A06C8CF8B /* bitchat_macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat_macOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
96D0D41CA19EE5A772AA8434 /* bitchat_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = bitchat_iOS.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocol.swift; sourceTree = "<group>"; };
|
||||||
AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = "<group>"; };
|
AA4D7595A613F7ED3B386132 /* MessageRetryService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRetryService.swift; sourceTree = "<group>"; };
|
||||||
@@ -144,8 +147,8 @@
|
|||||||
9F37F9F2C353B58AC809E93B /* Products */ = {
|
9F37F9F2C353B58AC809E93B /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
997D512074C64904D75DDD40 /* bitchat.app */,
|
96D0D41CA19EE5A772AA8434 /* bitchat_iOS.app */,
|
||||||
7EEBDA723E1CFD88758DA4AC /* bitchat.app */,
|
8F3A7C058C2C8E1A06C8CF8B /* bitchat_macOS.app */,
|
||||||
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */,
|
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */,
|
||||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */,
|
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */,
|
||||||
);
|
);
|
||||||
@@ -187,6 +190,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */,
|
D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */,
|
||||||
|
12B9C3EDF3BC73D3BC106DA4 /* DeliveryTracker.swift */,
|
||||||
6DC1563390A15C042D059CF9 /* EncryptionService.swift */,
|
6DC1563390A15C042D059CF9 /* EncryptionService.swift */,
|
||||||
136696FC4436A02D98CE6A77 /* KeychainManager.swift */,
|
136696FC4436A02D98CE6A77 /* KeychainManager.swift */,
|
||||||
67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */,
|
67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */,
|
||||||
@@ -214,7 +218,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
productName = bitchat_macOS;
|
productName = bitchat_macOS;
|
||||||
productReference = 7EEBDA723E1CFD88758DA4AC /* bitchat.app */;
|
productReference = 8F3A7C058C2C8E1A06C8CF8B /* bitchat_macOS.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */ = {
|
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */ = {
|
||||||
@@ -268,7 +272,7 @@
|
|||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
);
|
);
|
||||||
productName = bitchat_iOS;
|
productName = bitchat_iOS;
|
||||||
productReference = 997D512074C64904D75DDD40 /* bitchat.app */;
|
productReference = 96D0D41CA19EE5A772AA8434 /* bitchat_iOS.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
};
|
};
|
||||||
/* End PBXNativeTarget section */
|
/* End PBXNativeTarget section */
|
||||||
@@ -298,6 +302,7 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 18198ED912AAF495D8AF7763;
|
mainGroup = 18198ED912AAF495D8AF7763;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
preferredProjectObjectVersion = 54;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
@@ -342,6 +347,7 @@
|
|||||||
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
|
D450CF41F207BDE1A1AAA56E /* ChatViewModel.swift in Sources */,
|
||||||
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */,
|
B0CA7796B2B2AC2B33F84548 /* CompressionUtil.swift in Sources */,
|
||||||
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
|
92D34E7A07C990C8A815B0CE /* ContentView.swift in Sources */,
|
||||||
|
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.swift in Sources */,
|
||||||
739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */,
|
739429DFDE5C5829CF70DA7D /* EncryptionService.swift in Sources */,
|
||||||
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
FB8819B4C84FAFEF5C36B216 /* KeychainManager.swift in Sources */,
|
||||||
4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */,
|
4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */,
|
||||||
@@ -364,6 +370,7 @@
|
|||||||
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
|
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */,
|
||||||
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */,
|
7DCA0DBCB8884E3B31C7BCE3 /* CompressionUtil.swift in Sources */,
|
||||||
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
|
1D9674FA5F998503831DC281 /* ContentView.swift in Sources */,
|
||||||
|
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.swift in Sources */,
|
||||||
DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */,
|
DDA1DFAB1FF7AADE52DC0F53 /* EncryptionService.swift in Sources */,
|
||||||
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
8F737CE0435792CC2AD65FCB /* KeychainManager.swift in Sources */,
|
||||||
4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */,
|
4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */,
|
||||||
@@ -425,6 +432,7 @@
|
|||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
PRODUCT_NAME = bitchatTests;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -444,6 +452,7 @@
|
|||||||
"@loader_path/Frameworks",
|
"@loader_path/Frameworks",
|
||||||
);
|
);
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
PRODUCT_NAME = bitchatTests;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
@@ -464,6 +473,7 @@
|
|||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
PRODUCT_NAME = bitchatTests;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat";
|
||||||
@@ -479,25 +489,19 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
|
||||||
SUPPORTS_MACCATALYST = NO;
|
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
@@ -514,6 +518,7 @@
|
|||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat.tests;
|
||||||
|
PRODUCT_NAME = bitchatTests;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/bitchat.app/Contents/MacOS/bitchat";
|
||||||
@@ -529,25 +534,19 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MACOSX_DEPLOYMENT_TARGET = 13.0;
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
|
||||||
SUPPORTS_MACCATALYST = NO;
|
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
@@ -560,9 +559,9 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
@@ -571,7 +570,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
@@ -610,7 +608,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
ENABLE_NS_ASSERTIONS = NO;
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
@@ -643,9 +641,9 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
ENABLE_HARDENED_RUNTIME = YES;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/../Frameworks",
|
"@executable_path/../Frameworks",
|
||||||
@@ -654,7 +652,6 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
PRODUCT_BUNDLE_IDENTIFIER = chat.bitchat;
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
@@ -693,7 +690,7 @@
|
|||||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
COPY_PHASE_STRIP = NO;
|
COPY_PHASE_STRIP = NO;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 5;
|
||||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
ENABLE_TESTABILITY = YES;
|
ENABLE_TESTABILITY = YES;
|
||||||
|
|||||||
+2
-14
@@ -26,19 +26,7 @@
|
|||||||
<string>bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.</string>
|
<string>bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.</string>
|
||||||
<key>NSBluetoothPeripheralUsageDescription</key>
|
<key>NSBluetoothPeripheralUsageDescription</key>
|
||||||
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
|
<string>bitchat uses Bluetooth to discover and connect with other bitchat users nearby.</string>
|
||||||
<key>UIBackgroundModes</key>
|
<key>NSUserNotificationAlertStyle</key>
|
||||||
<array>
|
<string>alert</string>
|
||||||
<string>bluetooth-central</string>
|
|
||||||
<string>bluetooth-peripheral</string>
|
|
||||||
</array>
|
|
||||||
<key>UILaunchScreen</key>
|
|
||||||
<dict>
|
|
||||||
<key>UIColorName</key>
|
|
||||||
<string>Black</string>
|
|
||||||
</dict>
|
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
|
||||||
<array>
|
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -74,6 +74,8 @@ enum MessageType: UInt8 {
|
|||||||
case fragmentEnd = 0x07
|
case fragmentEnd = 0x07
|
||||||
case roomAnnounce = 0x08 // Announce password-protected room status
|
case roomAnnounce = 0x08 // Announce password-protected room status
|
||||||
case roomRetention = 0x09 // Announce room retention status
|
case roomRetention = 0x09 // Announce room retention status
|
||||||
|
case deliveryAck = 0x0A // Acknowledge message received
|
||||||
|
case deliveryStatusRequest = 0x0B // Request delivery status update
|
||||||
}
|
}
|
||||||
|
|
||||||
// Special recipient ID for broadcast messages
|
// Special recipient ID for broadcast messages
|
||||||
@@ -127,6 +129,57 @@ struct BitchatPacket: Codable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delivery acknowledgment structure
|
||||||
|
struct DeliveryAck: Codable {
|
||||||
|
let originalMessageID: String
|
||||||
|
let ackID: String
|
||||||
|
let recipientID: String // Who received it
|
||||||
|
let recipientNickname: String
|
||||||
|
let timestamp: Date
|
||||||
|
let hopCount: UInt8 // How many hops to reach recipient
|
||||||
|
|
||||||
|
init(originalMessageID: String, recipientID: String, recipientNickname: String, hopCount: UInt8) {
|
||||||
|
self.originalMessageID = originalMessageID
|
||||||
|
self.ackID = UUID().uuidString
|
||||||
|
self.recipientID = recipientID
|
||||||
|
self.recipientNickname = recipientNickname
|
||||||
|
self.timestamp = Date()
|
||||||
|
self.hopCount = hopCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode() -> Data? {
|
||||||
|
try? JSONEncoder().encode(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(from data: Data) -> DeliveryAck? {
|
||||||
|
try? JSONDecoder().decode(DeliveryAck.self, from: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delivery status for messages
|
||||||
|
enum DeliveryStatus: Codable, Equatable {
|
||||||
|
case sending
|
||||||
|
case sent // Left our device
|
||||||
|
case delivered(to: String, at: Date) // Confirmed by recipient
|
||||||
|
case failed(reason: String)
|
||||||
|
case partiallyDelivered(reached: Int, total: Int) // For rooms
|
||||||
|
|
||||||
|
var displayText: String {
|
||||||
|
switch self {
|
||||||
|
case .sending:
|
||||||
|
return "Sending..."
|
||||||
|
case .sent:
|
||||||
|
return "Sent"
|
||||||
|
case .delivered(let nickname, _):
|
||||||
|
return "Delivered to \(nickname)"
|
||||||
|
case .failed(let reason):
|
||||||
|
return "Failed: \(reason)"
|
||||||
|
case .partiallyDelivered(let reached, let total):
|
||||||
|
return "Delivered to \(reached)/\(total)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct BitchatMessage: Codable, Equatable {
|
struct BitchatMessage: Codable, Equatable {
|
||||||
let id: String
|
let id: String
|
||||||
let sender: String
|
let sender: String
|
||||||
@@ -141,9 +194,10 @@ struct BitchatMessage: Codable, Equatable {
|
|||||||
let room: String? // Room hashtag (e.g., "#general")
|
let room: String? // Room hashtag (e.g., "#general")
|
||||||
let encryptedContent: Data? // For password-protected rooms
|
let encryptedContent: Data? // For password-protected rooms
|
||||||
let isEncrypted: Bool // Flag to indicate if content is encrypted
|
let isEncrypted: Bool // Flag to indicate if content is encrypted
|
||||||
|
var deliveryStatus: DeliveryStatus? // Delivery tracking
|
||||||
|
|
||||||
init(sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil, room: String? = nil, encryptedContent: Data? = nil, isEncrypted: Bool = false) {
|
init(id: String? = nil, sender: String, content: String, timestamp: Date, isRelay: Bool, originalSender: String? = nil, isPrivate: Bool = false, recipientNickname: String? = nil, senderPeerID: String? = nil, mentions: [String]? = nil, room: String? = nil, encryptedContent: Data? = nil, isEncrypted: Bool = false, deliveryStatus: DeliveryStatus? = nil) {
|
||||||
self.id = UUID().uuidString
|
self.id = id ?? UUID().uuidString
|
||||||
self.sender = sender
|
self.sender = sender
|
||||||
self.content = content
|
self.content = content
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
@@ -156,6 +210,7 @@ struct BitchatMessage: Codable, Equatable {
|
|||||||
self.room = room
|
self.room = room
|
||||||
self.encryptedContent = encryptedContent
|
self.encryptedContent = encryptedContent
|
||||||
self.isEncrypted = isEncrypted
|
self.isEncrypted = isEncrypted
|
||||||
|
self.deliveryStatus = deliveryStatus ?? (isPrivate ? .sending : nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +226,10 @@ protocol BitchatDelegate: AnyObject {
|
|||||||
|
|
||||||
// Optional method to check if a fingerprint belongs to a favorite peer
|
// Optional method to check if a fingerprint belongs to a favorite peer
|
||||||
func isFavorite(fingerprint: String) -> Bool
|
func isFavorite(fingerprint: String) -> Bool
|
||||||
|
|
||||||
|
// Delivery confirmation methods
|
||||||
|
func didReceiveDeliveryAck(_ ack: DeliveryAck)
|
||||||
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Provide default implementation to make it effectively optional
|
// Provide default implementation to make it effectively optional
|
||||||
@@ -195,4 +254,12 @@ extension BitchatDelegate {
|
|||||||
// Default returns nil (unable to decrypt)
|
// Default returns nil (unable to decrypt)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didReceiveDeliveryAck(_ ack: DeliveryAck) {
|
||||||
|
// Default empty implementation
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||||
|
// Default empty implementation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -558,14 +558,18 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String) {
|
func sendPrivateMessage(_ content: String, to recipientPeerID: String, recipientNickname: String, messageID: String? = nil) {
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
let nickname = self.delegate as? ChatViewModel
|
let nickname = self.delegate as? ChatViewModel
|
||||||
let senderNick = nickname?.nickname ?? self.myPeerID
|
let senderNick = nickname?.nickname ?? self.myPeerID
|
||||||
|
|
||||||
|
// Use provided message ID or generate a new one
|
||||||
|
let msgID = messageID ?? UUID().uuidString
|
||||||
|
|
||||||
let message = BitchatMessage(
|
let message = BitchatMessage(
|
||||||
|
id: msgID,
|
||||||
sender: senderNick,
|
sender: senderNick,
|
||||||
content: content,
|
content: content,
|
||||||
timestamp: Date(),
|
timestamp: Date(),
|
||||||
@@ -634,6 +638,8 @@ class BluetoothMeshService: NSObject {
|
|||||||
self?.recentlySentMessages.remove(msgID)
|
self?.recentlySentMessages.remove(msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Message tracking is now done in ChatViewModel to ensure consistent message IDs
|
||||||
|
|
||||||
// Add random delay for timing obfuscation
|
// Add random delay for timing obfuscation
|
||||||
let delay = self.randomDelay()
|
let delay = self.randomDelay()
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
@@ -666,6 +672,41 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sendDeliveryAck(_ ack: DeliveryAck, to recipientID: String) {
|
||||||
|
messageQueue.async { [weak self] in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
// Encode the ACK
|
||||||
|
guard let ackData = ack.encode() else {
|
||||||
|
print("[DeliveryTracker] Failed to encode ACK")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt ACK for the original sender
|
||||||
|
let encryptedPayload: Data
|
||||||
|
do {
|
||||||
|
encryptedPayload = try self.encryptionService.encrypt(ackData, for: recipientID)
|
||||||
|
} catch {
|
||||||
|
print("[DeliveryTracker] Failed to encrypt ACK: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create ACK packet with direct routing to original sender
|
||||||
|
let packet = BitchatPacket(
|
||||||
|
type: MessageType.deliveryAck.rawValue,
|
||||||
|
senderID: Data(self.myPeerID.utf8),
|
||||||
|
recipientID: Data(recipientID.utf8),
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: encryptedPayload,
|
||||||
|
signature: nil, // ACKs don't need signatures
|
||||||
|
ttl: 3 // Limited TTL for ACKs
|
||||||
|
)
|
||||||
|
|
||||||
|
// Send immediately without delay (ACKs should be fast)
|
||||||
|
self.broadcastPacket(packet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func announcePasswordProtectedRoom(_ room: String, isProtected: Bool = true, creatorID: String? = nil, keyCommitment: String? = nil) {
|
func announcePasswordProtectedRoom(_ room: String, isProtected: Bool = true, creatorID: String? = nil, keyCommitment: String? = nil) {
|
||||||
messageQueue.async { [weak self] in
|
messageQueue.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
@@ -1371,6 +1412,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let messageWithPeerID = BitchatMessage(
|
let messageWithPeerID = BitchatMessage(
|
||||||
|
id: message.id, // Preserve the original message ID
|
||||||
sender: message.sender,
|
sender: message.sender,
|
||||||
content: finalContent,
|
content: finalContent,
|
||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
@@ -1393,6 +1435,22 @@ class BluetoothMeshService: NSObject {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceiveMessage(messageWithPeerID)
|
self.delegate?.didReceiveMessage(messageWithPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate and send ACK for room messages if we're mentioned or it's a small room
|
||||||
|
let viewModel = self.delegate as? ChatViewModel
|
||||||
|
let myNickname = viewModel?.nickname ?? self.myPeerID
|
||||||
|
if let _ = message.room,
|
||||||
|
let mentions = message.mentions,
|
||||||
|
(mentions.contains(myNickname) || self.activePeers.count < 10) {
|
||||||
|
if let ack = DeliveryTracker.shared.generateAck(
|
||||||
|
for: messageWithPeerID,
|
||||||
|
myPeerID: self.myPeerID,
|
||||||
|
myNickname: myNickname,
|
||||||
|
hopCount: UInt8(self.maxTTL - packet.ttl)
|
||||||
|
) {
|
||||||
|
self.sendDeliveryAck(ack, to: senderID)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Relay broadcast messages
|
// Relay broadcast messages
|
||||||
@@ -1476,6 +1534,7 @@ class BluetoothMeshService: NSObject {
|
|||||||
peerNicknamesLock.unlock()
|
peerNicknamesLock.unlock()
|
||||||
|
|
||||||
let messageWithPeerID = BitchatMessage(
|
let messageWithPeerID = BitchatMessage(
|
||||||
|
id: message.id, // Preserve the original message ID
|
||||||
sender: message.sender,
|
sender: message.sender,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
timestamp: message.timestamp,
|
timestamp: message.timestamp,
|
||||||
@@ -1496,6 +1555,18 @@ class BluetoothMeshService: NSObject {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.delegate?.didReceiveMessage(messageWithPeerID)
|
self.delegate?.didReceiveMessage(messageWithPeerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate and send ACK for private messages
|
||||||
|
let viewModel = self.delegate as? ChatViewModel
|
||||||
|
let myNickname = viewModel?.nickname ?? self.myPeerID
|
||||||
|
if let ack = DeliveryTracker.shared.generateAck(
|
||||||
|
for: messageWithPeerID,
|
||||||
|
myPeerID: self.myPeerID,
|
||||||
|
myNickname: myNickname,
|
||||||
|
hopCount: UInt8(self.maxTTL - packet.ttl)
|
||||||
|
) {
|
||||||
|
self.sendDeliveryAck(ack, to: senderID)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if packet.ttl > 0 {
|
} else if packet.ttl > 0 {
|
||||||
@@ -1845,6 +1916,35 @@ class BluetoothMeshService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .deliveryAck:
|
||||||
|
// Handle delivery acknowledgment
|
||||||
|
if let recipientIDData = packet.recipientID,
|
||||||
|
let recipientID = String(data: recipientIDData.trimmingNullBytes(), encoding: .utf8),
|
||||||
|
recipientID == myPeerID {
|
||||||
|
// This ACK is for us - decrypt it
|
||||||
|
if let senderID = String(data: packet.senderID.trimmingNullBytes(), encoding: .utf8) {
|
||||||
|
do {
|
||||||
|
let decryptedData = try encryptionService.decrypt(packet.payload, from: senderID)
|
||||||
|
if let ack = DeliveryAck.decode(from: decryptedData) {
|
||||||
|
// Process the ACK
|
||||||
|
DeliveryTracker.shared.processDeliveryAck(ack)
|
||||||
|
|
||||||
|
// Notify delegate
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.delegate?.didReceiveDeliveryAck(ack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Failed to decrypt ACK - might be from unknown sender
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if packet.ttl > 0 {
|
||||||
|
// Relay the ACK if not for us
|
||||||
|
var relayPacket = packet
|
||||||
|
relayPacket.ttl -= 1
|
||||||
|
self.broadcastPacket(relayPacket)
|
||||||
|
}
|
||||||
|
|
||||||
case .roomRetention:
|
case .roomRetention:
|
||||||
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
|
if let payloadStr = String(data: packet.payload, encoding: .utf8) {
|
||||||
// Parse payload: room|enabled|creatorID
|
// Parse payload: room|enabled|creatorID
|
||||||
|
|||||||
@@ -0,0 +1,273 @@
|
|||||||
|
//
|
||||||
|
// DeliveryTracker.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// This is free and unencumbered software released into the public domain.
|
||||||
|
// For more information, see <https://unlicense.org>
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class DeliveryTracker {
|
||||||
|
static let shared = DeliveryTracker()
|
||||||
|
|
||||||
|
// Track pending deliveries
|
||||||
|
private var pendingDeliveries: [String: PendingDelivery] = [:]
|
||||||
|
private let pendingLock = NSLock()
|
||||||
|
|
||||||
|
// Track received ACKs to prevent duplicates
|
||||||
|
private var receivedAckIDs = Set<String>()
|
||||||
|
private var sentAckIDs = Set<String>()
|
||||||
|
|
||||||
|
// Timeout configuration
|
||||||
|
private let privateMessageTimeout: TimeInterval = 30 // 30 seconds
|
||||||
|
private let roomMessageTimeout: TimeInterval = 60 // 1 minute
|
||||||
|
private let favoriteTimeout: TimeInterval = 300 // 5 minutes for favorites
|
||||||
|
|
||||||
|
// Retry configuration
|
||||||
|
private let maxRetries = 3
|
||||||
|
private let retryDelay: TimeInterval = 5 // Base retry delay
|
||||||
|
|
||||||
|
// Publishers for UI updates
|
||||||
|
let deliveryStatusUpdated = PassthroughSubject<(messageID: String, status: DeliveryStatus), Never>()
|
||||||
|
|
||||||
|
// Cleanup timer
|
||||||
|
private var cleanupTimer: Timer?
|
||||||
|
|
||||||
|
struct PendingDelivery {
|
||||||
|
let messageID: String
|
||||||
|
let sentAt: Date
|
||||||
|
let recipientID: String
|
||||||
|
let recipientNickname: String
|
||||||
|
let retryCount: Int
|
||||||
|
let isRoomMessage: Bool
|
||||||
|
let isFavorite: Bool
|
||||||
|
var ackedBy: Set<String> = [] // For tracking partial room delivery
|
||||||
|
let expectedRecipients: Int // For room messages
|
||||||
|
var timeoutTimer: Timer?
|
||||||
|
|
||||||
|
var isTimedOut: Bool {
|
||||||
|
let timeout: TimeInterval = isFavorite ? 300 : (isRoomMessage ? 60 : 30)
|
||||||
|
return Date().timeIntervalSince(sentAt) > timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
var shouldRetry: Bool {
|
||||||
|
return retryCount < 3 && isFavorite && !isRoomMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private init() {
|
||||||
|
startCleanupTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
cleanupTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Methods
|
||||||
|
|
||||||
|
func trackMessage(_ message: BitchatMessage, recipientID: String, recipientNickname: String, isFavorite: Bool = false, expectedRecipients: Int = 1) {
|
||||||
|
pendingLock.lock()
|
||||||
|
defer { pendingLock.unlock() }
|
||||||
|
|
||||||
|
// Don't track broadcasts or certain message types
|
||||||
|
guard message.isPrivate || message.room != nil else { return }
|
||||||
|
|
||||||
|
let delivery = PendingDelivery(
|
||||||
|
messageID: message.id,
|
||||||
|
sentAt: Date(),
|
||||||
|
recipientID: recipientID,
|
||||||
|
recipientNickname: recipientNickname,
|
||||||
|
retryCount: 0,
|
||||||
|
isRoomMessage: message.room != nil,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
expectedRecipients: expectedRecipients,
|
||||||
|
timeoutTimer: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
pendingDeliveries[message.id] = delivery
|
||||||
|
|
||||||
|
// Update status to sent
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in
|
||||||
|
self?.updateDeliveryStatus(message.id, status: .sent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule timeout
|
||||||
|
scheduleTimeout(for: message.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processDeliveryAck(_ ack: DeliveryAck) {
|
||||||
|
pendingLock.lock()
|
||||||
|
defer { pendingLock.unlock() }
|
||||||
|
|
||||||
|
// Prevent duplicate ACK processing
|
||||||
|
guard !receivedAckIDs.contains(ack.ackID) else { return }
|
||||||
|
receivedAckIDs.insert(ack.ackID)
|
||||||
|
|
||||||
|
// Find the pending delivery
|
||||||
|
guard var delivery = pendingDeliveries[ack.originalMessageID] else {
|
||||||
|
// Message might have already been delivered or timed out
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel timeout timer
|
||||||
|
delivery.timeoutTimer?.invalidate()
|
||||||
|
|
||||||
|
if delivery.isRoomMessage {
|
||||||
|
// Track partial delivery for room messages
|
||||||
|
delivery.ackedBy.insert(ack.recipientID)
|
||||||
|
pendingDeliveries[ack.originalMessageID] = delivery
|
||||||
|
|
||||||
|
let deliveredCount = delivery.ackedBy.count
|
||||||
|
let totalExpected = delivery.expectedRecipients
|
||||||
|
|
||||||
|
if deliveredCount >= totalExpected || deliveredCount >= max(1, totalExpected / 2) {
|
||||||
|
// Consider delivered if we got ACKs from at least half the expected recipients
|
||||||
|
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: "\(deliveredCount) members", at: Date()))
|
||||||
|
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
|
||||||
|
} else {
|
||||||
|
// Update partial delivery status
|
||||||
|
updateDeliveryStatus(ack.originalMessageID, status: .partiallyDelivered(reached: deliveredCount, total: totalExpected))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Direct message - mark as delivered
|
||||||
|
updateDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: Date()))
|
||||||
|
pendingDeliveries.removeValue(forKey: ack.originalMessageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateAck(for message: BitchatMessage, myPeerID: String, myNickname: String, hopCount: UInt8) -> DeliveryAck? {
|
||||||
|
// Don't ACK our own messages
|
||||||
|
guard message.senderPeerID != myPeerID else { return nil }
|
||||||
|
|
||||||
|
// Don't ACK broadcasts or system messages
|
||||||
|
guard message.isPrivate || message.room != nil else { return nil }
|
||||||
|
|
||||||
|
// Don't ACK if we've already sent an ACK for this message
|
||||||
|
guard !sentAckIDs.contains(message.id) else { return nil }
|
||||||
|
sentAckIDs.insert(message.id)
|
||||||
|
|
||||||
|
return DeliveryAck(
|
||||||
|
originalMessageID: message.id,
|
||||||
|
recipientID: myPeerID,
|
||||||
|
recipientNickname: myNickname,
|
||||||
|
hopCount: hopCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearDeliveryStatus(for messageID: String) {
|
||||||
|
pendingLock.lock()
|
||||||
|
defer { pendingLock.unlock() }
|
||||||
|
|
||||||
|
if let delivery = pendingDeliveries[messageID] {
|
||||||
|
delivery.timeoutTimer?.invalidate()
|
||||||
|
}
|
||||||
|
pendingDeliveries.removeValue(forKey: messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private Methods
|
||||||
|
|
||||||
|
private func updateDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.deliveryStatusUpdated.send((messageID: messageID, status: status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func scheduleTimeout(for messageID: String) {
|
||||||
|
guard let delivery = pendingDeliveries[messageID] else { return }
|
||||||
|
|
||||||
|
let timeout = delivery.isFavorite ? favoriteTimeout :
|
||||||
|
(delivery.isRoomMessage ? roomMessageTimeout : privateMessageTimeout)
|
||||||
|
|
||||||
|
let timer = Timer.scheduledTimer(withTimeInterval: timeout, repeats: false) { [weak self] _ in
|
||||||
|
self?.handleTimeout(messageID: messageID)
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingLock.lock()
|
||||||
|
if var updatedDelivery = pendingDeliveries[messageID] {
|
||||||
|
updatedDelivery.timeoutTimer = timer
|
||||||
|
pendingDeliveries[messageID] = updatedDelivery
|
||||||
|
}
|
||||||
|
pendingLock.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleTimeout(messageID: String) {
|
||||||
|
pendingLock.lock()
|
||||||
|
defer { pendingLock.unlock() }
|
||||||
|
|
||||||
|
guard let delivery = pendingDeliveries[messageID] else { return }
|
||||||
|
|
||||||
|
if delivery.shouldRetry {
|
||||||
|
// Retry for favorites
|
||||||
|
retryDelivery(messageID: messageID)
|
||||||
|
} else {
|
||||||
|
// Mark as failed
|
||||||
|
let reason = delivery.isRoomMessage ? "No response from room members" : "Message not delivered"
|
||||||
|
updateDeliveryStatus(messageID, status: .failed(reason: reason))
|
||||||
|
pendingDeliveries.removeValue(forKey: messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func retryDelivery(messageID: String) {
|
||||||
|
guard let delivery = pendingDeliveries[messageID] else { return }
|
||||||
|
|
||||||
|
// Increment retry count
|
||||||
|
let newDelivery = PendingDelivery(
|
||||||
|
messageID: delivery.messageID,
|
||||||
|
sentAt: delivery.sentAt,
|
||||||
|
recipientID: delivery.recipientID,
|
||||||
|
recipientNickname: delivery.recipientNickname,
|
||||||
|
retryCount: delivery.retryCount + 1,
|
||||||
|
isRoomMessage: delivery.isRoomMessage,
|
||||||
|
isFavorite: delivery.isFavorite,
|
||||||
|
ackedBy: delivery.ackedBy,
|
||||||
|
expectedRecipients: delivery.expectedRecipients,
|
||||||
|
timeoutTimer: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
pendingDeliveries[messageID] = newDelivery
|
||||||
|
|
||||||
|
// Exponential backoff for retry
|
||||||
|
let delay = retryDelay * pow(2, Double(delivery.retryCount))
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
|
// Trigger resend through delegate or notification
|
||||||
|
NotificationCenter.default.post(
|
||||||
|
name: Notification.Name("bitchat.retryMessage"),
|
||||||
|
object: nil,
|
||||||
|
userInfo: ["messageID": messageID]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Schedule new timeout
|
||||||
|
self?.scheduleTimeout(for: messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startCleanupTimer() {
|
||||||
|
cleanupTimer = Timer.scheduledTimer(withTimeInterval: 60, repeats: true) { [weak self] _ in
|
||||||
|
self?.cleanupOldDeliveries()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cleanupOldDeliveries() {
|
||||||
|
pendingLock.lock()
|
||||||
|
defer { pendingLock.unlock() }
|
||||||
|
|
||||||
|
let now = Date()
|
||||||
|
let maxAge: TimeInterval = 3600 // 1 hour
|
||||||
|
|
||||||
|
// Clean up old pending deliveries
|
||||||
|
pendingDeliveries = pendingDeliveries.filter { (_, delivery) in
|
||||||
|
now.timeIntervalSince(delivery.sentAt) < maxAge
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up old ACK IDs (keep last 1000)
|
||||||
|
if receivedAckIDs.count > 1000 {
|
||||||
|
receivedAckIDs.removeAll()
|
||||||
|
}
|
||||||
|
if sentAckIDs.count > 1000 {
|
||||||
|
sentAckIDs.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,9 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
// Messages are naturally ephemeral - no persistent storage
|
// Messages are naturally ephemeral - no persistent storage
|
||||||
|
|
||||||
|
// Delivery tracking
|
||||||
|
private var deliveryTrackerCancellable: AnyCancellable?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
loadNickname()
|
loadNickname()
|
||||||
loadFavorites()
|
loadFavorites()
|
||||||
@@ -87,6 +90,13 @@ class ChatViewModel: ObservableObject {
|
|||||||
|
|
||||||
// Request notification permission
|
// Request notification permission
|
||||||
NotificationService.shared.requestAuthorization()
|
NotificationService.shared.requestAuthorization()
|
||||||
|
|
||||||
|
// Subscribe to delivery status updates
|
||||||
|
deliveryTrackerCancellable = DeliveryTracker.shared.deliveryStatusUpdated
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] (messageID, status) in
|
||||||
|
self?.updateMessageDeliveryStatus(messageID, status: status)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadNickname() {
|
private func loadNickname() {
|
||||||
@@ -847,7 +857,8 @@ class ChatViewModel: ObservableObject {
|
|||||||
originalSender: nil,
|
originalSender: nil,
|
||||||
isPrivate: true,
|
isPrivate: true,
|
||||||
recipientNickname: recipientNickname,
|
recipientNickname: recipientNickname,
|
||||||
senderPeerID: meshService.myPeerID
|
senderPeerID: meshService.myPeerID,
|
||||||
|
deliveryStatus: .sending
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add to our private chat history
|
// Add to our private chat history
|
||||||
@@ -856,11 +867,15 @@ class ChatViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
privateChats[peerID]?.append(message)
|
privateChats[peerID]?.append(message)
|
||||||
|
|
||||||
|
// Track the message for delivery confirmation
|
||||||
|
let isFavorite = isFavorite(peerID: peerID)
|
||||||
|
DeliveryTracker.shared.trackMessage(message, recipientID: peerID, recipientNickname: recipientNickname, isFavorite: isFavorite)
|
||||||
|
|
||||||
// Trigger UI update
|
// Trigger UI update
|
||||||
objectWillChange.send()
|
objectWillChange.send()
|
||||||
|
|
||||||
// Send via mesh
|
// Send via mesh with the same message ID
|
||||||
meshService.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname)
|
meshService.sendPrivateMessage(content, to: peerID, recipientNickname: recipientNickname, messageID: message.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startPrivateChat(with peerID: String) {
|
func startPrivateChat(with peerID: String) {
|
||||||
@@ -2030,4 +2045,42 @@ extension ChatViewModel: BitchatDelegate {
|
|||||||
return favoritePeers.contains(fingerprint)
|
return favoritePeers.contains(fingerprint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func didReceiveDeliveryAck(_ ack: DeliveryAck) {
|
||||||
|
// Find the message and update its delivery status
|
||||||
|
updateMessageDeliveryStatus(ack.originalMessageID, status: .delivered(to: ack.recipientNickname, at: ack.timestamp))
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUpdateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||||
|
updateMessageDeliveryStatus(messageID, status: status)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateMessageDeliveryStatus(_ messageID: String, status: DeliveryStatus) {
|
||||||
|
// Update in main messages
|
||||||
|
if let index = messages.firstIndex(where: { $0.id == messageID }) {
|
||||||
|
var updatedMessage = messages[index]
|
||||||
|
updatedMessage.deliveryStatus = status
|
||||||
|
messages[index] = updatedMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update in private chats
|
||||||
|
for (peerID, var chatMessages) in privateChats {
|
||||||
|
if let index = chatMessages.firstIndex(where: { $0.id == messageID }) {
|
||||||
|
var updatedMessage = chatMessages[index]
|
||||||
|
updatedMessage.deliveryStatus = status
|
||||||
|
chatMessages[index] = updatedMessage
|
||||||
|
privateChats[peerID] = chatMessages
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update in room messages
|
||||||
|
for (room, var roomMsgs) in roomMessages {
|
||||||
|
if let index = roomMsgs.firstIndex(where: { $0.id == messageID }) {
|
||||||
|
var updatedMessage = roomMsgs[index]
|
||||||
|
updatedMessage.deliveryStatus = status
|
||||||
|
roomMsgs[index] = updatedMessage
|
||||||
|
roomMessages[room] = roomMsgs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -453,7 +453,7 @@ struct ContentView: View {
|
|||||||
.textSelection(.enabled)
|
.textSelection(.enabled)
|
||||||
} else {
|
} else {
|
||||||
// Regular messages with tappable sender name
|
// Regular messages with tappable sender name
|
||||||
HStack(alignment: .firstTextBaseline, spacing: 0) {
|
HStack(alignment: .center, spacing: 0) {
|
||||||
// Timestamp
|
// Timestamp
|
||||||
Text("[\(viewModel.formatTimestamp(message.timestamp))] ")
|
Text("[\(viewModel.formatTimestamp(message.timestamp))] ")
|
||||||
.font(.system(size: 14, design: .monospaced))
|
.font(.system(size: 14, design: .monospaced))
|
||||||
@@ -491,6 +491,14 @@ struct ContentView: View {
|
|||||||
isMentioned: isMentioned
|
isMentioned: isMentioned
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Delivery status indicator for private messages
|
||||||
|
if message.isPrivate && message.sender == viewModel.nickname,
|
||||||
|
let status = message.deliveryStatus {
|
||||||
|
DeliveryStatusView(status: status, colorScheme: colorScheme)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
.alignmentGuide(.firstTextBaseline) { _ in 12 }
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1061,3 +1069,57 @@ struct MessageContentView: View {
|
|||||||
return segments
|
return segments
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delivery status indicator view
|
||||||
|
struct DeliveryStatusView: View {
|
||||||
|
let status: DeliveryStatus
|
||||||
|
let colorScheme: ColorScheme
|
||||||
|
|
||||||
|
private var textColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var secondaryTextColor: Color {
|
||||||
|
colorScheme == .dark ? Color.green.opacity(0.8) : Color(red: 0, green: 0.5, blue: 0).opacity(0.8)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
switch status {
|
||||||
|
case .sending:
|
||||||
|
Image(systemName: "circle")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(secondaryTextColor.opacity(0.6))
|
||||||
|
|
||||||
|
case .sent:
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(secondaryTextColor.opacity(0.6))
|
||||||
|
|
||||||
|
case .delivered(let nickname, _):
|
||||||
|
HStack(spacing: -2) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
}
|
||||||
|
.foregroundColor(textColor.opacity(0.8))
|
||||||
|
.help("Delivered to \(nickname)")
|
||||||
|
|
||||||
|
case .failed(let reason):
|
||||||
|
Image(systemName: "exclamationmark.triangle")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
.foregroundColor(Color.red.opacity(0.8))
|
||||||
|
.help("Failed: \(reason)")
|
||||||
|
|
||||||
|
case .partiallyDelivered(let reached, let total):
|
||||||
|
HStack(spacing: 1) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.system(size: 10))
|
||||||
|
Text("\(reached)/\(total)")
|
||||||
|
.font(.system(size: 10, design: .monospaced))
|
||||||
|
}
|
||||||
|
.foregroundColor(secondaryTextColor.opacity(0.6))
|
||||||
|
.help("Delivered to \(reached) of \(total) members")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+62
-15
@@ -5,17 +5,62 @@ options:
|
|||||||
iOS: 16.0
|
iOS: 16.0
|
||||||
macOS: 13.0
|
macOS: 13.0
|
||||||
createIntermediateGroups: true
|
createIntermediateGroups: true
|
||||||
|
groupSortPosition: bottom
|
||||||
|
|
||||||
settings:
|
settings:
|
||||||
MARKETING_VERSION: 1.0.0
|
MARKETING_VERSION: 1.0.0
|
||||||
CURRENT_PROJECT_VERSION: 1
|
CURRENT_PROJECT_VERSION: 5
|
||||||
|
|
||||||
targets:
|
targets:
|
||||||
bitchat:
|
bitchat_iOS:
|
||||||
type: application
|
type: application
|
||||||
platform: [iOS, macOS]
|
platform: iOS
|
||||||
sources:
|
sources:
|
||||||
- bitchat
|
- path: bitchat
|
||||||
|
excludes:
|
||||||
|
- "**/.DS_Store"
|
||||||
|
createIntermediateGroups: true
|
||||||
|
resources:
|
||||||
|
- bitchat/Assets.xcassets
|
||||||
|
info:
|
||||||
|
path: bitchat/Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleDisplayName: bitchat
|
||||||
|
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||||
|
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||||
|
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
|
||||||
|
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
|
||||||
|
NSUserNotificationAlertStyle: alert
|
||||||
|
UIBackgroundModes:
|
||||||
|
- bluetooth-central
|
||||||
|
- bluetooth-peripheral
|
||||||
|
- remote-notification
|
||||||
|
UILaunchScreen:
|
||||||
|
UIColorName: Black
|
||||||
|
UISupportedInterfaceOrientations:
|
||||||
|
- UIInterfaceOrientationPortrait
|
||||||
|
settings:
|
||||||
|
PRODUCT_NAME: bitchat
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat
|
||||||
|
INFOPLIST_FILE: bitchat/Info.plist
|
||||||
|
ENABLE_PREVIEWS: YES
|
||||||
|
SWIFT_VERSION: 5.0
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET: 16.0
|
||||||
|
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: NO
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
CODE_SIGNING_REQUIRED: YES
|
||||||
|
CODE_SIGNING_ALLOWED: YES
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||||
|
|
||||||
|
bitchat_macOS:
|
||||||
|
type: application
|
||||||
|
platform: macOS
|
||||||
|
sources:
|
||||||
|
- path: bitchat
|
||||||
|
excludes:
|
||||||
|
- "**/.DS_Store"
|
||||||
|
createIntermediateGroups: true
|
||||||
resources:
|
resources:
|
||||||
- bitchat/Assets.xcassets
|
- bitchat/Assets.xcassets
|
||||||
info:
|
info:
|
||||||
@@ -27,35 +72,33 @@ targets:
|
|||||||
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
|
LSMinimumSystemVersion: $(MACOSX_DEPLOYMENT_TARGET)
|
||||||
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
|
NSBluetoothAlwaysUsageDescription: bitchat uses Bluetooth to create a secure mesh network for chatting with nearby users.
|
||||||
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
|
NSBluetoothPeripheralUsageDescription: bitchat uses Bluetooth to discover and connect with other bitchat users nearby.
|
||||||
UIBackgroundModes:
|
NSUserNotificationAlertStyle: alert
|
||||||
- bluetooth-central
|
|
||||||
- bluetooth-peripheral
|
|
||||||
UILaunchScreen:
|
|
||||||
UIColorName: Black
|
|
||||||
UISupportedInterfaceOrientations:
|
|
||||||
- UIInterfaceOrientationPortrait
|
|
||||||
settings:
|
settings:
|
||||||
|
PRODUCT_NAME: bitchat
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat
|
||||||
INFOPLIST_FILE: bitchat/Info.plist
|
INFOPLIST_FILE: bitchat/Info.plist
|
||||||
ENABLE_PREVIEWS: YES
|
ENABLE_PREVIEWS: YES
|
||||||
SWIFT_VERSION: 5.0
|
SWIFT_VERSION: 5.0
|
||||||
IPHONEOS_DEPLOYMENT_TARGET: 16.0
|
|
||||||
MACOSX_DEPLOYMENT_TARGET: 13.0
|
MACOSX_DEPLOYMENT_TARGET: 13.0
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD: YES
|
|
||||||
CODE_SIGN_STYLE: Automatic
|
CODE_SIGN_STYLE: Automatic
|
||||||
CODE_SIGNING_REQUIRED: YES
|
CODE_SIGNING_REQUIRED: YES
|
||||||
CODE_SIGNING_ALLOWED: YES
|
CODE_SIGNING_ALLOWED: YES
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||||
|
ENABLE_HARDENED_RUNTIME: YES
|
||||||
|
|
||||||
bitchatTests_iOS:
|
bitchatTests_iOS:
|
||||||
type: bundle.unit-test
|
type: bundle.unit-test
|
||||||
platform: iOS
|
platform: iOS
|
||||||
sources:
|
sources:
|
||||||
- bitchatTests
|
- path: bitchatTests
|
||||||
|
excludes:
|
||||||
|
- "**/.DS_Store"
|
||||||
|
createIntermediateGroups: true
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: bitchat_iOS
|
- target: bitchat_iOS
|
||||||
settings:
|
settings:
|
||||||
|
PRODUCT_NAME: bitchatTests
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.tests
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.tests
|
||||||
INFOPLIST_FILE: bitchatTests/Info.plist
|
INFOPLIST_FILE: bitchatTests/Info.plist
|
||||||
SWIFT_VERSION: 5.0
|
SWIFT_VERSION: 5.0
|
||||||
@@ -67,10 +110,14 @@ targets:
|
|||||||
type: bundle.unit-test
|
type: bundle.unit-test
|
||||||
platform: macOS
|
platform: macOS
|
||||||
sources:
|
sources:
|
||||||
- bitchatTests
|
- path: bitchatTests
|
||||||
|
excludes:
|
||||||
|
- "**/.DS_Store"
|
||||||
|
createIntermediateGroups: true
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: bitchat_macOS
|
- target: bitchat_macOS
|
||||||
settings:
|
settings:
|
||||||
|
PRODUCT_NAME: bitchatTests
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.tests
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.tests
|
||||||
INFOPLIST_FILE: bitchatTests/Info.plist
|
INFOPLIST_FILE: bitchatTests/Info.plist
|
||||||
SWIFT_VERSION: 5.0
|
SWIFT_VERSION: 5.0
|
||||||
|
|||||||
Reference in New Issue
Block a user