From 562b15696ecbf0e48fa5829a79b95ba0742a2d01 Mon Sep 17 00:00:00 2001 From: serrebidev Date: Tue, 20 Oct 2026 01:16:05 -0700 Subject: [PATCH] Android: offer Open links in a post's actions Settings lists "Open links" as a post action, but StatusRow had no case for the "links" key, so turning it on did nothing. Wire it to open_post_links, as iOS does, and handle the core's url_picker event with a chooser like the media picker. Co-Authored-By: Claude Opus 5.5 --- .../me/masonasons/fastsm/ui/CoreViewModel.kt | 27 +++++++++++++++++++ .../masonasons/fastsm/ui/home/HomeScreen.kt | 24 +++++++++++++++++ .../me/masonasons/fastsm/ui/home/StatusRow.kt | 6 ++++- docs/changelog.txt | 1 + 4 files changed, 57 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/java/me/masonasons/fastsm/ui/CoreViewModel.kt b/android/app/src/main/java/me/masonasons/fastsm/ui/CoreViewModel.kt index e32947b..5ce896b 100644 --- a/android/app/src/main/java/me/masonasons/fastsm/ui/CoreViewModel.kt +++ b/android/app/src/main/java/me/masonasons/fastsm/ui/CoreViewModel.kt @@ -331,6 +331,10 @@ class CoreViewModel(app: Application) : AndroidViewModel(app) { private val _mediaPicker = MutableStateFlow?>(null) val mediaPicker: StateFlow?> = _mediaPicker.asStateFlow() + // A post's links to choose from (url_picker), when "Open links" finds several. + private val _linkPicker = MutableStateFlow?>(null) + val linkPicker: StateFlow?> = _linkPicker.asStateFlow() + // Push notifications: whether the user has them on. PushManager owns the // persisted value and the device-side plumbing; this mirrors it for the UI. private val _pushEnabled = MutableStateFlow(false) @@ -662,6 +666,16 @@ class CoreViewModel(app: Application) : AndroidViewModel(app) { "media_open" -> _media.value = MediaItemUi(e.optString("url"), e.optString("kind"), e.optString("title")) + "url_picker" -> { + val arr = e.optJSONArray("links") + _linkPicker.value = buildList { + if (arr != null) for (i in 0 until arr.length()) { + val l = arr.getJSONObject(i) + add(LinkUi(l.optString("title"), l.optString("url"))) + } + } + } + "media_picker" -> { val arr = e.optJSONArray("items") _mediaPicker.value = buildList { @@ -1338,6 +1352,19 @@ class CoreViewModel(app: Application) : AndroidViewModel(app) { fun checkForUpdate() = core.dispatch("check_for_update") { put("silent", false); put("branch", "stable") } + /** Open a post's links: one opens directly, several raise a chooser. */ + fun openPostLinks(id: String) = core.dispatch("open_post_links") { put("id", id) } + + /** Open the link chosen from a post's several. */ + fun openPickedLink(link: LinkUi) { + _linkPicker.value = null + openLink(link.url) + } + + fun dismissLinkPicker() { + _linkPicker.value = null + } + /** Open a specific URL (a per-post "expand links" action) in a Custom Tab. */ fun openLink(url: String) { if (url.isNotEmpty()) _openUrls.tryEmit(url) diff --git a/android/app/src/main/java/me/masonasons/fastsm/ui/home/HomeScreen.kt b/android/app/src/main/java/me/masonasons/fastsm/ui/home/HomeScreen.kt index 1d6dae6..145fb05 100644 --- a/android/app/src/main/java/me/masonasons/fastsm/ui/home/HomeScreen.kt +++ b/android/app/src/main/java/me/masonasons/fastsm/ui/home/HomeScreen.kt @@ -337,6 +337,7 @@ fun HomeScreen( scrollRequest = scrollRequest?.takeIf { it.tab == pageIndex }, actionOrder = postActionOrder, onOpenLink = viewModel::openLink, + onOpenLinks = viewModel::openPostLinks, onLoadOlder = { viewModel.loadOlder(automatic = true) }, onNoteSelection = viewModel::noteSelection, onOpenThread = viewModel::openThread, @@ -605,6 +606,27 @@ fun HomeScreen( ) } + val linkPicker by viewModel.linkPicker.collectAsStateWithLifecycle() + linkPicker?.let { links -> + AlertDialog( + onDismissRequest = viewModel::dismissLinkPicker, + title = { Text("Which link?") }, + text = { + Column { + links.forEach { link -> + TextButton(onClick = { viewModel.openPickedLink(link) }) { + Text(link.title.ifBlank { link.url }) + } + } + } + }, + confirmButton = {}, + dismissButton = { + TextButton(onClick = viewModel::dismissLinkPicker) { Text("Cancel") } + }, + ) + } + val hashtagPicker by viewModel.hashtagTimelinePicker.collectAsStateWithLifecycle() hashtagPicker?.let { tags -> AlertDialog( @@ -765,6 +787,7 @@ private fun StatusList( scrollRequest: CoreViewModel.ScrollRequest?, actionOrder: List, onOpenLink: (String) -> Unit, + onOpenLinks: (String) -> Unit, onLoadOlder: () -> Unit, onNoteSelection: (String) -> Unit, onOpenThread: (String) -> Unit, @@ -875,6 +898,7 @@ private fun StatusList( actionOrder = actionOrder, onSelect = onNoteSelection, onOpenLink = onOpenLink, + onOpenLinks = onOpenLinks, onOpenThread = onOpenThread, onOpenAuthor = onOpenAuthor, onOpenProfile = onOpenProfile, diff --git a/android/app/src/main/java/me/masonasons/fastsm/ui/home/StatusRow.kt b/android/app/src/main/java/me/masonasons/fastsm/ui/home/StatusRow.kt index 5eecb53..bd4da23 100644 --- a/android/app/src/main/java/me/masonasons/fastsm/ui/home/StatusRow.kt +++ b/android/app/src/main/java/me/masonasons/fastsm/ui/home/StatusRow.kt @@ -34,7 +34,7 @@ import me.masonasons.fastsm.ui.RowUi */ // The Android-supported post actions, in default order (View Media first). // A subset of the shared post_action_catalog(): the actions StatusRow has a -// handler for. Keys not here (post_info, links, browser, followers, following, +// handler for. Keys not here (post_info, browser, followers, following, // follow_hashtag, pin_post) aren't wired on Android yet. private val DEFAULT_POST_ACTION_ORDER = listOf( "play_media", "reply", "boost", "favorite", "bookmark", "quote", "thread", @@ -51,6 +51,7 @@ fun StatusRow( /** Acting on a row makes it the reading position (the core persists it). */ onSelect: (String) -> Unit, onOpenLink: (String) -> Unit, + onOpenLinks: (String) -> Unit, onOpenThread: (String) -> Unit, onOpenAuthor: (String) -> Unit, onOpenProfile: (String) -> Unit, @@ -101,6 +102,9 @@ fun StatusRow( "bookmark" -> MenuAction(if (row.bookmarked) "Remove bookmark" else "Bookmark") { onToggleBookmark(row.id) } "quote" -> MenuAction("Quote") { onQuote(row.id) } + // Offered on every post, as on iPhone: the core says when a post + // has no links, opens a single one, and asks which of several. + "links" -> MenuAction("Open links") { onOpenLinks(row.id) } "play_media" -> if (row.hasMedia) MenuAction("View media") { onViewMedia(row.id) } else null "thread" -> MenuAction("View conversation") { onOpenThread(row.id) } "mute_conversation" -> diff --git a/docs/changelog.txt b/docs/changelog.txt index 819bfd8..1fa187e 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -4,6 +4,7 @@ FastSMRW changelog 0.6.1 ----- +- Fixed: on Android, the Open links action now appears in a post's actions when it is turned on in Settings; it opens the post's link, or asks which one when there are several. - Fixed: on Android, FastSMRW now installs on 32-bit phones, which previously refused it with "app not compatible" and no way around it. - Changed: @-mention autocomplete now suggests people you've seen go past in your timelines, instantly and without waiting on the server, and matches on display names as well as handles. - Fixed: completing a mention with the cursor in the middle of a half-typed handle no longer leaves the end of it behind, and no longer adds a second space when one is already there.