From a034d2f96e88a20c09c3f28c7c2ed95407c9983f Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Mar 2024 14:02:50 -0400 Subject: [PATCH 01/14] Updating Jackson --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ed198b6e2..301746ec8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ espressoCore = "3.5.1" firebaseBom = "32.7.4" fragmentKtx = "1.6.2" gms = "4.4.1" -jacksonModuleKotlin = "2.16.1" +jacksonModuleKotlin = "2.16.2" jna = "5.14.0" jsoup = "1.17.2" junit = "4.13.2" From 8641bd36c3af32c64faff6ed60c56119e36fde81 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Mar 2024 17:28:30 -0400 Subject: [PATCH 02/14] Fixes Scheduled Tag in LiveStreams --- .../ui/screen/loggedIn/ChannelScreen.kt | 22 ++++++++++--------- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 8 +++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt index 7286cfc0b..91703a58e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChannelScreen.kt @@ -133,7 +133,6 @@ import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.note.ZapReaction import com.vitorpamplona.amethyst.ui.note.elements.DisplayUncitedHashtags import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton -import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.timeAgoShort import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefreshingChatroomFeedView @@ -155,6 +154,7 @@ import com.vitorpamplona.amethyst.ui.theme.SmallBorder import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdPadding import com.vitorpamplona.amethyst.ui.theme.ZeroPadding +import com.vitorpamplona.amethyst.ui.theme.liveStreamTag import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.EmptyTagList import com.vitorpamplona.quartz.events.LiveActivitiesEvent.Companion.STATUS_LIVE @@ -167,6 +167,9 @@ import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.Date import java.util.Locale @Composable @@ -1096,20 +1099,19 @@ fun OfflineFlag() { @Composable fun ScheduledFlag(starts: Long?) { - val context = LocalContext.current - val startsIn = starts?.let { timeAgo(it, context) } + val startsIn = + starts?.let { + SimpleDateFormat.getDateTimeInstance( + DateFormat.SHORT, + DateFormat.SHORT, + ).format(Date(starts * 1000)) + } Text( text = startsIn ?: stringResource(id = R.string.live_stream_planned_tag), color = Color.White, fontWeight = FontWeight.Bold, - modifier = - remember { - Modifier - .clip(SmallBorder) - .background(Color.Black) - .padding(horizontal = 5.dp) - }, + modifier = liveStreamTag, ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 585516526..e3d769455 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.ui.theme +import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth @@ -33,6 +34,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp val Shapes = @@ -220,3 +222,9 @@ val boostedNoteModifier = end = 0.dp, top = 0.dp, ) + +val liveStreamTag = + Modifier + .clip(SmallBorder) + .background(Color.Black) + .padding(horizontal = Size5dp) From f2dc2ef0d08dc77d312d43584afcbfb82f6a6c43 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Mar 2024 17:57:08 -0400 Subject: [PATCH 03/14] Uses the new default factory instead of the 3 separate methods. --- .../service/playback/PlaybackService.kt | 141 +++++++----------- 1 file changed, 58 insertions(+), 83 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt index 5ea55e77d..02cc61bed 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/PlaybackService.kt @@ -23,62 +23,73 @@ package com.vitorpamplona.amethyst.service.playback import android.content.Intent import android.util.Log import androidx.annotation.OptIn +import androidx.media3.common.MediaItem import androidx.media3.common.util.UnstableApi import androidx.media3.datasource.okhttp.OkHttpDataSource -import androidx.media3.exoplayer.hls.HlsMediaSource +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider +import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.MediaSource -import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy import androidx.media3.session.MediaSession import androidx.media3.session.MediaSessionService -import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.service.HttpClientManager +import okhttp3.OkHttpClient + +class WssOrHttpFactory(httpClient: OkHttpClient) : MediaSource.Factory { + @UnstableApi + val http = DefaultMediaSourceFactory(OkHttpDataSource.Factory(httpClient)) + + @UnstableApi + val wss = DefaultMediaSourceFactory(WssStreamDataSource.Factory(httpClient)) + + @OptIn(UnstableApi::class) + override fun setDrmSessionManagerProvider(drmSessionManagerProvider: DrmSessionManagerProvider): MediaSource.Factory { + http.setDrmSessionManagerProvider(drmSessionManagerProvider) + wss.setDrmSessionManagerProvider(drmSessionManagerProvider) + return this + } + + @OptIn(UnstableApi::class) + override fun setLoadErrorHandlingPolicy(loadErrorHandlingPolicy: LoadErrorHandlingPolicy): MediaSource.Factory { + http.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) + wss.setLoadErrorHandlingPolicy(loadErrorHandlingPolicy) + return this + } + + @OptIn(UnstableApi::class) + override fun getSupportedTypes(): IntArray { + return http.supportedTypes + } + + @OptIn(UnstableApi::class) + override fun createMediaSource(mediaItem: MediaItem): MediaSource { + return if (mediaItem.mediaId.startsWith("wss")) { + wss.createMediaSource(mediaItem) + } else { + http.createMediaSource(mediaItem) + } + } +} @UnstableApi // Extend MediaSessionService class PlaybackService : MediaSessionService() { private var videoViewedPositionCache = VideoViewedPositionCache() - private var managerHls: MultiPlayerPlaybackManager? = null - private var managerProgressive: MultiPlayerPlaybackManager? = null - private var managerLocal: MultiPlayerPlaybackManager? = null + private var managerAllInOne: MultiPlayerPlaybackManager? = null - fun newHslDataSource(): MediaSource.Factory { - return HlsMediaSource.Factory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient())) + fun newAllInOneDataSource(): MediaSource.Factory { + // This might be needed for live kit. + // return WssOrHttpFactory(HttpClientManager.getHttpClient()) + return DefaultMediaSourceFactory(OkHttpDataSource.Factory(HttpClientManager.getHttpClient())) } - fun newProgressiveDataSource(): MediaSource.Factory { - return ProgressiveMediaSource.Factory( - (applicationContext as Amethyst).videoCache.get(HttpClientManager.getHttpClient()), - ) - } - - fun lazyHlsDS(): MultiPlayerPlaybackManager { - managerHls?.let { + fun lazyDS(): MultiPlayerPlaybackManager { + managerAllInOne?.let { return it } - val newInstance = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache) - managerHls = newInstance - return newInstance - } - - fun lazyProgressiveDS(): MultiPlayerPlaybackManager { - managerProgressive?.let { - return it - } - - val newInstance = - MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache) - managerProgressive = newInstance - return newInstance - } - - fun lazyLocalDS(): MultiPlayerPlaybackManager { - managerLocal?.let { - return it - } - - val newInstance = MultiPlayerPlaybackManager(cachedPositions = videoViewedPositionCache) - managerLocal = newInstance + val newInstance = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache) + managerAllInOne = newInstance return newInstance } @@ -94,15 +105,11 @@ class PlaybackService : MediaSessionService() { } private fun onProxyUpdated() { - val toDestroyHls = managerHls - val toDestroyProgressive = managerProgressive + val toDestroyAllInOne = managerAllInOne - managerHls = MultiPlayerPlaybackManager(newHslDataSource(), videoViewedPositionCache) - managerProgressive = - MultiPlayerPlaybackManager(newProgressiveDataSource(), videoViewedPositionCache) + managerAllInOne = MultiPlayerPlaybackManager(newAllInOneDataSource(), videoViewedPositionCache) - toDestroyHls?.releaseAppPlayers() - toDestroyProgressive?.releaseAppPlayers() + toDestroyAllInOne?.releaseAppPlayers() } override fun onTaskRemoved(rootIntent: Intent?) { @@ -116,23 +123,11 @@ class PlaybackService : MediaSessionService() { HttpClientManager.proxyChangeListeners.remove(this@PlaybackService::onProxyUpdated) - managerHls?.releaseAppPlayers() - managerLocal?.releaseAppPlayers() - managerProgressive?.releaseAppPlayers() + managerAllInOne?.releaseAppPlayers() super.onDestroy() } - fun getAppropriateMediaSessionManager(fileName: String): MultiPlayerPlaybackManager? { - return if (fileName.startsWith("file")) { - lazyLocalDS() - } else if (fileName.endsWith("m3u8")) { - lazyHlsDS() - } else { - lazyProgressiveDS() - } - } - override fun onUpdateNotification( session: MediaSession, startInForegroundRequired: Boolean, @@ -141,38 +136,18 @@ class PlaybackService : MediaSessionService() { super.onUpdateNotification(session, startInForegroundRequired) // Overrides the notification with any player actually playing - managerHls?.playingContent()?.forEach { + managerAllInOne?.playingContent()?.forEach { if (it.player.isPlaying) { super.onUpdateNotification(it, startInForegroundRequired) } } - managerLocal?.playingContent()?.forEach { - if (it.player.isPlaying) { - super.onUpdateNotification(session, startInForegroundRequired) - } - } - managerProgressive?.playingContent()?.forEach { - if (it.player.isPlaying) { - super.onUpdateNotification(session, startInForegroundRequired) - } - } // Overrides again with playing with audio - managerHls?.playingContent()?.forEach { + managerAllInOne?.playingContent()?.forEach { if (it.player.isPlaying && it.player.volume > 0) { super.onUpdateNotification(it, startInForegroundRequired) } } - managerLocal?.playingContent()?.forEach { - if (it.player.isPlaying && it.player.volume > 0) { - super.onUpdateNotification(session, startInForegroundRequired) - } - } - managerProgressive?.playingContent()?.forEach { - if (it.player.isPlaying && it.player.volume > 0) { - super.onUpdateNotification(session, startInForegroundRequired) - } - } } // Return a MediaSession to link with the MediaController that is making @@ -182,9 +157,9 @@ class PlaybackService : MediaSessionService() { val uri = controllerInfo.connectionHints.getString("uri") ?: return null val callbackUri = controllerInfo.connectionHints.getString("callbackUri") - val manager = getAppropriateMediaSessionManager(uri) + val manager = lazyDS() - return manager?.getMediaSession( + return manager.getMediaSession( id, uri, callbackUri, From 45974fb09be46d26bdc701b9f84b0f30b384ef58 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Mar 2024 17:57:31 -0400 Subject: [PATCH 04/14] change the isOnlineCheck to prepare for nostr nests websocket-based streaming. --- .../amethyst/service/OnlineCheck.kt | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt index fd508b905..b2eff00bd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/OnlineCheck.kt @@ -24,7 +24,11 @@ import android.util.Log import android.util.LruCache import androidx.compose.runtime.Immutable import com.vitorpamplona.amethyst.BuildConfig +import com.vitorpamplona.quartz.crypto.CryptoUtils +import okhttp3.EventListener +import okhttp3.Protocol import okhttp3.Request +import okio.ByteString.Companion.toByteString import kotlin.coroutines.cancellation.CancellationException @Immutable data class OnlineCheckResult(val timeInMs: Long, val online: Boolean) @@ -49,21 +53,44 @@ object OnlineChecker { return checkOnlineCache.get(url).online } - Log.d("OnlineChecker", "isOnline $url") - return try { - val request = - Request.Builder() - .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") - .url(url) - .get() - .build() - val result = - HttpClientManager.getHttpClient().newCall(request).execute().use { - checkNotInMainThread() - it.isSuccessful + if (url.startsWith("wss")) { + val request = + Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url.replace("wss+livekit://", "wss://")) + .header("Upgrade", "websocket") + .header("Connection", "Upgrade") + .header("Sec-WebSocket-Key", CryptoUtils.random(16).toByteString().base64()) + .header("Sec-WebSocket-Version", "13") + .header("Sec-WebSocket-Extensions", "permessage-deflate") + .build() + + val client = + HttpClientManager.getHttpClient().newBuilder() + .eventListener(EventListener.NONE) + .protocols(listOf(Protocol.HTTP_1_1)) + .build() + + client.newCall(request).execute().use { + checkNotInMainThread() + it.isSuccessful + } + } else { + val request = + Request.Builder() + .header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}") + .url(url) + .get() + .build() + + HttpClientManager.getHttpClient().newCall(request).execute().use { + checkNotInMainThread() + it.isSuccessful + } } + checkOnlineCache.put(url, OnlineCheckResult(System.currentTimeMillis(), result)) result } catch (e: Exception) { From 99850573f7b1cf74a6ab6646928e986323f4cc5c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Mar 2024 17:58:01 -0400 Subject: [PATCH 05/14] Reduces video cache from 10 to 4 videos. --- .../amethyst/service/playback/MultiPlayerPlaybackManager.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt index 8a9ef89bb..fac3a396a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/MultiPlayerPlaybackManager.kt @@ -46,7 +46,7 @@ class MultiPlayerPlaybackManager( private val playingMap = mutableMapOf() private val cache = - object : LruCache(10) { // up to 10 videos in the screen at the same time + object : LruCache(4) { // up to 4 videos in the screen at the same time override fun entryRemoved( evicted: Boolean, key: String?, From eadb28321c442f78cb8fc5e7797834ad08bbf4fe Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 13 Mar 2024 17:58:26 -0400 Subject: [PATCH 06/14] Adds missing classes to support WebServer conections in the Video Playback --- .../playback/WssDataStreamCollector.kt | 54 +++++++++ .../service/playback/WssStreamDataSource.kt | 112 ++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssDataStreamCollector.kt create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssStreamDataSource.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssDataStreamCollector.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssDataStreamCollector.kt new file mode 100644 index 000000000..2af443c10 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssDataStreamCollector.kt @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2024 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 + +import okhttp3.WebSocket +import okhttp3.WebSocketListener +import okio.ByteString +import java.util.concurrent.ConcurrentSkipListSet + +class WssDataStreamCollector : WebSocketListener() { + private val wssData = ConcurrentSkipListSet() + + override fun onMessage( + webSocket: WebSocket, + bytes: ByteString, + ) { + wssData.add(bytes) + } + + override fun onClosing( + webSocket: WebSocket, + code: Int, + reason: String, + ) { + super.onClosing(webSocket, code, reason) + wssData.removeAll(wssData) + } + + fun canStream(): Boolean { + return wssData.size > 0 + } + + fun getNextStream(): ByteString { + return wssData.pollFirst() + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssStreamDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssStreamDataSource.kt new file mode 100644 index 000000000..0a95bd7d6 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/playback/WssStreamDataSource.kt @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2024 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 + +import android.net.Uri +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.BaseDataSource +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.WebSocket +import kotlin.math.min + +@OptIn(UnstableApi::class) +class WssStreamDataSource(val httpClient: OkHttpClient) : BaseDataSource(true) { + val dataStreamCollector: WssDataStreamCollector = WssDataStreamCollector() + var webSocketClient: WebSocket? = null + + private var currentByteStream: ByteArray? = null + private var currentPosition = 0 + private var remainingBytes = 0 + + override fun open(dataSpec: DataSpec): Long { + // Form the request and open the socket. + // Provide the listener + // which collects the data for us (Previous class). + webSocketClient = + httpClient.newWebSocket( + Request.Builder().apply { + dataSpec.httpRequestHeaders.forEach { entry -> + addHeader(entry.key, entry.value) + } + }.url(dataSpec.uri.toString()).build(), + dataStreamCollector, + ) + + return -1 // Return -1 as the size is unknown (streaming) + } + + override fun getUri(): Uri? { + webSocketClient?.request()?.url?.let { + return Uri.parse(it.toString()) + } + + return null + } + + override fun read( + target: ByteArray, + offset: Int, + length: Int, + ): Int { + // return 0 (nothing read) when no data present... + if (currentByteStream == null && !dataStreamCollector.canStream()) { + return 0 + } + + // parse one (data) ByteString at a time. + // reset the current position and remaining bytes + // for every new data + if (currentByteStream == null) { + currentByteStream = dataStreamCollector.getNextStream().toByteArray() + currentPosition = 0 + remainingBytes = currentByteStream?.size ?: 0 + } + + val readSize = min(length, remainingBytes) + + currentByteStream?.copyInto(target, offset, currentPosition, currentPosition + readSize) + currentPosition += readSize + remainingBytes -= readSize + + // once the data is read set currentByteStream to null + // so the next data would be collected to process in next + // iteration. + if (remainingBytes == 0) { + currentByteStream = null + } + + return readSize + } + + override fun close() { + // close the socket and relase the resources + webSocketClient?.cancel() + } + + // Factory class for DataSource + class Factory(val okHttpClient: OkHttpClient) : DataSource.Factory { + override fun createDataSource(): DataSource = WssStreamDataSource(okHttpClient) + } +} From 2d6aab954f509d8213dbdea11ffbfc67347deeb4 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Wed, 13 Mar 2024 22:00:20 +0000 Subject: [PATCH 07/14] New Crowdin translations by GitHub Action --- app/src/main/res/values-cs/strings.xml | 4 ++++ app/src/main/res/values-de/strings.xml | 4 ++++ app/src/main/res/values-fr/strings.xml | 4 ++++ app/src/main/res/values-pt-rBR/strings.xml | 4 ++++ app/src/main/res/values-sv-rSE/strings.xml | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 1fbfee90c..41d6f5ab6 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -439,6 +439,8 @@ Vždy Pouze Wi-Fi Nikdy + Hotovo + Zjednodušené Systém Světlý Tmavý @@ -450,6 +452,8 @@ Automaticky zobrazit náhled URL Imersivní rolování Skrýt navigační panely při rolování + Režim UI + Zvolte styl příspěvku Načíst obrázek Spamovací uživatelé Ztlumené. Klikněte pro odztlumení diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index cd528fecc..62f880b57 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -444,6 +444,8 @@ anz der Bedingungen ist erforderlich Immer Nur WLAN Nie + Fertig + Vereinfacht System Hell Dunkel @@ -455,6 +457,8 @@ anz der Bedingungen ist erforderlich URL-Vorschau automatisch anzeigen Immersives Scrollen Navigationsleisten beim Scrollen ausblenden + UI-Modus + Wählen Sie den Beitragsstil Bild laden Spammer Stummgeschaltet. Drücken, um Ton einzuschalten diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 371847a85..048ba6fae 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -441,6 +441,8 @@ Toujours Wifi uniquement Jamais + Complet + Simplifié Système Clair Sombre @@ -452,6 +454,8 @@ Prévisualisation des URLs Défilement immersif Masquer les barres de navigation lors du défilement + Mode UI + Choisir le style du message Charger l\'image Spammeurs Silencieux. Cliquer pour réactiver le son diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index d7484c17d..e494e0a16 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -439,6 +439,8 @@ Sempre Somente wifi Nunca + Concluído + Simplificado Sistema Claro Escuro @@ -450,6 +452,8 @@ Mostrar automaticamente a visualização da URL Rolagem Imersiva Ocultar as barras de navegação ao rolar + Modo de interface + Escolha o estilo da publicação Carregar imagem Spammers Silenciado. Clique para ativar o som diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 0378413f2..03888e2d3 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -438,6 +438,8 @@ Alltid Endast Wi-Fi Aldrig + Slutförd + Förenklad System Ljus Mörk @@ -449,6 +451,8 @@ Visa automatiskt förhandsgranskning av URL Omslutande bläddring Dölj navigeringsfält vid bläddring + UI läge + Välj stilen för inlägg Ladda bild Spammare Ljud avstängt. Klicka för att ta bort ljudlöst From 1014e292891fe4fc2af909967abe8d7304d57ca8 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 14 Mar 2024 09:54:43 -0400 Subject: [PATCH 08/14] Fixes some contract issues when follow and names are the same. --- .../vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt | 2 +- .../com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt index b2439727d..f4e338316 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/EditPostViewModel.kt @@ -257,7 +257,7 @@ open class EditPostViewModel() : ViewModel() { viewModelScope.launch(Dispatchers.IO) { userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) - .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })) + .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex })) .reversed() } } else { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt index 7643e8e80..ff45610e0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.kt @@ -693,7 +693,7 @@ open class NewPostViewModel() : ViewModel() { viewModelScope.launch(Dispatchers.IO) { userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) - .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })) + .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex })) .reversed() } } else { @@ -716,7 +716,7 @@ open class NewPostViewModel() : ViewModel() { viewModelScope.launch(Dispatchers.IO) { userSuggestions = LocalCache.findUsersStartingWith(lastWord.removePrefix("@")) - .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() })) + .sortedWith(compareBy({ account?.isFollowing(it) }, { it.toBestDisplayName() }, { it.pubkeyHex })) .reversed() } } else { From 837865a6991e5140cec3632fce73c1ca7f79081e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 14 Mar 2024 11:11:38 -0400 Subject: [PATCH 09/14] Adds simplified views to the video and chat feeds. --- .../ui/note/ChatroomMessageCompose.kt | 166 ++++++++---------- .../ui/screen/loggedIn/VideoScreen.kt | 62 +++---- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 5 +- 3 files changed, 112 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 8ea57ab90..17d717e3b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -21,6 +21,7 @@ package com.vitorpamplona.amethyst.ui.note import androidx.compose.animation.Crossfade +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable @@ -33,7 +34,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -41,6 +41,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -51,7 +52,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.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver @@ -66,7 +66,6 @@ import androidx.lifecycle.map import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User -import com.vitorpamplona.amethyst.ui.components.CreateClickableTextWithEmoji import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage import com.vitorpamplona.amethyst.ui.components.SensitivityWarning @@ -84,10 +83,9 @@ import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeightChat import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size15Modifier import com.vitorpamplona.amethyst.ui.theme.Size20dp -import com.vitorpamplona.amethyst.ui.theme.Size25dp import com.vitorpamplona.amethyst.ui.theme.Size5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer -import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer +import com.vitorpamplona.amethyst.ui.theme.chatAuthorImage import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.subtleBorder @@ -306,12 +304,16 @@ fun NormalChatNote( val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier + val showDetails = remember { mutableStateOf(false) } + val clickableModifier = remember { Modifier.combinedClickable( onClick = { if (note.event is ChannelCreateEvent) { nav("Channel/${note.idHex}") + } else { + showDetails.value = !showDetails.value } }, onLongClick = { popupExpanded = true }, @@ -341,6 +343,7 @@ fun NormalChatNote( onWantsToReply, canPreview, availableBubbleSize, + showDetails, accountViewModel, nav, ) @@ -367,6 +370,7 @@ private fun RenderBubble( onWantsToReply: (Note) -> Unit, canPreview: Boolean, availableBubbleSize: MutableState, + showDetails: State, accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { @@ -375,12 +379,13 @@ private fun RenderBubble( val bubbleModifier = remember { Modifier - .padding(start = 10.dp, end = 5.dp, bottom = 5.dp) + .padding(start = 10.dp, end = 10.dp, bottom = 5.dp) .onSizeChanged { if (bubbleSize.intValue != it.width) { bubbleSize.intValue = it.width } } + .animateContentSize() } Column(modifier = bubbleModifier) { @@ -388,14 +393,15 @@ private fun RenderBubble( drawAuthorInfo, baseNote, alignment, - nav, + availableBubbleSize, innerQuote, backgroundBubbleColor, - accountViewModel, + bubbleSize, onWantsToReply, canPreview, - bubbleSize, - availableBubbleSize, + showDetails, + accountViewModel, + nav, ) } } @@ -405,14 +411,15 @@ private fun MessageBubbleLines( drawAuthorInfo: Boolean, baseNote: Note, alignment: Arrangement.Horizontal, - nav: (String) -> Unit, + availableBubbleSize: MutableState, innerQuote: Boolean, backgroundBubbleColor: MutableState, - accountViewModel: AccountViewModel, + bubbleSize: MutableState, onWantsToReply: (Note) -> Unit, canPreview: Boolean, - bubbleSize: MutableState, - availableBubbleSize: MutableState, + showDetails: State, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, ) { if (drawAuthorInfo) { DrawAuthorInfo( @@ -421,8 +428,6 @@ private fun MessageBubbleLines( accountViewModel.settings.showProfilePictures.value, nav, ) - } else { - Spacer(modifier = StdVertSpacer) } RenderReplyRow( @@ -442,32 +447,34 @@ private fun MessageBubbleLines( nav = nav, ) - ConstrainedStatusRow( - bubbleSize = bubbleSize, - availableBubbleSize = availableBubbleSize, - firstColumn = { - IncognitoBadge(baseNote) - ChatTimeAgo(baseNote) - RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav) - Spacer(modifier = DoubleHorzSpacer) - }, - secondColumn = { - LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav) - Spacer(modifier = StdHorzSpacer) - ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) - Spacer(modifier = DoubleHorzSpacer) - ReplyReaction( - baseNote = baseNote, - grayTint = MaterialTheme.colorScheme.placeholderText, - accountViewModel = accountViewModel, - showCounter = false, - iconSizeModifier = Size15Modifier, - ) { - onWantsToReply(baseNote) - } - Spacer(modifier = StdHorzSpacer) - }, - ) + if (showDetails.value || baseNote.zaps.isNotEmpty() || baseNote.zapPayments.isNotEmpty() || baseNote.reactions.isNotEmpty()) { + ConstrainedStatusRow( + bubbleSize = bubbleSize, + availableBubbleSize = availableBubbleSize, + firstColumn = { + IncognitoBadge(baseNote) + ChatTimeAgo(baseNote) + RelayBadgesHorizontal(baseNote, accountViewModel, nav = nav) + Spacer(modifier = DoubleHorzSpacer) + }, + secondColumn = { + LikeReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav) + Spacer(modifier = StdHorzSpacer) + ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) + Spacer(modifier = DoubleHorzSpacer) + ReplyReaction( + baseNote = baseNote, + grayTint = MaterialTheme.colorScheme.placeholderText, + accountViewModel = accountViewModel, + showCounter = false, + iconSizeModifier = Size15Modifier, + ) { + onWantsToReply(baseNote) + } + Spacer(modifier = StdHorzSpacer) + }, + ) + } } @Composable @@ -479,9 +486,7 @@ private fun RenderReplyRow( nav: (String) -> Unit, onWantsToReply: (Note) -> Unit, ) { - val hasReply by remember { derivedStateOf { !innerQuote && note.replyTo?.lastOrNull() != null } } - - if (hasReply) { + if (!innerQuote && note.replyTo?.lastOrNull() != null) { RenderReply(note, backgroundBubbleColor, accountViewModel, nav, onWantsToReply) } } @@ -504,8 +509,8 @@ private fun RenderReply( replyTo.value?.let { note -> ChatroomMessageCompose( - note, - null, + baseNote = note, + routeForLastRead = null, innerQuote = true, parentBackgroundColor = backgroundBubbleColor, accountViewModel = accountViewModel, @@ -525,7 +530,7 @@ private fun NoteRow( nav: (String) -> Unit, ) { Row(verticalAlignment = Alignment.CenterVertically) { - when (remember(note) { note.event }) { + when (note.event) { is ChannelCreateEvent -> { RenderCreateChannelNote(note) } @@ -719,41 +724,36 @@ private fun DrawAuthorInfo( loadProfilePicture: Boolean, nav: (String) -> Unit, ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = alignment, - modifier = Modifier.padding(top = Size10dp), - ) { - DisplayAndWatchNoteAuthor(baseNote, loadProfilePicture, nav) + baseNote.author?.let { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = alignment, + modifier = + Modifier + .padding(top = Size10dp) + .clickable { + nav("User/${baseNote.author?.pubkeyHex}") + }, + ) { + WatchAndDisplayUser(it, loadProfilePicture, nav) + } } } -@Composable -private fun DisplayAndWatchNoteAuthor( - baseNote: Note, - loadProfilePicture: Boolean, - nav: (String) -> Unit, -) { - val author = remember { baseNote.author } - author?.let { WatchAndDisplayUser(it, loadProfilePicture, nav) } -} - @Composable private fun WatchAndDisplayUser( author: User, loadProfilePicture: Boolean, nav: (String) -> Unit, ) { - val route = "User/${author.pubkeyHex}" - val userState by author.live().userMetadataInfo.observeAsState() - UserIcon(author.pubkeyHex, userState?.picture, loadProfilePicture, nav, route) + UserIcon(author.pubkeyHex, userState?.picture, loadProfilePicture) - userState?.let { - it.bestName()?.let { name -> - DisplayMessageUsername(name, it.tags, route, nav) - } + if (userState != null) { + DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList) + } else { + DisplayMessageUsername(author.pubkeyDisplayHex(), EmptyTagList) } } @@ -762,40 +762,26 @@ private fun UserIcon( pubkeyHex: String, userProfilePicture: String?, loadProfilePicture: Boolean, - nav: (String) -> Unit, - route: String, ) { RobohashFallbackAsyncImage( robot = pubkeyHex, model = userProfilePicture, contentDescription = stringResource(id = R.string.profile_image), loadProfilePicture = loadProfilePicture, - modifier = - remember { - Modifier - .width(Size25dp) - .height(Size25dp) - .clip(shape = CircleShape) - .clickable(onClick = { nav(route) }) - }, + modifier = chatAuthorImage, ) } @Composable private fun DisplayMessageUsername( userDisplayName: String, - userTags: ImmutableListOfLists?, - route: String, - nav: (String) -> Unit, + userTags: ImmutableListOfLists, ) { Spacer(modifier = StdHorzSpacer) - CreateClickableTextWithEmoji( - clickablePart = userDisplayName, - maxLines = 1, + CreateTextWithEmoji( + text = userDisplayName, tags = userTags, + maxLines = 1, fontWeight = FontWeight.Bold, - overrideColor = MaterialTheme.colorScheme.onBackground, - route = route, - nav = nav, ) } 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 59eb824e3..cf77d9a21 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 @@ -35,7 +35,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState @@ -64,6 +63,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.vitorpamplona.amethyst.R +import com.vitorpamplona.amethyst.model.FeatureSetType import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.NostrVideoDataSource import com.vitorpamplona.amethyst.ui.actions.NewPostView @@ -91,11 +91,14 @@ import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel import com.vitorpamplona.amethyst.ui.screen.RefresheableBox import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys import com.vitorpamplona.amethyst.ui.screen.rememberForeverPagerState +import com.vitorpamplona.amethyst.ui.theme.AuthorInfoVideoFeed +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.Size35Modifier import com.vitorpamplona.amethyst.ui.theme.Size35dp import com.vitorpamplona.amethyst.ui.theme.Size39Modifier import com.vitorpamplona.amethyst.ui.theme.Size40Modifier import com.vitorpamplona.amethyst.ui.theme.Size40dp +import com.vitorpamplona.amethyst.ui.theme.Size55dp import com.vitorpamplona.amethyst.ui.theme.onBackgroundColorFilter import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.quartz.events.FileHeaderEvent @@ -321,8 +324,8 @@ private fun RenderVideoOrPictureNote( accountViewModel: AccountViewModel, nav: (String) -> Unit, ) { - Column(remember { Modifier.fillMaxSize(1f) }, verticalArrangement = Arrangement.Center) { - Row(remember { Modifier.weight(1f) }, verticalAlignment = Alignment.CenterVertically) { + Column(Modifier.fillMaxSize(1f), verticalArrangement = Arrangement.Center) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { val noteEvent = remember { note.event } if (noteEvent is FileHeaderEvent) { FileHeaderDisplay(note, false, accountViewModel) @@ -332,18 +335,17 @@ private fun RenderVideoOrPictureNote( } } - Row(verticalAlignment = Alignment.Bottom, modifier = remember { Modifier.fillMaxSize(1f) }) { - Column(remember { Modifier.weight(1f) }) { + Row(modifier = Modifier.fillMaxSize(1f), verticalAlignment = Alignment.Bottom) { + Column(Modifier.weight(1f), verticalArrangement = Arrangement.Center) { RenderAuthorInformation(note, nav, accountViewModel) } Column( - remember { Modifier.width(65.dp).padding(bottom = 10.dp) }, + modifier = AuthorInfoVideoFeed, verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, ) { - Row(horizontalArrangement = Arrangement.Center) { - ReactionsColumn(note, accountViewModel, nav) - } + ReactionsColumn(note, accountViewModel, nav) } } } @@ -354,32 +356,34 @@ private fun RenderAuthorInformation( nav: (String) -> Unit, accountViewModel: AccountViewModel, ) { - Row(remember { Modifier.padding(10.dp) }, verticalAlignment = Alignment.Bottom) { - Column(remember { Modifier.size(55.dp) }, verticalArrangement = Arrangement.Center) { - NoteAuthorPicture(note, nav, accountViewModel, 55.dp) - } + Row(modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp), verticalAlignment = Alignment.CenterVertically) { + NoteAuthorPicture(note, nav, accountViewModel, Size55dp) + + Spacer(modifier = DoubleHorzSpacer) Column( - remember { Modifier.padding(start = 10.dp, end = 10.dp).height(65.dp).weight(1f) }, + Modifier.height(65.dp).weight(1f), verticalArrangement = Arrangement.Center, ) { Row(verticalAlignment = Alignment.CenterVertically) { NoteUsernameDisplay(note, remember { Modifier.weight(1f) }) VideoUserOptionAction(note, accountViewModel, nav) } - Row(verticalAlignment = Alignment.CenterVertically) { - ObserveDisplayNip05Status( - note.author!!, - Modifier.weight(1f), - accountViewModel, - nav = nav, - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 2.dp), - ) { - RelayBadges(baseNote = note, accountViewModel, nav) + if (accountViewModel.settings.featureSet != FeatureSetType.SIMPLIFIED) { + Row(verticalAlignment = Alignment.CenterVertically) { + ObserveDisplayNip05Status( + note.author!!, + Modifier.weight(1f), + accountViewModel, + nav = nav, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 2.dp), + ) { + RelayBadges(baseNote = note, accountViewModel, nav) + } } } } @@ -457,11 +461,9 @@ fun ReactionsColumn( ) } - Spacer(modifier = Modifier.height(8.dp)) - Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(bottom = 75.dp, end = 20.dp), + modifier = Modifier.padding(bottom = 75.dp, end = 10.dp), ) { ReplyReaction( baseNote = baseNote, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index e3d769455..96424ca0a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -175,7 +175,7 @@ val ZeroPadding = PaddingValues(0.dp) val FeedPadding = PaddingValues(top = 10.dp, bottom = 10.dp) val ButtonPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp) -val ChatPaddingInnerQuoteModifier = Modifier.padding(top = 10.dp, end = 5.dp) +val ChatPaddingInnerQuoteModifier = Modifier.padding(top = 10.dp) val ChatPaddingModifier = Modifier.fillMaxWidth(1f) .padding( @@ -228,3 +228,6 @@ val liveStreamTag = .clip(SmallBorder) .background(Color.Black) .padding(horizontal = Size5dp) + +val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape) +val AuthorInfoVideoFeed = Modifier.width(75.dp) From 86c328c8a77880754f85826df7993827a7e6fde3 Mon Sep 17 00:00:00 2001 From: Crowdin Bot Date: Thu, 14 Mar 2024 15:13:23 +0000 Subject: [PATCH 10/14] New Crowdin translations by GitHub Action --- app/src/main/res/values-ar/strings.xml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0c2a3d4a0..68a24913d 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -495,4 +495,14 @@ أخرى فشل في تحميل الوسائط خطأ في التحميل: %1$s + تسجيل الدخول بواسطة رمز الQR + الصفحة الرئيسية + البحث + الرسائل + مرشحات الأمان + منشور جديد للمجتمع + فتح جميع ردود الفعل على هذا المنشور + إغلاق جميع ردود الفعل على هذا المنشور + الرد + مسح رمز ال QR From f941397cc4748024f1536917fe8bf6a17ede0a48 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 14 Mar 2024 11:54:53 -0400 Subject: [PATCH 11/14] Fixes tickmarks on dropdowns --- .../components/TranslatableRichTextViewer.kt | 136 +++++++++--------- 1 file changed, 72 insertions(+), 64 deletions(-) diff --git a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt index cff6738d5..4b27d4234 100644 --- a/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt +++ b/app/src/play/java/com/vitorpamplona/amethyst/ui/components/TranslatableRichTextViewer.kt @@ -222,24 +222,26 @@ private fun TranslationMessage( ) { DropdownMenuItem( text = { - if (source in accountViewModel.account.dontTranslateFrom) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), + Row { + if (source in accountViewModel.account.dontTranslateFrom) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + + Spacer(modifier = Modifier.size(10.dp)) + + Text( + stringResource( + R.string.translations_never_translate_from_lang, + Locale(source).displayName, + ), ) - } else { - Spacer(modifier = Modifier.size(24.dp)) } - - Spacer(modifier = Modifier.size(10.dp)) - - Text( - stringResource( - R.string.translations_never_translate_from_lang, - Locale(source).displayName, - ), - ) }, onClick = { scope.launch(Dispatchers.IO) { @@ -251,24 +253,26 @@ private fun TranslationMessage( HorizontalDivider(thickness = DividerThickness) DropdownMenuItem( text = { - if (accountViewModel.account.preferenceBetween(source, target) == source) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), + Row { + if (accountViewModel.account.preferenceBetween(source, target) == source) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + + Spacer(modifier = Modifier.size(10.dp)) + + Text( + stringResource( + R.string.translations_show_in_lang_first, + Locale(source).displayName, + ), ) - } else { - Spacer(modifier = Modifier.size(24.dp)) } - - Spacer(modifier = Modifier.size(10.dp)) - - Text( - stringResource( - R.string.translations_show_in_lang_first, - Locale(source).displayName, - ), - ) }, onClick = { scope.launch(Dispatchers.IO) { @@ -279,24 +283,26 @@ private fun TranslationMessage( ) DropdownMenuItem( text = { - if (accountViewModel.account.preferenceBetween(source, target) == target) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), + Row { + if (accountViewModel.account.preferenceBetween(source, target) == target) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + + Spacer(modifier = Modifier.size(10.dp)) + + Text( + stringResource( + R.string.translations_show_in_lang_first, + Locale(target).displayName, + ), ) - } else { - Spacer(modifier = Modifier.size(24.dp)) } - - Spacer(modifier = Modifier.size(10.dp)) - - Text( - stringResource( - R.string.translations_show_in_lang_first, - Locale(target).displayName, - ), - ) }, onClick = { scope.launch(Dispatchers.IO) { @@ -312,24 +318,26 @@ private fun TranslationMessage( languageList.get(i)?.let { lang -> DropdownMenuItem( text = { - if (lang.language in accountViewModel.account.translateTo) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(24.dp), + Row { + if (lang.language in accountViewModel.account.translateTo) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(24.dp), + ) + } else { + Spacer(modifier = Modifier.size(24.dp)) + } + + Spacer(modifier = Modifier.size(10.dp)) + + Text( + stringResource( + R.string.translations_always_translate_to_lang, + lang.displayName, + ), ) - } else { - Spacer(modifier = Modifier.size(24.dp)) } - - Spacer(modifier = Modifier.size(10.dp)) - - Text( - stringResource( - R.string.translations_always_translate_to_lang, - lang.displayName, - ), - ) }, onClick = { scope.launch(Dispatchers.IO) { From 2c54ba1a9248e52afda5bee8e84497c1dfbeb8d3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 14 Mar 2024 11:55:13 -0400 Subject: [PATCH 12/14] Corrects the starting point to show reactions on messages. --- .../amethyst/ui/note/ChatroomMessageCompose.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 17d717e3b..07d60234a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -304,7 +304,10 @@ fun NormalChatNote( val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier - val showDetails = remember { mutableStateOf(false) } + val showDetails = + remember { + mutableStateOf(note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty()) + } val clickableModifier = remember { @@ -447,7 +450,7 @@ private fun MessageBubbleLines( nav = nav, ) - if (showDetails.value || baseNote.zaps.isNotEmpty() || baseNote.zapPayments.isNotEmpty() || baseNote.reactions.isNotEmpty()) { + if (showDetails.value) { ConstrainedStatusRow( bubbleSize = bubbleSize, availableBubbleSize = availableBubbleSize, From 1abdb425523f777b7cbda0171be200c11a0f7142 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 14 Mar 2024 11:55:24 -0400 Subject: [PATCH 13/14] Sets default to the simplified view. --- .../main/java/com/vitorpamplona/amethyst/model/Settings.kt | 4 ++-- .../amethyst/ui/screen/SharedPreferencesViewModel.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt index 3d5993718..fc5c83fba 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Settings.kt @@ -34,7 +34,7 @@ data class Settings( val automaticallyShowProfilePictures: ConnectivityType = ConnectivityType.ALWAYS, val dontShowPushNotificationSelector: Boolean = false, val dontAskForNotificationPermissions: Boolean = false, - val featureSet: FeatureSetType = FeatureSetType.COMPLETE, + val featureSet: FeatureSetType = FeatureSetType.SIMPLIFIED, ) enum class ThemeType(val screenCode: Int, val resourceId: Int) { @@ -92,7 +92,7 @@ fun parseFeatureSetType(screenCode: Int): FeatureSetType { FeatureSetType.COMPLETE.screenCode -> FeatureSetType.COMPLETE FeatureSetType.SIMPLIFIED.screenCode -> FeatureSetType.SIMPLIFIED else -> { - FeatureSetType.COMPLETE + FeatureSetType.SIMPLIFIED } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt index 83f506cfd..cdbbbd958 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/SharedPreferencesViewModel.kt @@ -53,7 +53,7 @@ class SettingsState() { var automaticallyShowProfilePictures by mutableStateOf(ConnectivityType.ALWAYS) var dontShowPushNotificationSelector by mutableStateOf(false) var dontAskForNotificationPermissions by mutableStateOf(false) - var featureSet by mutableStateOf(FeatureSetType.COMPLETE) + var featureSet by mutableStateOf(FeatureSetType.SIMPLIFIED) var isOnMobileData: State = mutableStateOf(false) From fbf4f6dd0894683040996bb6bdbddafb233d226a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 14 Mar 2024 14:09:13 -0400 Subject: [PATCH 14/14] Small Refactoring --- .../java/com/vitorpamplona/amethyst/service/relays/Relay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt index 04a6aaaa2..8d2e67d98 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/relays/Relay.kt @@ -452,7 +452,7 @@ class Relay( eventUploadCounterInBytes += event.bytesUsedInMemory() // Sends everything. - Client.allSubscriptions().forEach { sendFilter(requestId = it) } + renewFilters() } } }