kopia lustrzana https://github.com/vitorpamplona/amethyst
Porównaj commity
44 Commity
e7fc6e4efe
...
261481ecba
Autor | SHA1 | Data |
---|---|---|
Tony Giorgio | 261481ecba | |
Vitor Pamplona | 9be4895080 | |
Crowdin Bot | 6250db01d1 | |
Vitor Pamplona | 48f9045f1b | |
Vitor Pamplona | 818ca7e39e | |
Vitor Pamplona | e8675b8e45 | |
David Kaspar | cef7e17447 | |
greenart7c3 | 6b15a0db8e | |
greenart7c3 | 50c5845a11 | |
Vitor Pamplona | 1b7ba3de01 | |
Vitor Pamplona | 712063f5d2 | |
Vitor Pamplona | d92f23e274 | |
Vitor Pamplona | 3b7f530c0b | |
Crowdin Bot | 623a8d377c | |
Vitor Pamplona | 79489d0b07 | |
Vitor Pamplona | 827512b225 | |
Vitor Pamplona | 6acfadeb9b | |
Vitor Pamplona | e159af2cd7 | |
Vitor Pamplona | 89c2e9d2e0 | |
Vitor Pamplona | 06f6ab6719 | |
Vitor Pamplona | 98c48e8b6b | |
Vitor Pamplona | 25cde455d8 | |
Vitor Pamplona | ef0fdf553c | |
Vitor Pamplona | 719b950272 | |
Vitor Pamplona | 2d02fad6b9 | |
Crowdin Bot | a39db5bf7b | |
Vitor Pamplona | 7fd37367fc | |
Vitor Pamplona | e1c134830e | |
Vitor Pamplona | 621d1c7731 | |
Vitor Pamplona | 7475143506 | |
Vitor Pamplona | 2509d639bd | |
Crowdin Bot | 85dd5cf698 | |
Vitor Pamplona | 274ce6ad77 | |
Vitor Pamplona | 0e8d2fc33a | |
Vitor Pamplona | b88723b68b | |
Vitor Pamplona | 638dba770d | |
Vitor Pamplona | 4d7de6bc19 | |
Vitor Pamplona | fbf676bdb2 | |
Vitor Pamplona | 793780f02c | |
Crowdin Bot | adf31ed115 | |
Vitor Pamplona | 96e434bfcf | |
Vitor Pamplona | c7563c938d | |
Vitor Pamplona | 4380393c5b | |
Tony Giorgio | 08f1b43908 |
|
@ -84,7 +84,7 @@ jobs:
|
|||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: Release ${{ github.ref }}
|
||||
|
@ -96,7 +96,7 @@ jobs:
|
|||
id: upload-release-asset-play-universal-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
|
||||
|
@ -107,7 +107,7 @@ jobs:
|
|||
id: upload-release-asset-play-x86-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
|
||||
|
@ -118,7 +118,7 @@ jobs:
|
|||
id: upload-release-asset-play-x86-64-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
|
||||
|
@ -152,7 +152,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-universal-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
|
||||
|
@ -163,7 +163,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-x86-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
|
||||
|
@ -174,7 +174,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-x86-64-apk
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
|
||||
|
@ -210,7 +210,7 @@ jobs:
|
|||
id: upload-release-asset-play-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
|
||||
|
@ -222,7 +222,7 @@ jobs:
|
|||
id: upload-release-asset-fdroid-aab
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
|
||||
|
|
|
@ -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 364
|
||||
versionName "0.86.1"
|
||||
buildConfigField "String", "RELEASE_NOTES_ID", "\"a704a11334ed4fe6fc6ee6f8856f6f005da33644770616f1437f8b2b488b52b1\""
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
|
|
@ -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>,
|
||||
|
@ -2353,7 +2302,7 @@ class Account(
|
|||
} else if (event is LnZapRequestEvent && event.isPrivateZap() && isWriteable()) {
|
||||
event.cachedPrivateZap()?.content
|
||||
} else {
|
||||
event?.content()
|
||||
event.content()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2306,6 +2306,10 @@ object LocalCache {
|
|||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
fun hasConsumed(notificationEvent: Event): Boolean {
|
||||
return notes.containsKey(notificationEvent.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
|
|
|
@ -26,8 +26,6 @@ import com.vitorpamplona.amethyst.service.previews.BahaUrlPreview
|
|||
import com.vitorpamplona.amethyst.service.previews.IUrlPreviewCallback
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@Stable
|
||||
object UrlCachedPreviewer {
|
||||
|
@ -37,46 +35,44 @@ object UrlCachedPreviewer {
|
|||
suspend fun previewInfo(
|
||||
url: String,
|
||||
onReady: suspend (UrlPreviewState) -> Unit,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
) {
|
||||
cache[url]?.let {
|
||||
onReady(it)
|
||||
return@withContext
|
||||
return
|
||||
}
|
||||
|
||||
BahaUrlPreview(
|
||||
url,
|
||||
object : IUrlPreviewCallback {
|
||||
override suspend fun onComplete(urlInfo: UrlInfoItem) =
|
||||
withContext(Dispatchers.IO) {
|
||||
cache[url]?.let {
|
||||
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
|
||||
onReady(it)
|
||||
return@withContext
|
||||
}
|
||||
}
|
||||
|
||||
val state =
|
||||
if (urlInfo.fetchComplete() && urlInfo.url == url) {
|
||||
UrlPreviewState.Loaded(urlInfo)
|
||||
} else {
|
||||
UrlPreviewState.Empty
|
||||
}
|
||||
|
||||
cache.put(url, state)
|
||||
onReady(state)
|
||||
}
|
||||
|
||||
override suspend fun onFailed(throwable: Throwable) =
|
||||
withContext(Dispatchers.IO) {
|
||||
cache[url]?.let {
|
||||
override suspend fun onComplete(urlInfo: UrlInfoItem) {
|
||||
cache[url]?.let {
|
||||
if (it is UrlPreviewState.Loaded || it is UrlPreviewState.Empty) {
|
||||
onReady(it)
|
||||
return@withContext
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
val state =
|
||||
if (urlInfo.fetchComplete() && urlInfo.url == url) {
|
||||
UrlPreviewState.Loaded(urlInfo)
|
||||
} else {
|
||||
UrlPreviewState.Empty
|
||||
}
|
||||
|
||||
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
|
||||
cache.put(url, state)
|
||||
onReady(state)
|
||||
cache.put(url, state)
|
||||
onReady(state)
|
||||
}
|
||||
|
||||
override suspend fun onFailed(throwable: Throwable) {
|
||||
cache[url]?.let {
|
||||
onReady(it)
|
||||
return
|
||||
}
|
||||
|
||||
val state = UrlPreviewState.Error(throwable.message ?: "Error Loading url preview")
|
||||
cache.put(url, state)
|
||||
onReady(state)
|
||||
}
|
||||
},
|
||||
)
|
||||
.fetchUrlPreview()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,18 +21,14 @@
|
|||
package com.vitorpamplona.amethyst.service.previews
|
||||
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class BahaUrlPreview(val url: String, var callback: IUrlPreviewCallback?) {
|
||||
suspend fun fetchUrlPreview(timeOut: Int = 30000) =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
fetch(timeOut)
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
callback?.onFailed(t)
|
||||
}
|
||||
try {
|
||||
fetch(timeOut)
|
||||
} catch (t: Throwable) {
|
||||
if (t is CancellationException) throw t
|
||||
callback?.onFailed(t)
|
||||
}
|
||||
|
||||
private suspend fun fetch(timeOut: Int = 30000) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 = ""
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -501,8 +501,6 @@ private fun AddedImageFeatures(
|
|||
ImageUrlWithDownloadButton(content.url, showImage)
|
||||
}
|
||||
} else {
|
||||
var verifiedHash by remember(content.url) { mutableStateOf<Boolean?>(null) }
|
||||
|
||||
when (painter.value) {
|
||||
null,
|
||||
is AsyncImagePainter.State.Loading,
|
||||
|
@ -528,24 +526,32 @@ private fun AddedImageFeatures(
|
|||
}
|
||||
}
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
if (content.hash != null) {
|
||||
LaunchedEffect(key1 = content.url) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newVerifiedHash = verifyHash(content)
|
||||
if (newVerifiedHash != verifiedHash) {
|
||||
verifiedHash = newVerifiedHash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifiedHash?.let { HashVerificationSymbol(it, verifiedModifier) }
|
||||
ShowHash(content, verifiedModifier)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowHash(
|
||||
content: MediaUrlContent,
|
||||
verifiedModifier: Modifier,
|
||||
) {
|
||||
var verifiedHash by remember(content.url) { mutableStateOf<Boolean?>(null) }
|
||||
|
||||
if (content.hash != null) {
|
||||
LaunchedEffect(key1 = content.url) {
|
||||
val newVerifiedHash = verifyHash(content)
|
||||
if (newVerifiedHash != verifiedHash) {
|
||||
verifiedHash = newVerifiedHash
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifiedHash?.let { HashVerificationSymbol(it, verifiedModifier) }
|
||||
}
|
||||
|
||||
fun aspectRatio(dim: String?): Float? {
|
||||
if (dim == null) return null
|
||||
if (dim == "0x0") return null
|
||||
|
|
|
@ -20,11 +20,11 @@
|
|||
*/
|
||||
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
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
@ -83,6 +83,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size15Modifier
|
|||
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size5dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.chatAuthorBox
|
||||
import com.vitorpamplona.amethyst.ui.theme.chatAuthorImage
|
||||
import com.vitorpamplona.amethyst.ui.theme.mediumImportanceLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
|
@ -303,7 +304,6 @@ private fun RenderBubble(
|
|||
bubbleSize.intValue = it.width
|
||||
}
|
||||
}
|
||||
.animateContentSize()
|
||||
}
|
||||
|
||||
Column(modifier = bubbleModifier) {
|
||||
|
@ -346,6 +346,7 @@ private fun MessageBubbleLines(
|
|||
baseNote,
|
||||
alignment,
|
||||
accountViewModel.settings.showProfilePictures.value,
|
||||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
|
@ -466,13 +467,9 @@ private fun NoteRow(
|
|||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
when (note.event) {
|
||||
is ChannelCreateEvent -> {
|
||||
RenderCreateChannelNote(note)
|
||||
}
|
||||
is ChannelMetadataEvent -> {
|
||||
RenderChangeChannelMetadataNote(note)
|
||||
}
|
||||
is DraftEvent -> {
|
||||
is ChannelCreateEvent -> RenderCreateChannelNote(note)
|
||||
is ChannelMetadataEvent -> RenderChangeChannelMetadataNote(note)
|
||||
is DraftEvent ->
|
||||
RenderDraftEvent(
|
||||
note,
|
||||
canPreview,
|
||||
|
@ -483,8 +480,7 @@ private fun NoteRow(
|
|||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
else ->
|
||||
RenderRegularTextNote(
|
||||
note,
|
||||
canPreview,
|
||||
|
@ -493,7 +489,6 @@ private fun NoteRow(
|
|||
accountViewModel,
|
||||
nav,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -710,6 +705,7 @@ private fun DrawAuthorInfo(
|
|||
baseNote: Note,
|
||||
alignment: Arrangement.Horizontal,
|
||||
loadProfilePicture: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
baseNote.author?.let {
|
||||
|
@ -723,7 +719,7 @@ private fun DrawAuthorInfo(
|
|||
nav("User/${baseNote.author?.pubkeyHex}")
|
||||
},
|
||||
) {
|
||||
WatchAndDisplayUser(it, loadProfilePicture, nav)
|
||||
WatchAndDisplayUser(it, loadProfilePicture, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -732,11 +728,23 @@ private fun DrawAuthorInfo(
|
|||
private fun WatchAndDisplayUser(
|
||||
author: User,
|
||||
loadProfilePicture: Boolean,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
) {
|
||||
val userState by author.live().userMetadataInfo.observeAsState()
|
||||
|
||||
UserIcon(author.pubkeyHex, userState?.picture, loadProfilePicture)
|
||||
Box(chatAuthorBox, contentAlignment = Alignment.TopEnd) {
|
||||
InnerUserPicture(
|
||||
userHex = author.pubkeyHex,
|
||||
userPicture = userState?.picture,
|
||||
userName = userState?.bestName(),
|
||||
size = Size20dp,
|
||||
modifier = Modifier,
|
||||
accountViewModel = accountViewModel,
|
||||
)
|
||||
|
||||
ObserveAndDisplayFollowingMark(author.pubkeyHex, Size5dp, accountViewModel)
|
||||
}
|
||||
|
||||
if (userState != null) {
|
||||
DisplayMessageUsername(userState?.bestName() ?: author.pubkeyDisplayHex(), userState?.tags ?: EmptyTagList)
|
||||
|
|
|
@ -25,6 +25,7 @@ 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.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
@ -70,19 +71,11 @@ fun LoadDecryptedContentOrNull(
|
|||
accountViewModel: AccountViewModel,
|
||||
inner: @Composable (String?) -> Unit,
|
||||
) {
|
||||
var decryptedContent by
|
||||
remember(note.event) {
|
||||
mutableStateOf(
|
||||
accountViewModel.cachedDecrypt(note),
|
||||
)
|
||||
val decryptedContent by
|
||||
produceState(initialValue = accountViewModel.cachedDecrypt(note), key1 = note.event?.id()) {
|
||||
accountViewModel.decrypt(note) { value = it }
|
||||
}
|
||||
|
||||
if (decryptedContent == null) {
|
||||
LaunchedEffect(key1 = decryptedContent) {
|
||||
accountViewModel.decrypt(note) { decryptedContent = it }
|
||||
}
|
||||
}
|
||||
|
||||
inner(decryptedContent)
|
||||
}
|
||||
|
||||
|
|
|
@ -39,7 +39,6 @@ import androidx.compose.runtime.derivedStateOf
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -54,6 +53,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.compose.produceCachedStateAsync
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.FeatureSetType
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
|
@ -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)
|
||||
|
@ -724,16 +724,8 @@ fun ObserveDraftEvent(
|
|||
val noteState by note.live().metadata.observeAsState()
|
||||
|
||||
val noteEvent = noteState?.note?.event as? DraftEvent ?: return
|
||||
val noteAuthor = noteState?.note?.author ?: return
|
||||
|
||||
val innerNote =
|
||||
produceState(initialValue = accountViewModel.createTempCachedDraftNote(noteEvent, noteAuthor), noteEvent.id) {
|
||||
if (value == null || value?.event?.id() != noteEvent.id) {
|
||||
accountViewModel.createTempDraftNote(noteEvent, noteAuthor) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
val innerNote = produceCachedStateAsync(cache = accountViewModel.draftNoteCache, key = noteEvent)
|
||||
|
||||
innerNote.value?.let {
|
||||
render(it)
|
||||
|
@ -743,6 +735,7 @@ fun ObserveDraftEvent(
|
|||
@Composable
|
||||
fun RenderDraft(
|
||||
note: Note,
|
||||
quotesLeft: Int,
|
||||
backgroundColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
|
@ -756,7 +749,7 @@ fun RenderDraft(
|
|||
makeItShort = false,
|
||||
canPreview = true,
|
||||
editState = edits,
|
||||
quotesLeft = 3,
|
||||
quotesLeft = quotesLeft,
|
||||
unPackReply = true,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) =
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -38,6 +38,7 @@ import coil.request.ImageRequest
|
|||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCache
|
||||
import com.vitorpamplona.amethyst.commons.compose.GenericBaseCacheAsync
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
|
@ -1322,23 +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) {
|
||||
noteEvent.cachedDraft(account.signer) {
|
||||
onReady(createTempDraftNote(it, author))
|
||||
}
|
||||
}
|
||||
draftNoteCache.update(noteEvent, onReady)
|
||||
}
|
||||
|
||||
fun createTempDraftNote(
|
||||
|
@ -1355,6 +1344,21 @@ class AccountViewModel(val account: Account, val settings: SettingsState) : View
|
|||
return note
|
||||
}
|
||||
|
||||
val draftNoteCache = CachedDraftNotes(this)
|
||||
|
||||
class CachedDraftNotes(val accountViewModel: AccountViewModel) : GenericBaseCacheAsync<DraftEvent, Note>(20) {
|
||||
override suspend fun compute(
|
||||
key: DraftEvent,
|
||||
onReady: (Note?) -> Unit,
|
||||
) = withContext(Dispatchers.IO) {
|
||||
key.cachedDraft(accountViewModel.account.signer) {
|
||||
val author = LocalCache.getOrCreateUser(key.pubKey)
|
||||
val note = accountViewModel.createTempDraftNote(it, author)
|
||||
onReady(note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val bechLinkCache = CachedLoadedBechLink(this)
|
||||
|
||||
class CachedLoadedBechLink(val accountViewModel: AccountViewModel) : GenericBaseCache<String, LoadedBechLink>(20) {
|
||||
|
|
|
@ -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
|
||||
|
@ -174,7 +175,6 @@ import java.text.DateFormat
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChannelScreen(
|
||||
|
@ -328,7 +328,6 @@ fun ChannelScreen(
|
|||
newPostModel.message = TextFieldValue("")
|
||||
replyTo.value = null
|
||||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
newPostModel.draftTag = UUID.randomUUID().toString()
|
||||
feedViewModel.sendToTop()
|
||||
}
|
||||
}
|
||||
|
@ -976,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -128,7 +128,6 @@ import kotlinx.coroutines.flow.debounce
|
|||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
|
||||
@Composable
|
||||
fun ChatroomScreen(
|
||||
|
@ -356,7 +355,6 @@ fun ChatroomScreen(
|
|||
accountViewModel.deleteDraft(newPostModel.draftTag)
|
||||
|
||||
newPostModel.message = TextFieldValue("")
|
||||
newPostModel.draftTag = UUID.randomUUID().toString()
|
||||
|
||||
replyTo.value = null
|
||||
feedViewModel.sendToTop()
|
||||
|
|
|
@ -276,7 +276,7 @@ fun MainScreen(
|
|||
discoveryChatFeedViewModel.sendToTop()
|
||||
}
|
||||
Route.Notification.base -> {
|
||||
notifFeedViewModel.invalidateDataAndSendToTop()
|
||||
notifFeedViewModel.invalidateDataAndSendToTop(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -228,5 +228,6 @@ val liveStreamTag =
|
|||
.background(Color.Black)
|
||||
.padding(horizontal = Size5dp)
|
||||
|
||||
val chatAuthorBox = Modifier.size(20.dp)
|
||||
val chatAuthorImage = Modifier.size(20.dp).clip(shape = CircleShape)
|
||||
val AuthorInfoVideoFeed = Modifier.width(75.dp).padding(end = 15.dp)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -276,6 +276,7 @@
|
|||
<string name="report_dialog_title">Blokkeer en rapporteer</string>
|
||||
<string name="block_only">Blokkeren</string>
|
||||
<string name="bookmarks">Bladwijzers</string>
|
||||
<string name="drafts">Concepten</string>
|
||||
<string name="private_bookmarks">Privé bladwijzers</string>
|
||||
<string name="public_bookmarks">Publieke Bladwijzers</string>
|
||||
<string name="add_to_private_bookmarks">Toevoegen aan privé bladwijzers</string>
|
||||
|
@ -441,6 +442,8 @@
|
|||
<string name="connectivity_type_always">Altijd</string>
|
||||
<string name="connectivity_type_wifi_only">Alleen wifi</string>
|
||||
<string name="connectivity_type_never">Nooit</string>
|
||||
<string name="ui_feature_set_type_complete">Voltooid</string>
|
||||
<string name="ui_feature_set_type_simplified">Vereenvoudigd</string>
|
||||
<string name="system">Systeem</string>
|
||||
<string name="light">Licht</string>
|
||||
<string name="dark">Donker</string>
|
||||
|
@ -452,6 +455,8 @@
|
|||
<string name="automatically_show_url_preview">Automatisch URL preview tonen</string>
|
||||
<string name="automatically_hide_nav_bars">Volledig scrollen</string>
|
||||
<string name="automatically_hide_nav_bars_description">Navigatie verbergen bij scrollen</string>
|
||||
<string name="ui_style">UI modus</string>
|
||||
<string name="ui_style_description">Kies de stijl van het bericht</string>
|
||||
<string name="load_image">Afbeelding laden</string>
|
||||
<string name="spamming_users">Spammers</string>
|
||||
<string name="muted_button">Geen geluid. Klik om voor geluid</string>
|
||||
|
@ -618,6 +623,7 @@
|
|||
<string name="server_did_not_provide_a_url_after_uploading">Server heeft geen URL opgegeven na uploaden</string>
|
||||
<string name="could_not_download_from_the_server">Kan de geüploade media niet downloaden van de server</string>
|
||||
<string name="could_not_prepare_local_file_to_upload">Kon lokaal bestand niet voorbereiden voor upload: %1$s</string>
|
||||
<string name="edit_draft">Concept bewerken</string>
|
||||
<string name="login_with_qr_code">Inloggen met QR-Code</string>
|
||||
<string name="route">Route</string>
|
||||
<string name="route_home">Beginscherm</string>
|
||||
|
@ -689,4 +695,9 @@
|
|||
<string name="accessibility_play_username">Speel gebruikersnaam af als audio</string>
|
||||
<string name="accessibility_scan_qr_code">Scan de QR-code</string>
|
||||
<string name="accessibility_navigate_to_alby">Navigeer naar de externe wallet provider Alby</string>
|
||||
<string name="it_s_not_possible_to_reply_to_a_draft_note">Het is niet mogelijk om een concept te beantwoorden</string>
|
||||
<string name="it_s_not_possible_to_quote_to_a_draft_note">Het is niet mogelijk om een concept te citeren</string>
|
||||
<string name="it_s_not_possible_to_react_to_a_draft_note">Het is niet mogelijk om op een concept te reageren</string>
|
||||
<string name="it_s_not_possible_to_zap_to_a_draft_note">Het is niet mogelijk om een concept op te zappen</string>
|
||||
<string name="draft_note">Concept notitie</string>
|
||||
</resources>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 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.compose
|
||||
|
||||
import android.util.LruCache
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.produceState
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedStateAsync(
|
||||
cache: AsyncCachedState<K, V>,
|
||||
key: K,
|
||||
): State<V?> {
|
||||
return produceState(initialValue = cache.cached(key), key1 = key) {
|
||||
cache.update(key) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <K, V> produceCachedStateAsync(
|
||||
cache: AsyncCachedState<K, V>,
|
||||
key: String,
|
||||
updateValue: K,
|
||||
): State<V?> {
|
||||
return produceState(initialValue = cache.cached(updateValue), key1 = key) {
|
||||
cache.update(updateValue) {
|
||||
value = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AsyncCachedState<K, V> {
|
||||
fun cached(k: K): V?
|
||||
|
||||
suspend fun update(
|
||||
k: K,
|
||||
onReady: (V?) -> Unit,
|
||||
)
|
||||
}
|
||||
|
||||
abstract class GenericBaseCacheAsync<K, V>(capacity: Int) : AsyncCachedState<K, V> {
|
||||
private val cache = LruCache<K, V>(capacity)
|
||||
|
||||
override fun cached(k: K): V? {
|
||||
return cache[k]
|
||||
}
|
||||
|
||||
override suspend fun update(
|
||||
k: K,
|
||||
onReady: (V?) -> Unit,
|
||||
) {
|
||||
cache[k]?.let { onReady(it) }
|
||||
|
||||
compute(k) {
|
||||
if (it != null) {
|
||||
cache.put(k, it)
|
||||
}
|
||||
|
||||
onReady(it)
|
||||
}
|
||||
}
|
||||
|
||||
abstract suspend fun compute(
|
||||
key: K,
|
||||
onReady: (V?) -> Unit,
|
||||
)
|
||||
}
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue