diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 390e79f0..94ea6896 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -38,7 +38,7 @@ android {
buildTypes {
debug {
applicationIdSuffix = ".dev"
- resValue("string", "app_name", "Hackers\\' Pub Dev")
+ resValue("string", "app_name", "Hackers Pub Dev")
}
release {
resValue("string", "app_name", "Hackers\\' Pub")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 88a8c173..dd48bcd6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -27,6 +27,11 @@
+
+
+
+
+
diff --git a/app/src/main/java/pub/hackers/android/MainActivity.kt b/app/src/main/java/pub/hackers/android/MainActivity.kt
index d69dff2f..8a0502ff 100644
--- a/app/src/main/java/pub/hackers/android/MainActivity.kt
+++ b/app/src/main/java/pub/hackers/android/MainActivity.kt
@@ -32,7 +32,9 @@ import pub.hackers.android.data.local.PreferencesManager
import pub.hackers.android.data.local.SessionManager
import pub.hackers.android.navigation.HackersPubRoute
import pub.hackers.android.navigation.HackersPubUrlRouter
+import pub.hackers.android.navigation.ShareTargetText
import pub.hackers.android.navigation.toNavRoute
+import pub.hackers.android.ui.DetailScreen
import pub.hackers.android.ui.HackersPubApp
import pub.hackers.android.ui.theme.HackersPubTheme
import pub.hackers.android.ui.theme.LocalAppColors
@@ -85,6 +87,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
handleDeepLink(intent)
+ handleShareIntent(intent)
handleNavigationIntent(intent)
}
requestNotificationPermissionIfNeeded()
@@ -117,6 +120,7 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleDeepLink(intent)
+ handleShareIntent(intent)
handleNavigationIntent(intent)
}
@@ -168,6 +172,20 @@ class MainActivity : ComponentActivity() {
navigationIntent = NavigationIntent(route = route)
}
+ private fun handleShareIntent(intent: Intent?) {
+ if (intent?.action != Intent.ACTION_SEND) return
+ if (intent.type != "text/plain") return
+
+ val sharedText = ShareTargetText.format(
+ subject = intent.getCharSequenceExtra(Intent.EXTRA_SUBJECT),
+ text = intent.getCharSequenceExtra(Intent.EXTRA_TEXT),
+ ) ?: return
+
+ navigationIntent = NavigationIntent(
+ route = DetailScreen.Compose.createRoute(prefill = sharedText)
+ )
+ }
+
private fun handleDeepLink(intent: Intent?) {
val data = intent?.data ?: return
diff --git a/app/src/main/java/pub/hackers/android/navigation/ShareTargetText.kt b/app/src/main/java/pub/hackers/android/navigation/ShareTargetText.kt
new file mode 100644
index 00000000..9a13646e
--- /dev/null
+++ b/app/src/main/java/pub/hackers/android/navigation/ShareTargetText.kt
@@ -0,0 +1,14 @@
+package pub.hackers.android.navigation
+
+internal object ShareTargetText {
+ fun format(subject: CharSequence?, text: CharSequence?): String? {
+ val sharedText = text?.toString()?.takeIf { it.isNotBlank() } ?: return null
+ val sharedSubject = subject?.toString()?.takeIf { it.isNotBlank() }
+
+ return if (sharedSubject != null) {
+ "$sharedSubject\n\n$sharedText"
+ } else {
+ sharedText
+ }
+ }
+}
diff --git a/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt b/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt
index 0def6b38..f6eef523 100644
--- a/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt
+++ b/app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt
@@ -116,11 +116,12 @@ sealed class DetailScreen(val route: String) {
return if (params.isEmpty()) "signin" else "signin?${params.joinToString("&")}"
}
}
- data object Compose : DetailScreen("compose?replyTo={replyTo}"eOf={quoteOf}") {
- fun createRoute(replyTo: String? = null, quoteOf: String? = null): String {
+ data object Compose : DetailScreen("compose?replyTo={replyTo}"eOf={quoteOf}&prefill={prefill}") {
+ fun createRoute(replyTo: String? = null, quoteOf: String? = null, prefill: String? = null): String {
val params = mutableListOf()
- if (replyTo != null) params.add("replyTo=$replyTo")
- if (quoteOf != null) params.add("quoteOf=$quoteOf")
+ if (replyTo != null) params.add("replyTo=${android.net.Uri.encode(replyTo)}")
+ if (quoteOf != null) params.add("quoteOf=${android.net.Uri.encode(quoteOf)}")
+ if (prefill != null) params.add("prefill=${android.net.Uri.encode(prefill)}")
return if (params.isEmpty()) "compose" else "compose?${params.joinToString("&")}"
}
}
@@ -487,14 +488,21 @@ fun HackersPubApp(
type = NavType.StringType
nullable = true
defaultValue = null
+ },
+ navArgument("prefill") {
+ type = NavType.StringType
+ nullable = true
+ defaultValue = null
}
)
) { backStackEntry ->
val replyTo = backStackEntry.arguments?.getString("replyTo")
val quoteOf = backStackEntry.arguments?.getString("quoteOf")
+ val prefill = backStackEntry.arguments?.getString("prefill")
ComposeScreen(
replyToId = replyTo,
quotedPostId = quoteOf,
+ initialContent = prefill,
onPostSuccess = {
viewModel.timelineRefreshTrigger.requestRefresh()
navController.popBackStack()
diff --git a/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt b/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt
index 5f182f5f..c0f3f697 100644
--- a/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt
+++ b/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt
@@ -140,6 +140,7 @@ private fun calculateMentionPopupOffset(
fun ComposeScreen(
replyToId: String?,
quotedPostId: String? = null,
+ initialContent: String? = null,
onPostSuccess: () -> Unit,
onNavigateBack: () -> Unit,
viewModel: ComposeViewModel = hiltViewModel()
@@ -235,6 +236,10 @@ fun ComposeScreen(
quotedPostId?.let { viewModel.setQuotedPost(it) }
}
+ LaunchedEffect(initialContent) {
+ initialContent?.let { viewModel.setInitialContent(it) }
+ }
+
LaunchedEffect(uiState.isPosted) {
if (uiState.isPosted) {
onPostSuccess()
diff --git a/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeViewModel.kt b/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeViewModel.kt
index f1544ad8..0cd8f039 100644
--- a/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeViewModel.kt
+++ b/app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeViewModel.kt
@@ -299,6 +299,22 @@ class ComposeViewModel @Inject constructor(
}
}
+ fun setInitialContent(content: String) {
+ if (content.isBlank()) return
+
+ _uiState.update {
+ if (it.content.isBlank()) {
+ it.copy(
+ content = content,
+ cursorPosition = content.length,
+ )
+ } else {
+ it
+ }
+ }
+ detectLanguage(content)
+ }
+
fun updateContent(content: String, cursorPosition: Int = content.length) {
_uiState.update { it.copy(content = content, cursorPosition = cursorPosition) }
diff --git a/app/src/test/java/pub/hackers/android/navigation/ShareTargetTextTest.kt b/app/src/test/java/pub/hackers/android/navigation/ShareTargetTextTest.kt
new file mode 100644
index 00000000..50dc5ba8
--- /dev/null
+++ b/app/src/test/java/pub/hackers/android/navigation/ShareTargetTextTest.kt
@@ -0,0 +1,37 @@
+package pub.hackers.android.navigation
+
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
+import org.junit.Test
+
+class ShareTargetTextTest {
+ @Test
+ fun `formats subject and text separated by blank line`() {
+ val result = ShareTargetText.format(
+ subject = "Article title",
+ text = "https://example.com/article",
+ )
+
+ assertEquals("Article title\n\nhttps://example.com/article", result)
+ }
+
+ @Test
+ fun `uses text as-is when subject is missing`() {
+ val result = ShareTargetText.format(
+ subject = null,
+ text = " https://example.com/article\n",
+ )
+
+ assertEquals(" https://example.com/article\n", result)
+ }
+
+ @Test
+ fun `ignores subject without shared text`() {
+ val result = ShareTargetText.format(
+ subject = "Article title",
+ text = " ",
+ )
+
+ assertNull(result)
+ }
+}
diff --git a/app/src/test/java/pub/hackers/android/ui/screens/compose/ComposeViewModelTest.kt b/app/src/test/java/pub/hackers/android/ui/screens/compose/ComposeViewModelTest.kt
index 38a4d330..9a8b61c8 100644
--- a/app/src/test/java/pub/hackers/android/ui/screens/compose/ComposeViewModelTest.kt
+++ b/app/src/test/java/pub/hackers/android/ui/screens/compose/ComposeViewModelTest.kt
@@ -123,6 +123,28 @@ class ComposeViewModelTest {
assertEquals("user typed this", vm.uiState.value.content)
}
+ @Test
+ fun `setInitialContent preloads blank composer`() = runTest {
+ val vm = newViewModel()
+ advanceUntilIdle()
+
+ vm.setInitialContent("Shared title\n\nhttps://example.com")
+
+ assertEquals("Shared title\n\nhttps://example.com", vm.uiState.value.content)
+ assertEquals("Shared title\n\nhttps://example.com".length, vm.uiState.value.cursorPosition)
+ }
+
+ @Test
+ fun `setInitialContent does not overwrite typed content`() = runTest {
+ val vm = newViewModel()
+ advanceUntilIdle()
+
+ vm.updateContent("user typed this")
+ vm.setInitialContent("shared content")
+
+ assertEquals("user typed this", vm.uiState.value.content)
+ }
+
@Test
fun `post sends selected quote policy for public notes`() = runTest {
val createdPost = samplePost(id = "created")