Merge remote-tracking branch 'origin/HEAD' into less_memory_test_branch

This commit is contained in:
Vitor Pamplona
2023-03-10 11:28:13 -05:00
32 changed files with 655 additions and 120 deletions
+2 -2
View File
@@ -30,6 +30,8 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [x] Long-form Content (NIP-23)
- [x] Parameterized Replaceable Events (NIP-33)
- [x] Online Relay Search (NIP-50)
- [x] Internationalization
- [x] Badges (NIP-58)
- [ ] Local Database
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
@@ -41,9 +43,7 @@ Amethyst brings the best social network to your Android phone. Just insert your
- [ ] Events with a Subject (NIP-14)
- [ ] Workspaces
- [ ] Expiration Support (NIP-40)
- [ ] Internationalization
- [ ] Infinity Scroll
- [ ] Badges (NIP-58)
- [ ] Relay List Metadata (NIP-65)
- [ ] Signing Requests (NIP-46)
- [ ] Delegated Event Signing (NIP-26)
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 90
versionName "0.24.1"
versionCode 91
versionName "0.24.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -2,18 +2,20 @@ package com.vitorpamplona.amethyst
import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import androidx.security.crypto.MasterKey
class EncryptedStorage {
object EncryptedStorage {
private const val PREFERENCES_NAME = "secret_keeper"
fun preferences(context: Context): EncryptedSharedPreferences {
val secretKey: String = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
val preferencesName = "secret_keeper"
val masterKey: MasterKey = MasterKey.Builder(context, MasterKey.DEFAULT_MASTER_KEY_ALIAS)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
return EncryptedSharedPreferences.create(
preferencesName,
secretKey,
context,
PREFERENCES_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
) as EncryptedSharedPreferences
@@ -29,7 +29,7 @@ class LocalPreferences(context: Context) {
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
}
private val encryptedPreferences = EncryptedStorage().preferences(context)
private val encryptedPreferences = EncryptedStorage.preferences(context)
private val gson = GsonBuilder().create()
fun clearEncryptedStorage() {
@@ -300,7 +300,7 @@ object LocalCache {
// Saves relay list only if it's a user that is currently been seen
user.updateContactList(event)
Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
}
}
@@ -368,7 +368,7 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
}
}
fun refresh() {
private fun refresh() {
postValue(NoteState(note))
}
@@ -33,12 +33,17 @@ class LnZapEvent(
}
override fun amount(): BigDecimal? {
return lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
return amount
}
// Keeps this as a field because it's a heavier function used everywhere.
val amount by lazy {
lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
try {
lnInvoice()?.let { LnInvoiceUtil.getAmountInSats(it) }
} catch (e: Exception) {
Log.e("LnZapEvent", "Failed to Parse LnInvoice ${description()}", e)
null
}
}
override fun containedPost(): Event? = try {
@@ -21,6 +21,9 @@ import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -66,7 +69,9 @@ class MainActivity : FragmentActivity() {
super.onResume()
// Only starts after login
ServiceManager.start()
GlobalScope.launch(Dispatchers.IO) {
ServiceManager.start()
}
}
override fun onPause() {
@@ -40,7 +40,9 @@ import androidx.navigation.NavHostController
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
val bottomNavigationItems = listOf(
Route.Home,
@@ -157,11 +159,15 @@ private fun NotifiableIcon(item: Route, currentRoute: String?, accountViewModel:
val context = LocalContext.current.applicationContext
LaunchedEffect(key1 = notif) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
withContext(Dispatchers.IO) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
}
}
LaunchedEffect(key1 = db) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
withContext(Dispatchers.IO) {
hasNewItems = item.hasNewItems(account, notif.cache, context)
}
}
if (hasNewItems) {
@@ -43,6 +43,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import androidx.navigation.NavHostController
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.BuildConfig
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.RoboHashCache
@@ -114,8 +115,8 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
Box {
val banner = accountUser.info?.banner
if (banner != null && banner.isNotBlank()) {
AsyncImageProxy(
model = ResizeImage(banner, 150.dp),
AsyncImage(
model = banner,
contentDescription = stringResource(id = R.string.profile_image),
contentScale = ContentScale.FillWidth,
modifier = Modifier
@@ -11,10 +11,12 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MilitaryTech
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -35,6 +37,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.BadgeCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -56,9 +60,11 @@ fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerN
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = likeSetCard) {
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = likeSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt(), context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt(), context)
}
}
var backgroundColor = if (isNew) {
@@ -108,11 +114,33 @@ fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerN
}
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
Text(
stringResource(R.string.new_badge_award_notif),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 5.dp)
)
Row() {
Text(
stringResource(R.string.new_badge_award_notif),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 5.dp).weight(1f)
)
Text(
timeAgo(note.createdAt(), context = context),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
IconButton(
modifier = Modifier.then(Modifier.size(24.dp)),
onClick = { popupExpanded = true }
) {
Icon(
imageVector = Icons.Default.MoreVert,
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
note.replyTo?.firstOrNull()?.let {
NoteCompose(
@@ -125,8 +153,6 @@ fun BadgeCompose(likeSetCard: BadgeCard, modifier: Modifier = Modifier, isInnerN
)
}
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
@@ -32,6 +32,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -53,9 +55,11 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = boostSetCard) {
isNew = boostSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = boostSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, boostSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, boostSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -52,6 +52,8 @@ import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun ChatroomCompose(
@@ -62,9 +64,6 @@ fun ChatroomCompose(
val noteState by baseNote.live().metadata.observeAsState()
val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val notificationCacheState = NotificationCache.live.observeAsState()
val notificationCache = notificationCacheState.value ?: return
@@ -92,9 +91,11 @@ fun ChatroomCompose(
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache, key2 = note) {
note.createdAt()?.let {
hasNewMessages =
it > notificationCache.cache.load("Channel/${channel.idHex}", context)
withContext(Dispatchers.IO) {
note.createdAt()?.let {
hasNewMessages =
it > notificationCache.cache.load("Channel/${channel.idHex}", context)
}
}
}
@@ -146,7 +147,7 @@ fun ChatroomCompose(
var userToComposeOn = note.author!!
if (replyAuthorBase != null) {
if (note.author == account.userProfile()) {
if (note.author == accountViewModel.userProfile()) {
userToComposeOn = replyAuthorBase
}
}
@@ -157,11 +158,13 @@ fun ChatroomCompose(
var hasNewMessages by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = notificationCache, key2 = note) {
noteEvent?.let {
hasNewMessages = it.createdAt() > notificationCache.cache.load(
"Room/${userToComposeOn.pubkeyHex}",
context
)
withContext(Dispatchers.IO) {
noteEvent?.let {
hasNewMessages = it.createdAt() > notificationCache.cache.load(
"Room/${userToComposeOn.pubkeyHex}",
context
)
}
}
}
@@ -169,7 +172,7 @@ fun ChatroomCompose(
channelPicture = {
UserPicture(
userToComposeOn,
account.userProfile(),
accountViewModel.userProfile(),
size = 55.dp
)
},
@@ -64,6 +64,8 @@ import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
val ChatBubbleShapeThem = RoundedCornerShape(3.dp, 15.dp, 15.dp, 15.dp)
@@ -131,12 +133,14 @@ fun ChatroomMessageCompose(
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
withContext(Dispatchers.IO) {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
}
}
}
}
@@ -32,6 +32,8 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -53,9 +55,11 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, modifier: Modifier = Modifier, isIn
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = likeSetCard) {
isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = likeSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, likeSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -0,0 +1,126 @@
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.NotificationCache
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.MessageSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = false, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
val noteState by messageSetCard.note.live().metadata.observeAsState()
val note = noteState?.note
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val context = LocalContext.current.applicationContext
val noteEvent = note?.event
var popupExpanded by remember { mutableStateOf(false) }
if (note == null) {
BlankNote(Modifier, isInnerNote)
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = messageSetCard) {
withContext(Dispatchers.IO) {
isNew =
messageSetCard.createdAt() > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, messageSetCard.createdAt(), context)
}
}
var backgroundColor = if (isNew) {
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
} else {
MaterialTheme.colors.background
}
Column(
modifier = Modifier.background(backgroundColor).combinedClickable(
onClick = {
if (noteEvent !is ChannelMessageEvent) {
navController.navigate("Note/${note.idHex}") {
launchSingleTop = true
}
} else {
note.channel()?.let {
navController.navigate("Channel/${it.idHex}")
}
}
},
onLongClick = { popupExpanded = true }
)
) {
Row(
modifier = Modifier
.padding(
start = if (!isInnerNote) 12.dp else 0.dp,
end = if (!isInnerNote) 12.dp else 0.dp,
top = 10.dp
)
) {
// Draws the like picture outside the boosted card.
if (!isInnerNote) {
Box(
modifier = Modifier
.width(55.dp)
.padding(top = 5.dp)
) {
Icon(
painter = painterResource(R.drawable.ic_dm),
null,
modifier = Modifier.size(16.dp).align(Alignment.TopEnd),
tint = MaterialTheme.colors.primary
)
}
}
Column(modifier = Modifier.padding(start = if (!isInnerNote) 10.dp else 0.dp)) {
NoteCompose(
baseNote = note,
routeForLastRead = null,
isBoostedNote = true,
addMarginTop = false,
parentBackgroundColor = backgroundColor,
accountViewModel = accountViewModel,
navController = navController
)
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}
}
}
@@ -36,6 +36,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -57,9 +59,11 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, r
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = multiSetCard) {
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -60,6 +60,8 @@ import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
import com.vitorpamplona.amethyst.ui.theme.Following
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -71,6 +73,7 @@ fun NoteCompose(
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
parentBackgroundColor: Color? = null,
accountViewModel: AccountViewModel,
navController: NavController
@@ -119,13 +122,15 @@ fun NoteCompose(
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = routeForLastRead) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
withContext(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it, context)
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt, context)
isNew = createdAt > lastTime
}
}
}
}
@@ -168,7 +173,7 @@ fun NoteCompose(
.padding(
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
top = 10.dp
top = if (addMarginTop) 10.dp else 0.dp
)
) {
if (!isBoostedNote && !isQuotedNote) {
@@ -414,6 +419,34 @@ fun NoteCompose(
ReactionsRow(note, accountViewModel)
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
)
} else if (noteEvent is PrivateDmEvent &&
noteEvent.recipientPubKey() != account.userProfile().pubkeyHex &&
note.author != account.userProfile()
) {
val recepient = noteEvent.recipientPubKey()?.let { LocalCache.checkGetOrCreateUser(it) }
TranslateableRichTextViewer(
stringResource(
id = R.string.private_conversation_notification,
"@${note.author?.pubkeyNpub()}",
"@${recepient?.pubkeyNpub()}"
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
backgroundColor,
accountViewModel,
navController
)
if (!makeItShort) {
ReactionsRow(note, accountViewModel)
}
Divider(
modifier = Modifier.padding(top = 10.dp),
thickness = 0.25.dp
@@ -220,12 +220,12 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
onDismiss()
}
) {
Text("Don't show again")
Text(stringResource(R.string.quick_action_dont_show_again_button))
}
Button(
onClick = { accountViewModel.delete(note); onDismiss() }
) {
Text("Delete")
Text(stringResource(R.string.quick_action_delete_button))
}
}
}
@@ -72,7 +72,9 @@ import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.actions.SaveButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
import java.math.RoundingMode
@@ -99,7 +101,9 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel) {
}
Row(
modifier = Modifier.padding(top = 8.dp).fillMaxWidth(),
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
@@ -350,7 +354,12 @@ fun ZapReaction(
.show()
}
} else if (account.zapAmountChoices.size == 1) {
accountViewModel.zap(baseNote, account.zapAmountChoices.first() * 1000, "", context) {
accountViewModel.zap(
baseNote,
account.zapAmountChoices.first() * 1000,
"",
context
) {
scope.launch {
Toast
.makeText(context, it, Toast.LENGTH_SHORT)
@@ -405,8 +414,16 @@ fun ZapReaction(
}
}
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = zappedNote) {
withContext(Dispatchers.IO) {
zapAmount = zappedNote?.zappedAmount()
}
}
Text(
showAmount(zappedNote?.zappedAmount()),
showAmount(zapAmount),
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = textModifier
@@ -9,9 +9,13 @@ import androidx.compose.material.Divider
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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
import androidx.compose.ui.platform.LocalContext
@@ -31,6 +35,8 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.UnfollowButton
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@Composable
fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewModel, navController: NavController) {
@@ -88,15 +94,20 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
)
}
val amount =
(noteZap.event as? LnZapEvent)?.amount
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = noteZap) {
withContext(Dispatchers.IO) {
zapAmount = (noteZap.event as? LnZapEvent)?.amount
}
}
Column(
modifier = Modifier.padding(start = 10.dp),
verticalArrangement = Arrangement.Center
) {
Text(
"${showAmount(amount)} ${stringResource(R.string.sats)}",
"${showAmount(zapAmount)} ${stringResource(R.string.sats)}",
color = BitcoinOrange,
fontSize = 20.sp,
fontWeight = FontWeight.W500
@@ -34,6 +34,8 @@ import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@OptIn(ExperimentalFoundationApi::class)
@Composable
@@ -55,9 +57,11 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner
var isNew by remember { mutableStateOf<Boolean>(false) }
LaunchedEffect(key1 = zapSetCard) {
isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
withContext(Dispatchers.IO) {
isNew = zapSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context)
NotificationCache.markAsRead(routeForLastRead, zapSetCard.createdAt, context)
}
}
var backgroundColor = if (isNew) {
@@ -63,6 +63,14 @@ class BoostSetCard(val note: Note, val boostEvents: List<Note>) : Card() {
override fun id() = note.idHex + "B" + createdAt
}
class MessageSetCard(val note: Note) : Card() {
override fun createdAt(): Long {
return note.createdAt() ?: 0
}
override fun id() = note.idHex
}
sealed class CardFeedState {
object Loading : CardFeedState()
class Loaded(val feed: MutableState<List<Card>>) : CardFeedState()
@@ -21,6 +21,7 @@ import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import com.vitorpamplona.amethyst.ui.note.BadgeCompose
import com.vitorpamplona.amethyst.ui.note.BoostSetCompose
import com.vitorpamplona.amethyst.ui.note.LikeSetCompose
import com.vitorpamplona.amethyst.ui.note.MessageSetCompose
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.ZapSetCompose
@@ -134,6 +135,12 @@ private fun FeedLoaded(
navController = navController,
routeForLastRead = routeForLastRead
)
is MessageSetCard -> MessageSetCompose(
messageSetCard = item,
routeForLastRead = routeForLastRead,
accountViewModel = accountViewModel,
navController = navController
)
}
}
}
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.LnZapEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
import com.vitorpamplona.amethyst.service.model.ReactionEvent
import com.vitorpamplona.amethyst.service.model.RepostEvent
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
@@ -111,7 +112,9 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
}
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
if (it.event is BadgeAwardEvent) {
if (it.event is PrivateDmEvent) {
MessageSetCard(it)
} else if (it.event is BadgeAwardEvent) {
BadgeCard(it)
} else {
NoteCard(it)
@@ -63,7 +63,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
var handlerWaiting = AtomicBoolean()
private fun invalidateData() {
fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return
val scope = CoroutineScope(Job() + Dispatchers.Default)
@@ -76,7 +76,6 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>) : Vi
handlerWaiting.set(false)
}
}
handlerWaiting.set(false)
}
}
@@ -67,7 +67,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>) : ViewModel() {
var handlerWaiting = AtomicBoolean()
private fun invalidateData() {
fun invalidateData() {
if (handlerWaiting.getAndSet(true)) return
val scope = CoroutineScope(Job() + Dispatchers.Default)
@@ -62,7 +62,6 @@ import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.IdentityClaim
import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.DisplayNip05ProfileStatus
import com.vitorpamplona.amethyst.ui.components.InvoiceRequest
import com.vitorpamplona.amethyst.ui.components.ResizeImage
@@ -89,6 +88,8 @@ import com.vitorpamplona.amethyst.ui.screen.UserFeedView
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@OptIn(ExperimentalPagerApi::class)
@Composable
@@ -210,9 +211,20 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
},
{
val userState by baseUser.live().zaps.observeAsState()
val userZaps = userState?.user?.zappedAmount()
val userZaps = userState?.user
Text(text = "${showAmount(userZaps)} ${stringResource(id = R.string.zaps)}")
var zapAmount by remember { mutableStateOf<BigDecimal?>(null) }
LaunchedEffect(key1 = userState) {
withContext(Dispatchers.IO) {
val tempAmount = userZaps?.zappedAmount()
withContext(Dispatchers.Main) {
zapAmount = tempAmount
}
}
}
Text(text = "${showAmount(zapAmount)} ${stringResource(id = R.string.zaps)}")
},
{
val userState by baseUser.live().reports.observeAsState()
@@ -610,8 +622,8 @@ private fun DrawBanner(baseUser: User) {
var zoomImageDialogOpen by remember { mutableStateOf(false) }
if (!banner.isNullOrBlank()) {
AsyncImageProxy(
model = ResizeImage(banner, 125.dp),
AsyncImage(
model = banner,
contentDescription = stringResource(id = R.string.profile_image),
contentScale = ContentScale.FillWidth,
modifier = Modifier
@@ -682,11 +694,13 @@ fun TabNotesConversations(user: User, accountViewModel: AccountViewModel, navCon
}
@Composable
fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: NavController) {
fun TabFollows(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowsUserFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
val userState by baseUser.live().follows.observeAsState()
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
@@ -699,11 +713,13 @@ fun TabFollows(user: User, accountViewModel: AccountViewModel, navController: Na
}
@Composable
fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController: NavController) {
fun TabFollowers(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileFollowersUserFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
val userState by baseUser.live().follows.observeAsState()
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
@@ -716,41 +732,39 @@ fun TabFollowers(user: User, accountViewModel: AccountViewModel, navController:
}
@Composable
fun TabReceivedZaps(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
if (accountState != null) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel()
fun TabReceivedZaps(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileZapsFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
val userState by baseUser.live().zaps.observeAsState()
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
LnZapFeedView(feedViewModel, accountViewModel, navController)
}
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
LnZapFeedView(feedViewModel, accountViewModel, navController)
}
}
}
@Composable
fun TabReports(user: User, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState()
if (accountState != null) {
val feedViewModel: NostrUserProfileReportFeedViewModel = viewModel()
fun TabReports(baseUser: User, accountViewModel: AccountViewModel, navController: NavController) {
val feedViewModel: NostrUserProfileReportFeedViewModel = viewModel()
LaunchedEffect(Unit) {
feedViewModel.refresh()
}
val userState by baseUser.live().reports.observeAsState()
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel, navController, null)
}
LaunchedEffect(userState) {
feedViewModel.invalidateData()
}
Column(Modifier.fillMaxHeight()) {
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
FeedView(feedViewModel, accountViewModel, navController, null)
}
}
}
@@ -153,11 +153,16 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
onCheckedChange = { acceptedTerms.value = it }
)
val regularText =
SpanStyle(color = MaterialTheme.colors.onBackground)
val clickableTextStyle =
SpanStyle(color = MaterialTheme.colors.primary)
val annotatedTermsString = buildAnnotatedString {
append(stringResource(R.string.i_accept_the))
withStyle(regularText) {
append(stringResource(R.string.i_accept_the))
}
withStyle(clickableTextStyle) {
pushStringAnnotation("openTerms", "")
+211
View File
@@ -0,0 +1,211 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name_release" translatable="false">Amethyst</string>
<string name="app_name_debug" translatable="false">Amethyst debug</string>
<string name="point_to_the_qr_code">Richt naar de QR-Code</string>
<string name="show_qr">QR tonen</string>
<string name="profile_image">Profielafbeelding</string>
<string name="scan_qr">Scan QR</string>
<string name="show_anyway">Laat evengoed zien</string>
<string name="post_was_flagged_as_inappropriate_by">Bericht was gemarkeerd als ongepast door</string>
<string name="post_not_found">Bericht niet gevonden</string>
<string name="channel_image">Kanaalafbeelding</string>
<string name="referenced_event_not_found">Verwezen event niet gevonden</string>
<string name="could_not_decrypt_the_message">Kon het bericht niet ontcijferen</string>
<string name="group_picture">Groepafbeelding</string>
<string name="explicit_content">Expliciete inhoud</string>
<string name="spam">Spam</string>
<string name="impersonation">Imitatie</string>
<string name="illegal_behavior">Illegaal gedrag</string>
<string name="unknown">Onbekend</string>
<string name="relay_icon">Relay icoon</string>
<string name="unknown_author">Onbekende auteur</string>
<string name="copy_text">Kopieer tekst</string>
<string name="copy_user_pubkey">Kopieer auteur ID</string>
<string name="copy_note_id">Kopieer note ID</string>
<string name="broadcast">Verzenden</string>
<string name="block_hide_user"><![CDATA[Block & verberg gebruiker]]></string>
<string name="report_spam_scam">Meld spam / scam</string>
<string name="report_impersonation">Meld imitatie</string>
<string name="report_explicit_content">Rapporteer expliciete inhoud</string>
<string name="report_illegal_behaviour">Rapporteer illegaal gedrag</string>
<string name="login_with_a_private_key_to_be_able_to_reply">Login met een privésleutel om te reageren</string>
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Login met een privésleutel om berichten te boosten</string>
<string name="login_with_a_private_key_to_like_posts">Login met een privésleutel om berichten te liken</string>
<string name="no_zap_amount_setup_long_press_to_change">Geen Zap bedrag. Houdt ingedrukt om te veranderen</string>
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Login met een privésleutel om Zaps te versturen</string>
<string name="zaps">Zaps</string>
<string name="view_count">Bekijk telling</string>
<string name="boost">Boost</string>
<string name="boosted">boosted</string>
<string name="quote">Quote</string>
<string name="new_amount_in_sats">Nieuw bedrag in sats</string>
<string name="add">Toevoegen</string>
<string name="replying_to">"reageren op "</string>
<string name="and">" en "</string>
<string name="in_channel">"in kanaal "</string>
<string name="profile_banner">Profielbanner</string>
<string name="following">" Volgend"</string>
<string name="followers">" Volgers"</string>
<string name="profile">Profiel</string>
<string name="security_filters">Beveiligingsfilters</string>
<string name="log_out">Uitloggen</string>
<string name="show_more">Meer</string>
<string name="lightning_invoice">Lightning invoice</string>
<string name="pay">Betalen</string>
<string name="lightning_tips">Lightning Tips</string>
<string name="note_to_receiver">Notitie aan ontvanger</string>
<string name="thank_you_so_much">Hartelijk bedankt!</string>
<string name="amount_in_sats">Bedrag in sats</string>
<string name="send_sats">Verstuur sats</string>
<string name="never_translate_from">"Nooit vertalen van "</string>
<string name="error_parsing_preview_for">"Foutieve parsing preview voor %1$s : %2$s"</string>
<string name="preview_card_image_for">"Voorbeeld kaartafbeelding voor %1$s"</string>
<string name="new_channel">Nieuw kanaal</string>
<string name="channel_name">Kanaalnaam</string>
<string name="my_awesome_group">Mijn geweldige groep</string>
<string name="picture_url">Afbeelding URL</string>
<string name="description">Omschrijving</string>
<string name="about_us">"Over ons.. "</string>
<string name="what_s_on_your_mind">Waar denk je aan?</string>
<string name="post">Verzenden</string>
<string name="save">Opslaan</string>
<string name="create">Maken</string>
<string name="cancel">Annuleren</string>
<string name="failed_to_upload_the_image">Het uploaden van de afbeelding is mislukt</string>
<string name="relay_address">Relay addres</string>
<string name="posts">Berichten</string>
<string name="errors">Errors</string>
<string name="home_feed">Beginfeed</string>
<string name="private_message_feed">Privéberichten</string>
<string name="public_chat_feed">Publieke chats</string>
<string name="global_feed">Globale feed</string>
<string name="search_feed">Zoeken</string>
<string name="add_a_relay">AVoeg een relay toe</string>
<string name="display_name">Naam</string>
<string name="my_display_name">Mijn naam</string>
<string name="username">Gebruikersnaam</string>
<string name="my_username">Mijn gebruikersnaam</string>
<string name="about_me">Over mij</string>
<string name="avatar_url">Avatar URL</string>
<string name="banner_url">Banner URL</string>
<string name="website_url">Website URL</string>
<string name="ln_address">LN Address</string>
<string name="ln_url_outdated">LN URL (verouderd)</string>
<string name="image_saved_to_the_gallery">Afbeelding opgeslagen in de galerij</string>
<string name="failed_to_save_the_image">De afbeelding is niet opgeslagen</string>
<string name="upload_image">Afbeelding uploaden</string>
<string name="uploading">Uploaden…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">De gebruiker heeft geen Lightning Adress ingesteld om sats te ontvangen</string>
<string name="reply_here">"hier reageren.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Kopieert de Notitie ID naar klembord om te delen</string>
<string name="copy_channel_id_note_to_the_clipboard">Kopieer kanaal ID (Notitie) naar klembord</string>
<string name="edits_the_channel_metadata">Past de kanaal-metadata aan</string>
<string name="join">Lid worden</string>
<string name="known">Bekend</string>
<string name="new_requests">Nieuw verzoek</string>
<string name="blocked_users">Geblokeerde gebruikers</string>
<string name="new_threads">Nieuwe draadjes</string>
<string name="conversations">Gesprekken</string>
<string name="notes">Notities</string>
<string name="replies">Reacties</string>
<string name="follows">"Volgend"</string>
<string name="reports">"Rapporten"</string>
<string name="more_options">Meer opties</string>
<string name="relays">" Relays"</string>
<string name="website">Website</string>
<string name="lightning_address">Lightning Address</string>
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Kopieert het NSec ID (uw wachtwoord) naar het klembord voor back-up.</string>
<string name="copy_private_key_to_the_clipboard">Privésleutel kopiëren naar het klembord</string>
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Kopieert de publieke sleutel naar het klembord om te delen</string>
<string name="copy_public_key_npub_to_the_clipboard">Kopieer publieke sleutel (NPub) naar het klembord</string>
<string name="send_a_direct_message">Stuur een privébericht</string>
<string name="edits_the_user_s_metadata">Bewerkt de metagegevens van de gebruiker</string>
<string name="follow">Volgen</string>
<string name="unblock">Deblokkeren</string>
<string name="copy_user_id">Kopieer gebruikers ID</string>
<string name="unblock_user">Deblokkeer gebruiker</string>
<string name="npub_hex_username">"npub, hex, gebruikersnaam "</string>
<string name="clear">Wissen</string>
<string name="app_logo">App logo</string>
<string name="nsec_npub_hex_private_key">nsec / npub / hex privésleutel</string>
<string name="show_password">Toon wachtwoord</string>
<string name="hide_password">Verberg wachtwoord</string>
<string name="invalid_key">Ongeldige sleutel</string>
<string name="i_accept_the">"Ik accepteer de "</string>
<string name="terms_of_use">gebruiksvoorwaarden</string>
<string name="acceptance_of_terms_is_required">Accepteren van de voorwaarden is vereist</string>
<string name="key_is_required">Sleutel is vereist</string>
<string name="login">Inloggen</string>
<string name="generate_a_new_key">Genereer een nieuwe sleutel</string>
<string name="loading_feed">Feed laden</string>
<string name="error_loading_replies">"Foutmelding bij het laden reacties: "</string>
<string name="try_again">Opnieuw proberen</string>
<string name="feed_is_empty">Feed is leeg.</string>
<string name="refresh">Verversen</string>
<string name="created">gemaakt</string>
<string name="with_description_of">met beschrijving van</string>
<string name="and_picture">en afbeelding</string>
<string name="changed_chat_name_to">heeft chatnaam veranderd naar</string>
<string name="description_to">omschrijving naar</string>
<string name="and_picture_to">en afbeelding naar</string>
<string name="leave">Verlaten</string>
<string name="unfollow">Ontvolgen</string>
<string name="channel_created">Kanaal gemaakt</string>
<string name="channel_information_changed_to">"Kanaalinformatie veranderd naar"</string>
<string name="public_chat">Publieke chat</string>
<string name="posts_received">berichten ontvangen</string>
<string name="remove">Verwijderen</string>
<string name="sats" translatable="false">sats</string>
<string name="auto">Auto</string>
<string name="translated_from">vertaald van</string>
<string name="to">naar</string>
<string name="show_in">Laten zien in</string>
<string name="first">eerste</string>
<string name="always_translate_to">Altijd vertalen naar</string>
<string name="nip_05" translatable="false">NIP-05</string>
<string name="lnurl" translatable="false">LNURL...</string>
<string name="never">nooit</string>
<string name="now">nu</string>
<string name="h">h</string>
<string name="m">m</string>
<string name="d">d</string>
<string name="nudity">Naaktheid</string>
<string name="profanity_hateful_speech">Godslastering / haatdragende taal</string>
<string name="report_hateful_speech">Haatdragende taal rapporteren</string>
<string name="report_nudity_porn">Naaktheid / porno rapporteren</string>
<string name="others">anderen</string>
<string name="mark_all_known_as_read">Alle bekende als gelezen markeren</string>
<string name="mark_all_new_as_read">Alle nieuwe als gelezen markeren</string>
<string name="mark_all_as_read">Alles als gelezen markeren</string>
<string name="backup_keys">Backup sleutels</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## Sleutel back-up en veiligheidstips
\n\nUw account is beveiligd met een privésleutel. De sleutel is een lange, willekeurige reeks die begint met **nsec1**. Iedereen die toegang heeft tot uw privésleutel kan inhoud publiceren met uw identiteit.
\n\n- Voer uw privésleutel **nooit** in een website of software die u niet vertrouwt.
\n- Amethyst ontwikkelaars zullen u **nooit** om uw privésleutel vragen.
\n- **Bewaar** een veilige back-up van uw privésleutel voor accountherstel. Wij raden u aan een wachtwoordmanager te gebruiken.
</string>
<string name="secret_key_copied_to_clipboard">Privésleutel (nsec) gekopieerd naar klembord</string>
<string name="copy_my_secret_key">Kopieer mijn privésleutel</string>
<string name="biometric_authentication_failed">Authenticatie mislukt</string>
<string name="biometric_error">Error</string>
<string name="badge_created_by">"Gemaakt door %1$s"</string>
<string name="badge_award_image_for">"Badge award afbeelding voor %1$s"</string>
<string name="new_badge_award_notif">Je hebt een nieuwe Badge award ontvangen</string>
<string name="award_granted_to">Badge award gegeven aan</string>
<string name="copied_note_text_to_clipboard">Notitie tekst gekopieerd naar klembord</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">@npub auteur gekopieerd naar klembord</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Notitie ID (@note1) gekopieerd naar klembord</string>
<string name="select_text_dialog_top">Selecteer tekst</string>
<string name="quick_action_select">Selecteer</string>
<string name="quick_action_share_browser_link">Deel browserlink</string>
<string name="quick_action_share">Delen</string>
<string name="quick_action_copy_user_id">Auteur ID</string>
<string name="quick_action_copy_note_id">Notitie ID</string>
<string name="quick_action_copy_text">Kopieer tekst</string>
<string name="quick_action_delete">Verwijderen</string>
<string name="quick_action_unfollow">Ontvolgen</string>
<string name="quick_action_follow">Volgen</string>
<string name="quick_action_request_deletion_alert_title">Verzoek om te verwijderen</string>
<string name="quick_action_request_deletion_alert_body">Amethyst zal vragen om uw notitie te verwijderen van de relays waarmee u momenteel verbonden bent. Er is geen garantie dat uw notitie permanent wordt verwijderd van deze relays, of van andere relays waar het kan worden opgeslagen.</string>
</resources>
@@ -170,4 +170,41 @@
<string name="report_hateful_speech">Denunciar discurso de ódio</string>
<string name="report_nudity_porn">Denunciar nudez / pornografia</string>
<string name="others">outros</string>
<string name="mark_all_known_as_read">Marcar todos conhecidos como lidos</string>
<string name="mark_all_new_as_read">Marcar todas novas solicitações como lidas</string>
<string name="mark_all_as_read">Marcar todas como lidas</string>
<string name="biometric_error">Erro</string>
<string name="account_backup_tips_md">
## Backup de chaves e dicas de segurança
\n\nSua conta é protegida por uma chave secreta. A chave é uma string aleatória longa começando com **nsec1**. Qualquer pessoa que tenha acesso à sua chave secreta pode publicar conteúdo usando sua identidade.
\n\n- **Não** coloque sua chave secreta em qualquer site ou software em que não confie.
\n- Os desenvolvedores do Amethyst **nunca** pedirão sua chave secreta.
\n- **Faça** um backup seguro de sua chave secreta para recuperação de conta. Recomendamos o uso de um gerenciador de senhas.
</string>
<string name="secret_key_copied_to_clipboard">Chave secreta (nsec) copiada</string>
<string name="copy_my_secret_key">Copiar minha chave secreta</string>
<string name="biometric_authentication_failed">Autenticação falhou</string>
<string name="badge_created_by">Criado por %1$s</string>
<string name="badge_award_image_for">Imagem da medalha para %1$s</string>
<string name="new_badge_award_notif">Você recebeu uma nova medalha</string>
<string name="award_granted_to">Medalha concedida para</string>
<string name="copied_note_text_to_clipboard">Texto copiado</string>
<string name="copied_user_id_to_clipboard">Copiado @npub do autor</string>
<string name="copied_note_id_to_clipboard">Copiado ID da nota (@note1)</string>
<string name="select_text_dialog_top">Selecionar texto</string>
<string name="quick_action_select">Selecionar</string>
<string name="quick_action_share_browser_link">Compartilhar link do navegador</string>
<string name="quick_action_share">Compartilhar</string>
<string name="quick_action_copy_user_id">ID do Autor</string>
<string name="quick_action_copy_note_id">ID da Nota</string>
<string name="quick_action_copy_text">Copiar texto</string>
<string name="quick_action_delete">Excluir</string>
<string name="quick_action_unfollow">Remover seguidor</string>
<string name="quick_action_follow">Seguir</string>
<string name="quick_action_request_deletion_alert_title">Pedir para excluir</string>
<string name="quick_action_request_deletion_alert_body">Amethyst solicitará que sua nota seja excluída dos relays aos quais você está conectado no momento. Não há garantia de que sua nota será excluída permanentemente desses relays ou de outros relays onde possa estar armazenada.</string>
<string name="backup_keys">Copia de segurança das chaves</string>
<string name="private_conversation_notification">&lt;Não foi possível descriptografar a mensagem privada&gt;\\n\\nVocê foi citado em uma conversa privada/criptografada entre %1$s and %2$s.</string>
<string name="quick_action_delete_button">Excluir</string>
<string name="quick_action_dont_show_again_button">Não mostar novamente</string>
</resources>
+4 -8
View File
@@ -178,13 +178,7 @@
<string name="mark_all_new_as_read">Mark all New as read</string>
<string name="mark_all_as_read">Mark all as read</string>
<string name="backup_keys">Backup Keys</string>
<string name="account_backup_tips_md" tools:ignore="Typos">
## Key Backup and Safety Tips
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
\n\n- Do **not** put your secret key in any website or software you do not trust.
\n- Amethyst developers will **never** ask you for your secret key.
\n- **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager.
</string>
<string name="account_backup_tips_md" tools:ignore="Typos">" ## Key Backup and Safety Tips Your account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity. - Do **not** put your secret key in any website or software you do not trust. - Amethyst developers will **never** ask you for your secret key. - **Do** keep a secure backup of your secret key for account recovery. We recommend using a password manager. "</string>
<string name="secret_key_copied_to_clipboard">Secret key (nsec) copied to clipboard</string>
<string name="copy_my_secret_key">Copy my secret key</string>
<string name="biometric_authentication_failed">Authentication failed</string>
@@ -219,6 +213,8 @@
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;Unable to decrypt private message&gt;\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s."</string>
<string name="quick_action_delete_button">Delete</string>
<string name="quick_action_dont_show_again_button">Don\'t show again</string>
</resources>