mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 19:28:25 +00:00
refactor: composer seams for location, expiration, message field and imeta
- DeviceLocation port in commons (LocationState implements it), reached through AccountViewModelHost and AppServices; composers and geohash screens stop reading Amethyst.instance.locationManager. - rememberCoarseLocationPermission expect/actual (accompanist on Android, denied elsewhere) replaces direct permission-state calls. - ExpirationDatePicker drops java.time/android.text for LocalClock's new utcOffsetSeconds and a rememberIs24HourClock expect. - MessageField takes MediaUri; PreviewState moves to commonMain. - MediaUploader.remoteFileHeader replaces IMetaAttachments' MimeTypeMap + OkHttp download. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.service.location
|
||||
import android.content.Context
|
||||
import android.location.Location
|
||||
import android.location.LocationManager
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeohashPrecision
|
||||
@@ -75,7 +76,7 @@ class LocationState(
|
||||
LocationFlow(context.getSystemService(Context.LOCATION_SERVICE) as LocationManager)
|
||||
.get(minTimeMs, minDistanceM, onListening)
|
||||
},
|
||||
) {
|
||||
) : DeviceLocation {
|
||||
companion object {
|
||||
/** A 5 km cell takes 2.5 minutes to cross at 120 km/h; 60s/500m is ample. */
|
||||
const val COARSE_MIN_TIME: Long = 60_000L
|
||||
@@ -111,7 +112,7 @@ class LocationState(
|
||||
|
||||
private val latestPreciseLocation = MutableStateFlow<LocationResult>(LocationResult.Loading)
|
||||
|
||||
fun setLocationPermission(newValue: Boolean) {
|
||||
override fun setLocationPermission(newValue: Boolean) {
|
||||
if (newValue != hasLocationPermission.value) {
|
||||
hasLocationPermission.tryEmit(newValue)
|
||||
}
|
||||
@@ -195,7 +196,7 @@ class LocationState(
|
||||
}
|
||||
}.stateIn(scope, SharingStarted.WhileSubscribed(SUBSCRIPTION_STOP_TIMEOUT_MS), cache.value)
|
||||
|
||||
val geohashStateFlow: StateFlow<LocationResult> by lazy {
|
||||
override val geohashStateFlow: StateFlow<LocationResult> by lazy {
|
||||
buildGeohashStateFlow(
|
||||
tag = "GeohashStateFlow",
|
||||
charsCount = GeohashPrecision.KM_5_X_5.digits,
|
||||
@@ -217,7 +218,7 @@ class LocationState(
|
||||
* building-level today. The profile is kept so the intent survives if the
|
||||
* app ever requests `ACCESS_FINE_LOCATION`.
|
||||
*/
|
||||
val preciseGeohashStateFlow: StateFlow<LocationResult> by lazy {
|
||||
override val preciseGeohashStateFlow: StateFlow<LocationResult> by lazy {
|
||||
buildGeohashStateFlow(
|
||||
tag = "PreciseGeohashStateFlow",
|
||||
charsCount = GeohashChannelLevel.BUILDING.chars,
|
||||
|
||||
+13
@@ -22,11 +22,13 @@ package com.vitorpamplona.amethyst.service.uploads
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.webkit.MimeTypeMap
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerType
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.blossomUploadOrder
|
||||
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.BlossomClient
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.BlossomPaymentException
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.FileHeader
|
||||
@@ -52,6 +54,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.File
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
/**
|
||||
@@ -592,4 +595,14 @@ class AndroidMediaUploader(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun remoteFileHeader(
|
||||
url: String,
|
||||
httpClients: IRoleBasedHttpClientBuilder,
|
||||
): FileHeader? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val fileExtension: String = MimeTypeMap.getFileExtensionFromUrl(url)
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault()))
|
||||
FileHeader.prepare(url, mimeType, null) { httpClients.okHttpClientForImage(it) }.getOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -34,13 +34,13 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions
|
||||
import com.vitorpamplona.amethyst.commons.resources.loading_location
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Font12SP
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
@@ -61,7 +61,7 @@ fun AndroidAroundMeLocationLabel() {
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
} else {
|
||||
val location by Amethyst.instance.locationManager.geohashStateFlow
|
||||
val location by LocalAppServices.current.deviceLocation.geohashStateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
when (val myLocation = location) {
|
||||
|
||||
+14
-15
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.expiration
|
||||
|
||||
import android.text.format.DateFormat
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -49,7 +48,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -64,14 +62,15 @@ import com.vitorpamplona.amethyst.commons.resources.expiration_date_select
|
||||
import com.vitorpamplona.amethyst.commons.resources.expiration_expires_in
|
||||
import com.vitorpamplona.amethyst.commons.resources.expiration_time
|
||||
import com.vitorpamplona.amethyst.commons.resources.next
|
||||
import com.vitorpamplona.amethyst.commons.search.calendar.LocalClock
|
||||
import com.vitorpamplona.amethyst.commons.search.calendar.SearchDate
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.rememberIs24HourClock
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.timeAheadNoDot
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import com.vitorpamplona.quartz.utils.currentTimeMillis
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -79,24 +78,26 @@ fun ExpirationDatePicker(model: IExpiration) {
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
var showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
val currentTime = Instant.ofEpochMilli(model.expirationDate * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime()
|
||||
// The expiration as a local wall-clock time: seconds since the epoch shifted by this zone's offset.
|
||||
val localSeconds = model.expirationDate + LocalClock.utcOffsetSeconds(model.expirationDate)
|
||||
val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY)
|
||||
val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year
|
||||
|
||||
val datePickerState =
|
||||
rememberDatePickerState(
|
||||
initialSelectedDateMillis = model.expirationDate * 1000,
|
||||
yearRange = currentTime.year..2050,
|
||||
yearRange = currentYear..2050,
|
||||
selectableDates =
|
||||
object : SelectableDates {
|
||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= System.currentTimeMillis() - 86400000
|
||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= currentTimeMillis() - 86400000
|
||||
},
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
val timePickerState =
|
||||
rememberTimePickerState(
|
||||
initialHour = currentTime.hour,
|
||||
initialMinute = currentTime.minute,
|
||||
is24Hour = DateFormat.is24HourFormat(context),
|
||||
initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
|
||||
initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
|
||||
is24Hour = rememberIs24HourClock(),
|
||||
)
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
@@ -183,9 +184,7 @@ fun ExpirationDatePicker(model: IExpiration) {
|
||||
(timePickerState.minute * TimeUtils.ONE_MINUTE)
|
||||
} ?: TimeUtils.oneDayAhead()
|
||||
|
||||
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
|
||||
|
||||
model.expirationDate = datetimeLocalTimeZone - offset.totalSeconds
|
||||
model.expirationDate = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
|
||||
|
||||
showTimePicker = false
|
||||
},
|
||||
|
||||
+1
@@ -35,6 +35,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.lack_location_permissions
|
||||
import com.vitorpamplona.amethyst.commons.resources.loading_location
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.StdHorzSpacer
|
||||
|
||||
+3
-2
@@ -82,7 +82,6 @@ import androidx.compose.ui.window.DialogProperties
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
@@ -97,6 +96,8 @@ import com.vitorpamplona.amethyst.commons.resources.location_picker_search_hint
|
||||
import com.vitorpamplona.amethyst.commons.resources.location_picker_title
|
||||
import com.vitorpamplona.amethyst.commons.resources.location_picker_use_mine
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.service.location.ForwardGeolocation
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
@@ -206,7 +207,7 @@ fun GeohashLocationPickerContent(
|
||||
val context = LocalContext.current
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val locationManager = Amethyst.instance.locationManager
|
||||
val locationManager = LocalAppServices.current.deviceLocation
|
||||
|
||||
val seed = remember(initialGeohash) { initialGeohash?.takeIf { it.isNotBlank() }?.let { GeoHash.decode(it) } }
|
||||
val seedLen = initialGeohash?.trim()?.length ?: 0
|
||||
|
||||
+2
-2
@@ -20,12 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.location
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
interface ILocationGrabber {
|
||||
fun locationManager(): LocationState
|
||||
fun locationManager(): DeviceLocation
|
||||
|
||||
fun locationFlow(): StateFlow<LocationResult>
|
||||
|
||||
|
||||
+6
-13
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.location
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -39,37 +38,31 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.geohash_explainer
|
||||
import com.vitorpamplona.amethyst.commons.resources.geohash_title
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun LocationAsHash(
|
||||
postViewModel: ILocationGrabber,
|
||||
innerContent: @Composable () -> Unit = {},
|
||||
) {
|
||||
val locationPermissionState =
|
||||
rememberPermissionState(
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
)
|
||||
val locationPermission = rememberCoarseLocationPermission()
|
||||
|
||||
LaunchedEffect(locationPermissionState.status.isGranted) {
|
||||
postViewModel.locationManager().setLocationPermission(locationPermissionState.status.isGranted)
|
||||
LaunchedEffect(locationPermission.isGranted) {
|
||||
postViewModel.locationManager().setLocationPermission(locationPermission.isGranted)
|
||||
}
|
||||
|
||||
if (locationPermissionState.status.isGranted) {
|
||||
if (locationPermission.isGranted) {
|
||||
DisplayLocationBox(postViewModel, innerContent)
|
||||
} else {
|
||||
LaunchedEffect(locationPermissionState) { locationPermissionState.launchPermissionRequest() }
|
||||
LaunchedEffect(locationPermission) { locationPermission.launchRequest() }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.note.creators.messagefield
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
@@ -40,6 +39,7 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.IMessageField
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.MentionPreservingInputTransformation
|
||||
@@ -55,7 +55,7 @@ fun MessageField(
|
||||
placeholder: StringResource,
|
||||
viewModel: IMessageField,
|
||||
requestFocus: Boolean = true,
|
||||
onContentReceived: ((Uri, String?) -> Unit)? = null,
|
||||
onContentReceived: ((MediaUri, String?) -> Unit)? = null,
|
||||
) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
+3
-6
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.PreviewState
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
@@ -73,7 +73,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.setTextAndPlaceCursorAtBeginning
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
|
||||
@@ -1006,9 +1005,7 @@ open class CommentPostViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -1081,5 +1078,5 @@ open class CommentPostViewModel :
|
||||
return location!!
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry
|
||||
import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
|
||||
import com.vitorpamplona.amethyst.commons.service.AppServices
|
||||
import com.vitorpamplona.amethyst.commons.service.BlossomServerFinder
|
||||
@@ -52,6 +53,8 @@ object AndroidAppServices : AppServices {
|
||||
override val namecoinResolver: NamecoinNameResolver get() = Amethyst.instance.namecoinResolver
|
||||
|
||||
override val blossomServerFinder: BlossomServerFinder = AndroidBlossomServerFinder
|
||||
|
||||
override val deviceLocation: DeviceLocation get() = Amethyst.instance.locationManager
|
||||
}
|
||||
|
||||
/** [BlossomServerFinder] over the app's BUD-10 resolver, read lazily (main process only). */
|
||||
|
||||
+3
@@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
|
||||
import com.vitorpamplona.amethyst.AccountInfo
|
||||
import com.vitorpamplona.amethyst.AppModules
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport
|
||||
import com.vitorpamplona.amethyst.commons.service.lnurl.OkHttpLnurlTransport
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure
|
||||
@@ -74,6 +75,8 @@ class AndroidAccountViewModelHost(
|
||||
|
||||
override val mediaUploader: MediaUploader by lazy { AndroidMediaUploader(modules.appContext) }
|
||||
|
||||
override val deviceLocation: DeviceLocation get() = modules.locationManager
|
||||
|
||||
override val savedAccounts: Flow<Set<HexKey>> =
|
||||
LocalPreferences
|
||||
.accountsFlow()
|
||||
|
||||
+6
-4
@@ -45,6 +45,8 @@ import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_opening_external_signer
|
||||
import com.vitorpamplona.amethyst.commons.resources.error_opening_external_signer_description
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelPreload
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||
@@ -133,13 +135,13 @@ fun LoggedInPage(
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun WatchLocationPermissions() {
|
||||
val locationPermissionState = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
val locationPermissionState = rememberCoarseLocationPermission()
|
||||
|
||||
LaunchedEffect(locationPermissionState.status.isGranted) {
|
||||
Amethyst.instance.locationManager.setLocationPermission(locationPermissionState.status.isGranted)
|
||||
val locationSource = LocalAppServices.current.deviceLocation
|
||||
LaunchedEffect(locationPermissionState.isGranted) {
|
||||
locationSource.setLocationPermission(locationPermissionState.isGranted)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-11
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -57,16 +56,13 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.user.observeUserPicture
|
||||
import com.vitorpamplona.amethyst.commons.search.SearchSeed
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.RobohashFallbackAsyncImage
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
|
||||
@@ -74,6 +70,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.SearchTopBarAction
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.LocalDisplaySettings
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatActingIdentities
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.LocalChatDisplayNameResolver
|
||||
@@ -121,7 +118,6 @@ fun GeohashChatScreen(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
private fun GeohashChatRoom(
|
||||
channel: GeohashChatChannel,
|
||||
@@ -159,13 +155,14 @@ private fun GeohashChatRoom(
|
||||
// otherwise we fall back to how the user arrived — the map picker passes teleported=true, "near me"
|
||||
// and manual entry default to false. This drives the ["t","teleport"] tag honestly. rememberPermissionState
|
||||
// only reads the current grant (it never prompts), so opening a location chat can't trigger a GPS dialog.
|
||||
val locationPermission = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
LaunchedEffect(locationPermission.status.isGranted) {
|
||||
if (locationPermission.status.isGranted) {
|
||||
Amethyst.instance.locationManager.setLocationPermission(true)
|
||||
val locationPermission = rememberCoarseLocationPermission()
|
||||
val locationSource = LocalAppServices.current.deviceLocation
|
||||
LaunchedEffect(locationPermission.isGranted) {
|
||||
if (locationPermission.isGranted) {
|
||||
locationSource.setLocationPermission(true)
|
||||
}
|
||||
}
|
||||
val deviceLocation by Amethyst.instance.locationManager.preciseGeohashStateFlow
|
||||
val deviceLocation by locationSource.preciseGeohashStateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
val isTeleported =
|
||||
remember(deviceLocation, geohash, teleported) {
|
||||
|
||||
+9
-12
@@ -20,7 +20,6 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.geohashChat
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -56,18 +55,16 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbol
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberCoarseLocationPermission
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarExtensibleWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.platform.LoadCityName
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.experimental.bitchat.geohash.GeohashChannelLevel
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.geohash.GeoHash
|
||||
@@ -274,18 +271,18 @@ private fun ManualEntrySection(onOpen: (String) -> Unit) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
private fun NearMeSection(onOpen: (String) -> Unit) {
|
||||
Column {
|
||||
SectionHeader(MaterialSymbols.Explore, "Near me")
|
||||
|
||||
val permission = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
LaunchedEffect(permission.status.isGranted) {
|
||||
Amethyst.instance.locationManager.setLocationPermission(permission.status.isGranted)
|
||||
val permission = rememberCoarseLocationPermission()
|
||||
val locationSource = LocalAppServices.current.deviceLocation
|
||||
LaunchedEffect(permission.isGranted) {
|
||||
locationSource.setLocationPermission(permission.isGranted)
|
||||
}
|
||||
|
||||
if (!permission.status.isGranted) {
|
||||
if (!permission.isGranted) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -297,7 +294,7 @@ private fun NearMeSection(onOpen: (String) -> Unit) {
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 12.dp),
|
||||
)
|
||||
FilledTonalButton(onClick = { permission.launchPermissionRequest() }) {
|
||||
FilledTonalButton(onClick = { permission.launchRequest() }) {
|
||||
SymbolIcon(symbol = MaterialSymbols.LocationOn, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||
Text(" Use my location")
|
||||
}
|
||||
@@ -306,7 +303,7 @@ private fun NearMeSection(onOpen: (String) -> Unit) {
|
||||
return
|
||||
}
|
||||
|
||||
val location by Amethyst.instance.locationManager.preciseGeohashStateFlow
|
||||
val location by LocalAppServices.current.deviceLocation.preciseGeohashStateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
when (val loc = location) {
|
||||
is LocationResult.Success -> {
|
||||
|
||||
+3
-6
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.PreviewState
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState
|
||||
@@ -57,7 +57,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.insertUrlAtCursor
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileSender
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.upload.ChatFileUploader
|
||||
@@ -798,9 +797,7 @@ class ChatNewMessageViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -870,5 +867,5 @@ class ChatNewMessageViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
}
|
||||
|
||||
+5
-10
@@ -20,13 +20,12 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send
|
||||
|
||||
import android.webkit.MimeTypeMap
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.FileHeader
|
||||
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator
|
||||
import com.vitorpamplona.amethyst.service.uploads.prepare
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTag
|
||||
import com.vitorpamplona.quartz.nip92IMeta.IMetaTagBuilder
|
||||
import com.vitorpamplona.quartz.nip92IMeta.imetaTagBuilder
|
||||
@@ -40,21 +39,17 @@ import com.vitorpamplona.quartz.nip94FileMetadata.originalHash
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.sensitiveContent
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.size
|
||||
import com.vitorpamplona.quartz.nip94FileMetadata.thumbhash
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.Locale
|
||||
|
||||
class IMetaAttachments {
|
||||
var iMetaAttachments by mutableStateOf<List<IMetaTag>>(emptyList())
|
||||
|
||||
suspend fun downloadAndPrepare(
|
||||
url: String,
|
||||
okHttpClient: (String) -> OkHttpClient,
|
||||
uploader: MediaUploader,
|
||||
httpClients: IRoleBasedHttpClientBuilder,
|
||||
) {
|
||||
val fileExtension: String = MimeTypeMap.getFileExtensionFromUrl(url)
|
||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension.lowercase(Locale.getDefault()))
|
||||
|
||||
val imeta =
|
||||
FileHeader.prepare(url, mimeType, null, okHttpClient).getOrNull()?.let {
|
||||
uploader.remoteFileHeader(url, httpClients)?.let {
|
||||
IMetaTagBuilder(url)
|
||||
.apply {
|
||||
hash(it.hash)
|
||||
|
||||
+3
-6
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Channel
|
||||
@@ -44,6 +43,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.emphChat.EphemeralChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.geohashChat.GeohashChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.nip28PublicChats.PublicChatChannel
|
||||
import com.vitorpamplona.amethyst.commons.model.nip29RelayGroups.RelayGroupChannel
|
||||
@@ -71,7 +71,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.ReplyMode
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
@@ -1016,9 +1015,7 @@ open class ChannelNewMessageViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -1041,7 +1038,7 @@ open class ChannelNewMessageViewModel :
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
|
||||
override fun locationFlow(): StateFlow<LocationResult> {
|
||||
if (location == null) {
|
||||
|
||||
+3
-6
@@ -31,7 +31,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.IZapRaiser
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
||||
@@ -72,7 +72,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.uploadToDefaultServer
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
|
||||
@@ -686,9 +685,7 @@ class LongFormPostViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -762,5 +759,5 @@ class LongFormPostViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
}
|
||||
|
||||
+3
-6
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.PreviewState
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
@@ -67,7 +67,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
|
||||
@@ -579,9 +578,7 @@ open class NewProductViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaDescription.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaDescription.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -681,5 +678,5 @@ open class NewProductViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
}
|
||||
|
||||
+2
-2
@@ -59,7 +59,6 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedContentState
|
||||
import com.vitorpamplona.amethyst.commons.feeds.ChannelFeedState
|
||||
import com.vitorpamplona.amethyst.commons.feeds.FeedContentState
|
||||
@@ -92,6 +91,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.zonedDrawerSwipeIfModal
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
|
||||
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geohash.NewGeoPostButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.NewHashtagPostButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -334,7 +334,7 @@ fun HomeScreenFloatingButton(
|
||||
|
||||
when (val filter = list.value) {
|
||||
TopFilter.AroundMe -> {
|
||||
val location by Amethyst.instance.locationManager.geohashStateFlow
|
||||
val location by LocalAppServices.current.deviceLocation.geohashStateFlow
|
||||
.collectAsStateWithLifecycle()
|
||||
|
||||
when (val myLocation = location) {
|
||||
|
||||
+3
-5
@@ -48,6 +48,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.PreviewState
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState.EmojiMedia
|
||||
@@ -91,7 +92,6 @@ import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.RecordingResult
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationController
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
@@ -1822,9 +1822,7 @@ open class ShortNotePostViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -2120,5 +2118,5 @@ open class ShortNotePostViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
}
|
||||
|
||||
+3
-6
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -40,6 +39,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.DraftTagState
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.IExpiration
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiSuggestionState
|
||||
@@ -61,7 +61,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ChatFileUploadState
|
||||
@@ -571,9 +570,7 @@ open class NestNewMessageViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -596,7 +593,7 @@ open class NestNewMessageViewModel :
|
||||
message.insertUrlAtCursor(newElement)
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
|
||||
override fun locationFlow(): StateFlow<LocationResult> {
|
||||
if (location == null) {
|
||||
|
||||
+3
-6
@@ -30,7 +30,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
@@ -43,6 +42,7 @@ import com.vitorpamplona.amethyst.commons.model.composer.NewMessageTagger
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.PreviewState
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.SplitBuilder
|
||||
import com.vitorpamplona.amethyst.commons.model.composer.toZapSplitSetup
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.model.location.LocationResult
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.model.nip30CustomEmojis.EmojiPackState
|
||||
@@ -69,7 +69,6 @@ import com.vitorpamplona.amethyst.commons.ui.text.onUiThread
|
||||
import com.vitorpamplona.amethyst.commons.ui.text.replaceCurrentWord
|
||||
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.location.LocationState
|
||||
import com.vitorpamplona.amethyst.ui.note.creators.location.ILocationGrabber
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.send.IMetaAttachments
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.UserSuggestionAnchor
|
||||
@@ -643,9 +642,7 @@ class NewPublicMessageViewModel :
|
||||
val wordToInsert = item.link + " "
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
iMetaAttachments.downloadAndPrepare(item.link) {
|
||||
accountViewModel.httpClientBuilder.okHttpClientForImage(item.link)
|
||||
}
|
||||
iMetaAttachments.downloadAndPrepare(item.link, accountViewModel.host.mediaUploader, accountViewModel.httpClientBuilder)
|
||||
}
|
||||
|
||||
message.replaceCurrentWord(wordToInsert)
|
||||
@@ -715,5 +712,5 @@ class NewPublicMessageViewModel :
|
||||
draftTag.newVersion()
|
||||
}
|
||||
|
||||
override fun locationManager(): LocationState = Amethyst.instance.locationManager
|
||||
override fun locationManager(): DeviceLocation = accountViewModel.host.deviceLocation
|
||||
}
|
||||
|
||||
+1
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.commons.richtext.CachedUrlParser
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.model.location
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* The device's position as geohashes, for "around me" feeds, location chats and the composers'
|
||||
* location tag. The platform decides when it actually listens; readers only collect.
|
||||
*/
|
||||
interface DeviceLocation {
|
||||
/** A roughly 5 km geohash cell, or why there is none. */
|
||||
val geohashStateFlow: StateFlow<LocationResult>
|
||||
|
||||
/** The finest geohash the platform offers, for location channels that truncate it per level. */
|
||||
val preciseGeohashStateFlow: StateFlow<LocationResult>
|
||||
|
||||
/** Tells the source whether the user has granted location access, so it can start or stop. */
|
||||
fun setLocationPermission(newValue: Boolean)
|
||||
|
||||
/** No location source on this platform: always reports a missing permission. */
|
||||
object None : DeviceLocation {
|
||||
private val lacking = MutableStateFlow<LocationResult>(LocationResult.LackPermission)
|
||||
|
||||
override val geohashStateFlow: StateFlow<LocationResult> = lacking
|
||||
override val preciseGeohashStateFlow: StateFlow<LocationResult> = lacking
|
||||
|
||||
override fun setLocationPermission(newValue: Boolean) = Unit
|
||||
}
|
||||
}
|
||||
+3
@@ -41,6 +41,9 @@ expect object LocalClock {
|
||||
/** Today, in the reader's timezone. */
|
||||
fun today(): SearchDate
|
||||
|
||||
/** How many seconds this zone is ahead of UTC at [unixSeconds]; a clock change moves it. */
|
||||
fun utcOffsetSeconds(unixSeconds: Long): Long
|
||||
|
||||
/**
|
||||
* Which local day a unix second falls on, with the zone read once for the whole counter
|
||||
* rather than per timestamp: for grouping a batch of events by day in one pass.
|
||||
|
||||
+4
@@ -24,6 +24,7 @@ import com.vitorpamplona.amethyst.commons.browser.BrowserHistoryRegistry
|
||||
import com.vitorpamplona.amethyst.commons.browser.BrowserIconRegistry
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.signers.NostrSignerPermissionStore
|
||||
import com.vitorpamplona.amethyst.commons.favorites.FavoriteAppsRegistry
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.napplet.permissions.NappletPermissionLedger
|
||||
import com.vitorpamplona.amethyst.commons.tor.TorSettingsFlow
|
||||
import com.vitorpamplona.quartz.nip05DnsIdentifiers.namecoin.NamecoinNameResolver
|
||||
@@ -56,4 +57,7 @@ interface AppServices {
|
||||
|
||||
/** Finds the server behind a `blossom:` URI (BUD-10). */
|
||||
val blossomServerFinder: BlossomServerFinder get() = BlossomServerFinder.None
|
||||
|
||||
/** The device's position as geohashes. */
|
||||
val deviceLocation: DeviceLocation get() = DeviceLocation.None
|
||||
}
|
||||
|
||||
+17
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.commons.service.uploads
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.mediaServers.ServerName
|
||||
import com.vitorpamplona.amethyst.commons.service.http.IRoleBasedHttpClientBuilder
|
||||
import com.vitorpamplona.amethyst.commons.service.upload.FileHeader
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.ciphers.NostrCipher
|
||||
|
||||
@@ -74,6 +76,16 @@ interface MediaUploader {
|
||||
useH265: Boolean,
|
||||
convertGifToMp4: Boolean,
|
||||
): MediaCompressorResult
|
||||
|
||||
/**
|
||||
* Downloads the already-hosted file at [url] and computes its [FileHeader] (hash, size,
|
||||
* dimensions, preview hashes) for an `imeta` tag, guessing the MIME type from the URL's
|
||||
* extension. Null when it cannot be downloaded or decoded.
|
||||
*/
|
||||
suspend fun remoteFileHeader(
|
||||
url: String,
|
||||
httpClients: IRoleBasedHttpClientBuilder,
|
||||
): FileHeader?
|
||||
}
|
||||
|
||||
/** No media pipeline on this front end yet: every upload fails with a clear message. */
|
||||
@@ -121,4 +133,9 @@ object UnavailableMediaUploader : MediaUploader {
|
||||
useH265: Boolean,
|
||||
convertGifToMp4: Boolean,
|
||||
): MediaCompressorResult = MediaCompressorResult(uri, mimeType, null)
|
||||
|
||||
override suspend fun remoteFileHeader(
|
||||
url: String,
|
||||
httpClients: IRoleBasedHttpClientBuilder,
|
||||
): FileHeader? = null
|
||||
}
|
||||
|
||||
+4
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.viewmodels
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.location.DeviceLocation
|
||||
import com.vitorpamplona.amethyst.commons.service.lnurl.LnurlHttpTransport
|
||||
import com.vitorpamplona.amethyst.commons.service.pow.PoWJobFailure
|
||||
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
|
||||
@@ -70,6 +71,9 @@ interface AccountViewModelHost {
|
||||
/** Compresses, strips, encrypts and uploads picked media for the composers. */
|
||||
val mediaUploader: MediaUploader
|
||||
|
||||
/** The device's position as geohashes. */
|
||||
val deviceLocation: DeviceLocation get() = DeviceLocation.None
|
||||
|
||||
/** Public keys of every account saved on this device. */
|
||||
val savedAccounts: Flow<Set<HexKey>>
|
||||
|
||||
|
||||
+2
@@ -67,6 +67,8 @@ actual object LocalClock {
|
||||
|
||||
actual fun today(): SearchDate = ZoneMath.dayAt(currentTimeSeconds(), localZone)
|
||||
|
||||
actual fun utcOffsetSeconds(unixSeconds: Long): Long = localZone.at(unixSeconds)
|
||||
|
||||
actual fun epochDayCounter(): EpochDayCounter = EpochDayCounter { ZoneMath.epochDayAt(it, localZone) }
|
||||
|
||||
/**
|
||||
|
||||
+7
@@ -54,6 +54,13 @@ actual object LocalClock {
|
||||
SearchDate(it.year, it.monthValue, it.dayOfMonth)
|
||||
}
|
||||
|
||||
actual fun utcOffsetSeconds(unixSeconds: Long): Long =
|
||||
zone()
|
||||
.rules
|
||||
.getOffset(Instant.ofEpochSecond(unixSeconds))
|
||||
.totalSeconds
|
||||
.toLong()
|
||||
|
||||
actual fun epochDayCounter(): EpochDayCounter {
|
||||
val rules = zone().rules
|
||||
val offsets = ZoneOffsets { rules.getOffset(Instant.ofEpochSecond(it)).totalSeconds.toLong() }
|
||||
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import android.Manifest
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
actual fun rememberCoarseLocationPermission(): PlatformPermission {
|
||||
val state = rememberPermissionState(Manifest.permission.ACCESS_COARSE_LOCATION)
|
||||
return remember(state) {
|
||||
object : PlatformPermission {
|
||||
override val isGranted: Boolean get() = state.status.isGranted
|
||||
|
||||
override fun launchRequest() = state.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -70,6 +70,12 @@ actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String {
|
||||
return remember(context) { { epochMillis -> DateFormat.getTimeFormat(context).format(Date(epochMillis)) } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun rememberIs24HourClock(): Boolean {
|
||||
val context = LocalContext.current
|
||||
return remember(context) { DateFormat.is24HourFormat(context) }
|
||||
}
|
||||
|
||||
actual fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/**
|
||||
* The approximate-location permission the geohash features need. Android asks for
|
||||
* `ACCESS_COARSE_LOCATION`; desktop and iOS have no location source wired yet and report it denied.
|
||||
*/
|
||||
@Composable
|
||||
expect fun rememberCoarseLocationPermission(): PlatformPermission
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
/** A runtime permission the user may grant or deny. [isGranted] is observable from composition. */
|
||||
@Stable
|
||||
interface PlatformPermission {
|
||||
val isGranted: Boolean
|
||||
|
||||
/** Asks the user for the permission; [isGranted] updates when they answer. */
|
||||
fun launchRequest()
|
||||
}
|
||||
+4
@@ -45,6 +45,10 @@ expect fun calendarYearAndDay(epochMillis: Long): Int
|
||||
@Composable
|
||||
expect fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String
|
||||
|
||||
/** Whether times should be shown on a 24-hour clock, under the same rules as [rememberTimeOfDayFormatter]. */
|
||||
@Composable
|
||||
expect fun rememberIs24HourClock(): Boolean
|
||||
|
||||
/**
|
||||
* The platform's abbreviated relative span ("5 min. ago", "Yesterday"), [nowLabel] for an
|
||||
* instant that rounds to now or lies ahead, or null where the platform has no such formatter.
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private object NoLocationPermission : PlatformPermission {
|
||||
override val isGranted: Boolean = false
|
||||
|
||||
override fun launchRequest() = Unit
|
||||
}
|
||||
|
||||
/** No location source on this platform yet. */
|
||||
@Composable
|
||||
actual fun rememberCoarseLocationPermission(): PlatformPermission = NoLocationPermission
|
||||
+5
@@ -21,6 +21,7 @@
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import platform.Foundation.NSCalendar
|
||||
import platform.Foundation.NSCalendarUnitDay
|
||||
import platform.Foundation.NSCalendarUnitYear
|
||||
@@ -88,6 +89,10 @@ private val timeOfDay: (Long) -> String = { epochMillis -> timeOfDayFormatter.st
|
||||
@Composable
|
||||
actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay
|
||||
|
||||
// The "j" template asks for the locale's preferred hour; an "a" in the result means an AM/PM clock.
|
||||
@Composable
|
||||
actual fun rememberIs24HourClock(): Boolean = remember { NSDateFormatter.dateFormatFromTemplate("j", 0u, NSLocale.currentLocale)?.contains('a') != true }
|
||||
|
||||
actual fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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.components
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
private object NoLocationPermission : PlatformPermission {
|
||||
override val isGranted: Boolean = false
|
||||
|
||||
override fun launchRequest() = Unit
|
||||
}
|
||||
|
||||
/** No location source on this platform yet. */
|
||||
@Composable
|
||||
actual fun rememberCoarseLocationPermission(): PlatformPermission = NoLocationPermission
|
||||
+11
@@ -21,9 +21,12 @@
|
||||
package com.vitorpamplona.amethyst.commons.ui.note
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.chrono.IsoChronology
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeFormatterBuilder
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.Locale
|
||||
|
||||
@@ -80,6 +83,14 @@ private val timeOfDay: (Long) -> String = { epochMillis ->
|
||||
@Composable
|
||||
actual fun rememberTimeOfDayFormatter(): (epochMillis: Long) -> String = timeOfDay
|
||||
|
||||
@Composable
|
||||
actual fun rememberIs24HourClock(): Boolean =
|
||||
remember {
|
||||
DateTimeFormatterBuilder
|
||||
.getLocalizedDateTimePattern(null, FormatStyle.SHORT, IsoChronology.INSTANCE, Locale.getDefault())
|
||||
.contains('H')
|
||||
}
|
||||
|
||||
actual fun relativeTimeSpanShortOrNull(
|
||||
epochMillis: Long,
|
||||
nowMillis: Long,
|
||||
|
||||
Reference in New Issue
Block a user