Porównaj commity

...

42 Commity

Autor SHA1 Wiadomość Data
Vitor Pamplona 0854bd34ff v0.86.2 2024-04-05 14:08:27 -04:00
Vitor Pamplona 1738a775ef Fixes the lack of the Comparator interface for the Deletion Index. 2024-04-05 14:05:04 -04:00
Vitor Pamplona a6953872ea Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-05 11:39:34 -04:00
Vitor Pamplona d48714456c Fixes check for Deleted Addressable events. 2024-04-05 11:34:50 -04:00
Vitor Pamplona c25aad482b
Merge pull request #829 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-05 11:23:19 -04:00
Crowdin Bot cbebfd263b New Crowdin translations by GitHub Action 2024-04-05 15:21:28 +00:00
Vitor Pamplona 89dbe82191 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-05 11:19:23 -04:00
Vitor Pamplona 7bb72d0c2d Adds a Deletion event cache. 2024-04-05 11:19:15 -04:00
Vitor Pamplona 9be4895080
Merge pull request #828 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-05 08:19:17 -04:00
Crowdin Bot 6250db01d1 New Crowdin translations by GitHub Action 2024-04-05 12:05:47 +00:00
Vitor Pamplona 48f9045f1b
Merge pull request #825 from greenart7c3/onion_url
add ws:// if not present in .onion urls
2024-04-05 08:03:52 -04:00
Vitor Pamplona 818ca7e39e
Merge pull request #826 from greenart7c3/draft_decryption_error
fix draft decryption error
2024-04-05 08:03:39 -04:00
Vitor Pamplona e8675b8e45
Merge pull request #827 from davotoula/update_translations
added  translations for CS, DE, SV
2024-04-05 08:03:11 -04:00
David Kaspar cef7e17447 added translations for CS, DE, SV 2024-04-05 11:38:51 +01:00
greenart7c3 6b15a0db8e fix draft decryption error 2024-04-05 05:41:03 -03:00
greenart7c3 50c5845a11 add ws:// if not present in .onion urls 2024-04-05 05:30:51 -03:00
Vitor Pamplona 1b7ba3de01 Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-04 23:06:08 -04:00
Vitor Pamplona 712063f5d2 Avoids double notifications. 2024-04-04 23:05:58 -04:00
Vitor Pamplona d92f23e274 LargeCache inner map should be private 2024-04-04 23:05:35 -04:00
Vitor Pamplona 3b7f530c0b
Merge pull request #824 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-04 18:15:14 -04:00
Crowdin Bot 623a8d377c New Crowdin translations by GitHub Action 2024-04-04 22:08:10 +00:00
Vitor Pamplona 79489d0b07 v0.86.1 2024-04-04 18:06:36 -04:00
Vitor Pamplona 827512b225 Avoids circular rendering of Drafts. 2024-04-04 18:06:27 -04:00
Vitor Pamplona 6acfadeb9b Reduces cache for expandable texts. 2024-04-04 18:06:09 -04:00
Vitor Pamplona e159af2cd7 v0.86.0 2024-04-04 17:21:03 -04:00
Vitor Pamplona 89c2e9d2e0 Changes precision of Zap Splits to 1% steps 2024-04-04 17:19:03 -04:00
Vitor Pamplona 06f6ab6719 Adds button to load Zap Splits from the cited users in the text 2024-04-04 17:18:42 -04:00
Vitor Pamplona 98c48e8b6b Fixes contract violation when sorting users. 2024-04-04 16:54:14 -04:00
Vitor Pamplona 25cde455d8 removes chat bubble animation when size changes 2024-04-04 15:58:19 -04:00
Vitor Pamplona ef0fdf553c Merge branch 'main' of https://github.com/vitorpamplona/amethyst 2024-04-04 15:24:59 -04:00
Vitor Pamplona 719b950272 Fixing the use of filters that didn't discriminate the relay type setup 2024-04-04 15:24:04 -04:00
Vitor Pamplona 2d02fad6b9
Merge pull request #823 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-04 15:16:49 -04:00
Crowdin Bot a39db5bf7b New Crowdin translations by GitHub Action 2024-04-04 19:01:46 +00:00
Vitor Pamplona 7fd37367fc refactoring of cache methods in GiftWraps 2024-04-04 14:59:49 -04:00
Vitor Pamplona e1c134830e Avoiding feed jitter when pressing the notification button twice. 2024-04-04 10:03:28 -04:00
Vitor Pamplona 621d1c7731 Migrating Refreshable feeds to the reusable box 2024-04-04 09:41:51 -04:00
Vitor Pamplona 7475143506 Continues the migration from LiveData to Flow 2024-04-04 08:56:15 -04:00
Vitor Pamplona 2509d639bd
Merge pull request #822 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2024-04-03 13:07:08 -04:00
Crowdin Bot 85dd5cf698 New Crowdin translations by GitHub Action 2024-04-03 17:02:53 +00:00
Vitor Pamplona 274ce6ad77 Fixing the spacing for channels 2024-04-03 12:26:57 -04:00
Vitor Pamplona 0e8d2fc33a adds save when closing the screen on new posts. 2024-04-03 10:20:19 -04:00
Vitor Pamplona b88723b68b Fixes wrong refactoring 2024-04-03 10:20:05 -04:00
39 zmienionych plików z 482 dodań i 327 usunięć

Wyświetl plik

@ -12,9 +12,9 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 34
versionCode 362
versionName "0.85.3"
buildConfigField "String", "RELEASE_NOTES_ID", "\"d8da33fd13d129d86c53564aedefafbe3716f007c520431be4a8e488d3925afb\""
versionCode 365
versionName "0.86.2"
buildConfigField "String", "RELEASE_NOTES_ID", "\"a704a11334ed4fe6fc6ee6f8856f6f005da33644770616f1437f8b2b488b52b1\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {

Wyświetl plik

