mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 16:05:19 +00:00
wip voice
This commit is contained in:
@@ -11,7 +11,6 @@
|
|||||||
0481A3A02E744D6300FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A39F2E744D6300FC845E /* tor-nolzma.xcframework */; };
|
0481A3A02E744D6300FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A39F2E744D6300FC845E /* tor-nolzma.xcframework */; };
|
||||||
0481A3A12E744D6300FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A39F2E744D6300FC845E /* tor-nolzma.xcframework */; };
|
0481A3A12E744D6300FC845E /* tor-nolzma.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A39F2E744D6300FC845E /* tor-nolzma.xcframework */; };
|
||||||
048A88812E76FD18000FBCDD /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
|
048A88812E76FD18000FBCDD /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 0481A35C2E6DA18600FC845E /* libz.tbd */; };
|
||||||
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
|
||||||
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
|
3EE336D150427F736F32B56C /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = B1D9136AA0083366353BFA2F /* P256K */; };
|
||||||
885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; };
|
885BBED78092484A5B069461 /* P256K in Frameworks */ = {isa = PBXBuildFile; productRef = 4EB6BA1B8464F1EA38F4E286 /* P256K */; };
|
||||||
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E56F2E77036A0032EA8A /* BitLogger */; };
|
A6E3E5702E77036A0032EA8A /* BitLogger in Frameworks */ = {isa = PBXBuildFile; productRef = A6E3E56F2E77036A0032EA8A /* BitLogger */; };
|
||||||
@@ -28,13 +27,6 @@
|
|||||||
remoteGlobalIDString = AF077EA0474EDEDE2C72716C;
|
remoteGlobalIDString = AF077EA0474EDEDE2C72716C;
|
||||||
remoteInfo = bitchat_iOS;
|
remoteInfo = bitchat_iOS;
|
||||||
};
|
};
|
||||||
E35E7AF9854A2E72452DD34F /* PBXContainerItemProxy */ = {
|
|
||||||
isa = PBXContainerItemProxy;
|
|
||||||
containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */;
|
|
||||||
proxyType = 1;
|
|
||||||
remoteGlobalIDString = 57CA17A36A2532A6CFF367BB;
|
|
||||||
remoteInfo = bitchatShareExtension;
|
|
||||||
};
|
|
||||||
FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */ = {
|
FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */ = {
|
||||||
isa = PBXContainerItemProxy;
|
isa = PBXContainerItemProxy;
|
||||||
containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */;
|
containerPortal = 475D96681D0EA0AE57A4E06E /* Project object */;
|
||||||
@@ -50,7 +42,6 @@
|
|||||||
dstPath = "";
|
dstPath = "";
|
||||||
dstSubfolder = PlugIns;
|
dstSubfolder = PlugIns;
|
||||||
files = (
|
files = (
|
||||||
17901751FD8010AFC8E750F2 /* bitchatShareExtension.appex in Embed Foundation Extensions */,
|
|
||||||
);
|
);
|
||||||
name = "Embed Foundation Extensions";
|
name = "Embed Foundation Extensions";
|
||||||
};
|
};
|
||||||
@@ -60,7 +51,6 @@
|
|||||||
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; };
|
||||||
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
0481A35C2E6DA18600FC845E /* libz.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libz.tbd; path = usr/lib/libz.tbd; sourceTree = SDKROOT; };
|
||||||
0481A39F2E744D6300FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
|
0481A39F2E744D6300FC845E /* tor-nolzma.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "tor-nolzma.xcframework"; sourceTree = "<group>"; };
|
||||||
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
|
||||||
8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
96D0D41CA19EE5A772AA8434 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
96D0D41CA19EE5A772AA8434 /* bitchat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = bitchatTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -68,13 +58,6 @@
|
|||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
A6E32D1B2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchatShareExtension" target */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
||||||
membershipExceptions = (
|
|
||||||
Services/TransportConfig.swift,
|
|
||||||
);
|
|
||||||
target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */;
|
|
||||||
};
|
|
||||||
A6E32D1C2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchat_iOS" target */ = {
|
A6E32D1C2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchat_iOS" target */ = {
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
membershipExceptions = (
|
membershipExceptions = (
|
||||||
@@ -91,20 +74,12 @@
|
|||||||
);
|
);
|
||||||
target = 0576A29205865664C0937536 /* bitchat_macOS */;
|
target = 0576A29205865664C0937536 /* bitchat_macOS */;
|
||||||
};
|
};
|
||||||
A6E32D232E762EAB0032EA8A /* Exceptions for "bitchatShareExtension" folder in "bitchatShareExtension" target */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
|
||||||
membershipExceptions = (
|
|
||||||
ShareViewController.swift,
|
|
||||||
);
|
|
||||||
target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */;
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
A6E32C972E762EA70032EA8A /* bitchat */ = {
|
A6E32C972E762EA70032EA8A /* bitchat */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
exceptions = (
|
||||||
A6E32D1B2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchatShareExtension" target */,
|
|
||||||
A6E32D1C2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchat_iOS" target */,
|
A6E32D1C2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchat_iOS" target */,
|
||||||
A6E32D1D2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchat_macOS" target */,
|
A6E32D1D2E762EA70032EA8A /* Exceptions for "bitchat" folder in "bitchat_macOS" target */,
|
||||||
);
|
);
|
||||||
@@ -113,9 +88,6 @@
|
|||||||
};
|
};
|
||||||
A6E32D212E762EAB0032EA8A /* bitchatShareExtension */ = {
|
A6E32D212E762EAB0032EA8A /* bitchatShareExtension */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
exceptions = (
|
|
||||||
A6E32D232E762EAB0032EA8A /* Exceptions for "bitchatShareExtension" folder in "bitchatShareExtension" target */,
|
|
||||||
);
|
|
||||||
path = bitchatShareExtension;
|
path = bitchatShareExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -180,7 +152,6 @@
|
|||||||
children = (
|
children = (
|
||||||
96D0D41CA19EE5A772AA8434 /* bitchat.app */,
|
96D0D41CA19EE5A772AA8434 /* bitchat.app */,
|
||||||
8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */,
|
8F3A7C058C2C8E1A06C8CF8B /* bitchat.app */,
|
||||||
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */,
|
|
||||||
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */,
|
C0DB1DE27F0AAB5092663E8E /* bitchatTests_iOS.xctest */,
|
||||||
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */,
|
03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */,
|
||||||
);
|
);
|
||||||
@@ -231,19 +202,6 @@
|
|||||||
productReference = 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */;
|
productReference = 03C57F452B55FD0FD8F51421 /* bitchatTests_macOS.xctest */;
|
||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
57CA17A36A2532A6CFF367BB /* bitchatShareExtension */ = {
|
|
||||||
isa = PBXNativeTarget;
|
|
||||||
buildConfigurationList = E4EA6DC648DF55FF84032EB5 /* Build configuration list for PBXNativeTarget "bitchatShareExtension" */;
|
|
||||||
buildPhases = (
|
|
||||||
0A08E70F08F55FD5BA8C7EF3 /* Sources */,
|
|
||||||
);
|
|
||||||
buildRules = (
|
|
||||||
);
|
|
||||||
name = bitchatShareExtension;
|
|
||||||
productName = bitchatShareExtension;
|
|
||||||
productReference = 61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */;
|
|
||||||
productType = "com.apple.product-type.app-extension";
|
|
||||||
};
|
|
||||||
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */ = {
|
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */;
|
buildConfigurationList = 38C4AF6313E5037F25CEF30B /* Build configuration list for PBXNativeTarget "bitchatTests_iOS" */;
|
||||||
@@ -274,9 +232,6 @@
|
|||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
|
||||||
6EB655BA5DB11909C1DEC460 /* PBXTargetDependency */,
|
|
||||||
);
|
|
||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
A6E32C972E762EA70032EA8A /* bitchat */,
|
A6E32C972E762EA70032EA8A /* bitchat */,
|
||||||
);
|
);
|
||||||
@@ -315,7 +270,6 @@
|
|||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
57CA17A36A2532A6CFF367BB /* bitchatShareExtension */,
|
|
||||||
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */,
|
6CB97DF2EA57234CB3E563B8 /* bitchatTests_iOS */,
|
||||||
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */,
|
47FF23248747DD7CB666CB91 /* bitchatTests_macOS */,
|
||||||
AF077EA0474EDEDE2C72716C /* bitchat_iOS */,
|
AF077EA0474EDEDE2C72716C /* bitchat_iOS */,
|
||||||
@@ -340,11 +294,6 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
0A08E70F08F55FD5BA8C7EF3 /* Sources */ = {
|
|
||||||
isa = PBXSourcesBuildPhase;
|
|
||||||
files = (
|
|
||||||
);
|
|
||||||
};
|
|
||||||
137ABE739BF20ACDDF8CC605 /* Sources */ = {
|
137ABE739BF20ACDDF8CC605 /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
files = (
|
files = (
|
||||||
@@ -373,11 +322,6 @@
|
|||||||
target = 0576A29205865664C0937536 /* bitchat_macOS */;
|
target = 0576A29205865664C0937536 /* bitchat_macOS */;
|
||||||
targetProxy = FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */;
|
targetProxy = FF470234EF8C6BB8865B80B5 /* PBXContainerItemProxy */;
|
||||||
};
|
};
|
||||||
6EB655BA5DB11909C1DEC460 /* PBXTargetDependency */ = {
|
|
||||||
isa = PBXTargetDependency;
|
|
||||||
target = 57CA17A36A2532A6CFF367BB /* bitchatShareExtension */;
|
|
||||||
targetProxy = E35E7AF9854A2E72452DD34F /* PBXContainerItemProxy */;
|
|
||||||
};
|
|
||||||
D8C09F21DB7DC06E8E672C21 /* PBXTargetDependency */ = {
|
D8C09F21DB7DC06E8E672C21 /* PBXTargetDependency */ = {
|
||||||
isa = PBXTargetDependency;
|
isa = PBXTargetDependency;
|
||||||
target = AF077EA0474EDEDE2C72716C /* bitchat_iOS */;
|
target = AF077EA0474EDEDE2C72716C /* bitchat_iOS */;
|
||||||
@@ -462,37 +406,6 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
name = Release;
|
||||||
};
|
};
|
||||||
3DCF45111852FB2AEBE05E31 /* Release configuration for PBXNativeTarget "bitchatShareExtension" */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReferenceAnchor = A6E367C92E76469E0032EA8A /* Configs */;
|
|
||||||
baseConfigurationReferenceRelativePath = Release.xcconfig;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGNING_ALLOWED = YES;
|
|
||||||
CODE_SIGNING_REQUIRED = YES;
|
|
||||||
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements;
|
|
||||||
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
|
||||||
INFOPLIST_FILE = bitchatShareExtension/Info.plist;
|
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = "$(IPHONEOS_DEPLOYMENT_TARGET)";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ShareExtension";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
|
||||||
SUPPORTS_MACCATALYST = NO;
|
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SWIFT_VERSION = "$(SWIFT_VERSION)";
|
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
|
||||||
};
|
|
||||||
name = Release;
|
|
||||||
};
|
|
||||||
702E7395723CADA4B830F4A9 /* Debug configuration for PBXNativeTarget "bitchat_iOS" */ = {
|
702E7395723CADA4B830F4A9 /* Debug configuration for PBXNativeTarget "bitchat_iOS" */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReferenceAnchor = A6E367C92E76469E0032EA8A /* Configs */;
|
baseConfigurationReferenceAnchor = A6E367C92E76469E0032EA8A /* Configs */;
|
||||||
@@ -504,7 +417,7 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
||||||
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 66W2RU3J5T;
|
||||||
ENABLE_PREVIEWS = NO;
|
ENABLE_PREVIEWS = NO;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
||||||
@@ -516,6 +429,7 @@
|
|||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "chat.bitchat-local";
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -564,7 +478,7 @@
|
|||||||
CODE_SIGNING_REQUIRED = YES;
|
CODE_SIGNING_REQUIRED = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
||||||
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 66W2RU3J5T;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
||||||
@@ -576,6 +490,7 @@
|
|||||||
);
|
);
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=iphoneos*]" = "chat.bitchat-local";
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
@@ -600,7 +515,7 @@
|
|||||||
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 66W2RU3J5T;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
||||||
@@ -611,6 +526,7 @@
|
|||||||
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
|
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "chat.bitchat-local";
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -691,7 +607,7 @@
|
|||||||
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
DEAD_CODE_STRIPPING = YES;
|
DEAD_CODE_STRIPPING = YES;
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
DEVELOPMENT_TEAM = 66W2RU3J5T;
|
||||||
ENABLE_PREVIEWS = NO;
|
ENABLE_PREVIEWS = NO;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
||||||
@@ -702,6 +618,7 @@
|
|||||||
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
|
MACOSX_DEPLOYMENT_TARGET = "$(MACOSX_DEPLOYMENT_TARGET)";
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER[sdk=macosx*]" = "chat.bitchat-local";
|
||||||
PRODUCT_NAME = bitchat;
|
PRODUCT_NAME = bitchat;
|
||||||
REGISTER_APP_GROUPS = YES;
|
REGISTER_APP_GROUPS = YES;
|
||||||
SDKROOT = macosx;
|
SDKROOT = macosx;
|
||||||
@@ -776,37 +693,6 @@
|
|||||||
};
|
};
|
||||||
name = Debug;
|
name = Debug;
|
||||||
};
|
};
|
||||||
DAC5E82049F8A97360BE63D6 /* Debug configuration for PBXNativeTarget "bitchatShareExtension" */ = {
|
|
||||||
isa = XCBuildConfiguration;
|
|
||||||
baseConfigurationReferenceAnchor = A6E367C92E76469E0032EA8A /* Configs */;
|
|
||||||
baseConfigurationReferenceRelativePath = Debug.xcconfig;
|
|
||||||
buildSettings = {
|
|
||||||
CODE_SIGNING_ALLOWED = YES;
|
|
||||||
CODE_SIGNING_REQUIRED = YES;
|
|
||||||
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
|
|
||||||
CODE_SIGN_ENTITLEMENTS = bitchatShareExtension/bitchatShareExtension.entitlements;
|
|
||||||
CODE_SIGN_STYLE = "$(CODE_SIGN_STYLE)";
|
|
||||||
DEVELOPMENT_TEAM = "$(DEVELOPMENT_TEAM)";
|
|
||||||
INFOPLIST_FILE = bitchatShareExtension/Info.plist;
|
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = bitchat;
|
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = "$(IPHONEOS_DEPLOYMENT_TARGET)";
|
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
|
||||||
"$(inherited)",
|
|
||||||
"@executable_path/Frameworks",
|
|
||||||
"@executable_path/../../Frameworks",
|
|
||||||
);
|
|
||||||
MARKETING_VERSION = "$(MARKETING_VERSION)";
|
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = "$(PRODUCT_BUNDLE_IDENTIFIER).ShareExtension";
|
|
||||||
SDKROOT = iphoneos;
|
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
|
||||||
SUPPORTS_MACCATALYST = NO;
|
|
||||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
|
|
||||||
SWIFT_VERSION = "$(SWIFT_VERSION)";
|
|
||||||
TARGETED_DEVICE_FAMILY = 1;
|
|
||||||
};
|
|
||||||
name = Debug;
|
|
||||||
};
|
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -850,14 +736,6 @@
|
|||||||
);
|
);
|
||||||
defaultConfigurationName = Debug;
|
defaultConfigurationName = Debug;
|
||||||
};
|
};
|
||||||
E4EA6DC648DF55FF84032EB5 /* Build configuration list for PBXNativeTarget "bitchatShareExtension" */ = {
|
|
||||||
isa = XCConfigurationList;
|
|
||||||
buildConfigurations = (
|
|
||||||
DAC5E82049F8A97360BE63D6 /* Debug configuration for PBXNativeTarget "bitchatShareExtension" */,
|
|
||||||
3DCF45111852FB2AEBE05E31 /* Release configuration for PBXNativeTarget "bitchatShareExtension" */,
|
|
||||||
);
|
|
||||||
defaultConfigurationName = Debug;
|
|
||||||
};
|
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCLocalSwiftPackageReference section */
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
final class VoiceRecorder: NSObject, AVAudioRecorderDelegate {
|
||||||
|
private var recorder: AVAudioRecorder?
|
||||||
|
private(set) var isRecording = false
|
||||||
|
|
||||||
|
func startRecording(to url: URL) throws {
|
||||||
|
let session = AVAudioSession.sharedInstance()
|
||||||
|
try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
|
||||||
|
try session.setActive(true)
|
||||||
|
|
||||||
|
let settings: [String: Any] = [
|
||||||
|
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||||
|
AVSampleRateKey: 44100,
|
||||||
|
AVNumberOfChannelsKey: 1,
|
||||||
|
AVEncoderBitRateKey: 32000
|
||||||
|
]
|
||||||
|
recorder = try AVAudioRecorder(url: url, settings: settings)
|
||||||
|
recorder?.delegate = self
|
||||||
|
recorder?.isMeteringEnabled = true
|
||||||
|
guard recorder?.record() == true else { throw NSError(domain: "VoiceRecorder", code: -1) }
|
||||||
|
isRecording = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func stopRecording(completion: @escaping () -> Void) {
|
||||||
|
guard isRecording else { completion(); return }
|
||||||
|
// Add 500ms padding to avoid clipping
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
|
self?.recorder?.stop()
|
||||||
|
self?.isRecording = false
|
||||||
|
completion()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum VoiceRecorderPaths {
|
||||||
|
static func outgoingURL() throws -> URL {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let base = try fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
let folder = base.appendingPathComponent("bitchat_voicenotes/outgoing", isDirectory: true)
|
||||||
|
if !fm.fileExists(atPath: folder.path) {
|
||||||
|
try fm.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
let ts = ISO8601DateFormatter()
|
||||||
|
ts.formatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime]
|
||||||
|
let name = "voice_\(Int(Date().timeIntervalSince1970)).m4a"
|
||||||
|
return folder.appendingPathComponent(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import Foundation
|
||||||
|
import AVFoundation
|
||||||
|
import Accelerate
|
||||||
|
|
||||||
|
struct WaveformCache {
|
||||||
|
static var shared = WaveformCache()
|
||||||
|
private var cache: [String: [Float]] = [:]
|
||||||
|
mutating func set(_ bins: [Float], for path: String) { cache[path] = bins }
|
||||||
|
func get(_ path: String) -> [Float]? { cache[path] }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WaveformExtractor {
|
||||||
|
static func extractBins(url: URL, binCount: Int = 120) -> [Float] {
|
||||||
|
if let cached = WaveformCache.shared.get(url.path) { return cached }
|
||||||
|
let asset = AVURLAsset(url: url)
|
||||||
|
guard let track = asset.tracks(withMediaType: .audio).first else { return [] }
|
||||||
|
let reader: AVAssetReader
|
||||||
|
do { reader = try AVAssetReader(asset: asset) } catch { return [] }
|
||||||
|
let outputSettings: [String: Any] = [
|
||||||
|
AVFormatIDKey: kAudioFormatLinearPCM,
|
||||||
|
AVLinearPCMBitDepthKey: 16,
|
||||||
|
AVLinearPCMIsBigEndianKey: false,
|
||||||
|
AVLinearPCMIsFloatKey: false,
|
||||||
|
AVLinearPCMIsNonInterleaved: false
|
||||||
|
]
|
||||||
|
let output = AVAssetReaderTrackOutput(track: track, outputSettings: outputSettings)
|
||||||
|
output.alwaysCopiesSampleData = false
|
||||||
|
if reader.canAdd(output) { reader.add(output) } else { return [] }
|
||||||
|
guard reader.startReading() else { return [] }
|
||||||
|
|
||||||
|
var samples: [Float] = []
|
||||||
|
while reader.status == .reading {
|
||||||
|
guard let buffer = output.copyNextSampleBuffer() else { break }
|
||||||
|
if let block = CMSampleBufferGetDataBuffer(buffer) {
|
||||||
|
var length = 0
|
||||||
|
var dataPointer: UnsafeMutablePointer<Int8>?
|
||||||
|
if CMBlockBufferGetDataPointer(block, atOffset: 0, lengthAtOffsetOut: nil, totalLengthOut: &length, dataPointerOut: &dataPointer) == kCMBlockBufferNoErr,
|
||||||
|
let base = dataPointer {
|
||||||
|
// 16-bit signed little endian
|
||||||
|
let count = length / 2
|
||||||
|
var floats = [Float](repeating: 0, count: count)
|
||||||
|
base.withMemoryRebound(to: Int16.self, capacity: count) { ptr in
|
||||||
|
vDSP.convertElements(of: UnsafeBufferPointer(start: ptr, count: count), to: &floats)
|
||||||
|
}
|
||||||
|
// Normalize to [-1,1]
|
||||||
|
var maxVal: Float = 32768.0
|
||||||
|
vDSP.divide(floats, maxVal, result: &floats)
|
||||||
|
samples += floats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CMSampleBufferInvalidate(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !samples.isEmpty, reader.status == .completed || reader.status == .reading else { return [] }
|
||||||
|
// Reduce to bins by RMS per window
|
||||||
|
let window = max(1, samples.count / binCount)
|
||||||
|
var bins: [Float] = []
|
||||||
|
bins.reserveCapacity(binCount)
|
||||||
|
var i = 0
|
||||||
|
while i < samples.count && bins.count < binCount {
|
||||||
|
let end = min(i + window, samples.count)
|
||||||
|
let slice = samples[i..<end]
|
||||||
|
var sum: Float = 0
|
||||||
|
vDSP_svesq(slice.map { $0 }, 1, &sum, vDSP_Length(slice.count))
|
||||||
|
let rms = sqrtf(sum / Float(slice.count))
|
||||||
|
bins.append(min(1.0, rms))
|
||||||
|
i = end
|
||||||
|
}
|
||||||
|
if bins.count < binCount {
|
||||||
|
bins += Array(repeating: 0, count: binCount - bins.count)
|
||||||
|
}
|
||||||
|
WaveformCache.shared.set(bins, for: url.path)
|
||||||
|
return bins
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -39,6 +39,8 @@
|
|||||||
<string>bitchat uses the camera to scan QR codes to verify peers.</string>
|
<string>bitchat uses the camera to scan QR codes to verify peers.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string>
|
<string>bitchat uses your approximate location to compute local geohash channels for optional public chats. Exact GPS is never shared.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>bitchat uses the microphone to record and send short voice notes to nearby peers.</string>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>bluetooth-central</string>
|
<string>bluetooth-central</string>
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
//
|
||||||
|
// BitchatFilePacket.swift
|
||||||
|
// bitchat
|
||||||
|
//
|
||||||
|
// TLV encoder/decoder for file transfer payloads (audio/images)
|
||||||
|
// Types: 0x01 FILE_NAME (utf8), 0x02 FILE_SIZE (8 bytes, BE), 0x03 MIME_TYPE (utf8), 0x04 CONTENT (raw)
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
struct BitchatFilePacket {
|
||||||
|
enum TLVType: UInt8 {
|
||||||
|
case fileName = 0x01
|
||||||
|
case fileSize = 0x02
|
||||||
|
case mimeType = 0x03
|
||||||
|
case content = 0x04
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileName: String
|
||||||
|
let fileSize: UInt64
|
||||||
|
let mimeType: String
|
||||||
|
let content: Data
|
||||||
|
|
||||||
|
func encode() -> Data? {
|
||||||
|
var out = Data()
|
||||||
|
func appendTLV(_ type: TLVType, _ value: Data) {
|
||||||
|
out.append(type.rawValue)
|
||||||
|
let len = UInt16(min(value.count, 0xFFFF))
|
||||||
|
out.append(UInt8((len >> 8) & 0xFF))
|
||||||
|
out.append(UInt8(len & 0xFF))
|
||||||
|
out.append(value.prefix(Int(len)))
|
||||||
|
}
|
||||||
|
// FILE_NAME
|
||||||
|
if let nameData = fileName.data(using: .utf8) { appendTLV(.fileName, nameData) }
|
||||||
|
// FILE_SIZE (8 bytes, BE)
|
||||||
|
var sizeBE = Data()
|
||||||
|
for i in (0..<8).reversed() { sizeBE.append(UInt8((fileSize >> (i * 8)) & 0xFF)) }
|
||||||
|
appendTLV(.fileSize, sizeBE)
|
||||||
|
// MIME_TYPE
|
||||||
|
if let mimeData = mimeType.data(using: .utf8) { appendTLV(.mimeType, mimeData) }
|
||||||
|
// CONTENT
|
||||||
|
appendTLV(.content, content)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
static func decode(from data: Data) -> BitchatFilePacket? {
|
||||||
|
var idx = 0
|
||||||
|
func read(_ n: Int) -> Data? {
|
||||||
|
guard idx + n <= data.count else { return nil }
|
||||||
|
defer { idx += n }
|
||||||
|
return data.subdata(in: idx..<(idx+n))
|
||||||
|
}
|
||||||
|
func read8() -> UInt8? { read(1)?.first }
|
||||||
|
func read16() -> UInt16? {
|
||||||
|
guard let d = read(2) else { return nil }
|
||||||
|
return (UInt16(d[0]) << 8) | UInt16(d[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
var name: String?
|
||||||
|
var size: UInt64?
|
||||||
|
var mime: String?
|
||||||
|
var content = Data()
|
||||||
|
|
||||||
|
while idx < data.count {
|
||||||
|
guard let t = read8(), let tlvType = TLVType(rawValue: t), let len = read16() else { return nil }
|
||||||
|
let l = Int(len)
|
||||||
|
guard let val = read(l) else { return nil }
|
||||||
|
switch tlvType {
|
||||||
|
case .fileName:
|
||||||
|
name = String(data: val, encoding: .utf8)
|
||||||
|
case .fileSize:
|
||||||
|
guard l == 8 else { return nil }
|
||||||
|
var s: UInt64 = 0
|
||||||
|
for b in val { s = (s << 8) | UInt64(b) }
|
||||||
|
size = s
|
||||||
|
case .mimeType:
|
||||||
|
mime = String(data: val, encoding: .utf8)
|
||||||
|
case .content:
|
||||||
|
content = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
guard !content.isEmpty else { return nil }
|
||||||
|
let finalSize = size ?? UInt64(content.count)
|
||||||
|
let finalName = name ?? "file"
|
||||||
|
let finalMime = mime ?? "application/octet-stream"
|
||||||
|
return BitchatFilePacket(fileName: finalName, fileSize: finalSize, mimeType: finalMime, content: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data { func sha256Hex() -> String { SHA256.hash(data: self).map { String(format: "%02x", $0) }.joined() } }
|
||||||
@@ -78,6 +78,7 @@ enum MessageType: UInt8 {
|
|||||||
|
|
||||||
// Fragmentation (simplified)
|
// Fragmentation (simplified)
|
||||||
case fragment = 0x20 // Single fragment type for large messages
|
case fragment = 0x20 // Single fragment type for large messages
|
||||||
|
case fileTransfer = 0x22 // File transfer (audio/images) TLV payload
|
||||||
|
|
||||||
var description: String {
|
var description: String {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -88,6 +89,7 @@ enum MessageType: UInt8 {
|
|||||||
case .noiseHandshake: return "noiseHandshake"
|
case .noiseHandshake: return "noiseHandshake"
|
||||||
case .noiseEncrypted: return "noiseEncrypted"
|
case .noiseEncrypted: return "noiseEncrypted"
|
||||||
case .fragment: return "fragment"
|
case .fragment: return "fragment"
|
||||||
|
case .fileTransfer: return "fileTransfer"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ final class BLEService: NSObject {
|
|||||||
// MARK: - Constants
|
// MARK: - Constants
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5A") // testnet
|
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // testnet
|
||||||
#else
|
#else
|
||||||
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
static let serviceUUID = CBUUID(string: "F47B5E2D-4A9E-4C5A-9B3F-8E1D2C3A4B5C") // mainnet
|
||||||
#endif
|
#endif
|
||||||
@@ -1276,6 +1276,118 @@ final class BLEService: NSObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// File transfer with progress: send TLV payload inside a FILE_TRANSFER packet.
|
||||||
|
// Reports .partiallyDelivered via delegate for the provided messageID during fragmentation.
|
||||||
|
func sendFileTransferTLV(_ payload: Data, recipientPeerID: String?, transferId: String, messageID: String) {
|
||||||
|
// Build packet
|
||||||
|
let recipientData: Data? = recipientPeerID.flatMap { Data(hexString: $0) }
|
||||||
|
var packet = BitchatPacket(
|
||||||
|
type: MessageType.fileTransfer.rawValue,
|
||||||
|
senderID: myPeerIDData,
|
||||||
|
recipientID: recipientData,
|
||||||
|
timestamp: UInt64(Date().timeIntervalSince1970 * 1000),
|
||||||
|
payload: payload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: messageTTL
|
||||||
|
)
|
||||||
|
// For private sends, sign (integrity)
|
||||||
|
if recipientData != nil {
|
||||||
|
if let signed = noiseService.signPacket(packet) { packet = signed }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode once to decide if fragmentation is needed on current links
|
||||||
|
let pad = padPolicy(for: packet.type)
|
||||||
|
guard let data = packet.toBinaryData(padding: pad) else { return }
|
||||||
|
|
||||||
|
// Determine max link MTU across active links similar to sendOnAllLinks
|
||||||
|
let states = snapshotPeripheralStates()
|
||||||
|
var minCentralWriteLen: Int?
|
||||||
|
for s in states where s.isConnected {
|
||||||
|
let m = s.peripheral.maximumWriteValueLength(for: .withoutResponse)
|
||||||
|
minCentralWriteLen = minCentralWriteLen.map { min($0, m) } ?? m
|
||||||
|
}
|
||||||
|
var minNotifyLen: Int?
|
||||||
|
do {
|
||||||
|
let (centrals, _) = snapshotSubscribedCentrals()
|
||||||
|
if !centrals.isEmpty {
|
||||||
|
minNotifyLen = centrals.map { $0.maximumUpdateValueLength }.min()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let needsFragment = {
|
||||||
|
if let minLen = [minCentralWriteLen, minNotifyLen].compactMap({ $0 }).min() {
|
||||||
|
return data.count > minLen
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}()
|
||||||
|
|
||||||
|
if needsFragment {
|
||||||
|
let overhead = 13 + 8 + 8 + 13
|
||||||
|
let minLen = [minCentralWriteLen, minNotifyLen].compactMap({ $0 }).min() ?? defaultFragmentSize
|
||||||
|
let chunk = max(64, minLen - overhead)
|
||||||
|
|
||||||
|
// Create fragments and emit progress per fragment
|
||||||
|
guard let fullData = packet.toBinaryData(padding: pad) else { return }
|
||||||
|
let fragmentID = Data((0..<8).map { _ in UInt8.random(in: 0...255) })
|
||||||
|
let fragments = stride(from: 0, to: fullData.count, by: chunk).map { offset in
|
||||||
|
Data(fullData[offset..<min(offset + chunk, fullData.count)])
|
||||||
|
}
|
||||||
|
let total = fragments.count
|
||||||
|
|
||||||
|
// Pause scanning for long trains as in sendFragmentedPacket
|
||||||
|
if total > 4 {
|
||||||
|
bleQueue.async { [weak self] in
|
||||||
|
guard let self = self, let c = self.centralManager, c.state == .poweredOn else { return }
|
||||||
|
if c.isScanning { c.stopScan() }
|
||||||
|
let expectedMs = min(TransportConfig.bleExpectedWriteMaxMs, total * TransportConfig.bleExpectedWritePerFragmentMs)
|
||||||
|
self.bleQueue.asyncAfter(deadline: .now() + .milliseconds(expectedMs)) { [weak self] in self?.startScanning() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (index, fragment) in fragments.enumerated() {
|
||||||
|
var fragPayload = Data()
|
||||||
|
fragPayload.append(fragmentID)
|
||||||
|
fragPayload.append(contentsOf: withUnsafeBytes(of: UInt16(index).bigEndian) { Data($0) })
|
||||||
|
fragPayload.append(contentsOf: withUnsafeBytes(of: UInt16(total).bigEndian) { Data($0) })
|
||||||
|
fragPayload.append(packet.type)
|
||||||
|
fragPayload.append(fragment)
|
||||||
|
|
||||||
|
let fragmentRecipient: Data? = recipientData
|
||||||
|
let fragmentPacket = BitchatPacket(
|
||||||
|
type: MessageType.fragment.rawValue,
|
||||||
|
senderID: packet.senderID,
|
||||||
|
recipientID: fragmentRecipient,
|
||||||
|
timestamp: packet.timestamp,
|
||||||
|
payload: fragPayload,
|
||||||
|
signature: nil,
|
||||||
|
ttl: packet.ttl
|
||||||
|
)
|
||||||
|
let perFragMs = (recipientData != nil) ? TransportConfig.bleFragmentSpacingDirectedMs : TransportConfig.bleFragmentSpacingMs
|
||||||
|
let delayMs = index * perFragMs
|
||||||
|
|
||||||
|
// Emit progress to UI just before scheduling send
|
||||||
|
notifyUI { [weak self] in
|
||||||
|
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .partiallyDelivered(reached: index, total: total))
|
||||||
|
}
|
||||||
|
|
||||||
|
messageQueue.asyncAfter(deadline: .now() + .milliseconds(delayMs)) { [weak self] in
|
||||||
|
self?.broadcastPacket(fragmentPacket)
|
||||||
|
if index == total - 1 {
|
||||||
|
self?.notifyUI { [weak self] in
|
||||||
|
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Single packet path
|
||||||
|
broadcastPacket(packet)
|
||||||
|
notifyUI { [weak self] in
|
||||||
|
self?.delegate?.didUpdateMessageDeliveryStatus(messageID, status: .sent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func handleFragment(_ packet: BitchatPacket, from peerID: String) {
|
private func handleFragment(_ packet: BitchatPacket, from peerID: String) {
|
||||||
// Don't process our own fragments
|
// Don't process our own fragments
|
||||||
@@ -1418,6 +1530,9 @@ final class BLEService: NSObject {
|
|||||||
|
|
||||||
case .fragment:
|
case .fragment:
|
||||||
handleFragment(packet, from: senderID)
|
handleFragment(packet, from: senderID)
|
||||||
|
|
||||||
|
case .fileTransfer:
|
||||||
|
handleFileTransfer(packet, from: senderID)
|
||||||
|
|
||||||
case .leave:
|
case .leave:
|
||||||
handleLeave(packet, from: senderID)
|
handleLeave(packet, from: senderID)
|
||||||
@@ -1460,6 +1575,64 @@ final class BLEService: NSObject {
|
|||||||
messageQueue.asyncAfter(deadline: .now() + .milliseconds(decision.delayMs), execute: work)
|
messageQueue.asyncAfter(deadline: .now() + .milliseconds(decision.delayMs), execute: work)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleFileTransfer(_ packet: BitchatPacket, from peerID: String) {
|
||||||
|
// Decode TLV
|
||||||
|
guard let file = BitchatFilePacket.decode(from: packet.payload) else {
|
||||||
|
SecureLogger.error("❌ Failed to decode file TLV from \(peerID)", category: .session)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Persist to app files in type-specific folder
|
||||||
|
let isAudio = file.mimeType.lowercased().hasPrefix("audio/")
|
||||||
|
let subfolder = isAudio ? "voicenotes/incoming" : "images/incoming"
|
||||||
|
let ext: String = {
|
||||||
|
if let inferred = file.fileName.split(separator: ".").last { return String(inferred) }
|
||||||
|
if isAudio { return "m4a" }
|
||||||
|
return "bin"
|
||||||
|
}()
|
||||||
|
let unique = "\(Int(Date().timeIntervalSince1970))_\(UUID().uuidString.prefix(8)).\(ext)"
|
||||||
|
guard let savedURL = saveToAppFiles(subpath: subfolder, fileName: unique, data: file.content) else { return }
|
||||||
|
|
||||||
|
// Build synthetic message
|
||||||
|
let nickname = peerNickname(peerID: peerID) ?? "user"
|
||||||
|
let ts = Date(timeIntervalSince1970: TimeInterval(packet.timestamp) / 1000.0)
|
||||||
|
let marker = isAudio ? "[voice]" : "[image]"
|
||||||
|
let content = "\(marker) \(savedURL.path)"
|
||||||
|
|
||||||
|
let message = BitchatMessage(
|
||||||
|
id: UUID().uuidString,
|
||||||
|
sender: nickname,
|
||||||
|
content: content,
|
||||||
|
timestamp: ts,
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: packet.recipientID != nil,
|
||||||
|
recipientNickname: nil,
|
||||||
|
senderPeerID: peerID,
|
||||||
|
mentions: nil,
|
||||||
|
deliveryStatus: nil
|
||||||
|
)
|
||||||
|
notifyUI { [weak self] in
|
||||||
|
self?.delegate?.didReceiveMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func saveToAppFiles(subpath: String, fileName: String, data: Data) -> URL? {
|
||||||
|
do {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let base = try fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
let folder = base.appendingPathComponent("bitchat_\(subpath)", isDirectory: true)
|
||||||
|
if !fm.fileExists(atPath: folder.path) {
|
||||||
|
try fm.createDirectory(at: folder, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
let url = folder.appendingPathComponent(fileName)
|
||||||
|
try data.write(to: url, options: .atomic)
|
||||||
|
return url
|
||||||
|
} catch {
|
||||||
|
SecureLogger.error("❌ Failed to save incoming file: \(error)", category: .session)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func handleAnnounce(_ packet: BitchatPacket, from peerID: String) {
|
private func handleAnnounce(_ packet: BitchatPacket, from peerID: String) {
|
||||||
guard let announcement = AnnouncementPacket.decode(from: packet.payload) else {
|
guard let announcement = AnnouncementPacket.decode(from: packet.payload) else {
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import Combine
|
|||||||
|
|
||||||
// Minimal Nostr transport conforming to Transport for offline sending
|
// Minimal Nostr transport conforming to Transport for offline sending
|
||||||
final class NostrTransport: Transport {
|
final class NostrTransport: Transport {
|
||||||
|
func sendFileTransferTLV(_ payload: Data, recipientPeerID: String?, transferId: String, messageID: String) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
weak var delegate: BitchatDelegate?
|
weak var delegate: BitchatDelegate?
|
||||||
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
weak var peerEventsDelegate: TransportPeerEventsDelegate?
|
||||||
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
var peerSnapshotPublisher: AnyPublisher<[TransportPeerSnapshot], Never> {
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ protocol Transport: AnyObject {
|
|||||||
func sendBroadcastAnnounce()
|
func sendBroadcastAnnounce()
|
||||||
func sendDeliveryAck(for messageID: String, to peerID: String)
|
func sendDeliveryAck(for messageID: String, to peerID: String)
|
||||||
|
|
||||||
|
// Media transfer (file transfer TLV)
|
||||||
|
func sendFileTransferTLV(_ payload: Data, recipientPeerID: String?, transferId: String, messageID: String)
|
||||||
|
|
||||||
// QR verification (optional for transports)
|
// QR verification (optional for transports)
|
||||||
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data)
|
func sendVerifyChallenge(to peerID: String, noiseKeyHex: String, nonceA: Data)
|
||||||
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data)
|
func sendVerifyResponse(to peerID: String, noiseKeyHex: String, nonceA: Data)
|
||||||
|
|||||||
@@ -6048,5 +6048,65 @@ private func checkForMentions(_ message: BitchatMessage) {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Voice Notes (Audio)
|
||||||
|
@MainActor
|
||||||
|
func sendVoiceNote(fileURL: URL) {
|
||||||
|
// Read file data
|
||||||
|
guard let data = try? Data(contentsOf: fileURL) else { return }
|
||||||
|
let name = fileURL.lastPathComponent
|
||||||
|
let mime = "audio/mp4"
|
||||||
|
let tlv = BitchatFilePacket(fileName: name, fileSize: UInt64(data.count), mimeType: mime, content: data)
|
||||||
|
guard let payload = tlv.encode() else { return }
|
||||||
|
let transferId = payload.sha256Hex()
|
||||||
|
|
||||||
|
let contentMarker = "[voice] \(fileURL.path)"
|
||||||
|
let now = Date()
|
||||||
|
|
||||||
|
if let peer = selectedPrivateChatPeer {
|
||||||
|
let messageID = UUID().uuidString
|
||||||
|
let msg = BitchatMessage(
|
||||||
|
id: messageID,
|
||||||
|
sender: nickname,
|
||||||
|
content: contentMarker,
|
||||||
|
timestamp: now,
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: true,
|
||||||
|
recipientNickname: meshService.peerNickname(peerID: peer),
|
||||||
|
senderPeerID: meshService.myPeerID,
|
||||||
|
mentions: nil,
|
||||||
|
deliveryStatus: .sending
|
||||||
|
)
|
||||||
|
appendToPrivateChat(peerID: peer, message: msg)
|
||||||
|
meshService.sendFileTransferTLV(payload, recipientPeerID: peer, transferId: transferId, messageID: messageID)
|
||||||
|
} else {
|
||||||
|
let messageID = UUID().uuidString
|
||||||
|
let msg = BitchatMessage(
|
||||||
|
id: messageID,
|
||||||
|
sender: nickname,
|
||||||
|
content: contentMarker,
|
||||||
|
timestamp: now,
|
||||||
|
isRelay: false,
|
||||||
|
originalSender: nil,
|
||||||
|
isPrivate: false,
|
||||||
|
recipientNickname: nil,
|
||||||
|
senderPeerID: meshService.myPeerID,
|
||||||
|
mentions: nil,
|
||||||
|
deliveryStatus: .sending
|
||||||
|
)
|
||||||
|
publicBuffer.append(msg)
|
||||||
|
schedulePublicFlush()
|
||||||
|
meshService.sendFileTransferTLV(payload, recipientPeerID: nil, transferId: transferId, messageID: messageID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func appendToPrivateChat(peerID: String, message: BitchatMessage) {
|
||||||
|
var arr = privateChats[peerID] ?? []
|
||||||
|
arr.append(message)
|
||||||
|
privateChats[peerID] = arr
|
||||||
|
objectWillChange.send()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// End of ChatViewModel class
|
// End of ChatViewModel class
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ struct ContentView: View {
|
|||||||
// Window sizes for rendering (infinite scroll up)
|
// Window sizes for rendering (infinite scroll up)
|
||||||
@State private var windowCountPublic: Int = 300
|
@State private var windowCountPublic: Int = 300
|
||||||
@State private var windowCountPrivate: [String: Int] = [:]
|
@State private var windowCountPrivate: [String: Int] = [:]
|
||||||
|
#if os(iOS)
|
||||||
|
@State private var voiceRecorder = VoiceRecorder()
|
||||||
|
@State private var isRecordingVoice = false
|
||||||
|
#endif
|
||||||
|
|
||||||
// MARK: - Computed Properties
|
// MARK: - Computed Properties
|
||||||
|
|
||||||
@@ -305,20 +309,44 @@ struct ContentView: View {
|
|||||||
// Precompute heavy token scans once per row
|
// Precompute heavy token scans once per row
|
||||||
let cashuTokens = message.content.extractCashuTokens()
|
let cashuTokens = message.content.extractCashuTokens()
|
||||||
let lightningLinks = message.content.extractLightningLinks()
|
let lightningLinks = message.content.extractLightningLinks()
|
||||||
HStack(alignment: .top, spacing: 0) {
|
let isVoice = message.content.hasPrefix("[voice] ")
|
||||||
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuTokens.isEmpty
|
if isVoice {
|
||||||
let isExpanded = expandedMessageIDs.contains(message.id)
|
// Hide the raw marker text; render custom voice view
|
||||||
Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme))
|
HStack(alignment: .center, spacing: 8) {
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
let path = String(message.content.dropFirst("[voice] ".count))
|
||||||
.lineLimit(isLong && !isExpanded ? TransportConfig.uiLongMessageLineLimit : nil)
|
let prog: Double? = {
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
if let st = message.deliveryStatus {
|
||||||
|
if case .partiallyDelivered(let r, let t) = st, t > 0 {
|
||||||
// Delivery status indicator for private messages
|
return min(1.0, max(0.0, Double(r) / Double(t)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
VoiceMessageRow(fileURL: URL(fileURLWithPath: path), sendProgress: prog)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
// Delivery status for our own private voice messages
|
||||||
if message.isPrivate && message.sender == viewModel.nickname,
|
if message.isPrivate && message.sender == viewModel.nickname,
|
||||||
let status = message.deliveryStatus {
|
let status = message.deliveryStatus {
|
||||||
DeliveryStatusView(status: status, colorScheme: colorScheme)
|
DeliveryStatusView(status: status, colorScheme: colorScheme)
|
||||||
.padding(.leading, 4)
|
.padding(.leading, 4)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
HStack(alignment: .top, spacing: 0) {
|
||||||
|
let isLong = (message.content.count > TransportConfig.uiLongMessageLengthThreshold || message.content.hasVeryLongToken(threshold: TransportConfig.uiVeryLongTokenThreshold)) && cashuTokens.isEmpty
|
||||||
|
let isExpanded = expandedMessageIDs.contains(message.id)
|
||||||
|
Text(viewModel.formatMessageAsText(message, colorScheme: colorScheme))
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.lineLimit(isLong && !isExpanded ? TransportConfig.uiLongMessageLineLimit : nil)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand/Collapse for very long messages
|
// Expand/Collapse for very long messages
|
||||||
@@ -870,17 +898,69 @@ struct ContentView: View {
|
|||||||
sendMessage()
|
sendMessage()
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: sendMessage) {
|
Group {
|
||||||
Image(systemName: "arrow.up.circle.fill")
|
if messageText.isEmpty {
|
||||||
.font(.system(size: 20))
|
#if os(iOS)
|
||||||
.foregroundColor(messageText.isEmpty ? Color.gray :
|
Image(systemName: isRecordingVoice ? "mic.circle.fill" : "mic.circle")
|
||||||
viewModel.selectedPrivateChatPeer != nil
|
.font(.system(size: 22))
|
||||||
? Color.orange : textColor)
|
.foregroundColor(isRecordingVoice ? .red : (viewModel.selectedPrivateChatPeer != nil ? Color.orange : textColor))
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
.gesture(
|
||||||
|
LongPressGesture(minimumDuration: 0.2)
|
||||||
|
.onChanged { _ in
|
||||||
|
if !isRecordingVoice {
|
||||||
|
do {
|
||||||
|
let url = try VoiceRecorderPaths.outgoingURL()
|
||||||
|
try voiceRecorder.startRecording(to: url)
|
||||||
|
isRecordingVoice = true
|
||||||
|
} catch {
|
||||||
|
isRecordingVoice = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onEnded { _ in
|
||||||
|
voiceRecorder.stopRecording {
|
||||||
|
isRecordingVoice = false
|
||||||
|
// Send the most recent recorded file
|
||||||
|
do {
|
||||||
|
let fm = FileManager.default
|
||||||
|
let base = try fm.url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
|
||||||
|
let folder = base.appendingPathComponent("bitchat_voicenotes/outgoing", isDirectory: true)
|
||||||
|
let files = try fm.contentsOfDirectory(at: folder, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles])
|
||||||
|
let latest = files.sorted { (a, b) -> Bool in
|
||||||
|
let ad = (try? a.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
||||||
|
let bd = (try? b.resourceValues(forKeys: [.contentModificationDateKey]).contentModificationDate) ?? .distantPast
|
||||||
|
return ad > bd
|
||||||
|
}.first
|
||||||
|
if let u = latest { viewModel.sendVoiceNote(fileURL: u) }
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
#else
|
||||||
|
// Non-iOS: keep send button placeholder
|
||||||
|
Button(action: sendMessage) {
|
||||||
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(Color.gray)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
Button(action: sendMessage) {
|
||||||
|
Image(systemName: "arrow.up.circle.fill")
|
||||||
|
.font(.system(size: 20))
|
||||||
|
.foregroundColor(viewModel.selectedPrivateChatPeer != nil ? Color.orange : textColor)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.padding(.trailing, 12)
|
||||||
|
.accessibilityLabel("Send message")
|
||||||
|
.accessibilityHint("Double tap to send")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
|
||||||
.padding(.trailing, 12)
|
|
||||||
.accessibilityLabel("Send message")
|
|
||||||
.accessibilityHint(messageText.isEmpty ? "Enter a message to send" : "Double tap to send")
|
|
||||||
}
|
}
|
||||||
.padding(.vertical, 8)
|
.padding(.vertical, 8)
|
||||||
.background(backgroundColor.opacity(0.95))
|
.background(backgroundColor.opacity(0.95))
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
struct VoiceMessageRow: View {
|
||||||
|
let fileURL: URL
|
||||||
|
var sendProgress: Double? = nil // 0..1 filling during send
|
||||||
|
@State private var bins: [Float] = []
|
||||||
|
@State private var isPlaying = false
|
||||||
|
@State private var progress: Double = 0
|
||||||
|
@State private var duration: TimeInterval = 0
|
||||||
|
@State private var player: AVAudioPlayer?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Button(action: togglePlay) {
|
||||||
|
Image(systemName: isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.system(size: 22))
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
|
||||||
|
GeometryReader { geo in
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
// Base waveform
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ForEach(Array(bins.enumerated()), id: \.offset) { _, v in
|
||||||
|
let h = max(2, CGFloat(v) * geo.size.height)
|
||||||
|
Capsule().fill(Color.gray.opacity(0.4)).frame(width: 2, height: h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Filled progress
|
||||||
|
let activeProgress = sendProgress ?? progress
|
||||||
|
let filled = Int(Double(bins.count) * activeProgress)
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
ForEach(0..<max(0, min(bins.count, filled)), id: \.self) { i in
|
||||||
|
let v = bins[i]
|
||||||
|
let h = max(2, CGFloat(v) * geo.size.height)
|
||||||
|
Capsule().fill(Color.blue).frame(width: 2, height: h)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 26)
|
||||||
|
|
||||||
|
Text(formattedDuration)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
.onAppear(perform: load)
|
||||||
|
.onDisappear { player?.stop(); isPlaying = false }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var formattedDuration: String {
|
||||||
|
let total = duration
|
||||||
|
let mins = Int(total) / 60
|
||||||
|
let secs = Int(total) % 60
|
||||||
|
return String(format: "%d:%02d", mins, secs)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func load() {
|
||||||
|
bins = WaveformExtractor.extractBins(url: fileURL, binCount: 120)
|
||||||
|
do {
|
||||||
|
player = try AVAudioPlayer(contentsOf: fileURL)
|
||||||
|
duration = player?.duration ?? 0
|
||||||
|
player?.prepareToPlay()
|
||||||
|
startProgressTimer()
|
||||||
|
} catch {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func togglePlay() {
|
||||||
|
guard let p = player else { return }
|
||||||
|
if isPlaying { p.pause(); isPlaying = false }
|
||||||
|
else { p.play(); isPlaying = true; startProgressTimer() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startProgressTimer() {
|
||||||
|
guard let p = player else { return }
|
||||||
|
Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { t in
|
||||||
|
if !isPlaying || p.duration == 0 { t.invalidate(); return }
|
||||||
|
progress = min(1.0, p.currentTime / p.duration)
|
||||||
|
if progress >= 1.0 { isPlaying = false; t.invalidate() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.chat.bitchat</string>
|
<string>group.chat.bitchat-local</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.device.bluetooth</key>
|
<key>com.apple.security.device.bluetooth</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.application-groups</key>
|
<key>com.apple.security.application-groups</key>
|
||||||
<array>
|
<array>
|
||||||
<string>group.chat.bitchat</string>
|
<string>group.chat.bitchat-local</string>
|
||||||
</array>
|
</array>
|
||||||
<key>com.apple.security.device.bluetooth</key>
|
<key>com.apple.security.device.bluetooth</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
Reference in New Issue
Block a user