Merge branch 'main' into main

pull/749/head
greenart7c3 2024-03-15 09:18:38 -03:00 zatwierdzone przez GitHub
commit 8ade5b7e5f
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
22 zmienionych plików z 509 dodań i 299 usunięć

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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?,

Wyświetl plik

@ -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,

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -452,7 +452,7 @@ class Relay(
eventUploadCounterInBytes += event.bytesUsedInMemory()
// Sends everything.
Client.allSubscriptions().forEach { sendFilter(requestId = it) }
renewFilters()
}
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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,
)
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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,
)
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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)

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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"