kopia lustrzana https://github.com/vitorpamplona/amethyst
Merge branch 'main' into less_memory_test_branch
# Conflicts: # app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt # app/src/main/java/com/vitorpamplona/amethyst/service/model/LongTextNoteEvent.kt # app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewPostViewModel.ktpull/233/head
commit
b93f28f09d
|
@ -25,6 +25,7 @@ class LocalPreferences(context: Context) {
|
|||
const val TRANSLATE_TO = "translateTo"
|
||||
const val ZAP_AMOUNTS = "zapAmounts"
|
||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||
const val HIDE_DELETE_REQUEST_INFO = "hideDeleteRequestInfo"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
|
@ -49,6 +50,7 @@ class LocalPreferences(context: Context) {
|
|||
account.translateTo.let { putString(PrefKeys.TRANSLATE_TO, it) }
|
||||
account.zapAmountChoices.let { putString(PrefKeys.ZAP_AMOUNTS, gson.toJson(it)) }
|
||||
account.backupContactList.let { putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(it)) }
|
||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, account.hideDeleteRequestInfo)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
|
@ -89,6 +91,8 @@ class LocalPreferences(context: Context) {
|
|||
mapOf<String, String>()
|
||||
}
|
||||
|
||||
val hideDeleteRequestInfo = getBoolean(PrefKeys.HIDE_DELETE_REQUEST_INFO, false)
|
||||
|
||||
if (pubKey != null) {
|
||||
return Account(
|
||||
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
|
||||
|
@ -99,6 +103,7 @@ class LocalPreferences(context: Context) {
|
|||
languagePreferences,
|
||||
translateTo,
|
||||
zapAmountChoices,
|
||||
hideDeleteRequestInfo,
|
||||
latestContactList
|
||||
)
|
||||
} else {
|
||||
|
|
|
@ -56,6 +56,7 @@ class Account(
|
|||
var languagePreferences: Map<String, String> = mapOf(),
|
||||
var translateTo: String = Locale.getDefault().language,
|
||||
var zapAmountChoices: List<Long> = listOf(500L, 1000L, 5000L),
|
||||
var hideDeleteRequestInfo: Boolean = false,
|
||||
var backupContactList: ContactListEvent? = null
|
||||
) {
|
||||
var transientHiddenUsers: Set<String> = setOf()
|
||||
|
@ -549,6 +550,11 @@ class Account(
|
|||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun setHideDeleteRequestInfo() {
|
||||
hideDeleteRequestInfo = true
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
init {
|
||||
backupContactList?.let {
|
||||
println("Loading saved contacts ${it.toJson()}")
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import fr.acinq.secp256k1.Hex
|
||||
import nostr.postr.Bech32
|
||||
import nostr.postr.Persona
|
||||
import nostr.postr.bechToBytes
|
||||
import nostr.postr.toHex
|
||||
import nostr.postr.toNpub
|
||||
|
||||
/** Makes the distinction between String and Hex **/
|
||||
typealias HexKey = String
|
||||
|
@ -48,7 +50,7 @@ fun decodePublicKey(key: String): ByteArray {
|
|||
}
|
||||
}
|
||||
|
||||
data class DirtyKeyInfo(val type: String, val keyHex: String, val restOfWord: String)
|
||||
data class DirtyKeyInfo(val key: Nip19.Return, val restOfWord: String)
|
||||
|
||||
fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
||||
var key = mightBeAKey
|
||||
|
@ -67,11 +69,30 @@ fun parseDirtyWordForKey(mightBeAKey: String): DirtyKeyInfo? {
|
|||
val restOfWord = key.substring(63)
|
||||
|
||||
if (key.startsWith("nsec1", true)) {
|
||||
return DirtyKeyInfo("npub", Persona(privKey = keyB32.bechToBytes()).pubKey.toHexKey(), restOfWord)
|
||||
// Converts to npub
|
||||
val pubkey = Nip19.uriToRoute(Persona(privKey = keyB32.bechToBytes()).pubKey.toNpub()) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkey, restOfWord)
|
||||
} else if (key.startsWith("npub1", true)) {
|
||||
return DirtyKeyInfo("npub", keyB32.bechToBytes().toHexKey(), restOfWord)
|
||||
val pubkey = Nip19.uriToRoute(keyB32) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkey, restOfWord)
|
||||
} else if (key.startsWith("note1", true)) {
|
||||
return DirtyKeyInfo("note", keyB32.bechToBytes().toHexKey(), restOfWord)
|
||||
val noteId = Nip19.uriToRoute(keyB32) ?: return null
|
||||
|
||||
return DirtyKeyInfo(noteId, restOfWord)
|
||||
} else if (key.startsWith("nprofile", true)) {
|
||||
val pubkeyRelay = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||
|
||||
return DirtyKeyInfo(pubkeyRelay, restOfWord)
|
||||
} else if (key.startsWith("nevent", true)) {
|
||||
val noteRelayId = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||
|
||||
return DirtyKeyInfo(noteRelayId, restOfWord)
|
||||
} else if (key.startsWith("naddr1", true)) {
|
||||
val address = Nip19.uriToRoute(keyB32 + restOfWord) ?: return null
|
||||
|
||||
return DirtyKeyInfo(address, "") // no way to know when they address ends and dirt begins
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
|
|
@ -186,8 +186,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo = event.replyToWithoutCitations().mapNotNull { checkGetOrCreateNote(it) } +
|
||||
event.taggedAddresses().mapNotNull { getOrCreateAddressableNote(it) }
|
||||
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
||||
|
@ -223,7 +222,7 @@ object LocalCache {
|
|||
return
|
||||
}
|
||||
|
||||
val replyTo = event.replyToWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
val replyTo = event.tagsWithoutCitations().mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, replyTo)
|
||||
|
|
|
@ -152,4 +152,22 @@ object LnInvoiceUtil {
|
|||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the string contains an LN invoice, returns a Pair of the start and end
|
||||
* positions of the invoice in the string. Otherwise, returns (0, 0). This is
|
||||
* used to ensure we don't accidentally cut an invoice in the middle when taking
|
||||
* only a portion of the available text.
|
||||
*/
|
||||
fun locateInvoice(input: String?): Pair<Int, Int> {
|
||||
if (input == null) {
|
||||
return Pair(0, 0)
|
||||
}
|
||||
val matcher = invoicePattern.matcher(input)
|
||||
return if (matcher.find()) {
|
||||
Pair(matcher.start(), matcher.end())
|
||||
} else {
|
||||
Pair(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,13 @@ open class BaseTextNoteEvent(
|
|||
fun mentions() = taggedUsers()
|
||||
fun replyTos() = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
||||
val aTagValue = it.getOrNull(1)
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||
}
|
||||
|
||||
fun findCitations(): Set<String> {
|
||||
var citations = mutableSetOf<String>()
|
||||
// Removes citations from replies:
|
||||
|
@ -25,20 +32,24 @@ open class BaseTextNoteEvent(
|
|||
if (tag != null && tag[0] == "e") {
|
||||
citations.add(tag[1])
|
||||
}
|
||||
if (tag != null && tag[0] == "a") {
|
||||
citations.add(tag[1])
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
return citations
|
||||
}
|
||||
|
||||
fun replyToWithoutCitations(): List<String> {
|
||||
fun tagsWithoutCitations(): List<String> {
|
||||
val repliesTo = replyTos()
|
||||
if (repliesTo.isEmpty()) return repliesTo
|
||||
val tagAddresses = taggedAddresses().map { it.toTag() }
|
||||
if (repliesTo.isEmpty() && tagAddresses.isEmpty()) return emptyList()
|
||||
|
||||
val citations = findCitations()
|
||||
|
||||
return if (citations.isEmpty()) {
|
||||
repliesTo
|
||||
repliesTo + tagAddresses
|
||||
} else {
|
||||
repliesTo.filter { it !in citations }
|
||||
}
|
||||
|
|
|
@ -14,13 +14,6 @@ class TextNoteEvent(
|
|||
sig: HexKey
|
||||
) : BaseTextNoteEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
fun taggedAddresses() = tags.filter { it.firstOrNull() == "a" }.mapNotNull {
|
||||
val aTagValue = it.getOrNull(1)
|
||||
val relay = it.getOrNull(2)
|
||||
|
||||
if (aTagValue != null) ATag.parse(aTagValue, relay) else null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 1
|
||||
|
||||
|
|
|
@ -38,7 +38,6 @@ import com.vitorpamplona.amethyst.model.Account
|
|||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.*
|
||||
import com.vitorpamplona.amethyst.ui.navigation.UploadFromGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.ReplyInformation
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.UserLine
|
||||
import kotlinx.coroutines.delay
|
||||
|
@ -74,9 +73,22 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||
decorFitsSystemWindows = false
|
||||
)
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
|
||||
Column(modifier = Modifier.fillMaxWidth().fillMaxHeight()) {
|
||||
Column(modifier = Modifier.padding(10.dp).imePadding().weight(1f)) {
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.fillMaxHeight()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp)
|
||||
.imePadding()
|
||||
.weight(1f)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
@ -87,12 +99,6 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||
onClose()
|
||||
})
|
||||
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImage
|
||||
) {
|
||||
postViewModel.upload(it, context)
|
||||
}
|
||||
|
||||
PostButton(
|
||||
onPost = {
|
||||
postViewModel.sendPost()
|
||||
|
@ -104,9 +110,15 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().weight(1f)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth().verticalScroll(scroolState)) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scroolState)
|
||||
) {
|
||||
if (postViewModel.replyTos != null && baseReplyTo?.event is TextNoteEvent) {
|
||||
ReplyInformation(postViewModel.replyTos, postViewModel.mentions, account, "✖ ") {
|
||||
postViewModel.removeFromReplyList(it)
|
||||
|
@ -203,6 +215,14 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
UploadFromGallery(
|
||||
isUploading = postViewModel.isUploadingImage
|
||||
) {
|
||||
postViewModel.upload(it, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -221,7 +241,12 @@ fun CloseButton(onCancel: () -> Unit) {
|
|||
backgroundColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = stringResource(R.string.cancel), color = Color.White)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_close),
|
||||
contentDescription = stringResource(id = R.string.cancel),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.*
|
||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -80,10 +81,15 @@ class NewPostViewModel : ViewModel() {
|
|||
paragraph.split(' ').forEach { word: String ->
|
||||
val results = parseDirtyWordForKey(word)
|
||||
|
||||
if (results?.type == "npub") {
|
||||
addUserToMentions(LocalCache.getOrCreateUser(results.keyHex))
|
||||
} else if (results?.type == "note") {
|
||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.keyHex))
|
||||
if (results?.key?.type == Nip19.Type.USER) {
|
||||
addUserToMentions(LocalCache.getOrCreateUser(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||
addNoteToReplyTos(LocalCache.getOrCreateNote(results.key.hex))
|
||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
if (note != null) {
|
||||
addNoteToReplyTos(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,14 +98,21 @@ class NewPostViewModel : ViewModel() {
|
|||
val newMessage = message.text.split('\n').map { paragraph: String ->
|
||||
paragraph.split(' ').map { word: String ->
|
||||
val results = parseDirtyWordForKey(word)
|
||||
if (results?.type == "npub") {
|
||||
val user = LocalCache.getOrCreateUser(results.keyHex)
|
||||
if (results?.key?.type == Nip19.Type.USER) {
|
||||
val user = LocalCache.getOrCreateUser(results.key.hex)
|
||||
|
||||
"#[${tagIndex(user)}]${results.restOfWord}"
|
||||
} else if (results?.type == "note") {
|
||||
val note = LocalCache.getOrCreateNote(results.keyHex)
|
||||
} else if (results?.key?.type == Nip19.Type.NOTE) {
|
||||
val note = LocalCache.getOrCreateNote(results.key.hex)
|
||||
|
||||
"#[${tagIndex(note)}]${results.restOfWord}"
|
||||
} else if (results?.key?.type == Nip19.Type.ADDRESS) {
|
||||
val note = LocalCache.checkGetOrCreateAddressableNote(results.key.hex)
|
||||
if (note != null) {
|
||||
"#[${tagIndex(note)}]${results.restOfWord}"
|
||||
} else {
|
||||
word
|
||||
}
|
||||
} else {
|
||||
word
|
||||
}
|
||||
|
|
|
@ -1,17 +1,15 @@
|
|||
package com.vitorpamplona.amethyst.ui.navigation
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
|
@ -47,15 +45,23 @@ fun UploadFromGallery(
|
|||
)
|
||||
} else {
|
||||
Box() {
|
||||
Button(
|
||||
TextButton(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopCenter)
|
||||
.padding(4.dp),
|
||||
.align(Alignment.TopCenter),
|
||||
enabled = !isUploading,
|
||||
onClick = {
|
||||
showGallerySelect = true
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_add_photo),
|
||||
contentDescription = stringResource(id = R.string.upload_image),
|
||||
modifier = Modifier
|
||||
.height(20.dp)
|
||||
.padding(end = 8.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
|
||||
if (!isUploading) {
|
||||
Text(stringResource(R.string.upload_image))
|
||||
} else {
|
||||
|
|
|
@ -26,8 +26,11 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
const val SHORT_TEXT_LENGTH = 350
|
||||
|
||||
@Composable
|
||||
fun ExpandableRichTextViewer(
|
||||
content: String,
|
||||
|
@ -40,7 +43,16 @@ fun ExpandableRichTextViewer(
|
|||
) {
|
||||
var showFullText by remember { mutableStateOf(false) }
|
||||
|
||||
val text = if (showFullText) content else content.take(350)
|
||||
val text = if (showFullText) {
|
||||
content
|
||||
} else {
|
||||
val (lnStart, lnEnd) = LnInvoiceUtil.locateInvoice(content)
|
||||
if (lnStart < SHORT_TEXT_LENGTH && lnEnd > 0) {
|
||||
content.take(lnEnd)
|
||||
} else {
|
||||
content.take(SHORT_TEXT_LENGTH)
|
||||
}
|
||||
}
|
||||
|
||||
Box(contentAlignment = Alignment.BottomCenter) {
|
||||
// CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) {
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Card
|
||||
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.ArrowBack
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
@Composable
|
||||
fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
|
||||
Dialog(
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
Card {
|
||||
Column {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
IconButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.background(MaterialTheme.colors.background)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
Text(text = stringResource(R.string.select_text_dialog_top))
|
||||
}
|
||||
Divider()
|
||||
Row(modifier = Modifier.padding(16.dp)) {
|
||||
SelectionContainer {
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -31,10 +31,13 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
|
|||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
|
@ -51,7 +54,11 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
|||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@Composable
|
||||
fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
fun ChatroomCompose(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
navController: NavController
|
||||
) {
|
||||
val noteState by baseNote.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
|
||||
|
@ -86,24 +93,43 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||
|
||||
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
||||
note.createdAt()?.let {
|
||||
hasNewMessages = it > notificationCache.cache.load("Channel/${channel.idHex}", context)
|
||||
hasNewMessages =
|
||||
it > notificationCache.cache.load("Channel/${channel.idHex}", context)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelPicture = channel.profilePicture(),
|
||||
channelPicturePlaceholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
channelPicturePlaceholder = BitmapPainter(
|
||||
RoboHashCache.get(
|
||||
context,
|
||||
channel.idHex
|
||||
)
|
||||
),
|
||||
channelTitle = {
|
||||
Text(
|
||||
"${channel.info.name}",
|
||||
text = buildAnnotatedString {
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
) {
|
||||
append(channel.info.name)
|
||||
}
|
||||
|
||||
withStyle(
|
||||
SpanStyle(
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
fontWeight = FontWeight.Normal
|
||||
)
|
||||
) {
|
||||
append(" ${stringResource(id = R.string.public_chat)}")
|
||||
}
|
||||
},
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = it,
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
Text(
|
||||
" ${stringResource(R.string.public_chat)}",
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = "${author?.toBestDisplayName()}: " + description,
|
||||
|
@ -132,12 +158,21 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
|||
|
||||
LaunchedEffect(key1 = notificationCache, key2 = note) {
|
||||
noteEvent?.let {
|
||||
hasNewMessages = it.createdAt() > notificationCache.cache.load("Room/${userToComposeOn.pubkeyHex}", context)
|
||||
hasNewMessages = it.createdAt() > notificationCache.cache.load(
|
||||
"Room/${userToComposeOn.pubkeyHex}",
|
||||
context
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ChannelName(
|
||||
channelPicture = { UserPicture(userToComposeOn, account.userProfile(), size = 55.dp) },
|
||||
channelPicture = {
|
||||
UserPicture(
|
||||
userToComposeOn,
|
||||
account.userProfile(),
|
||||
size = 55.dp
|
||||
)
|
||||
},
|
||||
channelTitle = { UsernameDisplay(userToComposeOn, it) },
|
||||
channelLastTime = note.createdAt(),
|
||||
channelLastContent = accountViewModel.decrypt(note),
|
||||
|
@ -214,7 +249,10 @@ fun ChannelName(
|
|||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (channelLastContent != null) {
|
||||
Text(
|
||||
channelLastContent,
|
||||
|
|
|
@ -25,7 +25,6 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
@ -442,7 +441,7 @@ fun NoteCompose(
|
|||
)
|
||||
}
|
||||
|
||||
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -767,16 +766,6 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
|||
}
|
||||
Divider()
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_text))
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkeyNpub() ?: "")); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_user_pubkey))
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) {
|
||||
Text(stringResource(R.string.copy_note_id))
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
||||
Text(stringResource(R.string.broadcast))
|
||||
}
|
||||
|
|
|
@ -0,0 +1,253 @@
|
|||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AlternateEmail
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.FormatQuote
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material.icons.filled.PersonRemove
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.ui.components.SelectTextDialog
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
fun lightenColor(color: Color, amount: Float): Color {
|
||||
var argb = color.toArgb()
|
||||
val hslOut = floatArrayOf(0f, 0f, 0f)
|
||||
ColorUtils.colorToHSL(argb, hslOut)
|
||||
hslOut[2] += amount
|
||||
argb = ColorUtils.HSLToColor(hslOut)
|
||||
return Color(argb)
|
||||
}
|
||||
|
||||
val externalLinkForNote = { note: Note -> "https://snort.social/e/${note.idNote()}" }
|
||||
|
||||
@Composable
|
||||
fun VerticalDivider(color: Color) =
|
||||
Divider(
|
||||
color = color,
|
||||
modifier = Modifier
|
||||
.fillMaxHeight()
|
||||
.width(1.dp)
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
|
||||
val context = LocalContext.current
|
||||
val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.2f)
|
||||
val cardShape = RoundedCornerShape(5.dp)
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val scope = rememberCoroutineScope()
|
||||
var showSelectTextDialog by remember { mutableStateOf(false) }
|
||||
var showDeleteAlertDialog by remember { mutableStateOf(false) }
|
||||
val isOwnNote = note.author == accountViewModel.userProfile()
|
||||
val isFollowingUser = !isOwnNote && accountViewModel.isFollowing(note.author!!)
|
||||
|
||||
val showToast = { stringResource: Int ->
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(stringResource),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
if (popupExpanded) {
|
||||
Popup(onDismissRequest = onDismiss) {
|
||||
Card(
|
||||
modifier = Modifier.shadow(elevation = 6.dp, shape = cardShape),
|
||||
shape = cardShape,
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
) {
|
||||
Column(modifier = Modifier.width(IntrinsicSize.Min)) {
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
NoteQuickActionItem(
|
||||
icon = Icons.Default.ContentCopy,
|
||||
label = stringResource(R.string.quick_action_copy_text)
|
||||
) {
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(
|
||||
accountViewModel.decrypt(note) ?: ""
|
||||
)
|
||||
)
|
||||
showToast(R.string.copied_note_text_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(Icons.Default.AlternateEmail, stringResource(R.string.quick_action_copy_user_id)) {
|
||||
clipboardManager.setText(AnnotatedString("@${note.author?.pubkeyNpub()}" ?: ""))
|
||||
showToast(R.string.copied_user_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(Icons.Default.FormatQuote, stringResource(R.string.quick_action_copy_note_id)) {
|
||||
clipboardManager.setText(AnnotatedString("@${note.idNote()}"))
|
||||
showToast(R.string.copied_note_id_to_clipboard)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
Divider(
|
||||
color = primaryLight,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.width(1.dp)
|
||||
)
|
||||
Row(modifier = Modifier.height(IntrinsicSize.Min)) {
|
||||
if (isOwnNote) {
|
||||
NoteQuickActionItem(Icons.Default.Delete, stringResource(R.string.quick_action_delete)) {
|
||||
if (accountViewModel.hideDeleteRequestInfo()) {
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
} else {
|
||||
showDeleteAlertDialog = true
|
||||
}
|
||||
}
|
||||
} else if (isFollowingUser) {
|
||||
NoteQuickActionItem(Icons.Default.PersonRemove, stringResource(R.string.quick_action_unfollow)) {
|
||||
accountViewModel.unfollow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
} else {
|
||||
NoteQuickActionItem(Icons.Default.PersonAdd, stringResource(R.string.quick_action_follow)) {
|
||||
accountViewModel.follow(note.author!!)
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(
|
||||
icon = ImageVector.vectorResource(id = R.drawable.text_select_move_forward_character),
|
||||
label = stringResource(R.string.quick_action_select)
|
||||
) {
|
||||
showSelectTextDialog = true
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
NoteQuickActionItem(icon = Icons.Default.Share, label = stringResource(R.string.quick_action_share)) {
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(
|
||||
Intent.EXTRA_TEXT,
|
||||
externalLinkForNote(note)
|
||||
)
|
||||
putExtra(Intent.EXTRA_TITLE, context.getString(R.string.quick_action_share_browser_link))
|
||||
}
|
||||
|
||||
val shareIntent = Intent.createChooser(sendIntent, context.getString(R.string.quick_action_share))
|
||||
ContextCompat.startActivity(context, shareIntent, null)
|
||||
onDismiss()
|
||||
}
|
||||
VerticalDivider(primaryLight)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSelectTextDialog) {
|
||||
accountViewModel.decrypt(note)?.let {
|
||||
SelectTextDialog(it) { showSelectTextDialog = false }
|
||||
}
|
||||
}
|
||||
|
||||
if (showDeleteAlertDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismiss() },
|
||||
title = {
|
||||
Text(text = stringResource(R.string.quick_action_request_deletion_alert_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.quick_action_request_deletion_alert_body))
|
||||
},
|
||||
buttons = {
|
||||
Row(
|
||||
modifier = Modifier.padding(all = 8.dp).fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
accountViewModel.setHideDeleteRequestInfo()
|
||||
accountViewModel.delete(note)
|
||||
onDismiss()
|
||||
}
|
||||
) {
|
||||
Text("Don't show again")
|
||||
}
|
||||
Button(
|
||||
onClick = { accountViewModel.delete(note); onDismiss() }
|
||||
) {
|
||||
Text("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clickable { onClick() },
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.onPrimary
|
||||
)
|
||||
Text(text = label, fontSize = 12.sp)
|
||||
}
|
||||
}
|
|
@ -120,4 +120,20 @@ class AccountViewModel(private val account: Account) : ViewModel() {
|
|||
fun follow(user: User) {
|
||||
account.follow(user)
|
||||
}
|
||||
|
||||
fun unfollow(user: User) {
|
||||
account.unfollow(user)
|
||||
}
|
||||
|
||||
fun isFollowing(user: User): Boolean {
|
||||
return account.userProfile().isFollowing(user)
|
||||
}
|
||||
|
||||
fun hideDeleteRequestInfo(): Boolean {
|
||||
return account.hideDeleteRequestInfo
|
||||
}
|
||||
|
||||
fun setHideDeleteRequestInfo() {
|
||||
account.setHideDeleteRequestInfo()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,39 +1,18 @@
|
|||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -50,18 +29,14 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.input.*
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
|
@ -178,13 +153,29 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
|||
onCheckedChange = { acceptedTerms.value = it }
|
||||
)
|
||||
|
||||
Text(text = stringResource(R.string.i_accept_the))
|
||||
val clickableTextStyle =
|
||||
SpanStyle(color = MaterialTheme.colors.primary)
|
||||
|
||||
val annotatedTermsString = buildAnnotatedString {
|
||||
append(stringResource(R.string.i_accept_the))
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("openTerms", "")
|
||||
append(stringResource(R.string.terms_of_use))
|
||||
}
|
||||
}
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.terms_of_use)),
|
||||
onClick = { runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary)
|
||||
)
|
||||
text = annotatedTermsString
|
||||
) { spanOffset ->
|
||||
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset)
|
||||
.firstOrNull()
|
||||
?.also { span ->
|
||||
if (span.tag == "openTerms") {
|
||||
runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
|
@ -201,7 +192,8 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
|||
Button(
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
|
@ -240,7 +232,8 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
|||
if (acceptedTerms.value) {
|
||||
accountViewModel.newKey()
|
||||
} else {
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
termsAcceptanceIsRequired =
|
||||
context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
},
|
||||
style = TextStyle(
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,7v2.99s-1.99,0.01 -2,0L17,7h-3s0.01,-1.99 0,-2h3L17,2h2v3h3v2h-3zM16,11L16,8h-3L13,5L5,5c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2v-8h-3zM5,19l3,-4 2,3 3,-4 4,5L5,19z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:autoMirrored="true" android:height="48dp"
|
||||
android:viewportHeight="960" android:viewportWidth="960"
|
||||
android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M439,840v-60h83v60h-83ZM439,180v-60h83v60h-83ZM609,840v-60h83v60h-83ZM609,180v-60h83v60h-83ZM780,840v-60h60v60h-60ZM780,180v-60h60v60h-60ZM120,840v-60h86L206,180h-86v-60h231v60h-85v600h85v60L120,840ZM694,626 L652,584 725,510L414,510v-60h311l-73,-74 42,-42 146,146 -146,146Z"/>
|
||||
</vector>
|
|
@ -67,7 +67,7 @@
|
|||
<string name="description">Описание</string>
|
||||
<string name="about_us">"О нас.. "</string>
|
||||
<string name="what_s_on_your_mind">Что нового?</string>
|
||||
<string name="post">Опубликовать</string>
|
||||
<string name="post">Отправить</string>
|
||||
<string name="save">Сохранить</string>
|
||||
<string name="create">Создать</string>
|
||||
<string name="cancel">Отменить</string>
|
||||
|
@ -92,7 +92,7 @@
|
|||
<string name="ln_url_outdated">LN URL (устаревш.)</string>
|
||||
<string name="image_saved_to_the_gallery">Фото сохранено в галерею</string>
|
||||
<string name="failed_to_save_the_image">Не удалось сохранить фото</string>
|
||||
<string name="upload_image">Загрузить\nфото</string>
|
||||
<string name="upload_image">Загрузить фото</string>
|
||||
<string name="uploading">Загрузка…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Пользователь не установил Lightning адрес для получения чаевых</string>
|
||||
<string name="reply_here">"ответить.. "</string>
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
<string name="description">Опис</string>
|
||||
<string name="about_us">"Про нас.. "</string>
|
||||
<string name="what_s_on_your_mind">Що нового?</string>
|
||||
<string name="post">Опублікувати</string>
|
||||
<string name="post">Надіслати</string>
|
||||
<string name="save">Зберегти</string>
|
||||
<string name="create">Створити</string>
|
||||
<string name="cancel">Скасувати</string>
|
||||
|
@ -92,7 +92,7 @@
|
|||
<string name="ln_url_outdated">LN URL (застарівш.)</string>
|
||||
<string name="image_saved_to_the_gallery">Фото збережено до галереї</string>
|
||||
<string name="failed_to_save_the_image">Не вдалося зберегти фото</string>
|
||||
<string name="upload_image">Завантажити\nфото</string>
|
||||
<string name="upload_image">Завантажити фото</string>
|
||||
<string name="uploading">Завантаження…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Користувач не встановив Lightning адресу для отримання чайових</string>
|
||||
<string name="reply_here">"відповісти.. "</string>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="app_name_release" translatable="false">Amethyst</string>
|
||||
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
|
||||
<string name="point_to_the_qr_code">Point to the QR Code</string>
|
||||
|
@ -20,7 +20,7 @@
|
|||
<string name="relay_icon">Relay Icon</string>
|
||||
<string name="unknown_author">Unknown Author</string>
|
||||
<string name="copy_text">Copy Text</string>
|
||||
<string name="copy_user_pubkey">Copy User PubKey</string>
|
||||
<string name="copy_user_pubkey">Copy Author @npub</string>
|
||||
<string name="copy_note_id">Copy Note ID</string>
|
||||
<string name="broadcast">Broadcast</string>
|
||||
<string name="block_hide_user"><![CDATA[Block & Hide User]]></string>
|
||||
|
@ -177,7 +177,7 @@
|
|||
<string name="mark_all_known_as_read">Mark all Known as read</string>
|
||||
<string name="mark_all_new_as_read">Mark all New as read</string>
|
||||
<string name="mark_all_as_read">Mark all as read</string>
|
||||
<string name="account_backup_tips_md">
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## Key Backup and Safety Tips
|
||||
\n\nYour account is secured by a secret key. The key is long random string starting with **nsec1**. Anyone who has access to your secret key can publish content using your identity.
|
||||
\n\n- Do **not** put your secret key in any website or software you do not trust.
|
||||
|
@ -192,4 +192,19 @@
|
|||
<string name="badge_award_image_for">"Badge award image for %1$s"</string>
|
||||
<string name="new_badge_award_notif">You Received a new Badge Award</string>
|
||||
<string name="award_granted_to">Badge award granted to</string>
|
||||
</resources>
|
||||
<string name="copied_note_text_to_clipboard">Copied note text to clipboard</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">Copied author’s @npub to clipboard</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Copied note ID (@note1) to clipboard</string>
|
||||
<string name="select_text_dialog_top">Select Text</string>
|
||||
<string name="quick_action_select">Select</string>
|
||||
<string name="quick_action_share_browser_link">Share Browser Link</string>
|
||||
<string name="quick_action_share">Share</string>
|
||||
<string name="quick_action_copy_user_id">Mention</string>
|
||||
<string name="quick_action_copy_note_id">Quote</string>
|
||||
<string name="quick_action_copy_text">Copy</string>
|
||||
<string name="quick_action_delete">Delete</string>
|
||||
<string name="quick_action_unfollow">Unfollow</string>
|
||||
<string name="quick_action_follow">Follow</string>
|
||||
<string name="quick_action_request_deletion_alert_title">Request Deletion</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored.</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.vitorpamplona.amethyst
|
||||
|
||||
import com.vitorpamplona.amethyst.model.parseDirtyWordForKey
|
||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -13,80 +14,80 @@ class KeyParseTest {
|
|||
@Test
|
||||
fun keyParseTestNote() {
|
||||
val result = parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn")
|
||||
assertEquals("note", result?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex)
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals("", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestPub() {
|
||||
val result = parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z")
|
||||
assertEquals("npub", result?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex)
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals("", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestNoteWithExtraChars() {
|
||||
val result = parseDirtyWordForKey("note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals("note", result?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex)
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestPubWithExtraChars() {
|
||||
val result = parseDirtyWordForKey("npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals("npub", result?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex)
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestNoteWithExtraCharsAndAt() {
|
||||
val result = parseDirtyWordForKey("@note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals("note", result?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex)
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestPubWithExtraCharsAndAt() {
|
||||
val result = parseDirtyWordForKey("@npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals("npub", result?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex)
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestNoteWithExtraCharsAndNostrPrefix() {
|
||||
val result = parseDirtyWordForKey("nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals("note", result?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex)
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestPubWithExtraCharsAndNostrPrefix() {
|
||||
val result = parseDirtyWordForKey("nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals("npub", result?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex)
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestUppercaseNoteWithExtraCharsAndNostrPrefix() {
|
||||
val result = parseDirtyWordForKey("Nostr:note1z5e2m0smx6d7e2d0zaq8d3rnd7httm6j0uf8tf90yqqjrs842czshwtkmn,")
|
||||
assertEquals("note", result?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.keyHex)
|
||||
assertEquals(Nip19.Type.NOTE, result?.key?.type)
|
||||
assertEquals("1532adbe1b369beca9af174076c4736faeb5ef527f1275a4af200121c0f55605", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun keyParseTestUppercasePubWithExtraCharsAndNostrPrefix() {
|
||||
val result = parseDirtyWordForKey("nOstr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z,")
|
||||
assertEquals("npub", result?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.keyHex)
|
||||
assertEquals(Nip19.Type.USER, result?.key?.type)
|
||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.key?.hex)
|
||||
assertEquals(",", result?.restOfWord)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,8 @@ plugins {
|
|||
}
|
||||
|
||||
task installGitHook(type: Copy) {
|
||||
from new File(rootProject.rootDir, 'pre-commit')
|
||||
from new File(rootProject.rootDir, 'git-hooks/pre-commit')
|
||||
from new File(rootProject.rootDir, 'git-hooks/pre-push')
|
||||
into { new File(rootProject.rootDir, '.git/hooks') }
|
||||
fileMode 0777
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
NO_COLOR='\033[0m'
|
||||
|
||||
echo "*********************************************************"
|
||||
echo "Running git pre-push hook. Running test... "
|
||||
echo "*********************************************************"
|
||||
|
||||
./gradlew test
|
||||
|
||||
status=$?
|
||||
|
||||
if [ "$status" = 0 ] ; then
|
||||
echo "All test passed."
|
||||
exit 0
|
||||
else
|
||||
echo "*********************************************************"
|
||||
echo 1>&2 "Failing test"
|
||||
printf "Run ${GREEN}./gradlew test${NO_COLOR} to make sure you have all tests green before pushing...\n"
|
||||
echo "*********************************************************"
|
||||
exit 1
|
||||
fi
|
Ładowanie…
Reference in New Issue