refactor: move the composers, calendars, chess, geocache and voice-reply screens

Clear the next ring of platform blockers and move the screens they freed:

- Date pickers (poll, zap-poll, schedule-at) read the local clock through
  LocalClock and rememberIs24HourClock instead of java.time / DateFormat.
- FileSharer.shareTextFile replaces the app's .ics share; PhoneCalendar
  wraps the system event composer; relativeTimeSpan wraps DateUtils (and
  NSRelativeDateTimeFormatter on iOS).
- rememberWindowViewModel + LocalWindowViewModelStoreOwner keep state that
  spans destinations (chess lobby and board, the Cordn group draft) without
  reaching for FragmentActivity.
- isHlsMedia moves to commons.video; ChessEventBroadcaster to commonMain;
  the host gains supportsWritingAssistant.
- geo: links and nostr: shares go through LocalUriHandler / TextSharer.

204 of 252 screens are now shared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S7FuNBSKiyVecARSoE4B9P
This commit is contained in:
Claude
2026-09-30 13:46:21 +00:00
parent 7a5000ce50
commit 532ee55a59
88 changed files with 768 additions and 476 deletions
@@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.compose.ContentFrame import androidx.media3.ui.compose.ContentFrame
import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.commons.video.isHlsMedia
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.controls.BottomGradientOverlay import com.vitorpamplona.amethyst.service.playback.composable.controls.BottomGradientOverlay
import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeControlsState import com.vitorpamplona.amethyst.service.playback.composable.controls.FullscreenSwipeControlsState
@@ -61,7 +62,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.applyView
import com.vitorpamplona.amethyst.service.playback.composable.controls.clampViewportShortSide import com.vitorpamplona.amethyst.service.playback.composable.controls.clampViewportShortSide
import com.vitorpamplona.amethyst.service.playback.composable.controls.fullscreenSwipeControls import com.vitorpamplona.amethyst.service.playback.composable.controls.fullscreenSwipeControls
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.AudioPlayingAnimation import com.vitorpamplona.amethyst.service.playback.composable.wavefront.AudioPlayingAnimation
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.rememberIsAudioTrack import com.vitorpamplona.amethyst.service.playback.composable.wavefront.rememberIsAudioTrack
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.PinBottomIconSize
import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.video.isHlsMedia
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
import com.vitorpamplona.amethyst.service.cast.CastRequest import com.vitorpamplona.amethyst.service.cast.CastRequest
@@ -80,7 +81,6 @@ import com.vitorpamplona.amethyst.service.cast.resolveCastLiveness
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState import com.vitorpamplona.amethyst.service.playback.composable.MediaControllerState
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.amethyst.service.playback.diskCache.HlsLivenessCache import com.vitorpamplona.amethyst.service.playback.diskCache.HlsLivenessCache
import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity
import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog
@@ -1,51 +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.service.playback.composable.mediaitem
import androidx.media3.common.MimeTypes
/**
* Whether a URL plus its imeta mime identifies HLS.
*
* This is the caller-side form for code that holds a URL and a mime but no `MediaItem` yet — UI that
* has a [MediaItemData] or a `MediaUrlContent`. Once a `MediaItem` exists,
* `isHlsMediaItem` is the equivalent, and both must answer the same way: the UI decides what to
* render, the factory decides how to load it, and a disagreement shows up as a player that streams
* something the surrounding chrome says is a still image.
*
* Defined in terms of [MediaItemCache.toExoPlayerMimeType] rather than re-deriving the rules, so it
* cannot drift from the mime that actually reaches ExoPlayer. That normalizer prefers an explicit
* mime (mapping the four HLS aliases onto [MimeTypes.APPLICATION_M3U8]) and otherwise falls back to a
* **path-anchored** `.m3u8` test.
*
* Both halves matter. A BUD-10 blossom playlist is `https://host/<sha256>` with no extension at all,
* so only the mime identifies it; and anchoring to the path stops `video.mp4?ref=a.m3u8` counting as
* HLS on the strength of its query string.
*
* Note this answers *is it HLS*, which callers use as a proxy for *is it live*. The proxy is
* imprecise in the same way for every HLS URL — an on-demand HLS playlist also answers true — and
* that imprecision is older than this function. Liveness is only truly knowable from
* `#EXT-X-ENDLIST` once the playlist is loaded, which is what `HlsLivenessCache` records.
*/
fun isHlsMedia(
url: String,
mimeType: String?,
): Boolean = MediaItemCache.toExoPlayerMimeType(mimeType, url) == MimeTypes.APPLICATION_M3U8
@@ -25,9 +25,9 @@ import androidx.core.net.toUri
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache
import com.vitorpamplona.amethyst.commons.video.normalizeStreamMimeType
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -45,23 +45,7 @@ class MediaItemCache : GenericBaseCache<MediaItemData, LoadedMediaItem>(20) {
internal fun toExoPlayerMimeType( internal fun toExoPlayerMimeType(
mimeType: String?, mimeType: String?,
videoUri: String? = null, videoUri: String? = null,
): String? { ): String? = normalizeStreamMimeType(mimeType, videoUri)
if (!mimeType.isNullOrBlank()) {
return when (mimeType.lowercase()) {
"application/vnd.apple.mpegurl",
"application/x-mpegurl",
"audio/x-mpegurl",
"audio/mpegurl",
-> MimeTypes.APPLICATION_M3U8
else -> mimeType
}
}
if (videoUri != null && hasM3u8PathExtension(videoUri)) {
return MimeTypes.APPLICATION_M3U8
}
return null
}
// A side-loaded track needs its media type declared: a NIP-71 `text-track` says nothing // A side-loaded track needs its media type declared: a NIP-71 `text-track` says nothing
// about the format, and a blossom-hosted one is `https://host/<sha256>` with no extension // about the format, and a blossom-hosted one is `https://host/<sha256>` with no extension
@@ -81,14 +65,6 @@ class MediaItemCache : GenericBaseCache<MediaItemData, LoadedMediaItem>(20) {
language != null && type != null -> "$language \u00B7 $type" language != null && type != null -> "$language \u00B7 $type"
else -> language ?: type else -> language ?: type
} }
// Restrict `.m3u8` matching to the path component so a query param or
// fragment that happens to mention .m3u8 (e.g. `?ref=a.m3u8`) on an
// MP4 URI doesn't misroute to HlsMediaSource.
private fun hasM3u8PathExtension(uri: String): Boolean {
val path = uri.substringBefore('?').substringBefore('#')
return path.endsWith(".m3u8", ignoreCase = true)
}
} }
override suspend fun compute(key: MediaItemData): LoadedMediaItem = override suspend fun compute(key: MediaItemData): LoadedMediaItem =
@@ -26,7 +26,9 @@ import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.CompositionLocalProvider
import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.ui.components.LocalWindowViewModelStoreOwner
import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider import com.vitorpamplona.amethyst.commons.ui.note.elements.NowProvider
import com.vitorpamplona.amethyst.debugState import com.vitorpamplona.amethyst.debugState
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
@@ -61,11 +63,15 @@ class MainActivity : AppCompatActivity() {
setContent { setContent {
AmethystTheme { AmethystTheme {
NowProvider { NowProvider {
// The Activity outlives every nav destination: screens that share state
// across destinations (chess lobby + board, the Cordn group draft) keep it here.
CompositionLocalProvider(LocalWindowViewModelStoreOwner provides this) {
AccountScreen(Amethyst.instance.sessionManager) AccountScreen(Amethyst.instance.sessionManager)
} }
} }
} }
} }
}
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
override fun onResume() { override fun onResume() {
@@ -88,9 +88,9 @@ import com.vitorpamplona.amethyst.commons.ui.components.getDialogWindow
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility import com.vitorpamplona.amethyst.commons.ui.components.rememberViewerControlsVisibility
import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.commons.video.isHlsMedia
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner import com.vitorpamplona.amethyst.service.playback.composable.VideoViewInner
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
@@ -67,6 +67,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.LocalTabResel
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.TabReselectCoordinator
import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue import com.vitorpamplona.amethyst.commons.ui.navigation.findQueryParameterValue
import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.ReplyCommentPostScreen import com.vitorpamplona.amethyst.commons.ui.note.nip22Comments.ReplyCommentPostScreen
import com.vitorpamplona.amethyst.commons.ui.note.share.ShareNoteAsQrScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.apps.recommendations.ProfileAppRecommendationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.apps.recommendations.ProfileAppRecommendationsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.ArticlesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.ArticlesScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.backups.BackupConflictReviewScreen
@@ -95,7 +96,11 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzInviteScre
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.BuzzNewDmScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.JobBoardScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.buzz.WorkflowRunBoardScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.CalendarCollectionsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.create.NewCalendarCollectionScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupInfoScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnGroupListScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup.CordnInvitationsScreen
@@ -123,6 +128,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.ephemChat.metadata.NewEphemeralChatScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.PublicChatChannelScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupBrowseScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChatScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupCreateScreen
@@ -131,6 +137,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupMembersScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupThreadsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share.ShareToDMScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.share.ShareToDMScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessGameScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessLobbyScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.CommunityScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.CommunitiesScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.CommunitiesScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.newCommunity.EditCommunityScreen
@@ -150,6 +158,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membersh
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.emojipacks.membershipManagement.MyEmojiListScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.FavoriteAppsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.favorites.FavoriteAppsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.FollowPacksScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.followPacks.list.FollowPacksScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.GeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.GeocacheHuntScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.NewGeocacheHuntScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.hunt.NewGeocacheHuntScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.log.LogGeocacheFindScreen
@@ -161,6 +171,8 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.hashtag.HashtagScre
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.HighlightsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.highlights.NewHighlightScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.HomeScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.HomeScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.VoiceReplyScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.nip75Goals.NewGoalScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.display.InterestSetScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.interestSets.list.ListOfInterestSetsScreen
@@ -190,6 +202,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.PodcastsSc
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.EditPodcastShowScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.NewPodcastTrailerScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastAuthoringScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.podcasts.authoring.PodcastAuthoringScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollPostScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.PollsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls.results.PollResultsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.privacy.PrivacyOptionsScreen
@@ -212,6 +225,7 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.AudioVisua
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BlockedUsersScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BlockedUsersScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.BottomBarSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CallSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.CallSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.ComposeSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.DrawerSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.DrawerSettingsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWordsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HiddenWordsScreen
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings.HomeTabsSettingsScreen
@@ -279,24 +293,16 @@ import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen
import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen
import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageFileScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageFileScreen
import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen
import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsQrScreen
import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout
import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.browser.WebAppScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarCollectionsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarReminderSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarEventScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.create.NewCalendarEventScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail.CalendarEventDetailScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateGroupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnCreateMembersScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.CordnGroupChatScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.LiveActivityChannelScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.relayGroup.RelayGroupChannelListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessGameScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessLobbyScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabAccountWatcher
import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabLayer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.EmbeddedTabPreloader
@@ -305,15 +311,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.embed.FavoriteAppManifestPr
import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.NostrAppScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.favorites.NostrAppScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.followPacks.feed.FollowPackFeedScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.GeocachesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.GeocachesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create.NewGeocacheScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail.GeocacheDetailScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitNewIssueScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryCodeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryIssuesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryPullsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.gitRepo.GitRepositoryScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.VoiceReplyScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.keyBackup.AccountBackupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.AddToMusicPlaylistSheet
import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.music.NewMusicTrackScreen
@@ -322,13 +324,11 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.napplets.NappletsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.NestsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.podcasts.authoring.NewPodcastEpisodeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.polls.PollPostScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.payment.SendPaymentScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode.ScanQrImageScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip86.RelayManagementScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.scheduledposts.ScheduledPostsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.ComposeSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NamecoinSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NotificationSettingsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen
@@ -93,6 +93,7 @@ import com.vitorpamplona.amethyst.commons.resources.share_as_image_watermark
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.commons.ui.components.LoadNote import com.vitorpamplona.amethyst.commons.ui.components.LoadNote
import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner import com.vitorpamplona.amethyst.commons.ui.components.TextSpinner
import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer import com.vitorpamplona.amethyst.commons.ui.components.TitleExplainer
@@ -103,7 +104,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource import com.vitorpamplona.amethyst.commons.ui.uploads.errorResource
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -48,13 +48,13 @@ import com.vitorpamplona.amethyst.commons.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarAppointmentLines import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarAppointmentLines
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarDateBadge import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.CalendarDateBadge
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detailRouteFor import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detailRouteFor
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel
import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
@@ -60,9 +60,9 @@ import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.commons.ui.note.LoadUser import com.vitorpamplona.amethyst.commons.ui.note.LoadUser
import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.commons.ui.note.UsernameDisplay
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessViewModelFactory
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory
import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent
import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent
import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent import com.vitorpamplona.quartz.nip64Chess.game.ChessGameEvent
@@ -62,6 +62,7 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.Size75Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size75Modifier
import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier import com.vitorpamplona.amethyst.commons.ui.theme.VoiceHeightModifier
import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier import com.vitorpamplona.amethyst.commons.ui.theme.imageModifier
import com.vitorpamplona.amethyst.commons.video.isHlsMedia
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING import com.vitorpamplona.amethyst.service.playback.composable.DEFAULT_MUTED_SETTING
import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController import com.vitorpamplona.amethyst.service.playback.composable.GetVideoController
@@ -74,7 +75,6 @@ import com.vitorpamplona.amethyst.service.playback.composable.controls.PictureIn
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.GetMediaItem
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.LoadedMediaItem
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.MediaItemData
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform import com.vitorpamplona.amethyst.service.playback.composable.wavefront.Waveform
import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity import com.vitorpamplona.amethyst.service.playback.pip.PipVideoActivity
import com.vitorpamplona.amethyst.ui.components.ShareMediaAction import com.vitorpamplona.amethyst.ui.components.ShareMediaAction
@@ -99,6 +99,8 @@ class AndroidAccountViewModelHost(
): Result<AnonymizedResult> = VoiceAnonymizer().anonymize(input.toFile(), VoicePreset.valueOf(presetName)) ): Result<AnonymizedResult> = VoiceAnonymizer().anonymize(input.toFile(), VoicePreset.valueOf(presetName))
// Built on the application context: the assistant outlives any one Activity inside the ViewModel. // Built on the application context: the assistant outlives any one Activity inside the ViewModel.
override val supportsWritingAssistant: Boolean get() = WritingAssistantFactory.IS_SUPPORTED
override fun createWritingAssistant(): WritingAssistant = WritingAssistantFactory.create(modules.appContext) override fun createWritingAssistant(): WritingAssistant = WritingAssistantFactory.create(modules.appContext)
override val savedAccounts: Flow<Set<HexKey>> = override val savedAccounts: Flow<Set<HexKey>> =
@@ -28,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.model.navigation.BottomBarEntry
import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem import com.vitorpamplona.amethyst.commons.model.navigation.NavBarItem
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.articles.datasource.ArticlesFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.badges.datasource.BadgesFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.concord.datasource.ConcordChannelSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.communities.list.datasource.CommunitiesListFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.discover.datasource.DiscoveryFilterAssemblerSubscription
@@ -51,7 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.softwareapps.dataso
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.datasource.VideoFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.workouts.datasource.WorkoutsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.datasource.ChatroomListFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription import com.vitorpamplona.amethyst.ui.screen.loggedIn.nests.datasource.NestsFilterAssemblerSubscription
@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.video.UserCardHeader
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
@@ -39,8 +39,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
@@ -1,60 +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.screen.loggedIn.calendars
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import java.io.File
/**
* Writes [content] to a `.ics` file in the app's cache dir and opens the system share sheet so
* the user can hand the file to a calendar app, email client, file manager, etc.
*
* The file lives in `cacheDir/calendar/` — covered by the existing `<cache-path>` entry in
* `file_paths.xml`, so [FileProvider] can hand out a `content://` URI without further config.
* The receiver gets a read-permission grant via [Intent.FLAG_GRANT_READ_URI_PERMISSION] that
* lasts only for the duration of the share.
*/
fun shareIcs(
context: Context,
filename: String,
content: String,
chooserTitle: String,
) {
val dir = File(context.cacheDir, "calendar").apply { mkdirs() }
val file = File(dir, filename)
file.writeText(content)
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
val intent =
Intent(Intent.ACTION_SEND).apply {
type = "text/calendar"
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val chooser =
Intent
.createChooser(intent, chooserTitle)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(chooser)
}
@@ -38,10 +38,10 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.new_chess_game import com.vitorpamplona.amethyst.commons.resources.new_chess_game
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.ChessViewModelFactory
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size55Modifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory
/** /**
* Floating action button for creating new chess game challenges * Floating action button for creating new chess game challenges
@@ -37,6 +37,7 @@ import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor import com.vitorpamplona.amethyst.commons.ui.navigation.routes.routeFor
import com.vitorpamplona.amethyst.commons.ui.note.CheckHiddenFeedWatchBlockAndReport import com.vitorpamplona.amethyst.commons.ui.note.CheckHiddenFeedWatchBlockAndReport
import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent import com.vitorpamplona.amethyst.commons.ui.note.WatchNoteEvent
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.GalleryThumbnail
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent import com.vitorpamplona.quartz.nip18Reposts.GenericRepostEvent
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.note.share
import com.vitorpamplona.amethyst.commons.model.AddressableNote import com.vitorpamplona.amethyst.commons.model.AddressableNote
import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.note.share.QrPayloadMode
import com.vitorpamplona.amethyst.commons.ui.note.share.qrPayloadFor
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
@@ -466,7 +466,18 @@ assemblers, `EventSync`, …). Packages are renamed on the way:
wraps BiometricPrompt with the keyguard fallback. `CalendarTimeFormat` is shared on wraps BiometricPrompt with the keyguard fallback. `CalendarTimeFormat` is shared on
`DateSkeletonFormatter`. Moved: search, the long-form composer, the calendar collection `DateSkeletonFormatter`. Moved: search, the long-form composer, the calendar collection
editor, NWC setup and the zap-amount settings. 185 of 252 screens shared. editor, NWC setup and the zap-amount settings. 185 of 252 screens shared.
- **Next:** 67 screens remain, and what blocks them is the platform itself: - **Wave 10 (2026-09-30):** Home, Discover, the nest lobby and the Marmot group editor
first; then the date pickers on `LocalClock` (poll and zap-poll deadlines, schedule-at
with its presets), `FileSharer.shareTextFile` (the .ics export), `PhoneCalendar` (the
system event composer), `relativeTimeSpan` (`DateUtils` / `NSRelativeDateTimeFormatter`),
`rememberWindowViewModel` + `LocalWindowViewModelStoreOwner` (state shared across
destinations: the chess lobby and board, the Cordn group draft), `isHlsMedia` in
`commons.video`, `ChessEventBroadcaster` in commonMain and `supportsWritingAssistant` on
the host. `geo:` links go through `LocalUriHandler`. Moved: the note and poll composers,
voice reply, the calendar collections and event detail, the geocache detail and editor,
the live-activity channel, chess, Cordn group creation, the QR share and the compose
settings. 204 of 252 screens shared.
- **Next:** 48 screens remain, and what blocks them is the platform itself:
- `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents). - `ReactionsRow` (2.7k lines: pay-to-app, voice replies, wallet intents).
- The Nests activity and PiP. - The Nests activity and PiP.
- The call screens. - The call screens.
@@ -25,6 +25,7 @@ import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import okio.IOException import okio.IOException
@@ -64,3 +65,11 @@ class ChessDismissedGamesStore(
} }
} }
} }
/**
* Where the app keeps the dismissed-chess-games store.
*
* A new file rather than a migration of `chess_dismissed_games`: anyone who had dismissed a game
* sees it once more and dismisses it again.
*/
fun AppPreferenceStores.chessDismissedGamesStore(): ChessDismissedGamesStore = ChessDismissedGamesStore(getDataStore("chess_dismissed_games_v2"))
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import com.vitorpamplona.quartz.utils.currentTimeMillis
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
/** /**
@@ -84,7 +85,7 @@ class ChessEventBroadcaster(
val dummyFilter = val dummyFilter =
Filter( Filter(
kinds = listOf(JesterProtocol.KIND), kinds = listOf(JesterProtocol.KIND),
since = (System.currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results since = (currentTimeMillis() / 1000) + 3600, // 1 hour in future = no results
limit = 1, limit = 1,
) )
@@ -136,10 +137,10 @@ class ChessEventBroadcaster(
targetRelays: Set<NormalizedRelayUrl>, targetRelays: Set<NormalizedRelayUrl>,
timeoutMs: Long, timeoutMs: Long,
): Set<NormalizedRelayUrl> { ): Set<NormalizedRelayUrl> {
val startTime = System.currentTimeMillis() val startTime = currentTimeMillis()
val pollInterval = 200L val pollInterval = 200L
while (System.currentTimeMillis() - startTime < timeoutMs) { while (currentTimeMillis() - startTime < timeoutMs) {
val connected = client.connectedRelaysFlow().value val connected = client.connectedRelaysFlow().value
val targetConnected = targetRelays.intersect(connected) val targetConnected = targetRelays.intersect(connected)
@@ -148,7 +149,7 @@ class ChessEventBroadcaster(
} }
// At least one connected is good enough after half the timeout // At least one connected is good enough after half the timeout
if (targetConnected.isNotEmpty() && System.currentTimeMillis() - startTime > timeoutMs / 2) { if (targetConnected.isNotEmpty() && currentTimeMillis() - startTime > timeoutMs / 2) {
return targetConnected return targetConnected
} }
@@ -0,0 +1,75 @@
/*
* 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.video
/** The mime HLS playlists are normalized to — the value of media3's `MimeTypes.APPLICATION_M3U8`. */
const val HLS_MIME_TYPE = "application/x-mpegURL"
/**
* The mime a player should be handed for [mimeType] on [url]: an explicit mime wins (the four HLS
* aliases map onto [HLS_MIME_TYPE]); without one, a path-anchored `.m3u8` means HLS; otherwise
* null, letting the player infer the format.
*/
fun normalizeStreamMimeType(
mimeType: String?,
url: String? = null,
): String? {
if (!mimeType.isNullOrBlank()) {
return when (mimeType.lowercase()) {
"application/vnd.apple.mpegurl",
"application/x-mpegurl",
"audio/x-mpegurl",
"audio/mpegurl",
-> HLS_MIME_TYPE
else -> mimeType
}
}
if (url != null && hasM3u8PathExtension(url)) {
return HLS_MIME_TYPE
}
return null
}
/**
* Whether a URL plus its imeta mime identifies HLS, for code that holds a URL and a mime but no
* player item yet. Defined through [normalizeStreamMimeType] so it cannot drift from the mime that
* actually reaches the player.
*
* Both halves matter. A BUD-10 blossom playlist is `https://host/<sha256>` with no extension at all,
* so only the mime identifies it; and anchoring to the path stops `video.mp4?ref=a.m3u8` counting as
* HLS on the strength of its query string.
*
* Note this answers *is it HLS*, which callers use as a proxy for *is it live*: an on-demand HLS
* playlist also answers true. Liveness is only truly knowable from `#EXT-X-ENDLIST` once the
* playlist is loaded.
*/
fun isHlsMedia(
url: String,
mimeType: String?,
): Boolean = normalizeStreamMimeType(mimeType, url) == HLS_MIME_TYPE
// Restrict `.m3u8` matching to the path component so a query param or fragment that happens to
// mention .m3u8 (e.g. `?ref=a.m3u8`) on an MP4 URI doesn't misroute to the HLS source.
private fun hasM3u8PathExtension(uri: String): Boolean {
val path = uri.substringBefore('?').substringBefore('#')
return path.endsWith(".m3u8", ignoreCase = true)
}
@@ -91,6 +91,9 @@ interface AccountViewModelHost {
presetName: String, presetName: String,
): Result<AnonymizedResult> = Result.failure(UnsupportedOperationException("Voice anonymization is not available on this platform")) ): Result<AnonymizedResult> = Result.failure(UnsupportedOperationException("Voice anonymization is not available on this platform"))
/** Whether this build can offer an on-device writing assistant at all (it shows the setting). */
val supportsWritingAssistant: Boolean get() = false
/** A new on-device writing assistant, or null where the platform has none. */ /** A new on-device writing assistant, or null where the platform has none. */
fun createWritingAssistant(): WritingAssistant? = null fun createWritingAssistant(): WritingAssistant? = null
@@ -18,11 +18,11 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.service.playback.composable.mediaitem package com.vitorpamplona.amethyst.commons.video
import org.junit.Assert.assertFalse import kotlin.test.Test
import org.junit.Assert.assertTrue import kotlin.test.assertFalse
import org.junit.Test import kotlin.test.assertTrue
/** /**
* The two cases that motivated replacing the old `.m3u8`-substring predicate are * The two cases that motivated replacing the old `.m3u8`-substring predicate are
@@ -39,7 +39,7 @@ class IsHlsMediaTest {
assertTrue(isHlsMedia(blossom, "application/x-mpegurl")) assertTrue(isHlsMedia(blossom, "application/x-mpegurl"))
assertTrue(isHlsMedia(blossom, "application/vnd.apple.mpegurl")) assertTrue(isHlsMedia(blossom, "application/vnd.apple.mpegurl"))
assertFalse("no mime and no extension leaves nothing to go on", isHlsMedia(blossom, null)) assertFalse(isHlsMedia(blossom, null), "no mime and no extension leaves nothing to go on")
} }
@Test @Test
@@ -54,7 +54,7 @@ class IsHlsMediaTest {
fun recognisesAPlainM3u8Path() { fun recognisesAPlainM3u8Path() {
assertTrue(isHlsMedia("https://host/live.m3u8", null)) assertTrue(isHlsMedia("https://host/live.m3u8", null))
assertTrue(isHlsMedia("https://host/live.M3U8", null)) assertTrue(isHlsMedia("https://host/live.M3U8", null))
assertTrue("query strings don't hide the path", isHlsMedia("https://host/live.m3u8?vt=abc", null)) assertTrue(isHlsMedia("https://host/live.m3u8?vt=abc", null), "query strings don't hide the path")
} }
@Test @Test
@@ -20,6 +20,7 @@
*/ */
package com.vitorpamplona.amethyst.commons.ui.components package com.vitorpamplona.amethyst.commons.ui.components
import android.content.Context
import android.content.Intent import android.content.Intent
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -33,8 +34,17 @@ import java.util.zip.ZipOutputStream
@Composable @Composable
actual fun rememberFileSharer(): FileSharer { actual fun rememberFileSharer(): FileSharer {
val context = LocalContext.current val context = LocalContext.current
return remember(context) { return remember(context) { AndroidFileSharer(context) }
FileSharer { zipName, files, title -> }
private class AndroidFileSharer(
val context: Context,
) : FileSharer {
override fun shareTextFilesAsZip(
zipName: String,
files: List<Pair<String, String>>,
title: String,
) {
val zipFile = File(context.cacheDir, zipName) val zipFile = File(context.cacheDir, zipName)
ZipOutputStream(FileOutputStream(zipFile)).use { zip -> ZipOutputStream(FileOutputStream(zipFile)).use { zip ->
files.forEach { (name, content) -> files.forEach { (name, content) ->
@@ -57,5 +67,29 @@ actual fun rememberFileSharer(): FileSharer {
context.startActivity(Intent.createChooser(sendIntent, title)) context.startActivity(Intent.createChooser(sendIntent, title))
} }
/**
* The file lives in `cacheDir/shared/`, covered by the `<cache-path>` entry in
* `file_paths.xml`; the receiver's read grant lasts only for the share.
*/
override fun shareTextFile(
fileName: String,
mimeType: String,
content: String,
title: String,
) {
val dir = File(context.cacheDir, "shared").apply { mkdirs() }
val file = File(dir, fileName)
file.writeText(content)
val uri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
val intent =
Intent(Intent.ACTION_SEND).apply {
type = mimeType
putExtra(Intent.EXTRA_STREAM, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
context.startActivity(Intent.createChooser(intent, title).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
} }
} }
@@ -18,17 +18,26 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
import android.content.ActivityNotFoundException import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.provider.CalendarContract import android.provider.CalendarContract
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds import com.vitorpamplona.amethyst.commons.model.nip52Calendar.parseIsoDateToUnixSeconds
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
@Composable
actual fun rememberPhoneCalendar(): PhoneCalendar {
val context = LocalContext.current
return remember(context) { PhoneCalendar { addToPhoneCalendar(context, it) } }
}
/** /**
* Opens the system "New Event" composer (Google Calendar / Samsung / iCloud / etc.) pre-populated * Opens the system "New Event" composer (Google Calendar / Samsung / iCloud / etc.) pre-populated
* with this appointment's title, time, location, and description. The user sees their normal * with this appointment's title, time, location, and description. The user sees their normal
@@ -38,7 +47,7 @@ import com.vitorpamplona.quartz.nip52Calendar.appt.time.CalendarTimeSlotEvent
* Returns true if a calendar app handled the intent, false if no handler was found — the caller * Returns true if a calendar app handled the intent, false if no handler was found — the caller
* can decide whether to fall back to the .ics share path. * can decide whether to fall back to the .ics share path.
*/ */
fun addToPhoneCalendar( private fun addToPhoneCalendar(
context: Context, context: Context,
event: Event, event: Event,
): Boolean { ): Boolean {
@@ -0,0 +1,36 @@
/*
* 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.screen.loggedIn.calendars
import android.text.format.DateUtils
actual suspend fun relativeTimeSpan(
targetSeconds: Long,
nowSeconds: Long,
dayResolution: Boolean,
): String =
DateUtils
.getRelativeTimeSpanString(
targetSeconds * 1000L,
nowSeconds * 1000L,
if (dayResolution) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.actions.mediaServers package com.vitorpamplona.amethyst.commons.ui.actions.mediaServers
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.actions.uploads package com.vitorpamplona.amethyst.commons.ui.actions.uploads
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.actions.uploads package com.vitorpamplona.amethyst.commons.ui.actions.uploads
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.actions.uploads package com.vitorpamplona.amethyst.commons.ui.actions.uploads
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -32,7 +32,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.audio.VoicePreset
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
@@ -44,7 +43,6 @@ fun VoicePresetSelector(
onPresetSelected: (VoicePreset) -> Unit, onPresetSelected: (VoicePreset) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val context = LocalContext.current
val isProcessing = processingPreset != null val isProcessing = processingPreset != null
Row( Row(
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
/** Hands generated files to the platform's share sheet. */ /** Hands generated files to the platform's share sheet. */
fun interface FileSharer { interface FileSharer {
/** /**
* Packs [files] (name to text content) into a zip named [zipName] and shares it, behind a * Packs [files] (name to text content) into a zip named [zipName] and shares it, behind a
* chooser titled [title] where the platform has one. * chooser titled [title] where the platform has one.
@@ -33,6 +33,17 @@ fun interface FileSharer {
files: List<Pair<String, String>>, files: List<Pair<String, String>>,
title: String, title: String,
) )
/**
* Writes [content] to a file named [fileName] and shares it as [mimeType], behind a chooser
* titled [title] where the platform has one.
*/
fun shareTextFile(
fileName: String,
mimeType: String,
content: String,
title: String,
)
} }
/** /**
@@ -67,6 +67,7 @@ inline fun <reified VM : ViewModel> rememberViewModel(
* when the store has no view model yet. * when the store has no view model yet.
*/ */
@Composable @Composable
@PublishedApi
internal fun <VM : ViewModel> rememberLambdaFactory(factory: () -> VM): ViewModelProvider.Factory { internal fun <VM : ViewModel> rememberLambdaFactory(factory: () -> VM): ViewModelProvider.Factory {
val currentFactory by rememberUpdatedState(factory) val currentFactory by rememberUpdatedState(factory)
return remember { return remember {
@@ -0,0 +1,64 @@
/*
* 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
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
import kotlin.reflect.KClass
/**
* The store that outlives every navigation destination in this window: Android's Activity. The
* platform entry point provides it above the nav host, whose destinations each get a store of
* their own.
*/
val LocalWindowViewModelStoreOwner: ProvidableCompositionLocal<ViewModelStoreOwner?> = staticCompositionLocalOf { null }
/**
* A view model shared by every destination of this window, for state that has to survive
* navigating between screens (a draft filled in over two screens, a game the lobby and the board
* both watch). Without a window store it falls back to [rememberViewModel], scoped to the caller.
*/
@Composable
fun <VM : ViewModel> rememberWindowViewModel(
modelClass: KClass<VM>,
key: String,
factory: ViewModelProvider.Factory,
): VM {
val owner = LocalWindowViewModelStoreOwner.current ?: return rememberViewModel(modelClass, key, factory)
return remember(owner, key) { ViewModelProvider.create(owner, factory).get(key, modelClass) }
}
@Composable
inline fun <reified VM : ViewModel> rememberWindowViewModel(
key: String,
factory: ViewModelProvider.Factory,
): VM = rememberWindowViewModel(VM::class, key, factory)
@Composable
inline fun <reified VM : ViewModel> rememberWindowViewModel(
key: String,
noinline factory: () -> VM,
): VM = rememberWindowViewModel(VM::class, key, rememberLambdaFactory(factory))
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.polls package com.vitorpamplona.amethyst.commons.ui.note.creators.polls
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.polls package com.vitorpamplona.amethyst.commons.ui.note.creators.polls
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.scheduling package com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling
import android.text.format.DateFormat
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -54,7 +53,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
@@ -76,18 +74,15 @@ import com.vitorpamplona.amethyst.commons.resources.schedule_post_time_label
import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_multi import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_multi
import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_single import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_single
import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_title import com.vitorpamplona.amethyst.commons.resources.schedule_post_warning_title
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.note.timeAheadNoDot
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.time.DayOfWeek import com.vitorpamplona.quartz.utils.currentTimeMillis
import java.time.Instant
import java.time.LocalDate
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZoneOffset
import java.time.temporal.TemporalAdjusters
/** /**
* Two-stage date + time picker for scheduling a post for future publication. * Two-stage date + time picker for scheduling a post for future publication.
@@ -107,29 +102,26 @@ fun ScheduleAtPicker(
var showDatePicker by remember { mutableStateOf(false) } var showDatePicker by remember { mutableStateOf(false) }
var showTimePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) }
val currentTime = // The scheduled time as a local wall-clock time: seconds since the epoch shifted by this zone's offset.
Instant val localSeconds = scheduledForSec + LocalClock.utcOffsetSeconds(scheduledForSec)
.ofEpochMilli(scheduledForSec * 1000) val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY)
.atZone(ZoneId.systemDefault()) val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year
.toLocalDateTime()
val datePickerState = val datePickerState =
rememberDatePickerState( rememberDatePickerState(
initialSelectedDateMillis = scheduledForSec * 1000, initialSelectedDateMillis = scheduledForSec * 1000,
yearRange = currentTime.year..2050, yearRange = currentYear..2050,
selectableDates = selectableDates =
object : SelectableDates { object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= System.currentTimeMillis() - 86_400_000 override fun isSelectableDate(utcTimeMillis: Long): Boolean = utcTimeMillis >= currentTimeMillis() - 86_400_000
}, },
) )
val context = LocalContext.current
val timePickerState = val timePickerState =
rememberTimePickerState( rememberTimePickerState(
initialHour = currentTime.hour, initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
initialMinute = currentTime.minute, initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
is24Hour = DateFormat.is24HourFormat(context), is24Hour = rememberIs24HourClock(),
) )
Column(Modifier.fillMaxWidth()) { Column(Modifier.fillMaxWidth()) {
@@ -220,8 +212,7 @@ fun ScheduleAtPicker(
(timePickerState.minute * TimeUtils.ONE_MINUTE) (timePickerState.minute * TimeUtils.ONE_MINUTE)
} ?: TimeUtils.oneDayAhead() } ?: TimeUtils.oneDayAhead()
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now()) val rawSec = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
val rawSec = datetimeLocalTimeZone - offset.totalSeconds
onChanged(roundUpToNextQuarterHour(rawSec)) onChanged(roundUpToNextQuarterHour(rawSec))
showTimePicker = false showTimePicker = false
@@ -300,24 +291,22 @@ private fun PresetChips(onPick: (Long) -> Unit) {
} }
} }
private fun presetInOneHour(): Long = (System.currentTimeMillis() / 1000) + 3600 private fun presetInOneHour(): Long = (currentTimeMillis() / 1000) + 3600
private fun presetTomorrowMorning(): Long { private fun presetTomorrowMorning(): Long = nineAm(LocalClock.today().plusDays(1))
val zone = ZoneId.systemDefault()
val tomorrow9am = LocalDate.now(zone).plusDays(1).atTime(LocalTime.of(9, 0))
return tomorrow9am.atZone(zone).toEpochSecond()
}
private fun presetNextMondayMorning(): Long { private fun presetNextMondayMorning(): Long {
val zone = ZoneId.systemDefault()
// Always step at least one day forward — if today is Monday, return next Monday. // Always step at least one day forward — if today is Monday, return next Monday.
val target = val tomorrow = LocalClock.today().plusDays(1)
LocalDate val monday = 1
.now(zone) return nineAm(tomorrow.plusDays((monday - tomorrow.dayOfWeek()).mod(7)))
.plusDays(1) }
.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY))
.atTime(LocalTime.of(9, 0)) /** 09:00 local time on [date], read with the offset in force at 09:00, not at midnight. */
return target.atZone(zone).toEpochSecond() private fun nineAm(date: SearchDate): Long {
val midnight = LocalClock.startOfDay(date)
val guess = midnight + 9 * TimeUtils.ONE_HOUR
return guess + LocalClock.utcOffsetSeconds(midnight) - LocalClock.utcOffsetSeconds(guess)
} }
/** /**
@@ -326,9 +315,9 @@ private fun presetNextMondayMorning(): Long {
* (rare — only if the user picks the exact current quarter-hour), bump forward * (rare — only if the user picks the exact current quarter-hour), bump forward
* one slot. * one slot.
*/ */
internal fun roundUpToNextQuarterHour(epochSec: Long): Long { fun roundUpToNextQuarterHour(epochSec: Long): Long {
val quarter = 15 * 60L val quarter = 15 * 60L
val rounded = ((epochSec + quarter - 1) / quarter) * quarter val rounded = ((epochSec + quarter - 1) / quarter) * quarter
val nowSec = System.currentTimeMillis() / 1000 val nowSec = currentTimeMillis() / 1000
return if (rounded <= nowSec) rounded + quarter else rounded return if (rounded <= nowSec) rounded + quarter else rounded
} }
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.zappolls package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
import android.text.format.DateFormat
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -45,7 +44,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
@@ -57,13 +55,14 @@ import com.vitorpamplona.amethyst.commons.resources.confirm
import com.vitorpamplona.amethyst.commons.resources.next import com.vitorpamplona.amethyst.commons.resources.next
import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time import com.vitorpamplona.amethyst.commons.resources.poll_closing_date_time
import com.vitorpamplona.amethyst.commons.resources.poll_closing_in import com.vitorpamplona.amethyst.commons.resources.poll_closing_in
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.note.timeAheadNoDot
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
import java.time.Instant import com.vitorpamplona.quartz.utils.currentTimeMillis
import java.time.ZoneId
import java.time.ZoneOffset
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -72,26 +71,28 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) {
var showTimePicker by remember { mutableStateOf(false) } var showTimePicker by remember { mutableStateOf(false) }
// Get current time details // Get current time details
val currentTime = Instant.ofEpochMilli(model.zapPollClosedAt * 1000).atZone(ZoneId.systemDefault()).toLocalDateTime() // The deadline as a local wall-clock time: seconds since the epoch shifted by this zone's offset.
val localSeconds = model.zapPollClosedAt + LocalClock.utcOffsetSeconds(model.zapPollClosedAt)
val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY)
val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year
val datePickerState = val datePickerState =
rememberDatePickerState( rememberDatePickerState(
initialSelectedDateMillis = model.zapPollClosedAt * 1000, initialSelectedDateMillis = model.zapPollClosedAt * 1000,
yearRange = currentTime.year..2050, yearRange = currentYear..2050,
selectableDates = selectableDates =
object : SelectableDates { object : SelectableDates {
override fun isSelectableDate(utcTimeMillis: Long): Boolean { override fun isSelectableDate(utcTimeMillis: Long): Boolean {
// Only allow today and future dates // Only allow today and future dates
return utcTimeMillis >= System.currentTimeMillis() - 86400000 // minus 24h buffer return utcTimeMillis >= currentTimeMillis() - 86400000 // minus 24h buffer
} }
}, },
) )
val context = LocalContext.current
val timePickerState = val timePickerState =
rememberTimePickerState( rememberTimePickerState(
initialHour = currentTime.hour, initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
initialMinute = currentTime.minute, initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
is24Hour = DateFormat.is24HourFormat(context), is24Hour = rememberIs24HourClock(),
) )
OutlinedCard( OutlinedCard(
@@ -148,10 +149,7 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) {
(timePickerState.minute * TimeUtils.ONE_MINUTE) (timePickerState.minute * TimeUtils.ONE_MINUTE)
} ?: TimeUtils.oneDayAhead() } ?: TimeUtils.oneDayAhead()
// Get the offset from UTC for the current instant in the local time zone model.zapPollClosedAt = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
model.zapPollClosedAt = datetimeLocalTimeZone - offset.totalSeconds
showTimePicker = false showTimePicker = false
}, },
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.zappolls package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.zappolls package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@@ -67,7 +67,7 @@ fun ZapPollOption(
}, },
label = { label = {
Text( Text(
text = stringRes(Res.string.poll_option_index).format(optionIndex + 1), text = stringRes(Res.string.poll_option_index, (optionIndex + 1).toString()),
color = MaterialTheme.colorScheme.placeholderText, color = MaterialTheme.colorScheme.placeholderText,
) )
}, },
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.creators.zappolls package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.share package com.vitorpamplona.amethyst.commons.ui.note.share
import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.share package com.vitorpamplona.amethyst.commons.ui.note.share
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.share package com.vitorpamplona.amethyst.commons.ui.note.share
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -57,10 +57,10 @@ import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons import com.vitorpamplona.amethyst.commons.ui.components.collectContentWarningReasons
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.NoteAuthorPicture import com.vitorpamplona.amethyst.commons.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.GalleryThumbnail
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery.UrlImageView
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.GalleryThumbnail
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery.UrlImageView
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -32,8 +32,8 @@ import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar import com.vitorpamplona.amethyst.commons.ui.platform.AppBottomBar
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
/** /**
* Top-level screen for browsing NIP-52 kind-31924 calendars (collections of appointments). Reuses * Top-level screen for browsing NIP-52 kind-31924 calendars (collections of appointments). Reuses
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -42,7 +42,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@@ -62,6 +61,7 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_s
import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_title import com.vitorpamplona.amethyst.commons.resources.calendar_empty_collections_title
import com.vitorpamplona.amethyst.commons.resources.calendar_export_event import com.vitorpamplona.amethyst.commons.resources.calendar_export_event
import com.vitorpamplona.amethyst.commons.resources.calendar_export_share_title import com.vitorpamplona.amethyst.commons.resources.calendar_export_share_title
import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys import com.vitorpamplona.amethyst.commons.ui.feeds.ScrollStateKeys
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
@@ -157,7 +157,7 @@ fun CalendarCollectionCard(
val title = remember(note.idHex) { event.title() } val title = remember(note.idHex) { event.title() }
val description = remember(note.idHex) { event.content.take(180) } val description = remember(note.idHex) { event.content.take(180) }
val count = remember(note.idHex) { event.calendarEventAddresses().size } val count = remember(note.idHex) { event.calendarEventAddresses().size }
val context = LocalContext.current val fileSharer = rememberFileSharer()
val icsChooserTitle = stringRes(Res.string.calendar_export_share_title) val icsChooserTitle = stringRes(Res.string.calendar_export_share_title)
// Calendar collections are addressable (kind 31924); route to the dedicated // Calendar collections are addressable (kind 31924); route to the dedicated
@@ -223,7 +223,7 @@ fun CalendarCollectionCard(
val members = collectMembers(event) val members = collectMembers(event)
val ics = IcsExport.calendarToIcs(event, members, TimeUtils.now()) val ics = IcsExport.calendarToIcs(event, members, TimeUtils.now())
val filename = IcsExport.calendarFilename(event) val filename = IcsExport.calendarFilename(event)
shareIcs(context, filename, ics, icsChooserTitle) fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle)
}) { }) {
Icon( Icon(
symbol = MaterialSymbols.Share, symbol = MaterialSymbols.Share,
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
import android.text.format.DateUtils
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
@@ -29,6 +28,7 @@ import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing
import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing_with_end import com.vitorpamplona.amethyst.commons.resources.calendar_relative_ongoing_with_end
import com.vitorpamplona.amethyst.commons.ui.loadStringRes import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.relativeTimeSpan
import com.vitorpamplona.quartz.utils.TimeUtils import com.vitorpamplona.quartz.utils.TimeUtils
/** /**
@@ -36,14 +36,14 @@ import com.vitorpamplona.quartz.utils.TimeUtils
* label for an appointment. Returns null when the event has no parseable start (nothing to * label for an appointment. Returns null when the event has no parseable start (nothing to
* anchor a relative phrase to). * anchor a relative phrase to).
* *
* Uses [DateUtils.getRelativeTimeSpanString] for the underlying minute/hour/day phrasing — that * Uses the platform's relative-time formatter ([relativeTimeSpan]) for the underlying minute/hour/day phrasing — that
* helper is locale-aware and ages from "just now" through "in N days" to absolute date for * helper is locale-aware and ages from "just now" through "in N days" to absolute date for
* far-out events. For all-day events we extend the resolution to DAY so we get "tomorrow", * far-out events. For all-day events we extend the resolution to DAY so we get "tomorrow",
* "in 3 days" instead of an hour-precision phrase that would lie about the start moment. * "in 3 days" instead of an hour-precision phrase that would lie about the start moment.
* *
* Ongoing events (start ≤ now ≤ end) get a composite "Happening now · ends in X" so a user * Ongoing events (start ≤ now ≤ end) get a composite "Happening now · ends in X" so a user
* mid-event sees how much time is left rather than the misleading "started X minutes ago" that * mid-event sees how much time is left rather than the misleading "started X minutes ago" that
* DateUtils would produce on its own. * the formatter would produce on its own.
*/ */
suspend fun relativeTimeLabel( suspend fun relativeTimeLabel(
view: CalendarAppointmentView, view: CalendarAppointmentView,
@@ -54,31 +54,11 @@ suspend fun relativeTimeLabel(
if (end != null && start <= nowSeconds && nowSeconds <= end) { if (end != null && start <= nowSeconds && nowSeconds <= end) {
val ongoing = loadStringRes(Res.string.calendar_relative_ongoing) val ongoing = loadStringRes(Res.string.calendar_relative_ongoing)
val endsIn = val endsIn = relativeTimeSpan(end, nowSeconds, view.isAllDay)
DateUtils
.getRelativeTimeSpanString(
end * 1000L,
nowSeconds * 1000L,
if (view.isAllDay) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
return loadStringRes(Res.string.calendar_relative_ongoing_with_end, ongoing, endsIn) return loadStringRes(Res.string.calendar_relative_ongoing_with_end, ongoing, endsIn)
} }
val minResolution = return relativeTimeSpan(start, nowSeconds, view.isAllDay)
if (view.isAllDay) {
DateUtils.DAY_IN_MILLIS
} else {
DateUtils.MINUTE_IN_MILLIS
}
return DateUtils
.getRelativeTimeSpanString(
start * 1000L,
nowSeconds * 1000L,
minResolution,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
} }
/** /**
@@ -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.screen.loggedIn.calendars
import androidx.compose.runtime.Composable
import com.vitorpamplona.quartz.nip01Core.core.Event
/** Hands a NIP-52 appointment to the device's own calendar app. */
fun interface PhoneCalendar {
/**
* Opens the calendar's "New Event" composer pre-filled with [event]. Returns false when there
* is no calendar app to take it (or [event] is not an appointment), so the caller can fall
* back to sharing an .ics file.
*/
fun add(event: Event): Boolean
}
/**
* Android opens the system calendar composer. Desktop and iOS have no calendar composer wired
* yet and always report false.
*/
@Composable
expect fun rememberPhoneCalendar(): PhoneCalendar
@@ -18,18 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
/** /**
* Where Android keeps the dismissed-chess-games store. * The platform's own phrase for how far [targetSeconds] is from [nowSeconds] ("in 2 hr.",
* * "5 min. ago", "tomorrow"), no finer than a day when [dayResolution] is set. Android asks
* A new file rather than a migration of `chess_dismissed_games`: the dismissed * `DateUtils`, iOS `NSRelativeDateTimeFormatter`; Desktop has no such formatter and writes a
* list is a convenience, and chess has few enough users that carrying the old * compact signed span.
* data over is not worth the code. Anyone who had dismissed a game sees it once
* more and dismisses it again.
*/ */
internal fun chessDismissedGamesData(stores: AppPreferenceStores): DataStore<Preferences> = stores.getDataStore("chess_dismissed_games_v2") expect suspend fun relativeTimeSpan(
targetSeconds: Long,
nowSeconds: Long,
dayResolution: Boolean,
): String
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -18,10 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail
import android.content.Intent
import android.net.Uri
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -52,11 +50,10 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -91,6 +88,8 @@ import com.vitorpamplona.amethyst.commons.resources.calendar_untitled
import com.vitorpamplona.amethyst.commons.resources.edit_calendar_event import com.vitorpamplona.amethyst.commons.resources.edit_calendar_event
import com.vitorpamplona.amethyst.commons.resources.route_calendar_event_detail import com.vitorpamplona.amethyst.commons.resources.route_calendar_event_detail
import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.commons.ui.components.rememberFileSharer
import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture
@@ -100,16 +99,15 @@ import com.vitorpamplona.amethyst.commons.ui.note.platform.ReactionsRow
import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter import com.vitorpamplona.amethyst.commons.ui.note.rememberTimeOfDayFormatter
import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow import com.vitorpamplona.amethyst.commons.ui.note.types.CalendarRsvpRow
import com.vitorpamplona.amethyst.commons.ui.pluralStringRes import com.vitorpamplona.amethyst.commons.ui.pluralStringRes
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatCalendarRange
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.formatLongDate
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberPhoneCalendar
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp import com.vitorpamplona.amethyst.commons.ui.theme.Size30dp
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.addToPhoneCalendar
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.shareIcs
import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
@@ -177,7 +175,9 @@ fun CalendarEventDetailScreen(
} }
}, },
actions = { actions = {
val context = LocalContext.current val phoneCalendar = rememberPhoneCalendar()
val fileSharer = rememberFileSharer()
val textSharer = rememberTextSharer()
val icsChooserTitle = stringRes(Res.string.calendar_export_share_title) val icsChooserTitle = stringRes(Res.string.calendar_export_share_title)
// Two share modes: // Two share modes:
// - .ics for non-nostr calendar apps (Google Calendar / iOS / Outlook) // - .ics for non-nostr calendar apps (Google Calendar / iOS / Outlook)
@@ -190,10 +190,10 @@ fun CalendarEventDetailScreen(
// has no calendar app registered for ACTION_INSERT (rare; Wear OS, some // has no calendar app registered for ACTION_INSERT (rare; Wear OS, some
// GrapheneOS profiles). // GrapheneOS profiles).
IconButton(onClick = { IconButton(onClick = {
if (!addToPhoneCalendar(context, event)) { if (!phoneCalendar.add(event)) {
val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now()) val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now())
val filename = IcsExport.appointmentFilename(event, targetAddress) val filename = IcsExport.appointmentFilename(event, targetAddress)
shareIcs(context, filename, ics, icsChooserTitle) fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle)
} }
}) { }) {
Icon( Icon(
@@ -206,7 +206,7 @@ fun CalendarEventDetailScreen(
IconButton(onClick = { IconButton(onClick = {
val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now()) val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now())
val filename = IcsExport.appointmentFilename(event, targetAddress) val filename = IcsExport.appointmentFilename(event, targetAddress)
shareIcs(context, filename, ics, icsChooserTitle) fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle)
}) { }) {
Icon( Icon(
symbol = MaterialSymbols.Share, symbol = MaterialSymbols.Share,
@@ -224,15 +224,7 @@ fun CalendarEventDetailScreen(
targetAddress.dTag, targetAddress.dTag,
null, null,
) )
val intent = textSharer.share("nostr:$naddr", null, shareTitle)
Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, "nostr:$naddr")
context.startActivity(
Intent
.createChooser(intent, shareTitle)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
)
}) { }) {
Icon( Icon(
symbol = MaterialSymbols.AutoMirrored.Send, symbol = MaterialSymbols.AutoMirrored.Send,
@@ -347,7 +339,6 @@ private fun EventBody(
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
) )
} }
val context = LocalContext.current
val timeFormatter = rememberTimeOfDayFormatter() val timeFormatter = rememberTimeOfDayFormatter()
formatCalendarRange(note, timeFormatter)?.let { range -> formatCalendarRange(note, timeFormatter)?.let { range ->
Text( Text(
@@ -595,7 +586,7 @@ private fun HeroImage(
@Composable @Composable
private fun LocationRow(location: String) { private fun LocationRow(location: String) {
val context = LocalContext.current val uriHandler = LocalUriHandler.current
// NIP-52 `location` is free-text — a place name, an address, OR a meeting URL (Zoom, Jitsi, // NIP-52 `location` is free-text — a place name, an address, OR a meeting URL (Zoom, Jitsi,
// a livestream link). Treat http(s) URLs as web links so they open in the browser; otherwise // a livestream link). Treat http(s) URLs as web links so they open in the browser; otherwise
// hand to the maps app via the geo: intent. // hand to the maps app via the geo: intent.
@@ -607,17 +598,10 @@ private fun LocationRow(location: String) {
.clickable { .clickable {
runCatching { runCatching {
val trimmed = location.trim() val trimmed = location.trim()
val intent = // `geo:0,0?q=<location>` is the Android geo URI; the user's installed maps
if (isUrl) { // app handles it. runCatching swallows the failure when no handler is
Intent(Intent.ACTION_VIEW, trimmed.toUri())
} else {
// `geo:0,0?q=<location>` is the Android geo intent; the user's
// installed maps app handles it.
Intent(Intent.ACTION_VIEW, "geo:0,0?q=${Uri.encode(trimmed)}".toUri())
}
// runCatching swallows ActivityNotFoundException when no handler is
// installed — we don't have anywhere useful to fall back to. // installed — we don't have anywhere useful to fall back to.
context.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) uriHandler.openUri(if (isUrl) trimmed else "geo:0,0?q=${encodeUriComponent(trimmed)}")
} }
}.padding(vertical = 4.dp), }.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -905,3 +889,18 @@ private fun rememberCalendarsContaining(targetAddress: Address): State<List<Cale
return calendars.collectAsStateWithLifecycle(emptyList()) return calendars.collectAsStateWithLifecycle(emptyList())
} }
/** Percent-encodes everything but the unreserved characters, as Android's `Uri.encode` does. */
private fun encodeUriComponent(text: String): String {
val sb = StringBuilder(text.length)
for (b in text.encodeToByteArray()) {
val c = (b.toInt() and 0xFF).toChar()
if (c in 'a'..'z' || c in 'A'..'Z' || c in '0'..'9' || c in "_-!.~'()*") {
sb.append(c)
} else {
sb.append('%')
sb.append((b.toInt() and 0xFF).toString(16).padStart(2, '0').uppercase())
}
}
return sb.toString()
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -48,9 +47,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.User import com.vitorpamplona.amethyst.commons.model.User
@@ -75,6 +72,7 @@ import com.vitorpamplona.amethyst.commons.resources.cordn_info_has_key_package
import com.vitorpamplona.amethyst.commons.resources.cordn_info_remove_member import com.vitorpamplona.amethyst.commons.resources.cordn_info_remove_member
import com.vitorpamplona.amethyst.commons.resources.cordn_member_count import com.vitorpamplona.amethyst.commons.resources.cordn_member_count
import com.vitorpamplona.amethyst.commons.ui.components.EmptyState import com.vitorpamplona.amethyst.commons.ui.components.EmptyState
import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.TopBarWithBackButton
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
@@ -367,13 +365,10 @@ private fun MemberRow(
* a roster built under another key. * a roster built under another key.
*/ */
@Composable @Composable
fun rememberCordnGroupDraft(accountViewModel: AccountViewModel): CordnGroupDraft { fun rememberCordnGroupDraft(accountViewModel: AccountViewModel): CordnGroupDraft =
val activity = LocalActivity.current as FragmentActivity rememberWindowViewModel(
return viewModel(
viewModelStoreOwner = activity,
key = "CordnGroupDraft-${accountViewModel.account.signer.pubKey}", key = "CordnGroupDraft-${accountViewModel.account.signer.pubKey}",
) ) { CordnGroupDraft() }
}
/** /**
* What each coordinator this account uses could do with [roster]. * What each coordinator this account uses could do with [roster].
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -28,9 +28,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.Note import com.vitorpamplona.amethyst.commons.model.Note
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.commons.ui.note.LoadLiveActivityChannel
@@ -38,13 +38,13 @@ import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.feed.Refreshi
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.dal.ChannelFeedViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.datasource.ChannelFilterAssemblerSubscription
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.ShowVideoStreaming
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamGoalHeader
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamTopZappers
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.ChannelNewMessageViewModel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.send.EditFieldRow
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.lobby.NestJoinCard import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.nests.room.lobby.NestJoinCard
import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamGoalHeader
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveStreamTopZappers
import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Address
@Composable @Composable
@@ -77,7 +77,7 @@ fun PrepareChannelViewModels(
nav: INav, nav: INav,
) { ) {
val feedViewModel: ChannelFeedViewModel = val feedViewModel: ChannelFeedViewModel =
viewModel( rememberViewModel(
key = baseChannel.address.toValue() + "ChannelFeedViewModel", key = baseChannel.address.toValue() + "ChannelFeedViewModel",
factory = factory =
ChannelFeedViewModel.Factory( ChannelFeedViewModel.Factory(
@@ -86,7 +86,7 @@ fun PrepareChannelViewModels(
), ),
) )
val channelScreenModel: ChannelNewMessageViewModel = viewModel() val channelScreenModel: ChannelNewMessageViewModel = rememberViewModel { ChannelNewMessageViewModel() }
channelScreenModel.init(accountViewModel) channelScreenModel.init(accountViewModel)
channelScreenModel.load(baseChannel) channelScreenModel.load(baseChannel)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -27,12 +27,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.OnlineChecker import com.vitorpamplona.amethyst.commons.service.OnlineStatusCache
import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold import com.vitorpamplona.amethyst.commons.ui.layouts.DisappearingScaffold
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.LoadLiveActivityChannel import com.vitorpamplona.amethyst.commons.ui.note.LoadLiveActivityChannel
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar
import com.vitorpamplona.quartz.nip01Core.core.Address import com.vitorpamplona.quartz.nip01Core.core.Address
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
@@ -55,7 +55,7 @@ fun LiveActivityChannelScreen(
LoadLiveActivityChannel(channelId) { LoadLiveActivityChannel(channelId) {
LaunchedEffect(it.info) { LaunchedEffect(it.info) {
it.info?.streaming()?.let { it.info?.streaming()?.let {
OnlineChecker.resetIfOfflineToRetry(it) OnlineStatusCache.resetIfOfflineToRetry(it)
} }
} }
LiveActivityTopBar(it, accountViewModel, nav) LiveActivityTopBar(it, accountViewModel, nav)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -47,13 +47,14 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.model.navigation.Route import com.vitorpamplona.amethyst.commons.model.navigation.Route
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
import com.vitorpamplona.amethyst.commons.nip53LiveActivities.TopZapperEntry import com.vitorpamplona.amethyst.commons.nip53LiveActivities.TopZapperEntry
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.chat_zap_anonymous import com.vitorpamplona.amethyst.commons.resources.chat_zap_anonymous
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon import com.vitorpamplona.amethyst.commons.ui.note.ZapIcon
@@ -64,7 +65,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size24dp
import com.vitorpamplona.amethyst.commons.util.showAmountInteger import com.vitorpamplona.amethyst.commons.util.showAmountInteger
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.LiveStreamTopZappersViewModel import com.vitorpamplona.amethyst.commons.viewmodels.LiveStreamTopZappersViewModel
import java.math.BigDecimal import com.vitorpamplona.quartz.utils.BigDecimal
import kotlin.reflect.KClass
/** /**
* Horizontally-scrollable leaderboard of the top zappers on a live stream. Matches * Horizontally-scrollable leaderboard of the top zappers on a live stream. Matches
@@ -80,12 +82,15 @@ fun LiveStreamTopZappers(
nav: INav, nav: INav,
) { ) {
val topZappersVm: LiveStreamTopZappersViewModel = val topZappersVm: LiveStreamTopZappersViewModel =
viewModel( rememberViewModel(
key = "TopZappers-${channel.address.toValue()}", key = "TopZappers-${channel.address.toValue()}",
factory = factory =
object : ViewModelProvider.Factory { object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T = LiveStreamTopZappersViewModel(channel) as T override fun <T : ViewModel> create(
modelClass: KClass<T>,
extras: CreationExtras,
): T = LiveStreamTopZappersViewModel(channel) as T
}, },
) )
@@ -164,7 +169,7 @@ private fun ZapperPill(
} }
ZapIcon(Size16Modifier, BitcoinOrange) ZapIcon(Size16Modifier, BitcoinOrange)
Text( Text(
text = showAmountInteger(BigDecimal.valueOf(entry.totalSats)), text = showAmountInteger(BigDecimal(entry.totalSats)),
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
) )
Spacer(Modifier.padding(start = 2.dp)) Spacer(Modifier.padding(start = 2.dp))
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -75,12 +72,13 @@ import com.vitorpamplona.amethyst.commons.resources.chess_game_waiting
import com.vitorpamplona.amethyst.commons.resources.chess_loading_game import com.vitorpamplona.amethyst.commons.resources.chess_loading_game
import com.vitorpamplona.amethyst.commons.resources.go_back import com.vitorpamplona.amethyst.commons.resources.go_back
import com.vitorpamplona.amethyst.commons.resources.relay_settings import com.vitorpamplona.amethyst.commons.resources.relay_settings
import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource.ChessSubscription
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier import com.vitorpamplona.amethyst.commons.ui.theme.Height4dpModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
import com.vitorpamplona.quartz.nip64Chess.ChessGameNameGenerator import com.vitorpamplona.quartz.nip64Chess.ChessGameNameGenerator
/** /**
@@ -100,10 +98,8 @@ fun ChessGameScreen(
nav: INav, nav: INav,
) { ) {
// Scope ViewModel to Activity so it's shared between lobby and game screens // Scope ViewModel to Activity so it's shared between lobby and game screens
val activity = LocalActivity.current as FragmentActivity
val chessViewModel: ChessViewModel = val chessViewModel: ChessViewModel =
viewModel( rememberWindowViewModel(
viewModelStoreOwner = activity,
key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}",
factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores),
) )
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
import androidx.activity.compose.LocalActivity
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -79,13 +76,14 @@ import com.vitorpamplona.amethyst.commons.resources.back
import com.vitorpamplona.amethyst.commons.resources.dismiss import com.vitorpamplona.amethyst.commons.resources.dismiss
import com.vitorpamplona.amethyst.commons.resources.relay_settings import com.vitorpamplona.amethyst.commons.resources.relay_settings
import com.vitorpamplona.amethyst.commons.resources.route_chess import com.vitorpamplona.amethyst.commons.resources.route_chess
import com.vitorpamplona.amethyst.commons.ui.components.rememberWindowViewModel
import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox import com.vitorpamplona.amethyst.commons.ui.feeds.RefresheableBox
import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded import com.vitorpamplona.amethyst.commons.ui.navigation.bottombars.FabBottomBarPadded
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices import com.vitorpamplona.amethyst.commons.ui.platform.LocalAppServices
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource.ChessSubscription
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
import com.vitorpamplona.quartz.nip64Chess.Color as ChessColor import com.vitorpamplona.quartz.nip64Chess.Color as ChessColor
/** /**
@@ -98,10 +96,8 @@ fun ChessLobbyScreen(
nav: INav, nav: INav,
) { ) {
// Scope ViewModel to Activity so it's shared between lobby and game screens // Scope ViewModel to Activity so it's shared between lobby and game screens
val activity = LocalActivity.current as FragmentActivity
val chessViewModel: ChessViewModel = val chessViewModel: ChessViewModel =
viewModel( rememberWindowViewModel(
viewModelStoreOwner = activity,
key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}", key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}",
factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores), factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores),
) )
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,16 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras import androidx.lifecycle.viewmodel.CreationExtras
import com.vitorpamplona.amethyst.commons.model.Account import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessDismissedGamesStore
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults import com.vitorpamplona.amethyst.commons.nip64Chess.ChessPollingDefaults
import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel import com.vitorpamplona.amethyst.commons.nip64Chess.ChessViewModel
import com.vitorpamplona.amethyst.commons.nip64Chess.chessDismissedGamesStore
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
@@ -49,7 +49,7 @@ class ChessViewModelFactory(
fetcher = AndroidRelayFetcher(account), fetcher = AndroidRelayFetcher(account),
metadataProvider = AndroidMetadataProvider(), metadataProvider = AndroidMetadataProvider(),
pollingConfig = ChessPollingDefaults.android, pollingConfig = ChessPollingDefaults.android,
dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData(appStores)), dismissedStorage = appStores.chessDismissedGamesStore(),
) as T ) as T
} }
throw IllegalArgumentException("Unknown ViewModel class") throw IllegalArgumentException("Unknown ViewModel class")
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
@@ -18,11 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -62,6 +59,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -70,7 +68,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.resources.Res import com.vitorpamplona.amethyst.commons.resources.Res
@@ -99,7 +96,9 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_owner_qr_warning
import com.vitorpamplona.amethyst.commons.resources.geocache_photo_remove import com.vitorpamplona.amethyst.commons.resources.geocache_photo_remove
import com.vitorpamplona.amethyst.commons.resources.route_edit_geocache import com.vitorpamplona.amethyst.commons.resources.route_edit_geocache
import com.vitorpamplona.amethyst.commons.resources.route_new_geocache import com.vitorpamplona.amethyst.commons.resources.route_new_geocache
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.GallerySelectSingle
import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage import com.vitorpamplona.amethyst.commons.ui.components.MyAsyncImage
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji
@@ -139,7 +138,7 @@ fun NewGeocacheScreen(
editPubKeyHex: String? = null, editPubKeyHex: String? = null,
editDTag: String? = null, editDTag: String? = null,
) { ) {
val model: NewGeocacheViewModel = viewModel() val model: NewGeocacheViewModel = rememberViewModel { NewGeocacheViewModel() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
remember(editKind, editPubKeyHex, editDTag, prefillGeohash) { remember(editKind, editPubKeyHex, editDTag, prefillGeohash) {
@@ -154,9 +153,12 @@ fun NewGeocacheScreen(
var pickingLocation by remember { mutableStateOf(false) } var pickingLocation by remember { mutableStateOf(false) }
val photoPicker = var pickingPhoto by rememberSaveable { mutableStateOf(false) }
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> if (pickingPhoto) {
if (uri != null) scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) } GallerySelectSingle(imagesOnly = true) { media ->
pickingPhoto = false
if (media != null) scope.launch { model.uploadImage(media.uri, media.mimeType, accountViewModel.host.mediaUploader) }
}
} }
Scaffold( Scaffold(
@@ -285,7 +287,7 @@ fun NewGeocacheScreen(
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
OutlinedButton( OutlinedButton(
enabled = !model.isUploading.value, enabled = !model.isUploading.value,
onClick = { photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) }, onClick = { pickingPhoto = true },
) { ) {
if (model.isUploading.value) { if (model.isUploading.value) {
CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp) CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp)
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.create package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.create
import android.net.Uri
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -28,6 +27,7 @@ import com.vitorpamplona.amethyst.commons.model.Account
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality import com.vitorpamplona.amethyst.commons.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader import com.vitorpamplona.amethyst.commons.service.uploads.MediaUploader
import com.vitorpamplona.amethyst.commons.service.uploads.MediaUri
import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator import com.vitorpamplona.amethyst.commons.service.uploads.UploadOrchestrator
import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState import com.vitorpamplona.amethyst.commons.service.uploads.UploadingState
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
@@ -175,7 +175,7 @@ class NewGeocacheViewModel : ViewModel() {
fun isValid() = hasName() && hasLocation() fun isValid() = hasName() && hasLocation()
suspend fun uploadImage( suspend fun uploadImage(
uri: Uri, uri: MediaUri,
mimeType: String?, mimeType: String?,
uploader: MediaUploader, uploader: MediaUploader,
): Boolean { ): Boolean {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
import android.content.Intent
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -40,7 +39,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -57,6 +55,8 @@ import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_dnf
import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_maintenance import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_maintenance
import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_note import com.vitorpamplona.amethyst.commons.resources.geocache_log_type_note
import com.vitorpamplona.amethyst.commons.resources.geocache_verified_find import com.vitorpamplona.amethyst.commons.resources.geocache_verified_find
import com.vitorpamplona.amethyst.commons.resources.quick_action_share
import com.vitorpamplona.amethyst.commons.ui.components.rememberTextSharer
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.commons.ui.note.ClickableUserPicture
import com.vitorpamplona.amethyst.commons.ui.note.GeocacheChip import com.vitorpamplona.amethyst.commons.ui.note.GeocacheChip
@@ -162,14 +162,11 @@ fun GeocacheWinnerStrip(
/** Shares the cache as `nostr:naddr…`, which is how it opens in another NIP-CC client. */ /** Shares the cache as `nostr:naddr…`, which is how it opens in another NIP-CC client. */
@Composable @Composable
fun GeocacheShareAction(address: Address) { fun GeocacheShareAction(address: Address) {
val context = LocalContext.current val sharer = rememberTextSharer()
val shareStr = stringRes(Res.string.quick_action_share)
IconButton(onClick = { IconButton(onClick = {
val naddr = NAddress.create(address.kind, address.pubKeyHex, address.dTag, null) val naddr = NAddress.create(address.kind, address.pubKeyHex, address.dTag, null)
val intent = sharer.share("nostr:$naddr", null, shareStr)
Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, "nostr:$naddr")
context.startActivity(Intent.createChooser(intent, null).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
}) { }) {
Icon( Icon(
symbol = MaterialSymbols.AutoMirrored.Send, symbol = MaterialSymbols.AutoMirrored.Send,
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
import android.content.Intent
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -64,10 +63,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
@@ -427,12 +425,11 @@ private fun GeocacheNavigateRow(
longitude: Double, longitude: Double,
name: String, name: String,
) { ) {
val context = LocalContext.current val uriHandler = LocalUriHandler.current
OutlinedButton(onClick = { OutlinedButton(onClick = {
val label = name.ifBlank { "Geocache" } val label = name.ifBlank { "Geocache" }
val uri = "geo:$latitude,$longitude?q=$latitude,$longitude($label)".toUri()
runCatching { runCatching {
context.startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) uriHandler.openUri("geo:$latitude,$longitude?q=$latitude,$longitude($label)")
} }
}) { }) {
Icon( Icon(
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.geocaches.detail package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
@@ -68,7 +68,6 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
import com.vitorpamplona.amethyst.commons.model.BooleanType import com.vitorpamplona.amethyst.commons.model.BooleanType
@@ -102,14 +101,18 @@ import com.vitorpamplona.amethyst.commons.resources.what_s_on_your_mind
import com.vitorpamplona.amethyst.commons.resources.zapraiser import com.vitorpamplona.amethyst.commons.resources.zapraiser
import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog import com.vitorpamplona.amethyst.commons.service.upload.ui.StrippingFailureDialog
import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia import com.vitorpamplona.amethyst.commons.service.uploads.SelectedMedia
import com.vitorpamplona.amethyst.commons.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.OnIncomingShare 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.SelectFromFiles
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery 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.TakePictureButton
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver import com.vitorpamplona.amethyst.commons.ui.actions.uploads.rememberSharedMediaResolver
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField import com.vitorpamplona.amethyst.commons.ui.components.ThinPaddingTextField
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
@@ -129,13 +132,17 @@ import com.vitorpamplona.amethyst.commons.ui.note.creators.messagefield.MessageF
import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceFlap import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceFlap
import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceSheet import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.AudienceSheet
import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.rememberAudienceLists import com.vitorpamplona.amethyst.commons.ui.note.creators.notify.rememberAudienceLists
import com.vitorpamplona.amethyst.commons.ui.note.creators.polls.PollOptionsField
import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.PowOverrideButton import com.vitorpamplona.amethyst.commons.ui.note.creators.pow.PowOverrideButton
import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews import com.vitorpamplona.amethyst.commons.ui.note.creators.previews.DisplayPreviews
import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtButton import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtButton
import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.ScheduleAtPicker
import com.vitorpamplona.amethyst.commons.ui.note.creators.scheduling.roundUpToNextQuarterHour
import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.AddSecretEmojiButton
import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.secretEmoji.SecretEmojiRequest
import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription import com.vitorpamplona.amethyst.commons.ui.note.creators.uploads.ImageVideoDescription
import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList import com.vitorpamplona.amethyst.commons.ui.note.creators.userSuggestions.ShowUserSuggestionList
import com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls.ZapPollField
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.AddZapraiserButton
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest import com.vitorpamplona.amethyst.commons.ui.note.creators.zapraiser.ZapRaiserRequest
import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo import com.vitorpamplona.amethyst.commons.ui.note.creators.zapsplits.ForwardZapTo
@@ -161,14 +168,8 @@ import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.ui.note.creators.polls.PollOptionsField
import com.vitorpamplona.amethyst.ui.note.creators.scheduling.ScheduleAtPicker
import com.vitorpamplona.amethyst.ui.note.creators.scheduling.roundUpToNextQuarterHour
import com.vitorpamplona.amethyst.ui.note.creators.zappolls.ZapPollField
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet import kotlinx.collections.immutable.toImmutableSet
@@ -189,7 +190,7 @@ fun ShortNotePostScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val postViewModel: ShortNotePostViewModel = viewModel() val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() }
postViewModel.init(accountViewModel) postViewModel.init(accountViewModel)
postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl) postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl)
@@ -739,7 +740,7 @@ private fun NewPostScreenBody(
showAlwaysOnPrompt = true showAlwaysOnPrompt = true
} else { } else {
postViewModel.scheduledForSec = postViewModel.scheduledForSec =
roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60)
} }
}, },
) )
@@ -761,7 +762,7 @@ private fun NewPostScreenBody(
TextButton(onClick = { TextButton(onClick = {
showAlwaysOnPrompt = false showAlwaysOnPrompt = false
postViewModel.scheduledForSec = postViewModel.scheduledForSec =
roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60)
}) { }) {
Text(stringRes(Res.string.schedule_post_always_on_prompt_continue)) Text(stringRes(Res.string.schedule_post_always_on_prompt_continue))
} }
@@ -779,7 +780,7 @@ private fun BottomRowActions(
if (postViewModel.scheduledForSec != null) { if (postViewModel.scheduledForSec != null) {
null null
} else { } else {
roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60) roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60)
} }
}, },
) { ) {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@@ -38,8 +38,11 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import com.vitorpamplona.amethyst.commons.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.VoiceAnonymizationSection
import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler import com.vitorpamplona.amethyst.commons.ui.components.PlatformBackHandler
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose import com.vitorpamplona.amethyst.commons.ui.note.NoteCompose
@@ -49,9 +52,6 @@ import com.vitorpamplona.amethyst.commons.ui.theme.Size10dp
import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.commons.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.actions.mediaServers.FileServerSelectionRow
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
import com.vitorpamplona.amethyst.ui.actions.uploads.VoiceAnonymizationSection
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -64,7 +64,7 @@ fun VoiceReplyScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val viewModel: VoiceReplyViewModel = viewModel() val viewModel: VoiceReplyViewModel = rememberViewModel { VoiceReplyViewModel() }
LaunchedEffect(replyToNoteId, recordingFilePath, accountViewModel) { LaunchedEffect(replyToNoteId, recordingFilePath, accountViewModel) {
viewModel.init(accountViewModel) viewModel.init(accountViewModel)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.home package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.audio.RecordingResult import com.vitorpamplona.amethyst.commons.audio.RecordingResult
import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController
import com.vitorpamplona.amethyst.commons.audio.VoicePreset import com.vitorpamplona.amethyst.commons.audio.VoicePreset
@@ -58,6 +57,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -215,7 +215,6 @@ class VoiceReplyViewModel : ViewModel() {
} catch (e: CancellationException) { } catch (e: CancellationException) {
Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e) Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e)
} catch (e: Exception) { } catch (e: Exception) {
val appContext = Amethyst.instance.appContext
accountViewModel.toastManager.toast( accountViewModel.toastManager.toast(
loadStringRes(Res.string.upload_error_title), loadStringRes(Res.string.upload_error_title),
loadStringRes( loadStringRes(
@@ -239,7 +238,6 @@ class VoiceReplyViewModel : ViewModel() {
result: UploadingState, result: UploadingState,
onSuccess: () -> Unit, onSuccess: () -> Unit,
) { ) {
val appContext = Amethyst.instance.appContext
val uploadErrorTitle = loadStringRes(Res.string.upload_error_title) val uploadErrorTitle = loadStringRes(Res.string.upload_error_title)
val uploadVoiceNip95NotSupported = loadStringRes(Res.string.upload_error_voice_message_nip95_not_supported) val uploadVoiceNip95NotSupported = loadStringRes(Res.string.upload_error_voice_message_nip95_not_supported)
val uploadVoiceFailed = loadStringRes(Res.string.upload_error_voice_message_failed) val uploadVoiceFailed = loadStringRes(Res.string.upload_error_voice_message_failed)
@@ -18,18 +18,18 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.polls package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.NewPostScreenInner
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner
import com.vitorpamplona.quartz.nip01Core.core.HexKey import com.vitorpamplona.quartz.nip01Core.core.HexKey
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
@@ -41,7 +41,7 @@ fun PollPostScreen(
accountViewModel: AccountViewModel, accountViewModel: AccountViewModel,
nav: INav, nav: INav,
) { ) {
val postViewModel: ShortNotePostViewModel = viewModel() val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() }
postViewModel.init(accountViewModel) postViewModel.init(accountViewModel)
LaunchedEffect(postViewModel, accountViewModel) { LaunchedEffect(postViewModel, accountViewModel) {
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.gallery package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.profile.gallery
import androidx.annotation.OptIn
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -35,7 +34,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.media3.common.util.UnstableApi
import coil3.compose.AsyncImagePainter import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent import coil3.compose.SubcomposeAsyncImageContent
@@ -61,8 +59,8 @@ import com.vitorpamplona.amethyst.commons.ui.note.elements.BannerImage
import com.vitorpamplona.amethyst.commons.ui.stringRes import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder import com.vitorpamplona.amethyst.commons.ui.theme.QuoteBorder
import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier import com.vitorpamplona.amethyst.commons.ui.theme.Size50Modifier
import com.vitorpamplona.amethyst.commons.video.isHlsMedia
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent import com.vitorpamplona.quartz.nip68Picture.PictureEvent
@@ -216,7 +214,6 @@ fun DisplayGalleryAuthorBanner(
} }
} }
@OptIn(UnstableApi::class)
@Composable @Composable
fun GalleryContentView( fun GalleryContentView(
contentList: List<MediaUrlContent>, contentList: List<MediaUrlContent>,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.screen.loggedIn.settings package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -39,7 +39,6 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -109,7 +108,6 @@ import com.vitorpamplona.amethyst.commons.ui.stringRes
import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn import com.vitorpamplona.amethyst.commons.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.StringResource
@@ -153,7 +151,7 @@ fun ComposeSettingsContent(
) )
SettingsDivider() SettingsDivider()
// The F-Droid build ships no on-device model, so the toggle would do nothing there. // The F-Droid build ships no on-device model, so the toggle would do nothing there.
if (WritingAssistantFactory.IS_SUPPORTED) { if (accountViewModel.host.supportsWritingAssistant) {
BooleanSwitchTile( BooleanSwitchTile(
flow = sharedPrefs.automaticallyProposeAiImprovements, flow = sharedPrefs.automaticallyProposeAiImprovements,
icon = MaterialSymbols.AutoAwesome, icon = MaterialSymbols.AutoAwesome,
@@ -246,7 +244,6 @@ private fun PowDifficultyTile(accountViewModel: AccountViewModel) {
private fun PowTimeEstimate(difficulty: Int) { private fun PowTimeEstimate(difficulty: Int) {
if (difficulty <= 0) return if (difficulty <= 0) return
val context = LocalContext.current
val estimate by val estimate by
produceState<String?>(initialValue = null, difficulty) { produceState<String?>(initialValue = null, difficulty) {
val rate = deviceHashesPerSecond() val rate = deviceHashesPerSecond()
@@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
private val NoFileSharer = FileSharer { _, _, _ -> } private object NoFileSharer : FileSharer {
override fun shareTextFilesAsZip(
zipName: String,
files: List<Pair<String, String>>,
title: String,
) {}
override fun shareTextFile(
fileName: String,
mimeType: String,
content: String,
title: String,
) {}
}
/** No share sheet wired yet. */ /** No share sheet wired yet. */
@Composable @Composable
@@ -0,0 +1,29 @@
/*
* 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.screen.loggedIn.calendars
import androidx.compose.runtime.Composable
private val NoPhoneCalendar = PhoneCalendar { false }
/** No calendar composer wired yet: callers fall back to the .ics share. */
@Composable
actual fun rememberPhoneCalendar(): PhoneCalendar = NoPhoneCalendar
@@ -0,0 +1,46 @@
/*
* 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.screen.loggedIn.calendars
import platform.Foundation.NSDate
import platform.Foundation.NSRelativeDateTimeFormatter
import platform.Foundation.NSRelativeDateTimeFormatterUnitsStyleAbbreviated
import platform.Foundation.dateWithTimeIntervalSince1970
actual suspend fun relativeTimeSpan(
targetSeconds: Long,
nowSeconds: Long,
dayResolution: Boolean,
): String {
val formatter = NSRelativeDateTimeFormatter()
formatter.unitsStyle = NSRelativeDateTimeFormatterUnitsStyleAbbreviated
val day = 86_400L
val (target, now) =
if (dayResolution) {
Pair(targetSeconds.floorDiv(day) * day, nowSeconds.floorDiv(day) * day)
} else {
Pair(targetSeconds, nowSeconds)
}
return formatter.localizedStringForDate(
NSDate.dateWithTimeIntervalSince1970(target.toDouble()),
relativeToDate = NSDate.dateWithTimeIntervalSince1970(now.toDouble()),
)
}
@@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
private val NoFileSharer = FileSharer { _, _, _ -> } private object NoFileSharer : FileSharer {
override fun shareTextFilesAsZip(
zipName: String,
files: List<Pair<String, String>>,
title: String,
) {}
override fun shareTextFile(
fileName: String,
mimeType: String,
content: String,
title: String,
) {}
}
/** No share sheet wired yet. */ /** No share sheet wired yet. */
@Composable @Composable
@@ -0,0 +1,29 @@
/*
* 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.screen.loggedIn.calendars
import androidx.compose.runtime.Composable
private val NoPhoneCalendar = PhoneCalendar { false }
/** No calendar composer wired yet: callers fall back to the .ics share. */
@Composable
actual fun rememberPhoneCalendar(): PhoneCalendar = NoPhoneCalendar
@@ -0,0 +1,48 @@
/*
* 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.screen.loggedIn.calendars
import com.vitorpamplona.amethyst.commons.resources.Res
import com.vitorpamplona.amethyst.commons.resources.d
import com.vitorpamplona.amethyst.commons.resources.h
import com.vitorpamplona.amethyst.commons.resources.m
import com.vitorpamplona.amethyst.commons.resources.now
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
import com.vitorpamplona.quartz.utils.TimeUtils
import kotlin.math.abs
/** The JDK has no relative-time formatter: a compact signed span ("+2h", "-5m"). */
actual suspend fun relativeTimeSpan(
targetSeconds: Long,
nowSeconds: Long,
dayResolution: Boolean,
): String {
val diff = targetSeconds - nowSeconds
val span = abs(diff)
val sign = if (diff < 0) "-" else "+"
return when {
span >= TimeUtils.ONE_DAY -> sign + (span / TimeUtils.ONE_DAY) + loadStringRes(Res.string.d)
dayResolution -> loadStringRes(Res.string.now)
span >= TimeUtils.ONE_HOUR -> sign + (span / TimeUtils.ONE_HOUR) + loadStringRes(Res.string.h)
span >= TimeUtils.ONE_MINUTE -> sign + (span / TimeUtils.ONE_MINUTE) + loadStringRes(Res.string.m)
else -> loadStringRes(Res.string.now)
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst.ui.note.share package com.vitorpamplona.amethyst.commons.ui.note.share
import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.core.Event
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
@@ -18,9 +18,8 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION * 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. * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/ */
package com.vitorpamplona.amethyst package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.cordnGroup.disambiguate
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Test import org.junit.Test