diff --git a/README.md b/README.md index 522bb9939..ff038543e 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/app/build.gradle b/app/build.gradle index 878ecefcd..0477fed22 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt index 9ab8a8684..d43669d7c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/EncryptedStorage.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index abec38a63..83e0f331a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -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() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index fbed902c2..cbe20430e 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -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}") } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt index b9df1a584..3cf1eba19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Note.kt @@ -368,7 +368,7 @@ class NoteLiveData(val note: Note) : LiveData(NoteState(note)) { } } - fun refresh() { + private fun refresh() { postValue(NoteState(note)) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt index b8845b8d5..e30336284 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/LnZapEvent.kt @@ -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 { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt index 9ea473a80..cdb3aab35 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/MainActivity.kt @@ -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() { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt index b83f06155..f59437670 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppBottomBar.kt @@ -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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt index 381e4a8dd..09cb7c69d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/DrawerContent.kt @@ -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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt index 7cbf6a432..9901856a7 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BadgeCompose.kt @@ -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(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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt index 718bb233e..053a2e253 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/BoostSetCompose.kt @@ -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(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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt index 909d5d92d..c38037e9b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomCompose.kt @@ -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(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(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 ) }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 437755f99..a4a619c1c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -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 + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt index e25a270cf..722a74220 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/LikeSetCompose.kt @@ -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(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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt new file mode 100644 index 000000000..743b06e73 --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MessageSetCompose.kt @@ -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(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) + } + } + } + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt index bc6a7db78..f1fd8e8fa 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/MultiSetCompose.kt @@ -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(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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 87f675c08..eb0f19734 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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(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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index d03997ab6..1b0aaf245 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -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)) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index b955120a7..a302ba1c6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -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(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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt index f4497bdc5..e26caff52 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapNoteCompose.kt @@ -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, accountViewModel: AccountViewModel, navController: NavController) { @@ -88,15 +94,20 @@ fun ZapNoteCompose(baseNote: Pair, accountViewModel: AccountViewMode ) } - val amount = - (noteZap.event as? LnZapEvent)?.amount + var zapAmount by remember { mutableStateOf(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 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt index 6785d3599..98b0c1e09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ZapSetCompose.kt @@ -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(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) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt index 950430cc1..bdacb2639 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedState.kt @@ -63,6 +63,14 @@ class BoostSetCard(val note: Note, val boostEvents: List) : 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>) : CardFeedState() diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt index 7bd61ec1d..df4fa0ebf 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedView.kt @@ -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 + ) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt index b7f9b1770..3060f307a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/CardFeedViewModel.kt @@ -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) : 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) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt index 1b92b2ec0..c76a25966 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/LnZapFeedViewModel.kt @@ -63,7 +63,7 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter>) : 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>) : Vi handlerWaiting.set(false) } } - handlerWaiting.set(false) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt index 8748a1f11..bab4039cd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/UserFeedViewModel.kt @@ -67,7 +67,7 @@ open class UserFeedViewModel(val dataSource: FeedFilter) : ViewModel() { var handlerWaiting = AtomicBoolean() - private fun invalidateData() { + fun invalidateData() { if (handlerWaiting.getAndSet(true)) return val scope = CoroutineScope(Job() + Dispatchers.Default) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 02ea533ae..82c7391fe 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -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(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) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt index c505c5738..ffa3c8ce4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedOff/LoginScreen.kt @@ -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", "") diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..d61578c79 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,211 @@ + + Amethyst + Amethyst debug + Richt naar de QR-Code + QR tonen + Profielafbeelding + Scan QR + Laat evengoed zien + Bericht was gemarkeerd als ongepast door + Bericht niet gevonden + Kanaalafbeelding + Verwezen event niet gevonden + Kon het bericht niet ontcijferen + Groepafbeelding + Expliciete inhoud + Spam + Imitatie + Illegaal gedrag + Onbekend + Relay icoon + Onbekende auteur + Kopieer tekst + Kopieer auteur ID + Kopieer note ID + Verzenden + + Meld spam / scam + Meld imitatie + Rapporteer expliciete inhoud + Rapporteer illegaal gedrag + Login met een privésleutel om te reageren + Login met een privésleutel om berichten te boosten + Login met een privésleutel om berichten te liken + Geen Zap bedrag. Houdt ingedrukt om te veranderen + Login met een privésleutel om Zaps te versturen + Zaps + Bekijk telling + Boost + boosted + Quote + Nieuw bedrag in sats + Toevoegen + "reageren op " + " en " + "in kanaal " + Profielbanner + " Volgend" + " Volgers" + Profiel + Beveiligingsfilters + Uitloggen + Meer + Lightning invoice + Betalen + Lightning Tips + Notitie aan ontvanger + Hartelijk bedankt! + Bedrag in sats + Verstuur sats + "Nooit vertalen van " + "Foutieve parsing preview voor %1$s : %2$s" + "Voorbeeld kaartafbeelding voor %1$s" + Nieuw kanaal + Kanaalnaam + Mijn geweldige groep + Afbeelding URL + Omschrijving + "Over ons.. " + Waar denk je aan? + Verzenden + Opslaan + Maken + Annuleren + Het uploaden van de afbeelding is mislukt + Relay addres + Berichten + Errors + Beginfeed + Privéberichten + Publieke chats + Globale feed + Zoeken + AVoeg een relay toe + Naam + Mijn naam + Gebruikersnaam + Mijn gebruikersnaam + Over mij + Avatar URL + Banner URL + Website URL + LN Address + LN URL (verouderd) + Afbeelding opgeslagen in de galerij + De afbeelding is niet opgeslagen + Afbeelding uploaden + Uploaden… + De gebruiker heeft geen Lightning Adress ingesteld om sats te ontvangen + "hier reageren.. " + Kopieert de Notitie ID naar klembord om te delen + Kopieer kanaal ID (Notitie) naar klembord + Past de kanaal-metadata aan + Lid worden + Bekend + Nieuw verzoek + Geblokeerde gebruikers + Nieuwe draadjes + Gesprekken + Notities + Reacties + "Volgend" + "Rapporten" + Meer opties + " Relays" + Website + Lightning Address + Kopieert het NSec ID (uw wachtwoord) naar het klembord voor back-up. + Privésleutel kopiëren naar het klembord + Kopieert de publieke sleutel naar het klembord om te delen + Kopieer publieke sleutel (NPub) naar het klembord + Stuur een privébericht + Bewerkt de metagegevens van de gebruiker + Volgen + Deblokkeren + Kopieer gebruikers ID + Deblokkeer gebruiker + "npub, hex, gebruikersnaam " + Wissen + App logo + nsec / npub / hex privésleutel + Toon wachtwoord + Verberg wachtwoord + Ongeldige sleutel + "Ik accepteer de " + gebruiksvoorwaarden + Accepteren van de voorwaarden is vereist + Sleutel is vereist + Inloggen + Genereer een nieuwe sleutel + Feed laden + "Foutmelding bij het laden reacties: " + Opnieuw proberen + Feed is leeg. + Verversen + gemaakt + met beschrijving van + en afbeelding + heeft chatnaam veranderd naar + omschrijving naar + en afbeelding naar + Verlaten + Ontvolgen + Kanaal gemaakt + "Kanaalinformatie veranderd naar" + Publieke chat + berichten ontvangen + Verwijderen + sats + Auto + vertaald van + naar + Laten zien in + eerste + Altijd vertalen naar + NIP-05 + LNURL... + nooit + nu + h + m + d + Naaktheid + Godslastering / haatdragende taal + Haatdragende taal rapporteren + Naaktheid / porno rapporteren + anderen + Alle bekende als gelezen markeren + Alle nieuwe als gelezen markeren + Alles als gelezen markeren + Backup sleutels + + ## 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. + + Privésleutel (nsec) gekopieerd naar klembord + Kopieer mijn privésleutel + Authenticatie mislukt + Error + "Gemaakt door %1$s" + "Badge award afbeelding voor %1$s" + Je hebt een nieuwe Badge award ontvangen + Badge award gegeven aan + Notitie tekst gekopieerd naar klembord + @npub auteur gekopieerd naar klembord + Notitie ID (@note1) gekopieerd naar klembord + Selecteer tekst + Selecteer + Deel browserlink + Delen + Auteur ID + Notitie ID + Kopieer tekst + Verwijderen + Ontvolgen + Volgen + Verzoek om te verwijderen + 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. + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 1333bb192..019bdbca8 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -170,4 +170,41 @@ Denunciar discurso de ódio Denunciar nudez / pornografia outros + Marcar todos conhecidos como lidos + Marcar todas novas solicitações como lidas + Marcar todas como lidas + Erro + + ## 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. + + Chave secreta (nsec) copiada + Copiar minha chave secreta + Autenticação falhou + Criado por %1$s + Imagem da medalha para %1$s + Você recebeu uma nova medalha + Medalha concedida para + Texto copiado + Copiado @npub do autor + Copiado ID da nota (@note1) + Selecionar texto + Selecionar + Compartilhar link do navegador + Compartilhar + ID do Autor + ID da Nota + Copiar texto + Excluir + Remover seguidor + Seguir + Pedir para excluir + 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. + Copia de segurança das chaves + <Não foi possível descriptografar a mensagem privada>\\n\\nVocê foi citado em uma conversa privada/criptografada entre %1$s and %2$s. + Excluir + Não mostar novamente diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f3c2a9d95..baff7e1ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -178,13 +178,7 @@ Mark all New as read Mark all as read Backup Keys - - ## 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. - + " ## 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. " Secret key (nsec) copied to clipboard Copy my secret key Authentication failed @@ -219,6 +213,8 @@ https://<server>/<user>/<proof post> https://twitter.com/<user>/status/<proof post> - + "<Unable to decrypt private message>\n\nYou were cited in a private/encrypted conversation between %1$s and %2$s." + Delete + Don\'t show again