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.kt
pull/233/head
Vitor Pamplona 2023-03-08 17:47:34 -05:00
commit b93f28f09d
28 zmienionych plików z 648 dodań i 4468 usunięć

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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()}")

Wyświetl plik

@ -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()

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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 }
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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
)
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)
}
}
}
}
}
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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))
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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(

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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 authors @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>

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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
}

4330
deps.txt

Plik diff jest za duży Load Diff

23
git-hooks/pre-push 100755
Wyświetl plik

@ -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