diff --git a/README.md b/README.md index 522bb99392..ff038543e9 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 878ecefcdf..0477fed22a 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 9ab8a86842..d43669d7c1 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 abec38a633..83e0f331a4 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 fbed902c26..cbe20430e9 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 b9df1a5841..3cf1eba19b 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 b8845b8d54..e30336284c 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 9ea473a801..cdb3aab35c 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 b83f061557..f59437670a 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 381e4a8dda..09cb7c69d9 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 7cbf6a4327..9901856a79 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 718bb233e2..053a2e253e 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 909d5d92d0..c38037e9b3 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 437755f994..a4a619c1cb 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 e25a270cf5..722a74220d 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 0000000000..743b06e737 --- /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 bc6a7db787..f1fd8e8fa7 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 87f675c08b..eb0f197341 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 d03997ab63..1b0aaf2451 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 b955120a74..a302ba1c6b 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 f4497bdc57..e26caff522 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 6785d35992..98b0c1e095 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 950430cc19..bdacb26395 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 7bd61ec1d4..df4fa0ebfa 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 b7f9b17702..3060f307aa 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 1b92b2ec08..c76a25966c 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 8748a1f11c..bab4039cdf 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 02ea533aee..82c7391fea 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 c505c5738a..ffa3c8ce43 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 0000000000..d61578c791 --- /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 1333bb192a..019bdbca8f 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 f3c2a9d955..baff7e1adf 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