mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-04-23 15:04:51 +02:00
Merge branch 'main' into main
This commit is contained in:
commit
8ade5b7e5f
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -46,7 +46,7 @@ class MultiPlayerPlaybackManager(
|
||||
private val playingMap = mutableMapOf<String, MediaSession>()
|
||||
|
||||
private val cache =
|
||||
object : LruCache<String, MediaSession>(10) { // up to 10 videos in the screen at the same time
|
||||
object : LruCache<String, MediaSession>(4) { // up to 4 videos in the screen at the same time
|
||||
override fun entryRemoved(
|
||||
evicted: Boolean,
|
||||
key: String?,
|
||||
|
@ -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,
|
||||
|
@ -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<ByteString>()
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -452,7 +452,7 @@ class Relay(
|
||||
eventUploadCounterInBytes += event.bytesUsedInMemory()
|
||||
|
||||
// Sends everything.
|
||||
Client.allSubscriptions().forEach { sendFilter(requestId = it) }
|
||||
renewFilters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -731,7 +731,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 {
|
||||
@ -758,7 +758,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 {
|
||||
|
@ -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,19 @@ fun NormalChatNote(
|
||||
|
||||
val modif2 = if (innerQuote) Modifier else ChatBubbleMaxSizeModifier
|
||||
|
||||
val showDetails =
|
||||
remember {
|
||||
mutableStateOf(note.zaps.isNotEmpty() || note.zapPayments.isNotEmpty() || note.reactions.isNotEmpty())
|
||||
}
|
||||
|
||||
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 +346,7 @@ fun NormalChatNote(
|
||||
onWantsToReply,
|
||||
canPreview,
|
||||
availableBubbleSize,
|
||||
showDetails,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
@ -367,6 +373,7 @@ private fun RenderBubble(
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
canPreview: Boolean,
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
showDetails: State<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
@ -375,12 +382,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 +396,15 @@ private fun RenderBubble(
|
||||
drawAuthorInfo,
|
||||
baseNote,
|
||||
alignment,
|
||||
nav,
|
||||
availableBubbleSize,
|
||||
innerQuote,
|
||||
backgroundBubbleColor,
|
||||
accountViewModel,
|
||||
bubbleSize,
|
||||
onWantsToReply,
|
||||
canPreview,
|
||||
bubbleSize,
|
||||
availableBubbleSize,
|
||||
showDetails,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -405,14 +414,15 @@ private fun MessageBubbleLines(
|
||||
drawAuthorInfo: Boolean,
|
||||
baseNote: Note,
|
||||
alignment: Arrangement.Horizontal,
|
||||
nav: (String) -> Unit,
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
innerQuote: Boolean,
|
||||
backgroundBubbleColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
bubbleSize: MutableState<Int>,
|
||||
onWantsToReply: (Note) -> Unit,
|
||||
canPreview: Boolean,
|
||||
bubbleSize: MutableState<Int>,
|
||||
availableBubbleSize: MutableState<Int>,
|
||||
showDetails: State<Boolean>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
if (drawAuthorInfo) {
|
||||
DrawAuthorInfo(
|
||||
@ -421,8 +431,6 @@ private fun MessageBubbleLines(
|
||||
accountViewModel.settings.showProfilePictures.value,
|
||||
nav,
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = StdVertSpacer)
|
||||
}
|
||||
|
||||
RenderReplyRow(
|
||||
@ -442,32 +450,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) {
|
||||
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 +489,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 +512,8 @@ private fun RenderReply(
|
||||
|
||||
replyTo.value?.let { note ->
|
||||
ChatroomMessageCompose(
|
||||
note,
|
||||
null,
|
||||
baseNote = note,
|
||||
routeForLastRead = null,
|
||||
innerQuote = true,
|
||||
parentBackgroundColor = backgroundBubbleColor,
|
||||
accountViewModel = accountViewModel,
|
||||
@ -525,7 +533,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 +727,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 +765,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<String>?,
|
||||
route: String,
|
||||
nav: (String) -> Unit,
|
||||
userTags: ImmutableListOfLists<String>,
|
||||
) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class SettingsState() {
|
||||
var automaticallyShowProfilePictures by mutableStateOf(ConnectivityType.ALWAYS)
|
||||
var dontShowPushNotificationSelector by mutableStateOf<Boolean>(false)
|
||||
var dontAskForNotificationPermissions by mutableStateOf<Boolean>(false)
|
||||
var featureSet by mutableStateOf(FeatureSetType.COMPLETE)
|
||||
var featureSet by mutableStateOf(FeatureSetType.SIMPLIFIED)
|
||||
|
||||
var isOnMobileData: State<Boolean> = mutableStateOf(false)
|
||||
|
||||
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 =
|
||||
@ -173,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(
|
||||
@ -220,3 +222,12 @@ val boostedNoteModifier =
|
||||
end = 0.dp,
|
||||
top = 0.dp,
|
||||
)
|
||||
|
||||
val liveStreamTag =
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(horizontal = Size5dp)
|
||||
|
||||
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
|
||||
val AuthorInfoVideoFeed = Modifier.width(75.dp)
|
||||
|
@ -495,4 +495,14 @@
|
||||
<string name="classifieds_category_other">أخرى</string>
|
||||
<string name="failed_to_upload_media_no_details">فشل في تحميل الوسائط</string>
|
||||
<string name="failed_to_upload_media">خطأ في التحميل: %1$s</string>
|
||||
<string name="login_with_qr_code">تسجيل الدخول بواسطة رمز الQR</string>
|
||||
<string name="route_home">الصفحة الرئيسية</string>
|
||||
<string name="route_search">البحث</string>
|
||||
<string name="route_messages">الرسائل</string>
|
||||
<string name="route_security_filters">مرشحات الأمان</string>
|
||||
<string name="new_community_note">منشور جديد للمجتمع</string>
|
||||
<string name="open_all_reactions_to_this_post">فتح جميع ردود الفعل على هذا المنشور</string>
|
||||
<string name="close_all_reactions_to_this_post">إغلاق جميع ردود الفعل على هذا المنشور</string>
|
||||
<string name="reply_description">الرد</string>
|
||||
<string name="accessibility_scan_qr_code">مسح رمز ال QR</string>
|
||||
</resources>
|
||||
|
@ -439,6 +439,8 @@
|
||||
<string name="connectivity_type_always">Vždy</string>
|
||||
<string name="connectivity_type_wifi_only">Pouze Wi-Fi</string>
|
||||
<string name="connectivity_type_never">Nikdy</string>
|
||||
<string name="ui_feature_set_type_complete">Hotovo</string>
|
||||
<string name="ui_feature_set_type_simplified">Zjednodušené</string>
|
||||
<string name="system">Systém</string>
|
||||
<string name="light">Světlý</string>
|
||||
<string name="dark">Tmavý</string>
|
||||
@ -450,6 +452,8 @@
|
||||
<string name="automatically_show_url_preview">Automaticky zobrazit náhled URL</string>
|
||||
<string name="automatically_hide_nav_bars">Imersivní rolování</string>
|
||||
<string name="automatically_hide_nav_bars_description">Skrýt navigační panely při rolování</string>
|
||||
<string name="ui_style">Režim UI</string>
|
||||
<string name="ui_style_description">Zvolte styl příspěvku</string>
|
||||
<string name="load_image">Načíst obrázek</string>
|
||||
<string name="spamming_users">Spamovací uživatelé</string>
|
||||
<string name="muted_button">Ztlumené. Klikněte pro odztlumení</string>
|
||||
|
@ -444,6 +444,8 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="connectivity_type_always">Immer</string>
|
||||
<string name="connectivity_type_wifi_only">Nur WLAN</string>
|
||||
<string name="connectivity_type_never">Nie</string>
|
||||
<string name="ui_feature_set_type_complete">Fertig</string>
|
||||
<string name="ui_feature_set_type_simplified">Vereinfacht</string>
|
||||
<string name="system">System</string>
|
||||
<string name="light">Hell</string>
|
||||
<string name="dark">Dunkel</string>
|
||||
@ -455,6 +457,8 @@ anz der Bedingungen ist erforderlich</string>
|
||||
<string name="automatically_show_url_preview">URL-Vorschau automatisch anzeigen</string>
|
||||
<string name="automatically_hide_nav_bars">Immersives Scrollen</string>
|
||||
<string name="automatically_hide_nav_bars_description">Navigationsleisten beim Scrollen ausblenden</string>
|
||||
<string name="ui_style">UI-Modus</string>
|
||||
<string name="ui_style_description">Wählen Sie den Beitragsstil</string>
|
||||
<string name="load_image">Bild laden</string>
|
||||
<string name="spamming_users">Spammer</string>
|
||||
<string name="muted_button">Stummgeschaltet. Drücken, um Ton einzuschalten</string>
|
||||
|
@ -441,6 +441,8 @@
|
||||
<string name="connectivity_type_always">Toujours</string>
|
||||
<string name="connectivity_type_wifi_only">Wifi uniquement</string>
|
||||
<string name="connectivity_type_never">Jamais</string>
|
||||
<string name="ui_feature_set_type_complete">Complet</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplifié</string>
|
||||
<string name="system">Système</string>
|
||||
<string name="light">Clair</string>
|
||||
<string name="dark">Sombre</string>
|
||||
@ -452,6 +454,8 @@
|
||||
<string name="automatically_show_url_preview">Prévisualisation des URLs</string>
|
||||
<string name="automatically_hide_nav_bars">Défilement immersif</string>
|
||||
<string name="automatically_hide_nav_bars_description">Masquer les barres de navigation lors du défilement</string>
|
||||
<string name="ui_style">Mode UI</string>
|
||||
<string name="ui_style_description">Choisir le style du message</string>
|
||||
<string name="load_image">Charger l\'image</string>
|
||||
<string name="spamming_users">Spammeurs</string>
|
||||
<string name="muted_button">Silencieux. Cliquer pour réactiver le son</string>
|
||||
|
@ -439,6 +439,8 @@
|
||||
<string name="connectivity_type_always">Sempre</string>
|
||||
<string name="connectivity_type_wifi_only">Somente wifi</string>
|
||||
<string name="connectivity_type_never">Nunca</string>
|
||||
<string name="ui_feature_set_type_complete">Concluído</string>
|
||||
<string name="ui_feature_set_type_simplified">Simplificado</string>
|
||||
<string name="system">Sistema</string>
|
||||
<string name="light">Claro</string>
|
||||
<string name="dark">Escuro</string>
|
||||
@ -450,6 +452,8 @@
|
||||
<string name="automatically_show_url_preview">Mostrar automaticamente a visualização da URL</string>
|
||||
<string name="automatically_hide_nav_bars">Rolagem Imersiva</string>
|
||||
<string name="automatically_hide_nav_bars_description">Ocultar as barras de navegação ao rolar</string>
|
||||
<string name="ui_style">Modo de interface</string>
|
||||
<string name="ui_style_description">Escolha o estilo da publicação</string>
|
||||
<string name="load_image">Carregar imagem</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Silenciado. Clique para ativar o som</string>
|
||||
|
@ -438,6 +438,8 @@
|
||||
<string name="connectivity_type_always">Alltid</string>
|
||||
<string name="connectivity_type_wifi_only">Endast Wi-Fi</string>
|
||||
<string name="connectivity_type_never">Aldrig</string>
|
||||
<string name="ui_feature_set_type_complete">Slutförd</string>
|
||||
<string name="ui_feature_set_type_simplified">Förenklad</string>
|
||||
<string name="system">System</string>
|
||||
<string name="light">Ljus</string>
|
||||
<string name="dark">Mörk</string>
|
||||
@ -449,6 +451,8 @@
|
||||
<string name="automatically_show_url_preview">Visa automatiskt förhandsgranskning av URL</string>
|
||||
<string name="automatically_hide_nav_bars">Omslutande bläddring</string>
|
||||
<string name="automatically_hide_nav_bars_description">Dölj navigeringsfält vid bläddring</string>
|
||||
<string name="ui_style">UI läge</string>
|
||||
<string name="ui_style_description">Välj stilen för inlägg</string>
|
||||
<string name="load_image">Ladda bild</string>
|
||||
<string name="spamming_users">Spammare</string>
|
||||
<string name="muted_button">Ljud avstängt. Klicka för att ta bort ljudlöst</string>
|
||||
|
@ -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) {
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user