Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
defaultConfig {
applicationId = "pub.hackers.android"
minSdk = 26
targetSdk = 36

Check warning on line 31 in app/build.gradle.kts

View workflow job for this annotation

GitHub Actions / lint

Not targeting the latest versions of Android; compatibility modes apply. Consider testing and updating this version. Consult the `android.os.Build.VERSION_CODES` javadoc for details. [OldTargetApi]
versionCode = 10
versionName = "1.5.0"

Expand All @@ -38,7 +38,7 @@
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")
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/pub/hackers/android/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -85,6 +87,7 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
handleDeepLink(intent)
handleShareIntent(intent)
handleNavigationIntent(intent)
}
requestNotificationPermissionIfNeeded()
Expand Down Expand Up @@ -117,6 +120,7 @@ class MainActivity : ComponentActivity() {
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleDeepLink(intent)
handleShareIntent(intent)
handleNavigationIntent(intent)
}

Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
16 changes: 12 additions & 4 deletions app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@
return if (params.isEmpty()) "signin" else "signin?${params.joinToString("&")}"
}
}
data object Compose : DetailScreen("compose?replyTo={replyTo}&quoteOf={quoteOf}") {
fun createRoute(replyTo: String? = null, quoteOf: String? = null): String {
data object Compose : DetailScreen("compose?replyTo={replyTo}&quoteOf={quoteOf}&prefill={prefill}") {
fun createRoute(replyTo: String? = null, quoteOf: String? = null, prefill: String? = null): String {
val params = mutableListOf<String>()
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("&")}"
}
}
Expand Down Expand Up @@ -160,7 +161,7 @@
navigationIntent: pub.hackers.android.NavigationIntent? = null,
onDeepLinkConsumed: () -> Unit = {},
onNavigationIntentConsumed: () -> Unit = {},
viewModel: AppViewModel = hiltViewModel()

Check warning on line 164 in app/src/main/java/pub/hackers/android/ui/HackersPubApp.kt

View workflow job for this annotation

GitHub Actions / lint

'fun <reified VM : ViewModel> hiltViewModel(viewModelStoreOwner: ViewModelStoreOwner = ..., key: String? = ...): VM' is deprecated. Moved to package: androidx.hilt.lifecycle.viewmodel.compose.
) {
val navController = rememberNavController()
val isLoggedInState by viewModel.isLoggedIn.collectAsState(initial = null as Boolean?)
Expand Down Expand Up @@ -487,14 +488,21 @@
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,10 @@
fun ComposeScreen(
replyToId: String?,
quotedPostId: String? = null,
initialContent: String? = null,
onPostSuccess: () -> Unit,
onNavigateBack: () -> Unit,
viewModel: ComposeViewModel = hiltViewModel()

Check warning on line 146 in app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt

View workflow job for this annotation

GitHub Actions / lint

'fun <reified VM : ViewModel> hiltViewModel(viewModelStoreOwner: ViewModelStoreOwner = ..., key: String? = ...): VM' is deprecated. Moved to package: androidx.hilt.lifecycle.viewmodel.compose.
) {
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
Expand Down Expand Up @@ -235,6 +236,10 @@
quotedPostId?.let { viewModel.setQuotedPost(it) }
}

LaunchedEffect(initialContent) {
initialContent?.let { viewModel.setInitialContent(it) }
}

LaunchedEffect(uiState.isPosted) {
if (uiState.isPosted) {
onPostSuccess()
Expand Down Expand Up @@ -969,7 +974,7 @@
}

@Composable
private fun visibilityMenuItem(

Check warning on line 977 in app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt

View workflow job for this annotation

GitHub Actions / lint

Composable functions that return Unit should start with an uppercase letter [ComposableNaming]
visibility: PostVisibility,
selectedVisibility: PostVisibility,
onClick: () -> Unit,
Expand Down Expand Up @@ -1018,7 +1023,7 @@
}

@Composable
internal fun quotePolicyMenuItem(

Check warning on line 1026 in app/src/main/java/pub/hackers/android/ui/screens/compose/ComposeScreen.kt

View workflow job for this annotation

GitHub Actions / lint

Composable functions that return Unit should start with an uppercase letter [ComposableNaming]
policy: QuotePolicy,
selectedPolicy: QuotePolicy,
enabled: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) }

Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading