merge with recent changes to main

This commit is contained in:
toadlyBroodle
2023-03-31 10:50:37 +09:00
35 changed files with 795 additions and 104 deletions
+7 -7
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 108
versionName "0.30.1"
versionCode 109
versionName "0.30.2"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -114,7 +114,7 @@ dependencies {
implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
// Bitcoin secp256k1 bindings to Android
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.7.1'
implementation 'fr.acinq.secp256k1:secp256k1-kmp-jni-android:0.8.0'
// Nostr Base Protocol
implementation('com.github.vitorpamplona.NostrPostr:nostrpostrlib:master-SNAPSHOT') {
@@ -137,14 +137,14 @@ dependencies {
implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha05'
// view videos
implementation 'com.google.android.exoplayer:exoplayer:2.18.4'
implementation 'com.google.android.exoplayer:exoplayer:2.18.5'
// Load images from the web.
implementation "io.coil-kt:coil-compose:2.2.2"
implementation "io.coil-kt:coil-compose:$coil_version"
// view gifs
implementation "io.coil-kt:coil-gif:2.2.2"
implementation "io.coil-kt:coil-gif:$coil_version"
// view svgs
implementation("io.coil-kt:coil-svg:2.2.2")
implementation "io.coil-kt:coil-svg:$coil_version"
// Rendering clickable text
implementation "com.google.accompanist:accompanist-flowlayout:$accompanist_version"
+7
View File
@@ -43,6 +43,13 @@
<data android:scheme="nostr" />
</intent-filter>
<intent-filter android:label="Amethyst">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="nostrwalletconnect" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
@@ -156,7 +156,7 @@ class Account(
}
}
fun createZapRequestFor(note: Note, pollOption: Int?): LnZapRequestEvent? {
fun createZapRequestFor(note: Note, pollOption: Int?, message: String = ""): LnZapRequestEvent? {
if (!isWriteable()) return null
note.event?.let { event ->
@@ -167,7 +167,10 @@ class Account(
loggedIn.privKey!!,
pollOption
)
}
/*note.event?.let {
return LnZapRequestEvent.create(it, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message)
}*/
return null
}
@@ -189,10 +192,10 @@ class Account(
return createZapRequestFor(user.pubkeyHex)
}
fun createZapRequestFor(userPubKeyHex: String): LnZapRequestEvent? {
fun createZapRequestFor(userPubKeyHex: String, message: String = ""): LnZapRequestEvent? {
if (!isWriteable()) return null
return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!)
return LnZapRequestEvent.create(userPubKeyHex, userProfile().latestContactList?.relays()?.keys?.ifEmpty { null } ?: localRelays.map { it.url }.toSet(), loggedIn.privKey!!, message)
}
fun report(note: Note, type: ReportEvent.ReportType, content: String = "") {
@@ -1,10 +1,11 @@
package com.vitorpamplona.amethyst.ui.note
import android.net.Uri
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.decodePublicKey
import com.vitorpamplona.amethyst.model.toHexKey
data class Nip47URI(val pubKeyHex: String, val relayUri: String?, val secret: String?)
data class Nip47URI(val pubKeyHex: HexKey, val relayUri: String?, val secret: HexKey?)
// Rename to the corect nip number when ready.
object Nip47 {
@@ -30,25 +30,25 @@ object NostrSearchEventOrUserDataSource : NostrDataSource("SingleEventFeed") {
null
}
if (hexToWatch == null) {
return null
}
// downloads all the reactions to a given event.
return listOf(
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
ids = listOfNotNull(hexToWatch)
return listOfNotNull(
hexToWatch?.let {
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
ids = listOfNotNull(hexToWatch)
)
)
),
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOfNotNull(hexToWatch)
},
hexToWatch?.let {
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
kinds = listOf(MetadataEvent.kind),
authors = listOfNotNull(hexToWatch)
)
)
),
},
TypedFilter(
types = FeedType.values().toSet(),
filter = JsonFilter(
@@ -71,8 +71,6 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
return null
}
val now = Date().time / 1000
return reactionsToWatch.map {
TypedFilter(
types = FeedType.values().toSet(),
@@ -142,7 +140,7 @@ object NostrSingleEventDataSource : NostrDataSource("SingleEventFeed") {
val addresses = createAddressFilter()
val addressReactions = createTagToAddressFilter()
singleEventChannel.typedFilters = listOfNotNull(reactions, missing, addresses, addressReactions).flatten().ifEmpty { null }
singleEventChannel.typedFilters = listOfNotNull(missing, addresses, reactions, addressReactions).flatten().ifEmpty { null }
}
fun add(eventId: Note) {
@@ -24,9 +24,10 @@ class LnZapRequestEvent(
relays: Set<String>,
privateKey: ByteArray,
pollOption: Int?,
message: String,
createdAt: Long = Date().time / 1000
): LnZapRequestEvent {
val content = ""
val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
var tags = listOf(
listOf("e", originalNote.id()),
@@ -49,14 +50,19 @@ class LnZapRequestEvent(
userHex: String,
relays: Set<String>,
privateKey: ByteArray,
pollOption: Int?,
message: String,
createdAt: Long = Date().time / 1000
): LnZapRequestEvent {
val content = ""
val content = message
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf(
var tags = listOf(
listOf("p", userHex),
listOf("relays") + relays
)
if (pollOption != null && pollOption >= 0) {
tags = tags + listOf(listOf(POLL_OPTION, pollOption.toString()))
}
val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey)
return LnZapRequestEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -4,6 +4,7 @@ import com.vitorpamplona.amethyst.model.RelaySetupInfo
object Constants {
val activeTypes = setOf(FeedType.FOLLOWS, FeedType.PRIVATE_DMS)
val activeTypesChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS)
val activeTypesGlobalChats = setOf(FeedType.FOLLOWS, FeedType.PUBLIC_CHATS, FeedType.PRIVATE_DMS, FeedType.GLOBAL)
val activeTypesSearch = setOf(FeedType.SEARCH)
@@ -14,16 +15,18 @@ object Constants {
}
val defaultRelays = arrayOf(
// Free relays
RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypes),
// Free relays for DMs and Follows
RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.snort.social", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://relay.damus.io", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://nostr-pub.wellorder.net", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://no.str.cr", read = true, write = true, feedTypes = activeTypes),
RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypes),
// Chats
RelaySetupInfo("wss://nostr.bitcoiner.social", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://relay.nostr.bg", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nostr.oxtr.dev", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nostr-pub.wellorder.net", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nostr.mom", read = true, write = true, feedTypes = activeTypesChats),
RelaySetupInfo("wss://nos.lol", read = true, write = true, feedTypes = activeTypesChats),
// Less Reliable
// NewRelayListViewModel.Relay("wss://nostr.orangepill.dev", read = true, write = true, feedTypes = activeTypes),
@@ -182,7 +182,7 @@ class Relay(
val filters = Client.getSubscriptionFilters(requestId).filter { activeTypes.intersect(it.types).isNotEmpty() }
if (filters.isNotEmpty()) {
val request =
"""["REQ","$requestId",${filters.take(10).joinToString(",") { it.filter.toJson(url) }}]"""
"""["REQ","$requestId",${filters.take(40).joinToString(",") { it.filter.toJson(url) }}]"""
// println("FILTERSSENT $url $request")
socket?.send(request)
eventUploadCounterInBytes += request.bytesUsedInMemory()
@@ -21,6 +21,8 @@ import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.service.nip19.Nip19
import com.vitorpamplona.amethyst.service.relays.Client
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.Nip47
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
@@ -28,6 +30,8 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class MainActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
@@ -40,6 +44,14 @@ class MainActivity : FragmentActivity() {
Nip19.Type.EVENT -> "Event/${nip19.hex}"
Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
else -> null
} ?: try {
intent?.data?.toString()?.let {
Nip47.parse(it)
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
Route.Home.base + "?nip47=" + encodedUri
}
} catch (e: Exception) {
null
}
Coil.setImageLoader {
@@ -49,6 +49,9 @@ open class NewPostViewModel : ViewModel() {
this.mentions = currentMentions.plus(replyUser)
}
}
} ?: {
replyTos = null
mentions = null
}
quote?.let {
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.actions
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -18,6 +19,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
@@ -30,9 +32,14 @@ import com.vitorpamplona.amethyst.model.Account
@Composable
fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
val postViewModel: NewUserMetadataViewModel = viewModel()
val context = LocalContext.current
LaunchedEffect(Unit) {
postViewModel.load(account)
postViewModel.imageUploadingError.collect { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
}
Dialog(
@@ -141,6 +148,15 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
leadingIcon = {
UploadFromGallery(
isUploading = postViewModel.isUploadingImageForPicture,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
postViewModel.uploadForPicture(it, context)
}
},
singleLine = true
)
@@ -157,6 +173,15 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
leadingIcon = {
UploadFromGallery(
isUploading = postViewModel.isUploadingImageForBanner,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
postViewModel.uploadForBanner(it, context)
}
},
singleLine = true
)
@@ -1,13 +1,20 @@
package com.vitorpamplona.amethyst.ui.actions
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.GitHubIdentity
import com.vitorpamplona.amethyst.service.model.MastodonIdentity
import com.vitorpamplona.amethyst.service.model.TwitterIdentity
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import java.io.ByteArrayInputStream
import java.io.StringWriter
@@ -30,6 +37,10 @@ class NewUserMetadataViewModel : ViewModel() {
val github = mutableStateOf("")
val mastodon = mutableStateOf("")
var isUploadingImageForPicture by mutableStateOf(false)
var isUploadingImageForBanner by mutableStateOf(false)
val imageUploadingError = MutableSharedFlow<String?>()
fun load(account: Account) {
this.account = account
@@ -127,4 +138,49 @@ class NewUserMetadataViewModel : ViewModel() {
github.value = ""
mastodon.value = ""
}
fun uploadForPicture(uri: Uri, context: Context) {
upload(
uri,
context,
onUploading = {
isUploadingImageForPicture = it
},
onUploaded = {
picture.value = it
}
)
}
fun uploadForBanner(uri: Uri, context: Context) {
upload(
uri,
context,
onUploading = {
isUploadingImageForBanner = it
},
onUploaded = {
banner.value = it
}
)
}
fun upload(it: Uri, context: Context, onUploading: (Boolean) -> Unit, onUploaded: (String) -> Unit) {
onUploading(true)
ImageUploader.uploadImage(
uri = it,
contentResolver = context.contentResolver,
onSuccess = { imageUrl ->
onUploading(false)
onUploaded(imageUrl)
},
onError = {
onUploading(false)
viewModelScope.launch {
imageUploadingError.emit("Failed to upload the image / video")
}
}
)
}
}
@@ -130,7 +130,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
Button(
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
onClick = {
val zapRequest = account.createZapRequestFor(toUserPubKeyHex)
val zapRequest = account.createZapRequestFor(toUserPubKeyHex, message)
LightningAddressResolver().lnAddressInvoice(
lud16,
@@ -10,8 +10,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -24,14 +28,21 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import com.google.accompanist.pager.ExperimentalPagerApi
import com.google.accompanist.pager.PagerState
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
import net.engawapg.lib.zoomable.rememberZoomState
import net.engawapg.lib.zoomable.zoomable
@@ -46,6 +57,11 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
mutableStateOf(false)
}
// store the dialog open or close state
var imageState by remember {
mutableStateOf<AsyncImagePainter.State?>(null)
}
if (imageExtension.matcher(word).matches()) {
AsyncImage(
model = word,
@@ -63,8 +79,48 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
.combinedClickable(
onClick = { dialogOpen = true },
onLongClick = { clipboardManager.setText(AnnotatedString(word)) }
)
),
onLoading = {
imageState = it
},
onSuccess = {
imageState = it
}
)
if (imageState !is AsyncImagePainter.State.Success) {
ClickableUrl(urlText = "$word ", url = word)
val myId = "inlineContent"
val emptytext = buildAnnotatedString {
withStyle(
LocalTextStyle.current.copy(color = MaterialTheme.colors.primary).toSpanStyle()
) {
append("")
appendInlineContent(myId, "[icon]")
}
}
val inlineContent = mapOf(
Pair(
myId,
InlineTextContent(
Placeholder(
width = 17.sp,
height = 17.sp,
placeholderVerticalAlign = PlaceholderVerticalAlign.Center
)
) {
LoadingAnimation()
}
)
)
// Empty Text for Size of Icon
Text(
text = emptytext,
inlineContent = inlineContent
)
}
} else {
VideoView(word) { dialogOpen = true }
}
@@ -86,7 +142,9 @@ fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(image
var pagerState: PagerState = remember { PagerState() }
Row(
modifier = Modifier.padding(10.dp).fillMaxWidth(),
modifier = Modifier
.padding(10.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -45,4 +45,8 @@ object NotificationFeedFilter : FeedFilter<Note>() {
.toList()
.reversed()
}
fun isDifferentAccount(account: Account): Boolean {
return this::account.isInitialized && this.account != account
}
}
@@ -1,8 +1,12 @@
package com.vitorpamplona.amethyst.ui.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
@@ -39,6 +43,7 @@ fun AppNavigation(
nextPage: String? = null
) {
val homePagerState = rememberPagerState()
var actionableNextPage by remember { mutableStateOf<String?>(nextPage) }
// Avoids creating ViewModels for performance reasons (up to 1 second delays)
val accountState by accountViewModel.accountLiveData.observeAsState()
@@ -76,8 +81,9 @@ fun AppNavigation(
}
Route.Home.let { route ->
composable(route.route, route.arguments, content = {
composable(route.route, route.arguments, content = { it ->
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
val nip47 = it.arguments?.getString("nip47")
HomeScreen(
homeFeedViewModel = homeFeedViewModel,
@@ -85,6 +91,28 @@ fun AppNavigation(
accountViewModel = accountViewModel,
navController = navController,
pagerState = homePagerState,
scrollToTop = scrollToTop,
nip47 = nip47
)
// Avoids running scroll to top when back button is pressed
if (scrollToTop) {
it.arguments?.remove("scrollToTop")
}
if (nip47 != null) {
it.arguments?.remove("nip47")
}
})
}
Route.Notification.let { route ->
composable(route.route, route.arguments, content = {
val scrollToTop = it.arguments?.getBoolean("scrollToTop") ?: false
NotificationScreen(
notifFeedViewModel = notifFeedViewModel,
accountViewModel = accountViewModel,
navController = navController,
scrollToTop = scrollToTop
)
@@ -96,7 +124,6 @@ fun AppNavigation(
}
composable(Route.Message.route, content = { ChatroomListScreen(accountViewModel, navController) })
composable(Route.Notification.route, content = { NotificationScreen(notifFeedViewModel, accountViewModel, navController) })
composable(Route.BlockedUsers.route, content = { HiddenUsersScreen(accountViewModel, navController) })
composable(Route.Bookmarks.route, content = { BookmarkListScreen(accountViewModel, navController) })
@@ -161,7 +188,10 @@ fun AppNavigation(
}
}
if (nextPage != null) {
navController.navigate(nextPage)
actionableNextPage?.let {
LaunchedEffect(it) {
navController.navigate(it)
}
actionableNextPage = null
}
}
@@ -24,9 +24,12 @@ sealed class Route(
get() = route.substringBefore("?")
object Home : Route(
route = "Home?scrollToTop={scrollToTop}",
route = "Home?scrollToTop={scrollToTop}&nip47={nip47}",
icon = R.drawable.ic_home,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
arguments = listOf(
navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false },
navArgument("nip47") { type = NavType.StringType; nullable = true; defaultValue = null }
),
hasNewItems = { accountViewModel, cache -> homeHasNewItems(accountViewModel, cache) }
)
@@ -37,8 +40,9 @@ sealed class Route(
)
object Notification : Route(
route = "Notification",
route = "Notification?scrollToTop={scrollToTop}",
icon = R.drawable.ic_notifications,
arguments = listOf(navArgument("scrollToTop") { type = NavType.BoolType; defaultValue = false }),
hasNewItems = { accountViewModel, cache -> notificationHasNewItems(accountViewModel, cache) }
)
@@ -346,7 +346,7 @@ fun NoteComposeInner(
}
val pow = noteEvent.getPoWRank()
if (pow > 1) {
if (pow > 20) {
DisplayPoW(pow)
}
}
@@ -303,10 +303,11 @@ fun ZapReaction(
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = zapsState?.note
val zapMessage = ""
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -348,7 +349,7 @@ fun ZapReaction(
baseNote,
account.zapAmountChoices.first() * 1000,
null,
"",
zapMessage,
context,
onError = {
scope.launch {
@@ -371,6 +372,9 @@ fun ZapReaction(
},
onLongClick = {
wantsToChangeZapAmount = true
},
onDoubleClick = {
wantsToSetCustomZap = true
}
)
) {
@@ -403,6 +407,10 @@ fun ZapReaction(
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
}
if (wantsToSetCustomZap) {
ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
}
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
zappingProgress = 1f
Icon(
@@ -531,7 +539,7 @@ fun ZapAmountChoicePopup(
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
val zapMessage = ""
val scope = rememberCoroutineScope()
Popup(
@@ -549,7 +557,7 @@ fun ZapAmountChoicePopup(
baseNote,
amountInSats * 1000,
null,
"",
zapMessage,
context,
onError,
onProgress
@@ -574,7 +582,7 @@ fun ZapAmountChoicePopup(
baseNote,
amountInSats * 1000,
null,
"",
zapMessage,
context,
onError,
onProgress
@@ -49,6 +49,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
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.input.ImeAction
@@ -114,20 +115,38 @@ class UpdateZapAmountViewModel : ViewModel() {
account?.changeZapAmounts(amountSet)
if (walletConnectRelay.text.isNotBlank() && walletConnectPubkey.text.isNotBlank()) {
val pubkeyHex = try {
decodePublicKey(walletConnectPubkey.text.trim()).toHexKey()
} catch (e: Exception) {
null
}
val relayUrl = walletConnectRelay.text.ifBlank { null }?.let {
var addedWSS =
if (!it.startsWith("wss://") && !it.startsWith("ws://")) "wss://$it" else it
if (addedWSS.endsWith("/")) addedWSS = addedWSS.dropLast(1)
addedWSS
}
val unverifiedPrivKey = walletConnectSecret.text.ifBlank { null }
val privKey = try {
val privKeyHex = try {
unverifiedPrivKey?.let { decodePublicKey(it).toHexKey() }
} catch (e: Exception) {
null
}
account?.changeZapPaymentRequest(
Nip47URI(
walletConnectPubkey.text,
walletConnectRelay.text.ifBlank { null },
privKey
if (pubkeyHex != null) {
account?.changeZapPaymentRequest(
Nip47URI(
pubkeyHex,
relayUrl,
privKeyHex
)
)
)
} else {
account?.changeZapPaymentRequest(null)
}
} else {
account?.changeZapPaymentRequest(null)
}
@@ -147,17 +166,40 @@ class UpdateZapAmountViewModel : ViewModel() {
walletConnectSecret.text != (account?.zapPaymentRequest?.secret ?: "")
)
}
fun updateNIP47(uri: String) {
val contact = Nip47.parse(uri)
if (contact != null) {
walletConnectPubkey =
TextFieldValue(contact.pubKeyHex)
walletConnectRelay =
TextFieldValue(contact.relayUri ?: "")
walletConnectSecret =
TextFieldValue(contact.secret ?: "")
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account, nip47uri: String? = null) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val postViewModel: UpdateZapAmountViewModel = viewModel()
val uri = LocalUriHandler.current
LaunchedEffect(account) {
postViewModel.load(account)
if (nip47uri != null) {
try {
postViewModel.updateNIP47(nip47uri)
} catch (e: IllegalArgumentException) {
scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
.show()
}
}
}
}
Dialog(
@@ -281,6 +323,18 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
stringResource(id = R.string.wallet_connect_service),
Modifier.weight(1f)
)
IconButton(onClick = {
runCatching { uri.openUri("https://nwc.getalby.com/apps/new?c=Amethyst") }
}) {
Icon(
painter = painterResource(R.drawable.alby),
null,
modifier = Modifier.size(24.dp),
tint = Color.Unspecified
)
}
IconButton(onClick = {
qrScanning = true
}) {
@@ -312,15 +366,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
qrScanning = false
if (!it.isNullOrEmpty()) {
try {
val contact = Nip47.parse(it)
if (contact != null) {
postViewModel.walletConnectPubkey =
TextFieldValue(contact.pubKeyHex)
postViewModel.walletConnectRelay =
TextFieldValue(contact.relayUri ?: "")
postViewModel.walletConnectSecret =
TextFieldValue(contact.secret ?: "")
}
postViewModel.updateNIP47(it)
} catch (e: IllegalArgumentException) {
scope.launch {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT)
@@ -370,7 +416,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
onValueChange = { postViewModel.walletConnectRelay = it },
placeholder = {
Text(
text = "relay.server.com",
text = "wss://relay.server.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
maxLines = 1
)
@@ -0,0 +1,193 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.ui.actions.CloseButton
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ZapOptionstViewModel : ViewModel() {
private var account: Account? = null
var customAmount by mutableStateOf(TextFieldValue("1000"))
var customMessage by mutableStateOf(TextFieldValue(""))
fun load(account: Account) {
this.account = account
}
fun canSend(): Boolean {
return value() != null
}
fun value(): Long? {
return try {
customAmount.text.trim().toLongOrNull()
} catch (e: Exception) {
null
}
}
fun cancel() {
}
}
@Composable
fun ZapCustomDialog(onClose: () -> Unit, account: Account, accountViewModel: AccountViewModel, baseNote: Note) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val postViewModel: ZapOptionstViewModel = viewModel()
LaunchedEffect(account) {
postViewModel.load(account)
}
Dialog(
onDismissRequest = { onClose() },
properties = DialogProperties(
dismissOnClickOutside = false,
usePlatformDefaultWidth = false
)
) {
Surface() {
Column(modifier = Modifier.padding(10.dp)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
CloseButton(onCancel = {
postViewModel.cancel()
onClose()
})
ZapButton(
isActive = postViewModel.canSend()
) {
scope.launch(Dispatchers.IO) {
accountViewModel.zap(
baseNote,
postViewModel.value()!! * 1000L,
postViewModel.customMessage.text,
context,
onError = {
scope.launch {
Toast
.makeText(context, it, Toast.LENGTH_SHORT).show()
}
},
onProgress = {
scope.launch(Dispatchers.Main) {
}
}
)
}
onClose()
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 5.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
// stringResource(R.string.new_amount_in_sats
label = { Text(text = stringResource(id = R.string.amount_in_sats)) },
value = postViewModel.customAmount,
onValueChange = {
postViewModel.customAmount = it
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Number
),
placeholder = {
Text(
text = "100, 1000, 5000",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true,
modifier = Modifier
.padding(end = 10.dp)
.weight(1f)
)
}
Spacer(modifier = Modifier.height(5.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 5.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
// stringResource(R.string.new_amount_in_sats
label = { Text(text = stringResource(id = R.string.custom_zaps_add_a_message)) },
value = postViewModel.customMessage,
onValueChange = {
postViewModel.customMessage = it
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.None,
keyboardType = KeyboardType.Text
),
placeholder = {
Text(
text = stringResource(id = R.string.custom_zaps_add_a_message_example),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true,
modifier = Modifier
.padding(end = 10.dp)
.weight(1f)
)
}
}
}
}
}
@Composable
fun ZapButton(isActive: Boolean, onPost: () -> Unit) {
Button(
onClick = { onPost() },
shape = RoundedCornerShape(20.dp),
colors = ButtonDefaults
.buttonColors(
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
)
) {
Text(text = "⚡Zap ", color = Color.White)
}
}
@@ -13,6 +13,7 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -33,7 +34,14 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewModel, navController: NavController, routeForLastRead: String) {
fun CardFeedView(
viewModel: CardFeedViewModel,
accountViewModel: AccountViewModel,
navController: NavController,
routeForLastRead: String,
scrollStateKey: String? = null,
scrollToTop: Boolean = false
) {
val feedState by viewModel.feedContent.collectAsState()
var refreshing by remember { mutableStateOf(false) }
@@ -57,10 +65,12 @@ fun CardFeedView(viewModel: CardFeedViewModel, accountViewModel: AccountViewMode
is CardFeedState.Loaded -> {
refreshing = false
FeedLoaded(
state,
accountViewModel,
navController,
routeForLastRead
state = state,
accountViewModel = accountViewModel,
navController = navController,
routeForLastRead = routeForLastRead,
scrollStateKey = scrollStateKey,
scrollToTop = scrollToTop
)
}
CardFeedState.Loading -> {
@@ -79,9 +89,21 @@ private fun FeedLoaded(
state: CardFeedState.Loaded,
accountViewModel: AccountViewModel,
navController: NavController,
routeForLastRead: String
routeForLastRead: String,
scrollStateKey: String?,
scrollToTop: Boolean = false
) {
val listState = rememberLazyListState()
val listState = if (scrollStateKey != null) {
rememberForeverLazyListState(scrollStateKey)
} else {
rememberLazyListState()
}
if (scrollToTop) {
LaunchedEffect(Unit) {
listState.scrollToItem(index = 0)
}
}
LazyColumn(
contentPadding = PaddingValues(
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.LocalCacheState
import com.vitorpamplona.amethyst.model.Note
@@ -32,6 +33,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
private var lastAccount: Account? = null
private var lastNotes: List<Note>? = null
private fun refresh() {
@@ -45,18 +47,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
private fun refreshSuspended() {
val notes = dataSource.loadTop()
val lastNotesCopy = lastNotes
val thisAccount = (dataSource as? NotificationFeedFilter)?.account
val lastNotesCopy = if (thisAccount == lastAccount) lastNotes else null
val oldNotesState = feedContent.value
if (lastNotesCopy != null && oldNotesState is CardFeedState.Loaded) {
val newCards = convertToCard(notes.minus(lastNotesCopy))
if (newCards.isNotEmpty()) {
lastNotes = notes
lastAccount = (dataSource as? NotificationFeedFilter)?.account
updateFeed((oldNotesState.feed.value + newCards).distinctBy { it.id() }.sortedBy { it.createdAt() }.reversed())
}
} else {
val cards = convertToCard(notes)
lastNotes = notes
lastAccount = (dataSource as? NotificationFeedFilter)?.account
updateFeed(cards)
}
}
@@ -100,14 +105,21 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
}
val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys
val multiCards = allBaseNotes.map {
MultiSetCard(
it,
boostsPerEvent.get(it) ?: emptyList(),
reactionsPerEvent.get(it) ?: emptyList(),
zapsPerEvent.get(it) ?: emptyMap()
)
}
val multiCards = allBaseNotes.map { baseNote ->
val boostsInCard = boostsPerEvent[baseNote] ?: emptyList()
val reactionsInCard = reactionsPerEvent[baseNote] ?: emptyList()
val zapsInCard = zapsPerEvent[baseNote] ?: emptyMap()
val singleList = (boostsInCard + zapsInCard.values + reactionsInCard).sortedBy { it.createdAt() }.reversed()
singleList.chunked(50).map { chunk ->
MultiSetCard(
baseNote,
boostsInCard.filter { it in chunk },
reactionsInCard.filter { it in chunk },
zapsInCard.filter { it.value in chunk }
)
}
}.flatten()
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map {
if (it.event is PrivateDmEvent) {
@@ -160,7 +172,13 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>) : ViewModel() {
LocalCache.live.observeForever(cacheListener)
}
fun clear() {
lastAccount = null
lastNotes = null
}
override fun onCleared() {
clear()
LocalCache.live.removeObserver(cacheListener)
super.onCleared()
}
@@ -11,6 +11,7 @@ private data class ScrollState(val index: Int, val scrollOffset: Int)
object ScrollStateKeys {
const val GLOBAL_SCREEN = "Global"
const val NOTIFICATION_SCREEN = "Notifications"
val HOME_FOLLOWS = Route.Home.base + "Follows"
val HOME_REPLIES = Route.Home.base + "FollowsReplies"
}
@@ -61,6 +61,7 @@ import com.vitorpamplona.amethyst.ui.note.*
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
import com.vitorpamplona.amethyst.ui.note.BlankNote
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
import com.vitorpamplona.amethyst.ui.note.DisplayPoW
import com.vitorpamplona.amethyst.ui.note.DisplayReward
import com.vitorpamplona.amethyst.ui.note.DisplayUncitedHashtags
import com.vitorpamplona.amethyst.ui.note.HiddenNote
@@ -286,6 +287,11 @@ fun NoteMaster(
if (baseReward != null) {
DisplayReward(baseReward, baseNote, account, navController)
}
val pow = noteEvent.getPoWRank()
if (pow > 20) {
DisplayPoW(pow)
}
}
}
}
@@ -60,7 +60,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return
}
val zapRequest = account.createZapRequestFor(note, pollOption)
val zapRequest = account.createZapRequestFor(note, pollOption, message)
onProgress(0.10f)
@@ -11,7 +11,11 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource
@@ -28,6 +32,7 @@ import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
import com.vitorpamplona.amethyst.ui.screen.FeedView
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
@@ -42,10 +47,12 @@ fun HomeScreen(
accountViewModel: AccountViewModel,
navController: NavController,
pagerState: PagerState,
scrollToTop: Boolean = false
scrollToTop: Boolean = false,
nip47: String? = null
) {
val coroutineScope = rememberCoroutineScope()
val account = accountViewModel.accountLiveData.value?.account ?: return
var wantsToAddNip47 by remember { mutableStateOf<String?>(nip47) }
LaunchedEffect(accountViewModel) {
HomeNewThreadFeedFilter.account = account
@@ -55,6 +62,10 @@ fun HomeScreen(
repliesFeedViewModel.invalidateData()
}
if (wantsToAddNip47 != null) {
UpdateZapAmountDialog({ wantsToAddNip47 = null }, account = account, wantsToAddNip47)
}
val lifeCycleOwner = LocalLifecycleOwner.current
DisposableEffect(accountViewModel) {
val observer = LifecycleEventObserver { _, event ->
@@ -18,12 +18,22 @@ import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.CardFeedView
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
@Composable
fun NotificationScreen(notifFeedViewModel: NotificationViewModel, accountViewModel: AccountViewModel, navController: NavController) {
fun NotificationScreen(
notifFeedViewModel: NotificationViewModel,
accountViewModel: AccountViewModel,
navController: NavController,
scrollToTop: Boolean = false
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
if (scrollToTop) {
notifFeedViewModel.clear()
}
LaunchedEffect(accountViewModel) {
NotificationFeedFilter.account = account
notifFeedViewModel.invalidateData()
@@ -48,7 +58,14 @@ fun NotificationScreen(notifFeedViewModel: NotificationViewModel, accountViewMod
Column(
modifier = Modifier.padding(vertical = 0.dp)
) {
CardFeedView(notifFeedViewModel, accountViewModel = accountViewModel, navController, Route.Notification.route)
CardFeedView(
viewModel = notifFeedViewModel,
accountViewModel = accountViewModel,
navController = navController,
routeForLastRead = Route.Notification.base,
scrollStateKey = ScrollStateKeys.NOTIFICATION_SCREEN,
scrollToTop = scrollToTop
)
}
}
}
@@ -430,12 +430,13 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
fontSize = 25.sp
)
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
user.bestUsername()?.let {
Text(
"@$it",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp)
)
}
}
@@ -140,6 +140,9 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
val onlineSearch = NostrSearchEventOrUserDataSource
val dbState = LocalCache.live.observeAsState()
val db = dbState.value ?: return
val isTrailingIconVisible by remember {
derivedStateOf {
searchValue.isNotBlank()
@@ -151,6 +154,18 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
CoroutineChannel<String>(CoroutineChannel.CONFLATED)
}
LaunchedEffect(db) {
if (searchValue.length > 1) {
withContext(Dispatchers.IO) {
searchResults.value = LocalCache.findUsersStartingWith(searchValue)
searchResultsNotes.value =
LocalCache.findNotesStartingWith(searchValue).sortedBy { it.createdAt() }
.reversed()
searchResultsChannels.value = LocalCache.findChannelsStartingWith(searchValue)
}
}
}
LaunchedEffect(Unit) {
// Wait for text changes to stop for 300 ms before firing off search.
withContext(Dispatchers.IO) {
@@ -161,7 +176,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
.collectLatest {
hashtagResults.value = findHashtags(it)
if (it.removePrefix("npub").removePrefix("note").length >= 4) {
if (it.length >= 4) {
onlineSearch.search(it.trim())
}
+58
View File
@@ -0,0 +1,58 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="489dp"
android:height="489dp"
android:viewportWidth="489"
android:viewportHeight="489">
<path
android:pathData="M89.58,181.77C96.83,182.51 103.84,181.66 110.26,179.49L110.42,179.63L110.66,179.87C83.35,210.92 64.09,251.1 56.14,295.56C50.5,327.03 67.39,357.29 95.35,370.61C140.92,392.29 191.9,404.42 245.64,404.42C299.39,404.42 349.32,392.54 394.53,371.27C422.59,358.07 439.6,327.82 434.06,296.31C426.2,251.6 406.52,211.48 379.48,179.96C345.33,140.15 379.58,179.8 379.58,179.8C379.58,179.8 391.32,182.34 397.62,181.9C421.93,180.22 441.52,160.37 442.89,136.03C444.51,106.88 420.52,82.9 391.36,84.54C367.36,85.89 347.63,105.01 345.58,128.97C344.95,136.44 346.01,143.62 348.41,150.15L348.02,150.54L347.79,150.77C318.32,129.03 283.17,116.22 245.05,116.22C206.93,116.22 171.54,129.12 141.99,151.01L141.77,150.78L141.4,150.42L140.69,149.71C143.01,143.22 144.01,136.11 143.33,128.71C141.16,104.88 121.47,85.9 97.57,84.55C68.36,82.92 44.37,106.91 46.01,136.09C47.34,159.78 65.97,179.34 89.56,181.78L89.58,181.77Z"
android:strokeWidth="19.962"
android:fillColor="#ffffff"
android:strokeColor="#ffffff"/>
<path
android:pathData="M104.48,351.42C84.72,342.01 73.22,320.89 77.07,299.33C85.11,254.32 105.57,214.8 133.95,186.08C138.28,181.69 142.81,177.57 147.49,173.7C175.19,150.87 208.78,137.51 244.99,137.51C281.2,137.51 314.55,150.78 342.19,173.45C346.89,177.3 351.41,181.43 355.76,185.81C384.43,214.69 405.06,254.58 413.04,300.02C416.84,321.6 405.24,342.72 385.41,352.05C342.98,372.01 295.59,383.17 245.6,383.17C195.6,383.17 147.22,371.79 104.45,351.42H104.48Z"
android:fillColor="#FFDF6F"
android:fillType="evenOdd"/>
<path
android:pathData="M95.07,121.28L82.11,134.24L112.95,165.08C118.32,161.99 122.85,157.57 126.06,152.26L95.07,121.28Z"
android:fillColor="#000000"/>
<path
android:pathData="M394.79,96.61C374.95,96.28 357.68,113.06 357.49,132.9C357.41,140.15 359.45,146.93 363.03,152.66L394.39,121.29L407.36,134.26L376.3,165.3C382.44,168.73 389.68,170.45 397.35,169.8C415.01,168.3 429.27,153.98 430.7,136.32C432.42,114.9 415.73,96.96 394.79,96.61ZM363.03,152.66L348.73,166.94C353.43,170.82 357.94,174.97 362.27,179.33L376.3,165.3C370.87,162.28 366.3,157.91 363.03,152.66ZM422.09,298.44C413.84,251.46 392.38,209.71 362.27,179.33L355.79,185.81C351.44,181.43 346.92,177.31 342.22,173.47L348.73,166.94C319.53,142.71 283.85,128.37 245.01,128.37C206.16,128.37 170.25,142.8 140.99,167.2L147.51,173.71C142.81,177.57 138.3,181.71 133.97,186.1L127.49,179.62C97.66,209.82 76.37,251.19 68.04,297.74C63.44,323.55 77.25,348.61 100.54,359.7C144.51,380.63 193.71,392.35 245.63,392.35C297.54,392.35 345.72,380.87 389.35,360.35C412.72,349.34 426.65,324.29 422.09,298.44ZM381.54,343.75C340.31,363.17 294.24,374.01 245.63,374.01C197.01,374.01 149.99,362.94 108.42,343.15C92.2,335.42 83,318.23 86.09,300.97C93.86,257.51 113.49,219.81 140.45,192.57L154.03,180.24C180.14,158.99 211.55,146.69 245.01,146.69C278.46,146.69 309.63,158.88 335.69,179.98L349.31,192.29C376.54,219.69 396.33,257.73 404.04,301.61C407.08,318.92 397.82,336.11 381.54,343.75ZM126.08,152.28C122.87,157.58 118.34,161.99 112.96,165.09L127.49,179.62C131.82,175.23 136.31,171.09 140.99,167.2L126.08,152.28ZM126.08,152.28C129.56,146.54 131.51,139.77 131.38,132.56C130.98,112.5 114.04,96.21 93.97,96.61C73.05,97.03 56.44,114.95 58.17,136.32C59.61,154.07 74.01,168.43 91.76,169.82C99.51,170.42 106.8,168.61 112.96,165.09L82.13,134.26L95.09,121.29L126.08,152.28Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M407.36,134.26L376.3,165.3C370.87,162.28 366.3,157.91 363.03,152.66L394.39,121.29L407.36,134.26Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M362.27,179.33L355.79,185.81C351.44,181.43 346.92,177.32 342.22,173.47L348.73,166.94C353.43,170.82 357.94,174.97 362.27,179.33Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M147.51,173.71C142.81,177.57 138.3,181.71 133.97,186.1L127.49,179.62C131.82,175.23 136.31,171.09 140.99,167.2L147.51,173.71Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M126.08,152.28C122.87,157.58 118.34,161.99 112.96,165.09L82.13,134.26L95.09,121.29L126.08,152.28Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M407.36,134.24L376.3,165.29C370.87,162.26 366.3,157.9 363.03,152.64L394.39,121.28L407.36,134.24Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M348.73,166.93L342.2,173.45C346.9,177.3 351.43,181.43 355.77,185.81L362.26,179.33C357.92,174.95 353.41,170.82 348.73,166.94V166.93Z"
android:fillColor="#000000"/>
<path
android:pathData="M140.98,167.18C136.3,171.07 131.8,175.23 127.47,179.6L133.95,186.08C138.28,181.69 142.81,177.57 147.49,173.7L140.98,167.18Z"
android:fillColor="#000000"/>
<path
android:pathData="M140.37,329.06C124.47,322.58 115.04,305.67 120.55,289.4C137.54,239.24 187.12,202.92 245.64,202.92C304.16,202.92 353.74,239.23 370.75,289.4C376.27,305.67 366.83,322.6 350.93,329.06C318.44,342.3 282.9,349.58 245.66,349.58C208.42,349.58 172.87,342.3 140.39,329.06H140.37Z"
android:fillColor="#000000"
android:fillType="evenOdd"/>
<path
android:pathData="M287.82,305.59C304.7,305.59 318.38,294.65 318.38,281.15C318.38,267.65 304.7,256.7 287.82,256.7C270.94,256.7 257.26,267.65 257.26,281.15C257.26,294.65 270.94,305.59 287.82,305.59Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M200.35,305.61C217.22,305.61 230.91,294.66 230.91,281.16C230.91,267.66 217.22,256.72 200.35,256.72C183.47,256.72 169.78,267.66 169.78,281.16C169.78,294.66 183.47,305.61 200.35,305.61Z"
android:fillColor="#ffffff"/>
</vector>
+81
View File
@@ -22,6 +22,7 @@
<string name="copy_user_pubkey">A felhasználó PubKulcs másolása</string>
<string name="copy_note_id">A bejegyzés azonosítójának másolása</string>
<string name="broadcast">Közvetít</string>
<string name="request_deletion">Törlés Kérése</string>
<string name="block_hide_user"><![CDATA[Tiltás és Felhasználó elrejtése]]></string>
<string name="report_spam_scam">Spam / Csalás bejelentés</string>
<string name="report_impersonation">Megszemélyesítés bejelentés</string>
@@ -72,6 +73,7 @@
<string name="failed_to_upload_the_image">Nem sikerült feltölteni a képet</string>
<string name="relay_address">Csomópont Címe</string>
<string name="posts">Hozzászólások</string>
<string name="bytes">Bájtok</string>
<string name="errors">Hibák</string>
<string name="home_feed">Hírfolyamod</string>
<string name="private_message_feed">Privát Üzenetek listája</string>
@@ -119,6 +121,7 @@
<string name="send_a_direct_message">Közvetlen üzenet küldése</string>
<string name="edits_the_user_s_metadata">Szerkeszti a felhasználó metaadatait</string>
<string name="follow">Követés</string>
<string name="follow_back">Visszakövetem</string>
<string name="unblock">Tiltás feloldása</string>
<string name="copy_user_id">Felhasználói azonosító másolása</string>
<string name="unblock_user">Felhasználó feloldása</string>
@@ -172,6 +175,7 @@
<string name="mark_all_known_as_read">Az összes ismert megjelölése olvasottként</string>
<string name="mark_all_new_as_read">Az összes új megjelölése olvasottként</string>
<string name="mark_all_as_read">Összes megjelölése olvasottként</string>
<string name="backup_keys">Biztonsági Kulcsok</string>
<string name="account_backup_tips_md">
## Kulcs- és biztonsági mentési tippek
\n\nFiókját titkos kulcs védi. A kulcs egy hosszú véletlenszerű karakterlánc, amely **nsec1**-el kezdődik. Bárki, aki az Ön titkos kulcsához hozzáfér, az Ön személyazonosságának használatával bármilyen tartalmat közzétehet.
@@ -187,4 +191,81 @@
<string name="badge_award_image_for">"Kitűző %1$s"</string>
<string name="new_badge_award_notif">Új Jelvényt kapott</string>
<string name="award_granted_to">Kitüntetésben részesült</string>
<string name="copied_note_text_to_clipboard">Bejegyzés vágólapra helyezve</string>
<string name="copied_user_id_to_clipboard">A szerző @npub-ja vágólapra helyezve</string>
<string name="copied_note_id_to_clipboard">A bejegyzés azonosítója (@note1) vágólapra helyezve</string>
<string name="select_text_dialog_top">Szöveg Kijelölése</string>
<string name="github" translatable="false">Github Gist w/ Proof</string>
<string name="telegram" translatable="false">Telegram</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;Nem sikerült a privát üzenetet visszafejteni&gt;\n\nTéged egy privát/titkosított beszélgetésben %1$s és %2$s idéztek."</string>
<string name="account_switch_add_account_dialog_title">Új Fiók Hozzáadása</string>
<string name="drawer_accounts">Fiókok</string>
<string name="account_switch_select_account">Fiók Kiválasztása</string>
<string name="account_switch_add_account_btn">Új Fiók Hozzáadása</string>
<string name="account_switch_active_account">Aktív fiók</string>
<string name="account_switch_has_private_key">Tartalmaz privát kulcsot</string>
<string name="account_switch_pubkey_only">Csak olvasásra, nincs privát kulcs</string>
<string name="back">Vissza</string>
<string name="quick_action_select">Kiválaszt</string>
<string name="quick_action_share_browser_link">Link megosztása</string>
<string name="quick_action_share">Megosztás</string>
<string name="quick_action_copy_user_id">Szerző azonosító</string>
<string name="quick_action_copy_note_id">Bejegyzés azonosító</string>
<string name="quick_action_copy_text">Szöveg Másolása</string>
<string name="quick_action_delete">Törlés</string>
<string name="quick_action_unfollow">Kikövetés</string>
<string name="quick_action_follow">Követem</string>
<string name="quick_action_request_deletion_alert_title">Törlés Kérése</string>
<string name="quick_action_request_deletion_alert_body">Az Amethyst kérni fogja, hogy töröljék a bejegyzésed azokról a csomópontokról, amelyekhez jelenleg csatlakozol. Nincs garancia arra, hogy a bejegyzésed ezekről a csomópontokról vagy más közvetítőkből (ahol tárolni lehet) véglegesen törlődik.</string>
<string name="quick_action_block_dialog_btn">Tiltás</string>
<string name="quick_action_delete_dialog_btn">Törlés</string>
<string name="quick_action_block">Tiltás</string>
<string name="quick_action_report">Jelentés</string>
<string name="quick_action_delete_button">Törlés</string>
<string name="quick_action_dont_show_again_button">Ne mutasd újra</string>
<string name="report_dialog_spam">Spam vagy átverés</string>
<string name="report_dialog_profanity">Trágárság vagy gyűlöletkeltő magatartás</string>
<string name="report_dialog_impersonation">Rosszindulatú megszemélyesítés</string>
<string name="report_dialog_nudity">Meztelenség vagy pornográf tartalom</string>
<string name="report_dialog_illegal">Illegális viselkedés</string>
<string name="report_dialog_blocking_a_user">Az alkalmazásban egy felhasználó letiltása minden hozzá köthető tartalmat elrejt. Jegyzeteid továbbra is nyilvánosan megtekinthetők, beleértve a letiltott személyek számára is. A letiltott felhasználók a Biztonsági szűrők felületen jelennek meg.</string>
<string name="report_dialog_block_hide_user_btn"><![CDATA[Felhasználó Tiltása és Elrejtése]]></string>
<string name="report_dialog_report_btn">Visszaélés jelentése</string>
<string name="report_dialog_reminder_public">Minden közzétett jelentés nyilvánosan látható lesz.</string>
<string name="report_dialog_additional_reason_placeholder">Opcionálisan a jelentéséhez további kontextust addhat…</string>
<string name="report_dialog_additional_reason_label">További kontextus</string>
<string name="report_dialog_select_reason_label">Ok</string>
<string name="report_dialog_select_reason_placeholder">Válassz egy okot…</string>
<string name="report_dialog_post_report_btn">Jelentés Elküldése</string>
<string name="report_dialog_title">Tiltás és Jelentés</string>
<string name="block_only">Tiltás</string>
<string name="bookmarks">Könyvjelzők</string>
<string name="private_bookmarks">Privát Könyvjelzők</string>
<string name="public_bookmarks">Publikus Könyvjelzők</string>
<string name="add_to_private_bookmarks">Hozzáadás a Privát Könyvjelzőimhez</string>
<string name="add_to_public_bookmarks">Hozzáadás a Publikus Könyvjelzőimhez</string>
<string name="remove_from_private_bookmarks">Törlés a Privát Könyvjelzőimből</string>
<string name="remove_from_public_bookmarks">Törlés a Publikus Könyvjelzőimből</string>
<string name="wallet_connect_service">Wallet Connect Szolgáltatás</string>
<string name="wallet_connect_service_explainer">Engedélyezed a Nostr Secret-et, hogy az alkalmazás elhagyása nélkül Zap-ot küldjön. Tartsa a titkot biztonságban, és ha lehetséges, privát csomópontot használjon</string>
<string name="wallet_connect_service_pubkey">Wallet Connect Publikus Kulcs</string>
<string name="wallet_connect_service_relay">Wallet Connect Csomópont</string>
<string name="wallet_connect_service_secret">Wallet Connect Titok</string>
<string name="wallet_connect_service_show_secret">Titkos Kulcs Megjelenítése</string>
<string name="wallet_connect_service_secret_placeholder">nsec / hex privát kulcs</string>
<string name="pledge_amount_in_sats">Hozzájárulás összege sats-ban</string>
<string name="looking_for_event">"%1$s esemény keresése"</string>
<string name="custom_zaps_add_a_message">Nyilvános üzenet hozzáadása</string>
<string name="custom_zaps_add_a_message_example">Köszönöm a kemény munkát!</string>
</resources>
+3
View File
@@ -285,4 +285,7 @@
<string name="one_vote_per_user_on_atomic_votes">Only one vote per user is allowed on this type of poll</string>
<string name="looking_for_event">"Looking for Event %1$s"</string>
<string name="custom_zaps_add_a_message">Add a public message</string>
<string name="custom_zaps_add_a_message_example">Thank you for all your work!</string>
</resources>
+5 -4
View File
@@ -1,11 +1,12 @@
buildscript {
ext {
fragment_version = "1.5.5"
lifecycle_version = '2.6.0'
compose_ui_version = '1.4.0-rc01'
fragment_version = "1.5.6"
lifecycle_version = '2.6.1'
compose_ui_version = '1.5.0-alpha01'
nav_version = "2.5.3"
room_version = "2.4.3"
accompanist_version = "0.28.0"
accompanist_version = '0.30.0'
coil_version = '2.3.0'
}
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {