diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt deleted file mode 100644 index 465afd5ee8..0000000000 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/actions/uploads/SharedMediaResolver.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2025 Vitor Pamplona - * - * Permission is hereby granted, free of charge, to any person obtaining a copy of - * this software and associated documentation files (the "Software"), to deal in - * the Software without restriction, including without limitation the rights to use, - * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the - * Software, and to permit persons to whom the Software is furnished to do so, - * subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS - * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR - * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN - * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION - * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - */ -package com.vitorpamplona.amethyst.ui.actions.uploads - -import android.content.Context -import androidx.core.net.toUri -import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia -import kotlinx.collections.immutable.ImmutableList -import kotlinx.collections.immutable.persistentListOf -import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -/** - * Resolves a shared content-URI string (from an Android SEND intent) into a - * [SelectedMedia] by reading its MIME type off the main thread. Returns null for - * a null/blank URI. Shared by the share-to-compose pre-fill paths (DM chatroom, - * group DM, …) so the URI→MIME→SelectedMedia logic lives in one place. - */ -suspend fun resolveSharedMedia( - context: Context, - uriString: String?, -): SelectedMedia? = - uriString?.ifBlank { null }?.toUri()?.let { uri -> - withContext(Dispatchers.IO) { - SelectedMedia(uri, context.contentResolver.getType(uri)) - } - } - -/** - * Batch form for SEND_MULTIPLE shares: resolves every URI in one trip to the IO dispatcher - * instead of one context switch per file. Blank entries are dropped, so an empty list in means - * an empty list out and the composer stays closed. - */ -suspend fun resolveSharedMedia( - context: Context, - uriStrings: List, -): ImmutableList { - val uris = uriStrings.mapNotNull { it.ifBlank { null }?.toUri() } - if (uris.isEmpty()) return persistentListOf() - - return withContext(Dispatchers.IO) { - uris.map { SelectedMedia(it, context.contentResolver.getType(it)) }.toImmutableList() - } -} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt index 03f99c89ff..d440dc8c46 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/nip22Comments/GenericCommentPostScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.note.nip22Comments -import android.content.Intent -import android.net.Uri import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box @@ -46,20 +44,14 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp -import androidx.core.content.IntentCompat -import androidx.core.net.toUri -import androidx.core.util.Consumer import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols @@ -79,10 +71,12 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -114,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.SuggestionListDefaultHeightPage import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -132,9 +125,6 @@ import com.vitorpamplona.quartz.nip73ExternalIds.location.GeohashId import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Composable fun ReplyCommentPostScreen( @@ -149,7 +139,7 @@ fun ReplyCommentPostScreen( val postViewModel: CommentPostViewModel = viewModel() postViewModel.init(accountViewModel) - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(postViewModel, accountViewModel) { replyId?.let { LocalCache.getNoteIfExists(it) }?.let { @@ -165,11 +155,8 @@ fun ReplyCommentPostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null) { postViewModel.applySignature() @@ -188,32 +175,11 @@ fun GenericCommentPostScreen( ) { WatchAndLoadMyEmojiList(accountViewModel) - val context = LocalContext.current - val activity = context.getActivity() - val scope = rememberCoroutineScope() - - DisposableEffect(nav, activity) { - // Microsoft's swift key sends Gifs as new actions - val consumer = - Consumer { intent -> - if (intent.action == Intent.ACTION_SEND) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let { - postViewModel.addToMessage(it) - } - - // Use the `intent` parameter (the new intent), not activity.intent (the launch intent). - IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> - scope.launch(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(uri) - postViewModel.selectImage(persistentListOf(SelectedMedia(uri, mediaType))) - } - } - } - } - - activity.addOnNewIntentListener(consumer) - onDispose { activity.removeOnNewIntentListener(consumer) } - } + // Microsoft's swift key sends Gifs as new actions + OnIncomingShare( + onText = postViewModel::addToMessage, + onMedia = { postViewModel.selectImage(persistentListOf(it)) }, + ) // NIP-9B: when replying into a NIP-72 community, mount the community feed // subscription so the latest kind:34551 rules document is fetched and @@ -466,7 +432,6 @@ private fun GenericCommentPostBody( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp), ) { - val context = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt index cd03b8f84b..1c7dd54571 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/buzz/BuzzWorkspaceOverflowMenu.kt @@ -157,7 +157,7 @@ fun BuzzWorkspaceOverflowMenu( ) } catch (e: Exception) { if (e is CancellationException) throw e - error = e.message ?: e::class.simpleName + error = e.message ?: e.javaClass.simpleName } finally { minting = false } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt index c8023478bb..f9f8b86e19 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/ChatroomView.kt @@ -34,7 +34,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -47,6 +46,7 @@ import com.vitorpamplona.amethyst.commons.relayClient.reqCommand.event.EventFind import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip04 import com.vitorpamplona.amethyst.commons.resources.chats_history_proto_nip17 +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.feeds.DmHistoryLoadingCard import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachCursor import com.vitorpamplona.amethyst.commons.ui.feeds.RelayReachDetailDialog @@ -60,7 +60,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.privateDM.hea import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import com.vitorpamplona.amethyst.ui.note.elements.ObserveRelayListForDMsAndDisplayIfNotFound import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.RefreshingChatroomFeedView import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.dal.ChatroomFeedViewModel @@ -144,10 +143,10 @@ fun ChatroomView( newPostModel.onMessageChanged() } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() if (attachmentUri != null) { LaunchedEffect(key1 = attachmentUri) { - resolveSharedMedia(context, attachmentUri)?.let { + mediaResolver.resolve(attachmentUri)?.let { newPostModel.pickedMedia(persistentListOf(it)) } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt index 27d164978e..2f7bf5b5f5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/chats/privateDM/send/ChatNewMessageViewModel.kt @@ -823,7 +823,7 @@ class ChatNewMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } // NIP-04 sending is deprecated. NIP-17 is always used. diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt index f121e543ec..623388bce5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip23LongForm/LongFormPostViewModel.kt @@ -718,7 +718,7 @@ class LongFormPostViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt index c0bd0a7c17..a923c1b1c3 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/discover/nip99Classifieds/NewProductViewModel.kt @@ -616,7 +616,7 @@ open class NewProductViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt index 1fd8054efb..1ce2877986 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostScreen.kt @@ -20,8 +20,6 @@ */ package com.vitorpamplona.amethyst.ui.screen.loggedIn.home -import android.content.Intent -import android.net.Uri import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.clickable @@ -55,13 +53,11 @@ import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier @@ -72,9 +68,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.IntentCompat -import androidx.core.net.toUri -import androidx.core.util.Consumer import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -110,10 +103,12 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromFiles import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav @@ -156,7 +151,6 @@ import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceMessagePreview import com.vitorpamplona.amethyst.ui.components.ThinPaddingTextField -import com.vitorpamplona.amethyst.ui.components.getActivity import com.vitorpamplona.amethyst.ui.note.creators.emojiSuggestions.WatchAndLoadMyEmojiList import com.vitorpamplona.amethyst.ui.note.creators.expiration.ExpirationDatePicker import com.vitorpamplona.amethyst.ui.note.creators.invoice.InvoiceRequest @@ -177,10 +171,7 @@ import com.vitorpamplona.quartz.nip01Core.core.HexKey import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableSet -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @Composable @@ -202,8 +193,7 @@ fun ShortNotePostScreen( postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl) val context = LocalContext.current - val activity = context.getActivity() - val scope = rememberCoroutineScope() + val mediaResolver = rememberSharedMediaResolver() val proposeAiImprovements by accountViewModel.settings.uiSettingsFlow.automaticallyProposeAiImprovements @@ -227,39 +217,19 @@ fun ShortNotePostScreen( postViewModel.message.setTextAndPlaceCursorAtEnd(it) postViewModel.onMessageChanged() } - attachment?.ifBlank { null }?.toUri()?.let { - withContext(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(it) - postViewModel.selectImage(persistentListOf(SelectedMedia(it, mediaType))) - } + mediaResolver.resolve(attachment)?.let { + postViewModel.selectImage(persistentListOf(it)) } if (draftId == null && forkId == null) { postViewModel.applySignature() } } - DisposableEffect(nav, activity) { - // Microsoft's swift key sends Gifs as new actions - val consumer = - Consumer { intent -> - if (intent.action == Intent.ACTION_SEND) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let { - postViewModel.addToMessage(it) - } - - // Use the `intent` parameter (the new intent), not activity.intent (the launch intent). - IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> - scope.launch(Dispatchers.IO) { - val mediaType = context.contentResolver.getType(uri) - postViewModel.selectImage(persistentListOf(SelectedMedia(uri, mediaType))) - } - } - } - } - - activity.addOnNewIntentListener(consumer) - onDispose { activity.removeOnNewIntentListener(consumer) } - } + // Microsoft's swift key sends Gifs as new actions + OnIncomingShare( + onText = postViewModel::addToMessage, + onMedia = { postViewModel.selectImage(persistentListOf(it)) }, + ) NewPostScreenInner(postViewModel, accountViewModel, nav) } @@ -610,7 +580,6 @@ private fun NewPostScreenBody( verticalAlignment = CenterVertically, modifier = Modifier.padding(vertical = Size10dp, horizontal = Size10dp), ) { - val context = LocalContext.current ImageVideoDescription( it, accountViewModel.account.settings.defaultFileServer, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt index d8f4703e56..fafc19c91c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/ShortNotePostViewModel.kt @@ -1995,7 +1995,7 @@ open class ShortNotePostViewModel : } catch (e: Exception) { onError( uploadErrorTitle, - loadStringRes(Res.string.upload_error_voice_message_exception, e.message ?: e.javaClass.simpleName), + loadStringRes(Res.string.upload_error_voice_message_exception, e.message ?: e::class.simpleName.orEmpty()), ) voiceRecording = null } finally { @@ -2015,7 +2015,7 @@ open class ShortNotePostViewModel : override fun onCleared() { writingAssistant?.close() writingAssistant = null - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt index 850b450647..1f1e65aca0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/home/VoiceReplyViewModel.kt @@ -326,6 +326,6 @@ class VoiceReplyViewModel : ViewModel() { override fun onCleared() { cancel() - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt index 670e8b49ef..80e8fa567d 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/chat/NestNewMessageViewModel.kt @@ -604,7 +604,7 @@ open class NestNewMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt index 1a371c87b1..a83271bea9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/nests/room/participants/ParticipantHostActionsSheet.kt @@ -555,7 +555,7 @@ private fun broadcastWithDiagnostics( } catch (ce: CancellationException) { throw ce } catch (e: Throwable) { - val klass = e::class.simpleName ?: "Throwable" + val klass = e.javaClass.simpleName ?: "Throwable" val msg = e.message?.takeIf { it.isNotBlank() } ?: "no message" accountViewModel.toastManager.toast(failedTitle, "$klass: $msg") } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt index ccdf7f60ca..a7611652b2 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/notifications/publicMessages/NewPublicMessageViewModel.kt @@ -670,7 +670,7 @@ class NewPublicMessageViewModel : } override fun onCleared() { - Log.d("Init") { "OnCleared: ${this.javaClass.simpleName}" } + Log.d("Init") { "OnCleared: ${this::class.simpleName}" } } override fun updateZapPercentage( diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt index aa81f029da..c122b7de76 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/pictures/NewPictureButton.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -54,6 +53,7 @@ import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -62,7 +62,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -102,9 +101,9 @@ fun NewPictureButton( } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(sharedAttachments) { - resolveSharedMedia(context, sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } + mediaResolver.resolve(sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } } if (wantsToPostFromCamera) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt index d87da1be98..6dbe33d697 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/shorts/NewShortVideoButton.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -55,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.resources.upload_image import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -63,7 +63,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -103,9 +102,9 @@ fun NewShortVideoButton( } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(sharedAttachments) { - resolveSharedMedia(context, sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } + mediaResolver.resolve(sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } } if (wantsToRecordVideo) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt index 81868ac4d1..325cfcfbf0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/NewVideoFeedButton.kt @@ -41,7 +41,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.icons.symbols.Icon @@ -56,6 +55,7 @@ import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelect import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePicture import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideo +import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.painterRes import com.vitorpamplona.amethyst.commons.ui.stringRes @@ -64,7 +64,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.ui.actions.NewMediaModel import com.vitorpamplona.amethyst.ui.actions.NewMediaView -import com.vitorpamplona.amethyst.ui.actions.uploads.resolveSharedMedia import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.coroutines.Dispatchers @@ -110,9 +109,9 @@ fun NewVideoFeedButton( } } - val context = LocalContext.current + val mediaResolver = rememberSharedMediaResolver() LaunchedEffect(sharedAttachments) { - resolveSharedMedia(context, sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } + mediaResolver.resolve(sharedAttachments).takeIf { it.isNotEmpty() }?.let { pickedURIs = it } } if (wantsToPostFromCamera) { diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt index 7a42bf22e3..c8245ef129 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/video/hls/HlsPublishOrchestrator.kt @@ -279,7 +279,7 @@ class HlsPublishOrchestrator( // scope; NewHlsVideoViewModel.cancel() calls reset() to put state back to Idle. throw e } catch (e: Throwable) { - _state.value = HlsPublishState.Failure(message = e.message ?: e::class.simpleName.orEmpty()) + _state.value = HlsPublishState.Failure(message = e.message ?: e.javaClass.simpleName.orEmpty()) } } diff --git a/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt new file mode 100644 index 0000000000..371121e23e --- /dev/null +++ b/commonsUI/src/androidMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.android.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.actions.uploads + +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.net.Uri +import androidx.activity.ComponentActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.IntentCompat +import androidx.core.net.toUri +import androidx.core.util.Consumer +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private class ContentResolverSharedMedia( + private val context: Context, +) : SharedMediaResolver { + override suspend fun resolve(uriString: String?): SelectedMedia? = + uriString?.ifBlank { null }?.toUri()?.let { uri -> + withContext(Dispatchers.IO) { + SelectedMedia(uri, context.contentResolver.getType(uri)) + } + } + + // One trip to the IO dispatcher for the whole batch instead of one context switch per file. + override suspend fun resolve(uriStrings: List): ImmutableList { + val uris = uriStrings.mapNotNull { it.ifBlank { null }?.toUri() } + if (uris.isEmpty()) return persistentListOf() + + return withContext(Dispatchers.IO) { + uris.map { SelectedMedia(it, context.contentResolver.getType(it)) }.toImmutableList() + } + } +} + +@Composable +actual fun rememberSharedMediaResolver(): SharedMediaResolver { + val context = LocalContext.current.applicationContext + return remember(context) { ContentResolverSharedMedia(context) } +} + +private tailrec fun Context.findComponentActivity(): ComponentActivity? = + when (this) { + is ComponentActivity -> this + is ContextWrapper -> baseContext.findComponentActivity() + else -> null + } + +@Composable +actual fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) { + val context = LocalContext.current + val activity = context.findComponentActivity() ?: return + val scope = rememberCoroutineScope() + val currentOnText by rememberUpdatedState(onText) + val currentOnMedia by rememberUpdatedState(onMedia) + + DisposableEffect(activity) { + val consumer = + Consumer { intent -> + if (intent.action == Intent.ACTION_SEND) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.ifBlank { null }?.let { + currentOnText(it) + } + + // Use the `intent` parameter (the new intent), not activity.intent (the launch intent). + IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)?.let { uri -> + scope.launch(Dispatchers.IO) { + val mediaType = context.contentResolver.getType(uri) + currentOnMedia(SelectedMedia(uri, mediaType)) + } + } + } + } + + activity.addOnNewIntentListener(consumer) + onDispose { activity.removeOnNewIntentListener(consumer) } + } +} diff --git a/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt new file mode 100644 index 0000000000..8949989d98 --- /dev/null +++ b/commonsUI/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import kotlinx.collections.immutable.ImmutableList + +/** + * Turns the file addresses another app shared with this one (an Android SEND intent's + * `content://` URIs) into [SelectedMedia], reading each MIME type off the main thread. + */ +interface SharedMediaResolver { + /** Null for a null or blank address. */ + suspend fun resolve(uriString: String?): SelectedMedia? + + /** Batch form for multi-file shares; blank entries are dropped. */ + suspend fun resolve(uriStrings: List): ImmutableList +} + +@Composable +expect fun rememberSharedMediaResolver(): SharedMediaResolver + +/** + * Calls [onText] and [onMedia] for content shared into the app while this screen is showing (a + * keyboard sending a GIF as a new share, say). Android listens for new SEND intents; the other + * platforms receive none yet. + */ +@Composable +expect fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) diff --git a/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt new file mode 100644 index 0000000000..2f541f5d43 --- /dev/null +++ b/commonsUI/src/iosMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.ios.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.StringMediaUri +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +// Addresses are plain paths or URLs here; nothing reports a MIME type for them yet. +private object StringSharedMedia : SharedMediaResolver { + override suspend fun resolve(uriString: String?): SelectedMedia? = uriString?.ifBlank { null }?.let { SelectedMedia(StringMediaUri(it), null) } + + override suspend fun resolve(uriStrings: List): ImmutableList = uriStrings.mapNotNull { resolve(it) }.toImmutableList() +} + +@Composable +actual fun rememberSharedMediaResolver(): SharedMediaResolver = StringSharedMedia + +/** No app shares into this one on this platform yet. */ +@Composable +actual fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) = Unit diff --git a/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt new file mode 100644 index 0000000000..2f541f5d43 --- /dev/null +++ b/commonsUI/src/jvmMain/kotlin/com/vitorpamplona/amethyst/commons/ui/actions/uploads/SharedMediaResolver.jvm.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.commons.ui.actions.uploads + +import androidx.compose.runtime.Composable +import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia +import com.vitorpamplona.amethyst.commons.service.uploads.StringMediaUri +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +// Addresses are plain paths or URLs here; nothing reports a MIME type for them yet. +private object StringSharedMedia : SharedMediaResolver { + override suspend fun resolve(uriString: String?): SelectedMedia? = uriString?.ifBlank { null }?.let { SelectedMedia(StringMediaUri(it), null) } + + override suspend fun resolve(uriStrings: List): ImmutableList = uriStrings.mapNotNull { resolve(it) }.toImmutableList() +} + +@Composable +actual fun rememberSharedMediaResolver(): SharedMediaResolver = StringSharedMedia + +/** No app shares into this one on this platform yet. */ +@Composable +actual fun OnIncomingShare( + onText: (String) -> Unit, + onMedia: (SelectedMedia) -> Unit, +) = Unit