mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-05 11:18:24 +00:00
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:
+1
-1
@@ -46,6 +46,7 @@ import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.ContentFrame
|
||||
import androidx.media3.ui.compose.SURFACE_TYPE_TEXTURE_VIEW
|
||||
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.service.playback.composable.controls.BottomGradientOverlay
|
||||
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.fullscreenSwipeControls
|
||||
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.rememberIsAudioTrack
|
||||
|
||||
|
||||
+1
-1
@@ -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.Size50Modifier
|
||||
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.mockAccountViewModel
|
||||
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.MediaControllerState
|
||||
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.pip.PipVideoActivity
|
||||
import com.vitorpamplona.amethyst.ui.cast.CastDevicePickerDialog
|
||||
|
||||
-51
@@ -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
|
||||
+2
-26
@@ -25,9 +25,9 @@ import androidx.core.net.toUri
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MimeTypes
|
||||
import com.vitorpamplona.amethyst.commons.model.nip71Video.CaptionTrack
|
||||
import com.vitorpamplona.amethyst.commons.ui.state.GenericBaseCache
|
||||
import com.vitorpamplona.amethyst.commons.video.normalizeStreamMimeType
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -45,23 +45,7 @@ class MediaItemCache : GenericBaseCache<MediaItemData, LoadedMediaItem>(20) {
|
||||
internal fun toExoPlayerMimeType(
|
||||
mimeType: String?,
|
||||
videoUri: String? = null,
|
||||
): String? {
|
||||
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
|
||||
}
|
||||
): String? = normalizeStreamMimeType(mimeType, videoUri)
|
||||
|
||||
// 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
|
||||
@@ -81,14 +65,6 @@ class MediaItemCache : GenericBaseCache<MediaItemData, LoadedMediaItem>(20) {
|
||||
language != null && type != null -> "$language \u00B7 $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 =
|
||||
|
||||
@@ -26,7 +26,9 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.debugState
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
@@ -61,11 +63,15 @@ class MainActivity : AppCompatActivity() {
|
||||
setContent {
|
||||
AmethystTheme {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
override fun onResume() {
|
||||
|
||||
+1
-1
@@ -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.loadStringRes
|
||||
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.service.playback.composable.VideoViewInner
|
||||
import com.vitorpamplona.amethyst.service.playback.composable.mediaitem.isHlsMedia
|
||||
import com.vitorpamplona.amethyst.ui.actions.MediaSaverToDisk
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
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.findQueryParameterValue
|
||||
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.articles.ArticlesScreen
|
||||
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.JobBoardScreen
|
||||
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.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.CordnGroupListScreen
|
||||
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.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.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.RelayGroupChatScreen
|
||||
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.RelayGroupThreadsScreen
|
||||
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.list.CommunitiesScreen
|
||||
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.favorites.FavoriteAppsScreen
|
||||
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.NewGeocacheHuntScreen
|
||||
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.NewHighlightScreen
|
||||
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.interestSets.display.InterestSetScreen
|
||||
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.NewPodcastTrailerScreen
|
||||
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.results.PollResultsScreen
|
||||
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.BottomBarSettingsScreen
|
||||
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.HiddenWordsScreen
|
||||
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.share.ShareNoteAsImageFileScreen
|
||||
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.loggedIn.AccountSwitcherAndLeftDrawerLayout
|
||||
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.CalendarsScreen
|
||||
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.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.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.EmbeddedTabLayer
|
||||
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.followPacks.feed.FollowPackFeedScreen
|
||||
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.GitRepositoryCodeScreen
|
||||
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.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.music.AddToMusicPlaylistSheet
|
||||
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.notifications.NotificationScreen
|
||||
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.payment.SendPaymentScreen
|
||||
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.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.NotificationSettingsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.OtsSettingsScreen
|
||||
|
||||
+1
-1
@@ -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.UploadOrchestrator
|
||||
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.TextSpinner
|
||||
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.uploads.errorResource
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.actions.uploads.UploadProgressIndicator
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
+1
-1
@@ -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.rememberTimeOfDayFormatter
|
||||
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.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
|
||||
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.detailRouteFor
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.calendars.rememberRelativeTimeLabel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
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.UsernameDisplay
|
||||
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.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.ChessViewModelFactory
|
||||
import com.vitorpamplona.quartz.nip64Chess.challenge.offer.LiveChessGameChallengeEvent
|
||||
import com.vitorpamplona.quartz.nip64Chess.end.LiveChessGameEndEvent
|
||||
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.VoiceHeightModifier
|
||||
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.service.playback.composable.DEFAULT_MUTED_SETTING
|
||||
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.LoadedMediaItem
|
||||
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.pip.PipVideoActivity
|
||||
import com.vitorpamplona.amethyst.ui.components.ShareMediaAction
|
||||
|
||||
+2
@@ -99,6 +99,8 @@ class AndroidAccountViewModelHost(
|
||||
): Result<AnonymizedResult> = VoiceAnonymizer().anonymize(input.toFile(), VoicePreset.valueOf(presetName))
|
||||
|
||||
// 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 val savedAccounts: Flow<Set<HexKey>> =
|
||||
|
||||
+1
-1
@@ -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.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.calendars.datasource.CalendarsFilterAssemblerSubscription
|
||||
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.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.workouts.datasource.WorkoutsFilterAssemblerSubscription
|
||||
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.nests.datasource.NestsFilterAssemblerSubscription
|
||||
|
||||
|
||||
+1
@@ -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.note.rememberTimeOfDayFormatter
|
||||
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.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.quartz.nip01Core.core.BaseAddressableEvent
|
||||
|
||||
+1
-1
@@ -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.navs.INav
|
||||
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.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
|
||||
-60
@@ -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)
|
||||
}
|
||||
+1
-1
@@ -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.ui.navigation.navs.INav
|
||||
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.theme.Size55Modifier
|
||||
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
|
||||
|
||||
+1
@@ -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.note.CheckHiddenFeedWatchBlockAndReport
|
||||
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.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||
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.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.crypto.KeyPair
|
||||
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
|
||||
`DateSkeletonFormatter`. Moved: search, the long-form composer, the calendar collection
|
||||
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).
|
||||
- The Nests activity and PiP.
|
||||
- The call screens.
|
||||
|
||||
+9
@@ -25,6 +25,7 @@ import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.emptyPreferences
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.first
|
||||
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"))
|
||||
|
||||
+5
-4
@@ -29,6 +29,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip64Chess.jester.JesterProtocol
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import com.vitorpamplona.quartz.utils.currentTimeMillis
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
@@ -84,7 +85,7 @@ class ChessEventBroadcaster(
|
||||
val dummyFilter =
|
||||
Filter(
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -136,10 +137,10 @@ class ChessEventBroadcaster(
|
||||
targetRelays: Set<NormalizedRelayUrl>,
|
||||
timeoutMs: Long,
|
||||
): Set<NormalizedRelayUrl> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
val startTime = currentTimeMillis()
|
||||
val pollInterval = 200L
|
||||
|
||||
while (System.currentTimeMillis() - startTime < timeoutMs) {
|
||||
while (currentTimeMillis() - startTime < timeoutMs) {
|
||||
val connected = client.connectedRelaysFlow().value
|
||||
val targetConnected = targetRelays.intersect(connected)
|
||||
|
||||
@@ -148,7 +149,7 @@ class ChessEventBroadcaster(
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
+3
@@ -91,6 +91,9 @@ interface AccountViewModelHost {
|
||||
presetName: String,
|
||||
): 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. */
|
||||
fun createWritingAssistant(): WritingAssistant? = null
|
||||
|
||||
|
||||
+6
-6
@@ -18,11 +18,11 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.commons.video
|
||||
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/**
|
||||
* 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/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
|
||||
@@ -54,7 +54,7 @@ class IsHlsMediaTest {
|
||||
fun recognisesAPlainM3u8Path() {
|
||||
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
|
||||
+36
-2
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -33,8 +34,17 @@ import java.util.zip.ZipOutputStream
|
||||
@Composable
|
||||
actual fun rememberFileSharer(): FileSharer {
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
FileSharer { zipName, files, title ->
|
||||
return remember(context) { AndroidFileSharer(context) }
|
||||
}
|
||||
|
||||
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)
|
||||
ZipOutputStream(FileOutputStream(zipFile)).use { zip ->
|
||||
files.forEach { (name, content) ->
|
||||
@@ -57,5 +67,29 @@ actual fun rememberFileSharer(): FileSharer {
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -18,17 +18,26 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip52Calendar.appt.day.CalendarDateSlotEvent
|
||||
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
|
||||
* 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
|
||||
* can decide whether to fall back to the .ics share path.
|
||||
*/
|
||||
fun addToPhoneCalendar(
|
||||
private fun addToPhoneCalendar(
|
||||
context: Context,
|
||||
event: Event,
|
||||
): Boolean {
|
||||
+36
@@ -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()
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.mediaServers
|
||||
package com.vitorpamplona.amethyst.commons.ui.actions.mediaServers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||
package com.vitorpamplona.amethyst.commons.ui.actions.uploads
|
||||
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||
package com.vitorpamplona.amethyst.commons.ui.actions.uploads
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
+1
-3
@@ -18,7 +18,7 @@
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.actions.uploads
|
||||
package com.vitorpamplona.amethyst.commons.ui.actions.uploads
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -32,7 +32,6 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.audio.VoicePreset
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
@@ -44,7 +43,6 @@ fun VoicePresetSelector(
|
||||
onPresetSelected: (VoicePreset) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isProcessing = processingPreset != null
|
||||
|
||||
Row(
|
||||
+12
-1
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.commons.ui.components
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
/** 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
|
||||
* chooser titled [title] where the platform has one.
|
||||
@@ -33,6 +33,17 @@ fun interface FileSharer {
|
||||
files: List<Pair<String, 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,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
@@ -67,6 +67,7 @@ inline fun <reified VM : ViewModel> rememberViewModel(
|
||||
* when the store has no view model yet.
|
||||
*/
|
||||
@Composable
|
||||
@PublishedApi
|
||||
internal fun <VM : ViewModel> rememberLambdaFactory(factory: () -> VM): ViewModelProvider.Factory {
|
||||
val currentFactory by rememberUpdatedState(factory)
|
||||
return remember {
|
||||
|
||||
+64
@@ -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))
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.creators.polls
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.creators.polls
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.creators.polls
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.creators.polls
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
+29
-40
@@ -18,9 +18,8 @@
|
||||
* 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.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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -54,7 +53,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -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_single
|
||||
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.stringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.DividerThickness
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.DayOfWeek
|
||||
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
|
||||
import com.vitorpamplona.quartz.utils.currentTimeMillis
|
||||
|
||||
/**
|
||||
* 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 showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
val currentTime =
|
||||
Instant
|
||||
.ofEpochMilli(scheduledForSec * 1000)
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime()
|
||||
// The scheduled time as a local wall-clock time: seconds since the epoch shifted by this zone's offset.
|
||||
val localSeconds = scheduledForSec + LocalClock.utcOffsetSeconds(scheduledForSec)
|
||||
val localSecondOfDay = localSeconds.mod(TimeUtils.ONE_DAY)
|
||||
val currentYear = SearchDate.civilFromDays(localSeconds.floorDiv(TimeUtils.ONE_DAY)).year
|
||||
|
||||
val datePickerState =
|
||||
rememberDatePickerState(
|
||||
initialSelectedDateMillis = scheduledForSec * 1000,
|
||||
yearRange = currentTime.year..2050,
|
||||
yearRange = currentYear..2050,
|
||||
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 =
|
||||
rememberTimePickerState(
|
||||
initialHour = currentTime.hour,
|
||||
initialMinute = currentTime.minute,
|
||||
is24Hour = DateFormat.is24HourFormat(context),
|
||||
initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
|
||||
initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
|
||||
is24Hour = rememberIs24HourClock(),
|
||||
)
|
||||
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
@@ -220,8 +212,7 @@ fun ScheduleAtPicker(
|
||||
(timePickerState.minute * TimeUtils.ONE_MINUTE)
|
||||
} ?: TimeUtils.oneDayAhead()
|
||||
|
||||
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
|
||||
val rawSec = datetimeLocalTimeZone - offset.totalSeconds
|
||||
val rawSec = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
|
||||
|
||||
onChanged(roundUpToNextQuarterHour(rawSec))
|
||||
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 {
|
||||
val zone = ZoneId.systemDefault()
|
||||
val tomorrow9am = LocalDate.now(zone).plusDays(1).atTime(LocalTime.of(9, 0))
|
||||
return tomorrow9am.atZone(zone).toEpochSecond()
|
||||
}
|
||||
private fun presetTomorrowMorning(): Long = nineAm(LocalClock.today().plusDays(1))
|
||||
|
||||
private fun presetNextMondayMorning(): Long {
|
||||
val zone = ZoneId.systemDefault()
|
||||
// Always step at least one day forward — if today is Monday, return next Monday.
|
||||
val target =
|
||||
LocalDate
|
||||
.now(zone)
|
||||
.plusDays(1)
|
||||
.with(TemporalAdjusters.nextOrSame(DayOfWeek.MONDAY))
|
||||
.atTime(LocalTime.of(9, 0))
|
||||
return target.atZone(zone).toEpochSecond()
|
||||
val tomorrow = LocalClock.today().plusDays(1)
|
||||
val monday = 1
|
||||
return nineAm(tomorrow.plusDays((monday - tomorrow.dayOfWeek()).mod(7)))
|
||||
}
|
||||
|
||||
/** 09:00 local time on [date], read with the offset in force at 09:00, not at midnight. */
|
||||
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
|
||||
* one slot.
|
||||
*/
|
||||
internal fun roundUpToNextQuarterHour(epochSec: Long): Long {
|
||||
fun roundUpToNextQuarterHour(epochSec: Long): Long {
|
||||
val quarter = 15 * 60L
|
||||
val rounded = ((epochSec + quarter - 1) / quarter) * quarter
|
||||
val nowSec = System.currentTimeMillis() / 1000
|
||||
val nowSec = currentTimeMillis() / 1000
|
||||
return if (rounded <= nowSec) rounded + quarter else rounded
|
||||
}
|
||||
+15
-17
@@ -18,9 +18,8 @@
|
||||
* 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.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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@@ -45,7 +44,6 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.poll_closing_date_time
|
||||
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.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import com.vitorpamplona.quartz.utils.currentTimeMillis
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -72,26 +71,28 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) {
|
||||
var showTimePicker by remember { mutableStateOf(false) }
|
||||
|
||||
// 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 =
|
||||
rememberDatePickerState(
|
||||
initialSelectedDateMillis = model.zapPollClosedAt * 1000,
|
||||
yearRange = currentTime.year..2050,
|
||||
yearRange = currentYear..2050,
|
||||
selectableDates =
|
||||
object : SelectableDates {
|
||||
override fun isSelectableDate(utcTimeMillis: Long): Boolean {
|
||||
// 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 =
|
||||
rememberTimePickerState(
|
||||
initialHour = currentTime.hour,
|
||||
initialMinute = currentTime.minute,
|
||||
is24Hour = DateFormat.is24HourFormat(context),
|
||||
initialHour = localSecondOfDay / TimeUtils.ONE_HOUR,
|
||||
initialMinute = (localSecondOfDay % TimeUtils.ONE_HOUR) / TimeUtils.ONE_MINUTE,
|
||||
is24Hour = rememberIs24HourClock(),
|
||||
)
|
||||
|
||||
OutlinedCard(
|
||||
@@ -148,10 +149,7 @@ fun ZapPollDeadlinePicker(model: ShortNotePostViewModel) {
|
||||
(timePickerState.minute * TimeUtils.ONE_MINUTE)
|
||||
} ?: TimeUtils.oneDayAhead()
|
||||
|
||||
// Get the offset from UTC for the current instant in the local time zone
|
||||
val offset: ZoneOffset = ZoneId.systemDefault().rules.getOffset(Instant.now())
|
||||
|
||||
model.zapPollClosedAt = datetimeLocalTimeZone - offset.totalSeconds
|
||||
model.zapPollClosedAt = datetimeLocalTimeZone - LocalClock.utcOffsetSeconds(TimeUtils.now())
|
||||
|
||||
showTimePicker = false
|
||||
},
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.creators.zappolls
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Column
|
||||
+2
-2
@@ -18,7 +18,7 @@
|
||||
* 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.note.creators.zappolls
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@@ -67,7 +67,7 @@ fun ZapPollOption(
|
||||
},
|
||||
label = {
|
||||
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,
|
||||
)
|
||||
},
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.creators.zappolls
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.creators.zappolls
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.share
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.share
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.externalLinkForNote
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.share
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.share
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
+3
-3
@@ -18,7 +18,7 @@
|
||||
* 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.note.share
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.share
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.navigation.navs.INav
|
||||
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.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.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip23LongContent.LongFormContentEvent
|
||||
+2
-2
@@ -18,7 +18,7 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
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.navs.INav
|
||||
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.ui.screen.loggedIn.calendars.datasource.CalendarsFilterAssemblerSubscription
|
||||
|
||||
/**
|
||||
* Top-level screen for browsing NIP-52 kind-31924 calendars (collections of appointments). Reuses
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
+4
-4
@@ -18,7 +18,7 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -42,7 +42,6 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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_export_event
|
||||
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.ScrollStateKeys
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchScrollToTop
|
||||
@@ -157,7 +157,7 @@ fun CalendarCollectionCard(
|
||||
val title = remember(note.idHex) { event.title() }
|
||||
val description = remember(note.idHex) { event.content.take(180) }
|
||||
val count = remember(note.idHex) { event.calendarEventAddresses().size }
|
||||
val context = LocalContext.current
|
||||
val fileSharer = rememberFileSharer()
|
||||
val icsChooserTitle = stringRes(Res.string.calendar_export_share_title)
|
||||
|
||||
// Calendar collections are addressable (kind 31924); route to the dedicated
|
||||
@@ -223,7 +223,7 @@ fun CalendarCollectionCard(
|
||||
val members = collectMembers(event)
|
||||
val ics = IcsExport.calendarToIcs(event, members, TimeUtils.now())
|
||||
val filename = IcsExport.calendarFilename(event)
|
||||
shareIcs(context, filename, ics, icsChooserTitle)
|
||||
fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle)
|
||||
}) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Share,
|
||||
+6
-26
@@ -18,9 +18,8 @@
|
||||
* 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
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
|
||||
|
||||
import android.text.format.DateUtils
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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_with_end
|
||||
import com.vitorpamplona.amethyst.commons.ui.loadStringRes
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.relativeTimeSpan
|
||||
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
|
||||
* 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
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
* DateUtils would produce on its own.
|
||||
* the formatter would produce on its own.
|
||||
*/
|
||||
suspend fun relativeTimeLabel(
|
||||
view: CalendarAppointmentView,
|
||||
@@ -54,31 +54,11 @@ suspend fun relativeTimeLabel(
|
||||
|
||||
if (end != null && start <= nowSeconds && nowSeconds <= end) {
|
||||
val ongoing = loadStringRes(Res.string.calendar_relative_ongoing)
|
||||
val endsIn =
|
||||
DateUtils
|
||||
.getRelativeTimeSpanString(
|
||||
end * 1000L,
|
||||
nowSeconds * 1000L,
|
||||
if (view.isAllDay) DateUtils.DAY_IN_MILLIS else DateUtils.MINUTE_IN_MILLIS,
|
||||
DateUtils.FORMAT_ABBREV_RELATIVE,
|
||||
).toString()
|
||||
val endsIn = relativeTimeSpan(end, nowSeconds, view.isAllDay)
|
||||
return loadStringRes(Res.string.calendar_relative_ongoing_with_end, ongoing, endsIn)
|
||||
}
|
||||
|
||||
val minResolution =
|
||||
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()
|
||||
return relativeTimeSpan(start, nowSeconds, view.isAllDay)
|
||||
}
|
||||
|
||||
/**
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.commons.ui.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
|
||||
+10
-12
@@ -18,18 +18,16 @@
|
||||
* 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.chess
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import com.vitorpamplona.amethyst.commons.model.preferences.AppPreferenceStores
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars
|
||||
|
||||
/**
|
||||
* Where Android keeps the dismissed-chess-games store.
|
||||
*
|
||||
* A new file rather than a migration of `chess_dismissed_games`: the dismissed
|
||||
* list is a convenience, and chess has few enough users that carrying the old
|
||||
* data over is not worth the code. Anyone who had dismissed a game sees it once
|
||||
* more and dismisses it again.
|
||||
* 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
|
||||
* `DateUtils`, iOS `NSRelativeDateTimeFormatter`; Desktop has no such formatter and writes a
|
||||
* compact signed span.
|
||||
*/
|
||||
internal fun chessDismissedGamesData(stores: AppPreferenceStores): DataStore<Preferences> = stores.getDataStore("chess_dismissed_games_v2")
|
||||
expect suspend fun relativeTimeSpan(
|
||||
targetSeconds: Long,
|
||||
nowSeconds: Long,
|
||||
dayResolution: Boolean,
|
||||
): String
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.datasource
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.detail
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.calendars.detail
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
+33
-34
@@ -18,10 +18,8 @@
|
||||
* 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.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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -52,11 +50,10 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
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.route_calendar_event_detail
|
||||
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.navigation.navs.INav
|
||||
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.types.CalendarRsvpRow
|
||||
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.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.theme.Size30dp
|
||||
import com.vitorpamplona.amethyst.commons.ui.theme.Size35dp
|
||||
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.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.tags.people.PTag
|
||||
@@ -177,7 +175,9 @@ fun CalendarEventDetailScreen(
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
val context = LocalContext.current
|
||||
val phoneCalendar = rememberPhoneCalendar()
|
||||
val fileSharer = rememberFileSharer()
|
||||
val textSharer = rememberTextSharer()
|
||||
val icsChooserTitle = stringRes(Res.string.calendar_export_share_title)
|
||||
// Two share modes:
|
||||
// - .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
|
||||
// GrapheneOS profiles).
|
||||
IconButton(onClick = {
|
||||
if (!addToPhoneCalendar(context, event)) {
|
||||
if (!phoneCalendar.add(event)) {
|
||||
val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now())
|
||||
val filename = IcsExport.appointmentFilename(event, targetAddress)
|
||||
shareIcs(context, filename, ics, icsChooserTitle)
|
||||
fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle)
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
@@ -206,7 +206,7 @@ fun CalendarEventDetailScreen(
|
||||
IconButton(onClick = {
|
||||
val ics = IcsExport.appointmentToIcs(event, targetAddress, TimeUtils.now())
|
||||
val filename = IcsExport.appointmentFilename(event, targetAddress)
|
||||
shareIcs(context, filename, ics, icsChooserTitle)
|
||||
fileSharer.shareTextFile(filename, "text/calendar", ics, icsChooserTitle)
|
||||
}) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.Share,
|
||||
@@ -224,15 +224,7 @@ fun CalendarEventDetailScreen(
|
||||
targetAddress.dTag,
|
||||
null,
|
||||
)
|
||||
val intent =
|
||||
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),
|
||||
)
|
||||
textSharer.share("nostr:$naddr", null, shareTitle)
|
||||
}) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.Send,
|
||||
@@ -347,7 +339,6 @@ private fun EventBody(
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val timeFormatter = rememberTimeOfDayFormatter()
|
||||
formatCalendarRange(note, timeFormatter)?.let { range ->
|
||||
Text(
|
||||
@@ -595,7 +586,7 @@ private fun HeroImage(
|
||||
|
||||
@Composable
|
||||
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,
|
||||
// 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.
|
||||
@@ -607,17 +598,10 @@ private fun LocationRow(location: String) {
|
||||
.clickable {
|
||||
runCatching {
|
||||
val trimmed = location.trim()
|
||||
val intent =
|
||||
if (isUrl) {
|
||||
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
|
||||
// `geo:0,0?q=<location>` is the Android geo URI; the user's installed maps
|
||||
// app handles it. runCatching swallows the failure when no handler is
|
||||
// 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),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -905,3 +889,18 @@ private fun rememberCalendarsContaining(targetAddress: Address): State<List<Cale
|
||||
|
||||
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()
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.chats.cordnGroup
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.cordnGroup
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
+5
-10
@@ -18,9 +18,8 @@
|
||||
* 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.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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -48,9 +47,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
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.MaterialSymbols
|
||||
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_member_count
|
||||
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.topbars.TopBarWithBackButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.UserPicture
|
||||
@@ -367,13 +365,10 @@ private fun MemberRow(
|
||||
* a roster built under another key.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberCordnGroupDraft(accountViewModel: AccountViewModel): CordnGroupDraft {
|
||||
val activity = LocalActivity.current as FragmentActivity
|
||||
return viewModel(
|
||||
viewModelStoreOwner = activity,
|
||||
fun rememberCordnGroupDraft(accountViewModel: AccountViewModel): CordnGroupDraft =
|
||||
rememberWindowViewModel(
|
||||
key = "CordnGroupDraft-${accountViewModel.account.signer.pubKey}",
|
||||
)
|
||||
}
|
||||
) { CordnGroupDraft() }
|
||||
|
||||
/**
|
||||
* What each coordinator this account uses could do with [roster].
|
||||
+6
-6
@@ -18,7 +18,7 @@
|
||||
* 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.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.Spacer
|
||||
@@ -28,9 +28,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
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.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.ui.components.rememberViewModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.feeds.WatchLifecycleAndUpdateModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
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.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.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.EditFieldRow
|
||||
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.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
|
||||
|
||||
@Composable
|
||||
@@ -77,7 +77,7 @@ fun PrepareChannelViewModels(
|
||||
nav: INav,
|
||||
) {
|
||||
val feedViewModel: ChannelFeedViewModel =
|
||||
viewModel(
|
||||
rememberViewModel(
|
||||
key = baseChannel.address.toValue() + "ChannelFeedViewModel",
|
||||
factory =
|
||||
ChannelFeedViewModel.Factory(
|
||||
@@ -86,7 +86,7 @@ fun PrepareChannelViewModels(
|
||||
),
|
||||
)
|
||||
|
||||
val channelScreenModel: ChannelNewMessageViewModel = viewModel()
|
||||
val channelScreenModel: ChannelNewMessageViewModel = rememberViewModel { ChannelNewMessageViewModel() }
|
||||
channelScreenModel.init(accountViewModel)
|
||||
channelScreenModel.load(baseChannel)
|
||||
|
||||
+4
-4
@@ -18,7 +18,7 @@
|
||||
* 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.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.padding
|
||||
@@ -27,12 +27,12 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
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.navigation.navs.INav
|
||||
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.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header.LiveActivityTopBar
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Address
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
@@ -55,7 +55,7 @@ fun LiveActivityChannelScreen(
|
||||
LoadLiveActivityChannel(channelId) {
|
||||
LaunchedEffect(it.info) {
|
||||
it.info?.streaming()?.let {
|
||||
OnlineChecker.resetIfOfflineToRetry(it)
|
||||
OnlineStatusCache.resetIfOfflineToRetry(it)
|
||||
}
|
||||
}
|
||||
LiveActivityTopBar(it, accountViewModel, nav)
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.chats.publicChannels.nip53LiveActivities.header
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chats.publicChannels.nip53LiveActivities.header
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.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.Column
|
||||
+11
-6
@@ -18,7 +18,7 @@
|
||||
* 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.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.clickable
|
||||
@@ -47,13 +47,14 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.navigation.Route
|
||||
import com.vitorpamplona.amethyst.commons.model.nip53LiveActivities.LiveActivitiesChannel
|
||||
import com.vitorpamplona.amethyst.commons.nip53LiveActivities.TopZapperEntry
|
||||
import com.vitorpamplona.amethyst.commons.resources.Res
|
||||
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.note.UserPicture
|
||||
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.viewmodels.AccountViewModel
|
||||
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
|
||||
@@ -80,12 +82,15 @@ fun LiveStreamTopZappers(
|
||||
nav: INav,
|
||||
) {
|
||||
val topZappersVm: LiveStreamTopZappersViewModel =
|
||||
viewModel(
|
||||
rememberViewModel(
|
||||
key = "TopZappers-${channel.address.toValue()}",
|
||||
factory =
|
||||
object : ViewModelProvider.Factory {
|
||||
@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)
|
||||
Text(
|
||||
text = showAmountInteger(BigDecimal.valueOf(entry.totalSats)),
|
||||
text = showAmountInteger(BigDecimal(entry.totalSats)),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
Spacer(Modifier.padding(start = 2.dp))
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.chess
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
+4
-8
@@ -18,9 +18,8 @@
|
||||
* 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.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.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.MaterialSymbols
|
||||
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.go_back
|
||||
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.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.theme.Height4dpModifier
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
|
||||
import com.vitorpamplona.quartz.nip64Chess.ChessGameNameGenerator
|
||||
|
||||
/**
|
||||
@@ -100,10 +98,8 @@ fun ChessGameScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
// Scope ViewModel to Activity so it's shared between lobby and game screens
|
||||
val activity = LocalActivity.current as FragmentActivity
|
||||
val chessViewModel: ChessViewModel =
|
||||
viewModel(
|
||||
viewModelStoreOwner = activity,
|
||||
rememberWindowViewModel(
|
||||
key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}",
|
||||
factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores),
|
||||
)
|
||||
+4
-8
@@ -18,9 +18,8 @@
|
||||
* 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.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.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -56,8 +55,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
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.MaterialSymbols
|
||||
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.relay_settings
|
||||
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.navigation.bottombars.FabBottomBarPadded
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
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.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chess.datasource.ChessSubscription
|
||||
import com.vitorpamplona.quartz.nip64Chess.Color as ChessColor
|
||||
|
||||
/**
|
||||
@@ -98,10 +96,8 @@ fun ChessLobbyScreen(
|
||||
nav: INav,
|
||||
) {
|
||||
// Scope ViewModel to Activity so it's shared between lobby and game screens
|
||||
val activity = LocalActivity.current as FragmentActivity
|
||||
val chessViewModel: ChessViewModel =
|
||||
viewModel(
|
||||
viewModelStoreOwner = activity,
|
||||
rememberWindowViewModel(
|
||||
key = "ChessViewModel-${accountViewModel.account.userProfile().pubkeyHex}",
|
||||
factory = ChessViewModelFactory(accountViewModel.account, LocalAppServices.current.appStores),
|
||||
)
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.chess
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
+3
-3
@@ -18,16 +18,16 @@
|
||||
* 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.chess
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewmodel.CreationExtras
|
||||
import com.vitorpamplona.amethyst.commons.model.Account
|
||||
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.ChessViewModel
|
||||
import com.vitorpamplona.amethyst.commons.nip64Chess.chessDismissedGamesStore
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
@@ -49,7 +49,7 @@ class ChessViewModelFactory(
|
||||
fetcher = AndroidRelayFetcher(account),
|
||||
metadataProvider = AndroidMetadataProvider(),
|
||||
pollingConfig = ChessPollingDefaults.android,
|
||||
dismissedStorage = ChessDismissedGamesStore(chessDismissedGamesData(appStores)),
|
||||
dismissedStorage = appStores.chessDismissedGamesStore(),
|
||||
) as T
|
||||
}
|
||||
throw IllegalArgumentException("Unknown ViewModel class")
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.chess.datasource
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.chess.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
+12
-10
@@ -18,11 +18,8 @@
|
||||
* 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.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.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -62,6 +59,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
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.style.TextAlign
|
||||
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.MaterialSymbols
|
||||
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.route_edit_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.rememberViewModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.insets.imePaddingSafe
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.note.geocacheEmoji
|
||||
@@ -139,7 +138,7 @@ fun NewGeocacheScreen(
|
||||
editPubKeyHex: String? = null,
|
||||
editDTag: String? = null,
|
||||
) {
|
||||
val model: NewGeocacheViewModel = viewModel()
|
||||
val model: NewGeocacheViewModel = rememberViewModel { NewGeocacheViewModel() }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
remember(editKind, editPubKeyHex, editDTag, prefillGeohash) {
|
||||
@@ -154,9 +153,12 @@ fun NewGeocacheScreen(
|
||||
|
||||
var pickingLocation by remember { mutableStateOf(false) }
|
||||
|
||||
val photoPicker =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri ->
|
||||
if (uri != null) scope.launch { model.uploadImage(uri, null, accountViewModel.host.mediaUploader) }
|
||||
var pickingPhoto by rememberSaveable { mutableStateOf(false) }
|
||||
if (pickingPhoto) {
|
||||
GallerySelectSingle(imagesOnly = true) { media ->
|
||||
pickingPhoto = false
|
||||
if (media != null) scope.launch { model.uploadImage(media.uri, media.mimeType, accountViewModel.host.mediaUploader) }
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -285,7 +287,7 @@ fun NewGeocacheScreen(
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
OutlinedButton(
|
||||
enabled = !model.isUploading.value,
|
||||
onClick = { photoPicker.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)) },
|
||||
onClick = { pickingPhoto = true },
|
||||
) {
|
||||
if (model.isUploading.value) {
|
||||
CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||
+3
-3
@@ -18,9 +18,8 @@
|
||||
* 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.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.mutableStateOf
|
||||
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.service.uploads.CompressorQuality
|
||||
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.UploadingState
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
@@ -175,7 +175,7 @@ class NewGeocacheViewModel : ViewModel() {
|
||||
fun isValid() = hasName() && hasLocation()
|
||||
|
||||
suspend fun uploadImage(
|
||||
uri: Uri,
|
||||
uri: MediaUri,
|
||||
mimeType: String?,
|
||||
uploader: MediaUploader,
|
||||
): Boolean {
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.geocaches.detail
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
+6
-9
@@ -18,9 +18,8 @@
|
||||
* 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.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.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -40,7 +39,6 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
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_note
|
||||
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.note.ClickableUserPicture
|
||||
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. */
|
||||
@Composable
|
||||
fun GeocacheShareAction(address: Address) {
|
||||
val context = LocalContext.current
|
||||
val sharer = rememberTextSharer()
|
||||
val shareStr = stringRes(Res.string.quick_action_share)
|
||||
IconButton(onClick = {
|
||||
val naddr = NAddress.create(address.kind, address.pubKeyHex, address.dTag, null)
|
||||
val intent =
|
||||
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))
|
||||
sharer.share("nostr:$naddr", null, shareStr)
|
||||
}) {
|
||||
Icon(
|
||||
symbol = MaterialSymbols.AutoMirrored.Send,
|
||||
+4
-7
@@ -18,9 +18,8 @@
|
||||
* 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.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.clickable
|
||||
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.graphics.Color
|
||||
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.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.model.cache.LocalCache
|
||||
@@ -427,12 +425,11 @@ private fun GeocacheNavigateRow(
|
||||
longitude: Double,
|
||||
name: String,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
OutlinedButton(onClick = {
|
||||
val label = name.ifBlank { "Geocache" }
|
||||
val uri = "geo:$latitude,$longitude?q=$latitude,$longitude($label)".toUri()
|
||||
runCatching {
|
||||
context.startActivity(Intent(Intent.ACTION_VIEW, uri).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
uriHandler.openUri("geo:$latitude,$longitude?q=$latitude,$longitude($label)")
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.geocaches.detail
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.geocaches.detail
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.geocaches.detail
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
+14
-13
@@ -18,7 +18,7 @@
|
||||
* 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.home
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
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.unit.dp
|
||||
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.MaterialSymbols
|
||||
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.service.upload.ui.StrippingFailureDialog
|
||||
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.SelectFromFiles
|
||||
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.SelectFromGallery
|
||||
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakePictureButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.TakeVideoButton
|
||||
import com.vitorpamplona.amethyst.commons.ui.actions.uploads.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.components.PlatformBackHandler
|
||||
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.navigation.navs.INav
|
||||
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.AudienceSheet
|
||||
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.previews.DisplayPreviews
|
||||
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.SecretEmojiRequest
|
||||
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.zappolls.ZapPollField
|
||||
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.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.replyModifier
|
||||
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.utils.TimeUtils
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
@@ -189,7 +190,7 @@ fun ShortNotePostScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val postViewModel: ShortNotePostViewModel = viewModel()
|
||||
val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() }
|
||||
postViewModel.init(accountViewModel)
|
||||
postViewModel.setGroupThread(groupThreadId, groupThreadRelayUrl)
|
||||
|
||||
@@ -739,7 +740,7 @@ private fun NewPostScreenBody(
|
||||
showAlwaysOnPrompt = true
|
||||
} else {
|
||||
postViewModel.scheduledForSec =
|
||||
roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60)
|
||||
roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60)
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -761,7 +762,7 @@ private fun NewPostScreenBody(
|
||||
TextButton(onClick = {
|
||||
showAlwaysOnPrompt = false
|
||||
postViewModel.scheduledForSec =
|
||||
roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60)
|
||||
roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60)
|
||||
}) {
|
||||
Text(stringRes(Res.string.schedule_post_always_on_prompt_continue))
|
||||
}
|
||||
@@ -779,7 +780,7 @@ private fun BottomRowActions(
|
||||
if (postViewModel.scheduledForSec != null) {
|
||||
null
|
||||
} else {
|
||||
roundUpToNextQuarterHour((System.currentTimeMillis() / 1000) + 60 * 60)
|
||||
roundUpToNextQuarterHour(TimeUtils.now() + 60 * 60)
|
||||
}
|
||||
},
|
||||
) {
|
||||
+6
-6
@@ -18,7 +18,7 @@
|
||||
* 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.home
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -38,8 +38,11 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
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.rememberViewModel
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.navs.INav
|
||||
import com.vitorpamplona.amethyst.commons.ui.navigation.topbars.PostingTopBar
|
||||
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.replyModifier
|
||||
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)
|
||||
@Composable
|
||||
@@ -64,7 +64,7 @@ fun VoiceReplyScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val viewModel: VoiceReplyViewModel = viewModel()
|
||||
val viewModel: VoiceReplyViewModel = rememberViewModel { VoiceReplyViewModel() }
|
||||
|
||||
LaunchedEffect(replyToNoteId, recordingFilePath, accountViewModel) {
|
||||
viewModel.init(accountViewModel)
|
||||
+2
-4
@@ -18,7 +18,7 @@
|
||||
* 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.home
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -26,7 +26,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.commons.audio.RecordingResult
|
||||
import com.vitorpamplona.amethyst.commons.audio.VoiceAnonymizationController
|
||||
import com.vitorpamplona.amethyst.commons.audio.VoicePreset
|
||||
@@ -58,6 +57,7 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -215,7 +215,6 @@ class VoiceReplyViewModel : ViewModel() {
|
||||
} catch (e: CancellationException) {
|
||||
Log.w("VoiceReplyViewModel", "User canceled, or ViewModel cleared", e)
|
||||
} catch (e: Exception) {
|
||||
val appContext = Amethyst.instance.appContext
|
||||
accountViewModel.toastManager.toast(
|
||||
loadStringRes(Res.string.upload_error_title),
|
||||
loadStringRes(
|
||||
@@ -239,7 +238,6 @@ class VoiceReplyViewModel : ViewModel() {
|
||||
result: UploadingState,
|
||||
onSuccess: () -> Unit,
|
||||
) {
|
||||
val appContext = Amethyst.instance.appContext
|
||||
val uploadErrorTitle = loadStringRes(Res.string.upload_error_title)
|
||||
val uploadVoiceNip95NotSupported = loadStringRes(Res.string.upload_error_voice_message_nip95_not_supported)
|
||||
val uploadVoiceFailed = loadStringRes(Res.string.upload_error_voice_message_failed)
|
||||
+4
-4
@@ -18,18 +18,18 @@
|
||||
* 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.polls
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.polls
|
||||
|
||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
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.screen.loggedIn.home.NewPostScreenInner
|
||||
import com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.home.ShortNotePostViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.NewPostScreenInner
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
|
||||
@@ -41,7 +41,7 @@ fun PollPostScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val postViewModel: ShortNotePostViewModel = viewModel()
|
||||
val postViewModel: ShortNotePostViewModel = rememberViewModel { ShortNotePostViewModel() }
|
||||
postViewModel.init(accountViewModel)
|
||||
|
||||
LaunchedEffect(postViewModel, accountViewModel) {
|
||||
+2
-5
@@ -18,9 +18,8 @@
|
||||
* 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.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.layout.Box
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import coil3.compose.AsyncImagePainter
|
||||
import coil3.compose.SubcomposeAsyncImage
|
||||
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.theme.QuoteBorder
|
||||
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.service.playback.composable.mediaitem.isHlsMedia
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.ProfileGalleryEntryEvent
|
||||
import com.vitorpamplona.quartz.nip53LiveActivities.clip.LiveActivitiesClipEvent
|
||||
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
|
||||
@@ -216,7 +214,6 @@ fun DisplayGalleryAuthorBanner(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
fun GalleryContentView(
|
||||
contentList: List<MediaUrlContent>,
|
||||
+2
-5
@@ -18,7 +18,7 @@
|
||||
* 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.settings
|
||||
package com.vitorpamplona.amethyst.commons.ui.screen.loggedIn.settings
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@@ -39,7 +39,6 @@ import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
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.viewmodels.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.commons.viewmodels.mockAccountViewModel
|
||||
import com.vitorpamplona.amethyst.service.ai.WritingAssistantFactory
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
|
||||
@@ -153,7 +151,7 @@ fun ComposeSettingsContent(
|
||||
)
|
||||
SettingsDivider()
|
||||
// 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(
|
||||
flow = sharedPrefs.automaticallyProposeAiImprovements,
|
||||
icon = MaterialSymbols.AutoAwesome,
|
||||
@@ -246,7 +244,6 @@ private fun PowDifficultyTile(accountViewModel: AccountViewModel) {
|
||||
private fun PowTimeEstimate(difficulty: Int) {
|
||||
if (difficulty <= 0) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val estimate by
|
||||
produceState<String?>(initialValue = null, difficulty) {
|
||||
val rate = deviceHashesPerSecond()
|
||||
+14
-1
@@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
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. */
|
||||
@Composable
|
||||
|
||||
+29
@@ -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
|
||||
+46
@@ -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()),
|
||||
)
|
||||
}
|
||||
+14
-1
@@ -22,7 +22,20 @@ package com.vitorpamplona.amethyst.commons.ui.components
|
||||
|
||||
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. */
|
||||
@Composable
|
||||
|
||||
+29
@@ -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
|
||||
+48
@@ -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)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
* 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.note.share
|
||||
package com.vitorpamplona.amethyst.commons.ui.note.share
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.hexToByteArray
|
||||
+1
-2
@@ -18,9 +18,8 @@
|
||||
* 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
|
||||
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.Test
|
||||
|
||||
Reference in New Issue
Block a user