move models to commons
This commit is contained in:
+37
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 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.commons.threading
|
||||
|
||||
import android.os.Looper
|
||||
|
||||
/**
|
||||
* Android implementation of checkNotInMainThread.
|
||||
* Uses Looper to detect if running on main thread.
|
||||
*/
|
||||
actual fun checkNotInMainThread() {
|
||||
// BuildConfig check removed - commons doesn't have BuildConfig
|
||||
// Enable this check in debug builds at the app level if needed
|
||||
if (isMainThread()) {
|
||||
throw OnMainThreadException("It should not be in the MainThread")
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMainThread() = Looper.myLooper() == Looper.getMainLooper()
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.commons.util
|
||||
|
||||
import java.text.NumberFormat
|
||||
|
||||
actual class PlatformNumberFormatter {
|
||||
private val formatter = NumberFormat.getInstance()
|
||||
|
||||
actual fun format(value: Long): String = formatter.format(value)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* 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.commons.model
|
||||
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentRequestEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.LnZapPaymentResponseEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Response
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapPrivateEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
|
||||
/**
|
||||
* Interface for NIP-47 wallet connect signer state.
|
||||
* Used by Note.kt for checking NWC payment status.
|
||||
*/
|
||||
interface INwcSignerState {
|
||||
suspend fun decryptResponse(event: LnZapPaymentResponseEvent): Response?
|
||||
|
||||
suspend fun decryptRequest(event: LnZapPaymentRequestEvent): Request?
|
||||
|
||||
fun isNIP47Author(pubKey: String?): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for private zap decryption cache.
|
||||
* Used by Note.kt for checking private zap status.
|
||||
*/
|
||||
interface IPrivateZapsDecryptionCache {
|
||||
fun cachedPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent?
|
||||
|
||||
suspend fun decryptPrivateZap(event: LnZapRequestEvent): LnZapPrivateEvent?
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden content settings for filtering notes.
|
||||
* Used by Note.isHiddenFor() to check if content should be hidden.
|
||||
*/
|
||||
data class LiveHiddenUsers(
|
||||
val showSensitiveContent: Boolean?,
|
||||
val hiddenWordsCase: List<DualCase>,
|
||||
val hiddenUsersHashCodes: Set<Int>,
|
||||
val spammersHashCodes: Set<Int>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Interface for account operations needed by Note.kt.
|
||||
* Abstracts Android-specific Account class for use in commons.
|
||||
*/
|
||||
interface IAccount {
|
||||
/** NIP-47 wallet connect state for payment verification */
|
||||
val nip47SignerState: INwcSignerState
|
||||
|
||||
/** Private zaps decryption cache */
|
||||
val privateZapsDecryptionCache: IPrivateZapsDecryptionCache
|
||||
|
||||
/** Current user's profile */
|
||||
fun userProfile(): User
|
||||
|
||||
/** Whether account has write permissions */
|
||||
fun isWriteable(): Boolean
|
||||
|
||||
/** Current user's public key */
|
||||
val pubKey: String
|
||||
|
||||
/** Content filter settings */
|
||||
val showSensitiveContent: Boolean?
|
||||
val hiddenWordsCase: List<DualCase>
|
||||
val hiddenUsersHashCodes: Set<Int>
|
||||
val spammersHashCodes: Set<Int>
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Vendored
+77
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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.commons.model.cache
|
||||
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
|
||||
/**
|
||||
* Cache provider interface for accessing cached Notes, Users, and Channels.
|
||||
*
|
||||
* This abstraction allows Note and User models to access the cache without
|
||||
* direct coupling to LocalCache singleton. Platform-specific implementations
|
||||
* (Android LocalCache, Desktop DesktopLocalCache) implement this interface.
|
||||
*
|
||||
* Benefits:
|
||||
* - Dependency injection instead of singleton coupling
|
||||
* - Testable (can mock for unit tests)
|
||||
* - Platform-agnostic model layer
|
||||
*/
|
||||
interface ICacheProvider {
|
||||
/**
|
||||
* Gets a channel by Note reference.
|
||||
* Used for resolving relay hints for channel messages.
|
||||
*
|
||||
* @param note The note to look up channel for
|
||||
* @return The channel if found, null otherwise
|
||||
*/
|
||||
fun getAnyChannel(note: Any?): IChannel?
|
||||
|
||||
/**
|
||||
* Gets a User by public key hex.
|
||||
* Used for updating follower counts and user relationships.
|
||||
*
|
||||
* @param pubkey The user's public key in hex format
|
||||
* @return The User if exists in cache, null otherwise
|
||||
*/
|
||||
fun getUserIfExists(pubkey: HexKey): Any?
|
||||
|
||||
/**
|
||||
* Counts users matching a predicate.
|
||||
* Used for calculating follower counts.
|
||||
*
|
||||
* @param predicate Filter function for counting users
|
||||
* @return Count of users matching the predicate
|
||||
*/
|
||||
fun countUsers(predicate: (String, Any) -> Boolean): Int
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimal channel interface for relay resolution.
|
||||
* Full channel implementations (PublicChatChannel, LiveActivitiesChannel)
|
||||
* implement this interface.
|
||||
*/
|
||||
interface IChannel {
|
||||
/**
|
||||
* Gets the relay URLs for this channel.
|
||||
* @return List of relay URLs or null if none configured
|
||||
*/
|
||||
fun relays(): List<Any>?
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* 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.commons.model.nip56Reports
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.Note
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.UserDependencies
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportType
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class UserReportCache : UserDependencies {
|
||||
val receivedReportsByAuthor = MutableStateFlow(mapOf<User, Set<Note>>())
|
||||
|
||||
fun addReport(note: Note) {
|
||||
val author = note.author ?: return
|
||||
|
||||
val reportsBy = receivedReportsByAuthor.value[author]
|
||||
|
||||
// if it's already there, quick exit
|
||||
if (reportsBy != null && reportsBy.contains(note)) return
|
||||
|
||||
receivedReportsByAuthor.update {
|
||||
val author = note.author
|
||||
if (author == null) {
|
||||
it
|
||||
} else {
|
||||
val reportsByInner = it[author] ?: emptySet()
|
||||
it + (author to reportsByInner + note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeReport(deleteNote: Note) {
|
||||
val author = deleteNote.author ?: return
|
||||
val reportsBy = receivedReportsByAuthor.value[author]
|
||||
|
||||
// if it's not already there, quick exit
|
||||
if (reportsBy == null || !reportsBy.contains(deleteNote)) return
|
||||
|
||||
receivedReportsByAuthor.update {
|
||||
val author = deleteNote.author
|
||||
if (author == null) {
|
||||
it
|
||||
} else {
|
||||
val reportsByInner = it[author]
|
||||
if (reportsByInner == null) {
|
||||
it
|
||||
} else {
|
||||
it + (author to reportsByInner - deleteNote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun reportsBy(user: User): Set<Note> = receivedReportsByAuthor.value[user] ?: emptySet()
|
||||
|
||||
fun count() = receivedReportsByAuthor.value.values.sumOf { it.size }
|
||||
|
||||
fun countFlow() =
|
||||
receivedReportsByAuthor.map { reportPerAuthor ->
|
||||
reportPerAuthor.values.sumOf { it.size }
|
||||
}
|
||||
|
||||
fun countReportAuthorsBy(users: Set<HexKey>): Int = receivedReportsByAuthor.value.count { it.key.pubkeyHex in users }
|
||||
|
||||
fun all() = receivedReportsByAuthor.value.values.flatten()
|
||||
|
||||
fun reportsBy(users: Set<HexKey>): List<Note> =
|
||||
receivedReportsByAuthor.value
|
||||
.flatMap {
|
||||
if (it.key.pubkeyHex in users) {
|
||||
it.value
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun hasReport(
|
||||
loggedIn: User,
|
||||
type: ReportType,
|
||||
): Boolean =
|
||||
receivedReportsByAuthor.value[loggedIn]?.firstOrNull {
|
||||
(it.event as? ReportEvent)?.reportedAuthor()?.any { it.type == type } ?: false
|
||||
} != null
|
||||
|
||||
fun hasReportNewerThan(timeInSeconds: Long): Boolean = receivedReportsByAuthor.value.any { pair -> pair.value.firstOrNull { (it.createdAt() ?: 0L) > timeInSeconds } != null }
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* 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.commons.model.trustedAssertions
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.commons.model.User
|
||||
import com.vitorpamplona.amethyst.commons.model.UserDependencies
|
||||
import com.vitorpamplona.amethyst.commons.util.PlatformNumberFormatter
|
||||
import com.vitorpamplona.quartz.experimental.relationshipStatus.ContactCardEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.combineTransform
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
||||
class UserCardsCache : UserDependencies {
|
||||
val receivedCards = MutableStateFlow(mapOf<User, AddressableNote>())
|
||||
|
||||
fun addCard(note: AddressableNote) {
|
||||
val author = note.author ?: return
|
||||
|
||||
val cardBy = receivedCards.value[author]
|
||||
|
||||
// if it's already there, quick exit
|
||||
if (cardBy != null && cardBy == note) return
|
||||
|
||||
receivedCards.update {
|
||||
val author = note.author
|
||||
if (author == null) {
|
||||
it
|
||||
} else {
|
||||
it + (author to note)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCard(note: AddressableNote) {
|
||||
val author = note.author ?: return
|
||||
val cardBy = receivedCards.value[author]
|
||||
|
||||
// if it's not already there, quick exit
|
||||
if (cardBy == null || cardBy != note) return
|
||||
|
||||
receivedCards.update {
|
||||
val author = note.author
|
||||
if (author == null) {
|
||||
it
|
||||
} else {
|
||||
val reportsByInner = it[author]
|
||||
if (reportsByInner == null) {
|
||||
it
|
||||
} else {
|
||||
it - author
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun rankFlow(trustProviderList: TrustProviderListState) =
|
||||
combineTransform(receivedCards, trustProviderList.liveUserRankProvider) { cards, provider ->
|
||||
if (provider != null) {
|
||||
val flow =
|
||||
cards.firstNotNullOfOrNull {
|
||||
if (it.key.pubkeyHex == provider.pubkey) {
|
||||
it.value
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (flow != null) {
|
||||
emitAll(flow)
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
}.map {
|
||||
(it?.note?.event as? ContactCardEvent)?.rank()
|
||||
}.flowOn(Dispatchers.IO)
|
||||
|
||||
private val formatter = PlatformNumberFormatter()
|
||||
|
||||
fun followerCountStrFlow(trustProviderList: TrustProviderListState) =
|
||||
combineTransform(receivedCards, trustProviderList.liveUserFollowerCount) { cards, provider ->
|
||||
if (provider != null) {
|
||||
val flow =
|
||||
cards.firstNotNullOfOrNull {
|
||||
if (it.key.pubkeyHex == provider.pubkey) {
|
||||
it.value
|
||||
.flow()
|
||||
.metadata.stateFlow
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
if (flow != null) {
|
||||
emitAll(flow)
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
} else {
|
||||
emit(null)
|
||||
}
|
||||
}.map {
|
||||
val value = (it?.note?.event as? ContactCardEvent)?.followerCount()
|
||||
|
||||
if (value != null && value > 0) {
|
||||
formatter.format(value.toLong())
|
||||
} else {
|
||||
"--"
|
||||
}
|
||||
}.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
// Placeholder for TrustProviderListState - will be properly extracted later
|
||||
// For now, this allows UserCardsCache to compile
|
||||
interface TrustProviderListState {
|
||||
val liveUserRankProvider: kotlinx.coroutines.flow.StateFlow<ServiceProviderTag?>
|
||||
val liveUserFollowerCount: kotlinx.coroutines.flow.StateFlow<ServiceProviderTag?>
|
||||
}
|
||||
|
||||
// Placeholder for ServiceProviderTag
|
||||
interface ServiceProviderTag {
|
||||
val pubkey: String
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* 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.commons.threading
|
||||
|
||||
/**
|
||||
* Checks that the current code is not running on the main thread.
|
||||
* Throws an exception in debug builds if called from the main thread.
|
||||
*
|
||||
* Platform-specific: Android uses Looper, Desktop may use different mechanism or no-op.
|
||||
*/
|
||||
expect fun checkNotInMainThread()
|
||||
|
||||
/**
|
||||
* Exception thrown when code expected to run on a background thread is executed on the main thread.
|
||||
*/
|
||||
class OnMainThreadException(
|
||||
str: String,
|
||||
) : RuntimeException(str)
|
||||
@@ -0,0 +1,134 @@
|
||||
/**
|
||||
* 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.commons.util
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.emojicoder.EmojiCoder
|
||||
import com.vitorpamplona.quartz.nip01Core.core.ImmutableListOfLists
|
||||
|
||||
fun String.isUTF16Char(pos: Int): Boolean = Character.charCount(this.codePointAt(pos)) == 2
|
||||
|
||||
fun String.firstFullCharOld(): String {
|
||||
return when (this.length) {
|
||||
0,
|
||||
1,
|
||||
-> return this
|
||||
2,
|
||||
3,
|
||||
-> return if (isUTF16Char(0)) this.take(2) else this.take(1)
|
||||
else -> {
|
||||
val first = isUTF16Char(0)
|
||||
val second = isUTF16Char(2)
|
||||
if (first && second) {
|
||||
this.take(4)
|
||||
} else if (first) {
|
||||
this.take(2)
|
||||
} else {
|
||||
this.take(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun String.firstFullChar(): String {
|
||||
var isInJoin = false
|
||||
var hasHadSecondChance = false
|
||||
var start = 0
|
||||
var previousCharLength = 0
|
||||
var next: Int
|
||||
var codePoint: Int
|
||||
|
||||
var i = 0
|
||||
|
||||
while (i < this.length) {
|
||||
codePoint = codePointAt(i)
|
||||
|
||||
// Skips if it starts with the join char 0x200D
|
||||
if (codePoint == 0x200D && previousCharLength == 0) {
|
||||
next = offsetByCodePoints(i, 1)
|
||||
start = next
|
||||
} else {
|
||||
// If join, searches for the next char
|
||||
if (codePoint == 0xFE0F) {
|
||||
} else if (codePoint == 0x200D) {
|
||||
isInJoin = true
|
||||
} else {
|
||||
// stops when two chars are not joined together
|
||||
if (previousCharLength > 0 && !isInJoin) {
|
||||
if (Character.charCount(codePoint) == 1 || hasHadSecondChance) {
|
||||
break
|
||||
} else {
|
||||
hasHadSecondChance = true
|
||||
}
|
||||
} else {
|
||||
hasHadSecondChance = false
|
||||
}
|
||||
|
||||
isInJoin = false
|
||||
}
|
||||
|
||||
// next char to evaluate
|
||||
next = offsetByCodePoints(i, 1)
|
||||
previousCharLength += (next - i)
|
||||
}
|
||||
|
||||
i = next
|
||||
}
|
||||
|
||||
// if ends in join, then seachers backwards until a char is found.
|
||||
if (isInJoin) {
|
||||
i = previousCharLength - 1
|
||||
while (i > 0) {
|
||||
if (this[i].code == 0x200D) {
|
||||
previousCharLength -= 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
|
||||
return substring(start, start + previousCharLength)
|
||||
}
|
||||
|
||||
fun String.firstFullCharOrEmoji(tags: ImmutableListOfLists<String>): String {
|
||||
if (length <= 2) {
|
||||
return firstFullChar()
|
||||
}
|
||||
|
||||
if (this[0] == ':') {
|
||||
// makes sure an emoji exists
|
||||
val emojiParts = this.split(":", limit = 3)
|
||||
if (emojiParts.size >= 2) {
|
||||
val emojiName = emojiParts[1]
|
||||
val emojiUrl = tags.lists.firstOrNull { it.size > 1 && it[1] == emojiName }?.getOrNull(2)
|
||||
if (emojiUrl != null) {
|
||||
return ":$emojiName:$emojiUrl"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (EmojiCoder.isCoded(this)) {
|
||||
return EmojiCoder.cropToFirstMessage(this)
|
||||
}
|
||||
|
||||
return firstFullChar()
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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.commons.util
|
||||
|
||||
fun <T> Iterable<T>.replace(
|
||||
old: T,
|
||||
new: T,
|
||||
): List<T> = map { if (it == old) new else it }
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.commons.util
|
||||
|
||||
/**
|
||||
* Platform-agnostic number formatter.
|
||||
* Uses NumberFormat on JVM platforms, custom formatting elsewhere.
|
||||
*/
|
||||
expect class PlatformNumberFormatter() {
|
||||
fun format(value: Long): String
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* 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.commons.threading
|
||||
|
||||
/**
|
||||
* Desktop JVM implementation of checkNotInMainThread.
|
||||
* Currently a no-op as Desktop Compose doesn't have the same main thread restrictions as Android.
|
||||
* Could be enhanced to check Swing EDT if needed.
|
||||
*/
|
||||
actual fun checkNotInMainThread() {
|
||||
// No-op for Desktop - different threading model
|
||||
// Could check for Swing EDT with: javax.swing.SwingUtilities.isEventDispatchThread()
|
||||
// but Compose Desktop doesn't have the same restrictions as Android
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 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.commons.util
|
||||
|
||||
import java.text.NumberFormat
|
||||
|
||||
actual class PlatformNumberFormatter {
|
||||
private val formatter = NumberFormat.getInstance()
|
||||
|
||||
actual fun format(value: Long): String = formatter.format(value)
|
||||
}
|
||||
Reference in New Issue
Block a user