Add native sharing handler
This commit is contained in:
parent
a572918e6c
commit
9cd717d5b3
33 changed files with 976 additions and 72 deletions
|
|
@ -27,6 +27,17 @@
|
|||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="https" android:host="app.flotilla.social" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Puts Flotilla in the system share sheet. These intents carry their payload in
|
||||
extras rather than a data uri, so Capacitor never emits appUrlOpen for them;
|
||||
ShareIntentPlugin picks them up instead. -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="text/plain" />
|
||||
<data android:mimeType="image/*" />
|
||||
<data android:mimeType="video/*" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@ import android.os.Bundle;
|
|||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
import social.flotilla.notifications.AndroidPushFallbackPlugin;
|
||||
import social.flotilla.share.ShareIntentPlugin;
|
||||
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
registerPlugin(AndroidPushFallbackPlugin.class);
|
||||
registerPlugin(ShareIntentPlugin.class);
|
||||
createPushNotificationChannel();
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package social.flotilla.share
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.core.content.IntentCompat
|
||||
import com.getcapacitor.JSObject
|
||||
import com.getcapacitor.Plugin
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import java.io.File
|
||||
|
||||
@CapacitorPlugin(name = "ShareIntent")
|
||||
class ShareIntentPlugin : Plugin() {
|
||||
// Capacitor runs the launch intent through here as well as intents delivered while the app is
|
||||
// already running, so this is the only hook we need. Retaining the event covers a cold start,
|
||||
// where the web view isn't listening yet.
|
||||
override fun handleOnNewIntent(intent: Intent) {
|
||||
if (intent.action == Intent.ACTION_SEND) {
|
||||
val uri = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)
|
||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
|
||||
// Android hands the same intent back when the activity is recreated, so drop the payload
|
||||
// once we've taken it to avoid re-opening the share dialog.
|
||||
if (uri != null) {
|
||||
intent.removeExtra(Intent.EXTRA_STREAM)
|
||||
|
||||
// Copying a shared video can take a while, and this runs on the main thread
|
||||
bridge.execute { notifyListeners("shareReceived", copyToCache(uri), true) }
|
||||
} else if (text != null) {
|
||||
intent.removeExtra(Intent.EXTRA_TEXT)
|
||||
notifyListeners("shareReceived", JSObject().put("text", text), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A content uri is readable by us but meaningless to the web view, so hand over a copy that
|
||||
// capacitor's file server can serve.
|
||||
private fun copyToCache(uri: Uri): JSObject {
|
||||
val resolver = context.contentResolver
|
||||
val type = resolver.getType(uri)
|
||||
val extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type)
|
||||
val file = File.createTempFile("shared", extension?.let { ".$it" }, context.cacheDir)
|
||||
|
||||
resolver.openInputStream(uri)?.use { input ->
|
||||
file.outputStream().use { output -> input.copyTo(output) }
|
||||
}
|
||||
|
||||
return JSObject()
|
||||
.put("path", file.absolutePath)
|
||||
.put("name", file.name)
|
||||
.put("type", type)
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
052C05E5302BD03100DD0620 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 052C05E2302BD03100DD0620 /* ShareViewController.swift */; };
|
||||
052C05D0302BD03100DD0620 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 052C05C6302BD03100DD0620 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */; };
|
||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||
|
|
@ -18,8 +20,36 @@
|
|||
AC8D5382B9575A9124613C5D /* Pods_Flotilla_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
052C05CE302BD03100DD0620 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 504EC2FC1FED79650016851F /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 052C05C5302BD03100DD0620;
|
||||
remoteInfo = ShareExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
052C05D1302BD03100DD0620 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
052C05D0302BD03100DD0620 /* ShareExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
052C05E2302BD03100DD0620 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
|
||||
052C05E3302BD03100DD0620 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
052C05E4302BD03100DD0620 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
|
||||
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Flotilla Chat.entitlements"; sourceTree = "<group>"; };
|
||||
052C05C6302BD03100DD0620 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
1F53EE54954731A2328CBC4B /* Pods-Flotilla Chat.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Flotilla Chat.release.xcconfig"; path = "Pods/Target Support Files/Pods-Flotilla Chat/Pods-Flotilla Chat.release.xcconfig"; sourceTree = "<group>"; };
|
||||
2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
|
||||
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
|
|
@ -38,6 +68,13 @@
|
|||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
052C05C3302BD03100DD0620 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
504EC3011FED79650016851F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -49,6 +86,16 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
052C05E1302BD03100DD0620 /* ShareExtension */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
052C05E2302BD03100DD0620 /* ShareViewController.swift */,
|
||||
052C05E3302BD03100DD0620 /* Info.plist */,
|
||||
052C05E4302BD03100DD0620 /* ShareExtension.entitlements */,
|
||||
);
|
||||
path = ShareExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -63,6 +110,7 @@
|
|||
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
|
||||
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
052C05E1302BD03100DD0620 /* ShareExtension */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
|
|
@ -73,6 +121,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
504EC3041FED79650016851F /* Flotilla Chat.app */,
|
||||
052C05C6302BD03100DD0620 /* ShareExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -106,6 +155,25 @@
|
|||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
052C05C5302BD03100DD0620 /* ShareExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 052C05D5302BD03100DD0620 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
|
||||
buildPhases = (
|
||||
052C05C2302BD03100DD0620 /* Sources */,
|
||||
052C05C3302BD03100DD0620 /* Frameworks */,
|
||||
052C05C4302BD03100DD0620 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = ShareExtension;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = ShareExtension;
|
||||
productReference = 052C05C6302BD03100DD0620 /* ShareExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
504EC3031FED79650016851F /* Flotilla Chat */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */;
|
||||
|
|
@ -115,10 +183,12 @@
|
|||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||
052C05D1302BD03100DD0620 /* Embed Foundation Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
052C05CF302BD03100DD0620 /* PBXTargetDependency */,
|
||||
);
|
||||
name = "Flotilla Chat";
|
||||
productName = App;
|
||||
|
|
@ -132,9 +202,12 @@
|
|||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastSwiftUpdateCheck = 2630;
|
||||
LastUpgradeCheck = 2630;
|
||||
TargetAttributes = {
|
||||
052C05C5302BD03100DD0620 = {
|
||||
CreatedOnToolsVersion = 26.3;
|
||||
};
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
LastSwiftMigration = 1100;
|
||||
|
|
@ -156,11 +229,19 @@
|
|||
projectRoot = "";
|
||||
targets = (
|
||||
504EC3031FED79650016851F /* Flotilla Chat */,
|
||||
052C05C5302BD03100DD0620 /* ShareExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
052C05C4302BD03100DD0620 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
504EC3021FED79650016851F /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -214,6 +295,14 @@
|
|||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
052C05C2302BD03100DD0620 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
052C05E5302BD03100DD0620 /* ShareViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
504EC3001FED79650016851F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
|
@ -224,6 +313,14 @@
|
|||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
052C05CF302BD03100DD0620 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 052C05C5302BD03100DD0620 /* ShareExtension */;
|
||||
targetProxy = 052C05CE302BD03100DD0620 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
|
|
@ -244,6 +341,78 @@
|
|||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
052C05D2302BD03100DD0620 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 41;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.9.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
052C05D3302BD03100DD0620 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 41;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = NO;
|
||||
INFOPLIST_FILE = ShareExtension/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.9.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla.ShareExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
|
@ -424,6 +593,15 @@
|
|||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
052C05D5302BD03100DD0620 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
052C05D2302BD03100DD0620 /* Debug */,
|
||||
052C05D3302BD03100DD0620 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "2630"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "Flotilla Chat.app"
|
||||
BlueprintName = "Flotilla Chat"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "Flotilla Chat.app"
|
||||
BlueprintName = "Flotilla Chat"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "504EC3031FED79650016851F"
|
||||
BuildableName = "Flotilla Chat.app"
|
||||
BlueprintName = "Flotilla Chat"
|
||||
ReferencedContainer = "container:App.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
|
|
@ -16,6 +16,17 @@
|
|||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>social.flotilla</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>flotilla</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
|
|
|
|||
|
|
@ -8,5 +8,9 @@
|
|||
<array>
|
||||
<string>applinks:app.flotilla.social</string>
|
||||
</array>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.social.flotilla</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
45
ios/App/ShareExtension/Info.plist
Normal file
45
ios/App/ShareExtension/Info.plist
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Flotilla</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationRule</key>
|
||||
<dict>
|
||||
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
|
||||
<integer>1</integer>
|
||||
<key>NSExtensionActivationSupportsText</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.share-services</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
10
ios/App/ShareExtension/ShareExtension.entitlements
Normal file
10
ios/App/ShareExtension/ShareExtension.entitlements
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.social.flotilla</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
159
ios/App/ShareExtension/ShareViewController.swift
Normal file
159
ios/App/ShareExtension/ShareViewController.swift
Normal file
|
|
@ -0,0 +1,159 @@
|
|||
import ObjectiveC
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
// A share extension runs in its own process and can't reach the app or its web view, so the
|
||||
// payload is handed over as a flotilla://share url, which the root layout picks up via
|
||||
// appUrlOpen. Media is far too big for a url, so it gets copied into the shared app group
|
||||
// container — the one directory both processes can read — and passed along by path.
|
||||
class ShareViewController: UIViewController {
|
||||
private let appGroup = "group.social.flotilla"
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
let attachments = (extensionContext?.inputItems.first as? NSExtensionItem)?.attachments ?? []
|
||||
|
||||
// Browsers attach a page's title alongside its url, so take these in order of specificity.
|
||||
// Photos offers a jpeg representation of a heic asset, which saves us re-encoding it later.
|
||||
for type in [UTType.movie, UTType.jpeg, UTType.image, UTType.url, UTType.plainText] {
|
||||
for provider in attachments where provider.hasItemConformingToTypeIdentifier(type.identifier) {
|
||||
provider.loadItem(forTypeIdentifier: type.identifier) { value, _ in
|
||||
if type == .url {
|
||||
self.shareText((value as? URL)?.absoluteString)
|
||||
} else if type == .plainText {
|
||||
self.shareText(value as? String)
|
||||
} else {
|
||||
self.shareFile(value)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
finish()
|
||||
}
|
||||
|
||||
private func shareText(_ text: String?) {
|
||||
guard let text = text else {
|
||||
return finish()
|
||||
}
|
||||
|
||||
open([URLQueryItem(name: "text", value: text)])
|
||||
}
|
||||
|
||||
private func shareFile(_ value: NSSecureCoding?) {
|
||||
let manager = FileManager.default
|
||||
|
||||
guard let container = manager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
|
||||
return finish()
|
||||
}
|
||||
|
||||
// Nothing prunes the group container for us, and the app is done with the last share by the
|
||||
// time a new one arrives. Emptying a directory of our own leaves the rest of the container,
|
||||
// Library/ included, alone.
|
||||
let directory = container.appendingPathComponent("shares")
|
||||
|
||||
try? manager.removeItem(at: directory)
|
||||
|
||||
do {
|
||||
try manager.createDirectory(at: directory, withIntermediateDirectories: true)
|
||||
|
||||
if let source = value as? URL {
|
||||
let destination = directory.appendingPathComponent(source.lastPathComponent)
|
||||
|
||||
try manager.copyItem(at: source, to: destination)
|
||||
open(file: destination)
|
||||
} else if let image = value as? UIImage, let data = image.jpegData(compressionQuality: 0.9) {
|
||||
let destination = directory.appendingPathComponent("shared.jpg")
|
||||
|
||||
try data.write(to: destination)
|
||||
open(file: destination)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
} catch {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private func open(file: URL) {
|
||||
open([
|
||||
URLQueryItem(name: "path", value: file.path),
|
||||
URLQueryItem(name: "name", value: file.lastPathComponent),
|
||||
URLQueryItem(
|
||||
name: "type",
|
||||
value: UTType(filenameExtension: file.pathExtension)?.preferredMIMEType
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
private func open(_ queryItems: [URLQueryItem]) {
|
||||
guard var components = URLComponents(string: "flotilla://share") else {
|
||||
return finish()
|
||||
}
|
||||
|
||||
components.queryItems = queryItems
|
||||
|
||||
guard let url = components.url else {
|
||||
return finish()
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NSLog("Flotilla: opening %@", url.absoluteString)
|
||||
|
||||
// Which of these works varies by ios version and neither reports whether the app actually
|
||||
// came forward, so try both. Opening the same url twice is harmless.
|
||||
let foundApplication = self.openViaResponderChain(url)
|
||||
|
||||
self.extensionContext?.open(url) { opened in
|
||||
NSLog(
|
||||
"Flotilla: responder chain %@, extensionContext.open %@",
|
||||
foundApplication ? "found UIApplication" : "found nothing",
|
||||
opened ? "succeeded" : "failed"
|
||||
)
|
||||
|
||||
// Completing the request tears down the extension, which can cancel an open springboard
|
||||
// hasn't picked up yet
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UIApplication isn't reachable from an extension, so walk the responder chain to find it. Its
|
||||
// open(_:options:completionHandler:) isn't visible to us at compile time either, and the
|
||||
// openURL: that extensions used for years now force-returns false, so call it via the runtime.
|
||||
private func openViaResponderChain(_ url: URL) -> Bool {
|
||||
let selector = NSSelectorFromString("openURL:options:completionHandler:")
|
||||
var responder: UIResponder? = self
|
||||
|
||||
while let current = responder {
|
||||
if let application = current as? UIApplication,
|
||||
let method = class_getInstanceMethod(type(of: application), selector)
|
||||
{
|
||||
typealias OpenURL = @convention(c) (UIApplication, Selector, NSURL, NSDictionary, AnyObject?)
|
||||
-> Void
|
||||
|
||||
let open = unsafeBitCast(method_getImplementation(method), to: OpenURL.self)
|
||||
let completion: @convention(block) (Bool) -> Void = { opened in
|
||||
NSLog("Flotilla: UIApplication.open %@", opened ? "succeeded" : "failed")
|
||||
}
|
||||
|
||||
open(application, selector, url as NSURL, NSDictionary(), completion as AnyObject)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
responder = current.next
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private func finish() {
|
||||
extensionContext?.completeRequest(returningItems: nil)
|
||||
}
|
||||
}
|
||||
|
|
@ -71,6 +71,7 @@
|
|||
"@sveltejs/adapter-static": "^3.0.10",
|
||||
"@tiptap/core": "^2.27.2",
|
||||
"@tiptap/pm": "^2.27.2",
|
||||
"@tiptap/suggestion": "^2.27.2",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/throttle-debounce": "^5.0.2",
|
||||
"@vite-pwa/assets-generator": "^1.0.2",
|
||||
|
|
@ -98,6 +99,10 @@
|
|||
"nostr-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
||||
"nostr-tools": "^2.19.4",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"prosemirror-model": "^1.25.7",
|
||||
"prosemirror-state": "^1.4.4",
|
||||
"prosemirror-transform": "^1.12.0",
|
||||
"prosemirror-view": "^1.41.8",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"qrcode": "^1.5.4",
|
||||
"throttle-debounce": "^5.0.2",
|
||||
|
|
|
|||
BIN
pnpm-lock.yaml
BIN
pnpm-lock.yaml
Binary file not shown.
|
|
@ -6,7 +6,7 @@
|
|||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import EventInfo from "@app/components/EventInfo.svelte"
|
||||
import {shareEventToChat} from "@app/share"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
const showInfo = () => pushModal(EventInfo, {url, event: board.event})
|
||||
|
||||
const share = () => shareEventToChat(url, "Shelf", board.event)
|
||||
const share = () => shareEvent(url, "Shelf", board.event)
|
||||
|
||||
let ul: Element
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
enumerate,
|
||||
formatTimestampAsDate,
|
||||
} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
||||
import {makeEvent, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
|
||||
import {parse, isLink} from "@welshman/content"
|
||||
|
|
@ -44,8 +45,9 @@
|
|||
import {userSettingsValues} from "@app/settings"
|
||||
import {deriveChat, makeChatId} from "@app/chats"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {DraftKey} from "@app/drafts"
|
||||
import {DraftKey, type Draft} from "@app/drafts"
|
||||
import {prependParent} from "@app/rooms"
|
||||
import {pendingShare, type Share} from "@app/share"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -57,7 +59,7 @@
|
|||
|
||||
const chatId = makeChatId(pubkeys)
|
||||
const chat = deriveChat(chatId)
|
||||
const draftKey = new DraftKey<{content?: string | object}>(`dm:${chatId}`)
|
||||
const draftKey = new DraftKey<Draft>(`dm:${chatId}`)
|
||||
const others = remove($user.pubkey, pubkeys)
|
||||
const messagingRelayLists = $app.use(MessagingRelayLists).index.$
|
||||
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayLists.has(pk)))
|
||||
|
|
@ -183,6 +185,26 @@
|
|||
let compose: ChatCompose | undefined = $state()
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let eventToEdit: TrustedEvent | undefined = $state()
|
||||
let share: Maybe<Share> = $state()
|
||||
|
||||
// Claim the share once we're on screen. Sharing into the conversation you're already looking at
|
||||
// doesn't re-create this component, so this can't be read once on mount.
|
||||
$effect(() => {
|
||||
if ($pendingShare) {
|
||||
share = $pendingShare
|
||||
pendingShare.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
const initialValues = $derived.by((): Share | undefined => {
|
||||
if (eventToEdit) {
|
||||
return {type: "text", value: eventToEdit.content}
|
||||
}
|
||||
|
||||
if (share) {
|
||||
return share
|
||||
}
|
||||
})
|
||||
|
||||
const elements = $derived.by(() => {
|
||||
const elements = []
|
||||
|
|
@ -318,13 +340,13 @@
|
|||
<ChatComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key eventToEdit}
|
||||
{#key initialValues}
|
||||
<ChatCompose
|
||||
bind:this={compose}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
initialValues={eventToEdit}
|
||||
{initialValues}
|
||||
draftKey={eventToEdit ? undefined : draftKey}
|
||||
disabled={Boolean(missingRelayLists.length)} />
|
||||
{/key}
|
||||
|
|
|
|||
|
|
@ -11,22 +11,19 @@
|
|||
import Spinner from "@lib/components/Spinner.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {type DraftKey} from "@app/drafts"
|
||||
|
||||
type Values = {
|
||||
content?: string | object
|
||||
}
|
||||
import {type DraftKey, type Draft} from "@app/drafts"
|
||||
import type {Share} from "@app/share"
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean
|
||||
draftKey?: DraftKey<Values>
|
||||
draftKey?: DraftKey<Draft>
|
||||
onEscape?: () => void
|
||||
onEditPrevious?: () => void
|
||||
onSubmit: (event: EventContent) => void
|
||||
initialValues?: Values
|
||||
initialValues?: Share
|
||||
}
|
||||
|
||||
let {
|
||||
const {
|
||||
initialValues,
|
||||
disabled = false,
|
||||
draftKey,
|
||||
|
|
@ -35,10 +32,6 @@
|
|||
onSubmit,
|
||||
}: Props = $props()
|
||||
|
||||
if (!initialValues) {
|
||||
initialValues = draftKey?.get()
|
||||
}
|
||||
|
||||
const autofocus = !isMobile && !disabled
|
||||
|
||||
const uploading = writable(false)
|
||||
|
|
@ -81,7 +74,9 @@
|
|||
ed.chain().clearContent().run()
|
||||
}
|
||||
|
||||
let content = $state(initialValues?.content ?? "")
|
||||
let content = $state(
|
||||
initialValues?.type === "text" ? initialValues.value : (draftKey?.get()?.content ?? ""),
|
||||
)
|
||||
|
||||
const onChange = (json: object) => {
|
||||
content = json
|
||||
|
|
@ -103,6 +98,12 @@
|
|||
onMount(async () => {
|
||||
const ed = await editor
|
||||
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
if (initialValues?.type === "file") {
|
||||
ed.chain()
|
||||
.addFile(initialValues.value, ed.state.selection.from + 1)
|
||||
.run()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(async () => {
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
import Report from "@app/components/Report.svelte"
|
||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
||||
import {shareEventToChat} from "@app/share"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
|
@ -40,7 +40,7 @@
|
|||
|
||||
const addToLibrary = () => pushModal(PinboardSelect, {url, event})
|
||||
|
||||
const share = () => shareEventToChat(url, noun, event)
|
||||
const share = () => shareEvent(url, noun, event)
|
||||
|
||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
import PinEdit from "@app/components/PinEdit.svelte"
|
||||
import {deletes} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {shareEventToChat} from "@app/share"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
const showInfo = () => pushModal(EventInfo, {url, event: pin.event})
|
||||
|
||||
const share = () => shareEventToChat(url, "Link", pin.event)
|
||||
const share = () => shareEvent(url, "Link", pin.event)
|
||||
|
||||
const edit = () => pushModal(PinEdit, {url, pin})
|
||||
|
||||
|
|
|
|||
|
|
@ -30,10 +30,8 @@
|
|||
</div>
|
||||
</div>
|
||||
{#if $userSpaceUrls.includes(url)}
|
||||
<div
|
||||
class="tip absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content"
|
||||
data-tip="You are already a member of this space.">
|
||||
<Icon icon={CheckCircle} class="scale-110" />
|
||||
<div class="absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content flex justify-center items-center">
|
||||
<Icon icon={CheckCircle} class="scale-120" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import {debounce} from "throttle-debounce"
|
||||
import cx from "classnames"
|
||||
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||
import {publish} from "@welshman/app"
|
||||
|
|
@ -16,7 +17,6 @@
|
|||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||
import Close from "@assets/icons/close.svg?dataurl"
|
||||
import {fade, fly} from "@lib/transition"
|
||||
import {popKey} from "@lib/implicit"
|
||||
import {documentActive} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Divider from "@lib/components/Divider.svelte"
|
||||
|
|
@ -46,6 +46,7 @@
|
|||
import {makeFeed} from "@app/feeds"
|
||||
import {checked, deferredRoomPath, notifications, setChecked} from "@app/notifications"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
import {pendingShare, type Share} from "@app/share"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -183,7 +184,7 @@
|
|||
}
|
||||
|
||||
const onSubmit = async ({content, tags}: EventContent) => {
|
||||
if (!content && !share) {
|
||||
if (!content && !sharedEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -216,8 +217,8 @@
|
|||
command.publishToRelays([url])
|
||||
}
|
||||
|
||||
if (share) {
|
||||
template = await prependParent(share, template, url)
|
||||
if (sharedEvent) {
|
||||
template = await prependParent(sharedEvent, template, url)
|
||||
}
|
||||
|
||||
if (parent) {
|
||||
|
|
@ -324,7 +325,7 @@
|
|||
let isUserScrolling = $state(false)
|
||||
let loadingBackward = $state(true)
|
||||
let loadingForward = $state(true)
|
||||
let share = $state(popKey<TrustedEvent | undefined>("share"))
|
||||
let share: Maybe<Share> = $state()
|
||||
let parent: TrustedEvent | undefined = $state()
|
||||
let element: HTMLElement | undefined = $state()
|
||||
let lastVisibleAt = now()
|
||||
|
|
@ -338,6 +339,27 @@
|
|||
let compose: RoomCompose | undefined = $state()
|
||||
let eventToEdit: TrustedEvent | undefined = $state()
|
||||
|
||||
// Claim the share once we're on screen. Sharing into the room you're already looking at
|
||||
// doesn't re-create this component, so this can't be read once on mount.
|
||||
$effect(() => {
|
||||
if ($pendingShare) {
|
||||
share = $pendingShare
|
||||
pendingShare.set(undefined)
|
||||
}
|
||||
})
|
||||
|
||||
const sharedEvent = $derived(share?.type === "event" ? share.value : undefined)
|
||||
|
||||
const initialValues = $derived.by((): Share | undefined => {
|
||||
if (eventToEdit) {
|
||||
return {type: "text", value: eventToEdit.content}
|
||||
}
|
||||
|
||||
if (share) {
|
||||
return share
|
||||
}
|
||||
})
|
||||
|
||||
const clearIsUserScrolling = debounce(150, () => {
|
||||
isUserScrolling = false
|
||||
})
|
||||
|
|
@ -619,21 +641,21 @@
|
|||
{#if parent}
|
||||
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||
{/if}
|
||||
{#if share}
|
||||
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
||||
{#if sharedEvent}
|
||||
<RoomComposeParent event={sharedEvent} clear={clearShare} verb="Sharing" />
|
||||
{/if}
|
||||
{#if eventToEdit}
|
||||
<RoomComposeEdit clear={clearEventToEdit} />
|
||||
{/if}
|
||||
</div>
|
||||
{#key eventToEdit}
|
||||
{#key initialValues}
|
||||
<RoomCompose
|
||||
{url}
|
||||
{h}
|
||||
{onSubmit}
|
||||
{onEscape}
|
||||
{onEditPrevious}
|
||||
initialValues={eventToEdit}
|
||||
{initialValues}
|
||||
bind:this={compose} />
|
||||
{/key}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -13,29 +13,22 @@
|
|||
import ComposeMenu from "@app/components/ComposeMenu.svelte"
|
||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||
import {makeEditor} from "@app/editor"
|
||||
import {DraftKey} from "@app/drafts"
|
||||
import {DraftKey, type Draft} from "@app/drafts"
|
||||
import type {Share} from "@app/share"
|
||||
import {onDestroy, onMount} from "svelte"
|
||||
|
||||
type Values = {
|
||||
content?: string | object
|
||||
}
|
||||
|
||||
type Props = {
|
||||
url?: string
|
||||
h?: string
|
||||
onEscape?: () => void
|
||||
onEditPrevious?: () => void
|
||||
onSubmit: (event: EventContent) => void
|
||||
initialValues?: Values
|
||||
initialValues?: Share
|
||||
}
|
||||
|
||||
let {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||
const {url, h, initialValues, onEscape, onEditPrevious, onSubmit}: Props = $props()
|
||||
|
||||
const draftKey = url || h ? new DraftKey<Values>(`room:${url ?? ""}:${h ?? ""}`) : undefined
|
||||
|
||||
if (!initialValues) {
|
||||
initialValues = draftKey?.get()
|
||||
}
|
||||
const draftKey = url || h ? new DraftKey<Draft>(`room:${url ?? ""}:${h ?? ""}`) : undefined
|
||||
|
||||
const autofocus = !isMobile
|
||||
|
||||
|
|
@ -76,7 +69,9 @@
|
|||
}
|
||||
|
||||
let popover: Instance | undefined = $state()
|
||||
let content = $state(initialValues?.content ?? "")
|
||||
let content = $state(
|
||||
initialValues?.type === "text" ? initialValues.value : (draftKey?.get()?.content ?? ""),
|
||||
)
|
||||
|
||||
const onChange = (json: object) => {
|
||||
content = json
|
||||
|
|
@ -98,6 +93,12 @@
|
|||
onMount(async () => {
|
||||
const ed = await editor
|
||||
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||
|
||||
if (initialValues?.type === "file") {
|
||||
ed.chain()
|
||||
.addFile(initialValues.value, ed.state.selection.from + 1)
|
||||
.run()
|
||||
}
|
||||
})
|
||||
|
||||
onDestroy(async () => {
|
||||
|
|
|
|||
137
src/app/components/Share.svelte
Normal file
137
src/app/components/Share.svelte
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<script lang="ts">
|
||||
import {derived} from "svelte/store"
|
||||
import cx from "classnames"
|
||||
import {remove} from "@welshman/lib"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Modal from "@lib/components/Modal.svelte"
|
||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
||||
import ProfileName from "@app/components/ProfileName.svelte"
|
||||
import RelayName from "@app/components/RelayName.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import {chatSearch} from "@app/chats"
|
||||
import {user} from "@app/core"
|
||||
import {deriveUserRoomSearch} from "@app/rooms"
|
||||
import {makeChatPath, makeRoomPath} from "@app/routes"
|
||||
import {shareTo, type Share} from "@app/share"
|
||||
|
||||
type Props = {
|
||||
share: Share
|
||||
}
|
||||
|
||||
const {share}: Props = $props()
|
||||
|
||||
const roomSearch = deriveUserRoomSearch()
|
||||
|
||||
const toggle = (path: string) => {
|
||||
selection = path === selection ? "" : path
|
||||
}
|
||||
|
||||
const back = () => history.back()
|
||||
|
||||
const submit = () => shareTo(selection, share, {replaceState: true})
|
||||
|
||||
let term = $state("")
|
||||
let selection = $state("")
|
||||
|
||||
const roomResults = $derived($roomSearch.searchOptions(term))
|
||||
const chatResults = $derived($chatSearch.searchOptions(term))
|
||||
</script>
|
||||
|
||||
<Modal tag="form" onsubmit={preventDefault(submit)}>
|
||||
<ModalBody>
|
||||
<ModalHeader>
|
||||
<ModalTitle>Share</ModalTitle>
|
||||
<ModalSubtitle>Where would you like to share this?</ModalSubtitle>
|
||||
</ModalHeader>
|
||||
<label class="input input-sm flex w-full items-center gap-2">
|
||||
<Icon size={4} icon={Magnifier} />
|
||||
<input
|
||||
bind:value={term}
|
||||
class="min-w-0 grow"
|
||||
type="text"
|
||||
placeholder="Search rooms and conversations..." />
|
||||
</label>
|
||||
<div class="flex min-h-96 flex-col gap-4">
|
||||
{#if roomResults.length > 0}
|
||||
<p class="text-xs uppercase tracking-wide opacity-60">Rooms</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each roomResults as { url, h } (makeRoomPath(url, h))}
|
||||
{@const path = makeRoomPath(url, h)}
|
||||
<Button
|
||||
type="button"
|
||||
class={cx(
|
||||
"flex w-full min-w-0 items-center justify-between gap-2 button",
|
||||
selection === path ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={() => toggle(path)}>
|
||||
<RoomNameWithImage {url} {h} />
|
||||
<RelayName {url} class="min-w-0 truncate text-xs opacity-60" />
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if chatResults.length > 0}
|
||||
<p class="text-xs uppercase tracking-wide opacity-60">Direct Messages</p>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each chatResults as chat (chat.id)}
|
||||
{@const path = makeChatPath(chat.pubkeys)}
|
||||
{@const others = remove($user.pubkey, chat.pubkeys)}
|
||||
<Button
|
||||
type="button"
|
||||
class={cx(
|
||||
"flex w-full min-w-0 items-center justify-start gap-2 button",
|
||||
selection === path ? "button-primary" : "button-neutral",
|
||||
)}
|
||||
onclick={() => toggle(path)}>
|
||||
{#if others.length > 1}
|
||||
<ProfileCircles pubkeys={others} size={5} />
|
||||
<span class="min-w-0 truncate">
|
||||
<ProfileName pubkey={others[0]} />
|
||||
+{others.length - 1}
|
||||
</span>
|
||||
{:else if others.length === 1}
|
||||
<ProfileCircle pubkey={others[0]} size={5} />
|
||||
<span class="min-w-0 truncate">
|
||||
<ProfileName pubkey={others[0]} />
|
||||
</span>
|
||||
{:else}
|
||||
<ProfileCircle pubkey={$user.pubkey} size={5} />
|
||||
Note to self
|
||||
{/if}
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if roomResults.length === 0 && chatResults.length === 0}
|
||||
<p class="py-12 text-center opacity-75">
|
||||
{#if term}
|
||||
Nothing matched your search.
|
||||
{:else}
|
||||
Join a space or start a conversation to have somewhere to share to.
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button class="button button-link" onclick={back}>
|
||||
<Icon icon={AltArrowLeft} />
|
||||
Go back
|
||||
</Button>
|
||||
<Button type="submit" class="button button-primary" disabled={!selection}>
|
||||
Share
|
||||
<Icon icon={AltArrowRight} />
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
<script lang="ts">
|
||||
import {goto} from "$app/navigation"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {preventDefault} from "@lib/html"
|
||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
|
||||
import {setKey} from "@lib/implicit"
|
||||
import Icon from "@lib/components/Icon.svelte"
|
||||
import Button from "@lib/components/Button.svelte"
|
||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||
|
|
@ -15,6 +13,7 @@
|
|||
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
import {shareTo} from "@app/share"
|
||||
import {rooms} from "@app/core"
|
||||
|
||||
type Props = {
|
||||
|
|
@ -30,8 +29,7 @@
|
|||
const back = () => history.back()
|
||||
|
||||
const onSubmit = () => {
|
||||
setKey("share", event)
|
||||
goto(makeRoomPath(url, selection), {replaceState: true})
|
||||
shareTo(makeRoomPath(url, selection), {type: "event", value: event}, {replaceState: true})
|
||||
}
|
||||
|
||||
const toggleRoom = (h: string) => {
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
const store = new Map<string, unknown>()
|
||||
|
||||
export type Draft = {
|
||||
content?: string | object
|
||||
}
|
||||
|
||||
export class DraftKey<T> {
|
||||
constructor(private key: string) {}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
|||
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
|
||||
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
|
||||
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
|
||||
import {compressFileForUpload, uploadFile} from "@app/uploads"
|
||||
import {UPLOAD_MIME_TYPES, compressFileForUpload, uploadFile} from "@app/uploads"
|
||||
import {userSpaceUrls} from "@app/rooms"
|
||||
import {PLATFORM_RELAYS} from "@app/env"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
|
@ -108,6 +108,7 @@ export const makeEditor = async ({
|
|||
},
|
||||
fileUpload: {
|
||||
config: {
|
||||
allowedMimeTypes: UPLOAD_MIME_TYPES,
|
||||
upload: async (attrs: FileAttributes) =>
|
||||
uploadFile(await compressFileForUpload(attrs.file), {url, encrypt: encryptFiles}),
|
||||
onDrop: () => uploading?.set(true),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import {
|
|||
toNostrURI,
|
||||
} from "@welshman/util"
|
||||
import type {EventContent, TrustedEvent} from "@welshman/util"
|
||||
import {RoomLists, makeRoomKey} from "@welshman/app"
|
||||
import {RoomLists, makeRoomKey, createSearch} from "@welshman/app"
|
||||
import type {Room, RoomMeta} from "@welshman/app"
|
||||
import {
|
||||
deriveUserItem,
|
||||
|
|
@ -25,10 +25,12 @@ import {
|
|||
rooms,
|
||||
router,
|
||||
thunks,
|
||||
relays,
|
||||
user,
|
||||
} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {deriveEventsForUrl} from "@app/repository"
|
||||
import {makeRoomPath} from "@app/routes"
|
||||
|
||||
export enum MembershipStatus {
|
||||
Initial,
|
||||
|
|
@ -218,3 +220,30 @@ export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
|||
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
||||
},
|
||||
)
|
||||
|
||||
export const deriveUserRoomSearch = () =>
|
||||
derived(
|
||||
[userSpaceUrls, userRoomList, rooms.get().byUrl.$],
|
||||
([$userSpaceUrls, $userRoomList, $roomsByUrl]) => {
|
||||
const options = $userSpaceUrls.flatMap(url => {
|
||||
const favorites = new Set($userRoomList?.roomsForUrl(url) ?? [])
|
||||
// Spaces that aren't NIP-29 relays keep all their messages in the space-wide chat
|
||||
const roomIds = relays.get().get(url)?.hasNip(29)
|
||||
? ($roomsByUrl.get(url) ?? []).map(room => room.h)
|
||||
: ["chat"]
|
||||
|
||||
return roomIds.map(h => ({
|
||||
url,
|
||||
h,
|
||||
name: displayRoom(url, h),
|
||||
isFavorite: favorites.has(h),
|
||||
}))
|
||||
})
|
||||
|
||||
return createSearch(options, {
|
||||
getValue: option => makeRoomPath(option.url, option.h),
|
||||
fuseOptions: {keys: ["name", "url"]},
|
||||
sortFn: ({item}) => (item.isFavorite ? 0 : 1),
|
||||
})
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -107,10 +107,10 @@ export const goToMovedSpace = (oldUrl: string, newUrl: string) =>
|
|||
|
||||
export const goToHome = () => {
|
||||
if (PLATFORM_RELAYS.length > 0) {
|
||||
goToSpace(PLATFORM_RELAYS[0], get(page).url.hash)
|
||||
} else {
|
||||
goto("/home" + get(page).url.hash)
|
||||
return goToSpace(PLATFORM_RELAYS[0], get(page).url.hash)
|
||||
}
|
||||
|
||||
return goto("/home" + get(page).url.hash)
|
||||
}
|
||||
|
||||
// Content types, events
|
||||
|
|
|
|||
108
src/app/share.ts
108
src/app/share.ts
|
|
@ -1,18 +1,108 @@
|
|||
import {writable} from "svelte/store"
|
||||
import {goto} from "$app/navigation"
|
||||
import {Capacitor, registerPlugin} from "@capacitor/core"
|
||||
import type {PluginListenerHandle} from "@capacitor/core"
|
||||
import {noop} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
import type {TrustedEvent} from "@welshman/util"
|
||||
import {setKey} from "@lib/implicit"
|
||||
import {relays} from "@app/core"
|
||||
import {app, relays} from "@app/core"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {makeSpaceChatPath} from "@app/routes"
|
||||
import EventShare from "@app/components/EventShare.svelte"
|
||||
import {pushToast} from "@app/toast"
|
||||
import {UPLOAD_MIME_TYPES} from "@app/uploads"
|
||||
import ShareDialog from "@app/components/Share.svelte"
|
||||
import ShareEvent from "@app/components/ShareEvent.svelte"
|
||||
|
||||
// Share an event to chat. On NIP-29 relays we prompt for a room to share into;
|
||||
// otherwise we stash the event and jump to the space chat composer, which quotes it.
|
||||
export const shareEventToChat = (url: string, noun: string, event: TrustedEvent) => {
|
||||
export type Share =
|
||||
| {type: "event"; value: TrustedEvent}
|
||||
| {type: "text"; value: string}
|
||||
| {type: "file"; value: File}
|
||||
|
||||
export const pendingShare = writable<Maybe<Share>>(undefined)
|
||||
|
||||
// Set pendingShare after goto so the current view doesn't pop it
|
||||
export const shareTo = async (
|
||||
path: string,
|
||||
share: Share,
|
||||
options: {replaceState?: boolean} = {},
|
||||
) => {
|
||||
await goto(path, options)
|
||||
|
||||
pendingShare.set(share)
|
||||
}
|
||||
|
||||
export const shareEvent = (url: string, noun: string, event: TrustedEvent) => {
|
||||
if (relays.get().get(url)?.hasNip(29)) {
|
||||
pushModal(EventShare, {url, noun, event})
|
||||
pushModal(ShareEvent, {url, noun, event})
|
||||
} else {
|
||||
setKey("share", event)
|
||||
goto(makeSpaceChatPath(url))
|
||||
shareTo(makeSpaceChatPath(url), {type: "event", value: event})
|
||||
}
|
||||
}
|
||||
|
||||
const openShareDialog = (share: Share) => {
|
||||
if (app.get().user?.pubkey) {
|
||||
pushModal(ShareDialog, {share})
|
||||
}
|
||||
}
|
||||
|
||||
export const shareText = (value: string) => openShareDialog({type: "text", value})
|
||||
|
||||
type NativeShare = {
|
||||
text?: string
|
||||
path?: string
|
||||
name?: string
|
||||
type?: string
|
||||
}
|
||||
|
||||
// Shared media is copied somewhere capacitor's file server can reach, since neither an android
|
||||
// content uri nor an ios app group url means anything to the web view.
|
||||
export const shareFromNative = async ({
|
||||
text,
|
||||
path,
|
||||
name = "",
|
||||
type: mimeType = "",
|
||||
}: NativeShare) => {
|
||||
if (text) {
|
||||
shareText(text)
|
||||
} else if (path) {
|
||||
if (!UPLOAD_MIME_TYPES.includes(mimeType)) {
|
||||
return pushToast({theme: "error", message: "Flotilla can't share that type of file."})
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(Capacitor.convertFileSrc(path))
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to read ${path} (${response.status})`)
|
||||
}
|
||||
|
||||
const value = new File([await response.blob()], name, {type: mimeType})
|
||||
|
||||
openShareDialog({type: "file", value})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
pushToast({theme: "error", message: "Something went wrong reading the shared file."})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ShareIntentPlugin = {
|
||||
addListener(
|
||||
eventName: "shareReceived",
|
||||
listener: (share: NativeShare) => void,
|
||||
): Promise<PluginListenerHandle>
|
||||
}
|
||||
|
||||
const ShareIntent = registerPlugin<ShareIntentPlugin>("ShareIntent")
|
||||
|
||||
export const setupShareIntents = () => {
|
||||
if (Capacitor.getPlatform() === "android") {
|
||||
const listener = ShareIntent.addListener("shareReceived", shareFromNative)
|
||||
|
||||
return () => {
|
||||
listener.then(handle => handle.remove())
|
||||
}
|
||||
}
|
||||
|
||||
return noop
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,22 @@ export const getBlossomServer = async (options: GetBlossomServerOptions = {}) =>
|
|||
return first(DEFAULT_BLOSSOM_SERVERS)!
|
||||
}
|
||||
|
||||
// The editor's default list leaves out the formats an iPhone camera actually produces, so
|
||||
// anything shared from Photos would be rejected without a word. Heic gets re-encoded by the
|
||||
// compressor on its way to a blossom server; quicktime is uploaded as-is.
|
||||
export const UPLOAD_MIME_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/heic",
|
||||
"image/heif",
|
||||
"video/mp4",
|
||||
"video/mpeg",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
]
|
||||
|
||||
export type CompressFileOptions = {
|
||||
maxWidth?: number
|
||||
maxHeight?: number
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
import {kv, ss, storage} from "@app/storage"
|
||||
import {device} from "@app/device"
|
||||
import {userSettingsValues, notificationSettings} from "@app/settings"
|
||||
import {setupShareIntents, shareFromNative} from "@app/share"
|
||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||
import * as env from "@app/env"
|
||||
import {theme} from "@app/theme"
|
||||
|
|
@ -68,6 +69,13 @@
|
|||
return
|
||||
}
|
||||
|
||||
// The iOS share extension can't talk to us directly, so it hands its payload over as a
|
||||
// flotilla://share url
|
||||
if (url.host === "share") {
|
||||
shareFromNative(Object.fromEntries(url.searchParams))
|
||||
return
|
||||
}
|
||||
|
||||
if (url.host === "x-callback-url") {
|
||||
if (url.pathname === "/authError") {
|
||||
const errorMessage = url.searchParams.get("errorMessage")
|
||||
|
|
@ -161,6 +169,9 @@
|
|||
// History, navigation, application data
|
||||
unsubscribers.push(setupHistory(), setupAnalytics(), syncApplicationData())
|
||||
|
||||
// Listen for links shared into the app from elsewhere on the device
|
||||
unsubscribers.push(setupShareIntents())
|
||||
|
||||
// Initialize keyboard state tracking
|
||||
unsubscribers.push(syncKeyboard())
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
<script lang="ts">
|
||||
import {goToHome} from "@app/routes"
|
||||
|
||||
$effect(goToHome)
|
||||
$effect(() => {
|
||||
goToHome()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
14
src/routes/share/+page.svelte
Normal file
14
src/routes/share/+page.svelte
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import {onMount} from "svelte"
|
||||
import {page} from "$app/stores"
|
||||
import {goToHome} from "@app/routes"
|
||||
import {shareFromNative} from "@app/share"
|
||||
|
||||
const params = Object.fromEntries($page.url.searchParams)
|
||||
|
||||
onMount(async () => {
|
||||
await goToHome()
|
||||
|
||||
shareFromNative(params)
|
||||
})
|
||||
</script>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
import {app, deletes, network} from "@app/core"
|
||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||
import {decodeRelay} from "@app/relays"
|
||||
import {shareEventToChat} from "@app/share"
|
||||
import {shareEvent} from "@app/share"
|
||||
import {pushModal} from "@app/modal"
|
||||
import {pushToast} from "@app/toast"
|
||||
|
||||
|
|
@ -80,7 +80,7 @@
|
|||
menuOpen = false
|
||||
|
||||
if ($board) {
|
||||
shareEventToChat(url, "Shelf", $board.event)
|
||||
shareEvent(url, "Shelf", $board.event)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export default defineConfig({
|
|||
// carries node_modules from welshman's own install — a second prosemirror, whose Plugin and
|
||||
// PluginKey classes tiptap refuses to mix with ours ("Adding different instances of a keyed
|
||||
// plugin"). Deduping resolves these to this project's copy wherever they are imported from, and
|
||||
// does nothing when only one copy exists.
|
||||
// does nothing when only one copy exists. Vite resolves a deduped package from the project root
|
||||
// and nowhere else, so every name listed here has to be a direct dependency — under pnpm a
|
||||
// transitive one isn't reachable from the root and the build fails to resolve it.
|
||||
resolve: {
|
||||
dedupe: [
|
||||
"@tiptap/core",
|
||||
|
|
|
|||
Loading…
Reference in a new issue