Updates to conflict-causing files.

This commit is contained in:
KotlinGeekDev
2025-03-28 12:10:58 +01:00
101 changed files with 2052 additions and 923 deletions
-3
View File
@@ -28,9 +28,6 @@ alt="Get it on Obtaininum"
height="70">](https://github.com/ImranR98/Obtainium)
[<img src="https://github.com/machiav3lli/oandbackupx/raw/034b226cea5c1b30eb4f6a6f313e4dadcbb0ece4/badge_github.png" alt="Get it on GitHub"
height="70">](https://github.com/vitorpamplona/amethyst/releases)
[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
alt="Get it on F-Droid"
height="70">](https://f-droid.org/packages/com.vitorpamplona.amethyst/)
[<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png"
alt="Get it on Google Play"
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
+30 -30
View File
@@ -2,7 +2,7 @@
"project_info": {
"project_number": "768341258853",
"project_id": "amethyst-3057a",
"storage_bucket": "amethyst-3057a.appspot.com"
"storage_bucket": "amethyst-3057a.firebasestorage.app"
},
"client": [
{
@@ -34,6 +34,35 @@
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:3270514bee61de546b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst.benchmark"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:e0200680f552484d6b8c8c",
@@ -62,35 +91,6 @@
]
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:768341258853:android:5d07c35a37b24ff36b8c8c",
"android_client_info": {
"package_name": "com.vitorpamplona.amethyst.benchmark"
}
},
"oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": [
{
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}
],
"configuration_version": "1"
@@ -127,8 +127,6 @@ import com.vitorpamplona.quartz.nip19Bech32.entities.NPub
import com.vitorpamplona.quartz.nip19Bech32.entities.NRelay
import com.vitorpamplona.quartz.nip19Bech32.entities.NSec
import com.vitorpamplona.quartz.nip25Reactions.ReactionEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip30CustomEmoji.EmojiUrlTag
import com.vitorpamplona.quartz.nip30CustomEmoji.emojis
import com.vitorpamplona.quartz.nip30CustomEmoji.pack.EmojiPackEvent
@@ -2313,10 +2311,29 @@ class Account(
fun <T : Event> signAndSendPrivately(
template: EventTemplate<T>,
relayList: List<String>,
onDone: (T) -> Unit = {},
) {
signer.sign(template) {
LocalCache.justConsume(it, null)
Amethyst.instance.client.sendPrivately(it, relayList = convertRelayList(relayList))
onDone(it)
}
}
fun <T : Event> signAndSendPrivatelyOrBroadcast(
template: EventTemplate<T>,
relayList: (T) -> List<String>?,
onDone: (T) -> Unit = {},
) {
signer.sign(template) {
LocalCache.justConsume(it, null)
val relays = relayList(it)
if (relays != null) {
Amethyst.instance.client.sendPrivately(it, relayList = convertRelayList(relays))
} else {
Amethyst.instance.client.send(it)
}
onDone(it)
}
}
@@ -2806,28 +2823,6 @@ class Account(
}
}
fun sendChangeChannel(template: EventTemplate<ChannelMetadataEvent>) {
if (!isWriteable()) return
signer.sign(template) {
Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null)
it.channelId()?.let { LocalCache.getChannelIfExists(it)?.let { follow(it) } }
}
}
fun sendCreateNewChannel(template: EventTemplate<ChannelCreateEvent>) {
if (!isWriteable()) return
signer.sign(template) {
Amethyst.instance.client.send(it)
LocalCache.justConsume(it, null)
LocalCache.getChannelIfExists(it.id)?.let { follow(it) }
}
}
fun updateStatus(
oldStatus: AddressableNote,
newStatus: String,
@@ -33,9 +33,13 @@ import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.tags.addressables.ATag
import com.vitorpamplona.quartz.nip01Core.tags.addressables.Address
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip19Bech32.entities.NAddress
import com.vitorpamplona.quartz.nip19Bech32.entities.NEvent
import com.vitorpamplona.quartz.nip19Bech32.toNEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip53LiveActivities.streaming.LiveActivitiesEvent
import com.vitorpamplona.quartz.utils.Hex
@@ -46,17 +50,30 @@ class PublicChatChannel(
idHex: String,
) : Channel(idHex) {
var event: ChannelCreateEvent? = null
var infoTags = EmptyTagList
var info = ChannelData(null, null, null, null)
override fun relays() = info.relays ?: super.relays()
fun toNEvent() = NEvent.create(idHex, event?.pubKey, ChannelCreateEvent.KIND, *relays().toTypedArray())
fun toNostrUri() = "nostr:${toNEvent()}"
fun updateChannelInfo(
creator: User,
channelInfo: ChannelCreateEvent,
updatedAt: Long,
event: ChannelCreateEvent,
) {
this.event = channelInfo
updateChannelInfo(creator, channelInfo.channelInfo(), updatedAt)
this.event = event
this.infoTags = event.tags.toImmutableListOfLists()
updateChannelInfo(creator, event.channelInfo(), event.createdAt)
}
fun updateChannelInfo(
creator: User,
event: ChannelMetadataEvent,
) {
this.infoTags = event.tags.toImmutableListOfLists()
updateChannelInfo(creator, event.channelInfo(), event.createdAt)
}
fun updateChannelInfo(
@@ -116,9 +133,11 @@ class LiveActivitiesChannel(
.filter { it.contains(prefix, true) }
.isNotEmpty()
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, relayHintUrl())
fun toNAddr() = NAddress.create(address.kind, address.pubKeyHex, address.dTag, *relays().toTypedArray())
fun toATag() = ATag(address, relayHintUrl())
fun toNostrUri() = "nostr:${toNAddr()}"
}
data class Counter(
@@ -1385,7 +1385,7 @@ object LocalCache {
}
if (oldChannel.creator == null || oldChannel.creator == author) {
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event, event.createdAt)
oldChannel.updateChannelInfo(author, event)
}
}
}
@@ -1404,7 +1404,7 @@ object LocalCache {
val author = getOrCreateUser(event.pubKey)
if (event.createdAt > oldChannel.updatedMetadataAt) {
if (oldChannel is PublicChatChannel) {
oldChannel.updateChannelInfo(author, event.channelInfo(), event.createdAt)
oldChannel.updateChannelInfo(author, event)
}
} else {
// Log.d("MT","Relay sent a previous Metadata Event ${oldUser.toBestDisplayName()}
@@ -29,8 +29,8 @@ import com.vitorpamplona.amethyst.launchAndWaitAll
import com.vitorpamplona.amethyst.service.NostrSingleEventDataSource
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.firstFullCharOrEmoji
import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.amethyst.tryAndWait
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.amethyst.ui.note.combineWith
import com.vitorpamplona.amethyst.ui.note.toShortenHex
import com.vitorpamplona.ammolite.relays.BundledUpdate
@@ -791,28 +791,28 @@ open class Note(
// migrates these comments to a new version
replies.forEach {
note.addReply(it)
it.replyTo = it.replyTo?.updated(this, note)
it.replyTo = it.replyTo?.replace(this, note)
}
reactions.forEach {
it.value.forEach {
note.addReaction(it)
it.replyTo = it.replyTo?.updated(this, note)
it.replyTo = it.replyTo?.replace(this, note)
}
}
boosts.forEach {
note.addBoost(it)
it.replyTo = it.replyTo?.updated(this, note)
it.replyTo = it.replyTo?.replace(this, note)
}
reports.forEach {
it.value.forEach {
note.addReport(it)
it.replyTo = it.replyTo?.updated(this, note)
it.replyTo = it.replyTo?.replace(this, note)
}
}
zaps.forEach {
note.addZap(it.key, it.value)
it.key.replyTo = it.key.replyTo?.updated(this, note)
it.value?.replyTo = it.value?.replyTo?.updated(this, note)
it.key.replyTo = it.key.replyTo?.replace(this, note)
it.value?.replyTo = it.value?.replyTo?.replace(this, note)
}
replyTo = null
@@ -0,0 +1,39 @@
/**
* 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
import kotlin.math.roundToInt
fun countToHumanReadableBytes(counter: Int) =
when {
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()} GB"
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()} MB"
counter >= 1000 -> "${(counter / 1000f).roundToInt()} KB"
else -> "$counter"
}
fun countToHumanReadableBytes(counter: Long) =
when {
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()} GB"
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()} MB"
counter >= 1000 -> "${(counter / 1000f).roundToInt()} KB"
else -> "$counter"
}
@@ -18,31 +18,17 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.service
fun countToHumanReadableBytes(counter: Int) =
when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)} GB"
counter >= 1000000 -> "${Math.round(counter / 1000000f)} MB"
counter >= 1000 -> "${Math.round(counter / 1000f)} KB"
else -> "$counter"
}
fun countToHumanReadableBytes(counter: Long) =
when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)} GB"
counter >= 1000000 -> "${Math.round(counter / 1000000f)} MB"
counter >= 1000 -> "${Math.round(counter / 1000f)} KB"
else -> "$counter"
}
import kotlin.math.roundToInt
fun countToHumanReadable(
counter: Int,
str: String,
) = when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)}G $str"
counter >= 1000000 -> "${Math.round(counter / 1000000f)}M $str"
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()}G $str"
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()}M $str"
counter >= 1000 -> "${(counter / 1000f).roundToInt()}K $str"
else -> "$counter $str"
}
@@ -50,8 +36,8 @@ fun countToHumanReadable(
counter: Long,
str: String,
) = when {
counter >= 1000000000 -> "${Math.round(counter / 1000000000f)}G $str"
counter >= 1000000 -> "${Math.round(counter / 1000000f)}M $str"
counter >= 1000 -> "${Math.round(counter / 1000f)}K $str"
counter >= 1000000000 -> "${(counter / 1000000000f).roundToInt()}G $str"
counter >= 1000000 -> "${(counter / 1000000f).roundToInt()}M $str"
counter >= 1000 -> "${(counter / 1000f).roundToInt()}K $str"
else -> "$counter $str"
}
@@ -0,0 +1,26 @@
/**
* 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
fun <T> Iterable<T>.replace(
old: T,
new: T,
): List<T> = map { if (it == old) new else it }
@@ -0,0 +1,23 @@
/**
* 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
fun <T> Set<T>.togglePresenceInSet(item: T): Set<T> = if (contains(item)) minus(item) else plus(item)
@@ -221,7 +221,7 @@ fun NewUserMetadataScreen(
SelectSingleFromGallery(
isUploading = postViewModel.isUploadingImageForBanner,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 5.dp),
modifier = Modifier.padding(start = 5.dp).align(Alignment.CenterHorizontally),
) {
postViewModel.uploadForBanner(it, context, onError = accountViewModel.toastManager::toast)
}
@@ -47,11 +47,11 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
@@ -56,13 +56,13 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategory
import com.vitorpamplona.amethyst.ui.actions.relays.SettingsCategoryWithButton
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategoryWithButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.DoubleVertPadding
@@ -24,7 +24,6 @@ import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
@@ -37,7 +36,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
@@ -110,22 +108,20 @@ private fun GallerySelectButton(
modifier: Modifier,
onClick: () -> Unit,
) {
Box {
IconButton(
modifier = modifier.align(Alignment.Center),
enabled = !isUploading,
onClick = { onClick() },
) {
if (!isUploading) {
Icon(
imageVector = Icons.Default.AddPhotoAlternate,
contentDescription = stringRes(id = R.string.upload_image),
modifier = Modifier.height(25.dp),
tint = tint,
)
} else {
LoadingAnimation()
}
IconButton(
modifier = modifier,
enabled = !isUploading,
onClick = { onClick() },
) {
if (!isUploading) {
Icon(
imageVector = Icons.Default.AddPhotoAlternate,
contentDescription = stringRes(id = R.string.upload_image),
modifier = Modifier.height(25.dp),
tint = tint,
)
} else {
LoadingAnimation()
}
}
}
@@ -67,12 +67,12 @@ import com.vitorpamplona.amethyst.ui.note.ZapIcon
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import com.vitorpamplona.amethyst.ui.theme.CashuCardBorders
import com.vitorpamplona.amethyst.ui.theme.Size18Modifier
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.SmallishBorder
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
@@ -129,14 +129,12 @@ fun CashuPreviewPreview() {
sharedPreferencesViewModel.init()
sharedPreferencesViewModel.updateTheme(ThemeType.DARK)
AmethystTheme(sharedPrefsViewModel = sharedPreferencesViewModel) {
Column {
CashuPreviewNew(
token = CashuToken("token", "mint", 32400, listOf()),
melt = { token, context, onDone -> },
toast = { title, message -> },
)
}
ThemeComparisonColumn {
CashuPreviewNew(
token = CashuToken("token", "mint", 32400, listOf()),
melt = { token, context, onDone -> },
toast = { title, message -> },
)
}
}
@@ -210,7 +208,6 @@ fun CashuPreviewNew(
Text(
"Redeem",
color = MaterialTheme.colorScheme.onBackground,
fontSize = 16.sp,
)
}
@@ -232,7 +229,7 @@ fun CashuPreviewNew(
shape = SmallishBorder,
contentPadding = PaddingValues(0.dp),
) {
OpenInNewIcon(Size18Modifier, tint = MaterialTheme.colorScheme.onBackground)
OpenInNewIcon(Size18Modifier)
}
Spacer(modifier = StdHorzSpacer)
@@ -245,7 +242,7 @@ fun CashuPreviewNew(
shape = SmallishBorder,
contentPadding = PaddingValues(0.dp),
) {
CopyIcon(Size18Modifier, tint = MaterialTheme.colorScheme.onBackground)
CopyIcon(Size18Modifier)
}
}
}
@@ -26,9 +26,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
@@ -50,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
import com.vitorpamplona.amethyst.ui.theme.StdTopPadding
import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
object ShowFullTextCache {
@@ -128,16 +125,12 @@ fun ExpandableRichTextViewer(
@Composable
fun ShowMoreButton(onClick: () -> Unit) {
Button(
FilledTonalButton(
modifier = StdTopPadding,
onClick = onClick,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryButtonBackground,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.show_more), color = Color.White)
Text(text = stringRes(R.string.show_more))
}
}
@@ -32,8 +32,7 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -161,19 +160,14 @@ fun ContentWarningNote(onDismiss: () -> Unit) {
}
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
Button(
FilledTonalButton(
modifier = Modifier.padding(top = 10.dp),
onClick = onDismiss,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(
text = stringRes(R.string.show_anyway),
color = Color.White,
)
}
}
@@ -23,7 +23,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.quartz.nip01Core.tags.events.taggedEventIds
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
@@ -90,7 +90,7 @@ class ChatroomListKnownFeedFilter(
if (newNotePair.key == oldNote.channelHex()) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
myNewList = myNewList.updated(oldNote, newNotePair.value)
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
}
@@ -107,7 +107,7 @@ class ChatroomListKnownFeedFilter(
if (newNotePair.key == oldRoom) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
myNewList = myNewList.updated(oldNote, newNotePair.value)
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
}
@@ -22,7 +22,7 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.updated
import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.quartz.nip04Dm.messages.PrivateDmEvent
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKey
import com.vitorpamplona.quartz.nip17Dm.base.ChatroomKeyable
@@ -77,7 +77,7 @@ class ChatroomListNewFeedFilter(
if (newNotePair.key == oldRoom) {
hasUpdated = true
if ((newNotePair.value.createdAt() ?: 0) > (oldNote.createdAt() ?: 0)) {
myNewList = myNewList.updated(oldNote, newNotePair.value)
myNewList = myNewList.replace(oldNote, newNotePair.value)
}
}
}
@@ -47,7 +47,6 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataScreen
import com.vitorpamplona.amethyst.ui.actions.relays.AllRelayListView
import com.vitorpamplona.amethyst.ui.components.DisplayNotifyMessages
import com.vitorpamplona.amethyst.ui.components.getActivity
import com.vitorpamplona.amethyst.ui.components.toasts.DisplayErrorMessages
@@ -61,6 +60,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.bookmarks.BookmarkListScree
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomByAuthorScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.ChatroomScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.ChannelScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.MessagesScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.communities.CommunityScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.discover.DiscoverScreen
@@ -72,6 +72,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.home.HomeScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.lists.ListsScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.notifications.NotificationScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.ProfileScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.AllRelayListScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.search.SearchScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.NIP47SetupScreen
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SecurityFiltersScreen
@@ -269,6 +270,22 @@ fun AppNavigation(
)
}
composable(
Route.ChannelMetadataEdit.route,
Route.ChannelMetadataEdit.arguments,
enterTransition = { slideInVerticallyFromBottom },
exitTransition = { scaleOut },
popEnterTransition = { scaleIn },
popExitTransition = { slideOutVerticallyToBottom },
content = {
ChannelMetadataScreen(
channelId = it.id(),
accountViewModel = accountViewModel,
nav = nav,
)
},
)
composable(
Route.Event.route,
Route.Event.arguments,
@@ -313,7 +330,7 @@ fun AppNavigation(
content = {
val relayToAdd = it.arguments?.getString("toAdd")
AllRelayListView(
AllRelayListScreen(
relayToAdd = relayToAdd,
accountViewModel = accountViewModel,
nav = nav,
@@ -329,7 +346,7 @@ fun AppNavigation(
popEnterTransition = { scaleIn },
popExitTransition = { slideOutVerticallyToBottom },
) {
val draftMessage = it.arguments?.getString("message")?.ifBlank { null }
val draftMessage = it.message()?.ifBlank { null }
val attachment =
it.arguments?.getString("attachment")?.ifBlank { null }?.let {
Uri.parse(it)
@@ -339,8 +356,8 @@ fun AppNavigation(
val fork = it.arguments?.getString("fork")
val version = it.arguments?.getString("version")
val draft = it.arguments?.getString("draft")
val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") ?: false
val enableGeolocation = it.arguments?.getBoolean("enableGeolocation") ?: false
val enableMessageInterface = it.arguments?.getBoolean("enableMessageInterface") == true
val enableGeolocation = it.arguments?.getBoolean("enableGeolocation") == true
NewPostScreen(
message = draftMessage,
@@ -213,6 +213,20 @@ sealed class Route(
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList(),
)
object ChannelMetadataEdit :
Route(
route = "ChannelMetadataEdit?id={id}",
icon = R.drawable.ic_moments,
arguments =
listOf(
navArgument("id") {
type = NavType.StringType
nullable = true
defaultValue = null
},
).toImmutableList(),
)
object Event :
Route(
route = "Event/{id}",
@@ -21,7 +21,6 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -30,6 +29,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
@@ -38,6 +39,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.theme.isLight
@Composable
fun TopBarExtensibleWithBackButton(
@@ -90,11 +92,8 @@ fun MyExtensibleTopAppBar(
)
if (expanded.value && extendableRow != null) {
Row(
Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
) {
val elevation = if (MaterialTheme.colorScheme.isLight) 1.dp else 10.dp
Surface(modifier = Modifier.fillMaxWidth(), tonalElevation = elevation) {
Column { extendableRow() }
}
}
@@ -26,9 +26,7 @@ import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -155,17 +153,13 @@ fun HiddenNote(
}
}
Button(
FilledTonalButton(
modifier = Modifier.padding(top = 10.dp),
onClick = onClick,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.show_anyway), color = Color.White)
Text(text = stringRes(R.string.show_anyway))
}
}
}
@@ -201,17 +195,13 @@ fun HiddenNoteByMe(
textAlign = TextAlign.Center,
)
Button(
FilledTonalButton(
modifier = Modifier.padding(top = 10.dp),
onClick = onClick,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.show_anyway), color = Color.White)
Text(text = stringRes(R.string.show_anyway))
}
}
}
@@ -312,27 +312,19 @@ fun CashuIcon(modifier: Modifier) {
}
@Composable
fun CopyIcon(
modifier: Modifier,
tint: Color = Color.Unspecified,
) {
fun CopyIcon(modifier: Modifier) {
Icon(
imageVector = Icons.Default.ContentCopy,
stringRes(id = R.string.copy_to_clipboard),
tint = tint,
modifier = modifier,
)
}
@Composable
fun OpenInNewIcon(
modifier: Modifier,
tint: Color = Color.Unspecified,
) {
fun OpenInNewIcon(modifier: Modifier) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
stringRes(id = R.string.copy_to_clipboard),
tint = tint,
modifier = modifier,
)
}
@@ -87,7 +87,6 @@ import com.vitorpamplona.amethyst.ui.note.types.EditState
import com.vitorpamplona.amethyst.ui.note.types.EmptyState
import com.vitorpamplona.amethyst.ui.note.types.FileHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.FileStorageHeaderDisplay
import com.vitorpamplona.amethyst.ui.note.types.JustVideoDisplay
import com.vitorpamplona.amethyst.ui.note.types.PictureDisplay
import com.vitorpamplona.amethyst.ui.note.types.RenderAppDefinition
import com.vitorpamplona.amethyst.ui.note.types.RenderAudioHeader
@@ -194,7 +193,6 @@ import com.vitorpamplona.quartz.nip58Badges.BadgeAwardEvent
import com.vitorpamplona.quartz.nip58Badges.BadgeDefinitionEvent
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip68Picture.PictureEvent
import com.vitorpamplona.quartz.nip71Video.VideoEvent
import com.vitorpamplona.quartz.nip71Video.VideoHorizontalEvent
import com.vitorpamplona.quartz.nip71Video.VideoVerticalEvent
import com.vitorpamplona.quartz.nip72ModCommunities.approval.CommunityPostApprovalEvent
@@ -328,9 +326,6 @@ fun AcceptableNote(
)
}
is BadgeDefinitionEvent -> BadgeDisplay(baseNote = baseNote)
is FileHeaderEvent -> FileHeaderDisplay(baseNote, false, ContentScale.FillWidth, accountViewModel)
is FileStorageHeaderEvent -> FileStorageHeaderDisplay(baseNote, false, ContentScale.FillWidth, accountViewModel)
is VideoEvent -> JustVideoDisplay(baseNote, false, ContentScale.FillWidth, accountViewModel)
else ->
LongPressToQuickAction(baseNote = baseNote, accountViewModel = accountViewModel) { showPopup ->
CheckNewAndRenderNote(
@@ -25,9 +25,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
@@ -35,7 +34,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@@ -121,33 +119,25 @@ private fun RelayOptions(
@Composable
fun AddRelayButton(onClick: () -> Unit) {
Button(
OutlinedButton(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(id = R.string.add), color = Color.White)
Text(text = stringRes(id = R.string.add))
}
}
@Composable
fun RemoveRelayButton(onClick: () -> Unit) {
Button(
OutlinedButton(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.remove), color = Color.White)
Text(text = stringRes(R.string.remove))
}
}
@@ -54,11 +54,11 @@ import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.components.ClickableBox
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
@@ -52,7 +52,9 @@ fun UserCompose(
UserPicture(baseUser, Size55dp, accountViewModel = accountViewModel, nav = nav)
Column(modifier = remember { Modifier.padding(start = 10.dp).weight(1f) }) {
Row(verticalAlignment = Alignment.CenterVertically) { UsernameDisplay(baseUser, accountViewModel = accountViewModel) }
Row(verticalAlignment = Alignment.CenterVertically) {
UsernameDisplay(baseUser, accountViewModel = accountViewModel)
}
AboutDisplay(baseUser)
}
@@ -34,7 +34,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -165,8 +164,6 @@ fun UserActionOptions(
baseAuthor: User,
accountViewModel: AccountViewModel,
) {
val scope = rememberCoroutineScope()
WatchIsHiddenUser(baseAuthor, accountViewModel) { isHidden ->
if (isHidden) {
ShowUserButton { accountViewModel.show(baseAuthor) }
@@ -39,12 +39,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.relays.AddDMRelayListDialog
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.AddDMRelayListDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BigPadding
import com.vitorpamplona.amethyst.ui.theme.StdPadding
@@ -39,12 +39,12 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.relays.AddSearchRelayListDialog
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.AddSearchRelayListDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.BigPadding
import com.vitorpamplona.amethyst.ui.theme.StdPadding
@@ -24,11 +24,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -62,7 +61,7 @@ fun AddButton(
modifier: Modifier = Modifier.padding(start = 3.dp),
onClick: () -> Unit,
) {
Button(
OutlinedButton(
modifier = modifier,
onClick = {
if (isActive) {
@@ -73,7 +72,7 @@ fun AddButton(
enabled = isActive,
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp),
) {
Text(text = stringRes(text), color = Color.White, textAlign = TextAlign.Center)
Text(text = stringRes(text), textAlign = TextAlign.Center)
}
}
@@ -82,7 +81,7 @@ fun RemoveButton(
isActive: Boolean = true,
onClick: () -> Unit,
) {
Button(
OutlinedButton(
modifier = Modifier.padding(start = 3.dp),
onClick = {
if (isActive) {
@@ -93,6 +92,6 @@ fun RemoveButton(
enabled = isActive,
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp),
) {
Text(text = stringRes(R.string.remove), color = Color.White)
Text(text = stringRes(R.string.remove))
}
}
@@ -52,7 +52,7 @@ import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.countToHumanReadableBytes
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
import com.vitorpamplona.amethyst.ui.components.ShowMoreButton
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -47,7 +47,7 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.relays.countToHumanReadableBytes
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
import com.vitorpamplona.amethyst.ui.components.GenericLoadable
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
@@ -1698,19 +1698,11 @@ fun CreateButton(
modifier: Modifier = Modifier,
) {
Button(
enabled = isActive,
modifier = modifier,
onClick = {
if (isActive) {
onPost()
}
},
shape = ButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = if (isActive) MaterialTheme.colorScheme.primary else Color.Gray,
),
onClick = onPost,
) {
Text(text = stringRes(R.string.create), color = Color.White)
Text(text = stringRes(R.string.create))
}
}
@@ -22,15 +22,12 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
@Composable
@@ -41,27 +38,19 @@ fun RenderChangeChannelMetadataNote(
nav: INav,
) {
val noteEvent = note.event as? ChannelMetadataEvent ?: return
val channelInfo = remember(noteEvent) { noteEvent.channelInfo() }
val tags =
remember(noteEvent) {
noteEvent.tags.toImmutableListOfLists()
}
val channelInfo = noteEvent.channelInfo()
val text =
note.author?.toBestDisplayName().toString() +
" ${stringRes(R.string.changed_chat_name_to)} '" +
(channelInfo.name ?: "") +
"', ${stringRes(R.string.description_to)} '" +
(channelInfo.about ?: "") +
"' ${stringRes(R.string.and_picture_to)} " +
(channelInfo.picture ?: "")
TranslatableRichTextViewer(
content = text,
canPreview = true,
quotesLeft = 0,
modifier = Modifier,
tags = note.author?.info?.tags ?: EmptyTagList,
backgroundColor = bgColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
RenderChannelData(
noteEvent.id,
note.toNostrUri(),
channelInfo,
tags,
bgColor,
accountViewModel,
nav,
)
}
@@ -20,19 +20,63 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.feed.types
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.produceState
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.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import com.vitorpamplona.amethyst.ui.theme.Size20dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.quartz.nip02FollowList.ImmutableListOfLists
import com.vitorpamplona.quartz.nip02FollowList.toImmutableListOfLists
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.base.ChannelData
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@Composable
fun RenderCreateChannelNote(
@@ -42,27 +86,272 @@ fun RenderCreateChannelNote(
nav: INav,
) {
val noteEvent = note.event as? ChannelCreateEvent ?: return
val channelInfo = remember { noteEvent.channelInfo() }
val channelInfo = remember(noteEvent) { noteEvent.channelInfo() }
val tags =
remember(noteEvent) {
noteEvent.tags.toImmutableListOfLists()
}
val text =
note.author?.toBestDisplayName().toString() +
" ${stringRes(R.string.created)} " +
(channelInfo.name ?: "") +
" ${stringRes(R.string.with_description_of)} '" +
(channelInfo.about ?: "") +
"' ${stringRes(R.string.and_picture_to)} " +
(channelInfo.picture ?: "")
TranslatableRichTextViewer(
content = text,
canPreview = true,
quotesLeft = 0,
modifier = Modifier,
tags = note.author?.info?.tags ?: EmptyTagList,
backgroundColor = bgColor,
id = note.idHex,
callbackUri = note.toNostrUri(),
accountViewModel = accountViewModel,
nav = nav,
RenderChannelData(
noteEvent.id,
note.toNostrUri(),
channelInfo,
tags,
bgColor,
accountViewModel,
nav,
)
}
@Preview
@Composable
fun RenderChannelDataPreview() {
ThemeComparisonRow {
RenderChannelData(
id = "bbaacc",
uri = "nostr:nevent1...",
channelInfo =
ChannelData(
"My Group",
"Testing About me",
"http://test.com",
listOf("wss://nostr.mom", "wss://nos.lol"),
),
tags = EmptyTagList,
bgColor = remember { mutableStateOf(Color.Transparent) },
accountViewModel = mockAccountViewModel(),
nav = EmptyNav,
)
}
}
@Composable
fun RenderChannelData(
id: HexKey,
uri: String,
channelInfo: ChannelData,
tags: ImmutableListOfLists<String>,
bgColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: INav,
) {
Column {
Row {
TranslatableRichTextViewer(
content = stringRes(R.string.changed_chat_profile_to),
canPreview = true,
quotesLeft = 1,
modifier = Modifier,
tags = tags,
backgroundColor = bgColor,
id = id,
callbackUri = uri,
accountViewModel = accountViewModel,
nav = nav,
)
}
channelInfo.picture?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
RobohashFallbackAsyncImage(
robot = id,
model = it,
contentDescription = stringRes(R.string.channel_image),
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
}
channelInfo.name?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
CreateTextWithEmoji(
text = it,
tags = tags,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}
}
channelInfo.about?.let {
Row(
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
TranslatableRichTextViewer(
content = it,
canPreview = true,
quotesLeft = 1,
modifier = Modifier,
tags = tags,
backgroundColor = bgColor,
id = id,
callbackUri = uri,
accountViewModel = accountViewModel,
nav = nav,
)
}
}
channelInfo.relays?.let {
Text(
stringRes(R.string.public_chat_relays_title) + ": ",
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
)
it.forEach {
Spacer(StdVertSpacer)
RenderRelayLinePublicChat(
it,
accountViewModel,
nav,
)
}
}
}
}
@Preview
@Composable
fun RenderRelayLinePreview() {
ThemeComparisonRow {
RenderRelayLine(
"wss://nos.lol",
"http://icon.com/icon.ico",
Modifier,
true,
true,
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RenderRelayLinePublicChat(
dirtyUrl: String,
accountViewModel: AccountViewModel,
nav: INav,
) {
@Suppress("ProduceStateDoesNotAssignValue")
val relayInfo by produceState(
initialValue = Nip11CachedRetriever.getFromCache(dirtyUrl),
) {
if (value == null) {
accountViewModel.retrieveRelayDocument(
dirtyUrl,
onInfo = {
value = it
},
onError = { url, errorCode, exceptionMessage ->
},
)
}
}
var openRelayDialog by remember { mutableStateOf(false) }
val info =
remember(dirtyUrl) {
RelayBriefInfoCache.get(RelayUrlFormatter.normalize(dirtyUrl))
}
if (openRelayDialog && relayInfo != null) {
RelayInformationDialog(
onClose = { openRelayDialog = false },
relayInfo = relayInfo!!,
relayBriefInfo = info,
accountViewModel = accountViewModel,
nav = nav,
)
}
val clipboardManager = LocalClipboardManager.current
val clickableModifier =
remember(dirtyUrl) {
Modifier.combinedClickable(
onLongClick = {
clipboardManager.setText(AnnotatedString(dirtyUrl))
},
onClick = {
accountViewModel.retrieveRelayDocument(
dirtyUrl,
onInfo = {
openRelayDialog = true
},
onError = { url, errorCode, exceptionMessage ->
accountViewModel.toastManager.toast(
R.string.unable_to_download_relay_document,
when (errorCode) {
Nip11Retriever.ErrorCode.FAIL_TO_ASSEMBLE_URL ->
R.string.relay_information_document_error_failed_to_assemble_url
Nip11Retriever.ErrorCode.FAIL_TO_REACH_SERVER ->
R.string.relay_information_document_error_failed_to_reach_server
Nip11Retriever.ErrorCode.FAIL_TO_PARSE_RESULT ->
R.string.relay_information_document_error_failed_to_parse_response
Nip11Retriever.ErrorCode.FAIL_WITH_HTTP_STATUS ->
R.string.relay_information_document_error_failed_with_http
},
url,
exceptionMessage ?: errorCode.toString(),
)
},
)
},
)
}
RenderRelayLine(
info.displayUrl,
relayInfo?.icon ?: info.favIcon,
clickableModifier,
showPicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
@Composable
fun RenderRelayLine(
url: String,
icon: String?,
modifier: Modifier = Modifier,
showPicture: Boolean = true,
loadRobohash: Boolean = true,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = modifier,
) {
Text(" -")
Spacer(modifier = StdHorzSpacer)
RobohashFallbackAsyncImage(
robot = url,
model = icon,
contentDescription = stringRes(id = R.string.relay_info, url),
colorFilter = RelayIconFilter,
modifier =
Modifier
.size(Size20dp)
.clip(shape = CircleShape),
loadProfilePicture = showPicture,
loadRobohash = loadRobohash,
)
Spacer(modifier = StdHorzSpacer)
Text(
text = url,
)
}
}
@@ -31,7 +31,7 @@ import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@@ -42,7 +42,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.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
@@ -132,7 +131,7 @@ fun GroupMembersHeader(
val list = remember(room) { room.users.toPersistentList() }
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier.fillMaxWidth().padding(5.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -181,7 +180,7 @@ private fun EditRoomSubjectButton(
NewChatroomSubjectDialog({ wantsToPost = false }, accountViewModel, room)
}
Button(
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
@@ -190,7 +189,6 @@ private fun EditRoomSubjectButton(
contentPadding = ZeroPadding,
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.EditNote,
contentDescription = stringRes(R.string.edits_the_channel_metadata),
)
@@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@@ -62,11 +63,9 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
import com.vitorpamplona.amethyst.ui.theme.Size30Modifier
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonRow
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@@ -76,12 +75,12 @@ import kotlinx.coroutines.launch
@Preview
@Composable
fun PrivateMessageEditFieldRow() {
fun PrivateMessageEditFieldRowPreview() {
val channelScreenModel: ChatNewMessageViewModel = viewModel()
val accountViewModel = mockAccountViewModel()
channelScreenModel.init(accountViewModel)
ThemeComparisonRow {
ThemeComparisonColumn {
PrivateMessageEditFieldRow(
channelScreenModel = channelScreenModel,
accountViewModel = accountViewModel,
@@ -170,12 +169,12 @@ fun PrivateMessageEditFieldRow(
leadingIcon = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 6.dp),
modifier = Modifier.padding(start = 4.dp, end = 10.dp),
) {
SelectFromGallery(
isUploading = channelScreenModel.isUploadingImage,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = EditFieldLeadingIconModifier,
modifier = Modifier,
onImageChosen = channelScreenModel::pickedMedia,
)
@@ -190,7 +189,7 @@ fun PrivateMessageEditFieldRow(
}
IconButton(
modifier = Size30Modifier,
modifier = Modifier.width(30.dp),
onClick = {
if (
!accountViewModel.account.settings.hideNIP17WarningDialog &&
@@ -208,7 +207,7 @@ fun PrivateMessageEditFieldRow(
modifier =
Modifier
.padding(top = 2.dp)
.size(18.dp),
.size(20.dp),
tint = MaterialTheme.colorScheme.primary,
)
} else {
@@ -216,7 +215,7 @@ fun PrivateMessageEditFieldRow(
modifier =
Modifier
.padding(top = 2.dp)
.size(18.dp),
.size(20.dp),
tint = MaterialTheme.colorScheme.placeholderText,
)
}
@@ -1,120 +0,0 @@
/**
* 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.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.EditButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.JoinChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LeaveChatButton
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
@Composable
fun ShortChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
it?.let {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
LikeReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
ZapReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdHorzSpacer)
}
}
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav)
}
}
}
@Composable
fun LongChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val isMe by
remember(accountViewModel) {
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
}
if (isMe) {
EditButton(channel, accountViewModel)
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (isFollowing) {
LeaveChatButton(channel, accountViewModel, nav)
}
}
}
@Composable
private fun WatchChannelFollows(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
content: @Composable (Boolean) -> Unit,
) {
val isFollowing by
accountViewModel
.userProfile()
.live()
.follows
.map { it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false }
.distinctUntilChanged()
.observeAsState(
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false,
)
content(isFollowing)
}
@@ -20,25 +20,35 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.map
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
@@ -46,21 +56,64 @@ import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.elements.MoreOptionsButton
import com.vitorpamplona.amethyst.ui.note.elements.NormalTimeAgo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.EditButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LeaveChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.LinkChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.OpenChatButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.ShareChatButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.quartz.nip02FollowList.EmptyTagList
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.quartz.nip01Core.tags.events.isTaggedEvent
@Composable
fun LongPublicChatChannelHeader(
baseChannel: PublicChatChannel,
lineModifier: Modifier = Modifier.padding(horizontal = 10.dp, vertical = 5.dp),
lineModifier: Modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 5.dp),
accountViewModel: AccountViewModel,
nav: INav,
) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel as? PublicChatChannel ?: return
Spacer(StdVertSpacer)
channel.info.picture?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = lineModifier,
) {
RobohashFallbackAsyncImage(
robot = baseChannel.idHex,
model = it,
contentDescription = stringRes(R.string.channel_image),
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
}
channel.info.name?.let {
Row(
horizontalArrangement = Arrangement.Center,
modifier = lineModifier,
) {
CreateTextWithEmoji(
text = it,
tags = channel.infoTags,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}
}
Row(horizontalArrangement = Arrangement.Center, modifier = lineModifier) {
LongChannelActionOptions(channel, accountViewModel, nav)
}
Row(lineModifier) {
val summary = remember(channelState) { channel.summary()?.ifBlank { null } }
@@ -73,7 +126,7 @@ fun LongPublicChatChannelHeader(
content = summary ?: stringRes(id = R.string.groups_no_descriptor),
canPreview = false,
quotesLeft = 1,
tags = EmptyTagList,
tags = channel.infoTags,
backgroundColor = background,
id = baseChannel.idHex,
accountViewModel = accountViewModel,
@@ -81,9 +134,6 @@ fun LongPublicChatChannelHeader(
)
}
}
Spacer(DoubleHorzSpacer)
LongChannelActionOptions(channel, accountViewModel, nav)
}
LoadNote(baseNoteHex = channel.idHex, accountViewModel) { loadingNote ->
@@ -120,4 +170,54 @@ fun LongPublicChatChannelHeader(
}
}
}
Spacer(StdVertSpacer)
}
@Composable
fun LongChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val isMe by
remember(accountViewModel) {
derivedStateOf { channel.creator == accountViewModel.account.userProfile() }
}
OpenChatButton(channel, accountViewModel, nav)
LinkChatButton(channel, accountViewModel, nav)
ShareChatButton(channel, accountViewModel, nav)
if (isMe) {
EditButton(channel, accountViewModel, nav)
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (isFollowing) {
LeaveChatButton(channel, accountViewModel, nav)
}
}
}
@Composable
fun WatchChannelFollows(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
content: @Composable (Boolean) -> Unit,
) {
val isFollowing by
accountViewModel
.userProfile()
.live()
.follows
.map { it.user.latestContactList?.isTaggedEvent(channel.idHex) ?: false }
.distinctUntilChanged()
.observeAsState(
accountViewModel.userProfile().latestContactList?.isTaggedEvent(channel.idHex) ?: false,
)
content(isFollowing)
}
@@ -22,6 +22,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28
import androidx.compose.runtime.Composable
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.popBack
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.TopBarExtensibleWithBackButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -23,8 +23,10 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -38,12 +40,18 @@ import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.components.LoadNote
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions.JoinChatButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
@Composable
fun ShortPublicChatChannelHeader(
@@ -94,3 +102,36 @@ fun ShortPublicChatChannelHeader(
}
}
}
@Composable
fun ShortChannelActionOptions(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
it?.let {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
LikeReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
ZapReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdHorzSpacer)
}
}
}
WatchChannelFollows(channel, accountViewModel) { isFollowing ->
if (!isFollowing) {
JoinChatButton(channel, accountViewModel, nav)
}
}
}
@@ -24,20 +24,18 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@@ -45,23 +43,17 @@ import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
fun EditButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
var wantsToPost by remember { mutableStateOf(false) }
if (wantsToPost) {
ChannelMetadataDialog({ wantsToPost = false }, accountViewModel, channel)
}
Button(
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = { wantsToPost = true },
onClick = { nav.nav("ChannelMetadataEdit?id=${channel.idHex}") },
contentPadding = ZeroPadding,
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.EditNote,
contentDescription = stringRes(R.string.edits_the_channel_metadata),
)
@@ -20,10 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -38,11 +37,11 @@ fun JoinChatButton(
accountViewModel: AccountViewModel,
nav: INav,
) {
Button(
FilledTonalButton(
modifier = HalfHalfHorzModifier,
onClick = { accountViewModel.follow(channel) },
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.join), color = Color.White)
Text(text = stringRes(R.string.join))
}
}
@@ -20,10 +20,9 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
@@ -38,11 +37,11 @@ fun LeaveChatButton(
accountViewModel: AccountViewModel,
nav: INav,
) {
Button(
FilledTonalButton(
modifier = HalfHalfHorzModifier,
onClick = { accountViewModel.unfollow(channel) },
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.leave), color = Color.White)
Text(text = stringRes(R.string.leave))
}
}
@@ -0,0 +1,87 @@
/**
* 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.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@Composable
fun LinkChatButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, channel.toNostrUri().toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, channel.toNostrUri())
putExtra(Intent.EXTRA_TITLE, stringRes(context, R.string.quick_action_share_browser_link))
}
val shareIntent =
Intent.createChooser(
sendIntent,
stringRes(context, R.string.quick_action_copy_note_id),
)
ContextCompat.startActivity(context, shareIntent, null)
},
contentPadding = ZeroPadding,
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = stringRes(R.string.quick_action_copy_note_id),
modifier = Size20Modifier,
)
}
}
@@ -0,0 +1,72 @@
/**
* 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.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.OpenInNew
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@Composable
fun OpenChatButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, channel.toNostrUri().toUri())
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
ContextCompat.startActivity(context, intent, null)
},
contentPadding = ZeroPadding,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
contentDescription = stringRes(R.string.quick_actions_open_in_another_app),
modifier = Size20Modifier,
)
}
}
@@ -0,0 +1,84 @@
/**
* 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.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.header.actions
import android.content.Intent
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.note.njumpLink
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Size20Modifier
import com.vitorpamplona.amethyst.ui.theme.ZeroPadding
@Composable
fun ShareChatButton(
channel: PublicChatChannel,
accountViewModel: AccountViewModel,
nav: INav,
) {
val context = LocalContext.current
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
.width(50.dp),
onClick = {
val sendIntent =
Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, njumpLink(channel.toNEvent()))
putExtra(Intent.EXTRA_TITLE, stringRes(context, R.string.quick_action_share_browser_link))
}
val shareIntent =
Intent.createChooser(
sendIntent,
stringRes(context, R.string.quick_action_share),
)
ContextCompat.startActivity(context, shareIntent, null)
},
contentPadding = ZeroPadding,
) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = stringRes(R.string.quick_action_share),
modifier = Size20Modifier,
)
}
}
@@ -1,169 +0,0 @@
/**
* 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.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.PostButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@Composable
fun ChannelMetadataDialog(
onClose: () -> Unit,
accountViewModel: AccountViewModel,
channel: PublicChatChannel? = null,
) {
val postViewModel: ChannelMetadataViewModel = viewModel()
postViewModel.load(accountViewModel.account, channel)
Dialog(
onDismissRequest = { onClose() },
properties =
DialogProperties(
dismissOnClickOutside = false,
),
) {
DialogContent(postViewModel, onClose)
}
}
@Composable
private fun DialogContent(
postViewModel: ChannelMetadataViewModel,
onClose: () -> Unit,
) {
Surface {
Column(
modifier =
Modifier
.padding(10.dp)
.verticalScroll(rememberScrollState()),
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
CloseButton(
onPress = {
postViewModel.clear()
onClose()
},
)
PostButton(
onPost = {
postViewModel.create()
onClose()
},
postViewModel.channelName.value.text
.isNotBlank(),
)
}
Spacer(modifier = Modifier.height(15.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.channel_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelName.value,
onValueChange = { postViewModel.channelName.value = it },
placeholder = {
Text(
text = stringRes(R.string.my_awesome_group),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
Spacer(modifier = Modifier.height(15.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.picture_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelPicture.value,
onValueChange = { postViewModel.channelPicture.value = it },
placeholder = {
Text(
text = "http://mygroup.com/logo.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
)
Spacer(modifier = Modifier.height(15.dp))
OutlinedTextField(
label = { Text(text = stringRes(R.string.description)) },
modifier =
Modifier
.fillMaxWidth()
.height(100.dp),
value = postViewModel.channelDescription.value,
onValueChange = { postViewModel.channelDescription.value = it },
placeholder = {
Text(
text = stringRes(R.string.about_us),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
maxLines = 10,
)
}
}
}
@@ -0,0 +1,318 @@
/**
* 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.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectSingleFromGallery
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.routeFor
import com.vitorpamplona.amethyst.ui.note.LoadChannel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CreateButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.mockAccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.SettingsCategory
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip01Core.core.HexKey
@Composable
fun ChannelMetadataScreen(
channelId: HexKey? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
if (channelId == null) {
ChannelMetadataScreen(null as PublicChatChannel?, accountViewModel, nav)
} else {
LoadChannel(channelId, accountViewModel) {
if (it is PublicChatChannel) {
ChannelMetadataScreen(it, accountViewModel, nav)
}
}
}
}
@Composable
fun ChannelMetadataScreen(
channel: PublicChatChannel? = null,
accountViewModel: AccountViewModel,
nav: INav,
) {
val postViewModel: ChannelMetadataViewModel = viewModel()
postViewModel.load(accountViewModel.account, channel)
ChannelMetadataScaffold(
postViewModel = postViewModel,
accountViewModel = accountViewModel,
nav = nav,
)
}
@Preview
@Composable
private fun DialogContentPreview() {
val accountViewModel = mockAccountViewModel()
val postViewModel: ChannelMetadataViewModel = viewModel()
postViewModel.load(accountViewModel.account, null)
ThemeComparisonColumn {
ChannelMetadataScaffold(
postViewModel = postViewModel,
accountViewModel = accountViewModel,
nav = EmptyNav,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun ChannelMetadataScaffold(
postViewModel: ChannelMetadataViewModel,
accountViewModel: AccountViewModel,
nav: INav,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Spacer(modifier = MinHorzSpacer)
Text(
text = stringRes(R.string.public_chat),
modifier = Modifier.weight(1f),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.titleLarge,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
)
if (postViewModel.isNewChannel()) {
CreateButton(
onPost = {
postViewModel.createOrUpdate {
nav.nav(routeFor(it))
}
nav.popBack()
},
postViewModel.canPost,
)
} else {
SaveButton(
onPost = {
postViewModel.createOrUpdate { }
nav.popBack()
},
postViewModel.canPost,
)
}
}
},
navigationIcon = {
Row {
Spacer(modifier = StdHorzSpacer)
CloseButton(
onPress = {
postViewModel.clear()
nav.popBack()
},
)
}
},
colors =
TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
)
},
) { pad ->
val feedState by postViewModel.channelRelays.collectAsStateWithLifecycle()
LazyColumn(
Modifier
.fillMaxSize()
.padding(
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
) {
item {
SettingsCategory(
stringRes(R.string.public_chat_title),
stringRes(R.string.public_chat_explainer),
Modifier.padding(bottom = 8.dp),
)
ChannelName(postViewModel)
Spacer(modifier = DoubleVertSpacer)
Picture(postViewModel, accountViewModel)
Spacer(modifier = DoubleVertSpacer)
Description(postViewModel)
SettingsCategory(
stringRes(R.string.public_chat_relays_title),
stringRes(R.string.public_chat_relays_explainer),
)
}
itemsIndexed(feedState, key = { _, item -> "ChatRelays" + item.url }) { index, item ->
BasicRelaySetupInfoDialog(
item,
onDelete = { postViewModel.deleteHomeRelay(item) },
accountViewModel = accountViewModel,
nav,
)
}
item {
RelayUrlEditField { postViewModel.addHomeRelay(relaySetupInfoBuilder(it)) }
Spacer(modifier = DoubleVertSpacer)
}
}
}
}
@Composable
private fun Description(postViewModel: ChannelMetadataViewModel) {
OutlinedTextField(
label = { Text(text = stringRes(R.string.description)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelDescription.value,
onValueChange = { postViewModel.channelDescription.value = it },
placeholder = {
Text(
text = stringRes(R.string.about_us),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
minLines = 3,
)
}
@Composable
private fun Picture(
postViewModel: ChannelMetadataViewModel,
accountViewModel: AccountViewModel,
) {
OutlinedTextField(
label = { Text(text = stringRes(R.string.picture_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelPicture.value,
onValueChange = { postViewModel.channelPicture.value = it },
placeholder = {
Text(
text = "http://mygroup.com/logo.jpg",
color = MaterialTheme.colorScheme.placeholderText,
)
},
leadingIcon = {
val context = LocalContext.current
SelectSingleFromGallery(
isUploading = postViewModel.isUploadingImageForPicture,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = Modifier.padding(start = 2.dp),
) {
postViewModel.uploadForPicture(it, context, onError = accountViewModel.toastManager::toast)
}
},
)
}
@Composable
private fun ChannelName(postViewModel: ChannelMetadataViewModel) {
OutlinedTextField(
label = { Text(text = stringRes(R.string.channel_name)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.channelName.value,
onValueChange = { postViewModel.channelName.value = it },
placeholder = {
Text(
text = stringRes(R.string.my_awesome_group),
color = MaterialTheme.colorScheme.placeholderText,
)
},
keyboardOptions =
KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
),
textStyle = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
)
}
@@ -20,18 +20,40 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata
import android.content.Context
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.text.input.TextFieldValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.PublicChatChannel
import com.vitorpamplona.amethyst.service.uploads.CompressorQuality
import com.vitorpamplona.amethyst.service.uploads.MediaCompressor
import com.vitorpamplona.amethyst.service.uploads.blossom.BlossomUploader
import com.vitorpamplona.amethyst.service.uploads.nip96.Nip96Uploader
import com.vitorpamplona.amethyst.ui.actions.mediaServers.ServerType
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectedMedia
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
import com.vitorpamplona.quartz.nip01Core.tags.events.ETag
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelCreateEvent
import com.vitorpamplona.quartz.nip28PublicChat.admin.ChannelMetadataEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.collections.isNotEmpty
import kotlin.collections.map
import kotlin.collections.plus
import kotlin.coroutines.cancellation.CancellationException
class ChannelMetadataViewModel : ViewModel() {
private var account: Account? = null
@@ -41,6 +63,15 @@ class ChannelMetadataViewModel : ViewModel() {
val channelPicture = mutableStateOf(TextFieldValue())
val channelDescription = mutableStateOf(TextFieldValue())
private val _channelRelays = MutableStateFlow<List<BasicRelaySetupInfo>>(emptyList())
val channelRelays = _channelRelays.asStateFlow()
var isUploadingImageForPicture by mutableStateOf(false)
val canPost by derivedStateOf {
channelName.value.text.isNotBlank()
}
fun load(
account: Account,
channel: PublicChatChannel?,
@@ -51,10 +82,19 @@ class ChannelMetadataViewModel : ViewModel() {
channelName.value = TextFieldValue(channel.info.name ?: "")
channelPicture.value = TextFieldValue(channel.info.picture ?: "")
channelDescription.value = TextFieldValue(channel.info.about ?: "")
val relays =
channel.info.relays
?.map { relaySetupInfoBuilder(it) }
?.distinctBy { it.url }
_channelRelays.update { relays ?: emptyList() }
}
}
fun create() {
fun isNewChannel() = originalChannel == null && _channelRelays.value.isNotEmpty()
fun createOrUpdate(onDone: (PublicChatChannel) -> Unit) {
viewModelScope.launch(Dispatchers.IO) {
account?.let { account ->
val channel = originalChannel
@@ -64,10 +104,21 @@ class ChannelMetadataViewModel : ViewModel() {
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
null,
channelRelays.value.map { it.url },
)
account.sendCreateNewChannel(template)
account.signAndSendPrivatelyOrBroadcast(
template,
relayList = { it.channelInfo().relays },
onDone = {
val channel = LocalCache.getOrCreateChannel(it.id) { PublicChatChannel(it) }
// follows the channel
account.follow(channel)
if (channel is PublicChatChannel) {
onDone(channel)
}
},
)
} else {
val event = channel.event
@@ -79,7 +130,7 @@ class ChannelMetadataViewModel : ViewModel() {
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
null,
channelRelays.value.map { it.url },
hint,
)
} else {
@@ -89,12 +140,21 @@ class ChannelMetadataViewModel : ViewModel() {
channelName.value.text,
channelDescription.value.text,
channelPicture.value.text,
null,
channelRelays.value.map { it.url },
eTag,
)
}
account.sendChangeChannel(template)
account.signAndSendPrivatelyOrBroadcast(
template,
relayList = { it.channelInfo().relays },
onDone = {
val channel = LocalCache.getOrCreateChannel(it.id) { PublicChatChannel(it) }
if (channel is PublicChatChannel) {
onDone(channel)
}
},
)
}
}
@@ -102,9 +162,91 @@ class ChannelMetadataViewModel : ViewModel() {
}
}
fun addHomeRelay(relay: BasicRelaySetupInfo) {
if (_channelRelays.value.any { it.url == relay.url }) return
_channelRelays.update { it.plus(relay) }
}
fun deleteHomeRelay(relay: BasicRelaySetupInfo) {
_channelRelays.update { it.minus(relay) }
}
fun clear() {
channelName.value = TextFieldValue()
channelPicture.value = TextFieldValue()
channelDescription.value = TextFieldValue()
_channelRelays.update { emptyList() }
}
fun uploadForPicture(
uri: SelectedMedia,
context: Context,
onError: (String, String) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
upload(
uri,
context,
onUploading = { isUploadingImageForPicture = it },
onUploaded = { channelPicture.value = TextFieldValue(it) },
onError = onError,
)
}
}
private suspend fun upload(
galleryUri: SelectedMedia,
context: Context,
onUploading: (Boolean) -> Unit,
onUploaded: (String) -> Unit,
onError: (String, String) -> Unit,
) {
val account = account ?: return
onUploading(true)
val compResult = MediaCompressor().compress(galleryUri.uri, galleryUri.mimeType, CompressorQuality.MEDIUM, context.applicationContext)
try {
val result =
if (account.settings.defaultFileServer.type == ServerType.NIP96) {
Nip96Uploader().upload(
uri = compResult.uri,
contentType = compResult.contentType,
size = compResult.size,
alt = null,
sensitiveContent = null,
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
forceProxy = account::shouldUseTorForNIP96,
onProgress = {},
httpAuth = account::createHTTPAuthorization,
context = context,
)
} else {
BlossomUploader().upload(
uri = compResult.uri,
contentType = compResult.contentType,
size = compResult.size,
alt = null,
sensitiveContent = null,
serverBaseUrl = account.settings.defaultFileServer.baseUrl,
forceProxy = account::shouldUseTorForNIP96,
httpAuth = account::createBlossomUploadAuth,
context = context,
)
}
if (result.url != null) {
onUploading(false)
onUploaded(result.url)
} else {
onUploading(false)
onError(stringRes(context, R.string.failed_to_upload_media_no_details), stringRes(context, R.string.server_did_not_provide_a_url_after_uploading))
}
} catch (e: Exception) {
if (e is CancellationException) throw e
onUploading(false)
onError(stringRes(context, R.string.failed_to_upload_media_no_details), e.message ?: e.javaClass.simpleName)
}
}
}
@@ -22,6 +22,8 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.send
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -33,6 +35,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.UrlUserTagTransformation
import com.vitorpamplona.amethyst.ui.actions.uploads.SelectFromGallery
@@ -45,7 +48,6 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.DisplayReplying
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.utils.ThinSendButton
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.EditFieldBorder
import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
@@ -139,7 +141,7 @@ fun EditFieldRow(
SelectFromGallery(
isUploading = channelScreenModel.isUploadingImage,
tint = MaterialTheme.colorScheme.placeholderText,
modifier = EditFieldLeadingIconModifier,
modifier = Modifier.height(32.dp).padding(start = 2.dp),
onImageChosen = channelScreenModel::pickedMedia,
)
},
@@ -48,27 +48,17 @@ import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.navigation.EmptyNav.nav
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.buildNewPostRoute
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.publicChannels.nip28PublicChat.metadata.ChannelMetadataDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font12SP
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
@Composable
fun ChannelFabColumn(
accountViewModel: AccountViewModel,
nav: INav,
) {
fun ChannelFabColumn(nav: INav) {
var isOpen by remember { mutableStateOf(false) }
var wantsToCreateChannel by remember { mutableStateOf(false) }
if (wantsToCreateChannel) {
ChannelMetadataDialog({ wantsToCreateChannel = false }, accountViewModel = accountViewModel)
}
Column {
AnimatedVisibility(
visible = isOpen,
@@ -101,7 +91,7 @@ fun ChannelFabColumn(
FloatingActionButton(
onClick = {
wantsToCreateChannel = true
nav.nav("ChannelMetadataEdit")
isOpen = false
},
modifier = Size55Modifier,
@@ -90,7 +90,7 @@ fun MessagesSinglePane(
}
},
floatingButton = {
ChannelFabColumn(accountViewModel, nav)
ChannelFabColumn(nav)
},
accountViewModel = accountViewModel,
) {
@@ -97,7 +97,7 @@ fun MessagesTwoPane(
)
Box(Modifier.padding(Size20dp), contentAlignment = Alignment.Center) {
ChannelFabColumn(accountViewModel, nav)
ChannelFabColumn(nav)
}
}
},
@@ -21,13 +21,10 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@@ -40,32 +37,24 @@ fun FollowButton(
text: Int = R.string.follow,
onClick: () -> Unit,
) {
Button(
FilledTonalButton(
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
shape = LeftHalfCircleButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(text), color = Color.White, textAlign = TextAlign.Center)
Text(text = stringRes(text), textAlign = TextAlign.Center)
}
}
@Composable
fun UnfollowButton(onClick: () -> Unit) {
Button(
FilledTonalButton(
modifier = Modifier.padding(horizontal = 3.dp),
onClick = onClick,
shape = LeftHalfCircleButtonBorder,
colors =
ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary,
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.unfollow), color = Color.White)
Text(text = stringRes(R.string.unfollow))
}
}
@@ -24,11 +24,10 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.EditNote
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@@ -50,7 +49,7 @@ fun InnerEditButtonPreview() {
@Composable
fun InnerEditButton(onClick: () -> Unit) {
Button(
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
@@ -59,7 +58,6 @@ fun InnerEditButton(onClick: () -> Unit) {
contentPadding = ZeroPadding,
) {
Icon(
tint = Color.White,
imageVector = Icons.Default.EditNote,
contentDescription = stringRes(R.string.edits_the_user_s_metadata),
)
@@ -22,12 +22,11 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
@@ -48,7 +47,7 @@ fun MessageButton(
) {
val scope = rememberCoroutineScope()
Button(
FilledTonalButton(
modifier =
Modifier
.padding(horizontal = 3.dp)
@@ -62,7 +61,6 @@ fun MessageButton(
painter = painterResource(R.drawable.ic_dm),
stringRes(R.string.send_a_direct_message),
modifier = Size20Modifier,
tint = Color.White,
)
}
}
@@ -21,13 +21,12 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.zaps
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
@@ -36,7 +35,7 @@ import com.vitorpamplona.amethyst.ui.theme.ButtonPadding
@Composable
fun ShowUserButton(onClick: () -> Unit) {
Button(
FilledTonalButton(
modifier = Modifier.padding(start = 3.dp),
onClick = onClick,
shape = ButtonBorder,
@@ -46,6 +45,6 @@ fun ShowUserButton(onClick: () -> Unit) {
),
contentPadding = ButtonPadding,
) {
Text(text = stringRes(R.string.unblock), color = Color.White)
Text(text = stringRes(R.string.unblock))
}
}
@@ -20,22 +20,26 @@
*/
package com.vitorpamplona.amethyst.ui.screen.loggedIn.qrcode
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -43,7 +47,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.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
@@ -57,6 +60,7 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji
import com.vitorpamplona.amethyst.ui.components.DisplayNIP05
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SetDialogToEdgeToEdge
import com.vitorpamplona.amethyst.ui.components.nip05VerificationAsAState
import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@@ -65,6 +69,8 @@ import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.Font14SP
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.Size35dp
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.largeProfilePictureModifier
import com.vitorpamplona.quartz.nip01Core.metadata.UserMetadata
@Preview
@@ -98,6 +104,7 @@ fun BackButton(onPress: () -> Unit) {
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ShowQRDialog(
user: User,
@@ -111,96 +118,113 @@ fun ShowQRDialog(
onDismissRequest = onClose,
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Surface {
Column {
Row(
modifier = Modifier.padding(10.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
BackButton(onPress = onClose)
}
SetDialogToEdgeToEdge()
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
verticalArrangement = Arrangement.SpaceAround,
) {
if (presenting) {
Column {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
RobohashFallbackAsyncImage(
robot = user.pubkeyHex,
model = user.profilePicture(),
contentDescription = stringRes(R.string.profile_image),
modifier =
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, MaterialTheme.colorScheme.onBackground, CircleShape),
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
CreateTextWithEmoji(
text = user.info?.bestName() ?: "",
tags = user.info?.tags,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
) {
val nip05 = user.nip05()
if (nip05 != null) {
val nip05Verified =
nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
DisplayNIP05(nip05, nip05Verified, accountViewModel)
} else {
Text(
text = user.pubkeyDisplayHex(),
fontSize = Font14SP,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
Scaffold(
topBar = {
TopAppBar(
title = {},
navigationIcon = {
Row {
Spacer(modifier = StdHorzSpacer)
BackButton(onPress = onClose)
}
},
colors =
TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
)
},
) { pad ->
Surface(
Modifier
.fillMaxSize()
.padding(
start = 10.dp,
end = 10.dp,
top = pad.calculateTopPadding(),
bottom = pad.calculateBottomPadding(),
).consumeWindowInsets(pad)
.imePadding(),
) {
Column {
Column(
modifier = Modifier.fillMaxSize().padding(horizontal = 10.dp),
verticalArrangement = Arrangement.SpaceAround,
) {
if (presenting) {
Column {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth(),
) {
RobohashFallbackAsyncImage(
robot = user.pubkeyHex,
model = user.profilePicture(),
contentDescription = stringRes(R.string.profile_image),
modifier = MaterialTheme.colorScheme.largeProfilePictureModifier,
loadProfilePicture = accountViewModel.settings.showProfilePictures.value,
loadRobohash = accountViewModel.settings.featureSet != FeatureSetType.PERFORMANCE,
)
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 10.dp),
) {
CreateTextWithEmoji(
text = user.info?.bestName() ?: "",
tags = user.info?.tags,
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
) {
val nip05 = user.nip05()
if (nip05 != null) {
val nip05Verified =
nip05VerificationAsAState(user.info!!, user.pubkeyHex, accountViewModel)
DisplayNIP05(nip05, nip05Verified, accountViewModel)
} else {
Text(
text = user.pubkeyDisplayHex(),
fontSize = Font14SP,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = Size10dp),
) {
QrCodeDrawer(user.toNostrUri())
}
Row(modifier = Modifier.padding(horizontal = 30.dp)) {
FilledTonalButton(
onClick = { presenting = false },
shape = RoundedCornerShape(Size35dp),
modifier = Modifier.fillMaxWidth().height(50.dp),
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth().padding(horizontal = Size10dp),
) {
Text(text = stringRes(R.string.scan_qr))
QrCodeDrawer(user.toNostrUri())
}
}
} else {
NIP19QrCodeScanner {
if (it.isNullOrEmpty()) {
presenting = true
} else {
onScan(it)
Row(modifier = Modifier.padding(horizontal = 30.dp)) {
FilledTonalButton(
onClick = { presenting = false },
shape = RoundedCornerShape(Size35dp),
modifier = Modifier.fillMaxWidth().height(50.dp),
) {
Text(text = stringRes(R.string.scan_qr))
}
}
} else {
NIP19QrCodeScanner {
if (it.isNullOrEmpty()) {
presenting = true
} else {
onScan(it)
}
}
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -54,6 +54,21 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.DMRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm.renderDMItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3.Kind3RelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3.renderKind3Items
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3.renderKind3ProposalItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.LocalRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local.renderLocalItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.PrivateOutboxRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37.renderPrivateOutboxItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.Nip65RelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65HomeItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65.renderNip65NotifItems
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.SearchRelayListViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search.renderSearchItems
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.MinHorzSpacer
@@ -61,10 +76,9 @@ import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.grayText
import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@Composable
fun AllRelayListView(
fun AllRelayListScreen(
relayToAdd: String? = null,
accountViewModel: AccountViewModel,
nav: INav,
@@ -275,7 +289,11 @@ fun ResetSearchRelays(postViewModel: SearchRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultSearchRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
DefaultSearchRelayList.forEach {
postViewModel.addRelay(
relaySetupInfoBuilder(it),
)
}
postViewModel.loadRelayDocuments()
},
) {
@@ -288,7 +306,9 @@ fun ResetDMRelays(postViewModel: DMRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultDMRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
DefaultDMRelayList.forEach {
postViewModel.addRelay(relaySetupInfoBuilder(it))
}
postViewModel.loadRelayDocuments()
},
) {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -0,0 +1,44 @@
/**
* 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.ui.screen.loggedIn.relays.common
import androidx.compose.runtime.Immutable
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@Immutable
data class BasicRelaySetupInfo(
val url: String,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
fun relaySetupInfoBuilder(url: String): BasicRelaySetupInfo {
val normalized = RelayUrlFormatter.normalize(url)
return BasicRelaySetupInfo(
normalized,
RelayStats.get(normalized),
)
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
@@ -18,14 +18,13 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import com.vitorpamplona.amethyst.service.replace
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -80,12 +79,8 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
val relayList = getRelayList() ?: emptyList()
relayList
.map { relayUrl ->
BasicRelaySetupInfo(
RelayUrlFormatter.normalize(relayUrl),
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }
.map { relaySetupInfoBuilder(it) }
.distinctBy { it.url }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
@@ -112,6 +107,6 @@ abstract class BasicRelaySetupInfoModel : ViewModel() {
relay: BasicRelaySetupInfo,
paid: Boolean,
) {
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
_relays.update { it.replace(relay, relay.copy(paidRelay = paid)) }
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import android.widget.Toast
import androidx.compose.foundation.gestures.detectTapGestures
@@ -39,6 +39,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.countToHumanReadable
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.allGoodColor
@@ -18,10 +18,12 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
@@ -35,16 +37,27 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.Size10dp
import com.vitorpamplona.amethyst.ui.theme.ThemeComparisonColumn
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
@Preview
@Composable
fun RelayUrlEditFieldPreview() {
ThemeComparisonColumn {
RelayUrlEditField {}
}
}
@Composable
fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
fun RelayUrlEditField(onNewRelay: (String) -> Unit) {
var url by remember { mutableStateOf("") }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Size10dp)) {
@@ -61,12 +74,28 @@ fun RelayUrlEditField(onNewRelay: (BasicRelaySetupInfo) -> Unit) {
)
},
singleLine = true,
keyboardOptions =
KeyboardOptions.Default.copy(
autoCorrectEnabled = false,
imeAction = ImeAction.Go,
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Text,
),
keyboardActions =
KeyboardActions(
onGo = {
if (url.isNotBlank() && url != "/") {
onNewRelay(url)
url = ""
}
},
),
)
Button(
onClick = {
if (url.isNotBlank() && url != "/") {
onNewRelay(BasicRelaySetupInfo(RelayUrlFormatter.normalize(url), RelayStat()))
onNewRelay(url)
url = ""
}
},
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -49,11 +49,11 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -153,7 +153,9 @@ fun ResetDMRelaysLonger(postViewModel: DMRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultDMRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
DefaultDMRelayList.forEach {
postViewModel.addRelay(relaySetupInfoBuilder(it))
}
postViewModel.loadRelayDocuments()
},
) {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -32,6 +32,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -71,6 +75,6 @@ fun LazyListScope.renderDMItems(
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addRelay(it) }
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -18,7 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.dm
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
class DMRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<String>? = account.getDMRelayList()?.relays()
@@ -0,0 +1,38 @@
/**
* 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.ui.screen.loggedIn.relays.kind3
import androidx.compose.runtime.Immutable
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@Immutable
data class Kind3BasicRelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val feedTypes: Set<FeedType>,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi
@@ -70,11 +70,16 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.FeatureSetType
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.service.countToHumanReadable
import com.vitorpamplona.amethyst.service.countToHumanReadableBytes
import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.note.RenderRelayIcon
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations.Kind3RelayProposalSetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations.Kind3RelaySetupInfoProposalDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
import com.vitorpamplona.amethyst.ui.theme.DividerThickness
@@ -90,10 +95,10 @@ import com.vitorpamplona.amethyst.ui.theme.allGoodColor
import com.vitorpamplona.amethyst.ui.theme.largeRelayIconModifier
import com.vitorpamplona.amethyst.ui.theme.placeholderText
import com.vitorpamplona.amethyst.ui.theme.warningColor
import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats
import com.vitorpamplona.ammolite.relays.FeedType
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import kotlinx.coroutines.launch
@@ -184,7 +189,7 @@ fun ServerConfigPreview() {
sentBytes = 10000000,
spamCounter = 10,
),
feedTypes = Constants.activeTypesGlobalChats,
feedTypes = activeTypesGlobalChats,
paidRelay = true,
),
onDelete = {},
@@ -790,13 +795,14 @@ fun Kind3RelayEditBox(
Button(
onClick = {
if (url.isNotBlank() && url != "/") {
val normalized = RelayUrlFormatter.normalize(url)
onNewRelay(
Kind3BasicRelaySetupInfo(
url = RelayUrlFormatter.normalize(url),
url = normalized,
read = read,
write = write,
feedTypes = activeTypesGlobalChats,
relayStat = RelayStat(),
relayStat = RelayStats.get(normalized),
),
)
url = ""
@@ -18,13 +18,16 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.kind3
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.amethyst.service.togglePresenceInSet
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations.Kind3RelayProposalSetupInfo
import com.vitorpamplona.ammolite.relays.Constants
import com.vitorpamplona.ammolite.relays.Constants.activeTypesGlobalChats
import com.vitorpamplona.ammolite.relays.FeedType
@@ -38,6 +41,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlin.collections.plus
@Stable
class Kind3RelayListViewModel : ViewModel() {
@@ -206,7 +210,18 @@ class Kind3RelayListViewModel : ViewModel() {
fun addRelay(relay: Kind3RelayProposalSetupInfo) {
if (relays.value.any { it.url == relay.url }) return
_relays.update { it.plus(Kind3BasicRelaySetupInfo(relay.url, relay.read, relay.write, relay.feedTypes, relay.relayStat, relay.paidRelay)) }
_relays.update {
it.plus(
Kind3BasicRelaySetupInfo(
relay.url,
relay.read,
relay.write,
relay.feedTypes,
relay.relayStat,
relay.paidRelay,
),
)
}
refreshProposals()
@@ -230,42 +245,42 @@ class Kind3RelayListViewModel : ViewModel() {
}
fun toggleDownload(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.updated(relay, relay.copy(read = !relay.read)) }
_relays.update { it.replace(relay, relay.copy(read = !relay.read)) }
hasModified = true
}
fun toggleUpload(relay: Kind3BasicRelaySetupInfo) {
_relays.update { it.updated(relay, relay.copy(write = !relay.write)) }
_relays.update { it.replace(relay, relay.copy(write = !relay.write)) }
hasModified = true
}
fun toggleFollows(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.FOLLOWS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.FOLLOWS)
_relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleMessages(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PRIVATE_DMS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.PRIVATE_DMS)
_relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun togglePublicChats(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.PUBLIC_CHATS)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.PUBLIC_CHATS)
_relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleGlobal(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.GLOBAL)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.GLOBAL)
_relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
fun toggleSearch(relay: Kind3BasicRelaySetupInfo) {
val newTypes = togglePresenceInSet(relay.feedTypes, FeedType.SEARCH)
_relays.update { it.updated(relay, relay.copy(feedTypes = newTypes)) }
val newTypes = relay.feedTypes.togglePresenceInSet(FeedType.SEARCH)
_relays.update { it.replace(relay, relay.copy(feedTypes = newTypes)) }
hasModified = true
}
@@ -273,16 +288,6 @@ class Kind3RelayListViewModel : ViewModel() {
relay: Kind3BasicRelaySetupInfo,
paid: Boolean,
) {
_relays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
_relays.update { it.replace(relay, relay.copy(paidRelay = paid)) }
}
}
fun <T> Iterable<T>.updated(
old: T,
new: T,
): List<T> = map { if (it == old) new else it }
fun <T> togglePresenceInSet(
set: Set<T>,
item: T,
): Set<T> = if (set.contains(item)) set.minus(item) else set.plus(item)
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -32,6 +32,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -71,6 +75,6 @@ fun LazyListScope.renderLocalItems(
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addRelay(it) }
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -18,7 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.local
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
class LocalRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<String> = account.settings.localRelayServers.toList()
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -32,6 +32,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -71,6 +75,6 @@ fun LazyListScope.renderPrivateOutboxItems(
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addRelay(it) }
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -18,7 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip37
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
class PrivateOutboxRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<String>? = account.getPrivateOutboxRelayList()?.relays()
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -32,6 +32,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -74,7 +78,7 @@ fun LazyListScope.renderNip65HomeItems(
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addHomeRelay(it) }
RelayUrlEditField { postViewModel.addHomeRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -95,6 +99,6 @@ fun LazyListScope.renderNip65NotifItems(
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addNotifRelay(it) }
RelayUrlEditField { postViewModel.addNotifRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -18,16 +18,17 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.nip65
import androidx.compose.runtime.Stable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.Nip11CachedRetriever
import com.vitorpamplona.ammolite.relays.RelayStats
import com.vitorpamplona.amethyst.service.replace
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.quartz.nip65RelayList.AdvertisedRelayListEvent
import com.vitorpamplona.quartz.nip65RelayList.RelayUrlFormatter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -110,12 +111,8 @@ class Nip65RelayListViewModel : ViewModel() {
val relayList = account.getNIP65RelayList()?.writeRelays() ?: emptyList()
relayList
.map { relayUrl ->
BasicRelaySetupInfo(
RelayUrlFormatter.normalize(relayUrl),
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }
.map { relaySetupInfoBuilder(it) }
.distinctBy { it.url }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
@@ -124,12 +121,8 @@ class Nip65RelayListViewModel : ViewModel() {
val relayList = account.getNIP65RelayList()?.readRelays() ?: emptyList()
relayList
.map { relayUrl ->
BasicRelaySetupInfo(
RelayUrlFormatter.normalize(relayUrl),
RelayStats.get(relayUrl),
)
}.distinctBy { it.url }
.map { relaySetupInfoBuilder(it) }
.distinctBy { it.url }
.sortedBy { it.relayStat.receivedBytes }
.reversed()
}
@@ -156,7 +149,7 @@ class Nip65RelayListViewModel : ViewModel() {
relay: BasicRelaySetupInfo,
paid: Boolean,
) {
_homeRelays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
_homeRelays.update { it.replace(relay, relay.copy(paidRelay = paid)) }
}
fun addNotifRelay(relay: BasicRelaySetupInfo) {
@@ -180,6 +173,6 @@ class Nip65RelayListViewModel : ViewModel() {
relay: BasicRelaySetupInfo,
paid: Boolean,
) {
_notificationRelays.update { it.updated(relay, relay.copy(paidRelay = paid)) }
_notificationRelays.update { it.replace(relay, relay.copy(paidRelay = paid)) }
}
}
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations
import androidx.compose.runtime.Immutable
import com.vitorpamplona.ammolite.relays.FeedType
@@ -26,27 +26,6 @@ import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
import com.vitorpamplona.quartz.nip01Core.core.HexKey
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@Immutable
data class BasicRelaySetupInfo(
val url: String,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
@Immutable
data class Kind3BasicRelaySetupInfo(
val url: String,
val read: Boolean,
val write: Boolean,
val feedTypes: Set<FeedType>,
val relayStat: RelayStat,
val paidRelay: Boolean = false,
) {
val briefInfo: RelayBriefInfoCache.RelayBriefInfo = RelayBriefInfoCache.RelayBriefInfo(url)
}
@Immutable
data class Kind3RelayProposalSetupInfo(
val url: String,
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@@ -32,6 +32,7 @@ import com.vitorpamplona.amethyst.service.Nip11Retriever
import com.vitorpamplona.amethyst.ui.actions.RelayInfoDialog
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.RelayInformationDialog
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.ammolite.relays.RelayBriefInfoCache
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.recommendations
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -49,11 +49,11 @@ import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.stringRes
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
import com.vitorpamplona.amethyst.ui.theme.imageModifier
import com.vitorpamplona.quartz.nip01Core.relay.RelayStat
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -153,7 +153,9 @@ fun ResetSearchRelaysLonger(postViewModel: SearchRelayListViewModel) {
OutlinedButton(
onClick = {
postViewModel.deleteAll()
DefaultSearchRelayList.forEach { postViewModel.addRelay(BasicRelaySetupInfo(it, RelayStat())) }
DefaultSearchRelayList.forEach {
postViewModel.addRelay(relaySetupInfoBuilder(it))
}
postViewModel.loadRelayDocuments()
},
) {
@@ -18,7 +18,7 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -32,6 +32,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.vitorpamplona.amethyst.ui.navigation.INav
import com.vitorpamplona.amethyst.ui.navigation.rememberExtendedNav
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfo
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoDialog
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.RelayUrlEditField
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.relaySetupInfoBuilder
import com.vitorpamplona.amethyst.ui.theme.FeedPadding
import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer
@@ -72,6 +76,6 @@ fun LazyListScope.renderSearchItems(
item {
Spacer(modifier = StdVertSpacer)
RelayUrlEditField { postViewModel.addRelay(it) }
RelayUrlEditField { postViewModel.addRelay(relaySetupInfoBuilder(it)) }
}
}
@@ -18,7 +18,9 @@
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.ui.actions.relays
package com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.search
import com.vitorpamplona.amethyst.ui.screen.loggedIn.relays.common.BasicRelaySetupInfoModel
class SearchRelayListViewModel : BasicRelaySetupInfoModel() {
override fun getRelayList(): List<String>? = account.getSearchRelayList()?.relays()
@@ -24,6 +24,14 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
val Primary50 = Color(red = 127, green = 103, blue = 190)
val Primary60 = Color(red = 154, green = 130, blue = 219)
val Primary70 = Color(red = 182, green = 157, blue = 248)
val Primary80 = Color(red = 208, green = 188, blue = 255)
val DEFAULT_PRIMARY = Color(red = 208, green = 188, blue = 255)
val LIGHT_PURPLE = Color(red = 187, green = 134, blue = 252)
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
@@ -0,0 +1,74 @@
/**
* 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.ui.theme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.model.ThemeType
import com.vitorpamplona.amethyst.ui.screen.SharedPreferencesViewModel
@Composable
fun ThemeComparisonColumn(toPreview: @Composable () -> Unit) {
Column {
Box {
val darkTheme: SharedPreferencesViewModel = viewModel()
darkTheme.updateTheme(ThemeType.DARK)
AmethystTheme(darkTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
Box {
val lightTheme: SharedPreferencesViewModel = viewModel()
lightTheme.updateTheme(ThemeType.LIGHT)
AmethystTheme(lightTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
}
}
@Composable
fun ThemeComparisonRow(toPreview: @Composable () -> Unit) {
Row {
Box(modifier = Modifier.weight(1f)) {
val darkTheme: SharedPreferencesViewModel = viewModel()
darkTheme.updateTheme(ThemeType.DARK)
AmethystTheme(darkTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
Box(modifier = Modifier.weight(1f)) {
val lightTheme: SharedPreferencesViewModel = viewModel()
lightTheme.updateTheme(ThemeType.LIGHT)
AmethystTheme(lightTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
}
}
@@ -213,7 +213,6 @@ val CashuCardBorders = Modifier.fillMaxWidth().padding(10.dp).clip(shape = Quote
val EditFieldModifier =
Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth()
val EditFieldTrailingIconModifier = Modifier.padding(start = 5.dp, end = 0.dp)
val EditFieldLeadingIconModifier = Modifier.height(32.dp).padding(start = 2.dp)
val ZeroPadding = PaddingValues(0.dp)
val FeedPadding = PaddingValues(top = 10.dp, bottom = 10.dp)
@@ -23,19 +23,18 @@ package com.vitorpamplona.amethyst.ui.theme
import android.app.Activity
import android.app.UiModeManager
import android.content.Context
import android.graphics.Color.red
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.shape.CircleShape
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
@@ -55,7 +54,6 @@ import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel
import com.halilibo.richtext.ui.RichTextStyle
import com.halilibo.richtext.ui.resolveDefaults
import com.patrykandpatrick.vico.compose.common.VicoTheme
@@ -68,8 +66,9 @@ private val DarkColorPalette =
primary = Purple200,
secondary = Teal200,
tertiary = Teal200,
background = Color(red = 0, green = 0, blue = 0),
surface = Color(red = 0, green = 0, blue = 0),
background = Color.Black, // full black theme
surface = Color.Black, // full black theme
surfaceDim = Color.Black, // full black theme
surfaceVariant = Color(red = 29, green = 26, blue = 34),
)
@@ -78,6 +77,7 @@ private val LightColorPalette =
primary = Purple500,
secondary = Teal200,
tertiary = Teal200,
surfaceContainerHighest = Color(red = 236, green = 230, blue = 240),
surfaceVariant = Color(red = 250, green = 245, blue = 252),
)
@@ -273,6 +273,20 @@ val DarkLargeRelayIconModifier =
.size(Size55dp)
.clip(shape = CircleShape)
val darkLargeProfilePictureModifier =
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, DarkColorPalette.background, CircleShape)
val lightLargeProfilePictureModifier =
Modifier
.width(120.dp)
.height(120.dp)
.clip(shape = CircleShape)
.border(3.dp, LightColorPalette.background, CircleShape)
val RichTextDefaults = RichTextStyle().resolveDefaults()
val MarkDownStyleOnDark =
@@ -447,6 +461,9 @@ val ColorScheme.largeRelayIconModifier: Modifier
val ColorScheme.selectedReactionBoxModifier: Modifier
get() = if (isLight) LightSelectedReactionBoxModifier else DarkSelectedReactionBoxModifier
val ColorScheme.largeProfilePictureModifier: Modifier
get() = if (isLight) lightLargeProfilePictureModifier else darkLargeProfilePictureModifier
val chartLightColors =
VicoTheme(
candlestickCartesianLayerColors =
@@ -521,45 +538,3 @@ fun AmethystTheme(
}
}
}
@Composable
fun ThemeComparisonColumn(toPreview: @Composable () -> Unit) {
Column {
Box {
val darkTheme: SharedPreferencesViewModel = viewModel()
darkTheme.updateTheme(ThemeType.DARK)
AmethystTheme(darkTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
Box {
val lightTheme: SharedPreferencesViewModel = viewModel()
lightTheme.updateTheme(ThemeType.LIGHT)
AmethystTheme(lightTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
}
}
@Composable
fun ThemeComparisonRow(toPreview: @Composable () -> Unit) {
Row {
Box(modifier = Modifier.weight(1f)) {
val darkTheme: SharedPreferencesViewModel = viewModel()
darkTheme.updateTheme(ThemeType.DARK)
AmethystTheme(darkTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
Box(modifier = Modifier.weight(1f)) {
val lightTheme: SharedPreferencesViewModel = viewModel()
lightTheme.updateTheme(ThemeType.LIGHT)
AmethystTheme(lightTheme) {
Surface(color = MaterialTheme.colorScheme.background) { toPreview() }
}
}
}
}
@@ -212,6 +212,13 @@
<string name="channel_created">Kanál vytvořen</string>
<string name="channel_information_changed_to">"Informace kanálu změněna na"</string>
<string name="public_chat">Veřejný chat</string>
<string name="public_chat_title">Metadata veřejného chatu</string>
<string name="public_chat_explainer">Veřejné konverzace jsou viditelné pro všechny na Nostru a kdokoli
se na nich může podílet. Jsou skvělé pro otevřené komunity kolem konkrétních témat.
Moderaci lze ovládat smazáním příspěvků na relé</string>
<string name="public_chat_relays_title">Rele</string>
<string name="public_chat_relays_explainer">Vložte mezi 1-3 relé, které hostí tuto skupinu.
Klienti Nostr používají toto nastavení, aby věděli, kam stahovat zprávy a odesílat zprávy.</string>
<string name="posts_received">příspěvků přijato</string>
<string name="remove">Odebrat</string>
<string name="translations_auto">Automaticky</string>
@@ -216,6 +216,13 @@ anz der Bedingungen ist erforderlich</string>
<string name="channel_created">Kanal erstellt</string>
<string name="channel_information_changed_to">"Kanalinformationen geändert in"</string>
<string name="public_chat">Öffentlicher Chat</string>
<string name="public_chat_title">Öffentliche Chat Metadaten</string>
<string name="public_chat_explainer">Öffentliche Chats sind für jeden auf Nostr sichtbar und jeder
kann daran teilnehmen. Sie eignen sich hervorragend für offene Gemeinschaften rund um bestimmte Themen.
Moderation kann durch Löschen von Beiträgen auf den Relais gesteuert werden</string>
<string name="public_chat_relays_title">Relais</string>
<string name="public_chat_relays_explainer">Fügen Sie zwischen 1-3 Relais ein, die diese Gruppe beherbergen.
Nostr Clients verwenden diese Einstellung, um zu wissen, woher sie Nachrichten herunterladen und an welche gesendet werden sollen.</string>
<string name="posts_received">empfangene Beiträge</string>
<string name="remove">Entfernen</string>
<string name="translations_auto">Automatisch</string>
@@ -212,6 +212,13 @@
<string name="channel_created">Csatorna létrehozva</string>
<string name="channel_information_changed_to">"A csatornainformáció a következőre módosult:"</string>
<string name="public_chat">Nyilvános csevegés</string>
<string name="public_chat_title">Nyilvános csevegés metaadatai</string>
<string name="public_chat_explainer">A nyilvános csevegések mindenki számára láthatóak a Nostr-on, és bárki
részt vehet bennük. Ezek kiválóan alkalmasak bizonyos témák köré szerveződő nyílt közösségek számára.
A moderálás az átjátszókon lévő hozzászólások törlésével szabályozható</string>
<string name="public_chat_relays_title">Átjátszók</string>
<string name="public_chat_relays_explainer">Adjon hozzá 1-3 olyan átjátszót, amelyek kiszolgálják ezt a csoportot.
A Nostr-kliensek ezt a beállítást használják arra, hogy tudják, honnan töltsék le az üzeneteket, és hová küldjék az üzeneteket.</string>
<string name="posts_received">fogadott bejegyzések</string>
<string name="remove">Eltávolítás</string>
<string name="translations_auto">Automatikus</string>
@@ -865,6 +872,7 @@
<string name="message_to_author">Változások összefoglalása</string>
<string name="message_to_author_placeholder">Gyors javítások…</string>
<string name="accept_the_suggestion">Javaslat elfogadása</string>
<string name="enter_picture_in_picture">Videó lejátszása felugró ablakban</string>
<string name="accessibility_download_for_offline">Letöltés</string>
<string name="accessibility_lyrics_on">Feliratozás bekapcsolva</string>
<string name="accessibility_lyrics_off">Feliratozás kikapcsolva</string>
@@ -205,6 +205,7 @@
<string name="with_description_of">z opisem</string>
<string name="and_picture">i zdjęcie</string>
<string name="changed_chat_name_to">zmieniono nazwę czatu na</string>
<string name="changed_chat_profile_to">Nowy profil czatu:</string>
<string name="description_to">opis dla</string>
<string name="and_picture_to">i zdjęcie do</string>
<string name="leave">Wyjdź</string>
@@ -212,6 +213,10 @@
<string name="channel_created">Kanał utworzony</string>
<string name="channel_information_changed_to">"Informacje o kanale zmienione na"</string>
<string name="public_chat">Czat Publiczny</string>
<string name="public_chat_title">Metadane Czatu Publicznego</string>
<string name="public_chat_explainer">Czaty publiczne są widoczne dla wszystkich użytkowników Nostr i każdy może w nich uczestniczyć. Są idealne dla otwartych społeczności skupionych wokół konkretnych tematów. Moderację można kontrolować, usuwając posty z transmiterów</string>
<string name="public_chat_relays_title">Transmitery</string>
<string name="public_chat_relays_explainer">Wstaw od 1 do 3 transmiterów, które obsługują tę grupę. Klienci Nostr używają tego ustawienia, aby wiedzieć, skąd pobierać wiadomości i do kogo je wysyłać.</string>
<string name="posts_received">odebranych wiadomości</string>
<string name="remove">Usuń</string>
<string name="translations_auto">Automatycznie</string>
@@ -418,7 +423,7 @@
<string name="orbot_socks_port">Port Socks Orbota</string>
<string name="use_internal_tor">Aktywny silnik Tor</string>
<string name="use_internal_tor_explainer">Użyj wersji wewnętrznej lub Orbota</string>
<string name="tor_preset">TOR wstępne ustawienia prywatności</string>
<string name="tor_preset">Tor wstępne ustawienia prywatności</string>
<string name="tor_preset_explainer">Szybka modyfikacja wszystkich ustawień poniżej</string>
<string name="tor_use_onion_address">Adres Onion /Transmitery</string>
<string name="tor_use_onion_address_explainer">Użyj Tor dla dowolnego adresu .onion</string>
@@ -519,6 +524,7 @@
<string name="cashu_redeem_to_zap">Wyślij do Zap Wallet</string>
<string name="cashu_redeem_to_cashu">Otwórz w Portfelu Cashu</string>
<string name="cashu_copy_token">Skopiuj token</string>
<string name="quick_actions_open_in_another_app">Otwórz w innej aplikacji</string>
<string name="no_lightning_address_set">Nie ustawiono adresu Lightning</string>
<string name="copied_token_to_clipboard">Skopiowano token do schowka</string>
<string name="live_stream_live_tag">NA ŻYWO</string>
@@ -865,6 +871,7 @@
<string name="message_to_author">Podsumowanie zmian</string>
<string name="message_to_author_placeholder">Szybkie poprawki…</string>
<string name="accept_the_suggestion">Zaakceptuj sugestię</string>
<string name="enter_picture_in_picture">Uruchom wideo w pop-upie</string>
<string name="accessibility_download_for_offline">Pobierz</string>
<string name="accessibility_lyrics_on">Tekst włączony</string>
<string name="accessibility_lyrics_off">Tekst wyłączony</string>
@@ -212,6 +212,13 @@
<string name="channel_created">Canal criado</string>
<string name="channel_information_changed_to">"Informação do canal mudou para"</string>
<string name="public_chat">Chat público</string>
<string name="public_chat_title">Metadados do Chat Público</string>
<string name="public_chat_explainer">Os chats públicos são visíveis para todos no Nostr, e qualquer pessoa
pode participar deles. Eles são ótimos para comunidades abertas em torno de tópicos específicos.
A moderação pode ser controlada excluindo postagens em seus relays</string>
<string name="public_chat_relays_title">Relés</string>
<string name="public_chat_relays_explainer">Inserir entre 1-3 relés que hospedam esse grupo.
os clientes Nostr usam essa configuração para saber de onde fazer o download de mensagens e enviar suas mensagens.</string>
<string name="posts_received">postagens recebidas</string>
<string name="remove">Remover</string>
<string name="translations_auto">Automaticamente </string>
@@ -212,6 +212,13 @@
<string name="channel_created">Kanal skapad</string>
<string name="channel_information_changed_to">"Kanalinformation ändrad till"</string>
<string name="public_chat">Publik Chat</string>
<string name="public_chat_title">Metadata för offentlig chatt</string>
<string name="public_chat_explainer">Offentliga chattar är synliga för alla på Nostr och alla
kan delta på dem. De är bra för öppna samhällen kring specifika ämnen.
Moderation kan kontrolleras genom att ta bort inlägg på reläerna</string>
<string name="public_chat_relays_title">Reläer</string>
<string name="public_chat_relays_explainer">Infoga mellan 1-3 reläer som är värd för denna grupp.
Nostr klienter använder denna inställning för att veta var du kan ladda ner meddelanden från och skicka dina meddelanden till.</string>
<string name="posts_received">mottagna inlägg</string>
<string name="remove">Ta bort</string>
<string name="translations_auto">Automatiskt</string>
+12
View File
@@ -213,6 +213,7 @@
<string name="with_description_of">with description of</string>
<string name="and_picture">and picture</string>
<string name="changed_chat_name_to">changed chat name to</string>
<string name="changed_chat_profile_to">New chat profile:</string>
<string name="description_to">description to</string>
<string name="and_picture_to">and picture to</string>
<string name="leave">Leave</string>
@@ -220,6 +221,15 @@
<string name="channel_created">Channel created</string>
<string name="channel_information_changed_to">"Channel Information changed to"</string>
<string name="public_chat">Public Chat</string>
<string name="public_chat_title">Public Chat Metadata</string>
<string name="public_chat_explainer">Public chats are visible to everyone on Nostr and anyone
can participate on them. They are great for open communities around specific topics.
Moderation can be controlled by deleting posts on the its relays</string>
<string name="public_chat_relays_title">Relays</string>
<string name="public_chat_relays_explainer">Insert between 1-3 relays that host this group.
Nostr clients use this setting to know where to download messages from and send your messages to.</string>
<string name="posts_received">posts received</string>
<string name="remove">Remove</string>
<string name="sats" translatable="false">sats</string>
@@ -607,6 +617,8 @@
<string name="cashu_redeem_to_cashu">Open in Cashu Wallet</string>
<string name="cashu_copy_token">Copy Token</string>
<string name="quick_actions_open_in_another_app">Open in Another App</string>
<string name="no_lightning_address_set">No Lightning Address set</string>
<string name="copied_token_to_clipboard">Copied token to clipboard</string>
+5 -5
View File
@@ -16,11 +16,11 @@ coil = "3.1.0"
composeBom = "2025.03.00"
coreKtx = "1.15.0"
espressoCore = "3.6.1"
firebaseBom = "33.10.0"
firebaseBom = "33.11.0"
fragmentKtx = "1.8.6"
gms = "4.4.2"
jacksonModuleKotlin = "2.18.2"
jna = "5.16.0"
jacksonModuleKotlin = "2.18.3"
jna = "5.17.0"
jtorctl = "0.4.5.7"
junit = "4.13.2"
kotlin = "2.1.0"
@@ -46,9 +46,9 @@ torAndroid = "0.4.8.12"
translate = "17.0.3"
unifiedpush = "2.3.1"
urlDetector = "0.1.23"
vico-charts = "2.0.3"
vico-charts = "2.1.1"
zelory = "3.0.1"
zoomable = "2.3.0"
zoomable = "2.4.0"
zxing = "3.5.3"
zxingAndroidEmbedded = "4.3.0"
windowCoreAndroid = "1.3.0"

Some files were not shown because too many files have changed in this diff Show More