@ -108,11 +108,15 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combineTransform
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flattenMerge
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
@ -216,179 +220,114 @@ class Account(
val communities: ImmutableSet<String> = persistentSetOf(),
)
class ListNameNotePair(val listName: String, val event: GeneralListEvent?)
@OptIn(ExperimentalCoroutinesApi::class)
val liveKind3Follows: StateFlow<LiveFollowLists> by lazy {
userProfile()
.live()
.follows
.asFlow()
.transformLatest {
emit(
LiveFollowLists(
userProfile().cachedFollowingKeySet().toImmutableSet(),
userProfile().cachedFollowingTagSet().toImmutableSet(),
userProfile().cachedFollowingGeohashSet().toImmutableSet(),
userProfile().cachedFollowingCommunitiesSet().toImmutableSet(),
),
)
}
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
val liveKind3FollowsFlow: Flow<LiveFollowLists> =
userProfile().flow().follows.stateFlow.transformLatest {
emit(
LiveFollowLists(
it.user.cachedFollowingKeySet().toImmutableSet(),
it.user.cachedFollowingTagSet().toImmutableSet(),
it.user.cachedFollowingGeohashSet().toImmutableSet(),
it.user.cachedFollowingCommunitiesSet().toImmutableSet(),
),
)
}
val liveKind3Follows = liveKind3FollowsFlow.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
@OptIn(ExperimentalCoroutinesApi::class)
private val liveHomeList: Flow<ListNameNotePair> by lazy {
defaultHomeFollowList.flatMapLatest { listName ->
loadPeopleListFlowFromListName(listName)
}
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveHomeList: StateFlow<NoteState?> by lazy {
defaultHomeFollowList
.transformLatest {
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
emit(it)
}
fun loadPeopleListFlowFromListName(listName: String): Flow<ListNameNotePair> {
return if (listName != GLOBAL_FOLLOWS && listName != KIND3_FOLLOWS) {
val note = LocalCache.checkGetOrCreateAddressableNote(listName)
note?.flow()?.metadata?.stateFlow?.mapLatest {
val noteEvent = it.note.event as? GeneralListEvent
ListNameNotePair(listName, noteEvent)
} ?: MutableStateFlow(ListNameNotePair(listName, null))
} else {
MutableStateFlow(ListNameNotePair(listName, null))
}
}
fun combinePeopleListFlows(
kind3FollowsSource: Flow<LiveFollowLists>,
peopleListFollowsSource: Flow<ListNameNotePair>,
): Flow<LiveFollowLists?> {
return combineTransform(kind3FollowsSource, peopleListFollowsSource) { kind3Follows, peopleListFollows ->
if (peopleListFollows.listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (peopleListFollows.listName == KIND3_FOLLOWS) {
emit(kind3Follows)
} else if (peopleListFollows.event == null) {
emit(LiveFollowLists())
} else {
val result = waitToDecrypt(peopleListFollows.event)
if (result == null) {
emit(LiveFollowLists())
} else {
emit(result)
}
}
.flattenMerge()
.stateIn(scope, SharingStarted.Eagerly, null)
}
}
val liveHomeFollowLists: StateFlow<LiveFollowLists?> by lazy {
combineTransform(defaultHomeFollowList, liveKind3Follows, liveHomeList) {
listName,
kind3Follows,
peopleListFollows,
->
if (listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (listName == KIND3_FOLLOWS) {
emit(kind3Follows)
} else {
val result =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) { continuation.resume(it) }
}
}
result?.let { emit(it) } ?: run { emit(LiveFollowLists()) }
}
}
combinePeopleListFlows(liveKind3FollowsFlow, liveHomeList)
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveNotificationList: StateFlow<NoteState?> by lazy {
private val liveNotificationList: Flow<ListNameNotePair> by lazy {
defaultNotificationFollowList
.transformLatest {
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
emit(it)
}
}
}
.flattenMerge()
.stateIn(scope, SharingStarted.Eagerly, null)
.transformLatest { listName ->
emit(loadPeopleListFlowFromListName(listName))
}.flattenMerge()
}
val liveNotificationFollowLists: StateFlow<LiveFollowLists?> by lazy {
combineTransform(defaultNotificationFollowList, liveKind3Follows, liveNotificationList) {
listName,
kind3Follows,
peopleListFollows,
->
if (listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (listName == KIND3_FOLLOWS) {
emit(kind3Follows)
} else {
val result =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) { continuation.resume(it) }
}
}
result?.let { emit(it) } ?: run { emit(LiveFollowLists()) }
}
}
combinePeopleListFlows(liveKind3FollowsFlow, liveNotificationList)
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveStoriesList: StateFlow<NoteState?> by lazy {
private val liveStoriesList: Flow<ListNameNotePair> by lazy {
defaultStoriesFollowList
.transformLatest {
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
emit(it)
}
}
}
.flattenMerge()
.stateIn(scope, SharingStarted.Eagerly, null)
.transformLatest { listName ->
emit(loadPeopleListFlowFromListName(listName))
}.flattenMerge()
}
val liveStoriesFollowLists: StateFlow<LiveFollowLists?> by lazy {
combineTransform(defaultStoriesFollowList, liveKind3Follows, liveStoriesList) {
listName,
kind3Follows,
peopleListFollows,
->
if (listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (listName == KIND3_FOLLOWS) {
emit(kind3Follows)
} else {
val result =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) { continuation.resume(it) }
}
}
result?.let { emit(it) } ?: run { emit(LiveFollowLists()) }
}
}
combinePeopleListFlows(liveKind3FollowsFlow, liveStoriesList)
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
}
@OptIn(ExperimentalCoroutinesApi::class)
private val liveDiscoveryList: StateFlow<NoteState?> by lazy {
private val liveDiscoveryList: Flow<ListNameNotePair> by lazy {
defaultDiscoveryFollowList
.transformLatest {
if (it != GLOBAL_FOLLOWS && it != KIND3_FOLLOWS) {
LocalCache.checkGetOrCreateAddressableNote(it)?.flow()?.metadata?.stateFlow?.let {
emit(it)
}
}
}
.flattenMerge()
.stateIn(scope, SharingStarted.Eagerly, null)
.transformLatest { listName ->
emit(loadPeopleListFlowFromListName(listName))
}.flattenMerge()
}
val liveDiscoveryFollowLists: StateFlow<LiveFollowLists?> by lazy {
combineTransform(defaultDiscoveryFollowList, liveKind3Follows, liveDiscoveryList) {
listName,
kind3Follows,
peopleListFollows,
->
if (listName == GLOBAL_FOLLOWS) {
emit(null)
} else if (listName == KIND3_FOLLOWS) {
emit(kind3Follows)
} else {
val result =
withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) { continuation.resume(it) }
}
}
result?.let { emit(it) } ?: run { emit(LiveFollowLists()) }
}
}
combinePeopleListFlows(liveKind3FollowsFlow, liveDiscoveryList)
.stateIn(scope, SharingStarted.Eagerly, LiveFollowLists())
}
private fun decryptLiveFollows(
peopleListFollows: NoteState?,
listEvent: GeneralListEvent,
onReady: (LiveFollowLists) -> Unit,
) {
val listEvent = (peopleListFollows?.note?.event as? GeneralListEvent)
listEvent?.privateTags(signer) { privateTagList ->
listEvent.privateTags(signer) { privateTagList ->
onReady(
LiveFollowLists(
users =
@ -406,6 +345,16 @@ class Account(
}
}
suspend fun waitToDecrypt(peopleListFollows: GeneralListEvent): LiveFollowLists? {
return withTimeoutOrNull(1000) {
suspendCancellableCoroutine { continuation ->
decryptLiveFollows(peopleListFollows) {
continuation.resume(it)
}
}
}
}
@Immutable
data class LiveHiddenUsers(
val hiddenUsers: ImmutableSet<String>,

Wyświetl plik

@ -24,6 +24,7 @@ import android.util.Log
import android.util.LruCache
import androidx.compose.runtime.Stable
import com.vitorpamplona.amethyst.Amethyst
import com.vitorpamplona.amethyst.commons.data.DeletionIndex
import com.vitorpamplona.amethyst.commons.data.LargeCache
import com.vitorpamplona.amethyst.service.checkNotInMainThread
import com.vitorpamplona.amethyst.service.relays.Relay
@ -133,6 +134,8 @@ object LocalCache {
val channels = LargeCache<HexKey, Channel>()
val awaitingPaymentRequests = ConcurrentHashMap<HexKey, Pair<Note?, (LnZapPaymentResponseEvent) -> Unit>>(10)
val deletionIndex = DeletionIndex()
fun checkGetOrCreateUser(key: String): User? {
// checkNotInMainThread()
@ -956,52 +959,53 @@ object LocalCache {
}
fun consume(event: DeletionEvent) {
var deletedAtLeastOne = false
if (deletionIndex.add(event)) {
var deletedAtLeastOne = false
event
.deleteEvents()
.mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add
deleteNote(deleteNote)
event.deleteEvents()
.mapNotNull { getNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey) {
// reverts the add
deleteNote(deleteNote)
deletedAtLeastOne = true
deletedAtLeastOne = true
}
}
val addressList = event.deleteAddressTags()
val addressSet = addressList.toSet()
addressList
.mapNotNull { getAddressableNoteIfExists(it) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) <= event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
addressables.remove(deleteNote.idHex)
deletedAtLeastOne = true
}
}
notes.forEach { key, note ->
val noteEvent = note.event
if (noteEvent is AddressableEvent && noteEvent.addressTag() in addressSet) {
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
deleteNote(note)
deletedAtLeastOne = true
}
}
}
val addressList = event.deleteAddresses()
val addressSet = addressList.toSet()
addressList
.mapNotNull { getAddressableNoteIfExists(it.toTag()) }
.forEach { deleteNote ->
// must be the same author
if (deleteNote.author?.pubkeyHex == event.pubKey && (deleteNote.createdAt() ?: 0) < event.createdAt) {
// Counts the replies
deleteNote(deleteNote)
addressables.remove(deleteNote.idHex)
deletedAtLeastOne = true
}
if (deletedAtLeastOne) {
val note = Note(event.id)
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
refreshObservers(note)
}
notes.forEach { key, note ->
val noteEvent = note.event
if (noteEvent is AddressableEvent && noteEvent.address() in addressSet) {
if (noteEvent.pubKey() == event.pubKey && noteEvent.createdAt() <= event.createdAt) {
deleteNote(note)
deletedAtLeastOne = true
}
}
}
if (deletedAtLeastOne) {
val note = Note(event.id)
note.loadEvent(event, getOrCreateUser(event.pubKey), emptyList())
refreshObservers(note)
}
}
@ -2210,6 +2214,8 @@ object LocalCache {
event: Event,
relay: Relay?,
) {
if (deletionIndex.hasBeenDeleted(event)) return
checkNotInMainThread()
try {
@ -2306,6 +2312,10 @@ object LocalCache {
e.printStackTrace()
}
}
fun hasConsumed(notificationEvent: Event): Boolean {
return notes.containsKey(notificationEvent.id)
}
}
@Stable

Wyświetl plik

@ -127,6 +127,7 @@ class User(val pubkeyHex: String) {
// Update following of the current user
liveSet?.innerFollows?.invalidateData()
flowSet?.follows?.invalidateData()
// Update Followers of the past user list
// Update Followers of the new contact list
@ -474,14 +475,16 @@ class User(val pubkeyHex: String) {
@Stable
class UserFlowSet(u: User) {
// Observers line up here.
val follows = UserBundledRefresherFlow(u)
val relays = UserBundledRefresherFlow(u)
fun isInUse(): Boolean {
return relays.stateFlow.subscriptionCount.value > 0
return relays.stateFlow.subscriptionCount.value > 0 || follows.stateFlow.subscriptionCount.value > 0
}
fun destroy() {
relays.destroy()
follows.destroy()
}
}

Wyświetl plik

@ -64,15 +64,17 @@ class EventNotificationConsumer(private val applicationContext: Context) {
account: Account,
) {
pushWrappedEvent.cachedGift(account.signer) { notificationEvent ->
LocalCache.justConsume(notificationEvent, null)
unwrapAndConsume(notificationEvent, account) { innerEvent ->
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
if (!LocalCache.hasConsumed(notificationEvent) && LocalCache.justVerify(notificationEvent)) {
unwrapAndConsume(notificationEvent, account) { innerEvent ->
if (!LocalCache.hasConsumed(innerEvent)) {
if (innerEvent is PrivateDmEvent) {
notify(innerEvent, account)
} else if (innerEvent is LnZapEvent) {
notify(innerEvent, account)
} else if (innerEvent is ChatMessageEvent) {
notify(innerEvent, account)
}
}
}
}
}

Wyświetl plik

@ -353,9 +353,14 @@ class Relay(
if (read) {
if (isConnected()) {
if (isReady) {
if (filters.isNotEmpty()) {
val relayFilters =
filters.filter { filter ->
activeTypes.any { it in filter.types }
}
if (relayFilters.isNotEmpty()) {
val request =
filters.joinToStringLimited(
relayFilters.joinToStringLimited(
separator = ",",
limit = 20,
prefix = """["REQ","$requestId",""",
@ -423,12 +428,7 @@ class Relay(
fun renewFilters() {
// Force update all filters after AUTH.
Client.allSubscriptions().forEach {
val filters =
it.value.filter { filter ->
activeTypes.any { it in filter.types }
}
sendFilter(requestId = it.key, filters)
sendFilter(requestId = it.key, it.value)
}
}

Wyświetl plik

@ -81,7 +81,9 @@ object RelayPool : Relay.Listener {
subscriptionId: String,
filters: List<TypedFilter>,
) {
relays.forEach { it.sendFilter(subscriptionId, filters) }
relays.forEach { relay ->
relay.sendFilter(subscriptionId, filters)
}
}
fun connectAndSendFiltersIfDisconnected() {

Wyświetl plik

@ -235,7 +235,12 @@ fun NewPostView(
}
Dialog(
onDismissRequest = { onClose() },
onDismissRequest = {
scope.launch {
postViewModel.sendDraftSync(relayList = relayList)
onClose()
}
},
properties =
DialogProperties(
usePlatformDefaultWidth = false,
@ -294,8 +299,9 @@ fun NewPostView(
Spacer(modifier = StdHorzSpacer)
CloseButton(
onPress = {
postViewModel.cancel()
scope.launch {
postViewModel.sendDraftSync(relayList = relayList)
postViewModel.cancel()
delay(100)
onClose()
}
@ -1096,8 +1102,12 @@ fun FowardZapTo(
text = stringResource(R.string.zap_split_title),
fontSize = 20.sp,
fontWeight = FontWeight.W500,
modifier = Modifier.padding(start = 10.dp),
modifier = Modifier.padding(horizontal = 10.dp).weight(1f),
)
OutlinedButton(onClick = { postViewModel.updateZapFromText() }) {
Text(text = stringResource(R.string.load_from_text))
}
}
HorizontalDivider(thickness = DividerThickness)
@ -1133,7 +1143,7 @@ fun FowardZapTo(
Slider(
value = splitItem.percentage,
onValueChange = { sliderValue ->
val rounded = (round(sliderValue * 20)) / 20.0f
val rounded = (round(sliderValue * 100)) / 100.0f
postViewModel.updateZapPercentage(index, rounded)
},
modifier = Modifier.weight(1.5f),

Wyświetl plik

@ -77,6 +77,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.UUID
enum class UserSuggestionAnchor {
@ -200,12 +201,16 @@ open class NewPostViewModel() : ViewModel() {
val noteAuthor = draft?.author
if (draft != null && noteEvent is DraftEvent && noteAuthor != null) {
accountViewModel.createTempDraftNote(noteEvent, noteAuthor) { innerNote ->
val oldTag = (draft.event as? AddressableEvent)?.dTag()
if (oldTag != null) {
draftTag = oldTag
viewModelScope.launch(Dispatchers.IO) {
accountViewModel.createTempDraftNote(noteEvent) { innerNote ->
if (innerNote != null) {
val oldTag = (draft.event as? AddressableEvent)?.dTag()
if (oldTag != null) {
draftTag = oldTag
}
loadFromDraft(innerNote, accountViewModel)
}
}
loadFromDraft(innerNote, accountViewModel)
}
} else {
originalNote = replyingTo
@ -442,18 +447,22 @@ open class NewPostViewModel() : ViewModel() {
}
fun sendDraft(relayList: List<Relay>? = null) {
viewModelScope.launch(Dispatchers.IO) {
innerSendPost(relayList, draftTag)
viewModelScope.launch {
sendDraftSync(relayList)
}
}
suspend fun sendDraftSync(relayList: List<Relay>? = null) {
innerSendPost(relayList, draftTag)
}
private suspend fun innerSendPost(
relayList: List<Relay>? = null,
localDraft: String?,
) {
) = withContext(Dispatchers.IO) {
if (accountViewModel == null) {
cancel()
return
return@withContext
}
val tagger = NewMessageTagger(message.text, pTags, eTags, originalNote?.channelHex(), accountViewModel!!)
@ -919,6 +928,7 @@ open class NewPostViewModel() : ViewModel() {
compareBy(
{ account?.isFollowing(it) },
{ it.toBestDisplayName() },
{ it.pubkeyHex },
),
)
.reversed()
@ -928,7 +938,6 @@ open class NewPostViewModel() : ViewModel() {
userSuggestions = emptyList()
}
}
saveDraft()
}
open fun autocompleteWithUser(item: User) {
@ -947,16 +956,6 @@ open class NewPostViewModel() : ViewModel() {
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.FORWARD_ZAPS) {
forwardZapTo.addItem(item)
forwardZapToEditting = TextFieldValue("")
/*
val lastWord = forwardZapToEditting.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
val lastWordStart = it.end - lastWord.length
val wordToInsert = "@${item.pubkeyNpub()}"
forwardZapTo = item
forwardZapToEditting = TextFieldValue(
forwardZapToEditting.text.replaceRange(lastWordStart, it.end, wordToInsert),
TextRange(lastWordStart + wordToInsert.length, lastWordStart + wordToInsert.length)
)*/
} else if (userSuggestionsMainMessage == UserSuggestionAnchor.TO_USERS) {
val lastWord =
toUsers.text.substring(0, it.end).substringAfterLast("\n").substringAfterLast(" ")
@ -1199,6 +1198,18 @@ open class NewPostViewModel() : ViewModel() {
forwardZapTo.updatePercentage(index, sliderValue)
}
fun updateZapFromText() {
viewModelScope.launch(Dispatchers.Default) {
val tagger = NewMessageTagger(message.text, emptyList(), emptyList(), null, accountViewModel!!)
tagger.run()
tagger.pTags?.forEach { taggedUser ->
if (!forwardZapTo.items.any { it.key == taggedUser }) {
forwardZapTo.addItem(taggedUser)
}
}
}
}
fun updateZapRaiserAmount(newAmount: Long?) {
zapRaiserAmount = newAmount
saveDraft()

Wyświetl plik

@ -900,7 +900,15 @@ fun EditableServerConfig(
onClick = {
if (url.isNotBlank() && url != "/") {
var addedWSS =
if (!url.startsWith("wss://") && !url.startsWith("ws://")) "wss://$url" else url
if (!url.startsWith("wss://") && !url.startsWith("ws://")) {
if (url.endsWith(".onion") || url.endsWith(".onion/")) {
"ws://$url"
} else {
"wss://$url"
}
} else {
url
}
if (url.endsWith("/")) addedWSS = addedWSS.dropLast(1)
onNewRelay(RelaySetupInfo(addedWSS, read, write, feedTypes = FeedType.values().toSet()))
url = ""

Wyświetl plik

@ -53,7 +53,7 @@ import com.vitorpamplona.amethyst.ui.theme.secondaryButtonBackground
import com.vitorpamplona.quartz.events.ImmutableListOfLists
object ShowFullTextCache {
val cache = LruCache<String, Boolean>(20)
val cache = LruCache<String, Boolean>(10)
}
@Composable

Wyświetl plik

@ -20,7 +20,6 @@
*/
package com.vitorpamplona.amethyst.ui.note
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
@ -305,7 +304,6 @@ private fun RenderBubble(
bubbleSize.intValue = it.width
}
}
.animateContentSize()
}
Column(modifier = bubbleModifier) {

Wyświetl plik

@ -562,7 +562,7 @@ private fun RenderNoteRow(
is AppDefinitionEvent -> RenderAppDefinition(baseNote, accountViewModel, nav)
is AudioTrackEvent -> RenderAudioTrack(baseNote, accountViewModel, nav)
is AudioHeaderEvent -> RenderAudioHeader(baseNote, accountViewModel, nav)
is DraftEvent -> RenderDraft(baseNote, backgroundColor, accountViewModel, nav)
is DraftEvent -> RenderDraft(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is ReactionEvent -> RenderReaction(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is RepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
is GenericRepostEvent -> RenderRepost(baseNote, quotesLeft, backgroundColor, accountViewModel, nav)
@ -735,6 +735,7 @@ fun ObserveDraftEvent(
@Composable
fun RenderDraft(
note: Note,
quotesLeft: Int,
backgroundColor: MutableState<Color>,
accountViewModel: AccountViewModel,
nav: (String) -> Unit,
@ -748,7 +749,7 @@ fun RenderDraft(
makeItShort = false,
canPreview = true,
editState = edits,
quotesLeft = 3,
quotesLeft = quotesLeft,
unPackReply = true,
accountViewModel = accountViewModel,
nav = nav,

Wyświetl plik

@ -23,7 +23,6 @@ package com.vitorpamplona.amethyst.ui.screen
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -32,18 +31,13 @@ import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -68,30 +62,8 @@ fun RefreshableCardView(
scrollStateKey: String? = null,
enablePullRefresh: Boolean = true,
) {
var refreshing by remember { mutableStateOf(false) }
val pullRefreshState =
rememberPullRefreshState(
refreshing,
onRefresh = {
refreshing = true
viewModel.invalidateData()
refreshing = false
},
)
val modifier =
if (enablePullRefresh) {
Modifier.fillMaxSize().pullRefresh(pullRefreshState)
} else {
Modifier.fillMaxSize()
}
Box(modifier) {
RefresheableBox(viewModel, enablePullRefresh) {
SaveableCardFeedState(viewModel, accountViewModel, nav, routeForLastRead, scrollStateKey)
if (enablePullRefresh) {
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
}
}
}

Wyświetl plik

@ -70,7 +70,7 @@ class NotificationViewModel(val account: Account) :
}
@Stable
open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel(), InvalidatableViewModel {
private val _feedContent = MutableStateFlow<CardFeedState>(CardFeedState.Loading)
val feedContent = _feedContent.asStateFlow()
@ -358,7 +358,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
private val bundler = BundledUpdate(1000, Dispatchers.IO)
private val bundlerInsert = BundledInsert<Set<Note>>(1000, Dispatchers.IO)
fun invalidateData(ignoreIfDoing: Boolean = false) {
override fun invalidateData(ignoreIfDoing: Boolean) {
bundler.invalidate(ignoreIfDoing) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
@ -367,9 +367,9 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
}
}
fun invalidateDataAndSendToTop() {
fun invalidateDataAndSendToTop(ignoreIfDoing: Boolean) {
clear()
bundler.invalidate(false) {
bundler.invalidate(ignoreIfDoing) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
val (value, elapsed) =

Wyświetl plik

@ -21,23 +21,16 @@
package com.vitorpamplona.amethyst.ui.screen
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.pullrefresh.PullRefreshIndicator
import androidx.compose.material3.pullrefresh.pullRefresh
import androidx.compose.material3.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewModelScope
@ -57,7 +50,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
@Stable
class RelayFeedViewModel : ViewModel() {
class RelayFeedViewModel : ViewModel(), InvalidatableViewModel {
val order =
compareByDescending<RelayInfo> { it.lastEvent }
.thenByDescending { it.counter }
@ -112,8 +105,8 @@ class RelayFeedViewModel : ViewModel() {
private val bundler = BundledUpdate(250, Dispatchers.IO)
fun invalidateData() {
bundler.invalidate {
override fun invalidateData(ignoreIfDoing: Boolean) {
bundler.invalidate(ignoreIfDoing) {
// adds the time to perform the refresh into this delay
// holding off new updates in case of heavy refresh routines.
refreshSuspended()
@ -142,22 +135,7 @@ fun RelayFeedView(
NewRelayListView({ wantsToAddRelay = "" }, accountViewModel, wantsToAddRelay, nav = nav)
}
var refreshing by remember { mutableStateOf(false) }
val refresh = {
refreshing = true
viewModel.refresh()
refreshing = false
}
val pullRefreshState = rememberPullRefreshState(refreshing, onRefresh = refresh)
val modifier =
if (enablePullRefresh) {
Modifier.fillMaxSize().pullRefresh(pullRefreshState)
} else {
Modifier.fillMaxSize()
}
Box(modifier) {
RefresheableBox(viewModel, enablePullRefresh) {
val listState = rememberLazyListState()
LazyColumn(
@ -176,9 +154,5 @@ fun RelayFeedView(
)
}
}
if (enablePullRefresh) {
PullRefreshIndicator(refreshing, pullRefreshState, Modifier.align(Alignment.TopCenter))
}
}
}

Wyświetl plik

@ -554,7 +554,7 @@ fun NoteMaster(
} else if (noteEvent is AppDefinitionEvent) {
RenderAppDefinition(baseNote, accountViewModel, nav)
} else if (noteEvent is DraftEvent) {
RenderDraft(baseNote, backgroundColor, accountViewModel, nav)
RenderDraft(baseNote, 3, backgroundColor, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) {
DisplayHighlight(
noteEvent.quote(),

Wyświetl plik

@ -1323,20 +1323,11 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
account.deleteDraft(draftTag)
}
fun createTempCachedDraftNote(
suspend fun createTempDraftNote(
noteEvent: DraftEvent,
author: User,
): Note? {
return noteEvent.preCachedDraft(account.signer)?.let { createTempDraftNote(it, author) }
}
fun createTempDraftNote(
noteEvent: DraftEvent,
author: User,
onReady: (Note) -> Unit,
onReady: (Note?) -> Unit,
) {
viewModelScope.launch(Dispatchers.IO) {
}
draftNoteCache.update(noteEvent, onReady)
}
fun createTempDraftNote(

Wyświetl plik

@ -146,6 +146,7 @@ import com.vitorpamplona.amethyst.ui.theme.EditFieldLeadingIconModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldModifier
import com.vitorpamplona.amethyst.ui.theme.EditFieldTrailingIconModifier
import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier
import com.vitorpamplona.amethyst.ui.theme.RowColSpacing
import com.vitorpamplona.amethyst.ui.theme.Size25dp
import com.vitorpamplona.amethyst.ui.theme.Size34dp
import com.vitorpamplona.amethyst.ui.theme.Size35dp
@ -974,21 +975,21 @@ private fun ShortChannelActionOptions(
) {
LoadNote(baseNoteHex = channel.idHex, accountViewModel) {
it?.let {
Spacer(modifier = StdHorzSpacer)
LikeReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
Spacer(modifier = StdHorzSpacer)
ZapReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdHorzSpacer)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = RowColSpacing) {
LikeReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav,
)
ZapReaction(
baseNote = it,
grayTint = MaterialTheme.colorScheme.onSurface,
accountViewModel = accountViewModel,
nav = nav,
)
Spacer(modifier = StdHorzSpacer)
}
}
}

Wyświetl plik

@ -276,7 +276,7 @@ fun MainScreen(
discoveryChatFeedViewModel.sendToTop()
}
Route.Notification.base -> {
notifFeedViewModel.invalidateDataAndSendToTop()
notifFeedViewModel.invalidateDataAndSendToTop(true)
}
}

Wyświetl plik

@ -274,6 +274,7 @@
<string name="report_dialog_title">Blokovat a nahlásit</string>
<string name="block_only">Blokovat</string>
<string name="bookmarks">Záložky</string>
<string name="drafts">Koncepty</string>
<string name="private_bookmarks">Soukromé záložky</string>
<string name="public_bookmarks">Veřejné záložky</string>
<string name="add_to_private_bookmarks">Přidat do soukromých záložek</string>
@ -620,6 +621,7 @@
<string name="server_did_not_provide_a_url_after_uploading">Server po nahrání neposkytl URL</string>
<string name="could_not_download_from_the_server">Nepodařilo se stáhnout nahraná média ze serveru</string>
<string name="could_not_prepare_local_file_to_upload">Nelze připravit místní soubor k nahrání: %1$s</string>
<string name="edit_draft">Upravit koncept</string>
<string name="login_with_qr_code">Přihlášení pomocí QR kódu</string>
<string name="route">Trasa</string>
<string name="route_home">Domů</string>
@ -691,4 +693,10 @@
<string name="accessibility_play_username">Přehrát uživatelské jméno jako audio</string>
<string name="accessibility_scan_qr_code">Skenovat QR kód</string>
<string name="accessibility_navigate_to_alby">Přejít na poskytovatele peněženky třetí strany Alby</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">Není možné odpovědět na koncept</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">Není možné citovat koncept</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">Není možné reagovat na koncept</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">Není možné poslat zap konceptu</string>
<string name="draft_note">Koncept</string>
<string name="load_from_text">Ze zprávy</string>
</resources>

Wyświetl plik

@ -280,6 +280,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="report_dialog_title">Blockieren und melden</string>
<string name="block_only">Blockieren</string>
<string name="bookmarks">Lesezeichen</string>
<string name="drafts">Entwürfe</string>
<string name="private_bookmarks">Private Lesezeichen</string>
<string name="public_bookmarks">Öffentliche Lesezeichen</string>
<string name="add_to_private_bookmarks">Zu den privaten Lesezeichen hinzufügen</string>
@ -625,6 +626,7 @@ anz der Bedingungen ist erforderlich</string>
<string name="server_did_not_provide_a_url_after_uploading">Der Server hat nach dem Hochladen keine URL angegeben</string>
<string name="could_not_download_from_the_server">Hochgeladene Medien konnten nicht vom Server heruntergeladen werden</string>
<string name="could_not_prepare_local_file_to_upload">Lokale Datei konnte nicht zum Hochladen vorbereitet werden: %1$s</string>
<string name="edit_draft">Entwurf bearbeiten</string>
<string name="login_with_qr_code">Einloggen mit QR-Code</string>
<string name="route">Route</string>
<string name="route_home">Startseite</string>
@ -696,4 +698,10 @@ anz der Bedingungen ist erforderlich</string>
<string name="accessibility_play_username">Benutzernamen als Audio abspielen</string>
<string name="accessibility_scan_qr_code">QR-Code scannen</string>
<string name="accessibility_navigate_to_alby">Navigieren Sie zum Drittanbieter-Wallet-Anbieter Alby</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">Es ist nicht möglich, auf einen Entwurf zu antworten</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">Es ist nicht möglich, einen Entwurf zu zitieren</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">Es ist nicht möglich, auf einen Entwurf zu reagieren</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">Es ist nicht möglich, zap Zahlung an einen Entwurf senden</string>
<string name="draft_note">Entwurf</string>
<string name="load_from_text">Aus Nachricht laden</string>
</resources>

Wyświetl plik

@ -276,6 +276,7 @@
<string name="report_dialog_title">Bloquear y reportar</string>
<string name="block_only">Bloquear</string>
<string name="bookmarks">Marcadores</string>
<string name="drafts">Borradores</string>
<string name="private_bookmarks">Marcadores privados</string>
<string name="public_bookmarks">Marcadores públicos</string>
<string name="add_to_private_bookmarks">Agregar a marcadores privados</string>

Wyświetl plik

@ -276,6 +276,7 @@
<string name="report_dialog_title">Bloquear y reportar</string>
<string name="block_only">Bloquear</string>
<string name="bookmarks">Marcadores</string>
<string name="drafts">Borradores</string>
<string name="private_bookmarks">Marcadores privados</string>
<string name="public_bookmarks">Marcadores públicos</string>
<string name="add_to_private_bookmarks">Agregar a marcadores privados</string>

Wyświetl plik

@ -276,6 +276,7 @@
<string name="report_dialog_title">Bloquer et Signaler</string>
<string name="block_only">Bloquer</string>
<string name="bookmarks">Favoris</string>
<string name="drafts">Brouillons</string>
<string name="private_bookmarks">Favoris Privés</string>
<string name="public_bookmarks">Favoris Publics</string>
<string name="add_to_private_bookmarks">Ajouter aux Favoris Privés</string>

Wyświetl plik

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="point_to_the_qr_code">Aponte para o código QR</string>
<string name="point_to_the_qr_code">Aponte para o QR code</string>
<string name="show_qr">Mostrar QR</string>
<string name="profile_image">Imagem de perfil</string>
<string name="your_profile_image">Sua Foto de Perfil</string>
@ -274,6 +274,7 @@
<string name="report_dialog_title">Bloquear e Denunciar</string>
<string name="block_only">Bloquear</string>
<string name="bookmarks">Itens Salvos</string>
<string name="drafts">Rascunhos</string>
<string name="private_bookmarks">Itens Salvos Privados</string>
<string name="public_bookmarks">Itens Salvos Públicos</string>
<string name="add_to_private_bookmarks">Adicionar aos Itens Salvos Privados</string>
@ -620,6 +621,7 @@
<string name="server_did_not_provide_a_url_after_uploading">O servidor não forneceu uma URL após o upload</string>
<string name="could_not_download_from_the_server">Não foi possível baixar o arquivo de mídia carregado do servidor</string>
<string name="could_not_prepare_local_file_to_upload">Não foi possível preparar o arquivo local para enviar: %1$s</string>
<string name="edit_draft">Editar rascunho</string>
<string name="login_with_qr_code">Entrar com Código QR</string>
<string name="route">Rota</string>
<string name="route_home">Início</string>
@ -691,4 +693,10 @@
<string name="accessibility_play_username">Reproduzir nome de usuário como áudio</string>
<string name="accessibility_scan_qr_code">Escanear código QR</string>
<string name="accessibility_navigate_to_alby">Navegar para o provedor de carteira de terceiros Alby</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">Não é possível responder uma nota em rascunho</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">Não é possível citar uma nota em rascunho</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">Não é possível reagir uma nota em rascunho</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">Não é possível fazer um zap em uma nota em rascunho</string>
<string name="draft_note">Nota de rascunho</string>
<string name="load_from_text">Carregar do texto</string>
</resources>

Wyświetl plik

@ -274,6 +274,7 @@
<string name="report_dialog_title">Blockera och rapportera</string>
<string name="block_only">Blockera</string>
<string name="bookmarks">Bokmärken</string>
<string name="drafts">Utkast</string>
<string name="private_bookmarks">Privata Bokmärken</string>
<string name="public_bookmarks">Publika Bokmärken</string>
<string name="add_to_private_bookmarks">Lägg till i Privata Bokmärken</string>
@ -619,6 +620,7 @@
<string name="server_did_not_provide_a_url_after_uploading">Servern gav inte en URL efter uppladdning</string>
<string name="could_not_download_from_the_server">Kunde inte ladda ner uppladdade medier från servern</string>
<string name="could_not_prepare_local_file_to_upload">Kunde inte förbereda lokal fil att ladda upp: %1$s</string>
<string name="edit_draft">Redigera utkast</string>
<string name="login_with_qr_code">Logga in med QR-kod</string>
<string name="route">Rutt</string>
<string name="route_home">Hem</string>
@ -690,4 +692,10 @@
<string name="accessibility_play_username">Spela upp användarnamn som ljud</string>
<string name="accessibility_scan_qr_code">Skanna QR-kod</string>
<string name="accessibility_navigate_to_alby">Navigera till tredjeparts plånboksleverantören Alby</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">Det går inte att svara på ett utkast</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">Det går inte att citera ett utkast</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">Det går inte att reagera på ett utkast</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">Det går inte att zappa ett utkast</string>
<string name="draft_note">Utkast</string>
<string name="load_from_text">Från meddelande</string>
</resources>

Wyświetl plik

@ -275,6 +275,7 @@
<string name="report_dialog_title">บล๊อกและรายงาน</string>
<string name="block_only">บล๊อก</string>
<string name="bookmarks">บุ๊คมาร์ค</string>
<string name="drafts">ฉบับร่าง</string>
<string name="private_bookmarks">บุ๊คมาร์คส่วนตัว</string>
<string name="public_bookmarks">บุ๊คมาร์คสาธรณะ</string>
<string name="add_to_private_bookmarks">เพิ่มไปยังบุ๊คมาร์คส่วนตัว</string>
@ -440,6 +441,8 @@
<string name="connectivity_type_always">ตลอดเวลา</string>
<string name="connectivity_type_wifi_only">Wifi เท่านั้น</string>
<string name="connectivity_type_never">ไม่ต้องแสดง</string>
<string name="ui_feature_set_type_complete">เสร็จสมบูรณ์</string>
<string name="ui_feature_set_type_simplified">Simplified</string>
<string name="system">ระบบ(ค่าพื้นฐาน)</string>
<string name="light">สว่าง</string>
<string name="dark">มืด</string>
@ -451,6 +454,8 @@
<string name="automatically_show_url_preview">การแสดงตัวอย่าง URL</string>
<string name="automatically_hide_nav_bars">การไถฟีดแบบลื่นไหล</string>
<string name="automatically_hide_nav_bars_description">ซ่อนแถบเมนูขณะเลื่อนฟีด</string>
<string name="ui_style">UI Mode</string>
<string name="ui_style_description">เลือกรูปแบบการโพสต์</string>
<string name="load_image">โหลดรูปภาพ</string>
<string name="spamming_users">แสปม</string>
<string name="muted_button">ปิดการมองเห็น คลิกเพื่อปลดออก</string>
@ -615,6 +620,7 @@
<string name="server_did_not_provide_a_url_after_uploading">เซอเวอร์ไม่ตอบสนองหลังจากอัพโหลดแล้ว</string>
<string name="could_not_download_from_the_server">ไม่สามารถโหลดข้อมูลสื่อได้</string>
<string name="could_not_prepare_local_file_to_upload">ไม่สามารถเตรียมข้อมูลสำหรับอัพโหลด : %1$s</string>
<string name="edit_draft">แก้ไขฉบับร่าง</string>
<string name="login_with_qr_code">เข้าสู่ระบบด้วย qr code</string>
<string name="route">เส้นทาง</string>
<string name="route_home">หน้าแรก</string>
@ -638,11 +644,58 @@
<string name="relay_info">รีเลย์ %1$s</string>
<string name="expand_relay_list">แสดงรายการรีเลย์</string>
<string name="note_options">ตัวเลือกเพิ่มเติม</string>
<string name="relay_list_selector">ตัวเลือกรายการรีเลย์</string>
<string name="poll">โพลล์</string>
<string name="disable_poll">ยกเลิกโพลล์</string>
<string name="add_bitcoin_invoice">Bitcoin Invoice</string>
<string name="cancel_bitcoin_invoice">ยกเลิก Bitcoin Invoice</string>
<string name="cancel_classifieds">ยกเลิกการขายสิ่งนี้</string>
<string name="add_zapraiser">ระดมทุน</string>
<string name="cancel_zapraiser">ยกเลิกการระดมทุน</string>
<string name="add_location">ตำแหน่ง</string>
<string name="remove_location">ลบตำแหน่งออก</string>
<string name="add_zap_split">Zap splits</string>
<string name="cancel_zap_split">ยกเลิก Zap split</string>
<string name="add_content_warning">เพิ่มคําเตือนเนื้อหา</string>
<string name="remove_content_warning">ลบคําเตือนเนื้อหา</string>
<string name="show_npub_as_a_qr_code">แสดง npub เป็น qr code</string>
<string name="invalid_nip19_uri">ที่อยู่ไม่ถูกต้อง</string>
<string name="invalid_nip19_uri_description">Amethyst ได้รับ URI แต่ URI นั้นไม่ถูกต้อง: %1$s</string>
<string name="zap_the_devs_title">zap ให้นักพัฒนา</string>
<string name="zap_the_devs_description">การบริจาคของคุณช่วยให้เราสร้างความแตกต่าง ทุก sat มีค่า!</string>
<string name="donate_now">บริจาค</string>
<string name="brought_to_you_by">มาถึงคุณโดย:</string>
<string name="this_version_brought_to_you_by">เวอร์ชันนี้นำเสนอโดย:</string>
<string name="version_name">เวอร์ชัน %1$s</string>
<string name="thank_you">ขอบคุณ!</string>
<string name="max_limit">Max Limit</string>
<string name="restricted_writes">การเขียนที่ถูกจำกัด</string>
<string name="forked_from">คัดลอกมาจาก</string>
<string name="forked_tag">คัดลอก</string>
<string name="git_repository">Git Repository: %1$s</string>
<string name="git_web_address">เว็บไซน์</string>
<string name="git_clone_address">สำเนา:</string>
<string name="existed_since">OTS: %1$s</string>
<string name="ots_info_title">Timestamp ถูกยืนยัน</string>
<string name="ots_info_description">มีหลักฐานว่าโพสต์นี้มีการลงนามก่อน %1$s หลักฐานถูกประทับตราใน Bitcoin blockchain ณ วันและเวลาดังกล่าว</string>
<string name="edit_post">แก้ไขโพสต์</string>
<string name="proposal_to_edit">เสนอให้ปรับปรุงการโพสต์</string>
<string name="message_to_author">สรุปการเปลี่ยนแปลง</string>
<string name="message_to_author_placeholder">แก้ไขด่วน</string>
<string name="accept_the_suggestion">ยอมรับการแนะนำนี้</string>
<string name="accessibility_download_for_offline">ดาวน์โหลด</string>
<string name="accessibility_lyrics_on">Lyrics on</string>
<string name="accessibility_lyrics_off">Lyrics off</string>
<string name="accessibility_turn_on_sealed_message">ปิดผนึกข้อความแล้ว คลิกเพื่อเปิดข้อความ</string>
<string name="accessibility_turn_off_sealed_message">เปิดผนึกข้อความไว้ คลิกเพื่อปิด</string>
<string name="accessibility_send">ส่ง</string>
<string name="accessibility_play_username">อ่านชื่อผู้ใช้เป็นเสียง</string>
<string name="accessibility_scan_qr_code">สแกนคิวอาร์โค้ด</string>
<string name="accessibility_navigate_to_alby">นำทางไปยังผู้ให้บริการกระเป๋าเงินบุคคลที่สาม Alby</string>
<string name="it_s_not_possible_to_reply_to_a_draft_note">ไม่สามารถตอบกลับโน้ตฉบับร่างได้</string>
<string name="it_s_not_possible_to_quote_to_a_draft_note">ไม่สามารถโควทโน้ตฉบับร่างได้</string>
<string name="it_s_not_possible_to_react_to_a_draft_note">ไม่สามารถกดรีแอคโน้ตฉบับร่างได้</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">ไม่สามารถ zap โน้ตฉบับร่างได้</string>
<string name="draft_note">โน๊ตฉบับร่าง</string>
<string name="load_from_text">From Msg</string>
</resources>

Wyświetl plik

@ -827,4 +827,6 @@
<string name="it_s_not_possible_to_react_to_a_draft_note">It\'s not possible to react a draft note</string>
<string name="it_s_not_possible_to_zap_to_a_draft_note">It\'s not possible to zap a draft note</string>
<string name="draft_note">Draft Note</string>
<string name="load_from_text">From Msg</string>
</resources>

Wyświetl plik

@ -0,0 +1,94 @@
/**
* Copyright (c) 2024 Vitor Pamplona
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
* Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.vitorpamplona.amethyst.commons.data
import com.vitorpamplona.quartz.encoders.HexKey
import com.vitorpamplona.quartz.events.AddressableEvent
import com.vitorpamplona.quartz.events.DeletionEvent
import com.vitorpamplona.quartz.events.Event
class DeletionIndex {
data class DeletionRequest(val reference: String, val publicKey: HexKey) : Comparable<DeletionRequest> {
override fun compareTo(other: DeletionRequest): Int {
val compared = reference.compareTo(other.reference)
return if (compared == 0) {
publicKey.compareTo(publicKey)
} else {
compared
}
}
}
// stores a set of id OR atags (kind:pubkey:dtag) by pubkey with the created at of the deletion event.
// Anything newer than the date should not be deleted.
private val deletedReferencesBefore = LargeCache<DeletionRequest, Long>()
fun add(event: DeletionEvent): Boolean {
var atLeastOne = false
event.tags.forEach {
if (it.size > 1 && (it[0] == "a" || it[0] == "e")) {
if (add(it[1], event.pubKey, event.createdAt)) {
atLeastOne = true
}
}
}
return atLeastOne
}
private fun add(
ref: String,
byPubKey: HexKey,
createdAt: Long,
): Boolean {
val key = DeletionRequest(ref, byPubKey)
val previousDeletionTime = deletedReferencesBefore.get(key)
if (previousDeletionTime == null || createdAt > previousDeletionTime) {
deletedReferencesBefore.put(key, createdAt)
return true
}
return false
}
fun hasBeenDeleted(event: Event): Boolean {
val key = DeletionRequest(event.id, event.pubKey)
if (hasBeenDeleted(key)) return true
if (event is AddressableEvent) {
if (hasBeenDeleted(DeletionRequest(event.addressTag(), event.pubKey), event.createdAt)) return true
}
return false
}
private fun hasBeenDeleted(key: DeletionRequest) = deletedReferencesBefore.containsKey(key)
private fun hasBeenDeleted(
key: DeletionRequest,
createdAt: Long,
): Boolean {
val deletionTime = deletedReferencesBefore.get(key)
return deletionTime != null && createdAt <= deletionTime
}
}

Wyświetl plik

@ -24,7 +24,7 @@ import java.util.concurrent.ConcurrentSkipListMap
import java.util.function.BiConsumer
class LargeCache<K, V> {
val cache = ConcurrentSkipListMap<K, V>()
private val cache = ConcurrentSkipListMap<K, V>()
fun get(key: K) = cache.get(key)

Wyświetl plik

@ -25,7 +25,7 @@ import androidx.compose.runtime.Immutable
@Immutable
data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val relay: String?) {
fun toTag() = "$kind:$pubKeyHex:$dTag"
fun toTag() = assembleATag(kind, pubKeyHex, dTag)
fun toNAddr(): String {
return TlvBuilder()
@ -40,6 +40,12 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela
}
companion object {
fun assembleATag(
kind: Int,
pubKeyHex: String,
dTag: String,
) = "$kind:$pubKeyHex:$dTag"
fun isATag(key: String): Boolean {
return key.startsWith("naddr1") || key.contains(":")
}

Wyświetl plik

@ -38,6 +38,8 @@ class DeletionEvent(
fun deleteAddresses() = taggedAddresses()
fun deleteAddressTags() = tags.mapNotNull { if (it.size > 1 && it[0] == "a") it[1] else null }
companion object {
const val KIND = 5
const val ALT = "Deletion event"

Wyświetl plik

@ -78,7 +78,13 @@ class DraftEvent(
onReady: (Event) -> Unit,
) {
try {
plainContent(signer) { onReady(fromJson(it)) }
plainContent(signer) {
try {
onReady(fromJson(it))
} catch (e: Exception) {
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
}
}
} catch (e: Exception) {
// Log.e("UnwrapError", "Couldn't Decrypt the content", e)
}
@ -109,7 +115,7 @@ class DraftEvent(
pubKey: HexKey,
dTag: String,
): String {
return ATag(KIND, pubKey, dTag, null).toTag()
return ATag.assembleATag(KIND, pubKey, dTag)
}
fun create(

Wyświetl plik

@ -514,6 +514,8 @@ interface AddressableEvent {
fun dTag(): String
fun address(): ATag
fun addressTag(): String
}
@Immutable
@ -529,6 +531,11 @@ open class BaseAddressableEvent(
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
override fun address() = ATag(kind, pubKey, dTag(), null)
/**
* Creates the tag in a memory effecient way (without creating the ATag class
*/
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
}
fun String.bytesUsedInMemory(): Int {

Wyświetl plik

@ -44,6 +44,13 @@ class GiftWrapEvent(
return cachedInnerEvent[signer.pubKey]
}
fun addToCache(
pubKey: HexKey,
gift: Event,
) {
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, gift)
}
fun cachedGift(
signer: NostrSigner,
onReady: (Event) -> Unit,
@ -56,7 +63,7 @@ class GiftWrapEvent(
if (gift is WrappedEvent) {
gift.host = this
}
cachedInnerEvent = cachedInnerEvent + Pair(signer.pubKey, gift)
addToCache(signer.pubKey, gift)
onReady(gift)
}
@ -98,8 +105,8 @@ class GiftWrapEvent(
val serializedContent = toJson(event)
val tags = arrayOf(arrayOf("p", recipientPubKey))
signer.nip44Encrypt(serializedContent, recipientPubKey) {
signer.sign(createdAt, KIND, tags, it, onReady)
signer.nip44Encrypt(serializedContent, recipientPubKey) { content ->
signer.sign(createdAt, KIND, tags, content, onReady)
}
}
}

Wyświetl plik

@ -39,6 +39,8 @@ class LongTextNoteEvent(
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
fun topics() = hashtags()
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)

Wyświetl plik

@ -45,6 +45,13 @@ class SealedGossipEvent(
return cachedInnerEvent[signer.pubKey]
}
fun addToCache(
pubKey: HexKey,
gift: Event,
) {
cachedInnerEvent = cachedInnerEvent + Pair(pubKey, gift)
}
fun cachedGossip(
signer: NostrSigner,
onReady: (Event) -> Unit,
@ -59,8 +66,8 @@ class SealedGossipEvent(
if (event is WrappedEvent) {
event.host = host ?: this
}
addToCache(signer.pubKey, event)
cachedInnerEvent = cachedInnerEvent + Pair(signer.pubKey, event)
onReady(event)
}
}

Wyświetl plik

@ -39,6 +39,8 @@ class WikiNoteEvent(
override fun address() = ATag(kind, pubKey, dTag(), null)
override fun addressTag() = ATag.assembleATag(kind, pubKey, dTag())
fun topics() = hashtags()
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)