diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7c8916197..e14378264 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -37,8 +37,12 @@ android {
applicationId = "dev.dimension.flare"
minSdk = libs.versions.minSdk.get().toInt()
targetSdk = libs.versions.compileSdk.get().toInt()
- versionCode = System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: fdroidProp.getProperty("versionCode")?.toIntOrNull() ?: 1
- versionName = System.getenv("BUILD_VERSION")?.toString() ?: fdroidProp.getProperty("versionName")?.toString() ?: "0.0.0"
+ versionCode =
+ System.getenv("BUILD_NUMBER")?.toIntOrNull() ?: fdroidProp.getProperty("versionCode")
+ ?.toIntOrNull() ?: 1
+ versionName =
+ System.getenv("BUILD_VERSION")?.toString() ?: fdroidProp.getProperty("versionName")
+ ?.toString() ?: "0.0.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -188,3 +192,57 @@ if (project.file("google-services.json").exists()) {
uploadCrashlyticsMappingFileRelease.dependsOn(processDebugGoogleServices)
}
}
+
+
+abstract class GenerateDeepLinkManifestTask : DefaultTask() {
+ @get:InputFile
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val hostsFile: RegularFileProperty
+ @get:OutputFile
+ abstract val manifest: RegularFileProperty
+
+ @TaskAction
+ fun run() {
+ val hosts = hostsFile.get().asFile.readLines()
+ .map { it.trim() }
+ .filter { it.isNotEmpty() && !it.startsWith("#") }
+ .distinct()
+ val dataTags = hosts.joinToString("\n") { host ->
+ """"""
+ }
+
+ manifest.get().asFile.writeText(
+ """
+
+
+
+
+
+
+
+
+
+ $dataTags
+
+
+
+
+ """.trimIndent()
+ )
+ }
+}
+
+extensions.getByType(com.android.build.api.variant.AndroidComponentsExtension::class.java)
+ .onVariants { variant: com.android.build.api.variant.Variant ->
+ val t = tasks.register(
+ "generate${variant.name.replaceFirstChar { it.uppercase() }}DeepLinkManifest",
+ GenerateDeepLinkManifestTask::class.java
+ ) {
+ hostsFile = project.layout.projectDirectory.file("deeplink.txt")
+ }
+
+ variant.sources.manifests.addGeneratedManifestFile(
+ t,
+ GenerateDeepLinkManifestTask::manifest
+ )
+ }
diff --git a/app/deeplink.txt b/app/deeplink.txt
new file mode 100644
index 000000000..0e61ddef0
--- /dev/null
+++ b/app/deeplink.txt
@@ -0,0 +1,24 @@
+x.com
+twitter.com
+bsky.app
+pawoo.net
+mastodon.social
+misskey.io
+next.misskey.io
+mstdn.jp
+mstdn.social
+mastodon.world
+mastodon.sdf.org
+universeodon.com
+techhub.social
+mastodonapp.uk
+mastodon.uno
+m.cmx.im
+mstdn.party
+infosec.exchange
+hachyderm.io
+sfba.social
+misskey.design
+ohai.social
+mastodon.nl
+troet.cafe
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 539077157..9b558b21c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,7 +27,7 @@
@@ -41,17 +41,6 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt
index 3b526e569..050ab61a0 100644
--- a/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt
+++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMapping.kt
@@ -50,6 +50,21 @@ internal object DeepLinkMapping {
statusKey = MicroBlogKey(id, accountKey.host),
)
}
+
+ @Serializable
+ data class PostMedia(
+ val handle: String,
+ val id: String,
+ val index: Int,
+ ) : Type {
+ override fun deepLink(accountKey: MicroBlogKey): DeeplinkRoute =
+ DeeplinkRoute.Media.StatusMedia(
+ accountType = AccountType.Specific(accountKey),
+ statusKey = MicroBlogKey(id, accountKey.host),
+ index = index,
+ preview = null,
+ )
+ }
}
fun generatePattern(
@@ -111,6 +126,13 @@ internal object DeepLinkMapping {
"https://www.$xqtHost/{handle}/status/{id}",
"https://www.$xqtOldHost/{handle}/",
)
+ val media =
+ listOf(
+ "https://$xqtHost/{handle}/status/{id}/photo/{index}",
+ "https://$xqtOldHost/{handle}/status/{id}/photo/{index}",
+ "https://www.$xqtHost/{handle}/status/{id}/photo/{index}",
+ "https://www.$xqtOldHost/{handle}/status/{id}/photo/{index}",
+ )
profile.map {
DeepLinkPattern(
Type.Profile.serializer(),
@@ -122,6 +144,12 @@ internal object DeepLinkMapping {
Type.Post.serializer(),
Url(it),
)
+ } +
+ media.map {
+ DeepLinkPattern(
+ Type.PostMedia.serializer(),
+ Url(it),
+ )
}
}
diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt
index 0383cdc37..9d1ffcf56 100644
--- a/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt
+++ b/shared/src/commonTest/kotlin/dev/dimension/flare/common/deeplink/DeepLinkMappingTest.kt
@@ -29,7 +29,9 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/@{handle}"), profile.uriPattern)
assertEquals(
listOf("handle" to true),
- profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ profile.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
val post = patterns[1]
@@ -37,7 +39,9 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/@{handle}/{id}"), post.uriPattern)
assertEquals(
listOf("handle" to true, "id" to true),
- post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ post.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
}
@@ -54,7 +58,9 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/@{handle}"), profile.uriPattern)
assertEquals(
listOf("handle" to true),
- profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ profile.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
val post = patterns[1]
@@ -62,7 +68,9 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/notes/{id}"), post.uriPattern)
assertEquals(
listOf("notes" to false, "id" to true),
- post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ post.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
}
@@ -79,7 +87,9 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/profile/{handle}"), profile.uriPattern)
assertEquals(
listOf("profile" to false, "handle" to true),
- profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ profile.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
val post = patterns[1]
@@ -87,7 +97,9 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/profile/{handle}/post/{id}"), post.uriPattern)
assertEquals(
listOf("profile" to false, "handle" to true, "post" to false, "id" to true),
- post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ post.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
}
@@ -97,14 +109,16 @@ class DeepLinkMappingTest {
val patterns = DeepLinkMapping.generatePattern(PlatformType.xQt, host)
- assertEquals(8, patterns.size)
+ assertEquals(12, patterns.size)
val profile = patterns[0]
assertEquals(DeepLinkMapping.Type.Profile.serializer(), profile.serializer)
assertEquals(Url("https://$host/{handle}"), profile.uriPattern)
assertEquals(
listOf("handle" to true),
- profile.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ profile.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
val post = patterns[4]
@@ -112,7 +126,25 @@ class DeepLinkMappingTest {
assertEquals(Url("https://$host/{handle}/status/{id}"), post.uriPattern)
assertEquals(
listOf("handle" to true, "status" to false, "id" to true),
- post.pathSegments.filter { it.stringValue.isNotEmpty() }.map { it.stringValue to it.isParamArg },
+ post.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
+ )
+
+ val media = patterns[8]
+ assertEquals(DeepLinkMapping.Type.PostMedia.serializer(), media.serializer)
+ assertEquals(Url("https://$host/{handle}/status/{id}/photo/{index}"), media.uriPattern)
+ assertEquals(
+ listOf(
+ "handle" to true,
+ "status" to false,
+ "id" to true,
+ "photo" to false,
+ "index" to true,
+ ),
+ media.pathSegments
+ .filter { it.stringValue.isNotEmpty() }
+ .map { it.stringValue to it.isParamArg },
)
}
@@ -172,8 +204,18 @@ class DeepLinkMappingTest {
val mapping:
ImmutableMap>> =
persistentMapOf(
- account1 to DeepLinkMapping.generatePattern(PlatformType.Mastodon, account1.accountKey.host).toImmutableList(),
- account2 to DeepLinkMapping.generatePattern(PlatformType.Mastodon, account2.accountKey.host).toImmutableList(),
+ account1 to
+ DeepLinkMapping
+ .generatePattern(
+ PlatformType.Mastodon,
+ account1.accountKey.host,
+ ).toImmutableList(),
+ account2 to
+ DeepLinkMapping
+ .generatePattern(
+ PlatformType.Mastodon,
+ account2.accountKey.host,
+ ).toImmutableList(),
)
val matches = DeepLinkMapping.matches("https://mastodon.social/@alice", mapping)
@@ -193,7 +235,12 @@ class DeepLinkMappingTest {
val mapping:
ImmutableMap>> =
persistentMapOf(
- account to DeepLinkMapping.generatePattern(PlatformType.Mastodon, account.accountKey.host).toImmutableList(),
+ account to
+ DeepLinkMapping
+ .generatePattern(
+ PlatformType.Mastodon,
+ account.accountKey.host,
+ ).toImmutableList(),
)
// URL containing none of the valid hosts
@@ -239,33 +286,60 @@ class DeepLinkMappingTest {
PlatformType.Misskey,
misskeyAccount.accountKey.host,
).toImmutableList(),
- bskyAccount to DeepLinkMapping.generatePattern(PlatformType.Bluesky, bskyAccount.accountKey.host).toImmutableList(),
- xAccount to DeepLinkMapping.generatePattern(PlatformType.xQt, xAccount.accountKey.host).toImmutableList(),
+ bskyAccount to
+ DeepLinkMapping
+ .generatePattern(
+ PlatformType.Bluesky,
+ bskyAccount.accountKey.host,
+ ).toImmutableList(),
+ xAccount to
+ DeepLinkMapping
+ .generatePattern(
+ PlatformType.xQt,
+ xAccount.accountKey.host,
+ ).toImmutableList(),
)
// https://mastodon.example/@alice
- val mastodonProfileMatch = DeepLinkMapping.matches("https://mastodon.example/@alice", mapping)
+ val mastodonProfileMatch =
+ DeepLinkMapping.matches("https://mastodon.example/@alice", mapping)
assertEquals(DeepLinkMapping.Type.Profile("alice"), mastodonProfileMatch[mastodonAccount])
// https://mastodon.example/@alice/12345
- val mastodonPostMatch = DeepLinkMapping.matches("https://mastodon.example/@alice/12345", mapping)
- assertEquals(DeepLinkMapping.Type.Post("alice", "12345"), mastodonPostMatch[mastodonAccount])
+ val mastodonPostMatch =
+ DeepLinkMapping.matches("https://mastodon.example/@alice/12345", mapping)
+ assertEquals(
+ DeepLinkMapping.Type.Post("alice", "12345"),
+ mastodonPostMatch[mastodonAccount],
+ )
// https://misskey.example/@bob
val misskeyProfileMatch = DeepLinkMapping.matches("https://misskey.example/@bob", mapping)
assertEquals(DeepLinkMapping.Type.Profile("bob"), misskeyProfileMatch[misskeyAccount])
// https://misskey.example/notes/12345
- val misskeyPostMatch = DeepLinkMapping.matches("https://misskey.example/notes/12345", mapping)
+ val misskeyPostMatch =
+ DeepLinkMapping.matches("https://misskey.example/notes/12345", mapping)
assertEquals(DeepLinkMapping.Type.Post(null, "12345"), misskeyPostMatch[misskeyAccount])
// https://bsky.example/profile/alice.bsky.social
- val bskyProfileMatch = DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social", mapping)
- assertEquals(DeepLinkMapping.Type.Profile("alice.bsky.social"), bskyProfileMatch[bskyAccount])
+ val bskyProfileMatch =
+ DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social", mapping)
+ assertEquals(
+ DeepLinkMapping.Type.Profile("alice.bsky.social"),
+ bskyProfileMatch[bskyAccount],
+ )
// https://bsky.example/profile/alice.bsky.social/post/12345
- val bskyPostMatch = DeepLinkMapping.matches("https://bsky.example/profile/alice.bsky.social/post/12345", mapping)
- assertEquals(DeepLinkMapping.Type.Post("alice.bsky.social", "12345"), bskyPostMatch[bskyAccount])
+ val bskyPostMatch =
+ DeepLinkMapping.matches(
+ "https://bsky.example/profile/alice.bsky.social/post/12345",
+ mapping,
+ )
+ assertEquals(
+ DeepLinkMapping.Type.Post("alice.bsky.social", "12345"),
+ bskyPostMatch[bskyAccount],
+ )
// https://x.example/alice
val xProfileMatch = DeepLinkMapping.matches("https://$xqtHost/alice", mapping)
@@ -274,6 +348,11 @@ class DeepLinkMappingTest {
// https://x.example/alice/status/12345
val xPostMatch = DeepLinkMapping.matches("https://$xqtHost/alice/status/12345", mapping)
assertEquals(DeepLinkMapping.Type.Post("alice", "12345"), xPostMatch[xAccount])
+
+ // https://x.example/alice/status/12345/photo/1
+ val xPostPhotoMatch =
+ DeepLinkMapping.matches("https://$xqtHost/alice/status/12345/photo/1", mapping)
+ assertEquals(DeepLinkMapping.Type.PostMedia("alice", "12345", 1), xPostPhotoMatch[xAccount])
}
@Test