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" />
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="https" android:host="app.flotilla.social" />
|
<data android:scheme="https" android:host="app.flotilla.social" />
|
||||||
</intent-filter>
|
</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>
|
</activity>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import android.os.Bundle;
|
||||||
import com.getcapacitor.BridgeActivity;
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
|
||||||
import social.flotilla.notifications.AndroidPushFallbackPlugin;
|
import social.flotilla.notifications.AndroidPushFallbackPlugin;
|
||||||
|
import social.flotilla.share.ShareIntentPlugin;
|
||||||
|
|
||||||
public class MainActivity extends BridgeActivity {
|
public class MainActivity extends BridgeActivity {
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
registerPlugin(AndroidPushFallbackPlugin.class);
|
registerPlugin(AndroidPushFallbackPlugin.class);
|
||||||
|
registerPlugin(ShareIntentPlugin.class);
|
||||||
createPushNotificationChannel();
|
createPushNotificationChannel();
|
||||||
super.onCreate(savedInstanceState);
|
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 = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* 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 */; };
|
2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
|
||||||
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */; };
|
3478F0332E846FEB002431E0 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */; };
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
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 */; };
|
AC8D5382B9575A9124613C5D /* Pods_Flotilla_Chat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA42C7E3CF3FFF7A17A3A729 /* Pods_Flotilla_Chat.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* 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 */
|
/* 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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
|
|
@ -38,6 +68,13 @@
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
052C05C3302BD03100DD0620 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
504EC3011FED79650016851F /* Frameworks */ = {
|
504EC3011FED79650016851F /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|
@ -49,6 +86,16 @@
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
052C05E1302BD03100DD0620 /* ShareExtension */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
052C05E2302BD03100DD0620 /* ShareViewController.swift */,
|
||||||
|
052C05E3302BD03100DD0620 /* Info.plist */,
|
||||||
|
052C05E4302BD03100DD0620 /* ShareExtension.entitlements */,
|
||||||
|
);
|
||||||
|
path = ShareExtension;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -63,6 +110,7 @@
|
||||||
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
|
3478F0322E846FEB002431E0 /* PrivacyInfo.xcprivacy */,
|
||||||
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
051414282E0CC28400BE0BC8 /* Flotilla Chat.entitlements */,
|
||||||
504EC3061FED79650016851F /* App */,
|
504EC3061FED79650016851F /* App */,
|
||||||
|
052C05E1302BD03100DD0620 /* ShareExtension */,
|
||||||
504EC3051FED79650016851F /* Products */,
|
504EC3051FED79650016851F /* Products */,
|
||||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||||
|
|
@ -73,6 +121,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
504EC3041FED79650016851F /* Flotilla Chat.app */,
|
504EC3041FED79650016851F /* Flotilla Chat.app */,
|
||||||
|
052C05C6302BD03100DD0620 /* ShareExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -106,6 +155,25 @@
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget 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 */ = {
|
504EC3031FED79650016851F /* Flotilla Chat */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */;
|
buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "Flotilla Chat" */;
|
||||||
|
|
@ -115,10 +183,12 @@
|
||||||
504EC3011FED79650016851F /* Frameworks */,
|
504EC3011FED79650016851F /* Frameworks */,
|
||||||
504EC3021FED79650016851F /* Resources */,
|
504EC3021FED79650016851F /* Resources */,
|
||||||
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
|
||||||
|
052C05D1302BD03100DD0620 /* Embed Foundation Extensions */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
052C05CF302BD03100DD0620 /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = "Flotilla Chat";
|
name = "Flotilla Chat";
|
||||||
productName = App;
|
productName = App;
|
||||||
|
|
@ -132,9 +202,12 @@
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 920;
|
LastSwiftUpdateCheck = 2630;
|
||||||
LastUpgradeCheck = 2630;
|
LastUpgradeCheck = 2630;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
052C05C5302BD03100DD0620 = {
|
||||||
|
CreatedOnToolsVersion = 26.3;
|
||||||
|
};
|
||||||
504EC3031FED79650016851F = {
|
504EC3031FED79650016851F = {
|
||||||
CreatedOnToolsVersion = 9.2;
|
CreatedOnToolsVersion = 9.2;
|
||||||
LastSwiftMigration = 1100;
|
LastSwiftMigration = 1100;
|
||||||
|
|
@ -156,11 +229,19 @@
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
504EC3031FED79650016851F /* Flotilla Chat */,
|
504EC3031FED79650016851F /* Flotilla Chat */,
|
||||||
|
052C05C5302BD03100DD0620 /* ShareExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
052C05C4302BD03100DD0620 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
504EC3021FED79650016851F /* Resources */ = {
|
504EC3021FED79650016851F /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|
@ -214,6 +295,14 @@
|
||||||
/* End PBXShellScriptBuildPhase section */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
052C05C2302BD03100DD0620 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
052C05E5302BD03100DD0620 /* ShareViewController.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
504EC3001FED79650016851F /* Sources */ = {
|
504EC3001FED79650016851F /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
|
|
@ -224,6 +313,14 @@
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
052C05CF302BD03100DD0620 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 052C05C5302BD03100DD0620 /* ShareExtension */;
|
||||||
|
targetProxy = 052C05CE302BD03100DD0620 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
504EC30B1FED79650016851F /* Main.storyboard */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
|
|
@ -244,6 +341,78 @@
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration 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 */ = {
|
504EC3141FED79650016851F /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
|
@ -424,6 +593,15 @@
|
||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList 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" */ = {
|
504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
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>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<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>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(MARKETING_VERSION)</string>
|
<string>$(MARKETING_VERSION)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
|
|
|
||||||
|
|
@ -8,5 +8,9 @@
|
||||||
<array>
|
<array>
|
||||||
<string>applinks:app.flotilla.social</string>
|
<string>applinks:app.flotilla.social</string>
|
||||||
</array>
|
</array>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.social.flotilla</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</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",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@tiptap/core": "^2.27.2",
|
"@tiptap/core": "^2.27.2",
|
||||||
"@tiptap/pm": "^2.27.2",
|
"@tiptap/pm": "^2.27.2",
|
||||||
|
"@tiptap/suggestion": "^2.27.2",
|
||||||
"@types/qrcode": "^1.5.6",
|
"@types/qrcode": "^1.5.6",
|
||||||
"@types/throttle-debounce": "^5.0.2",
|
"@types/throttle-debounce": "^5.0.2",
|
||||||
"@vite-pwa/assets-generator": "^1.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-signer-capacitor-plugin": "github:coracle-social/nostr-signer-capacitor-plugin#main",
|
||||||
"nostr-tools": "^2.19.4",
|
"nostr-tools": "^2.19.4",
|
||||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
"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",
|
"qr-scanner": "^1.4.2",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"throttle-debounce": "^5.0.2",
|
"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 Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import {shareEventToChat} from "@app/share"
|
import {shareEvent} from "@app/share"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event: board.event})
|
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
|
let ul: Element
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
enumerate,
|
enumerate,
|
||||||
formatTimestampAsDate,
|
formatTimestampAsDate,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
|
import type {Maybe} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventTemplate, EventContent} from "@welshman/util"
|
||||||
import {makeEvent, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
|
import {makeEvent, DIRECT_MESSAGE, DIRECT_MESSAGE_FILE} from "@welshman/util"
|
||||||
import {parse, isLink} from "@welshman/content"
|
import {parse, isLink} from "@welshman/content"
|
||||||
|
|
@ -44,8 +45,9 @@
|
||||||
import {userSettingsValues} from "@app/settings"
|
import {userSettingsValues} from "@app/settings"
|
||||||
import {deriveChat, makeChatId} from "@app/chats"
|
import {deriveChat, makeChatId} from "@app/chats"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {DraftKey} from "@app/drafts"
|
import {DraftKey, type Draft} from "@app/drafts"
|
||||||
import {prependParent} from "@app/rooms"
|
import {prependParent} from "@app/rooms"
|
||||||
|
import {pendingShare, type Share} from "@app/share"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -57,7 +59,7 @@
|
||||||
|
|
||||||
const chatId = makeChatId(pubkeys)
|
const chatId = makeChatId(pubkeys)
|
||||||
const chat = deriveChat(chatId)
|
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 others = remove($user.pubkey, pubkeys)
|
||||||
const messagingRelayLists = $app.use(MessagingRelayLists).index.$
|
const messagingRelayLists = $app.use(MessagingRelayLists).index.$
|
||||||
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayLists.has(pk)))
|
const missingRelayLists = $derived(others.filter(pk => !$messagingRelayLists.has(pk)))
|
||||||
|
|
@ -183,6 +185,26 @@
|
||||||
let compose: ChatCompose | undefined = $state()
|
let compose: ChatCompose | undefined = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
let eventToEdit: 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 = $derived.by(() => {
|
||||||
const elements = []
|
const elements = []
|
||||||
|
|
@ -318,13 +340,13 @@
|
||||||
<ChatComposeEdit clear={clearEventToEdit} />
|
<ChatComposeEdit clear={clearEventToEdit} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#key eventToEdit}
|
{#key initialValues}
|
||||||
<ChatCompose
|
<ChatCompose
|
||||||
bind:this={compose}
|
bind:this={compose}
|
||||||
{onSubmit}
|
{onSubmit}
|
||||||
{onEscape}
|
{onEscape}
|
||||||
{onEditPrevious}
|
{onEditPrevious}
|
||||||
initialValues={eventToEdit}
|
{initialValues}
|
||||||
draftKey={eventToEdit ? undefined : draftKey}
|
draftKey={eventToEdit ? undefined : draftKey}
|
||||||
disabled={Boolean(missingRelayLists.length)} />
|
disabled={Boolean(missingRelayLists.length)} />
|
||||||
{/key}
|
{/key}
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,19 @@
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {makeEditor} from "@app/editor"
|
import {makeEditor} from "@app/editor"
|
||||||
import {type DraftKey} from "@app/drafts"
|
import {type DraftKey, type Draft} from "@app/drafts"
|
||||||
|
import type {Share} from "@app/share"
|
||||||
type Values = {
|
|
||||||
content?: string | object
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
draftKey?: DraftKey<Values>
|
draftKey?: DraftKey<Draft>
|
||||||
onEscape?: () => void
|
onEscape?: () => void
|
||||||
onEditPrevious?: () => void
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => void
|
onSubmit: (event: EventContent) => void
|
||||||
initialValues?: Values
|
initialValues?: Share
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
const {
|
||||||
initialValues,
|
initialValues,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
draftKey,
|
draftKey,
|
||||||
|
|
@ -35,10 +32,6 @@
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
if (!initialValues) {
|
|
||||||
initialValues = draftKey?.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
const autofocus = !isMobile && !disabled
|
const autofocus = !isMobile && !disabled
|
||||||
|
|
||||||
const uploading = writable(false)
|
const uploading = writable(false)
|
||||||
|
|
@ -81,7 +74,9 @@
|
||||||
ed.chain().clearContent().run()
|
ed.chain().clearContent().run()
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = $state(initialValues?.content ?? "")
|
let content = $state(
|
||||||
|
initialValues?.type === "text" ? initialValues.value : (draftKey?.get()?.content ?? ""),
|
||||||
|
)
|
||||||
|
|
||||||
const onChange = (json: object) => {
|
const onChange = (json: object) => {
|
||||||
content = json
|
content = json
|
||||||
|
|
@ -103,6 +98,12 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||||
|
|
||||||
|
if (initialValues?.type === "file") {
|
||||||
|
ed.chain()
|
||||||
|
.addFile(initialValues.value, ed.state.selection.from + 1)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(async () => {
|
onDestroy(async () => {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
import Report from "@app/components/Report.svelte"
|
import Report from "@app/components/Report.svelte"
|
||||||
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
import EventDeleteConfirm from "@app/components/EventDeleteConfirm.svelte"
|
||||||
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
import PinboardSelect from "@app/components/PinboardSelect.svelte"
|
||||||
import {shareEventToChat} from "@app/share"
|
import {shareEvent} from "@app/share"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
const addToLibrary = () => pushModal(PinboardSelect, {url, event})
|
const addToLibrary = () => pushModal(PinboardSelect, {url, event})
|
||||||
|
|
||||||
const share = () => shareEventToChat(url, noun, event)
|
const share = () => shareEvent(url, noun, event)
|
||||||
|
|
||||||
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
const showDelete = () => pushModal(EventDeleteConfirm, {url, event})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
import PinEdit from "@app/components/PinEdit.svelte"
|
import PinEdit from "@app/components/PinEdit.svelte"
|
||||||
import {deletes} from "@app/core"
|
import {deletes} from "@app/core"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||||
import {shareEventToChat} from "@app/share"
|
import {shareEvent} from "@app/share"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
|
|
||||||
const showInfo = () => pushModal(EventInfo, {url, event: pin.event})
|
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})
|
const edit = () => pushModal(PinEdit, {url, pin})
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,10 +30,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if $userSpaceUrls.includes(url)}
|
{#if $userSpaceUrls.includes(url)}
|
||||||
<div
|
<div class="absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content flex justify-center items-center">
|
||||||
class="tip absolute -right-1 -top-1 h-5 w-5 rounded-full bg-primary text-primary-content"
|
<Icon icon={CheckCircle} class="scale-120" />
|
||||||
data-tip="You are already a member of this space.">
|
|
||||||
<Icon icon={CheckCircle} class="scale-110" />
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||||
|
import type {Maybe} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||||
import {publish} from "@welshman/app"
|
import {publish} from "@welshman/app"
|
||||||
|
|
@ -16,7 +17,6 @@
|
||||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||||
import Close from "@assets/icons/close.svg?dataurl"
|
import Close from "@assets/icons/close.svg?dataurl"
|
||||||
import {fade, fly} from "@lib/transition"
|
import {fade, fly} from "@lib/transition"
|
||||||
import {popKey} from "@lib/implicit"
|
|
||||||
import {documentActive} from "@lib/html"
|
import {documentActive} from "@lib/html"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
|
|
@ -46,6 +46,7 @@
|
||||||
import {makeFeed} from "@app/feeds"
|
import {makeFeed} from "@app/feeds"
|
||||||
import {checked, deferredRoomPath, notifications, setChecked} from "@app/notifications"
|
import {checked, deferredRoomPath, notifications, setChecked} from "@app/notifications"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
|
import {pendingShare, type Share} from "@app/share"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -183,7 +184,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const onSubmit = async ({content, tags}: EventContent) => {
|
const onSubmit = async ({content, tags}: EventContent) => {
|
||||||
if (!content && !share) {
|
if (!content && !sharedEvent) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,8 +217,8 @@
|
||||||
command.publishToRelays([url])
|
command.publishToRelays([url])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (share) {
|
if (sharedEvent) {
|
||||||
template = await prependParent(share, template, url)
|
template = await prependParent(sharedEvent, template, url)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent) {
|
if (parent) {
|
||||||
|
|
@ -324,7 +325,7 @@
|
||||||
let isUserScrolling = $state(false)
|
let isUserScrolling = $state(false)
|
||||||
let loadingBackward = $state(true)
|
let loadingBackward = $state(true)
|
||||||
let loadingForward = $state(true)
|
let loadingForward = $state(true)
|
||||||
let share = $state(popKey<TrustedEvent | undefined>("share"))
|
let share: Maybe<Share> = $state()
|
||||||
let parent: TrustedEvent | undefined = $state()
|
let parent: TrustedEvent | undefined = $state()
|
||||||
let element: HTMLElement | undefined = $state()
|
let element: HTMLElement | undefined = $state()
|
||||||
let lastVisibleAt = now()
|
let lastVisibleAt = now()
|
||||||
|
|
@ -338,6 +339,27 @@
|
||||||
let compose: RoomCompose | undefined = $state()
|
let compose: RoomCompose | undefined = $state()
|
||||||
let eventToEdit: TrustedEvent | 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, () => {
|
const clearIsUserScrolling = debounce(150, () => {
|
||||||
isUserScrolling = false
|
isUserScrolling = false
|
||||||
})
|
})
|
||||||
|
|
@ -619,21 +641,21 @@
|
||||||
{#if parent}
|
{#if parent}
|
||||||
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if share}
|
{#if sharedEvent}
|
||||||
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
<RoomComposeParent event={sharedEvent} clear={clearShare} verb="Sharing" />
|
||||||
{/if}
|
{/if}
|
||||||
{#if eventToEdit}
|
{#if eventToEdit}
|
||||||
<RoomComposeEdit clear={clearEventToEdit} />
|
<RoomComposeEdit clear={clearEventToEdit} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#key eventToEdit}
|
{#key initialValues}
|
||||||
<RoomCompose
|
<RoomCompose
|
||||||
{url}
|
{url}
|
||||||
{h}
|
{h}
|
||||||
{onSubmit}
|
{onSubmit}
|
||||||
{onEscape}
|
{onEscape}
|
||||||
{onEditPrevious}
|
{onEditPrevious}
|
||||||
initialValues={eventToEdit}
|
{initialValues}
|
||||||
bind:this={compose} />
|
bind:this={compose} />
|
||||||
{/key}
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -13,29 +13,22 @@
|
||||||
import ComposeMenu from "@app/components/ComposeMenu.svelte"
|
import ComposeMenu from "@app/components/ComposeMenu.svelte"
|
||||||
import EditorContent from "@app/editor/EditorContent.svelte"
|
import EditorContent from "@app/editor/EditorContent.svelte"
|
||||||
import {makeEditor} from "@app/editor"
|
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"
|
import {onDestroy, onMount} from "svelte"
|
||||||
|
|
||||||
type Values = {
|
|
||||||
content?: string | object
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
url?: string
|
url?: string
|
||||||
h?: string
|
h?: string
|
||||||
onEscape?: () => void
|
onEscape?: () => void
|
||||||
onEditPrevious?: () => void
|
onEditPrevious?: () => void
|
||||||
onSubmit: (event: EventContent) => 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
|
const draftKey = url || h ? new DraftKey<Draft>(`room:${url ?? ""}:${h ?? ""}`) : undefined
|
||||||
|
|
||||||
if (!initialValues) {
|
|
||||||
initialValues = draftKey?.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
const autofocus = !isMobile
|
const autofocus = !isMobile
|
||||||
|
|
||||||
|
|
@ -76,7 +69,9 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let popover: Instance | undefined = $state()
|
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) => {
|
const onChange = (json: object) => {
|
||||||
content = json
|
content = json
|
||||||
|
|
@ -98,6 +93,12 @@
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const ed = await editor
|
const ed = await editor
|
||||||
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
ed.view.dom.addEventListener("keydown", handleKeyDown)
|
||||||
|
|
||||||
|
if (initialValues?.type === "file") {
|
||||||
|
ed.chain()
|
||||||
|
.addFile(initialValues.value, ed.state.selection.from + 1)
|
||||||
|
.run()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onDestroy(async () => {
|
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">
|
<script lang="ts">
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {preventDefault} from "@lib/html"
|
import {preventDefault} from "@lib/html"
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import AltArrowRight from "@assets/icons/alt-arrow-right.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 Icon from "@lib/components/Icon.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
|
@ -15,6 +13,7 @@
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
|
||||||
import {makeRoomPath} from "@app/routes"
|
import {makeRoomPath} from "@app/routes"
|
||||||
|
import {shareTo} from "@app/share"
|
||||||
import {rooms} from "@app/core"
|
import {rooms} from "@app/core"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -30,8 +29,7 @@
|
||||||
const back = () => history.back()
|
const back = () => history.back()
|
||||||
|
|
||||||
const onSubmit = () => {
|
const onSubmit = () => {
|
||||||
setKey("share", event)
|
shareTo(makeRoomPath(url, selection), {type: "event", value: event}, {replaceState: true})
|
||||||
goto(makeRoomPath(url, selection), {replaceState: true})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleRoom = (h: string) => {
|
const toggleRoom = (h: string) => {
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
const store = new Map<string, unknown>()
|
const store = new Map<string, unknown>()
|
||||||
|
|
||||||
|
export type Draft = {
|
||||||
|
content?: string | object
|
||||||
|
}
|
||||||
|
|
||||||
export class DraftKey<T> {
|
export class DraftKey<T> {
|
||||||
constructor(private key: string) {}
|
constructor(private key: string) {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import ProfileSuggestion from "@app/editor/ProfileSuggestion.svelte"
|
||||||
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
|
import {RoomReferenceExtension} from "@app/editor/RoomReferenceExtension"
|
||||||
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
|
import RoomSuggestion from "@app/editor/RoomSuggestion.svelte"
|
||||||
import {NativeClipboardPasteExtension} from "@app/editor/clipboard"
|
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 {userSpaceUrls} from "@app/rooms"
|
||||||
import {PLATFORM_RELAYS} from "@app/env"
|
import {PLATFORM_RELAYS} from "@app/env"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
@ -108,6 +108,7 @@ export const makeEditor = async ({
|
||||||
},
|
},
|
||||||
fileUpload: {
|
fileUpload: {
|
||||||
config: {
|
config: {
|
||||||
|
allowedMimeTypes: UPLOAD_MIME_TYPES,
|
||||||
upload: async (attrs: FileAttributes) =>
|
upload: async (attrs: FileAttributes) =>
|
||||||
uploadFile(await compressFileForUpload(attrs.file), {url, encrypt: encryptFiles}),
|
uploadFile(await compressFileForUpload(attrs.file), {url, encrypt: encryptFiles}),
|
||||||
onDrop: () => uploading?.set(true),
|
onDrop: () => uploading?.set(true),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import {
|
||||||
toNostrURI,
|
toNostrURI,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import type {EventContent, TrustedEvent} 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 type {Room, RoomMeta} from "@welshman/app"
|
||||||
import {
|
import {
|
||||||
deriveUserItem,
|
deriveUserItem,
|
||||||
|
|
@ -25,10 +25,12 @@ import {
|
||||||
rooms,
|
rooms,
|
||||||
router,
|
router,
|
||||||
thunks,
|
thunks,
|
||||||
|
relays,
|
||||||
user,
|
user,
|
||||||
} from "@app/core"
|
} from "@app/core"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||||
import {deriveEventsForUrl} from "@app/repository"
|
import {deriveEventsForUrl} from "@app/repository"
|
||||||
|
import {makeRoomPath} from "@app/routes"
|
||||||
|
|
||||||
export enum MembershipStatus {
|
export enum MembershipStatus {
|
||||||
Initial,
|
Initial,
|
||||||
|
|
@ -218,3 +220,30 @@ export const deriveUserRoomMembershipStatus = (url: string, h: string) =>
|
||||||
return isMember ? MembershipStatus.Granted : MembershipStatus.Initial
|
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 = () => {
|
export const goToHome = () => {
|
||||||
if (PLATFORM_RELAYS.length > 0) {
|
if (PLATFORM_RELAYS.length > 0) {
|
||||||
goToSpace(PLATFORM_RELAYS[0], get(page).url.hash)
|
return goToSpace(PLATFORM_RELAYS[0], get(page).url.hash)
|
||||||
} else {
|
|
||||||
goto("/home" + get(page).url.hash)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return goto("/home" + get(page).url.hash)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content types, events
|
// 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 {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 type {TrustedEvent} from "@welshman/util"
|
||||||
import {setKey} from "@lib/implicit"
|
import {app, relays} from "@app/core"
|
||||||
import {relays} from "@app/core"
|
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {makeSpaceChatPath} from "@app/routes"
|
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;
|
export type Share =
|
||||||
// otherwise we stash the event and jump to the space chat composer, which quotes it.
|
| {type: "event"; value: TrustedEvent}
|
||||||
export const shareEventToChat = (url: string, noun: string, event: 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)) {
|
if (relays.get().get(url)?.hasNip(29)) {
|
||||||
pushModal(EventShare, {url, noun, event})
|
pushModal(ShareEvent, {url, noun, event})
|
||||||
} else {
|
} else {
|
||||||
setKey("share", event)
|
shareTo(makeSpaceChatPath(url), {type: "event", value: event})
|
||||||
goto(makeSpaceChatPath(url))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)!
|
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 = {
|
export type CompressFileOptions = {
|
||||||
maxWidth?: number
|
maxWidth?: number
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
import {kv, ss, storage} from "@app/storage"
|
import {kv, ss, storage} from "@app/storage"
|
||||||
import {device} from "@app/device"
|
import {device} from "@app/device"
|
||||||
import {userSettingsValues, notificationSettings} from "@app/settings"
|
import {userSettingsValues, notificationSettings} from "@app/settings"
|
||||||
|
import {setupShareIntents, shareFromNative} from "@app/share"
|
||||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||||
import * as env from "@app/env"
|
import * as env from "@app/env"
|
||||||
import {theme} from "@app/theme"
|
import {theme} from "@app/theme"
|
||||||
|
|
@ -68,6 +69,13 @@
|
||||||
return
|
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.host === "x-callback-url") {
|
||||||
if (url.pathname === "/authError") {
|
if (url.pathname === "/authError") {
|
||||||
const errorMessage = url.searchParams.get("errorMessage")
|
const errorMessage = url.searchParams.get("errorMessage")
|
||||||
|
|
@ -161,6 +169,9 @@
|
||||||
// History, navigation, application data
|
// History, navigation, application data
|
||||||
unsubscribers.push(setupHistory(), setupAnalytics(), syncApplicationData())
|
unsubscribers.push(setupHistory(), setupAnalytics(), syncApplicationData())
|
||||||
|
|
||||||
|
// Listen for links shared into the app from elsewhere on the device
|
||||||
|
unsubscribers.push(setupShareIntents())
|
||||||
|
|
||||||
// Initialize keyboard state tracking
|
// Initialize keyboard state tracking
|
||||||
unsubscribers.push(syncKeyboard())
|
unsubscribers.push(syncKeyboard())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {goToHome} from "@app/routes"
|
import {goToHome} from "@app/routes"
|
||||||
|
|
||||||
$effect(goToHome)
|
$effect(() => {
|
||||||
|
goToHome()
|
||||||
|
})
|
||||||
</script>
|
</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 {app, deletes, network} from "@app/core"
|
||||||
import {deriveUserIsSpaceAdmin} from "@app/management"
|
import {deriveUserIsSpaceAdmin} from "@app/management"
|
||||||
import {decodeRelay} from "@app/relays"
|
import {decodeRelay} from "@app/relays"
|
||||||
import {shareEventToChat} from "@app/share"
|
import {shareEvent} from "@app/share"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
import {pushToast} from "@app/toast"
|
import {pushToast} from "@app/toast"
|
||||||
|
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
menuOpen = false
|
menuOpen = false
|
||||||
|
|
||||||
if ($board) {
|
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
|
// 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
|
// 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
|
// 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: {
|
resolve: {
|
||||||
dedupe: [
|
dedupe: [
|
||||||
"@tiptap/core",
|
"@tiptap/core",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue