diff --git a/app/build.gradle b/app/build.gradle index ff673bbc3..84c9fd55c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 34 - versionCode 306 - versionName "0.78.0" + versionCode 308 + versionName "0.78.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt index 27709e150..e4ad817e1 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/VideoCache.kt @@ -12,7 +12,7 @@ import java.io.File @UnstableApi object VideoCache { - var exoPlayerCacheSize: Long = 90 * 1024 * 1024 // 90MB + var exoPlayerCacheSize: Long = 150 * 1024 * 1024 // 90MB var leastRecentlyUsedCacheEvictor = LeastRecentlyUsedCacheEvictor(exoPlayerCacheSize) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt index 0b72847d2..74f9103fc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostView.kt @@ -214,7 +214,10 @@ fun NewPostView( PostButton( onPost = { postViewModel.sendPost(relayList = relayList) - onClose() + scope.launch { + delay(100) + onClose() + } }, isActive = postViewModel.canPost() ) @@ -224,7 +227,10 @@ fun NewPostView( Spacer(modifier = StdHorzSpacer) CloseButton(onPress = { postViewModel.cancel() - onClose() + scope.launch { + delay(100) + onClose() + } }) }, backgroundColor = MaterialTheme.colors.surface, @@ -1220,9 +1226,7 @@ private fun MarkAsSensitive( @Composable fun CloseButton(onPress: () -> Unit) { Button( - onClick = { - onPress() - }, + onClick = onPress, shape = ButtonBorder, colors = ButtonDefaults .buttonColors( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt index 44b0cb0b0..d46790ddb 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/VideoView.kt @@ -1,5 +1,6 @@ package com.vitorpamplona.amethyst.ui.components +import android.content.Context import android.graphics.Rect import android.graphics.drawable.Drawable import android.net.Uri @@ -206,34 +207,36 @@ fun VideoViewInner( ) } - val mediaItem = remember(videoUri) { - MediaItem.Builder() - .setMediaId(videoUri) - .setUri(videoUri) - .setMediaMetadata( - MediaMetadata.Builder() - .setArtist(authorName?.ifBlank { null }) - .setTitle(title?.ifBlank { null } ?: videoUri) - .setArtworkUri( - try { - if (artworkUri != null) { - Uri.parse(artworkUri) - } else { - null - } - } catch (e: Exception) { - null - } - ) - .build() - ) - .build() - } - if (!automaticallyStartPlayback.value) { ImageUrlWithDownloadButton(url = videoUri, showImage = automaticallyStartPlayback) } else { VideoPlayerActiveMutex(videoUri) { activeOnScreen -> + val mediaItem = remember(videoUri) { + mutableStateOf( + MediaItem.Builder() + .setMediaId(videoUri) + .setUri(videoUri) + .setMediaMetadata( + MediaMetadata.Builder() + .setArtist(authorName?.ifBlank { null }) + .setTitle(title?.ifBlank { null } ?: videoUri) + .setArtworkUri( + try { + if (artworkUri != null) { + Uri.parse(artworkUri) + } else { + null + } + } catch (e: Exception) { + null + } + ) + .build() + ) + .build() + ) + } + GetVideoController( mediaItem = mediaItem, videoUri = videoUri, @@ -258,7 +261,7 @@ fun VideoViewInner( @Composable @OptIn(UnstableApi::class) fun GetVideoController( - mediaItem: MediaItem, + mediaItem: MutableState, videoUri: String, defaultToStart: Boolean = false, nostrUriCallback: String? = null, @@ -288,14 +291,16 @@ fun GetVideoController( DisposableEffect(key1 = videoUri) { // If it is not null, the user might have come back from a playing video, like clicking on // the notification of the video player. - scope.launch(Dispatchers.IO) { - if (controller.value == null) { + if (controller.value == null) { + scope.launch(Dispatchers.IO) { PlaybackClientController.prepareController( uid, videoUri, nostrUriCallback, context ) { + // REQUIRED TO BE RUN IN THE MAIN THREAD + // checks again because of race conditions. if (controller.value == null) { // still prone to race conditions. controller.value = it @@ -311,7 +316,7 @@ fun GetVideoController( } } - controller.value?.setMediaItem(mediaItem) + controller.value?.setMediaItem(mediaItem.value) controller.value?.prepare() } else if (controller.value != it) { // discards the new controller because there is an existing one @@ -329,27 +334,27 @@ fun GetVideoController( it.volume = if (defaultToStart) 0f else 1f } - it.setMediaItem(mediaItem) + it.setMediaItem(mediaItem.value) it.prepare() } } } } - } else { - controller.value?.let { - if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) { - if (it.isPlaying) { - // There is a video playing, start this one on mute. - it.volume = 0f - } else { - // There is no other video playing. Use the default mute state to - // decide if sound is on or not. - it.volume = if (defaultToStart) 0f else 1f - } - - it.setMediaItem(mediaItem) - it.prepare() + } + } else { + controller.value?.let { + if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) { + if (it.isPlaying) { + // There is a video playing, start this one on mute. + it.volume = 0f + } else { + // There is no other video playing. Use the default mute state to + // decide if sound is on or not. + it.volume = if (defaultToStart) 0f else 1f } + + it.setMediaItem(mediaItem.value) + it.prepare() } } } @@ -379,6 +384,8 @@ fun GetVideoController( nostrUriCallback, context ) { + // REQUIRED TO BE RUN IN THE MAIN THREAD + // checks again to make sure no other thread has created a controller. if (controller.value == null) { controller.value = it @@ -394,7 +401,7 @@ fun GetVideoController( } } - controller.value?.setMediaItem(mediaItem) + controller.value?.setMediaItem(mediaItem.value) controller.value?.prepare() } else if (controller.value != it) { // discards the new controller because there is an existing one @@ -460,7 +467,7 @@ fun VideoPlayerActiveMutex(videoUri: String, inner: @Composable (MutableState, onDialog: ((Boolean) -> Unit)? ) { - val context = LocalContext.current - ControlWhenPlayerIsActive(controller, keepPlaying, automaticallyStartPlayback, activeOnScreen) val controllerVisible = remember(controller) { @@ -520,9 +527,11 @@ private fun RenderVideoPlayer( } BoxWithConstraints() { - AndroidView( - modifier = if (roundedCorner) { - MaterialTheme.colors.imageModifier + val borders = MaterialTheme.colors.imageModifier + + val myModifier = remember { + if (roundedCorner) { + borders .defaultMinSize(minHeight = 100.dp) .align(Alignment.Center) } else { @@ -530,8 +539,11 @@ private fun RenderVideoPlayer( .fillMaxWidth() .defaultMinSize(minHeight = 100.dp) .align(Alignment.Center) - }, - factory = { + } + } + + val factory = remember(controller) { + { context: Context -> PlayerView(context).apply { player = controller layoutParams = FrameLayout.LayoutParams( @@ -556,10 +568,15 @@ private fun RenderVideoPlayer( ) } } + } + + AndroidView( + modifier = myModifier, + factory = factory ) waveform?.let { - Waveform(it, controller, Modifier.align(Alignment.Center)) + Waveform(it, controller, remember { Modifier.align(Alignment.Center) }) } val startingMuteState = remember(controller) { @@ -580,7 +597,7 @@ private fun RenderVideoPlayer( controller.volume = if (mute) 0f else 1f } - KeepPlayingButton(keepPlaying, controllerVisible, Modifier.align(Alignment.TopEnd)) { newKeepPlaying: Boolean -> + KeepPlayingButton(keepPlaying, controllerVisible, remember { Modifier.align(Alignment.TopEnd) }) { newKeepPlaying: Boolean -> // If something else is playing and the user marks this video to keep playing, stops the other one. if (newKeepPlaying) { if (keepPlayingMutex != null && keepPlayingMutex != controller) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index a53a5a0b6..c116ae8ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -840,34 +840,17 @@ private fun CheckNewAndRenderNote( val backgroundColor = remember { mutableStateOf(defaultBackgroundColor) } LaunchedEffect(key1 = routeForLastRead, key2 = parentBackgroundColor?.value) { - launch(Dispatchers.IO) { - routeForLastRead?.let { - val lastTime = accountViewModel.account.loadLastRead(it) - - val createdAt = baseNote.createdAt() - if (createdAt != null) { - accountViewModel.account.markAsRead(it, createdAt) - - val isNew = createdAt > lastTime - - val newBackgroundColor = if (isNew) { - if (parentBackgroundColor != null) { - newItemColor.compositeOver(parentBackgroundColor.value) - } else { - newItemColor.compositeOver(defaultBackgroundColor) - } + routeForLastRead?.let { + accountViewModel.loadAndMarkAsRead(it, baseNote.createdAt()) { isNew -> + val newBackgroundColor = if (isNew) { + if (parentBackgroundColor != null) { + newItemColor.compositeOver(parentBackgroundColor.value) } else { - parentBackgroundColor?.value ?: defaultBackgroundColor - } - - if (newBackgroundColor != backgroundColor.value) { - launch(Dispatchers.Main) { - backgroundColor.value = newBackgroundColor - } + newItemColor.compositeOver(defaultBackgroundColor) } + } else { + parentBackgroundColor?.value ?: defaultBackgroundColor } - } ?: run { - val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor if (newBackgroundColor != backgroundColor.value) { launch(Dispatchers.Main) { @@ -875,6 +858,14 @@ private fun CheckNewAndRenderNote( } } } + } ?: run { + val newBackgroundColor = parentBackgroundColor?.value ?: defaultBackgroundColor + + if (newBackgroundColor != backgroundColor.value) { + launch(Dispatchers.Main) { + backgroundColor.value = newBackgroundColor + } + } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt index f245ee538..b37a68596 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/FeedViewModel.kt @@ -63,6 +63,7 @@ class NostrChatroomFeedViewModel(val user: ChatroomKey, val account: Account) : } } +@Stable class NostrVideoFeedViewModel(val account: Account) : FeedViewModel(VideoFeedFilter(account)) { class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): NostrVideoFeedViewModel { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index 72abe6a55..72f658dac 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -726,6 +726,19 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao { } } + fun loadAndMarkAsRead(routeForLastRead: String, baseNoteCreatedAt: Long?, onIsNew: (Boolean) -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + val lastTime = account.loadLastRead(routeForLastRead) + + if (baseNoteCreatedAt != null) { + account.markAsRead(routeForLastRead, baseNoteCreatedAt) + onIsNew(baseNoteCreatedAt > lastTime) + } else { + onIsNew(false) + } + } + } + class Factory(val account: Account) : ViewModelProvider.Factory { override fun create(modelClass: Class): AccountViewModel { return AccountViewModel(account) as AccountViewModel diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt index d2edde0df..184d0c780 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/VideoScreen.kt @@ -229,7 +229,7 @@ fun SlidingCarousel( VerticalPager( state = pagerState, beyondBoundsPageCount = 1, - modifier = Modifier.fillMaxSize(1f), + modifier = Modifier.fillMaxSize(), key = { index -> feed.value.getOrNull(index)?.idHex ?: "$index" } @@ -246,7 +246,7 @@ fun LoadedVideoCompose( accountViewModel: AccountViewModel, nav: (String) -> Unit ) { - var state by remember { + var state by remember(note) { mutableStateOf( AccountViewModel.NoteComposeReportState() ) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b482e4c0f..e6cceb51d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -12,6 +12,7 @@ Nepodařilo se dešifrovat zprávu Obrázek skupiny Explicitní obsah + Spam Zneužívání identity Nezákonné jednání Neznámý @@ -169,6 +170,9 @@ Nostr Adresa nikdy nyní + h + m + d Nahota Neslušnost / Nenávistná řeč Nahlásit nenávistnou řeč @@ -263,6 +267,7 @@ Minimální zaps Maximální zaps Konsensus + (0–100)% Uzavřít po dnech Hlasování je uzavřeno pro nové hlasy @@ -334,6 +339,7 @@ \n5. Nastavte port Socks v této obrazovce \n6. Stiskněte tlačítko Aktivovat Tor + Port Orbot Socks Neplatné číslo portu Použít Orbot Odpojit Tor/Orbot @@ -341,6 +347,7 @@ Upozorní vás, když dorazí soukromá zpráva Přijaté zapsy Upozorní vás, když vás někdo zapsne + %1$s sats Od %1$s pro %1$s Upozornit: @@ -369,6 +376,7 @@ Při pokusu o získání informací z Relay se vyskytla chyba z %1$s Vlastník Verze + Programové vybavení Kontakt Podporované NIPs Vstupné @@ -388,10 +396,12 @@ Minimální PoW Ověření Platba + Cashu Token Vyměnit Není nastavena žádná Lightning adresa Token zkopírován do schránky ŽIVĚ + OFFLINE UKONČENO PLÁNOVÁNO Živé vysílání je offline @@ -467,6 +477,7 @@ Hlasy jsou váženy podle hodnoty zapu. Můžete nastavit minimální částku, abyste se vyhnuli spammerům, a maximální částku, abyste zabránili velkým zapperům, kteří by mohli ovládnout hlasování. Použijte stejnou částku v obou polích, aby byla hodnota každého hlasu stejná. Nechte prázdné pro přijetí libovolné částky. Nelze odeslat zap Poslat zprávu uživateli + OK Nepodařilo se dosáhnout %1$s: %2$s Nepodařilo se dosáhnout %1$s: %2$s Nepodařilo se zpracovat výsledek z %1$s: %2$s @@ -483,6 +494,7 @@ Uživatelské jméno nebo zobrazované jméno Uživatel %1$s nemá nastavenou bleskovou adresu pro přijímání satoshi Procenta + 25 Rozdělování Zapů s Přeposílání Zapů na Bleskové peněženky nenalezený diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 645ec5cb3..492787bcf 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -12,6 +12,7 @@ Nachricht konnte nicht entschlüsselt werden Gruppenbild Expliziter Inhalt + Spam Vortäuschung Illegales Verhalten Unbekannt @@ -35,6 +36,7 @@ Melden Sie sich mit einem privaten Schlüssel an, um Zaps senden zu können Melden Sie sich mit einem privaten Schlüssel an, um zu folgen Melden Sie sich mit einem privaten Schlüssel an, um nicht mehr zu folgen + Zaps Aufrufe Verstärken Verstärkt @@ -76,6 +78,7 @@ Fehler beim Hochladen des Bildes Relay-Adresse Beiträge + Bytes Fehler Startseite Private Nachrichten @@ -162,6 +165,7 @@ anz der Bedingungen ist erforderlich Öffentlicher Chat empfangene Beiträge Entfernen + Automatisch übersetzt von zu Zuerst in %1$s anzeigen @@ -171,6 +175,7 @@ anz der Bedingungen ist erforderlich nie jetzt s + m t Nacktheit Beleidigungen / Hassrede @@ -261,11 +266,13 @@ anz der Bedingungen ist erforderlich Erforderliche Felder: Zap-Empfänger Hauptbeschreibung der Umfrage… + Wahl %s Beschreibung der Umfrageoption Optionale Felder: Mindest-Zap-Betrag Maximaler Zap-Betrag Konsens + (0–100)% Schließen nach Tagen Umfrage ist für neue Stimmen geschlossen @@ -372,6 +379,8 @@ anz der Bedingungen ist erforderlich In Relay schreiben Ein Fehler ist beim Abrufen von Relay-Informationen von %1$s aufgetreten Inhaber + Version + Programme Kontakt Unterstützte NIPs Eintrittsgebühren @@ -379,6 +388,7 @@ anz der Bedingungen ist erforderlich Einschränkungen Länder Sprachen + Tags Veröffentlichungsrichtlinie Nachrichtenlänge Abonnements @@ -394,6 +404,8 @@ anz der Bedingungen ist erforderlich Einlösen Keine Lightning-Adresse festgelegt Token in die Zwischenablage kopiert + LIVE + OFFLINE BEENDET GEPLANT Livestream ist offline @@ -401,6 +413,9 @@ anz der Bedingungen ist erforderlich Das Abmelden löscht alle Ihre lokalen Informationen. Stellen Sie sicher, dass Sie Ihre privaten Schlüssel gesichert haben, um einen Kontoverlust zu vermeiden. Möchten Sie fortfahren? Gefolgte Tags Relais + Live + Gemeinschaft + Chats Genehmigte Beiträge Diese Gruppe hat keine Beschreibung oder Regeln. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen. Diese Community hat keine Beschreibung. Sprechen Sie mit dem Eigentümer, um eine hinzuzufügen. @@ -410,6 +425,7 @@ anz der Bedingungen ist erforderlich Immer Nur WLAN Nie + System Hell Dunkel Anwendungseinstellungen @@ -465,6 +481,7 @@ anz der Bedingungen ist erforderlich Die Abstimmungen werden nach der Höhe des Zaps gewichtet. Sie können einen Mindestbetrag festlegen, um Spam zu verhindern, und einen Höchstbetrag, um zu verhindern, dass große Zapper die Abstimmung dominieren. Verwenden Sie denselben Betrag in beiden Feldern, um sicherzustellen, dass jeder Stimme der gleiche Wert zukommt. Lassen Sie es leer, um jeden Betrag zu akzeptieren. Zap konnte nicht gesendet werden Mit dem Benutzer kommunizieren + OK Konnte %1$s nicht erreichen: %2$s Konnte %1$s nicht erreichen: %2$s Konnte das Ergebnis von %1$s nicht verarbeiten: %2$s @@ -472,6 +489,8 @@ anz der Bedingungen ist erforderlich Aktiv für: Startseite Nachrichten + Chats + Global Suche Zaps aufteilen und weiterleiten Unterstützende Clients werden Zaps an die hier hinzugefügten Benutzer aufteilen und weiterleiten, anstatt an Sie @@ -479,6 +498,7 @@ anz der Bedingungen ist erforderlich Benutzername oder Anzeigename Benutzer %1$s hat keine Lightning-Adresse eingerichtet, um Sats zu empfangen Prozent + 25 Zaps aufteilen mit Zaps weiterleiten an Lightning-Wallets nicht gefunden diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a09371f77..54da2b17e 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -430,6 +430,8 @@ Képek/GIF-ek automatikus betöltése Videók automatikus betöltése Az URL előnézetének automatikus megjelenítése + Magával ragadó görgetés + Navigációs sáv görgetés közbeni elrejtése Kép Betöltése Spammerek Lenémítva. Kattínts a feloldásért @@ -502,4 +504,6 @@ Aláírási kérés elutasítva Nem található tárca a Lighning számla kifizetésére (Hiba: %1$s). Kérjük, a zap-ek használatához telepítsen egy Lightning pénztárcát Nem található tárca a Lighning számla kifizetésére. Kérjük, a zap-ek használatához telepítsen egy Lightning pénztárcát + Elrejtett Szavak + Új szó vagy mondat elrejtése