Merge branch 'vitorpamplona:main' into followset-improvements
This commit is contained in:
@@ -310,6 +310,8 @@ dependencies {
|
||||
|
||||
// Language picker and Theme chooser
|
||||
implementation libs.androidx.appcompat
|
||||
|
||||
// Dynamically adjust between phone and tablet UI
|
||||
implementation libs.androidx.window.core.android
|
||||
|
||||
// Local model for language identification
|
||||
|
||||
+2
-4
@@ -50,8 +50,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
@@ -59,8 +59,6 @@ import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.UiSettingsFlow
|
||||
import com.vitorpamplona.amethyst.service.notifications.PushDistributorHandler
|
||||
import com.vitorpamplona.amethyst.ui.components.SpinnerSelectionDialog
|
||||
import com.vitorpamplona.amethyst.ui.components.TitleExplainer
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.settings.SettingsRow
|
||||
import com.vitorpamplona.amethyst.ui.stringRes
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -130,7 +128,7 @@ fun SelectNotificationProvider(sharedPrefs: UiSettingsFlow) {
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
|
||||
@@ -25,6 +25,10 @@ import com.vitorpamplona.amethyst.service.logging.Logging
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
|
||||
class Amethyst : Application() {
|
||||
init {
|
||||
Log.d("AmethystApp", "Creating App $this")
|
||||
}
|
||||
|
||||
companion object {
|
||||
lateinit var instance: AppModules
|
||||
private set
|
||||
|
||||
@@ -25,6 +25,7 @@ import android.content.Context
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import coil3.disk.DiskCache
|
||||
import coil3.memory.MemoryCache
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.accountsCache.AccountCacheState
|
||||
import com.vitorpamplona.amethyst.model.nip03Timestamp.IncomingOtsEventVerifier
|
||||
@@ -251,6 +252,11 @@ class AppModules(
|
||||
fun initiate(appContext: Context) {
|
||||
Thread.setDefaultUncaughtExceptionHandler(UnexpectedCrashSaver(crashReportCache, applicationIOScope))
|
||||
|
||||
applicationIOScope.launch {
|
||||
// loads main account quickly.
|
||||
LocalPreferences.loadAccountConfigFromEncryptedStorage()
|
||||
}
|
||||
|
||||
// forces initialization of uiPrefs in the main thread to avoid blinking themes
|
||||
uiPrefs
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.content.pm.ApplicationInfo
|
||||
import android.os.Debug
|
||||
import androidx.core.content.getSystemService
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.normalizedUrls
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.measureTimedValue
|
||||
@@ -68,6 +69,13 @@ fun debugState(context: Context) {
|
||||
.value.available.size,
|
||||
)
|
||||
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Indexed Relays: " +
|
||||
Amethyst.instance.cache.relayHints.relayDB
|
||||
.size() + "/" + normalizedUrls.size(),
|
||||
)
|
||||
|
||||
Log.d(
|
||||
STATE_DUMP_TAG,
|
||||
"Image Disk Cache ${(Amethyst.instance.diskCache.size) / (1024 * 1024)}/${(Amethyst.instance.diskCache.maxSize) / (1024 * 1024)} MB",
|
||||
|
||||
+4
-5
@@ -36,7 +36,6 @@ import kotlinx.coroutines.CoroutineScope
|
||||
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.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@@ -79,7 +78,7 @@ class FollowListState(
|
||||
val userList =
|
||||
flow
|
||||
.map {
|
||||
it.authors.map {
|
||||
it.authors.mapNotNull {
|
||||
cache.checkGetOrCreateUser(it)
|
||||
}
|
||||
}.flowOn(Dispatchers.Default)
|
||||
@@ -87,7 +86,7 @@ class FollowListState(
|
||||
scope,
|
||||
SharingStarted.Eagerly,
|
||||
// this has priority.
|
||||
flow.value.authors.map {
|
||||
flow.value.authors.mapNotNull {
|
||||
cache.checkGetOrCreateUser(it)
|
||||
},
|
||||
)
|
||||
@@ -160,10 +159,10 @@ class FollowListState(
|
||||
|
||||
init {
|
||||
settings.backupContactList?.let {
|
||||
Log.d("AccountRegisterObservers", "Loading saved contacts ${it.toJson()}")
|
||||
Log.d("AccountRegisterObservers", "Loading saved ${it.tags.size} contacts")
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
scope.launch(Dispatchers.IO) { LocalCache.justConsumeMyOwnEvent(it) }
|
||||
}
|
||||
|
||||
// saves contact list for the next time.
|
||||
|
||||
+36
-4
@@ -24,6 +24,13 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocket
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebSocketListener
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.WebsocketBuilder
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.sockets.okhttp.BasicOkHttpWebSocket.Companion.exceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
@@ -33,7 +40,6 @@ class OkHttpWebSocket(
|
||||
val httpClient: (url: NormalizedRelayUrl) -> OkHttpClient,
|
||||
val out: WebSocketListener,
|
||||
) : WebSocket {
|
||||
private val listener = OkHttpWebsocketListener()
|
||||
private var usingOkHttp: OkHttpClient? = null
|
||||
private var socket: okhttp3.WebSocket? = null
|
||||
|
||||
@@ -64,10 +70,21 @@ class OkHttpWebSocket(
|
||||
|
||||
override fun connect() {
|
||||
usingOkHttp = httpClient(url)
|
||||
socket = usingOkHttp?.newWebSocket(buildRequest(), listener)
|
||||
socket = usingOkHttp?.newWebSocket(buildRequest(), OkHttpWebsocketListener(out))
|
||||
}
|
||||
|
||||
inner class OkHttpWebsocketListener : okhttp3.WebSocketListener() {
|
||||
inner class OkHttpWebsocketListener(
|
||||
val out: WebSocketListener,
|
||||
) : okhttp3.WebSocketListener() {
|
||||
val scope = CoroutineScope(Dispatchers.Default + exceptionHandler)
|
||||
val incomingMessages: Channel<String> = Channel(Channel.UNLIMITED)
|
||||
val job = // Launch a coroutine to process messages from the channel.
|
||||
scope.launch {
|
||||
for (message in incomingMessages) {
|
||||
out.onMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOpen(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
response: Response,
|
||||
@@ -79,7 +96,12 @@ class OkHttpWebSocket(
|
||||
override fun onMessage(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
text: String,
|
||||
) = out.onMessage(text)
|
||||
) {
|
||||
// Asynchronously send the received message to the channel.
|
||||
// `trySendBlocking` is used here for simplicity within the callback,
|
||||
// but it's important to understand potential thread blocking if the buffer is full.
|
||||
incomingMessages.trySendBlocking(text)
|
||||
}
|
||||
|
||||
override fun onClosing(
|
||||
webSocket: okhttp3.WebSocket,
|
||||
@@ -92,6 +114,11 @@ class OkHttpWebSocket(
|
||||
code: Int,
|
||||
reason: String,
|
||||
) {
|
||||
// Close the channel on failure, and propagate the error.
|
||||
incomingMessages.close()
|
||||
job.cancel()
|
||||
scope.cancel()
|
||||
|
||||
socket = null
|
||||
out.onClosed(code, reason)
|
||||
}
|
||||
@@ -101,6 +128,11 @@ class OkHttpWebSocket(
|
||||
t: Throwable,
|
||||
response: Response?,
|
||||
) {
|
||||
// Close the channel on failure, and propagate the error.
|
||||
incomingMessages.close()
|
||||
job.cancel()
|
||||
scope.cancel()
|
||||
|
||||
socket = null
|
||||
out.onFailure(t, response?.code, response?.message)
|
||||
}
|
||||
|
||||
+6
-3
@@ -72,7 +72,7 @@ class RelayProxyClientConnector(
|
||||
if (it.connectivity is ConnectivityStatus.StartingService) {
|
||||
// ignore
|
||||
} else if (it.connectivity is ConnectivityStatus.Off) {
|
||||
Log.d("ManageRelayServices", "Pausing Relay Services ${it.connectivity}")
|
||||
Log.d("ManageRelayServices", "Connectivity Off: Pausing Relay Services ${it.connectivity}")
|
||||
if (client.isActive()) {
|
||||
client.disconnect()
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class RelayProxyClientConnector(
|
||||
Log.d("ManageRelayServices", "Pausing Tor Activity")
|
||||
}
|
||||
} else if (it.connectivity is ConnectivityStatus.Active && !client.isActive()) {
|
||||
Log.d("ManageRelayServices", "Resuming Relay Services")
|
||||
Log.d("ManageRelayServices", "Connectivity On: Resuming Relay Services")
|
||||
|
||||
val torStatus = torManager.status.value
|
||||
if (torStatus is TorServiceStatus.Active) {
|
||||
@@ -95,7 +95,10 @@ class RelayProxyClientConnector(
|
||||
client.connect()
|
||||
} else {
|
||||
Log.d("ManageRelayServices", "Relay Services have changed, reconnecting relays that need to")
|
||||
client.reconnect(true)
|
||||
client.reconnect(
|
||||
onlyIfChanged = true,
|
||||
ignoreRetryDelays = true,
|
||||
)
|
||||
}
|
||||
}.onStart {
|
||||
Log.d("ManageRelayServices", "Resuming Relay Services")
|
||||
|
||||
+15
-9
@@ -29,17 +29,26 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
interface IEoseManager {
|
||||
fun invalidateFilters(ignoreIfDoing: Boolean = false)
|
||||
|
||||
fun destroy()
|
||||
|
||||
fun printStats()
|
||||
}
|
||||
|
||||
abstract class BaseEoseManager<T>(
|
||||
val client: INostrClient,
|
||||
val allKeys: () -> Set<T>,
|
||||
) {
|
||||
val sampleTime: Long = 500,
|
||||
) : IEoseManager {
|
||||
protected val logTag: String = this.javaClass.simpleName
|
||||
|
||||
private val orchestrator = SubscriptionController(client)
|
||||
|
||||
abstract fun updateSubscriptions(keys: Set<T>)
|
||||
|
||||
fun printStats() = orchestrator.printStats(logTag)
|
||||
override fun printStats() = orchestrator.printStats(logTag)
|
||||
|
||||
fun newSubscriptionId() = if (isDebug) logTag + newSubId() else newSubId()
|
||||
|
||||
@@ -50,21 +59,18 @@ abstract class BaseEoseManager<T>(
|
||||
fun dismissSubscription(subId: String) = orchestrator.dismissSubscription(subId)
|
||||
|
||||
// Refreshes observers in batches.
|
||||
private val bundler = BundledUpdate(500, Dispatchers.Default)
|
||||
private val bundler = BundledUpdate(sampleTime, Dispatchers.Default)
|
||||
|
||||
fun invalidateFilters() {
|
||||
bundler.invalidate {
|
||||
forceInvalidate()
|
||||
}
|
||||
override fun invalidateFilters(ignoreIfDoing: Boolean) {
|
||||
bundler.invalidate(ignoreIfDoing, ::forceInvalidate)
|
||||
}
|
||||
|
||||
fun forceInvalidate() {
|
||||
updateSubscriptions(allKeys())
|
||||
|
||||
orchestrator.updateRelays()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
override fun destroy() {
|
||||
bundler.cancel()
|
||||
orchestrator.destroy()
|
||||
if (isDebug) {
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.service.relayClient.reqCommand.account.drafts
|
||||
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.AccountQueryState
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AccountDraftsEoseManager(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<AccountQueryState>,
|
||||
) : PerUserEoseManager<AccountQueryState>(client, allKeys) {
|
||||
override fun user(key: AccountQueryState) = key.account.userProfile()
|
||||
|
||||
fun relayFlow(query: AccountQueryState) = query.account.homeRelays.flow
|
||||
|
||||
override fun updateFilter(
|
||||
key: AccountQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> =
|
||||
if (key.account.isWriteable()) {
|
||||
relayFlow(key).value.flatMap {
|
||||
listOf(
|
||||
filterDraftsFromKey(it, user(key).pubkeyHex, since?.get(it)?.time),
|
||||
).flatten()
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
override fun newSub(key: AccountQueryState): Subscription {
|
||||
val user = user(key)
|
||||
userJobMap[user]?.forEach { it.cancel() }
|
||||
userJobMap[user] =
|
||||
listOf(
|
||||
key.account.scope.launch(Dispatchers.Default) {
|
||||
relayFlow(key).collectLatest {
|
||||
invalidateFilters()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return super.newSub(key)
|
||||
}
|
||||
|
||||
override fun endSub(
|
||||
key: User,
|
||||
subId: String,
|
||||
) {
|
||||
super.endSub(key, subId)
|
||||
userJobMap[key]?.forEach { it.cancel() }
|
||||
}
|
||||
}
|
||||
+4
-8
@@ -18,24 +18,20 @@
|
||||
* 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.service.relayClient.reqCommand.account.metadata
|
||||
package com.vitorpamplona.amethyst.service.relayClient.reqCommand.account.drafts
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
|
||||
val ReportsAndBookmarksFromKeyKinds =
|
||||
val DraftKinds =
|
||||
listOf(
|
||||
DraftWrapEvent.KIND,
|
||||
ReportEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterDraftsAndReportsFromKey(
|
||||
fun filterDraftsFromKey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey?,
|
||||
since: Long?,
|
||||
@@ -47,7 +43,7 @@ fun filterDraftsAndReportsFromKey(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = ReportsAndBookmarksFromKeyKinds,
|
||||
kinds = DraftKinds,
|
||||
authors = listOf(pubkey),
|
||||
since = since,
|
||||
),
|
||||
+3
-2
@@ -27,6 +27,7 @@ import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.subscriptions.Subscription
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -50,8 +51,8 @@ class AccountMetadataEoseManager(
|
||||
listOf(
|
||||
filterAccountInfoAndListsFromKey(it, user(key).pubkeyHex, since),
|
||||
filterFollowsAndMutesFromKey(it, user(key).pubkeyHex, since),
|
||||
filterDraftsAndReportsFromKey(it, user(key).pubkeyHex, since),
|
||||
filterLastPostsFromKey(it, user(key).pubkeyHex, since),
|
||||
filterBookmarksAndReportsFromKey(it, user(key).pubkeyHex, since),
|
||||
filterLastPostsFromKey(it, user(key).pubkeyHex, since ?: TimeUtils.oneMonthAgo()),
|
||||
filterBasicAccountInfoFromKeys(it, key.otherAccounts.minus(key.account.userProfile().pubkeyHex).toList(), since),
|
||||
).flatten()
|
||||
}
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2025 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.service.relayClient.reqCommand.account.metadata
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
|
||||
val ReportsAndBookmarksFromKeyKinds =
|
||||
listOf(
|
||||
ReportEvent.KIND,
|
||||
BookmarkListEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterBookmarksAndReportsFromKey(
|
||||
relay: NormalizedRelayUrl,
|
||||
pubkey: HexKey?,
|
||||
since: Long?,
|
||||
): List<RelayBasedFilter> {
|
||||
if (pubkey == null || pubkey.isEmpty()) return emptyList()
|
||||
|
||||
return listOf(
|
||||
RelayBasedFilter(
|
||||
relay = relay,
|
||||
filter =
|
||||
Filter(
|
||||
kinds = ReportsAndBookmarksFromKeyKinds,
|
||||
authors = listOf(pubkey),
|
||||
since = since,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
+13
-7
@@ -42,14 +42,20 @@ class AccountGiftWrapsEoseManager(
|
||||
override fun updateFilter(
|
||||
key: AccountQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter> =
|
||||
key.account.dmRelays.flow.value.flatMap { relay ->
|
||||
filterGiftWrapsToPubkey(
|
||||
relay = relay,
|
||||
pubkey = user(key).pubkeyHex,
|
||||
since = since?.get(relay)?.time,
|
||||
)
|
||||
): List<RelayBasedFilter> {
|
||||
// Only loads DMs if the account is writeable
|
||||
return if (key.account.isWriteable()) {
|
||||
key.account.dmRelays.flow.value.flatMap { relay ->
|
||||
filterGiftWrapsToPubkey(
|
||||
relay = relay,
|
||||
pubkey = user(key).pubkeyHex,
|
||||
since = since?.get(relay)?.time,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
val userJobMap = mutableMapOf<User, List<Job>>()
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ class EventWatcherSubAssembler(
|
||||
allKeys: () -> Set<EventFinderQueryState>,
|
||||
) : SingleSubEoseManager<EventFinderQueryState>(client, allKeys) {
|
||||
var lastNotesOnFilter = emptyList<Note>()
|
||||
var latestEOSEs: EOSEAccountFast<Note> = EOSEAccountFast<Note>(10000)
|
||||
var latestEOSEs: EOSEAccountFast<Note> = EOSEAccountFast<Note>(1000)
|
||||
|
||||
override fun newEose(
|
||||
relay: NormalizedRelayUrl,
|
||||
|
||||
+21
-17
@@ -26,15 +26,17 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ProvideTextStyle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.platform.UriHandler
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -69,24 +71,25 @@ fun RenderContentAsMarkdown(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: INav,
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
val onClick =
|
||||
remember {
|
||||
{ link: String ->
|
||||
val route = uriToRoute(link, accountViewModel.account)
|
||||
if (route != null) {
|
||||
nav.nav(route)
|
||||
} else {
|
||||
runCatching { uri.openUri(link) }
|
||||
remember(uriHandler) {
|
||||
object : UriHandler {
|
||||
override fun openUri(uri: String) {
|
||||
val route = uriToRoute(uri, accountViewModel.account)
|
||||
if (route != null) {
|
||||
nav.nav(route)
|
||||
} else {
|
||||
runCatching { uriHandler.openUri(uri) }
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
ProvideTextStyle(MarkdownTextStyle) {
|
||||
val astNode =
|
||||
remember(content) {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
val renderer =
|
||||
@@ -103,12 +106,13 @@ fun RenderContentAsMarkdown(
|
||||
)
|
||||
}
|
||||
|
||||
RichText(
|
||||
style = MaterialTheme.colorScheme.markdownStyle,
|
||||
linkClickHandler = onClick,
|
||||
renderer = renderer,
|
||||
) {
|
||||
BasicMarkdown(astNode)
|
||||
CompositionLocalProvider(LocalUriHandler provides onClick) {
|
||||
RichText(
|
||||
style = MaterialTheme.colorScheme.markdownStyle,
|
||||
renderer = renderer,
|
||||
) {
|
||||
BasicMarkdown(astNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ fun NotificationRegistration(accountViewModel: AccountViewModel) {
|
||||
|
||||
if (notificationPermissionState.status.isGranted) {
|
||||
LifecycleResumeEffect(key1 = accountViewModel, notificationPermissionState.status.isGranted) {
|
||||
Log.d("RegisterAccounts", "Registering for push notifications $notificationPermissionState")
|
||||
Log.d("RegisterAccounts", "Registering for push notifications")
|
||||
scope.launch {
|
||||
PushNotificationUtils.checkAndInit(
|
||||
LocalPreferences.allSavedAccounts(),
|
||||
|
||||
+7
-1
@@ -23,6 +23,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.privateDM.datasource
|
||||
import com.vitorpamplona.amethyst.service.relayClient.eoseManagers.PerUserAndFollowListEoseManager
|
||||
import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
|
||||
class ChatroomFilterSubAssembler(
|
||||
client: INostrClient,
|
||||
@@ -31,7 +32,12 @@ class ChatroomFilterSubAssembler(
|
||||
override fun updateFilter(
|
||||
key: ChatroomQueryState,
|
||||
since: SincePerRelayMap?,
|
||||
) = filterNip04DMs(key.room.users, key.account, since)
|
||||
): List<RelayBasedFilter>? =
|
||||
if (key.account.isWriteable()) {
|
||||
filterNip04DMs(key.room.users, key.account, since)
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
override fun user(key: ChatroomQueryState) = key.account.userProfile()
|
||||
|
||||
|
||||
+10
-6
@@ -40,12 +40,16 @@ class DMsFromUserFilterSubAssembler(
|
||||
key: ChatroomListState,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? =
|
||||
key.account.homeRelays.flow.value.map {
|
||||
filterNip04DMsFromMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
} +
|
||||
key.account.dmRelays.flow.value.map {
|
||||
filterNip04DMsToMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
}
|
||||
if (key.account.isWriteable()) {
|
||||
key.account.homeRelays.flow.value.map {
|
||||
filterNip04DMsFromMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
} +
|
||||
key.account.dmRelays.flow.value.map {
|
||||
filterNip04DMsToMe(key.account.userProfile(), it, since?.get(it)?.time)
|
||||
}
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
override fun user(key: ChatroomListState) = key.account.userProfile()
|
||||
|
||||
|
||||
+3
-3
@@ -83,8 +83,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.halilibo.richtext.commonmark.CommonMarkdownParseOptions
|
||||
import com.halilibo.richtext.commonmark.CommonmarkAstNodeParser
|
||||
import com.halilibo.richtext.commonmark.MarkdownParseOptions
|
||||
import com.halilibo.richtext.markdown.BasicMarkdown
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.material3.RichText
|
||||
@@ -177,7 +177,7 @@ private fun DialogContents(
|
||||
|
||||
val astNode1 =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content1)
|
||||
}
|
||||
|
||||
RichText(
|
||||
@@ -205,7 +205,7 @@ private fun DialogContents(
|
||||
|
||||
val astNode =
|
||||
remember {
|
||||
CommonmarkAstNodeParser(MarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
CommonmarkAstNodeParser(CommonMarkdownParseOptions.MarkdownWithLinks).parse(content)
|
||||
}
|
||||
|
||||
RichText(
|
||||
|
||||
+2
-3
@@ -47,7 +47,6 @@ val UserProfilePostKinds1 =
|
||||
GenericRepostEvent.KIND,
|
||||
RepostEvent.KIND,
|
||||
LongTextNoteEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
PollNoteEvent.KIND,
|
||||
HighlightEvent.KIND,
|
||||
WikiNoteEvent.KIND,
|
||||
@@ -62,6 +61,7 @@ val UserProfilePostKinds2 =
|
||||
InteractiveStoryPrologueEvent.KIND,
|
||||
CommentEvent.KIND,
|
||||
VoiceReplyEvent.KIND,
|
||||
PinListEvent.KIND,
|
||||
)
|
||||
|
||||
fun filterUserProfilePosts(
|
||||
@@ -70,8 +70,7 @@ fun filterUserProfilePosts(
|
||||
): List<RelayBasedFilter> {
|
||||
val relays =
|
||||
user.outboxRelays()?.ifEmpty { null }
|
||||
?: user.relaysBeingUsed.keys.ifEmpty { null }
|
||||
?: LocalCache.relayHints.hintsForKey(user.pubkeyHex)
|
||||
?: (user.relaysBeingUsed.keys + LocalCache.relayHints.hintsForKey(user.pubkeyHex))
|
||||
|
||||
return relays
|
||||
.map { relay ->
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
|
||||
class SearchRelayListViewModel : BasicRelaySetupInfoModel() {
|
||||
override fun getRelayList(): List<NormalizedRelayUrl>? =
|
||||
account.searchRelayList.flow.value
|
||||
account.searchRelayList.flowNoDefaults.value
|
||||
.toList()
|
||||
|
||||
override suspend fun saveRelayList(urlList: List<NormalizedRelayUrl>) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.ui.theme
|
||||
|
||||
import android.R.attr.fontFamily
|
||||
import android.R.id.primary
|
||||
import android.app.Activity
|
||||
import android.app.UiModeManager
|
||||
import android.content.Context
|
||||
@@ -49,6 +51,7 @@ import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextLinkStyles
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -292,8 +295,11 @@ val MarkDownStyleOnDark =
|
||||
stringStyle =
|
||||
RichTextDefaults.stringStyle?.copy(
|
||||
linkStyle =
|
||||
SpanStyle(
|
||||
color = DarkColorPalette.primary,
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
color = DarkColorPalette.primary,
|
||||
),
|
||||
),
|
||||
codeStyle =
|
||||
SpanStyle(
|
||||
@@ -330,8 +336,11 @@ val MarkDownStyleOnLight =
|
||||
stringStyle =
|
||||
RichTextDefaults.stringStyle?.copy(
|
||||
linkStyle =
|
||||
SpanStyle(
|
||||
color = LightColorPalette.primary,
|
||||
TextLinkStyles(
|
||||
style =
|
||||
SpanStyle(
|
||||
color = LightColorPalette.primary,
|
||||
),
|
||||
),
|
||||
codeStyle =
|
||||
SpanStyle(
|
||||
|
||||
Reference in New Issue
Block a user