mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-05 14:24:38 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e70034758 | ||
|
|
ee30ff28f6 | ||
|
|
03654b5ebd | ||
|
|
7071174aed | ||
|
|
2c27531056 | ||
|
|
57d5556728 | ||
|
|
f4da2ae6be | ||
|
|
57dfe3af8c | ||
|
|
618d14a965 | ||
|
|
ee95942289 | ||
|
|
c6c15c3ec7 | ||
|
|
ed4c51b664 | ||
|
|
b3f39434ee |
+2
-2
@@ -13,8 +13,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 34
|
||||
versionCode 305
|
||||
versionName "0.77.8"
|
||||
versionCode 306
|
||||
versionName "0.78.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.util.Log
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.DefaultReactions
|
||||
import com.vitorpamplona.amethyst.model.DefaultZapAmounts
|
||||
@@ -16,6 +17,7 @@ import com.vitorpamplona.amethyst.model.Nip47URI
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.model.ServersAvailable
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
@@ -76,6 +78,7 @@ private object PrefKeys {
|
||||
const val THEME = "theme"
|
||||
const val PREFERRED_LANGUAGE = "preferred_Language"
|
||||
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
|
||||
const val AUTOMATICALLY_HIDE_NAV_BARS = "automatically_hide_nav_bars"
|
||||
const val LOGIN_WITH_EXTERNAL_SIGNER = "login_with_external_signer"
|
||||
}
|
||||
|
||||
@@ -272,6 +275,11 @@ object LocalPreferences {
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview.prefCode!!)
|
||||
}
|
||||
if (account.settings.automaticallyHideNavigationBars.prefCode == null) {
|
||||
remove(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, account.settings.automaticallyHideNavigationBars.prefCode!!)
|
||||
}
|
||||
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
|
||||
}.apply()
|
||||
}
|
||||
@@ -417,6 +425,11 @@ object LocalPreferences {
|
||||
} else {
|
||||
ConnectivityType.ALWAYS
|
||||
}
|
||||
settings.automaticallyHideNavigationBars = if (contains(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS)) {
|
||||
parseBooleanType(getBoolean(PrefKeys.AUTOMATICALLY_HIDE_NAV_BARS, false))
|
||||
} else {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
|
||||
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
|
||||
}
|
||||
|
||||
@@ -142,6 +142,13 @@ object ServiceManager {
|
||||
LocalCache.pruneExpiredEvents()
|
||||
}
|
||||
}
|
||||
|
||||
fun restartIfDifferentAccount(account: Account, context: Context) {
|
||||
if (this.account != account) {
|
||||
pause()
|
||||
start(account, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object SingletonDiskCache {
|
||||
|
||||
@@ -105,6 +105,7 @@ class Account(
|
||||
data class LiveHiddenUsers(
|
||||
val hiddenUsers: ImmutableSet<String>,
|
||||
val spammers: ImmutableSet<String>,
|
||||
val hiddenWords: ImmutableSet<String>,
|
||||
val showSensitiveContent: Boolean?
|
||||
)
|
||||
|
||||
@@ -129,23 +130,25 @@ class Account(
|
||||
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers
|
||||
?: persistentSetOf(),
|
||||
hiddenWords = persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
} else {
|
||||
blockList.decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
|
||||
val liveBlockedUsers = blockList.publicAndPrivateUsers(blockList.decryptedContent ?: "")
|
||||
val liveBlockedWords = blockList.publicAndPrivateWords(blockList.decryptedContent ?: "")
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = liveBlockedUsers,
|
||||
spammers = localLive?.account?.transientHiddenUsers
|
||||
?: persistentSetOf(),
|
||||
hiddenWords = liveBlockedWords,
|
||||
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = persistentSetOf(),
|
||||
hiddenWords = persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers
|
||||
?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
@@ -153,8 +156,10 @@ class Account(
|
||||
}
|
||||
} else {
|
||||
val liveBlockedUsers = blockList?.publicAndPrivateUsers(keyPair.privKey)
|
||||
val liveBlockedWords = blockList?.publicAndPrivateWords(keyPair.privKey)
|
||||
LiveHiddenUsers(
|
||||
hiddenUsers = liveBlockedUsers ?: persistentSetOf(),
|
||||
hiddenWords = liveBlockedWords ?: persistentSetOf(),
|
||||
spammers = localLive?.account?.transientHiddenUsers ?: persistentSetOf(),
|
||||
showSensitiveContent = showSensitiveContent
|
||||
)
|
||||
@@ -180,6 +185,14 @@ class Account(
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun updateAutomaticallyHideHavBars(
|
||||
automaticallyHideHavBars: BooleanType
|
||||
) {
|
||||
settings.automaticallyHideNavigationBars = automaticallyHideHavBars
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun updateAutomaticallyShowImages(
|
||||
automaticallyShowImages: ConnectivityType
|
||||
) {
|
||||
@@ -2268,6 +2281,157 @@ class Account(
|
||||
return returningList
|
||||
}
|
||||
|
||||
fun hideWord(word: String) {
|
||||
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
|
||||
if (loginWithExternalSigner) {
|
||||
val id = blockList?.id
|
||||
val encryptedContent = if (id == null) {
|
||||
val privateTags = listOf(listOf("word", word))
|
||||
val msg = Event.mapper.writeValueAsString(privateTags)
|
||||
|
||||
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted")
|
||||
val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
|
||||
ExternalSignerUtils.content.remove("encrypted")
|
||||
if (encryptedContent.isBlank()) return
|
||||
encryptedContent
|
||||
} else {
|
||||
var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[id]
|
||||
if (decryptedContent == null) {
|
||||
ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), id)
|
||||
val content = ExternalSignerUtils.content[id] ?: ""
|
||||
if (content.isBlank()) return
|
||||
decryptedContent = content
|
||||
}
|
||||
|
||||
val privateTags = blockList.privateTagsOrEmpty(decryptedContent).plus(element = listOf("word", word))
|
||||
val msg = Event.mapper.writeValueAsString(privateTags)
|
||||
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), id)
|
||||
val eventContent = ExternalSignerUtils.content[id] ?: ""
|
||||
if (eventContent.isBlank()) return
|
||||
eventContent
|
||||
}
|
||||
|
||||
var event = if (blockList != null) {
|
||||
PeopleListEvent.addWord(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
encryptedContent
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithWord(
|
||||
name = PeopleListEvent.blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
encryptedContent
|
||||
)
|
||||
}
|
||||
|
||||
ExternalSignerUtils.openSigner(event)
|
||||
|
||||
val eventContent = ExternalSignerUtils.content[event.id] ?: ""
|
||||
if (eventContent.isBlank()) return
|
||||
event = PeopleListEvent(
|
||||
event.id,
|
||||
event.pubKey,
|
||||
event.createdAt,
|
||||
event.tags,
|
||||
event.content,
|
||||
eventContent
|
||||
)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
} else {
|
||||
val event = if (blockList != null) {
|
||||
PeopleListEvent.addWord(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
privateKey = keyPair.privKey!!
|
||||
)
|
||||
} else {
|
||||
PeopleListEvent.createListWithWord(
|
||||
name = PeopleListEvent.blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
privateKey = keyPair.privKey!!
|
||||
)
|
||||
}
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun showWord(word: String) {
|
||||
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
|
||||
|
||||
if (blockList != null) {
|
||||
if (loginWithExternalSigner) {
|
||||
val content = blockList.content
|
||||
val encryptedContent = if (content.isBlank()) {
|
||||
val privateTags = listOf(listOf("word", word))
|
||||
val msg = Event.mapper.writeValueAsString(privateTags)
|
||||
|
||||
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id)
|
||||
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
|
||||
if (eventContent.isBlank()) return
|
||||
eventContent
|
||||
} else {
|
||||
var decryptedContent = ExternalSignerUtils.cachedDecryptedContent[blockList.id]
|
||||
if (decryptedContent == null) {
|
||||
ExternalSignerUtils.decrypt(blockList.content, keyPair.pubKey.toHexKey(), blockList.id)
|
||||
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
|
||||
if (eventContent.isBlank()) return
|
||||
decryptedContent = eventContent
|
||||
}
|
||||
val privateTags = blockList.privateTagsOrEmpty(decryptedContent).minus(element = listOf("word", word))
|
||||
val msg = Event.mapper.writeValueAsString(privateTags)
|
||||
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), blockList.id)
|
||||
val eventContent = ExternalSignerUtils.content[blockList.id] ?: ""
|
||||
if (eventContent.isBlank()) return
|
||||
eventContent
|
||||
}
|
||||
|
||||
var event = PeopleListEvent.removeTag(
|
||||
earlierVersion = blockList,
|
||||
tag = word,
|
||||
isPrivate = true,
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
encryptedContent
|
||||
)
|
||||
|
||||
ExternalSignerUtils.openSigner(event)
|
||||
val eventContent = ExternalSignerUtils.content[event.id] ?: ""
|
||||
if (eventContent.isBlank()) return
|
||||
event = PeopleListEvent.create(event, eventContent)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
} else {
|
||||
val event = PeopleListEvent.removeWord(
|
||||
earlierVersion = blockList,
|
||||
word = word,
|
||||
isPrivate = true,
|
||||
privateKey = keyPair.privKey!!
|
||||
)
|
||||
|
||||
Client.send(event)
|
||||
LocalCache.consume(event)
|
||||
}
|
||||
}
|
||||
|
||||
transientHiddenUsers = (transientHiddenUsers - word).toImmutableSet()
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun hideUser(pubkeyHex: String) {
|
||||
val blockList = migrateHiddenUsersIfNeeded(getBlockList())
|
||||
if (loginWithExternalSigner) {
|
||||
@@ -2276,7 +2440,7 @@ class Account(
|
||||
val privateTags = listOf(listOf("p", pubkeyHex))
|
||||
val msg = Event.mapper.writeValueAsString(privateTags)
|
||||
|
||||
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypt")
|
||||
ExternalSignerUtils.encrypt(msg, keyPair.pubKey.toHexKey(), "encrypted")
|
||||
val encryptedContent = ExternalSignerUtils.content["encrypted"] ?: ""
|
||||
ExternalSignerUtils.content.remove("encrypted")
|
||||
if (encryptedContent.isBlank()) return
|
||||
@@ -2385,9 +2549,9 @@ class Account(
|
||||
eventContent
|
||||
}
|
||||
|
||||
var event = PeopleListEvent.addUser(
|
||||
var event = PeopleListEvent.removeTag(
|
||||
earlierVersion = blockList,
|
||||
pubKeyHex = pubkeyHex,
|
||||
tag = pubkeyHex,
|
||||
isPrivate = true,
|
||||
pubKey = keyPair.pubKey.toHexKey(),
|
||||
encryptedContent
|
||||
|
||||
@@ -673,16 +673,24 @@ open class Note(val idHex: String) {
|
||||
}
|
||||
|
||||
fun isHiddenFor(accountChoices: Account.LiveHiddenUsers): Boolean {
|
||||
if (event == null) return false
|
||||
val thisEvent = event ?: return false
|
||||
|
||||
val isBoostedNoteHidden = if (event is GenericRepostEvent || event is RepostEvent || event is CommunityPostApprovalEvent) {
|
||||
val isBoostedNoteHidden = if (thisEvent is GenericRepostEvent || thisEvent is RepostEvent || thisEvent is CommunityPostApprovalEvent) {
|
||||
replyTo?.lastOrNull()?.isHiddenFor(accountChoices) ?: false
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isSensitive = event?.isSensitive() ?: false
|
||||
return isBoostedNoteHidden ||
|
||||
val isHiddenByWord = if (thisEvent is BaseTextNoteEvent) {
|
||||
accountChoices.hiddenWords.any {
|
||||
thisEvent.content.contains(it, true)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
val isSensitive = thisEvent.isSensitive()
|
||||
return isBoostedNoteHidden || isHiddenByWord ||
|
||||
accountChoices.hiddenUsers.contains(author?.pubkeyHex) ||
|
||||
accountChoices.spammers.contains(author?.pubkeyHex) ||
|
||||
(isSensitive && accountChoices.showSensitiveContent == false)
|
||||
|
||||
@@ -8,7 +8,8 @@ class Settings(
|
||||
var preferredLanguage: String? = null,
|
||||
var automaticallyShowImages: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
var automaticallyStartPlayback: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS
|
||||
var automaticallyShowUrlPreview: ConnectivityType = ConnectivityType.ALWAYS,
|
||||
var automaticallyHideNavigationBars: BooleanType = BooleanType.ALWAYS
|
||||
)
|
||||
|
||||
enum class ConnectivityType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
|
||||
@@ -38,3 +39,28 @@ fun parseConnectivityType(screenCode: Int): ConnectivityType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class BooleanType(val prefCode: Boolean?, val screenCode: Int, val reourceId: Int) {
|
||||
ALWAYS(null, 0, R.string.connectivity_type_always),
|
||||
NEVER(false, 1, R.string.connectivity_type_never)
|
||||
}
|
||||
|
||||
fun parseBooleanType(code: Boolean?): BooleanType {
|
||||
return when (code) {
|
||||
BooleanType.ALWAYS.prefCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.prefCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun parseBooleanType(screenCode: Int): BooleanType {
|
||||
return when (screenCode) {
|
||||
BooleanType.ALWAYS.screenCode -> BooleanType.ALWAYS
|
||||
BooleanType.NEVER.screenCode -> BooleanType.NEVER
|
||||
else -> {
|
||||
BooleanType.ALWAYS
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -281,70 +282,74 @@ fun GetVideoController(
|
||||
UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// Prepares a VideoPlayer from the foreground service.
|
||||
DisposableEffect(key1 = videoUri) {
|
||||
// If it is not null, the user might have come back from a playing video, like clicking on
|
||||
// the notification of the video player.
|
||||
if (controller.value == null) {
|
||||
PlaybackClientController.prepareController(
|
||||
uid,
|
||||
videoUri,
|
||||
nostrUriCallback,
|
||||
context
|
||||
) {
|
||||
// checks again because of race conditions.
|
||||
if (controller.value == null) { // still prone to race conditions.
|
||||
controller.value = it
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (controller.value == null) {
|
||||
PlaybackClientController.prepareController(
|
||||
uid,
|
||||
videoUri,
|
||||
nostrUriCallback,
|
||||
context
|
||||
) {
|
||||
// checks again because of race conditions.
|
||||
if (controller.value == null) { // still prone to race conditions.
|
||||
controller.value = it
|
||||
|
||||
if (!it.isPlaying) {
|
||||
if (keepPlayingMutex?.isPlaying == true) {
|
||||
// There is a video playing, start this one on mute.
|
||||
controller.value?.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
controller.value?.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
}
|
||||
|
||||
controller.value?.setMediaItem(mediaItem)
|
||||
controller.value?.prepare()
|
||||
} else if (controller.value != it) {
|
||||
// discards the new controller because there is an existing one
|
||||
it.stop()
|
||||
it.release()
|
||||
|
||||
controller.value?.let {
|
||||
if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) {
|
||||
if (it.isPlaying) {
|
||||
if (!it.isPlaying) {
|
||||
if (keepPlayingMutex?.isPlaying == true) {
|
||||
// There is a video playing, start this one on mute.
|
||||
it.volume = 0f
|
||||
controller.value?.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
it.volume = if (defaultToStart) 0f else 1f
|
||||
controller.value?.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
}
|
||||
|
||||
it.setMediaItem(mediaItem)
|
||||
it.prepare()
|
||||
controller.value?.setMediaItem(mediaItem)
|
||||
controller.value?.prepare()
|
||||
} else if (controller.value != it) {
|
||||
// discards the new controller because there is an existing one
|
||||
it.stop()
|
||||
it.release()
|
||||
|
||||
controller.value?.let {
|
||||
if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) {
|
||||
if (it.isPlaying) {
|
||||
// There is a video playing, start this one on mute.
|
||||
it.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
it.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
|
||||
it.setMediaItem(mediaItem)
|
||||
it.prepare()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
controller.value?.let {
|
||||
if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) {
|
||||
if (it.isPlaying) {
|
||||
// There is a video playing, start this one on mute.
|
||||
it.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
it.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
} else {
|
||||
controller.value?.let {
|
||||
if (it.playbackState == Player.STATE_IDLE || it.playbackState == Player.STATE_ENDED) {
|
||||
if (it.isPlaying) {
|
||||
// There is a video playing, start this one on mute.
|
||||
it.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
it.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
|
||||
it.setMediaItem(mediaItem)
|
||||
it.prepare()
|
||||
it.setMediaItem(mediaItem)
|
||||
it.prepare()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -366,34 +371,36 @@ fun GetVideoController(
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
// if the controller is null, restarts the controller with a new one
|
||||
// if the controller is not null, just continue playing what the controller was playing
|
||||
if (controller.value == null) {
|
||||
PlaybackClientController.prepareController(
|
||||
uid,
|
||||
videoUri,
|
||||
nostrUriCallback,
|
||||
context
|
||||
) {
|
||||
// checks again to make sure no other thread has created a controller.
|
||||
if (controller.value == null) {
|
||||
controller.value = it
|
||||
scope.launch(Dispatchers.IO) {
|
||||
if (controller.value == null) {
|
||||
PlaybackClientController.prepareController(
|
||||
uid,
|
||||
videoUri,
|
||||
nostrUriCallback,
|
||||
context
|
||||
) {
|
||||
// checks again to make sure no other thread has created a controller.
|
||||
if (controller.value == null) {
|
||||
controller.value = it
|
||||
|
||||
if (!it.isPlaying) {
|
||||
if (keepPlayingMutex?.isPlaying == true) {
|
||||
// There is a video playing, start this one on mute.
|
||||
controller.value?.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
controller.value?.volume = if (defaultToStart) 0f else 1f
|
||||
if (!it.isPlaying) {
|
||||
if (keepPlayingMutex?.isPlaying == true) {
|
||||
// There is a video playing, start this one on mute.
|
||||
controller.value?.volume = 0f
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
controller.value?.volume = if (defaultToStart) 0f else 1f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
controller.value?.setMediaItem(mediaItem)
|
||||
controller.value?.prepare()
|
||||
} else if (controller.value != it) {
|
||||
// discards the new controller because there is an existing one
|
||||
it.stop()
|
||||
it.release()
|
||||
controller.value?.setMediaItem(mediaItem)
|
||||
controller.value?.prepare()
|
||||
} else if (controller.value != it) {
|
||||
// discards the new controller because there is an existing one
|
||||
it.stop()
|
||||
it.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -519,7 +526,8 @@ private fun RenderVideoPlayer(
|
||||
.defaultMinSize(minHeight = 100.dp)
|
||||
.align(Alignment.Center)
|
||||
} else {
|
||||
Modifier.fillMaxWidth()
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.defaultMinSize(minHeight = 100.dp)
|
||||
.align(Alignment.Center)
|
||||
},
|
||||
|
||||
@@ -32,6 +32,32 @@ class HiddenAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
}
|
||||
}
|
||||
|
||||
class HiddenWordsFeedFilter(val account: Account) : FeedFilter<String>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
}
|
||||
|
||||
override fun showHiddenKey(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun feed(): List<String> {
|
||||
val blockList = account.getBlockList()
|
||||
val decryptedContent = blockList?.decryptedContent ?: ""
|
||||
if (account.loginWithExternalSigner) {
|
||||
if (decryptedContent.isEmpty()) return emptyList()
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateWords(decryptedContent)?.toList()
|
||||
?: emptyList()
|
||||
}
|
||||
|
||||
return blockList
|
||||
?.publicAndPrivateWords(account.keyPair.privKey)?.toList()
|
||||
?: emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
class SpammerAccountsFeedFilter(val account: Account) : FeedFilter<User>() {
|
||||
override fun feedKey(): String {
|
||||
return account.userProfile().pubkeyHex
|
||||
|
||||
@@ -292,7 +292,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
SendButton() {
|
||||
accountViewModel.createStatus(currentStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
@@ -338,7 +338,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
if (hasChanged) {
|
||||
UserStatusSendButton() {
|
||||
SendButton() {
|
||||
accountViewModel.updateStatus(it, thisStatus.value)
|
||||
focusManager.clearFocus(true)
|
||||
}
|
||||
@@ -356,7 +356,7 @@ private fun EditStatusBox(baseAccountUser: User, accountViewModel: AccountViewMo
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun UserStatusSendButton(onClick: () -> Unit) {
|
||||
fun SendButton(onClick: () -> Unit) {
|
||||
IconButton(
|
||||
modifier = Size26Modifier,
|
||||
onClick = onClick
|
||||
@@ -508,17 +508,19 @@ fun ListContent(
|
||||
route = Route.BlockedUsers.route
|
||||
)
|
||||
|
||||
IconRow(
|
||||
title = stringResource(R.string.backup_keys),
|
||||
icon = R.drawable.ic_key,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
accountViewModel.account.keyPair.privKey?.let {
|
||||
IconRow(
|
||||
title = stringResource(R.string.backup_keys),
|
||||
icon = R.drawable.ic_key,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
scaffoldState.drawerState.close()
|
||||
}
|
||||
backupDialogOpen = true
|
||||
}
|
||||
backupDialogOpen = true
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short)
|
||||
IconRow(
|
||||
|
||||
@@ -2250,14 +2250,23 @@ fun RemoveButton(onClick: () -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AddButton(text: Int = R.string.add, onClick: () -> Unit) {
|
||||
fun AddButton(
|
||||
text: Int = R.string.add,
|
||||
isActive: Boolean = true,
|
||||
modifier: Modifier = Modifier.padding(start = 3.dp),
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.padding(start = 3.dp),
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
onClick = {
|
||||
if (isActive) {
|
||||
onClick()
|
||||
}
|
||||
},
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
|
||||
@@ -88,7 +88,7 @@ class AccountStateViewModel(val context: Context) : ViewModel() {
|
||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.IO) {
|
||||
ServiceManager.start(account, context)
|
||||
ServiceManager.restartIfDifferentAccount(account, context)
|
||||
}
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
account.saveable.observeForever(saveListener)
|
||||
|
||||
@@ -258,9 +258,7 @@ fun FeedError(errorMessage: String, onRefresh: () -> Unit) {
|
||||
@Composable
|
||||
fun FeedEmpty(onRefresh: () -> Unit) {
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxHeight()
|
||||
.fillMaxWidth(),
|
||||
Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
sealed class StringFeedState {
|
||||
object Loading : StringFeedState()
|
||||
class Loaded(val feed: MutableState<ImmutableList<String>>) : StringFeedState()
|
||||
object Empty : StringFeedState()
|
||||
class FeedError(val errorMessage: String) : StringFeedState()
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.OutlinedButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
@Composable
|
||||
fun RefreshingFeedStringFeedView(
|
||||
viewModel: StringFeedViewModel,
|
||||
enablePullRefresh: Boolean = true,
|
||||
inner: @Composable (String) -> Unit
|
||||
) {
|
||||
RefresheableView(viewModel, enablePullRefresh) {
|
||||
StringFeedView(viewModel, inner = inner)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StringFeedView(
|
||||
viewModel: StringFeedViewModel,
|
||||
pre: (@Composable () -> Unit)? = null,
|
||||
post: (@Composable () -> Unit)? = null,
|
||||
inner: @Composable (String) -> Unit
|
||||
) {
|
||||
val feedState by viewModel.feedContent.collectAsState()
|
||||
|
||||
Crossfade(targetState = feedState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||
when (state) {
|
||||
is StringFeedState.Empty -> {
|
||||
StringFeedEmpty(pre, post) {
|
||||
viewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
is StringFeedState.FeedError -> {
|
||||
FeedError(state.errorMessage) {
|
||||
viewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
is StringFeedState.Loaded -> {
|
||||
FeedLoaded(state, pre, post, inner)
|
||||
}
|
||||
|
||||
is StringFeedState.Loading -> {
|
||||
LoadingFeed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun StringFeedEmpty(
|
||||
pre: (@Composable () -> Unit)? = null,
|
||||
post: (@Composable () -> Unit)? = null,
|
||||
onRefresh: () -> Unit
|
||||
) {
|
||||
Column() {
|
||||
pre?.let { it() }
|
||||
|
||||
Column(
|
||||
Modifier.weight(1f).fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(stringResource(R.string.feed_is_empty))
|
||||
OutlinedButton(onClick = onRefresh) {
|
||||
Text(text = stringResource(R.string.refresh))
|
||||
}
|
||||
}
|
||||
|
||||
post?.let { it() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FeedLoaded(
|
||||
state: StringFeedState.Loaded,
|
||||
pre: (@Composable () -> Unit)? = null,
|
||||
post: (@Composable () -> Unit)? = null,
|
||||
inner: @Composable (String) -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = PaddingValues(
|
||||
top = 10.dp,
|
||||
bottom = 10.dp
|
||||
),
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
pre?.let { it() }
|
||||
}
|
||||
|
||||
itemsIndexed(state.feed.value, key = { _, item -> item }) { _, item ->
|
||||
inner(item)
|
||||
}
|
||||
|
||||
item {
|
||||
post?.let { it() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.checkNotInMainThread
|
||||
import com.vitorpamplona.amethyst.ui.components.BundledUpdate
|
||||
import com.vitorpamplona.amethyst.ui.dal.FeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HiddenWordsFeedFilter
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class NostrHiddenWordsFeedViewModel(val account: Account) : StringFeedViewModel(
|
||||
HiddenWordsFeedFilter(account)
|
||||
) {
|
||||
class Factory(val account: Account) : ViewModelProvider.Factory {
|
||||
override fun <NostrHiddenWordsFeedViewModel : ViewModel> create(modelClass: Class<NostrHiddenWordsFeedViewModel>): NostrHiddenWordsFeedViewModel {
|
||||
return NostrHiddenWordsFeedViewModel(account) as NostrHiddenWordsFeedViewModel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
open class StringFeedViewModel(val dataSource: FeedFilter<String>) : ViewModel(), InvalidatableViewModel {
|
||||
private val _feedContent = MutableStateFlow<StringFeedState>(StringFeedState.Loading)
|
||||
val feedContent = _feedContent.asStateFlow()
|
||||
|
||||
private fun refresh() {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshSuspended() {
|
||||
checkNotInMainThread()
|
||||
|
||||
val notes = dataSource.loadTop().toImmutableList()
|
||||
|
||||
val oldNotesState = _feedContent.value
|
||||
if (oldNotesState is StringFeedState.Loaded) {
|
||||
// Using size as a proxy for has changed.
|
||||
if (!equalImmutableLists(notes, oldNotesState.feed.value)) {
|
||||
updateFeed(notes)
|
||||
}
|
||||
} else {
|
||||
updateFeed(notes)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: ImmutableList<String>) {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
val currentState = _feedContent.value
|
||||
if (notes.isEmpty()) {
|
||||
_feedContent.update { StringFeedState.Empty }
|
||||
} else if (currentState is StringFeedState.Loaded) {
|
||||
// updates the current list
|
||||
currentState.feed.value = notes
|
||||
} else {
|
||||
_feedContent.update { StringFeedState.Loaded(mutableStateOf(notes)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val bundler = BundledUpdate(250, Dispatchers.IO)
|
||||
|
||||
override fun invalidateData(ignoreIfDoing: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
bundler.invalidate(ignoreIfDoing) {
|
||||
// adds the time to perform the refresh into this delay
|
||||
// holding off new updates in case of heavy refresh routines.
|
||||
refreshSuspended()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var collectorJob: Job? = null
|
||||
|
||||
init {
|
||||
Log.d("Init", this.javaClass.simpleName)
|
||||
collectorJob = viewModelScope.launch(Dispatchers.IO) {
|
||||
checkNotInMainThread()
|
||||
|
||||
LocalCache.live.newEventBundles.collect { newNotes ->
|
||||
checkNotInMainThread()
|
||||
|
||||
invalidateData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
bundler.cancel()
|
||||
collectorJob?.cancel()
|
||||
super.onCleared()
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.AddressableNote
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
@@ -97,6 +98,12 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
account.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
|
||||
}
|
||||
|
||||
fun updateAutomaticallyHideNavBars(
|
||||
automaticallyHideHavBars: BooleanType
|
||||
) {
|
||||
account.updateAutomaticallyHideHavBars(automaticallyHideHavBars)
|
||||
}
|
||||
|
||||
fun updateAutomaticallyShowImages(
|
||||
automaticallyShowImages: ConnectivityType
|
||||
) {
|
||||
@@ -484,6 +491,12 @@ class AccountViewModel(val account: Account) : ViewModel(), Dao {
|
||||
}
|
||||
}
|
||||
|
||||
fun hide(word: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.hideWord(word)
|
||||
}
|
||||
}
|
||||
|
||||
fun showUser(pubkeyHex: String) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
account.showUser(pubkeyHex)
|
||||
|
||||
+238
-8
@@ -1,20 +1,31 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
import androidx.compose.foundation.pager.rememberPagerState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.ButtonDefaults
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.ScrollableTabRow
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.TabRow
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -23,19 +34,37 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.AddButton
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenWordsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrSpammerAccountsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefresheableView
|
||||
import com.vitorpamplona.amethyst.ui.screen.RefreshingFeedUserFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.StringFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.HorzPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.TabRowHeight
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
@@ -44,17 +73,22 @@ fun HiddenUsersScreen(accountViewModel: AccountViewModel, nav: (String) -> Unit)
|
||||
factory = NostrHiddenAccountsFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val hiddenWordsFeedViewModel: NostrHiddenWordsFeedViewModel = viewModel(
|
||||
factory = NostrHiddenWordsFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
val spammerFeedViewModel: NostrSpammerAccountsFeedViewModel = viewModel(
|
||||
factory = NostrSpammerAccountsFeedViewModel.Factory(accountViewModel.account)
|
||||
)
|
||||
|
||||
HiddenUsersScreen(hiddenFeedViewModel, spammerFeedViewModel, accountViewModel, nav)
|
||||
HiddenUsersScreen(hiddenFeedViewModel, hiddenWordsFeedViewModel, spammerFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun HiddenUsersScreen(
|
||||
hiddenFeedViewModel: NostrHiddenAccountsFeedViewModel,
|
||||
hiddenWordsViewModel: NostrHiddenWordsFeedViewModel,
|
||||
spammerFeedViewModel: NostrSpammerAccountsFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
@@ -65,6 +99,7 @@ fun HiddenUsersScreen(
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Hidden Users Start")
|
||||
hiddenWordsViewModel.invalidateData()
|
||||
hiddenFeedViewModel.invalidateData()
|
||||
spammerFeedViewModel.invalidateData()
|
||||
}
|
||||
@@ -78,7 +113,7 @@ fun HiddenUsersScreen(
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
|
||||
val pagerState = rememberPagerState() { 2 }
|
||||
val pagerState = rememberPagerState() { 3 }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var warnAboutReports by remember { mutableStateOf(accountViewModel.account.warnAboutPostsWithReports) }
|
||||
var filterSpam by remember { mutableStateOf(accountViewModel.account.filterSpamFromStrangers) }
|
||||
@@ -109,8 +144,9 @@ fun HiddenUsersScreen(
|
||||
Text(stringResource(R.string.filter_spam_from_strangers))
|
||||
}
|
||||
|
||||
TabRow(
|
||||
ScrollableTabRow(
|
||||
backgroundColor = MaterialTheme.colors.background,
|
||||
edgePadding = 8.dp,
|
||||
selectedTabIndex = pagerState.currentPage,
|
||||
modifier = TabRowHeight
|
||||
) {
|
||||
@@ -128,25 +164,98 @@ fun HiddenUsersScreen(
|
||||
Text(text = stringResource(R.string.spamming_users))
|
||||
}
|
||||
)
|
||||
Tab(
|
||||
selected = pagerState.currentPage == 2,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(2) } },
|
||||
text = {
|
||||
Text(text = stringResource(R.string.hidden_words))
|
||||
}
|
||||
)
|
||||
}
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
when (page) {
|
||||
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
|
||||
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel, nav)
|
||||
0 -> RefreshingUserFeedView(hiddenFeedViewModel, accountViewModel) {
|
||||
RefreshingFeedUserFeedView(hiddenFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
1 -> RefreshingUserFeedView(spammerFeedViewModel, accountViewModel) {
|
||||
RefreshingFeedUserFeedView(spammerFeedViewModel, accountViewModel, nav)
|
||||
}
|
||||
2 -> HiddenWordsFeed(hiddenWordsViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun HiddenWordsFeed(
|
||||
hiddenWordsViewModel: NostrHiddenWordsFeedViewModel,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
RefresheableView(hiddenWordsViewModel, false) {
|
||||
StringFeedView(
|
||||
hiddenWordsViewModel,
|
||||
post = { AddMuteWordTextField(accountViewModel) }
|
||||
) {
|
||||
MutedWordHeader(tag = it, account = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AddMuteWordTextField(accountViewModel: AccountViewModel) {
|
||||
Row(modifier = Modifier.padding(vertical = Size10dp)) {
|
||||
val currentWordToAdd = remember {
|
||||
mutableStateOf("")
|
||||
}
|
||||
val hasChanged by remember {
|
||||
derivedStateOf {
|
||||
currentWordToAdd.value != ""
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
value = currentWordToAdd.value,
|
||||
onValueChange = { currentWordToAdd.value = it },
|
||||
label = { Text(text = stringResource(R.string.hide_new_word_label)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.hide_new_word_label),
|
||||
color = MaterialTheme.colors.placeholderText
|
||||
)
|
||||
},
|
||||
keyboardOptions = KeyboardOptions.Default.copy(
|
||||
imeAction = ImeAction.Send,
|
||||
capitalization = KeyboardCapitalization.Sentences
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSend = {
|
||||
if (hasChanged) {
|
||||
accountViewModel.hide(currentWordToAdd.value)
|
||||
currentWordToAdd.value = ""
|
||||
}
|
||||
}
|
||||
),
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
AddButton(isActive = hasChanged, modifier = HorzPadding) {
|
||||
accountViewModel.hide(currentWordToAdd.value)
|
||||
currentWordToAdd.value = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RefreshingUserFeedView(
|
||||
feedViewModel: UserFeedViewModel,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
inner: @Composable () -> Unit
|
||||
) {
|
||||
WatchAccountAndBlockList(feedViewModel, accountViewModel)
|
||||
RefreshingFeedUserFeedView(feedViewModel, accountViewModel, nav)
|
||||
inner()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -161,3 +270,124 @@ fun WatchAccountAndBlockList(
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MutedWordHeader(tag: String, modifier: Modifier = StdPadding, account: AccountViewModel) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
tag,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
MutedWordActionOptions(tag, account)
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MutedWordActionOptions(
|
||||
word: String,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val isMutedWord by accountViewModel.account.liveHiddenUsers.map {
|
||||
word in it.hiddenWords
|
||||
}.distinctUntilChanged().observeAsState()
|
||||
|
||||
if (isMutedWord == true) {
|
||||
ShowWordButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.showWord(word)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_unfollow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.showWord(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HideWordButton {
|
||||
if (!accountViewModel.isWriteable()) {
|
||||
if (accountViewModel.loggedInWithExternalSigner()) {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.hideWord(word)
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_follow),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.account.hideWord(word)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun HideWordButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
onClick = onClick,
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.block_only), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShowWordButton(text: Int = R.string.unblock, onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(start = 3.dp),
|
||||
onClick = onClick,
|
||||
shape = ButtonBorder,
|
||||
colors = ButtonDefaults
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavBackStackEntry
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.vitorpamplona.amethyst.model.BooleanType
|
||||
import com.vitorpamplona.amethyst.ui.buttons.ChannelFabColumn
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewCommunityNoteButton
|
||||
import com.vitorpamplona.amethyst.ui.buttons.NewImageButton
|
||||
@@ -206,15 +207,18 @@ fun MainScreen(
|
||||
val nestedScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||
val delta = available.y
|
||||
val newOffset = bottomBarOffsetHeightPx.value + delta
|
||||
val newOffset = bottomBarOffsetHeightPx.value + available.y
|
||||
|
||||
val currentRoute = navState.value?.destination?.route
|
||||
|
||||
bottomBarOffsetHeightPx.value = if (currentRoute !in InvertedLayouts) {
|
||||
newOffset.coerceIn(-bottomBarHeightPx, 0f)
|
||||
if (accountViewModel.account.settings.automaticallyHideNavigationBars == BooleanType.ALWAYS) {
|
||||
bottomBarOffsetHeightPx.value = if (navState.value?.destination?.route !in InvertedLayouts) {
|
||||
newOffset.coerceIn(-bottomBarHeightPx, 0f)
|
||||
} else {
|
||||
newOffset.coerceIn(0f, bottomBarHeightPx)
|
||||
}
|
||||
} else {
|
||||
newOffset.coerceIn(0f, bottomBarHeightPx)
|
||||
if (abs(bottomBarOffsetHeightPx.value) > 0.1) {
|
||||
bottomBarOffsetHeightPx.value = 0f
|
||||
}
|
||||
}
|
||||
|
||||
return Offset.Zero
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.core.os.LocaleListCompat
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.ConnectivityType
|
||||
import com.vitorpamplona.amethyst.model.parseBooleanType
|
||||
import com.vitorpamplona.amethyst.model.parseConnectivityType
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
@@ -122,10 +123,16 @@ fun SettingsScreen(
|
||||
TitleExplainer(stringResource(R.string.dark))
|
||||
)
|
||||
|
||||
val booleanItems = persistentListOf(
|
||||
TitleExplainer(stringResource(ConnectivityType.ALWAYS.reourceId)),
|
||||
TitleExplainer(stringResource(ConnectivityType.NEVER.reourceId))
|
||||
)
|
||||
|
||||
val settings = accountViewModel.account.settings
|
||||
val showImagesIndex = settings.automaticallyShowImages.screenCode
|
||||
val videoIndex = settings.automaticallyStartPlayback.screenCode
|
||||
val linkIndex = settings.automaticallyShowUrlPreview.screenCode
|
||||
val hideNavBarsIndex = settings.automaticallyHideNavigationBars.screenCode
|
||||
|
||||
val themeIndex = themeViewModel.theme.value ?: 0
|
||||
|
||||
@@ -220,6 +227,22 @@ fun SettingsScreen(
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = HalfVertSpacer)
|
||||
|
||||
SettingsRow(
|
||||
R.string.automatically_hide_nav_bars,
|
||||
R.string.automatically_hide_nav_bars_description,
|
||||
booleanItems,
|
||||
hideNavBarsIndex
|
||||
) {
|
||||
val automaticallyHideNavBars = parseBooleanType(it)
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateAutomaticallyHideNavBars(automaticallyHideNavBars)
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@ val StdPadding = Modifier.padding(10.dp)
|
||||
val HalfHorzPadding = Modifier.padding(horizontal = 5.dp)
|
||||
val HalfVertPadding = Modifier.padding(vertical = 5.dp)
|
||||
|
||||
val HorzPadding = Modifier.padding(horizontal = 10.dp)
|
||||
val VertPadding = Modifier.padding(vertical = 10.dp)
|
||||
|
||||
val Size6Modifier = Modifier.size(6.dp)
|
||||
val Size10Modifier = Modifier.size(10.dp)
|
||||
val Size15Modifier = Modifier.size(15.dp)
|
||||
|
||||
@@ -428,6 +428,8 @@
|
||||
<string name="automatically_load_images_gifs">Ladda automatiskt bilder/gif</string>
|
||||
<string name="automatically_play_videos">Spela automatiskt videor</string>
|
||||
<string name="automatically_show_url_preview">Visa automatiskt förhandsgranskning av URL</string>
|
||||
<string name="automatically_hide_nav_bars">Omslutande bläddring</string>
|
||||
<string name="automatically_hide_nav_bars_description">Dölj navigeringsfält vid bläddring</string>
|
||||
<string name="load_image">Ladda bild</string>
|
||||
<string name="spamming_users">Spammare</string>
|
||||
<string name="muted_button">Ljud avstängt. Klicka för att ta bort ljudlöst</string>
|
||||
@@ -490,6 +492,7 @@
|
||||
<string name="zap_split_serarch_and_add_user_placeholder">Användarnamn eller visningsnamn</string>
|
||||
<string name="user_x_does_not_have_a_lightning_address_setup_to_receive_sats">Användaren %1$s har inte ställt in en blixtfakturaadress för att ta emot satoshis</string>
|
||||
<string name="zap_split_weight">Procent</string>
|
||||
<string name="zap_split_weight_placeholder">25</string>
|
||||
<string name="splitting_zaps_with">Delar zaps med</string>
|
||||
<string name="forwarding_zaps_to">Vidarebefordrar zaps till</string>
|
||||
<string name="lightning_wallets_not_found2">Blixtpengar hittades inte</string>
|
||||
|
||||
@@ -499,6 +499,8 @@
|
||||
<string name="automatically_load_images_gifs">Image Preview</string>
|
||||
<string name="automatically_play_videos">Video Playback</string>
|
||||
<string name="automatically_show_url_preview">URL Preview</string>
|
||||
<string name="automatically_hide_nav_bars">Immersive Scrolling</string>
|
||||
<string name="automatically_hide_nav_bars_description">Hide Nav Bars when Scrolling</string>
|
||||
<string name="load_image">Load Image</string>
|
||||
|
||||
<string name="spamming_users">Spammers</string>
|
||||
@@ -593,4 +595,7 @@
|
||||
|
||||
<string name="no_wallet_found_with_error">No Wallets found to pay a lightning invoice (Error: %1$s). Please install a Lightning wallet to use zaps</string>
|
||||
<string name="no_wallet_found">No Wallets found to pay a lightning invoice. Please install a Lightning wallet to use zaps</string>
|
||||
|
||||
<string name="hidden_words">Hidden Words</string>
|
||||
<string name="hide_new_word_label">Hide new word or sentence</string>
|
||||
</resources>
|
||||
|
||||
@@ -16,6 +16,7 @@ class BookmarkListEvent(
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient
|
||||
var decryptedContent = ""
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -125,14 +125,20 @@ open class Event(
|
||||
|
||||
override fun matchTag1With(text: String) = tags.any { it.size > 1 && it[1].contains(text, true) }
|
||||
|
||||
override fun isTaggedUser(idHex: String) = tags.any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||
override fun isTaggedUsers(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "p" && it[1] in idHexes }
|
||||
override fun isTagged(key: String, tag: String) = tags.any { it.size > 1 && it[0] == key && it[1] == tag }
|
||||
|
||||
override fun isTaggedEvent(idHex: String) = tags.any { it.size > 1 && it[0] == "e" && it[1] == idHex }
|
||||
override fun isAnyTagged(key: String, tags: Set<String>) = this.tags.any { it.size > 1 && it[0] == key && it[1] in tags }
|
||||
|
||||
override fun isTaggedAddressableNote(idHex: String) = tags.any { it.size > 1 && it[0] == "a" && it[1] == idHex }
|
||||
override fun isTaggedWord(word: String) = isTagged("word", word)
|
||||
|
||||
override fun isTaggedAddressableNotes(idHexes: Set<String>) = tags.any { it.size > 1 && it[0] == "a" && it[1] in idHexes }
|
||||
override fun isTaggedUser(idHex: String) = isTagged("p", idHex)
|
||||
override fun isTaggedUsers(idHexes: Set<String>) = isAnyTagged("p", idHexes)
|
||||
|
||||
override fun isTaggedEvent(idHex: String) = isTagged("e", idHex)
|
||||
|
||||
override fun isTaggedAddressableNote(idHex: String) = isTagged("a", idHex)
|
||||
|
||||
override fun isTaggedAddressableNotes(idHexes: Set<String>) = isAnyTagged( "a", idHexes)
|
||||
|
||||
override fun isTaggedHash(hashtag: String) = tags.any { it.size > 1 && it[0] == "t" && it[1].equals(hashtag, true) }
|
||||
|
||||
|
||||
@@ -29,6 +29,10 @@ interface EventInterface {
|
||||
|
||||
fun hasValidSignature(): Boolean
|
||||
|
||||
fun isTagged(key: String, tag: String): Boolean
|
||||
fun isAnyTagged(key: String, tags: Set<String>): Boolean
|
||||
fun isTaggedWord(word: String): Boolean
|
||||
|
||||
fun isTaggedUser(idHex: String): Boolean
|
||||
fun isTaggedUsers(idHex: Set<String>): Boolean
|
||||
|
||||
|
||||
@@ -66,7 +66,8 @@ abstract class GeneralListEvent(
|
||||
return privateTagsCache
|
||||
}
|
||||
|
||||
fun privateTagsOrEmpty(privKey: ByteArray): List<List<String>> {
|
||||
fun privateTagsOrEmpty(privKey: ByteArray?): List<List<String>> {
|
||||
if (privKey == null) return emptyList()
|
||||
return privateTags(privKey) ?: emptyList()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,62 +18,94 @@ class PeopleListEvent(
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : GeneralListEvent(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient
|
||||
var decryptedContent: String? = null
|
||||
@Transient
|
||||
var publicAndPrivateUserCache: ImmutableSet<HexKey>? = null
|
||||
@Transient
|
||||
var publicAndPrivateWordCache: ImmutableSet<String>? = null
|
||||
|
||||
fun filterTagList(key: String, privateTags: List<List<String>>?): ImmutableSet<String> {
|
||||
val privateUserList = privateTags?.let {
|
||||
it.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet()
|
||||
} ?: emptySet()
|
||||
val publicUserList = tags.filter { it.size > 1 && it[0] == key }.map { it[1] }.toSet()
|
||||
|
||||
return (privateUserList + publicUserList).toImmutableSet()
|
||||
}
|
||||
|
||||
fun publicAndPrivateWords(privateKey: ByteArray?): ImmutableSet<String> {
|
||||
publicAndPrivateWordCache?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
publicAndPrivateWordCache = filterTagList("word", privateTagsOrEmpty(privKey = privateKey))
|
||||
|
||||
return publicAndPrivateWordCache ?: persistentSetOf()
|
||||
}
|
||||
|
||||
fun publicAndPrivateUsers(privateKey: ByteArray?): ImmutableSet<HexKey> {
|
||||
publicAndPrivateUserCache?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val privateUserList = privateKey?.let {
|
||||
privateTagsOrEmpty(privKey = it).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
|
||||
} ?: emptySet()
|
||||
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
|
||||
|
||||
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
|
||||
publicAndPrivateUserCache = filterTagList("p", privateTagsOrEmpty(privKey = privateKey))
|
||||
|
||||
return publicAndPrivateUserCache ?: persistentSetOf()
|
||||
}
|
||||
|
||||
fun publicAndPrivateWords(decryptedContent: String): ImmutableSet<HexKey> {
|
||||
publicAndPrivateWordCache?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
publicAndPrivateWordCache = filterTagList("word", privateTagsOrEmpty(decryptedContent))
|
||||
|
||||
return publicAndPrivateWordCache ?: persistentSetOf()
|
||||
}
|
||||
|
||||
fun publicAndPrivateUsers(decryptedContent: String): ImmutableSet<HexKey> {
|
||||
publicAndPrivateUserCache?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val privateUserList = privateTagsOrEmpty(decryptedContent).filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
|
||||
|
||||
val publicUserList = tags.filter { it.size > 1 && it[0] == "p" }.map { it[1] }.toSet()
|
||||
|
||||
publicAndPrivateUserCache = (privateUserList + publicUserList).toImmutableSet()
|
||||
publicAndPrivateUserCache = filterTagList("p", privateTagsOrEmpty(decryptedContent))
|
||||
|
||||
return publicAndPrivateUserCache ?: persistentSetOf()
|
||||
}
|
||||
|
||||
fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray): Boolean {
|
||||
fun isTagged(key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray): Boolean {
|
||||
return if (isPrivate) {
|
||||
privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||
privateTagsOrEmpty(privKey = privateKey).any { it.size > 1 && it[0] == key && it[1] == tag }
|
||||
} else {
|
||||
isTaggedUser(idHex)
|
||||
isTagged(key, tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun isTaggedUser(idHex: String, isPrivate: Boolean, content: String): Boolean {
|
||||
fun isTagged(key: String, tag: String, isPrivate: Boolean, decryptedContent: String): Boolean {
|
||||
return if (isPrivate) {
|
||||
privateTagsOrEmpty(content).any { it.size > 1 && it[0] == "p" && it[1] == idHex }
|
||||
privateTagsOrEmpty(decryptedContent).any { it.size > 1 && it[0] == key && it[1] == tag }
|
||||
} else {
|
||||
isTaggedUser(idHex)
|
||||
isTagged(key, tag)
|
||||
}
|
||||
}
|
||||
|
||||
fun isTaggedWord(word: String, isPrivate: Boolean, privateKey: ByteArray) = isTagged( "word", word, isPrivate, privateKey)
|
||||
|
||||
fun isTaggedUser(idHex: String, isPrivate: Boolean, privateKey: ByteArray) = isTagged( "p", idHex, isPrivate, privateKey)
|
||||
|
||||
fun isTaggedUser(idHex: String, isPrivate: Boolean, decryptedContent: String) = isTagged( "p", idHex, isPrivate, decryptedContent)
|
||||
|
||||
fun isTaggedWord(idHex: String, isPrivate: Boolean, decryptedContent: String) = isTagged( "word", idHex, isPrivate, decryptedContent)
|
||||
|
||||
companion object {
|
||||
const val kind = 30000
|
||||
const val blockList = "mute"
|
||||
|
||||
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
fun createListWithTag(name: String, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(listOf(listOf("p", pubKeyHex)), privateKey),
|
||||
content = encryptTags(listOf(listOf(key, tag)), privateKey),
|
||||
tags = listOf(listOf("d", name)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
@@ -81,13 +113,21 @@ class PeopleListEvent(
|
||||
} else {
|
||||
create(
|
||||
content = "",
|
||||
tags = listOf(listOf("d", name), listOf("p", pubKeyHex)),
|
||||
tags = listOf(listOf("d", name), listOf(key, tag)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return createListWithTag(name, "p", pubKeyHex, isPrivate, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun createListWithWord(name: String, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return createListWithTag(name, "word", word, isPrivate, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun createListWithUser(name: String, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
@@ -106,6 +146,24 @@ class PeopleListEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun createListWithWord(name: String, word: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptedContent,
|
||||
tags = listOf(listOf("d", name)),
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = "",
|
||||
tags = listOf(listOf("d", name), listOf("word", word)),
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addUsers(earlierVersion: PeopleListEvent, listPubKeyHex: List<String>, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
@@ -135,6 +193,24 @@ class PeopleListEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptedContent,
|
||||
tags = earlierVersion.tags,
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("word", word)),
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
@@ -153,13 +229,40 @@ class PeopleListEvent(
|
||||
}
|
||||
}
|
||||
|
||||
fun removeTag(earlierVersion: PeopleListEvent, tag: String, isPrivate: Boolean, pubKey: HexKey, encryptedContent: String, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptedContent,
|
||||
tags = earlierVersion.tags,
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != tag },
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return addTag(earlierVersion, "word", word, isPrivate, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun addUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
if (earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
|
||||
return addTag(earlierVersion, "p", pubKeyHex, isPrivate, privateKey, createdAt)
|
||||
}
|
||||
|
||||
|
||||
fun addTag(earlierVersion: PeopleListEvent, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
if (earlierVersion.isTagged(key, tag, isPrivate, privateKey)) return earlierVersion
|
||||
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf("p", pubKeyHex)),
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).plus(element = listOf(key, tag)),
|
||||
privateKey = privateKey
|
||||
),
|
||||
tags = earlierVersion.tags,
|
||||
@@ -169,48 +272,38 @@ class PeopleListEvent(
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.plus(element = listOf("p", pubKeyHex)),
|
||||
tags = earlierVersion.tags.plus(element = listOf(key, tag)),
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, encryptedContent: String, pubKey: HexKey, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptedContent,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
pubKey = pubKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
}
|
||||
fun removeWord(earlierVersion: PeopleListEvent, word: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
return removeTag(earlierVersion, "word", word, isPrivate, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun removeUser(earlierVersion: PeopleListEvent, pubKeyHex: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
if (!earlierVersion.isTaggedUser(pubKeyHex, isPrivate, privateKey)) return earlierVersion
|
||||
return removeTag(earlierVersion, "p", pubKeyHex, isPrivate, privateKey, createdAt)
|
||||
}
|
||||
|
||||
fun removeTag(earlierVersion: PeopleListEvent, key: String, tag: String, isPrivate: Boolean, privateKey: ByteArray, createdAt: Long = TimeUtils.now()): PeopleListEvent {
|
||||
if (!earlierVersion.isTagged(key, tag, isPrivate, privateKey)) return earlierVersion
|
||||
|
||||
return if (isPrivate) {
|
||||
create(
|
||||
content = encryptTags(
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
privateTags = earlierVersion.privateTagsOrEmpty(privKey = privateKey).filter { it.size > 1 && !(it[0] == key && it[1] == tag) },
|
||||
privateKey = privateKey
|
||||
),
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
} else {
|
||||
create(
|
||||
content = earlierVersion.content,
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && it[1] != pubKeyHex },
|
||||
tags = earlierVersion.tags.filter { it.size > 1 && !(it[0] == key && it[1] == tag) },
|
||||
privateKey = privateKey,
|
||||
createdAt = createdAt
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user