mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-25 01:05:19 +00:00
Add iOS share extension with rich link previews
- Implement share extension to receive URLs from other apps - Display shared links with emoji indicator and rich preview cards - Add custom compact link preview with image, title, and domain - Configure app groups for data sharing between extension and main app - Handle URL detection and parsing in share extension - Update message formatting to show only emoji for shared links - Add proper entitlements and activation rules for share extension
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 */
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; };
|
1F48A8CEEE9399D1EBD08F0C /* OptimizedBloomFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = CB043CA5EEB9AC8B07D61E97 /* OptimizedBloomFilter.swift */; };
|
||||||
24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; };
|
24F17B1446E13F42652B7B08 /* PasswordProtectedChannelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 036A1A705AAF9EC21F4354BE /* PasswordProtectedChannelTests.swift */; };
|
||||||
2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
|
2E71E320EA921498C57E023B /* BitchatMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */; };
|
||||||
|
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; };
|
||||||
4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; };
|
4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; };
|
||||||
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
4B747085D07A1BCE0F5BA612 /* BinaryProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2136C3E22D02D4A8DBE7EAB /* BinaryProtocol.swift */; };
|
||||||
4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; };
|
4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67A85BFDDE65B4CD8BDF6DDB /* MessageRetentionService.swift */; };
|
||||||
@@ -26,6 +27,7 @@
|
|||||||
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
|
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3448F84BF86A42A3CC4A9379 /* NotificationService.swift */; };
|
||||||
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
7576A357B278E5733E9D9F33 /* ChatViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B8F7B7D55092C2540A7996 /* ChatViewModel.swift */; };
|
||||||
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
7A50E2F04A3515A7E90EEAE4 /* BluetoothMeshService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5C3D880FF8AE1673B20E1E3 /* BluetoothMeshService.swift */; };
|
||||||
|
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */; };
|
||||||
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 */; };
|
||||||
@@ -127,13 +129,14 @@
|
|||||||
3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatMessageTests.swift; sourceTree = "<group>"; };
|
3FA8FF26ABDC1C642A8C7AE5 /* BitchatMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BitchatMessageTests.swift; sourceTree = "<group>"; };
|
||||||
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = "<group>"; };
|
527EB217EFDFAD4CF1C91F07 /* bitchat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = bitchat.entitlements; sourceTree = "<group>"; };
|
||||||
53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
53D535D9CE0B875F47402290 /* BinaryProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinaryProtocolTests.swift; sourceTree = "<group>"; };
|
||||||
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
61F92EBA29C47C0FCC482F1F /* bitchatShareExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = bitchatShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
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; };
|
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; };
|
997D512074C64904D75DDD40 /* bitchat.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = bitchat.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewView.swift; sourceTree = "<group>"; };
|
||||||
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>"; };
|
||||||
@@ -220,6 +223,7 @@
|
|||||||
children = (
|
children = (
|
||||||
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
|
763E0DBA9492A654FC0CDCB9 /* AppInfoView.swift */,
|
||||||
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
A08E03AA0C63E97C91749AEC /* ContentView.swift */,
|
||||||
|
9AC141774F6671FCDC347DC7 /* LinkPreviewView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -366,12 +370,15 @@
|
|||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1430;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
0576A29205865664C0937536 = {
|
0576A29205865664C0937536 = {
|
||||||
|
DevelopmentTeam = L3N5LHJD5Y;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
57CA17A36A2532A6CFF367BB = {
|
57CA17A36A2532A6CFF367BB = {
|
||||||
|
DevelopmentTeam = L3N5LHJD5Y;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
AF077EA0474EDEDE2C72716C = {
|
AF077EA0474EDEDE2C72716C = {
|
||||||
|
DevelopmentTeam = L3N5LHJD5Y;
|
||||||
ProvisioningStyle = Automatic;
|
ProvisioningStyle = Automatic;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -386,6 +393,7 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 18198ED912AAF495D8AF7763;
|
mainGroup = 18198ED912AAF495D8AF7763;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
preferredProjectObjectVersion = 54;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
@@ -442,6 +450,7 @@
|
|||||||
8DCEEA289EF7C49E7CD38B08 /* DeliveryTracker.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 */,
|
||||||
|
31D147471B9F4E2815352DDA /* LinkPreviewView.swift in Sources */,
|
||||||
4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */,
|
4E778E5A414571ACAC2A0F01 /* MessageRetentionService.swift in Sources */,
|
||||||
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
|
C99763A4761567F587D21688 /* MessageRetryService.swift in Sources */,
|
||||||
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
749D8CF8A362B6CD0786782D /* NotificationService.swift in Sources */,
|
||||||
@@ -465,6 +474,7 @@
|
|||||||
CD0AE423F03AC52BAFC16834 /* DeliveryTracker.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 */,
|
||||||
|
7A5B1AB5642FEC168E917949 /* LinkPreviewView.swift in Sources */,
|
||||||
4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */,
|
4274B6016F755946FBF2513E /* MessageRetentionService.swift in Sources */,
|
||||||
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
|
CEAE115C9C3EB3C4ED82F128 /* MessageRetryService.swift in Sources */,
|
||||||
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
61C81ED5F679D5E973EE0C07 /* NotificationService.swift in Sources */,
|
||||||
@@ -623,12 +633,9 @@
|
|||||||
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_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||||
SUPPORTS_MACCATALYST = 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;
|
||||||
};
|
};
|
||||||
@@ -673,12 +680,9 @@
|
|||||||
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_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||||
SUPPORTS_MACCATALYST = 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;
|
||||||
};
|
};
|
||||||
@@ -692,6 +696,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
@@ -776,6 +781,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
CODE_SIGN_ENTITLEMENTS = bitchat/bitchat.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
COMBINE_HIDPI_IMAGES = YES;
|
COMBINE_HIDPI_IMAGES = YES;
|
||||||
|
DEVELOPMENT_TEAM = L3N5LHJD5Y;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
INFOPLIST_FILE = bitchat/Info.plist;
|
INFOPLIST_FILE = bitchat/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||||
|
|||||||
@@ -58,19 +58,24 @@ struct BitchatApp: App {
|
|||||||
private func checkForSharedContent() {
|
private func checkForSharedContent() {
|
||||||
// Check app group for shared content from extension
|
// Check app group for shared content from extension
|
||||||
guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else {
|
guard let userDefaults = UserDefaults(suiteName: "group.chat.bitchat") else {
|
||||||
print("Failed to access app group UserDefaults")
|
print("DEBUG: Failed to access app group UserDefaults")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let sharedContent = userDefaults.string(forKey: "sharedContent"),
|
guard let sharedContent = userDefaults.string(forKey: "sharedContent"),
|
||||||
let sharedDate = userDefaults.object(forKey: "sharedContentDate") as? Date else {
|
let sharedDate = userDefaults.object(forKey: "sharedContentDate") as? Date else {
|
||||||
print("No shared content found in UserDefaults")
|
print("DEBUG: No shared content found in UserDefaults")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
print("DEBUG: Found shared content: \(sharedContent)")
|
||||||
|
print("DEBUG: Shared date: \(sharedDate)")
|
||||||
|
print("DEBUG: Time since shared: \(Date().timeIntervalSince(sharedDate)) seconds")
|
||||||
|
|
||||||
// Only process if shared within last 30 seconds
|
// Only process if shared within last 30 seconds
|
||||||
if Date().timeIntervalSince(sharedDate) < 30 {
|
if Date().timeIntervalSince(sharedDate) < 30 {
|
||||||
let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text"
|
let contentType = userDefaults.string(forKey: "sharedContentType") ?? "text"
|
||||||
|
print("DEBUG: Content type: \(contentType)")
|
||||||
|
|
||||||
// Clear the shared content
|
// Clear the shared content
|
||||||
userDefaults.removeObject(forKey: "sharedContent")
|
userDefaults.removeObject(forKey: "sharedContent")
|
||||||
@@ -93,21 +98,28 @@ struct BitchatApp: App {
|
|||||||
// Send the shared content after a short delay
|
// Send the shared content after a short delay
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
||||||
if contentType == "url" {
|
if contentType == "url" {
|
||||||
|
print("DEBUG: Processing URL content")
|
||||||
// Try to parse as JSON first
|
// Try to parse as JSON first
|
||||||
if let data = sharedContent.data(using: .utf8),
|
if let data = sharedContent.data(using: .utf8),
|
||||||
let urlData = try? JSONSerialization.jsonObject(with: data) as? [String: String],
|
let urlData = try? JSONSerialization.jsonObject(with: data) as? [String: String],
|
||||||
let url = urlData["url"],
|
let url = urlData["url"],
|
||||||
let title = urlData["title"] {
|
let title = urlData["title"] {
|
||||||
// Send formatted link with title and URL
|
// Send just emoji with hidden markdown link
|
||||||
self.chatViewModel.sendMessage("[\(title)](\(url))")
|
let markdownLink = "👇 [\(title)](\(url))"
|
||||||
|
print("DEBUG: Sending markdown link: \(markdownLink)")
|
||||||
|
self.chatViewModel.sendMessage(markdownLink)
|
||||||
} else {
|
} else {
|
||||||
// Fallback to simple URL
|
// Fallback to simple URL
|
||||||
|
print("DEBUG: Failed to parse JSON, sending as plain URL")
|
||||||
self.chatViewModel.sendMessage("Shared link: \(sharedContent)")
|
self.chatViewModel.sendMessage("Shared link: \(sharedContent)")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
print("DEBUG: Sending plain text: \(sharedContent)")
|
||||||
self.chatViewModel.sendMessage(sharedContent)
|
self.chatViewModel.sendMessage(sharedContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
print("DEBUG: Shared content is too old, ignoring")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,5 +40,14 @@
|
|||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>bitchat</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1498,8 +1498,34 @@ class ChatViewModel: ObservableObject {
|
|||||||
senderStyle.font = .system(size: 14, weight: .medium, design: .monospaced)
|
senderStyle.font = .system(size: 14, weight: .medium, design: .monospaced)
|
||||||
result.append(sender.mergingAttributes(senderStyle))
|
result.append(sender.mergingAttributes(senderStyle))
|
||||||
|
|
||||||
// Process content with hashtags and mentions
|
// Process content with hashtags, mentions, and markdown links
|
||||||
let content = message.content
|
var content = message.content
|
||||||
|
|
||||||
|
// First, check if content starts with 👇 followed by markdown link
|
||||||
|
if content.hasPrefix("👇 [") {
|
||||||
|
// This is a URL share - remove everything after the emoji
|
||||||
|
if let linkStart = content.firstIndex(of: "[") {
|
||||||
|
let indexBeforeLink = content.index(before: linkStart)
|
||||||
|
content = String(content[..<indexBeforeLink])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle normal markdown links
|
||||||
|
let markdownLinkPattern = #"\[([^\]]+)\]\(([^)]+)\)"#
|
||||||
|
if let markdownRegex = try? NSRegularExpression(pattern: markdownLinkPattern, options: []) {
|
||||||
|
let markdownMatches = markdownRegex.matches(in: content, options: [], range: NSRange(location: 0, length: content.count))
|
||||||
|
|
||||||
|
// Process matches in reverse order to maintain string indices
|
||||||
|
for match in markdownMatches.reversed() {
|
||||||
|
if let fullRange = Range(match.range, in: content),
|
||||||
|
let titleRange = Range(match.range(at: 1), in: content) {
|
||||||
|
// Normal markdown link - replace with just the title
|
||||||
|
let linkTitle = String(content[titleRange])
|
||||||
|
content.replaceSubrange(fullRange, with: linkTitle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let hashtagPattern = "#([a-zA-Z0-9_]+)"
|
let hashtagPattern = "#([a-zA-Z0-9_]+)"
|
||||||
let mentionPattern = "@([a-zA-Z0-9_]+)"
|
let mentionPattern = "@([a-zA-Z0-9_]+)"
|
||||||
|
|
||||||
|
|||||||
@@ -433,11 +433,16 @@ struct ContentView: View {
|
|||||||
|
|
||||||
// Check for links and show preview
|
// Check for links and show preview
|
||||||
if let markdownLink = message.content.extractMarkdownLink() {
|
if let markdownLink = message.content.extractMarkdownLink() {
|
||||||
LinkPreviewView(url: markdownLink.url, title: markdownLink.title)
|
// Don't show link preview if the message is just the emoji
|
||||||
.padding(.top, 4)
|
let cleanContent = message.content.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if cleanContent.hasPrefix("👇") {
|
||||||
|
LinkPreviewView(url: markdownLink.url, title: markdownLink.title)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Check for plain URLs
|
// Check for plain URLs
|
||||||
let urls = message.content.extractURLs()
|
let urls = message.content.extractURLs()
|
||||||
|
let _ = urls.isEmpty ? nil : print("DEBUG: Found \(urls.count) plain URLs in message")
|
||||||
ForEach(urls.prefix(3), id: \.url) { urlInfo in
|
ForEach(urls.prefix(3), id: \.url) { urlInfo in
|
||||||
LinkPreviewView(url: urlInfo.url, title: nil)
|
LinkPreviewView(url: urlInfo.url, title: nil)
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ struct LinkPreviewView: View {
|
|||||||
let url: URL
|
let url: URL
|
||||||
let title: String?
|
let title: String?
|
||||||
@Environment(\.colorScheme) var colorScheme
|
@Environment(\.colorScheme) var colorScheme
|
||||||
@State private var isLoadingMetadata = false
|
@State private var metadata: LPLinkMetadata?
|
||||||
@State private var metadata: LinkMetadata?
|
|
||||||
|
|
||||||
private var textColor: Color {
|
private var textColor: Color {
|
||||||
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
colorScheme == .dark ? Color.green : Color(red: 0, green: 0.5, blue: 0)
|
||||||
@@ -31,6 +30,18 @@ struct LinkPreviewView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
// Always use our custom compact view for consistent appearance
|
||||||
|
compactLinkView
|
||||||
|
.onAppear {
|
||||||
|
loadMetadata()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@State private var previewImage: UIImage? = nil
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private var compactLinkView: some View {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
UIApplication.shared.open(url)
|
UIApplication.shared.open(url)
|
||||||
@@ -38,71 +49,183 @@ struct LinkPreviewView: View {
|
|||||||
NSWorkspace.shared.open(url)
|
NSWorkspace.shared.open(url)
|
||||||
#endif
|
#endif
|
||||||
}) {
|
}) {
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
HStack(spacing: 12) {
|
||||||
// Title
|
// Preview image or icon
|
||||||
Text(metadata?.title ?? title ?? url.host ?? "Link")
|
Group {
|
||||||
.font(.system(size: 14, weight: .medium, design: .monospaced))
|
#if os(iOS)
|
||||||
.foregroundColor(textColor)
|
if let image = previewImage {
|
||||||
.lineLimit(2)
|
Image(uiImage: image)
|
||||||
.multilineTextAlignment(.leading)
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.clipped()
|
||||||
|
.cornerRadius(8)
|
||||||
|
} else {
|
||||||
|
// Favicon or default icon
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.blue.opacity(0.1))
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: "link")
|
||||||
|
.font(.system(size: 24))
|
||||||
|
.foregroundColor(Color.blue)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.blue.opacity(0.1))
|
||||||
|
.frame(width: 60, height: 60)
|
||||||
|
.overlay(
|
||||||
|
Image(systemName: "link")
|
||||||
|
.font(.system(size: 24))
|
||||||
|
.foregroundColor(Color.blue)
|
||||||
|
)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
// URL
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(url.absoluteString)
|
// Title
|
||||||
.font(.system(size: 12, design: .monospaced))
|
Text(metadata?.title ?? title ?? url.host ?? "Link")
|
||||||
.foregroundColor(textColor.opacity(0.7))
|
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||||
.lineLimit(1)
|
.foregroundColor(textColor)
|
||||||
.truncationMode(.middle)
|
|
||||||
|
|
||||||
// Description if available
|
|
||||||
if let description = metadata?.description {
|
|
||||||
Text(description)
|
|
||||||
.font(.system(size: 12, design: .monospaced))
|
|
||||||
.foregroundColor(textColor.opacity(0.8))
|
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
|
// Host
|
||||||
|
Text(url.host ?? url.absoluteString)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundColor(textColor.opacity(0.6))
|
||||||
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(10)
|
.padding(10)
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.background(backgroundColor.opacity(0.05))
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(colorScheme == .dark ? Color.gray.opacity(0.15) : Color.gray.opacity(0.08))
|
||||||
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 8)
|
RoundedRectangle(cornerRadius: 10)
|
||||||
.stroke(borderColor, lineWidth: 1)
|
.stroke(borderColor, lineWidth: 1)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.onAppear {
|
}
|
||||||
loadMetadata()
|
|
||||||
|
private var simpleLinkView: some View {
|
||||||
|
Button(action: {
|
||||||
|
#if os(iOS)
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
#else
|
||||||
|
NSWorkspace.shared.open(url)
|
||||||
|
#endif
|
||||||
|
}) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
// Link icon
|
||||||
|
Image(systemName: "link.circle.fill")
|
||||||
|
.font(.system(size: 32))
|
||||||
|
.foregroundColor(Color.blue.opacity(0.8))
|
||||||
|
.frame(width: 40, height: 40)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
// Title
|
||||||
|
Text(title ?? url.host ?? "Link")
|
||||||
|
.font(.system(size: 14, weight: .semibold, design: .monospaced))
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
.lineLimit(2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
|
||||||
|
// URL
|
||||||
|
Text(url.absoluteString)
|
||||||
|
.font(.system(size: 11, design: .monospaced))
|
||||||
|
.foregroundColor(Color.blue)
|
||||||
|
.lineLimit(1)
|
||||||
|
.truncationMode(.middle)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
// Arrow indicator
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
.font(.system(size: 14))
|
||||||
|
.foregroundColor(textColor.opacity(0.5))
|
||||||
|
}
|
||||||
|
.padding(12)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.fill(colorScheme == .dark ? Color.gray.opacity(0.15) : Color.gray.opacity(0.08))
|
||||||
|
)
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: 10)
|
||||||
|
.stroke(borderColor, lineWidth: 1)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadMetadata() {
|
private func loadMetadata() {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
guard metadata == nil && !isLoadingMetadata else { return }
|
guard metadata == nil else { return }
|
||||||
|
|
||||||
isLoadingMetadata = true
|
|
||||||
let provider = LPMetadataProvider()
|
let provider = LPMetadataProvider()
|
||||||
provider.startFetchingMetadata(for: url) { metadata, error in
|
provider.startFetchingMetadata(for: url) { fetchedMetadata, error in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let metadata = metadata {
|
if let fetchedMetadata = fetchedMetadata {
|
||||||
self.metadata = LinkMetadata(
|
// Use the fetched metadata, or create new with our title
|
||||||
title: metadata.title,
|
if let title = self.title, !title.isEmpty {
|
||||||
description: metadata.value(forKey: "_summary") as? String,
|
fetchedMetadata.title = title
|
||||||
imageURL: metadata.imageProvider?.value(forKey: "_URL") as? URL
|
}
|
||||||
)
|
self.metadata = fetchedMetadata
|
||||||
|
|
||||||
|
// Try to extract image
|
||||||
|
if let imageProvider = fetchedMetadata.imageProvider {
|
||||||
|
imageProvider.loadObject(ofClass: UIImage.self) { image, error in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let image = image as? UIImage {
|
||||||
|
self.previewImage = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.isLoadingMetadata = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LinkMetadata {
|
#if os(iOS)
|
||||||
let title: String?
|
// UIViewRepresentable wrapper for LPLinkView
|
||||||
let description: String?
|
struct LinkPreview: UIViewRepresentable {
|
||||||
let imageURL: URL?
|
let metadata: LPLinkMetadata
|
||||||
|
|
||||||
|
func makeUIView(context: Context) -> UIView {
|
||||||
|
let containerView = UIView()
|
||||||
|
containerView.backgroundColor = .clear
|
||||||
|
|
||||||
|
let linkView = LPLinkView(metadata: metadata)
|
||||||
|
linkView.isUserInteractionEnabled = false // We handle taps at the SwiftUI level
|
||||||
|
linkView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
containerView.addSubview(linkView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
linkView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
||||||
|
linkView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
||||||
|
linkView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
||||||
|
linkView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
||||||
|
])
|
||||||
|
|
||||||
|
return containerView
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIView(_ uiView: UIView, context: Context) {
|
||||||
|
// Update if needed
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Helper to extract URLs from text
|
// Helper to extract URLs from text
|
||||||
extension String {
|
extension String {
|
||||||
@@ -130,7 +253,7 @@ extension String {
|
|||||||
if let range = Range(match.range, in: self),
|
if let range = Range(match.range, in: self),
|
||||||
let url = match.url {
|
let url = match.url {
|
||||||
// Don't add if this URL is already part of a markdown link
|
// Don't add if this URL is already part of a markdown link
|
||||||
let isPartOfMarkdown = urls.contains { $0.range.overlaps(range) }
|
let isPartOfMarkdown = urls.contains { $0.1.overlaps(range) }
|
||||||
if !isPartOfMarkdown {
|
if !isPartOfMarkdown {
|
||||||
urls.append((url, range))
|
urls.append((url, range))
|
||||||
}
|
}
|
||||||
@@ -142,15 +265,19 @@ extension String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func extractMarkdownLink() -> (title: String, url: URL)? {
|
func extractMarkdownLink() -> (title: String, url: URL)? {
|
||||||
|
print("DEBUG: Checking for markdown link in: \(self)")
|
||||||
let pattern = #"\[([^\]]+)\]\(([^)]+)\)"#
|
let pattern = #"\[([^\]]+)\]\(([^)]+)\)"#
|
||||||
if let regex = try? NSRegularExpression(pattern: pattern),
|
if let regex = try? NSRegularExpression(pattern: pattern),
|
||||||
let match = regex.firstMatch(in: self, range: NSRange(location: 0, length: self.utf16.count)) {
|
let match = regex.firstMatch(in: self, range: NSRange(location: 0, length: self.utf16.count)) {
|
||||||
if let titleRange = Range(match.range(at: 1), in: self),
|
if let titleRange = Range(match.range(at: 1), in: self),
|
||||||
let urlRange = Range(match.range(at: 2), in: self),
|
let urlRange = Range(match.range(at: 2), in: self),
|
||||||
let url = URL(string: String(self[urlRange])) {
|
let url = URL(string: String(self[urlRange])) {
|
||||||
return (String(self[titleRange]), url)
|
let result = (String(self[titleRange]), url)
|
||||||
|
print("DEBUG: Found markdown link - title: \(result.0), url: \(result.1)")
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
print("DEBUG: No markdown link found")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,18 +26,18 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionActivationRule</key>
|
<key>NSExtensionActivationRule</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||||
|
<integer>1</integer>
|
||||||
<key>NSExtensionActivationSupportsText</key>
|
<key>NSExtensionActivationSupportsText</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||||
<integer>1</integer>
|
<integer>1</integer>
|
||||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
|
||||||
<integer>1</integer>
|
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSExtensionPrincipalClass</key>
|
|
||||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
|
||||||
<key>NSExtensionPointIdentifier</key>
|
<key>NSExtensionPointIdentifier</key>
|
||||||
<string>com.apple.share-services</string>
|
<string>com.apple.share-services</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -35,65 +35,110 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func didSelectPost() {
|
override func didSelectPost() {
|
||||||
// If we have content text from the compose view, handle it directly
|
|
||||||
if let text = contentText, !text.isEmpty {
|
|
||||||
handleSharedText(text)
|
|
||||||
// Complete the share action after saving
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
|
||||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, process attachments
|
|
||||||
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
|
guard let extensionItem = extensionContext?.inputItems.first as? NSExtensionItem else {
|
||||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasProcessedContent = false
|
print("ShareExtension: Processing share with \(extensionItem.attachments?.count ?? 0) attachments")
|
||||||
|
|
||||||
|
// Get the page title from the compose view or extension item
|
||||||
|
let pageTitle = self.contentText ?? extensionItem.attributedContentText?.string ?? extensionItem.attributedTitle?.string
|
||||||
|
print("ShareExtension: Page title: \(pageTitle ?? "none")")
|
||||||
|
|
||||||
|
var foundURL: URL? = nil
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
// Process different types of shared content
|
// IMPORTANT: Check if the NSExtensionItem itself has a URL
|
||||||
for itemProvider in extensionItem.attachments ?? [] {
|
// Safari often provides the URL as an attributedString with a link
|
||||||
if itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
if let attributedText = extensionItem.attributedContentText {
|
||||||
group.enter()
|
print("ShareExtension: Checking attributed text for URLs")
|
||||||
itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { [weak self] (item, error) in
|
let text = attributedText.string
|
||||||
if let text = item as? String {
|
let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue)
|
||||||
self?.handleSharedText(text)
|
let matches = detector?.matches(in: text, options: [], range: NSRange(location: 0, length: text.utf16.count))
|
||||||
hasProcessedContent = true
|
if let firstMatch = matches?.first, let url = firstMatch.url {
|
||||||
}
|
print("ShareExtension: Found URL in attributed text: \(url.absoluteString)")
|
||||||
group.leave()
|
foundURL = url
|
||||||
}
|
|
||||||
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
|
||||||
group.enter()
|
|
||||||
itemProvider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { [weak self] (item, error) in
|
|
||||||
if let url = item as? URL {
|
|
||||||
self?.handleSharedURL(url)
|
|
||||||
hasProcessedContent = true
|
|
||||||
}
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
} else if itemProvider.hasItemConformingToTypeIdentifier(UTType.image.identifier) {
|
|
||||||
group.enter()
|
|
||||||
itemProvider.loadItem(forTypeIdentifier: UTType.image.identifier, options: nil) { [weak self] (item, error) in
|
|
||||||
if let image = item as? UIImage {
|
|
||||||
self?.handleSharedImage(image)
|
|
||||||
hasProcessedContent = true
|
|
||||||
} else if let data = item as? Data {
|
|
||||||
if let image = UIImage(data: data) {
|
|
||||||
self?.handleSharedImage(image)
|
|
||||||
hasProcessedContent = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete after all items are processed
|
// Only check attachments if we haven't found a URL yet
|
||||||
group.notify(queue: .main) {
|
if foundURL == nil {
|
||||||
self.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
for (index, itemProvider) in (extensionItem.attachments ?? []).enumerated() {
|
||||||
|
print("ShareExtension: Attachment \(index) types: \(itemProvider.registeredTypeIdentifiers)")
|
||||||
|
|
||||||
|
// Try multiple URL type identifiers that Safari might use
|
||||||
|
let urlTypes = [
|
||||||
|
UTType.url.identifier,
|
||||||
|
"public.url",
|
||||||
|
"public.file-url"
|
||||||
|
]
|
||||||
|
|
||||||
|
for urlType in urlTypes {
|
||||||
|
if itemProvider.hasItemConformingToTypeIdentifier(urlType) {
|
||||||
|
group.enter()
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: urlType, options: nil) { (item, error) in
|
||||||
|
defer { group.leave() }
|
||||||
|
|
||||||
|
if let url = item as? URL {
|
||||||
|
print("ShareExtension: Found URL: \(url.absoluteString)")
|
||||||
|
foundURL = url
|
||||||
|
} else if let data = item as? Data,
|
||||||
|
let urlString = String(data: data, encoding: .utf8),
|
||||||
|
let url = URL(string: urlString) {
|
||||||
|
print("ShareExtension: Found URL from data: \(url.absoluteString)")
|
||||||
|
foundURL = url
|
||||||
|
} else if let string = item as? String,
|
||||||
|
let url = URL(string: string) {
|
||||||
|
print("ShareExtension: Found URL from string: \(url.absoluteString)")
|
||||||
|
foundURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break // Found a URL type, no need to check other types
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for plain text that might be a URL
|
||||||
|
if foundURL == nil && itemProvider.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) {
|
||||||
|
group.enter()
|
||||||
|
itemProvider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { (item, error) in
|
||||||
|
defer { group.leave() }
|
||||||
|
|
||||||
|
if let text = item as? String {
|
||||||
|
// Check if the text is actually a URL
|
||||||
|
if let url = URL(string: text),
|
||||||
|
(url.scheme == "http" || url.scheme == "https") {
|
||||||
|
print("ShareExtension: Found URL in plain text: \(url.absoluteString)")
|
||||||
|
foundURL = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // End of if foundURL == nil
|
||||||
|
|
||||||
|
// Process after all checks complete
|
||||||
|
group.notify(queue: .main) { [weak self] in
|
||||||
|
if let url = foundURL {
|
||||||
|
// We have a URL! Create the JSON data
|
||||||
|
let urlData: [String: String] = [
|
||||||
|
"url": url.absoluteString,
|
||||||
|
"title": pageTitle ?? url.host ?? "Shared Link"
|
||||||
|
]
|
||||||
|
|
||||||
|
print("ShareExtension: Saving URL share - url: \(url.absoluteString), title: \(urlData["title"] ?? "")")
|
||||||
|
|
||||||
|
if let jsonData = try? JSONSerialization.data(withJSONObject: urlData),
|
||||||
|
let jsonString = String(data: jsonData, encoding: .utf8) {
|
||||||
|
self?.saveToSharedDefaults(content: jsonString, type: "url")
|
||||||
|
}
|
||||||
|
} else if let title = pageTitle, !title.isEmpty {
|
||||||
|
// No URL found, just share the text
|
||||||
|
print("ShareExtension: No URL found, sharing as text: \(title)")
|
||||||
|
self?.saveToSharedDefaults(content: title, type: "text")
|
||||||
|
}
|
||||||
|
|
||||||
|
self?.extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,11 +200,19 @@ class ShareViewController: SLComposeServiceViewController {
|
|||||||
userDefaults.synchronize()
|
userDefaults.synchronize()
|
||||||
|
|
||||||
print("ShareExtension: Saved content of type \(type) to shared defaults")
|
print("ShareExtension: Saved content of type \(type) to shared defaults")
|
||||||
|
print("ShareExtension: Content: \(content)")
|
||||||
|
|
||||||
|
// Force open the main app
|
||||||
|
self.openMainApp()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openMainApp() {
|
private func openMainApp() {
|
||||||
// Note: Share extensions cannot directly open the containing app
|
// Share extensions cannot directly open the containing app
|
||||||
// The user will need to tap on the notification or manually open the app
|
// The app will check for shared content when it becomes active
|
||||||
// to see the shared content
|
// Show success feedback to user
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.textView.text = "✓ Shared to bitchat"
|
||||||
|
self.textView.isEditable = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+13
@@ -34,6 +34,9 @@ targets:
|
|||||||
UIColorName: Black
|
UIColorName: Black
|
||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
|
CFBundleURLTypes:
|
||||||
|
- CFBundleURLSchemes:
|
||||||
|
- bitchat
|
||||||
settings:
|
settings:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat
|
||||||
INFOPLIST_FILE: bitchat/Info.plist
|
INFOPLIST_FILE: bitchat/Info.plist
|
||||||
@@ -48,6 +51,7 @@ targets:
|
|||||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||||
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS: YES
|
||||||
CODE_SIGN_ENTITLEMENTS: bitchat/bitchat.entitlements
|
CODE_SIGN_ENTITLEMENTS: bitchat/bitchat.entitlements
|
||||||
|
DEVELOPMENT_TEAM: L3N5LHJD5Y
|
||||||
dependencies:
|
dependencies:
|
||||||
- target: bitchatShareExtension
|
- target: bitchatShareExtension
|
||||||
platformFilter: ios
|
platformFilter: ios
|
||||||
@@ -64,6 +68,14 @@ targets:
|
|||||||
CFBundleDisplayName: bitchat
|
CFBundleDisplayName: bitchat
|
||||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||||
|
NSExtension:
|
||||||
|
NSExtensionPointIdentifier: com.apple.share-services
|
||||||
|
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).ShareViewController
|
||||||
|
NSExtensionAttributes:
|
||||||
|
NSExtensionActivationRule:
|
||||||
|
NSExtensionActivationSupportsText: true
|
||||||
|
NSExtensionActivationSupportsWebURLWithMaxCount: 1
|
||||||
|
NSExtensionActivationSupportsImageWithMaxCount: 1
|
||||||
settings:
|
settings:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.ShareExtension
|
PRODUCT_BUNDLE_IDENTIFIER: chat.bitchat.ShareExtension
|
||||||
INFOPLIST_FILE: bitchatShareExtension/Info.plist
|
INFOPLIST_FILE: bitchatShareExtension/Info.plist
|
||||||
@@ -73,6 +85,7 @@ targets:
|
|||||||
CODE_SIGNING_REQUIRED: YES
|
CODE_SIGNING_REQUIRED: YES
|
||||||
CODE_SIGNING_ALLOWED: YES
|
CODE_SIGNING_ALLOWED: YES
|
||||||
CODE_SIGN_ENTITLEMENTS: bitchatShareExtension/bitchatShareExtension.entitlements
|
CODE_SIGN_ENTITLEMENTS: bitchatShareExtension/bitchatShareExtension.entitlements
|
||||||
|
DEVELOPMENT_TEAM: L3N5LHJD5Y
|
||||||
|
|
||||||
bitchatTests_iOS:
|
bitchatTests_iOS:
|
||||||
type: bundle.unit-test
|
type: bundle.unit-test
|
||||||
|
|||||||
Reference in New Issue
Block a user