mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-07 07:05:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e7a955ada | ||
|
|
4aa961437b | ||
|
|
0d9399a1a4 | ||
|
|
9b62cf563a | ||
|
|
7c525e2f2f | ||
|
|
63888d55fd | ||
|
|
7287ead851 | ||
|
|
6fc829f4f1 | ||
|
|
d12b0ce9be | ||
|
|
393714eb42 | ||
|
|
fe363b8a2d | ||
|
|
064ce7e6e6 | ||
|
|
af72846998 | ||
|
|
899b4f7c90 | ||
|
|
2e5b41b711 | ||
|
|
809f048b44 | ||
|
|
51c0c44a2e | ||
|
|
811d6d8c67 | ||
|
|
c1b9bf42ad | ||
|
|
cafe742cd8 | ||
|
|
ba50ec052b | ||
|
|
386f97abf8 | ||
|
|
7678b1b100 | ||
|
|
9df82a78e7 | ||
|
|
c87466ada4 | ||
|
|
135eb6763a | ||
|
|
3254479ee5 | ||
|
|
6cc4266435 | ||
|
|
3e48b293b0 | ||
|
|
492b9494ec | ||
|
|
b31d2d49d6 | ||
|
|
245bd4a44d | ||
|
|
6d4a887d00 | ||
|
|
f95023f59d | ||
|
|
e69c1cf3a9 | ||
|
|
b5229f5fe1 | ||
|
|
e2ef7cfd70 | ||
|
|
6e89e69fb6 | ||
|
|
1e6ee67932 | ||
|
|
0fead83279 | ||
|
|
66952c9212 | ||
|
|
38bb9f457d | ||
|
|
3e615bb152 | ||
|
|
f83a19260f | ||
|
|
69c3c41196 | ||
|
|
6c1a4f3971 | ||
|
|
035ba899ee | ||
|
|
33cc261b62 | ||
|
|
fee6f19d5e | ||
|
|
68e8a13f73 | ||
|
|
a639c3f36d | ||
|
|
a5f10fef60 | ||
|
|
d334c04c47 | ||
|
|
7c3d9aad1d | ||
|
|
ceb81aa2bf |
@@ -63,7 +63,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] Custom Emoji (NIP-30)
|
||||
- [x] Zap Forwarding (NIP-57)
|
||||
- [x] Text Note References (NIP-27)
|
||||
- [x] Audio Tracks (zapstr.live) (NIP-TBD)
|
||||
- [x] Audio Tracks (zapstr.live) (Kind:31337)
|
||||
- [x] Push Notifications (Zaps and Messages)
|
||||
- [x] Generic Tags (NIP-12)
|
||||
- [x] Sensitive Content (NIP-36)
|
||||
@@ -76,6 +76,9 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases)
|
||||
- [x] HTTP Auth (NIP-98)
|
||||
- [x] Zapraiser (NIP-TBD)
|
||||
- [x] Moderated Communities (NIP-172)
|
||||
- [x] Emoji Packs (Kind:30030)
|
||||
- [x] Personal Emoji Lists (Kind:10030)
|
||||
- [x] Classifieds (Kind:30403)
|
||||
- [ ] Marketplace (NIP-15)
|
||||
- [ ] Image/Video Capture in the app
|
||||
- [ ] Local Database
|
||||
|
||||
+6
-3
@@ -13,14 +13,14 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 246
|
||||
versionName "0.67.0"
|
||||
versionCode 250
|
||||
versionName "0.68.2"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary true
|
||||
}
|
||||
resConfigs("ar", "cs", "eo", "es", "fa", "fr", "hu", "night", "nl", "pt-rBR", "ru", "sv-rSE", "ta", "tr", "uk", "zh", "sh-rHK", "zh-rTW" )
|
||||
resConfigs("ar", "cs", "eo", "es", "fa", "fr", "hu", "night", "nl", "pt-rBR", "ru", "sv-rSE", "ta", "tr", "uk", "zh", "sh-rHK", "zh-rTW", "ja")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@@ -173,6 +173,9 @@ dependencies {
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-ui-material:a0954aba63')
|
||||
implementation('com.github.vitorpamplona.compose-richtext:richtext-commonmark:a0954aba63')
|
||||
|
||||
// Language picker and Theme chooser
|
||||
implementation 'androidx.appcompat:appcompat:1.6.1'
|
||||
|
||||
// Local model for language identification
|
||||
playImplementation 'com.google.mlkit:language-id:17.0.4'
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS
|
||||
import com.vitorpamplona.amethyst.model.RelaySetupInfo
|
||||
import com.vitorpamplona.amethyst.model.Settings
|
||||
import com.vitorpamplona.amethyst.model.hexToByteArray
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
||||
@@ -67,6 +68,11 @@ private object PrefKeys {
|
||||
const val WARN_ABOUT_REPORTS = "warn_about_reports"
|
||||
const val FILTER_SPAM_FROM_STRANGERS = "filter_spam_from_strangers"
|
||||
const val LAST_READ_PER_ROUTE = "last_read_route_per_route"
|
||||
const val AUTOMATICALLY_SHOW_IMAGES = "automatically_show_images"
|
||||
const val AUTOMATICALLY_START_PLAYBACK = "automatically_start_playback"
|
||||
const val THEME = "theme"
|
||||
const val PREFERRED_LANGUAGE = "preferred_Language"
|
||||
const val AUTOMATICALLY_LOAD_URL_PREVIEW = "automatically_load_url_preview"
|
||||
val LAST_READ: (String) -> String = { route -> "last_read_route_$route" }
|
||||
}
|
||||
|
||||
@@ -236,6 +242,47 @@ object LocalPreferences {
|
||||
putBoolean(PrefKeys.SHOW_SENSITIVE_CONTENT, account.showSensitiveContent!!)
|
||||
}
|
||||
}.apply()
|
||||
|
||||
val globalPrefs = encryptedPreferences()
|
||||
globalPrefs.edit().apply {
|
||||
if (account.settings.automaticallyShowImages == null) {
|
||||
remove(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, account.settings.automaticallyShowImages!!)
|
||||
}
|
||||
|
||||
if (account.settings.automaticallyStartPlayback == null) {
|
||||
remove(PrefKeys.AUTOMATICALLY_START_PLAYBACK)
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, account.settings.automaticallyStartPlayback!!)
|
||||
}
|
||||
if (account.settings.automaticallyShowUrlPreview == null) {
|
||||
remove(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)
|
||||
} else {
|
||||
putBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, account.settings.automaticallyShowUrlPreview!!)
|
||||
}
|
||||
putString(PrefKeys.PREFERRED_LANGUAGE, account.settings.preferredLanguage ?: "")
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun updateTheme(theme: Int) {
|
||||
encryptedPreferences().edit().apply {
|
||||
putInt(PrefKeys.THEME, theme)
|
||||
}.apply()
|
||||
}
|
||||
|
||||
fun getTheme(): Int {
|
||||
encryptedPreferences().apply {
|
||||
return getInt(PrefKeys.THEME, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPreferredLanguage(): String {
|
||||
var language = ""
|
||||
encryptedPreferences().apply {
|
||||
language = getString(PrefKeys.PREFERRED_LANGUAGE, "") ?: ""
|
||||
}
|
||||
return language
|
||||
}
|
||||
|
||||
fun loadFromEncryptedStorage(): Account? {
|
||||
@@ -340,6 +387,28 @@ object LocalPreferences {
|
||||
mapOf()
|
||||
}
|
||||
|
||||
val settings = Settings()
|
||||
encryptedPreferences().apply {
|
||||
settings.automaticallyShowImages = if (contains(PrefKeys.AUTOMATICALLY_SHOW_IMAGES)) {
|
||||
getBoolean(PrefKeys.AUTOMATICALLY_SHOW_IMAGES, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
settings.automaticallyStartPlayback = if (contains(PrefKeys.AUTOMATICALLY_START_PLAYBACK)) {
|
||||
getBoolean(PrefKeys.AUTOMATICALLY_START_PLAYBACK, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
settings.automaticallyShowUrlPreview = if (contains(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW)) {
|
||||
getBoolean(PrefKeys.AUTOMATICALLY_LOAD_URL_PREVIEW, false)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
settings.preferredLanguage = getString(PrefKeys.PREFERRED_LANGUAGE, "")
|
||||
}
|
||||
|
||||
val a = Account(
|
||||
loggedIn = Persona(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray()),
|
||||
followingChannels = followingChannels,
|
||||
@@ -366,7 +435,8 @@ object LocalPreferences {
|
||||
showSensitiveContent = showSensitiveContent,
|
||||
warnAboutPostsWithReports = warnAboutReports,
|
||||
filterSpamFromStrangers = filterSpam,
|
||||
lastReadPerRoute = lastReadPerRoute
|
||||
lastReadPerRoute = lastReadPerRoute,
|
||||
settings = settings
|
||||
)
|
||||
|
||||
return a
|
||||
|
||||
@@ -73,7 +73,8 @@ class Account(
|
||||
var showSensitiveContent: Boolean? = null,
|
||||
var warnAboutPostsWithReports: Boolean = true,
|
||||
var filterSpamFromStrangers: Boolean = true,
|
||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>()
|
||||
var lastReadPerRoute: Map<String, Long> = mapOf<String, Long>(),
|
||||
var settings: Settings = Settings()
|
||||
) {
|
||||
var transientHiddenUsers: Set<String> = setOf()
|
||||
|
||||
@@ -84,6 +85,29 @@ class Account(
|
||||
val saveable: AccountLiveData = AccountLiveData(this)
|
||||
|
||||
var userProfileCache: User? = null
|
||||
fun updateAutomaticallyStartPlayback(
|
||||
automaticallyStartPlayback: Boolean?
|
||||
) {
|
||||
settings.automaticallyStartPlayback = automaticallyStartPlayback
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun updateAutomaticallyShowUrlPreview(
|
||||
automaticallyShowUrlPreview: Boolean?
|
||||
) {
|
||||
settings.automaticallyShowUrlPreview = automaticallyShowUrlPreview
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun updateAutomaticallyShowImages(
|
||||
automaticallyShowImages: Boolean?
|
||||
) {
|
||||
settings.automaticallyShowImages = automaticallyShowImages
|
||||
live.invalidateData()
|
||||
saveable.invalidateData()
|
||||
}
|
||||
|
||||
fun updateOptOutOptions(warnReports: Boolean, filterSpam: Boolean) {
|
||||
warnAboutPostsWithReports = warnReports
|
||||
|
||||
@@ -402,6 +402,25 @@ object LocalCache {
|
||||
}
|
||||
}
|
||||
|
||||
private fun consume(event: ClassifiedsEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
val author = getOrCreateUser(event.pubKey)
|
||||
|
||||
if (version.event == null) {
|
||||
version.loadEvent(event, author, emptyList())
|
||||
version.moveAllReferencesTo(note)
|
||||
}
|
||||
|
||||
if (note.event?.id() == event.id()) return
|
||||
|
||||
if (event.createdAt > (note.createdAt() ?: 0)) {
|
||||
note.loadEvent(event, author, emptyList())
|
||||
|
||||
refreshObservers(note)
|
||||
}
|
||||
}
|
||||
|
||||
private fun consume(event: PinListEvent) {
|
||||
val version = getOrCreateNote(event.id)
|
||||
val note = getOrCreateAddressableNote(event.address())
|
||||
@@ -1306,6 +1325,7 @@ object LocalCache {
|
||||
is ChannelMessageEvent -> consume(event, relay)
|
||||
is ChannelMetadataEvent -> consume(event)
|
||||
is ChannelMuteUserEvent -> consume(event)
|
||||
is ClassifiedsEvent -> consume(event)
|
||||
is CommunityDefinitionEvent -> consume(event, relay)
|
||||
is CommunityPostApprovalEvent -> {
|
||||
event.containedPost()?.let {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
class Settings(
|
||||
var automaticallyShowImages: Boolean? = null,
|
||||
var automaticallyStartPlayback: Boolean? = null,
|
||||
var preferredLanguage: String? = null,
|
||||
var automaticallyShowUrlPreview: Boolean? = null
|
||||
)
|
||||
@@ -30,9 +30,11 @@ data class RichTextViewerState(
|
||||
val imagesForPager: ImmutableMap<String, ZoomableUrlContent>,
|
||||
val imageList: ImmutableList<ZoomableUrlContent>,
|
||||
val customEmoji: ImmutableMap<String, String>,
|
||||
val paragraphs: ImmutableList<ImmutableList<Segment>>
|
||||
val paragraphs: ImmutableList<ParagraphState>
|
||||
)
|
||||
|
||||
data class ParagraphState(val words: ImmutableList<Segment>, val isRTL: Boolean)
|
||||
|
||||
object CachedRichTextParser {
|
||||
val richTextCache = LruCache<String, RichTextViewerState>(200)
|
||||
|
||||
@@ -108,13 +110,15 @@ class RichTextParser() {
|
||||
)
|
||||
}
|
||||
|
||||
private fun findTextSegments(content: String, images: Set<String>, urls: Set<String>, emojis: Map<String, String>, tags: ImmutableListOfLists<String>): ImmutableList<ImmutableList<Segment>> {
|
||||
var paragraphSegments = persistentListOf<ImmutableList<Segment>>()
|
||||
private fun findTextSegments(content: String, images: Set<String>, urls: Set<String>, emojis: Map<String, String>, tags: ImmutableListOfLists<String>): ImmutableList<ParagraphState> {
|
||||
var paragraphSegments = persistentListOf<ParagraphState>()
|
||||
|
||||
content.split('\n').forEach { paragraph ->
|
||||
var segments = persistentListOf<Segment>()
|
||||
var isDirty = false
|
||||
|
||||
val isRTL = isArabic(paragraph)
|
||||
|
||||
val wordList = paragraph.split(' ')
|
||||
wordList.forEach { word ->
|
||||
val wordSegment = wordIdentifier(word, images, urls, emojis, tags)
|
||||
@@ -125,13 +129,9 @@ class RichTextParser() {
|
||||
}
|
||||
|
||||
val newSegments = if (isDirty) {
|
||||
if (isArabic(paragraph)) {
|
||||
segments.asReversed().toImmutableList()
|
||||
} else {
|
||||
segments
|
||||
}
|
||||
ParagraphState(segments, isRTL)
|
||||
} else {
|
||||
persistentListOf<Segment>(RegularTextSegment(paragraph))
|
||||
ParagraphState(persistentListOf<Segment>(RegularTextSegment(paragraph)), isRTL)
|
||||
}
|
||||
|
||||
paragraphSegments = paragraphSegments.add(newSegments)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.service
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LiveActivitiesChatMessageEvent
|
||||
@@ -66,6 +67,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
TextNoteEvent.kind,
|
||||
RepostEvent.kind,
|
||||
GenericRepostEvent.kind,
|
||||
ClassifiedsEvent.kind,
|
||||
LongTextNoteEvent.kind,
|
||||
PollNoteEvent.kind,
|
||||
HighlightEvent.kind,
|
||||
@@ -89,7 +91,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, ClassifiedsEvent.kind, HighlightEvent.kind, AudioTrackEvent.kind, PinListEvent.kind),
|
||||
tags = mapOf(
|
||||
"t" to hashToLoad.map {
|
||||
listOf(it, it.lowercase(), it.uppercase(), it.capitalize())
|
||||
|
||||
@@ -27,6 +27,7 @@ fun String.firstFullCharOld(): String {
|
||||
|
||||
fun String.firstFullChar(): String {
|
||||
var isInJoin = false
|
||||
var hasHadSecondChance = false
|
||||
var start = 0
|
||||
var previousCharLength = 0
|
||||
var next: Int
|
||||
@@ -48,8 +49,14 @@ fun String.firstFullChar(): String {
|
||||
isInJoin = true
|
||||
} else {
|
||||
// stops when two chars are not joined together
|
||||
if ((previousCharLength > 0) && (!isInJoin) && Character.charCount(codePoint) == 1) {
|
||||
break
|
||||
if (previousCharLength > 0 && !isInJoin) {
|
||||
if (Character.charCount(codePoint) == 1 || hasHadSecondChance) {
|
||||
break
|
||||
} else {
|
||||
hasHadSecondChance = true
|
||||
}
|
||||
} else {
|
||||
hasHadSecondChance = false
|
||||
}
|
||||
|
||||
isInJoin = false
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.vitorpamplona.amethyst.service.connectivitystatus
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
||||
object ConnectivityStatus {
|
||||
private val onMobileData = mutableStateOf(false)
|
||||
val isOnMobileData: MutableState<Boolean> = onMobileData
|
||||
|
||||
private val onWifi = mutableStateOf(false)
|
||||
val isOnWifi: MutableState<Boolean> = onWifi
|
||||
|
||||
fun updateConnectivityStatus(isOnMobileData: Boolean, isOnWifi: Boolean) {
|
||||
onMobileData.value = isOnMobileData
|
||||
onWifi.value = isOnWifi
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.vitorpamplona.amethyst.model.HexKey
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import nostr.postr.Utils
|
||||
|
||||
@Immutable
|
||||
class ClassifiedsEvent(
|
||||
id: HexKey,
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
tags: List<List<String>>,
|
||||
content: String,
|
||||
sig: HexKey
|
||||
) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent {
|
||||
|
||||
override fun dTag() = tags.firstOrNull { it.size > 1 && it[0] == "d" }?.get(1) ?: ""
|
||||
override fun address() = ATag(kind, pubKey, dTag(), null)
|
||||
|
||||
fun title() = tags.firstOrNull { it.size > 1 && it[0] == "title" }?.get(1)
|
||||
fun image() = tags.firstOrNull { it.size > 1 && it[0] == "image" }?.get(1)
|
||||
fun summary() = tags.firstOrNull { it.size > 1 && it[0] == "summary" }?.get(1)
|
||||
fun price() = tags.firstOrNull { it.size > 1 && it[0] == "price" }?.let {
|
||||
Price(it[1], it.getOrNull(2), it.getOrNull(3))
|
||||
}
|
||||
fun location() = tags.firstOrNull { it.size > 1 && it[0] == "location" }?.get(1)
|
||||
|
||||
fun publishedAt() = try {
|
||||
tags.firstOrNull { it.size > 1 && it[0] == "published_at" }?.get(1)?.toLongOrNull()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 30402
|
||||
|
||||
fun create(
|
||||
dTag: String,
|
||||
title: String?,
|
||||
image: String?,
|
||||
summary: String?,
|
||||
price: Price?,
|
||||
location: String?,
|
||||
publishedAt: Long?,
|
||||
privateKey: ByteArray,
|
||||
createdAt: Long = TimeUtils.now()
|
||||
): ClassifiedsEvent {
|
||||
val tags = mutableListOf<List<String>>()
|
||||
|
||||
tags.add(listOf("d", dTag))
|
||||
title?.let { tags.add(listOf("title", it)) }
|
||||
image?.let { tags.add(listOf("image", it)) }
|
||||
summary?.let { tags.add(listOf("summary", it)) }
|
||||
price?.let {
|
||||
if (it.frequency != null && it.currency != null) {
|
||||
tags.add(listOf("price", it.amount, it.currency, it.frequency))
|
||||
} else if (it.currency != null) {
|
||||
tags.add(listOf("price", it.amount, it.currency))
|
||||
} else {
|
||||
tags.add(listOf("price", it.amount))
|
||||
}
|
||||
}
|
||||
location?.let { tags.add(listOf("location", it)) }
|
||||
publishedAt?.let { tags.add(listOf("publishedAt", it.toString())) }
|
||||
title?.let { tags.add(listOf("title", it)) }
|
||||
|
||||
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
|
||||
val id = generateId(pubKey, createdAt, kind, tags, "")
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return ClassifiedsEvent(id.toHexKey(), pubKey, createdAt, tags, "", sig.toHexKey())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class Price(val amount: String, val currency: String?, val frequency: String?)
|
||||
@@ -267,6 +267,7 @@ open class Event(
|
||||
ChannelMessageEvent.kind -> ChannelMessageEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelMetadataEvent.kind -> ChannelMetadataEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ChannelMuteUserEvent.kind -> ChannelMuteUserEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ClassifiedsEvent.kind -> ClassifiedsEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityDefinitionEvent.kind -> CommunityDefinitionEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
CommunityPostApprovalEvent.kind -> CommunityPostApprovalEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
ContactListEvent.kind -> ContactListEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
|
||||
@@ -10,15 +10,18 @@ import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
@@ -32,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.navigation.debugState
|
||||
import com.vitorpamplona.amethyst.ui.note.Nip47
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -40,8 +44,7 @@ import kotlinx.coroutines.launch
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class MainActivity : FragmentActivity() {
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -50,16 +53,22 @@ class MainActivity : FragmentActivity() {
|
||||
val startingPage = uriToRoute(uri)
|
||||
|
||||
LocalPreferences.migrateSingleUserPrefs()
|
||||
|
||||
val language = LocalPreferences.getPreferredLanguage()
|
||||
if (language.isNotBlank()) {
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(language)
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
setContent {
|
||||
AmethystTheme {
|
||||
val themeViewModel: ThemeViewModel = viewModel()
|
||||
themeViewModel.onChange(LocalPreferences.getTheme())
|
||||
AmethystTheme(themeViewModel) {
|
||||
// A surface container using the 'background' color from the theme
|
||||
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
|
||||
val accountStateViewModel: AccountStateViewModel = viewModel {
|
||||
AccountStateViewModel(this@MainActivity)
|
||||
}
|
||||
|
||||
AccountScreen(accountStateViewModel, startingPage)
|
||||
AccountScreen(accountStateViewModel, themeViewModel, startingPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,6 +139,14 @@ class MainActivity : FragmentActivity() {
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||
val hasMobileData = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)
|
||||
val hasWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasMobileData $hasMobileData")
|
||||
Log.d("NETWORKCALLBACK", "onCapabilitiesChanged: hasWifi $hasWifi")
|
||||
ConnectivityStatus.updateConnectivityStatus(
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR),
|
||||
networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
)
|
||||
}
|
||||
|
||||
// lost network connection
|
||||
|
||||
@@ -6,11 +6,22 @@ import android.os.Build
|
||||
import android.util.Size
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -29,8 +40,7 @@ import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.components.*
|
||||
import com.vitorpamplona.amethyst.ui.components.VideoView
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.TextSpinner
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
@@ -107,7 +117,7 @@ fun NewMediaView(uri: Uri, onClose: () -> Unit, postViewModel: NewMediaModel, ac
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(scroolState)
|
||||
) {
|
||||
ImageVideoPost(postViewModel, account)
|
||||
ImageVideoPost(postViewModel, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,7 +133,7 @@ fun isNIP94Server(selectedServer: ServersAvailable?): Boolean {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
||||
fun ImageVideoPost(postViewModel: NewMediaModel, accountViewModel: AccountViewModel) {
|
||||
val fileServers = listOf(
|
||||
// Triple(ServersAvailable.IMGUR_NIP_94, stringResource(id = R.string.upload_server_imgur_nip94), stringResource(id = R.string.upload_server_imgur_nip94_explainer)),
|
||||
Triple(ServersAvailable.NOSTRIMG_NIP_94, stringResource(id = R.string.upload_server_nostrimg_nip94), stringResource(id = R.string.upload_server_nostrimg_nip94_explainer)),
|
||||
@@ -180,7 +190,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
||||
}
|
||||
} else {
|
||||
postViewModel.galleryUri?.let {
|
||||
VideoView(it.toString())
|
||||
VideoView(it.toString(), accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +201,7 @@ fun ImageVideoPost(postViewModel: NewMediaModel, acc: Account) {
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(id = R.string.file_server),
|
||||
placeholder = fileServers.firstOrNull { it.first == acc.defaultFileServer }?.second ?: fileServers[0].second,
|
||||
placeholder = fileServers.firstOrNull { it.first == accountViewModel.account.defaultFileServer }?.second ?: fileServers[0].second,
|
||||
options = fileServerOptions,
|
||||
explainers = fileServerExplainers,
|
||||
onSelect = {
|
||||
|
||||
@@ -272,7 +272,8 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
scope.launch {
|
||||
postViewModel.imageUploadingError.emit(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -330,9 +331,9 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
)
|
||||
)
|
||||
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
|
||||
VideoView(myUrlPreview)
|
||||
VideoView(myUrlPreview, accountViewModel = accountViewModel)
|
||||
} else {
|
||||
UrlPreview(myUrlPreview, myUrlPreview)
|
||||
UrlPreview(myUrlPreview, myUrlPreview, accountViewModel)
|
||||
}
|
||||
} else if (startsWithNIP19Scheme(myUrlPreview)) {
|
||||
val bgColor = MaterialTheme.colors.background
|
||||
@@ -348,7 +349,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
nav
|
||||
)
|
||||
} else if (noProtocolUrlValidator.matcher(myUrlPreview).matches()) {
|
||||
UrlPreview("https://$myUrlPreview", myUrlPreview)
|
||||
UrlPreview("https://$myUrlPreview", myUrlPreview, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -836,7 +837,8 @@ fun ImageVideoDescription(
|
||||
defaultServer: ServersAvailable,
|
||||
onAdd: (String, ServersAvailable, Boolean) -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
onError: (String) -> Unit
|
||||
onError: (String) -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val resolver = LocalContext.current.contentResolver
|
||||
val mediaType = resolver.getType(uri) ?: ""
|
||||
@@ -966,7 +968,7 @@ fun ImageVideoDescription(
|
||||
)
|
||||
}
|
||||
} else {
|
||||
VideoView(uri.toString())
|
||||
VideoView(uri.toString(), accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,16 +17,18 @@ import androidx.compose.material.ProvideTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.map
|
||||
@@ -136,7 +138,6 @@ private fun RenderRegular(
|
||||
val currentTextStyle = LocalTextStyle.current
|
||||
|
||||
val textStyle = currentTextStyle.copy(
|
||||
textDirection = TextDirection.Content,
|
||||
lineHeight = 1.4.em,
|
||||
color = currentTextStyle.color.takeOrElse {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
@@ -144,36 +145,58 @@ private fun RenderRegular(
|
||||
)
|
||||
|
||||
MeasureSpaceWidth() { spaceWidth ->
|
||||
Column {
|
||||
Column() {
|
||||
if (canPreview) {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(spaceWidth)) {
|
||||
paragraph.forEach { word ->
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
val direction = if (paragraph.isRTL) {
|
||||
LayoutDirection.Rtl
|
||||
} else {
|
||||
LayoutDirection.Ltr
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides direction) {
|
||||
FlowRow(
|
||||
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start),
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceWidth)
|
||||
) {
|
||||
paragraph.words.forEach { word ->
|
||||
RenderWordWithPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
state.paragraphs.forEach { paragraph ->
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(spaceWidth)) {
|
||||
paragraph.forEach { word ->
|
||||
RenderWordWithoutPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
val direction = if (paragraph.isRTL) {
|
||||
LayoutDirection.Rtl
|
||||
} else {
|
||||
LayoutDirection.Ltr
|
||||
}
|
||||
|
||||
CompositionLocalProvider(LocalLayoutDirection provides direction) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(spaceWidth),
|
||||
modifier = Modifier.align(if (paragraph.isRTL) Alignment.End else Alignment.Start)
|
||||
) {
|
||||
paragraph.words.forEach { word ->
|
||||
RenderWordWithoutPreview(
|
||||
word,
|
||||
state,
|
||||
backgroundColor,
|
||||
textStyle,
|
||||
accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,8 +261,8 @@ private fun RenderWordWithPreview(
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (word) {
|
||||
is ImageSegment -> ZoomableContentView(word.segmentText, state)
|
||||
is LinkSegment -> UrlPreview(word.segmentText, word.segmentText)
|
||||
is ImageSegment -> ZoomableContentView(word.segmentText, state, accountViewModel)
|
||||
is LinkSegment -> UrlPreview(word.segmentText, word.segmentText, accountViewModel)
|
||||
is EmojiSegment -> RenderCustomEmoji(word.segmentText, state)
|
||||
is InvoiceSegment -> MayBeInvoicePreview(word.segmentText)
|
||||
is WithdrawSegment -> MayBeWithdrawal(word.segmentText)
|
||||
@@ -256,9 +279,13 @@ private fun RenderWordWithPreview(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ZoomableContentView(word: String, state: RichTextViewerState) {
|
||||
private fun ZoomableContentView(
|
||||
word: String,
|
||||
state: RichTextViewerState,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
state.imagesForPager[word]?.let {
|
||||
ZoomableContentView(it, state.imageList)
|
||||
ZoomableContentView(it, state.imageList, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,37 +9,56 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun UrlPreview(url: String, urlText: String) {
|
||||
var urlPreviewState by remember(url) {
|
||||
mutableStateOf(
|
||||
UrlCachedPreviewer.cache.get(url)?.let { it } ?: UrlPreviewState.Loading
|
||||
)
|
||||
fun UrlPreview(url: String, urlText: String, accountViewModel: AccountViewModel) {
|
||||
val settings = accountViewModel.account.settings
|
||||
val isMobile = ConnectivityStatus.isOnMobileData.value
|
||||
|
||||
val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) {
|
||||
true -> !isMobile
|
||||
false -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||
if (urlPreviewState == UrlPreviewState.Loading) {
|
||||
LaunchedEffect(url) {
|
||||
launch(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url) {
|
||||
launch(Dispatchers.Main) {
|
||||
urlPreviewState = it
|
||||
if (!automaticallyShowUrlPreview) {
|
||||
ClickableUrl(urlText, url)
|
||||
} else {
|
||||
var urlPreviewState by remember(url) {
|
||||
mutableStateOf(
|
||||
UrlCachedPreviewer.cache.get(url)?.let { it } ?: UrlPreviewState.Loading
|
||||
)
|
||||
}
|
||||
|
||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||
if (urlPreviewState == UrlPreviewState.Loading) {
|
||||
LaunchedEffect(url) {
|
||||
launch(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url) {
|
||||
launch(Dispatchers.Main) {
|
||||
urlPreviewState = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = urlPreviewState, animationSpec = tween(durationMillis = 100)) { state ->
|
||||
when (state) {
|
||||
is UrlPreviewState.Loaded -> {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
}
|
||||
else -> {
|
||||
ClickableUrl(urlText, url)
|
||||
Crossfade(
|
||||
targetState = urlPreviewState,
|
||||
animationSpec = tween(durationMillis = 100)
|
||||
) { state ->
|
||||
when (state) {
|
||||
is UrlPreviewState.Loaded -> {
|
||||
UrlPreviewCard(url, state.previewInfo, accountViewModel)
|
||||
}
|
||||
|
||||
else -> {
|
||||
ClickableUrl(urlText, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,66 +19,82 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.previews.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.QuoteBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.subtleBorder
|
||||
|
||||
@Composable
|
||||
fun UrlPreviewCard(
|
||||
url: String,
|
||||
previewInfo: UrlInfoItem
|
||||
previewInfo: UrlInfoItem,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val uri = LocalUriHandler.current
|
||||
val settings = accountViewModel.account.settings
|
||||
val isMobile = ConnectivityStatus.isOnMobileData.value
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { runCatching { uri.openUri(url) } }
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
AsyncImage(
|
||||
model = previewInfo.imageUrlFullPath,
|
||||
contentDescription = stringResource(R.string.preview_card_image_for, previewInfo.url),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) {
|
||||
true -> !isMobile
|
||||
false -> false
|
||||
else -> true
|
||||
}
|
||||
|
||||
Text(
|
||||
text = previewInfo.verifiedUrl?.host ?: previewInfo.url,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
if (!automaticallyShowUrlPreview) {
|
||||
ClickableUrl(url, url)
|
||||
} else {
|
||||
val uri = LocalUriHandler.current
|
||||
|
||||
Text(
|
||||
text = previewInfo.title,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { runCatching { uri.openUri(url) } }
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
AsyncImage(
|
||||
model = previewInfo.imageUrlFullPath,
|
||||
contentDescription = stringResource(R.string.preview_card_image_for, previewInfo.url),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Text(
|
||||
text = previewInfo.description,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
Text(
|
||||
text = previewInfo.verifiedUrl?.host ?: previewInfo.url,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = previewInfo.title,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
Text(
|
||||
text = previewInfo.description,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.VolumeUp
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -60,6 +61,8 @@ import com.google.android.exoplayer2.ui.StyledPlayerView
|
||||
import com.google.android.exoplayer2.upstream.DataSource
|
||||
import com.vitorpamplona.amethyst.VideoCache
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.ExperimentalTime
|
||||
@@ -68,7 +71,13 @@ import kotlin.time.measureTimedValue
|
||||
public var DefaultMutedSetting = mutableStateOf(true)
|
||||
|
||||
@Composable
|
||||
fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thumbUri: String, onDialog: ((Boolean) -> Unit)? = null) {
|
||||
fun LoadThumbAndThenVideoView(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
thumbUri: String,
|
||||
accountViewModel: AccountViewModel,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
var loadingFinished by remember { mutableStateOf<Pair<Boolean, Drawable?>>(Pair(false, null)) }
|
||||
|
||||
val context = LocalContext.current
|
||||
@@ -92,9 +101,9 @@ fun LoadThumbAndThenVideoView(videoUri: String, description: String? = null, thu
|
||||
|
||||
if (loadingFinished.first) {
|
||||
if (loadingFinished.second != null) {
|
||||
VideoView(videoUri, description, VideoThumb(loadingFinished.second), onDialog)
|
||||
VideoView(videoUri, description, VideoThumb(loadingFinished.second), accountViewModel, onDialog = onDialog)
|
||||
} else {
|
||||
VideoView(videoUri, description, null, onDialog)
|
||||
VideoView(videoUri, description, null, accountViewModel, onDialog = onDialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,10 +114,12 @@ fun VideoView(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwaysShowVideo: Boolean = false,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
VideoView1(videoUri, description, thumb, onDialog)
|
||||
VideoView1(videoUri, description, thumb, onDialog, accountViewModel, alwaysShowVideo)
|
||||
}
|
||||
Log.d("Rendering Metrics", "VideoView $elapsed $videoUri")
|
||||
}
|
||||
@@ -118,7 +129,9 @@ fun VideoView1(
|
||||
videoUri: String,
|
||||
description: String? = null,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwaysShowVideo: Boolean = false
|
||||
) {
|
||||
var exoPlayerData by remember { mutableStateOf<VideoPlayer?>(null) }
|
||||
val defaultToStart by remember { mutableStateOf(DefaultMutedSetting.value) }
|
||||
@@ -133,7 +146,7 @@ fun VideoView1(
|
||||
}
|
||||
|
||||
exoPlayerData?.let {
|
||||
VideoView(videoUri, description, it, defaultToStart, thumb, onDialog)
|
||||
VideoView(videoUri, description, it, defaultToStart, thumb, onDialog, accountViewModel, alwaysShowVideo)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
@@ -151,10 +164,12 @@ fun VideoView(
|
||||
exoPlayerData: VideoPlayer,
|
||||
defaultToStart: Boolean = false,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwaysShowVideo: Boolean = false
|
||||
) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
VideoView1(videoUri, description, exoPlayerData, defaultToStart, thumb, onDialog)
|
||||
val (_, elapsed) = measureTimedValue {
|
||||
VideoView1(videoUri, description, exoPlayerData, defaultToStart, thumb, onDialog, accountViewModel, alwaysShowVideo)
|
||||
}
|
||||
Log.d("Rendering Metrics", "VideoView $elapsed $videoUri")
|
||||
}
|
||||
@@ -166,12 +181,29 @@ fun VideoView1(
|
||||
exoPlayerData: VideoPlayer,
|
||||
defaultToStart: Boolean = false,
|
||||
thumb: VideoThumb? = null,
|
||||
onDialog: ((Boolean) -> Unit)? = null
|
||||
onDialog: ((Boolean) -> Unit)? = null,
|
||||
accountViewModel: AccountViewModel,
|
||||
alwaysShowVideo: Boolean = false
|
||||
) {
|
||||
val lifecycleOwner = rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
val media = remember { MediaItem.Builder().setUri(videoUri).build() }
|
||||
|
||||
val settings = accountViewModel.account.settings
|
||||
val isMobile = ConnectivityStatus.isOnMobileData.value
|
||||
|
||||
val automaticallyStartPlayback = remember {
|
||||
mutableStateOf(
|
||||
if (alwaysShowVideo) { true } else {
|
||||
when (settings.automaticallyStartPlayback) {
|
||||
true -> !isMobile
|
||||
false -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
exoPlayerData.exoPlayer.apply {
|
||||
repeatMode = Player.REPEAT_MODE_ALL
|
||||
videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT
|
||||
@@ -196,7 +228,11 @@ fun VideoView1(
|
||||
prepare()
|
||||
}
|
||||
|
||||
RenderVideoPlayer(exoPlayerData, thumb, onDialog)
|
||||
if (!automaticallyStartPlayback.value) {
|
||||
ImageUrlWithDownloadButton(url = videoUri, showImage = automaticallyStartPlayback)
|
||||
} else {
|
||||
RenderVideoPlayer(exoPlayerData, thumb, automaticallyStartPlayback, onDialog)
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
@@ -230,6 +266,7 @@ data class VideoThumb(
|
||||
private fun RenderVideoPlayer(
|
||||
playerData: VideoPlayer,
|
||||
thumbData: VideoThumb?,
|
||||
automaticallyStartPlayback: MutableState<Boolean>,
|
||||
onDialog: ((Boolean) -> Unit)?
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
@@ -241,7 +278,12 @@ private fun RenderVideoPlayer(
|
||||
.defaultMinSize(minHeight = 70.dp)
|
||||
.align(Alignment.Center)
|
||||
.onVisibilityChanges { visible ->
|
||||
if (visible && !playerData.exoPlayer.isPlaying) {
|
||||
if (!automaticallyStartPlayback.value) {
|
||||
playerData.exoPlayer.stop()
|
||||
}
|
||||
if (!automaticallyStartPlayback.value && visible && !playerData.exoPlayer.isPlaying) {
|
||||
playerData.exoPlayer.pause()
|
||||
} else if (visible && !playerData.exoPlayer.isPlaying) {
|
||||
playerData.exoPlayer.play()
|
||||
} else if (!visible && playerData.exoPlayer.isPlaying) {
|
||||
playerData.exoPlayer.pause()
|
||||
|
||||
+196
-87
@@ -5,7 +5,6 @@ import android.os.Build
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -34,6 +33,7 @@ import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.DownloadForOffline
|
||||
import androidx.compose.material.icons.filled.Report
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
@@ -70,10 +70,12 @@ import coil.imageLoader
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.toHexKey
|
||||
import com.vitorpamplona.amethyst.service.BlurHashRequester
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
import com.vitorpamplona.amethyst.ui.actions.LoadingAnimation
|
||||
import com.vitorpamplona.amethyst.ui.actions.SaveToGallery
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Font17SP
|
||||
import com.vitorpamplona.amethyst.ui.theme.imageModifier
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -167,7 +169,11 @@ fun figureOutMimeType(fullUrl: String): ZoomableContent {
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
fun ZoomableContentView(content: ZoomableContent, images: ImmutableList<ZoomableContent> = listOf(content).toImmutableList()) {
|
||||
fun ZoomableContentView(
|
||||
content: ZoomableContent,
|
||||
images: ImmutableList<ZoomableContent> = listOf(content).toImmutableList(),
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
// store the dialog open or close state
|
||||
@@ -194,27 +200,53 @@ fun ZoomableContentView(content: ZoomableContent, images: ImmutableList<Zoomable
|
||||
}
|
||||
|
||||
when (content) {
|
||||
is ZoomableUrlImage -> UrlImageView(content, mainImageModifier)
|
||||
is ZoomableUrlVideo -> VideoView(content.url, content.description) { dialogOpen = true }
|
||||
is ZoomableLocalImage -> LocalImageView(content, mainImageModifier)
|
||||
is ZoomableUrlImage -> UrlImageView(content, mainImageModifier, accountViewModel)
|
||||
is ZoomableUrlVideo -> VideoView(
|
||||
content.url,
|
||||
content.description,
|
||||
accountViewModel = accountViewModel
|
||||
) { dialogOpen = true }
|
||||
|
||||
is ZoomableLocalImage -> LocalImageView(content, mainImageModifier, accountViewModel)
|
||||
is ZoomableLocalVideo ->
|
||||
content.localFile?.let {
|
||||
VideoView(it.toUri().toString(), content.description) { dialogOpen = true }
|
||||
VideoView(
|
||||
it.toUri().toString(),
|
||||
content.description,
|
||||
accountViewModel = accountViewModel
|
||||
) { dialogOpen = true }
|
||||
}
|
||||
}
|
||||
|
||||
if (dialogOpen) {
|
||||
ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false })
|
||||
ZoomableImageDialog(content, images, onDismiss = { dialogOpen = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LocalImageView(
|
||||
content: ZoomableLocalImage,
|
||||
mainImageModifier: Modifier
|
||||
mainImageModifier: Modifier,
|
||||
accountViewModel: AccountViewModel?,
|
||||
alwayShowImage: Boolean = false
|
||||
) {
|
||||
if (content.localFile != null && content.localFile.exists()) {
|
||||
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
||||
val settings = accountViewModel?.account?.settings
|
||||
val isMobile = ConnectivityStatus.isOnMobileData.value
|
||||
|
||||
val showImage = remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) { true } else {
|
||||
when (settings?.automaticallyShowImages) {
|
||||
true -> !isMobile
|
||||
false -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val myModifier = remember {
|
||||
mainImageModifier
|
||||
.widthIn(max = maxWidth)
|
||||
@@ -236,17 +268,26 @@ private fun LocalImageView(
|
||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = content.localFile,
|
||||
contentDescription = content.description,
|
||||
contentScale = contentScale,
|
||||
modifier = myModifier,
|
||||
onState = {
|
||||
painterState.value = it
|
||||
}
|
||||
)
|
||||
if (showImage.value) {
|
||||
AsyncImage(
|
||||
model = content.localFile,
|
||||
contentDescription = content.description,
|
||||
contentScale = contentScale,
|
||||
modifier = myModifier,
|
||||
onState = {
|
||||
painterState.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier)
|
||||
AddedImageFeatures(
|
||||
painterState,
|
||||
content,
|
||||
contentScale,
|
||||
myModifier,
|
||||
verifierModifier,
|
||||
showImage
|
||||
)
|
||||
}
|
||||
} else {
|
||||
BlankNote()
|
||||
@@ -256,9 +297,26 @@ private fun LocalImageView(
|
||||
@Composable
|
||||
private fun UrlImageView(
|
||||
content: ZoomableUrlImage,
|
||||
mainImageModifier: Modifier
|
||||
mainImageModifier: Modifier,
|
||||
accountViewModel: AccountViewModel?,
|
||||
alwayShowImage: Boolean = false
|
||||
) {
|
||||
BoxWithConstraints(contentAlignment = Alignment.Center) {
|
||||
val settings = accountViewModel?.account?.settings
|
||||
val isMobile = ConnectivityStatus.isOnMobileData.value
|
||||
|
||||
val showImage = remember {
|
||||
mutableStateOf(
|
||||
if (alwayShowImage) { true } else {
|
||||
when (settings?.automaticallyShowImages) {
|
||||
true -> !isMobile
|
||||
false -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val myModifier = remember {
|
||||
mainImageModifier
|
||||
.widthIn(max = maxWidth)
|
||||
@@ -280,17 +338,45 @@ private fun UrlImageView(
|
||||
mutableStateOf<AsyncImagePainter.State?>(null)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = content.url,
|
||||
contentDescription = content.description,
|
||||
contentScale = contentScale,
|
||||
modifier = myModifier,
|
||||
onState = {
|
||||
painterState.value = it
|
||||
}
|
||||
)
|
||||
if (showImage.value) {
|
||||
AsyncImage(
|
||||
model = content.url,
|
||||
contentDescription = content.description,
|
||||
contentScale = contentScale,
|
||||
modifier = myModifier,
|
||||
onState = {
|
||||
painterState.value = it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
AddedImageFeatures(painterState, content, contentScale, myModifier, verifierModifier)
|
||||
AddedImageFeatures(
|
||||
painterState,
|
||||
content,
|
||||
contentScale,
|
||||
myModifier,
|
||||
verifierModifier,
|
||||
showImage
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun ImageUrlWithDownloadButton(url: String, showImage: MutableState<Boolean>) {
|
||||
FlowRow() {
|
||||
ClickableUrl(urlText = url, url = url)
|
||||
IconButton(
|
||||
modifier = Modifier.size(20.dp),
|
||||
onClick = { showImage.value = true }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DownloadForOffline,
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,30 +387,35 @@ private fun AddedImageFeatures(
|
||||
content: ZoomableLocalImage,
|
||||
contentScale: ContentScale,
|
||||
myModifier: Modifier,
|
||||
verifiedModifier: Modifier
|
||||
verifiedModifier: Modifier,
|
||||
showImage: MutableState<Boolean>
|
||||
) {
|
||||
when (painter.value) {
|
||||
null, is AsyncImagePainter.State.Loading -> {
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||
} else {
|
||||
FlowRow() {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
if (!showImage.value) {
|
||||
ImageUrlWithDownloadButton(content.uri, showImage)
|
||||
} else {
|
||||
when (painter.value) {
|
||||
null, is AsyncImagePainter.State.Loading -> {
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||
} else {
|
||||
FlowRow() {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
BlankNote()
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
if (content.isVerified != null) {
|
||||
HashVerificationSymbol(content.isVerified, verifiedModifier)
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
BlankNote()
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
if (content.isVerified != null) {
|
||||
HashVerificationSymbol(content.isVerified, verifiedModifier)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,46 +427,51 @@ private fun AddedImageFeatures(
|
||||
content: ZoomableUrlImage,
|
||||
contentScale: ContentScale,
|
||||
myModifier: Modifier,
|
||||
verifiedModifier: Modifier
|
||||
verifiedModifier: Modifier,
|
||||
showImage: MutableState<Boolean>
|
||||
) {
|
||||
var verifiedHash by remember {
|
||||
mutableStateOf<Boolean?>(null)
|
||||
}
|
||||
|
||||
when (painter.value) {
|
||||
null, is AsyncImagePainter.State.Loading -> {
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||
} else {
|
||||
FlowRow() {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
if (!showImage.value) {
|
||||
ImageUrlWithDownloadButton(content.url, showImage)
|
||||
} else {
|
||||
var verifiedHash by remember {
|
||||
mutableStateOf<Boolean?>(null)
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
}
|
||||
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
if (content.hash != null) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(key1 = content.url) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newVerifiedHash = verifyHash(content, context)
|
||||
if (newVerifiedHash != verifiedHash) {
|
||||
verifiedHash = newVerifiedHash
|
||||
}
|
||||
when (painter.value) {
|
||||
null, is AsyncImagePainter.State.Loading -> {
|
||||
if (content.blurhash != null) {
|
||||
DisplayBlurHash(content.blurhash, content.description, contentScale, myModifier)
|
||||
} else {
|
||||
FlowRow() {
|
||||
DisplayUrlWithLoadingSymbol(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifiedHash?.let {
|
||||
HashVerificationSymbol(it, verifiedModifier)
|
||||
is AsyncImagePainter.State.Error -> {
|
||||
ClickableUrl(urlText = "${content.url} ", url = content.url)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
is AsyncImagePainter.State.Success -> {
|
||||
if (content.hash != null) {
|
||||
val context = LocalContext.current
|
||||
LaunchedEffect(key1 = content.url) {
|
||||
launch(Dispatchers.IO) {
|
||||
val newVerifiedHash = verifyHash(content, context)
|
||||
if (newVerifiedHash != verifiedHash) {
|
||||
verifiedHash = newVerifiedHash
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
verifiedHash?.let {
|
||||
HashVerificationSymbol(it, verifiedModifier)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -473,7 +569,12 @@ private fun DisplayBlurHash(
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<ZoomableContent> = listOf(imageUrl).toImmutableList(), onDismiss: () -> Unit) {
|
||||
fun ZoomableImageDialog(
|
||||
imageUrl: ZoomableContent,
|
||||
allImages: ImmutableList<ZoomableContent> = listOf(imageUrl).toImmutableList(),
|
||||
onDismiss: () -> Unit,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val view = LocalView.current
|
||||
|
||||
DisposableEffect(key1 = Unit) {
|
||||
@@ -515,11 +616,11 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
|
||||
pagerState = pagerState,
|
||||
itemsCount = allImages.size,
|
||||
itemContent = { index ->
|
||||
RenderImageOrVideo(allImages[index])
|
||||
RenderImageOrVideo(allImages[index], accountViewModel)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
RenderImageOrVideo(imageUrl)
|
||||
RenderImageOrVideo(imageUrl, accountViewModel)
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -535,7 +636,10 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
|
||||
if (myContent is ZoomableUrlContent) {
|
||||
SaveToGallery(url = myContent.url)
|
||||
} else if (myContent is ZoomableLocalImage && myContent.localFile != null) {
|
||||
SaveToGallery(localFile = myContent.localFile, mimeType = myContent.mimeType)
|
||||
SaveToGallery(
|
||||
localFile = myContent.localFile,
|
||||
mimeType = myContent.mimeType
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -544,23 +648,28 @@ fun ZoomableImageDialog(imageUrl: ZoomableContent, allImages: ImmutableList<Zoom
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderImageOrVideo(content: ZoomableContent) {
|
||||
private fun RenderImageOrVideo(content: ZoomableContent, accountViewModel: AccountViewModel) {
|
||||
val mainModifier = Modifier
|
||||
.fillMaxSize()
|
||||
.zoomable(rememberZoomState())
|
||||
|
||||
if (content is ZoomableUrlImage) {
|
||||
UrlImageView(content = content, mainImageModifier = mainModifier)
|
||||
UrlImageView(content = content, mainImageModifier = mainModifier, accountViewModel, alwayShowImage = true)
|
||||
} else if (content is ZoomableUrlVideo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||
VideoView(content.url, content.description)
|
||||
VideoView(content.url, content.description, accountViewModel = accountViewModel, alwaysShowVideo = true)
|
||||
}
|
||||
} else if (content is ZoomableLocalImage) {
|
||||
LocalImageView(content = content, mainImageModifier = mainModifier)
|
||||
LocalImageView(content = content, mainImageModifier = mainModifier, accountViewModel, alwayShowImage = true)
|
||||
} else if (content is ZoomableLocalVideo) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize(1f)) {
|
||||
content.localFile?.let {
|
||||
VideoView(it.toUri().toString(), content.description)
|
||||
VideoView(
|
||||
it.toUri().toString(),
|
||||
content.description,
|
||||
accountViewModel = accountViewModel,
|
||||
alwaysShowVideo = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.TimeUtils
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.GenericRepostEvent
|
||||
import com.vitorpamplona.amethyst.service.model.HighlightEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
@@ -42,7 +43,7 @@ class HomeNewThreadFeedFilter(val account: Account) : AdditiveFeedFilter<Note>()
|
||||
.asSequence()
|
||||
.filter { it ->
|
||||
val noteEvent = it.event
|
||||
(noteEvent is TextNoteEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
||||
(noteEvent is TextNoteEvent || noteEvent is ClassifiedsEvent || noteEvent is RepostEvent || noteEvent is GenericRepostEvent || noteEvent is LongTextNoteEvent || noteEvent is PollNoteEvent || noteEvent is HighlightEvent || noteEvent is AudioTrackEvent) &&
|
||||
(isGlobal || it.author?.pubkeyHex in followingKeySet || noteEvent.isTaggedHashes(followingTagSet)) &&
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
it.author?.let { !account.isHidden(it.pubkeyHex) } ?: true &&
|
||||
|
||||
+10
-1
@@ -6,6 +6,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.AppRecommendationEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||
import com.vitorpamplona.amethyst.service.model.EmojiPackSelectionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||
|
||||
class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : FeedFilter<Note>() {
|
||||
@@ -15,7 +16,15 @@ class UserProfileNewThreadFeedFilter(val user: User, val account: Account) : Fee
|
||||
|
||||
override fun feed(): List<Note> {
|
||||
val longFormNotes = LocalCache.addressables.values
|
||||
.filter { it.author == user && (it.event !is PeopleListEvent && it.event !is BookmarkListEvent && it.event !is AppRecommendationEvent) }
|
||||
.filter {
|
||||
it.author == user &&
|
||||
(
|
||||
it.event !is PeopleListEvent &&
|
||||
it.event !is BookmarkListEvent &&
|
||||
it.event !is AppRecommendationEvent &&
|
||||
it.event !is EmojiPackSelectionEvent
|
||||
)
|
||||
}
|
||||
|
||||
return user.notes
|
||||
.plus(longFormNotes)
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.BookmarkListScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelScreen
|
||||
@@ -34,6 +35,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRedirectScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.NotificationScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ProfileScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SearchScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.SettingsScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ThreadScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.VideoScreen
|
||||
import kotlinx.coroutines.delay
|
||||
@@ -54,6 +56,7 @@ fun AppNavigation(
|
||||
|
||||
navController: NavHostController,
|
||||
accountViewModel: AccountViewModel,
|
||||
themeViewModel: ThemeViewModel,
|
||||
nextPage: String? = null
|
||||
) {
|
||||
var actionableNextPage by remember { mutableStateOf<String?>(nextPage) }
|
||||
@@ -219,6 +222,15 @@ fun AppNavigation(
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
Route.Settings.let { route ->
|
||||
composable(route.route, route.arguments, content = {
|
||||
SettingsScreen(
|
||||
accountViewModel = accountViewModel,
|
||||
themeViewModel
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
actionableNextPage?.let {
|
||||
|
||||
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size16dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -299,20 +300,19 @@ fun ListContent(
|
||||
) {
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = remember(accountState) { accountState?.account } ?: return
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
var checked by remember { mutableStateOf(account.proxy != null) }
|
||||
var disconnectTorDialog by remember { mutableStateOf(false) }
|
||||
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
var proxyPort = remember { mutableStateOf(account.proxyPort.toString()) }
|
||||
|
||||
val relayViewModel: RelayPoolViewModel = viewModel { RelayPoolViewModel() }
|
||||
|
||||
var wantsToEditRelays by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
var backupDialogOpen by remember { mutableStateOf(false) }
|
||||
var checked by remember { mutableStateOf(accountViewModel.account.proxy != null) }
|
||||
var disconnectTorDialog by remember { mutableStateOf(false) }
|
||||
var conectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||
val proxyPort = remember { mutableStateOf(accountViewModel.account.proxyPort.toString()) }
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxHeight()
|
||||
@@ -368,7 +368,6 @@ fun ListContent(
|
||||
)
|
||||
|
||||
val textTorProxy = if (checked) stringResource(R.string.disconnect_from_your_orbot_setup) else stringResource(R.string.connect_via_tor_short)
|
||||
|
||||
IconRow(
|
||||
title = textTorProxy,
|
||||
icon = R.drawable.ic_tor,
|
||||
@@ -391,6 +390,15 @@ fun ListContent(
|
||||
}
|
||||
)
|
||||
|
||||
NavigationRow(
|
||||
title = stringResource(R.string.settings),
|
||||
icon = Route.Settings.icon,
|
||||
tint = MaterialTheme.colors.onBackground,
|
||||
nav = nav,
|
||||
scaffoldState = scaffoldState,
|
||||
route = Route.Settings.route
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
IconRow(
|
||||
@@ -401,12 +409,12 @@ fun ListContent(
|
||||
)
|
||||
}
|
||||
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(account, onClose = { backupDialogOpen = false })
|
||||
if (wantsToEditRelays) {
|
||||
NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
|
||||
}
|
||||
if (backupDialogOpen) {
|
||||
AccountBackupDialog(accountViewModel.account, onClose = { backupDialogOpen = false })
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
if (conectOrbotDialogOpen) {
|
||||
ConnectOrbotDialog(
|
||||
onClose = { conectOrbotDialogOpen = false },
|
||||
@@ -414,7 +422,7 @@ fun ListContent(
|
||||
conectOrbotDialogOpen = false
|
||||
disconnectTorDialog = false
|
||||
checked = true
|
||||
enableTor(account, true, proxyPort, context = context)
|
||||
enableTor(accountViewModel.account, true, proxyPort, context, coroutineScope)
|
||||
},
|
||||
proxyPort
|
||||
)
|
||||
@@ -436,7 +444,7 @@ fun ListContent(
|
||||
onClick = {
|
||||
disconnectTorDialog = false
|
||||
checked = false
|
||||
enableTor(account, false, proxyPort, context)
|
||||
enableTor(accountViewModel.account, false, proxyPort, context, coroutineScope)
|
||||
}
|
||||
) {
|
||||
Text(text = stringResource(R.string.yes))
|
||||
@@ -453,23 +461,22 @@ fun ListContent(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToEditRelays) {
|
||||
NewRelayListView({ wantsToEditRelays = false }, accountViewModel, nav = nav)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableTor(
|
||||
account: Account,
|
||||
checked: Boolean,
|
||||
portNumber: MutableState<String>,
|
||||
context: Context
|
||||
context: Context,
|
||||
scope: CoroutineScope
|
||||
) {
|
||||
account.proxyPort = portNumber.value.toInt()
|
||||
account.proxy = HttpClient.initProxy(checked, "127.0.0.1", account.proxyPort)
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
ServiceManager.pause()
|
||||
ServiceManager.start(context)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalPreferences.saveToEncryptedStorage(account)
|
||||
ServiceManager.pause()
|
||||
ServiceManager.start(context)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -128,6 +128,11 @@ sealed class Route(
|
||||
icon = R.drawable.ic_moments,
|
||||
arguments = listOf(navArgument("id") { type = NavType.StringType }).toImmutableList()
|
||||
)
|
||||
|
||||
object Settings : Route(
|
||||
route = "Settings",
|
||||
icon = R.drawable.ic_settings
|
||||
)
|
||||
}
|
||||
|
||||
// **
|
||||
|
||||
@@ -355,7 +355,11 @@ private fun RenderNoteRow(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RenderLiveActivityThumb(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
|
||||
fun RenderLiveActivityThumb(
|
||||
baseNote: Note,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val noteEvent = baseNote.event as? LiveActivitiesEvent ?: return
|
||||
|
||||
val eventUpdates by baseNote.live().metadata.observeAsState()
|
||||
@@ -586,7 +590,7 @@ fun RenderCommunitiesThumb(baseNote: Note, accountViewModel: AccountViewModel, n
|
||||
)
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
}
|
||||
@@ -700,7 +704,7 @@ fun RenderChannelThumb(baseNote: Note, channel: Channel, accountViewModel: Accou
|
||||
)
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
LikeReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = baseNote, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ private fun StatusRow(
|
||||
|
||||
Column(modifier = ReactionRowHeightChat) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = ReactionRowHeightChat) {
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
Spacer(modifier = DoubleHorzSpacer)
|
||||
|
||||
@@ -88,6 +88,7 @@ import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.UserMetadata
|
||||
import com.vitorpamplona.amethyst.service.OnlineChecker
|
||||
import com.vitorpamplona.amethyst.service.connectivitystatus.ConnectivityStatus
|
||||
import com.vitorpamplona.amethyst.service.model.ATag
|
||||
import com.vitorpamplona.amethyst.service.model.AppDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
@@ -97,6 +98,7 @@ import com.vitorpamplona.amethyst.service.model.BaseTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
||||
@@ -169,6 +171,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55Modifier
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||
@@ -712,7 +715,7 @@ private fun ShortCommunityActionOptions(
|
||||
}
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
LikeReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = note, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
|
||||
@@ -840,7 +843,13 @@ fun ClickableNote(
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
routeFor(baseNote, accountViewModel.userProfile())?.let {
|
||||
val redirectToNote =
|
||||
if (baseNote.event is RepostEvent || baseNote.event is GenericRepostEvent) {
|
||||
baseNote.replyTo?.lastOrNull() ?: baseNote
|
||||
} else {
|
||||
baseNote
|
||||
}
|
||||
routeFor(redirectToNote, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
}
|
||||
@@ -998,7 +1007,8 @@ private fun RenderNoteRow(
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
when (baseNote.event) {
|
||||
val noteEvent = baseNote.event
|
||||
when (noteEvent) {
|
||||
is AppDefinitionEvent -> {
|
||||
RenderAppDefinition(baseNote, accountViewModel, nav)
|
||||
}
|
||||
@@ -1062,6 +1072,14 @@ private fun RenderNoteRow(
|
||||
)
|
||||
}
|
||||
|
||||
is ClassifiedsEvent -> {
|
||||
RenderClassifieds(
|
||||
noteEvent,
|
||||
baseNote,
|
||||
accountViewModel
|
||||
)
|
||||
}
|
||||
|
||||
is HighlightEvent -> {
|
||||
RenderHighlight(
|
||||
baseNote,
|
||||
@@ -1286,7 +1304,7 @@ fun RenderAppDefinition(
|
||||
)
|
||||
|
||||
if (zoomImageDialogOpen) {
|
||||
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false })
|
||||
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
@@ -1337,7 +1355,7 @@ fun RenderAppDefinition(
|
||||
}
|
||||
|
||||
if (zoomImageDialogOpen) {
|
||||
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false })
|
||||
ZoomableImageDialog(imageUrl = figureOutMimeType(it.banner!!), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
@@ -1962,7 +1980,7 @@ public fun RenderEmojiPack(
|
||||
}
|
||||
}
|
||||
|
||||
Box {
|
||||
Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) {
|
||||
FlowRow(modifier = Modifier.padding(top = 5.dp)) {
|
||||
emojisToShow.forEach { emoji ->
|
||||
if (onClick != null) {
|
||||
@@ -2046,14 +2064,14 @@ private fun EmojiListOptions(
|
||||
@Composable
|
||||
fun RemoveButton(onClick: () -> Unit) {
|
||||
Button(
|
||||
modifier = Modifier.padding(horizontal = 3.dp),
|
||||
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)
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(R.string.remove), color = Color.White)
|
||||
}
|
||||
@@ -2069,7 +2087,7 @@ fun AddButton(text: Int = R.string.add, onClick: () -> Unit) {
|
||||
.buttonColors(
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
contentPadding = PaddingValues(vertical = 0.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = stringResource(text), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
@@ -3069,7 +3087,7 @@ fun FileHeaderDisplay(note: Note, accountViewModel: AccountViewModel) {
|
||||
Crossfade(targetState = content) {
|
||||
if (it != null) {
|
||||
SensitivityWarning(note = note, accountViewModel = accountViewModel) {
|
||||
ZoomableContentView(content = it)
|
||||
ZoomableContentView(content = it, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3155,7 +3173,7 @@ private fun RenderNIP95(
|
||||
Crossfade(targetState = content) {
|
||||
if (it != null) {
|
||||
SensitivityWarning(note = header, accountViewModel = accountViewModel) {
|
||||
ZoomableContentView(content = it)
|
||||
ZoomableContentView(content = it, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3225,12 +3243,14 @@ fun AudioTrackHeader(noteEvent: AudioTrackEvent, accountViewModel: AccountViewMo
|
||||
LoadThumbAndThenVideoView(
|
||||
videoUri = media,
|
||||
description = noteEvent.subject(),
|
||||
thumbUri = cover
|
||||
thumbUri = cover,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
?: VideoView(
|
||||
videoUri = media,
|
||||
noteEvent.subject()
|
||||
description = noteEvent.subject(),
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -3354,7 +3374,8 @@ fun RenderLiveActivityEventInner(baseNote: Note, accountViewModel: AccountViewMo
|
||||
) {
|
||||
VideoView(
|
||||
videoUri = media,
|
||||
description = subject
|
||||
description = subject,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
} else {
|
||||
@@ -3404,17 +3425,27 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
image?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} ?: CreateImageHeader(note, accountViewModel)
|
||||
val settings = accountViewModel.account.settings
|
||||
val isMobile = ConnectivityStatus.isOnMobileData.value
|
||||
|
||||
val automaticallyShowUrlPreview = when (settings.automaticallyShowUrlPreview) {
|
||||
true -> !isMobile
|
||||
false -> false
|
||||
else -> true
|
||||
}
|
||||
if (automaticallyShowUrlPreview) {
|
||||
image?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} ?: CreateImageHeader(note, accountViewModel)
|
||||
}
|
||||
|
||||
title?.let {
|
||||
Text(
|
||||
@@ -3442,6 +3473,106 @@ private fun LongFormHeader(noteEvent: LongTextNoteEvent, note: Note, accountView
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderClassifieds(noteEvent: ClassifiedsEvent, note: Note, accountViewModel: AccountViewModel) {
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val summary = remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
|
||||
val price = remember(noteEvent) { noteEvent.price() }
|
||||
val location = remember(noteEvent) { noteEvent.location() }
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(shape = QuoteBorder)
|
||||
.border(
|
||||
1.dp,
|
||||
MaterialTheme.colors.subtleBorder,
|
||||
QuoteBorder
|
||||
)
|
||||
) {
|
||||
Column {
|
||||
Row() {
|
||||
image?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} ?: CreateImageHeader(note, accountViewModel)
|
||||
}
|
||||
|
||||
Row(Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
price?.let {
|
||||
val priceTag = remember(noteEvent) {
|
||||
if (price.frequency != null && price.currency != null) {
|
||||
"${price.amount} ${price.currency}/${price.frequency}"
|
||||
} else if (price.currency != null) {
|
||||
"${price.amount} ${price.currency}"
|
||||
} else {
|
||||
price.amount
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = priceTag,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(start = 5.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (summary != null || location != null) {
|
||||
Row(Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
color = Color.Gray,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
location?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(start = 5.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CreateImageHeader(
|
||||
note: Note,
|
||||
|
||||
@@ -206,7 +206,7 @@ private fun InnerReactionRow(
|
||||
) {
|
||||
val (value, elapsed) = measureTimedValue {
|
||||
Row(verticalAlignment = CenterVertically) {
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel)
|
||||
LikeReaction(baseNote, MaterialTheme.colors.placeholderText, accountViewModel, nav)
|
||||
}
|
||||
}
|
||||
Log.d("Rendering Metrics", "Reaction Likes: ${baseNote.event?.content()?.split("\n")?.getOrNull(0)?.take(15)}.. $elapsed")
|
||||
@@ -713,6 +713,7 @@ fun LikeReaction(
|
||||
baseNote: Note,
|
||||
grayTint: Color,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
iconSize: Dp = 20.dp,
|
||||
heartSize: Dp = 16.dp,
|
||||
iconFontSize: TextUnit = Font14SP
|
||||
@@ -755,7 +756,11 @@ fun LikeReaction(
|
||||
LikeText(baseNote, grayTint)
|
||||
|
||||
if (wantsToChangeReactionSymbol) {
|
||||
UpdateReactionTypeDialog({ wantsToChangeReactionSymbol = false }, accountViewModel = accountViewModel)
|
||||
UpdateReactionTypeDialog(
|
||||
{ wantsToChangeReactionSymbol = false },
|
||||
accountViewModel = accountViewModel,
|
||||
nav
|
||||
)
|
||||
}
|
||||
|
||||
if (wantsToReact) {
|
||||
@@ -927,7 +932,7 @@ private fun likeClick(
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
context.getString(R.string.login_with_a_private_key_to_like_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
@@ -32,6 +33,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -67,6 +69,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.ButtonBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
||||
var nextChoice by mutableStateOf(TextFieldValue(""))
|
||||
@@ -117,7 +120,11 @@ class UpdateReactionTypeViewModel(val account: Account) : ViewModel() {
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, accountViewModel: AccountViewModel) {
|
||||
fun UpdateReactionTypeDialog(
|
||||
onClose: () -> Unit,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit
|
||||
) {
|
||||
val postViewModel: UpdateReactionTypeViewModel = viewModel(
|
||||
key = accountViewModel.userProfile().pubkeyHex,
|
||||
factory = UpdateReactionTypeViewModel.Factory(accountViewModel.account)
|
||||
@@ -227,7 +234,10 @@ fun UpdateReactionTypeDialog(onClose: () -> Unit, nip47uri: String? = null, acco
|
||||
}
|
||||
}
|
||||
|
||||
EmojiSelector(accountViewModel) {
|
||||
EmojiSelector(
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
) {
|
||||
postViewModel.addChoice(it)
|
||||
}
|
||||
}
|
||||
@@ -297,7 +307,7 @@ private fun RenderReactionOption(
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmojiSelector(accountViewModel: AccountViewModel, onClick: ((EmojiUrl) -> Unit)? = null) {
|
||||
private fun EmojiSelector(accountViewModel: AccountViewModel, nav: (String) -> Unit, onClick: ((EmojiUrl) -> Unit)? = null) {
|
||||
LoadAddressableNote(
|
||||
aTag = ATag(
|
||||
EmojiPackSelectionEvent.kind,
|
||||
@@ -312,14 +322,14 @@ private fun EmojiSelector(accountViewModel: AccountViewModel, onClick: ((EmojiUr
|
||||
}.distinctUntilChanged().observeAsState((usersEmojiList.event as? EmojiPackSelectionEvent)?.taggedAddresses())
|
||||
|
||||
collections?.let {
|
||||
EmojiCollectionGallery(it, accountViewModel, onClick)
|
||||
EmojiCollectionGallery(it, accountViewModel, nav, onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: AccountViewModel, onClick: ((EmojiUrl) -> Unit)? = null) {
|
||||
fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: AccountViewModel, nav: (String) -> Unit, onClick: ((EmojiUrl) -> Unit)? = null) {
|
||||
val color = MaterialTheme.colors.background
|
||||
val bgColor = remember { mutableStateOf(color) }
|
||||
|
||||
@@ -331,7 +341,7 @@ fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: Accou
|
||||
itemsIndexed(emojiCollections, key = { _, item -> item.toTag() }) { _, item ->
|
||||
LoadAddressableNote(aTag = item) {
|
||||
it?.let {
|
||||
WatchAndRenderNote(it, bgColor, accountViewModel, onClick)
|
||||
WatchAndRenderNote(it, bgColor, accountViewModel, nav, onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,16 +350,31 @@ fun EmojiCollectionGallery(emojiCollections: List<ATag>, accountViewModel: Accou
|
||||
|
||||
@Composable
|
||||
private fun WatchAndRenderNote(
|
||||
it: AddressableNote,
|
||||
emojiPack: AddressableNote,
|
||||
bgColor: MutableState<Color>,
|
||||
accountViewModel: AccountViewModel,
|
||||
nav: (String) -> Unit,
|
||||
onClick: ((EmojiUrl) -> Unit)?
|
||||
) {
|
||||
RenderEmojiPack(
|
||||
baseNote = it,
|
||||
actionable = false,
|
||||
backgroundColor = bgColor,
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = onClick
|
||||
)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Column(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
scope.launch {
|
||||
routeFor(emojiPack, accountViewModel.userProfile())?.let {
|
||||
nav(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
RenderEmojiPack(
|
||||
baseNote = emojiPack,
|
||||
actionable = false,
|
||||
backgroundColor = bgColor,
|
||||
accountViewModel = accountViewModel,
|
||||
onClick = onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,7 +427,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { reportDialogShowing = true }) {
|
||||
Text("Block / Report")
|
||||
Text(stringResource(R.string.block_report))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
|
||||
@@ -12,7 +12,7 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
|
||||
|
||||
@Composable
|
||||
fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: String?) {
|
||||
fun AccountScreen(accountStateViewModel: AccountStateViewModel, themeViewModel: ThemeViewModel, startingPage: String?) {
|
||||
val accountState by accountStateViewModel.accountContent.collectAsState()
|
||||
|
||||
Column() {
|
||||
@@ -27,7 +27,7 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: St
|
||||
factory = AccountViewModel.Factory(state.account)
|
||||
)
|
||||
|
||||
MainScreen(accountViewModel, accountStateViewModel, startingPage)
|
||||
MainScreen(accountViewModel, accountStateViewModel, themeViewModel, startingPage)
|
||||
}
|
||||
is AccountState.LoggedInViewOnly -> {
|
||||
val accountViewModel: AccountViewModel = viewModel(
|
||||
@@ -35,7 +35,7 @@ fun AccountScreen(accountStateViewModel: AccountStateViewModel, startingPage: St
|
||||
factory = AccountViewModel.Factory(state.account)
|
||||
)
|
||||
|
||||
MainScreen(accountViewModel, accountStateViewModel, startingPage)
|
||||
MainScreen(accountViewModel, accountStateViewModel, themeViewModel, startingPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class ThemeViewModel : ViewModel() {
|
||||
private val _theme = MutableLiveData(0)
|
||||
val theme: LiveData<Int> = _theme
|
||||
|
||||
fun onChange(newValue: Int) {
|
||||
_theme.value = newValue
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.screen
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -40,6 +41,7 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -48,6 +50,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.em
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -59,6 +62,7 @@ import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
|
||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ClassifiedsEvent
|
||||
import com.vitorpamplona.amethyst.service.model.CommunityDefinitionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.CommunityPostApprovalEvent
|
||||
import com.vitorpamplona.amethyst.service.model.EmojiPackEvent
|
||||
@@ -74,21 +78,9 @@ import com.vitorpamplona.amethyst.service.model.RelaySetEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.note.*
|
||||
import com.vitorpamplona.amethyst.ui.note.BadgeDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayFollowingHashtagsInPost
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayPoW
|
||||
import com.vitorpamplona.amethyst.ui.note.DisplayReward
|
||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteQuickActionMenu
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
|
||||
import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader
|
||||
import com.vitorpamplona.amethyst.ui.theme.SmallBorder
|
||||
import com.vitorpamplona.amethyst.ui.theme.lessImportantLink
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.amethyst.ui.theme.selectedNote
|
||||
@@ -352,40 +344,9 @@ fun NoteMaster(
|
||||
if (noteEvent is BadgeDefinitionEvent) {
|
||||
BadgeDisplay(baseNote = note)
|
||||
} else if (noteEvent is LongTextNoteEvent) {
|
||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||
Column {
|
||||
noteEvent.image()?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
noteEvent.title()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
noteEvent.summary()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
RenderLongFormHeaderForThread(noteEvent)
|
||||
} else if (noteEvent is ClassifiedsEvent) {
|
||||
RenderClassifiedsReaderForThread(noteEvent, note, accountViewModel)
|
||||
}
|
||||
|
||||
Row(
|
||||
@@ -398,7 +359,13 @@ fun NoteMaster(
|
||||
) {
|
||||
Column() {
|
||||
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && note.channelHex() != null) {
|
||||
ChannelHeader(channelHex = note.channelHex()!!, showVideo = true, showBottomDiviser = false, accountViewModel = accountViewModel, nav = nav)
|
||||
ChannelHeader(
|
||||
channelHex = note.channelHex()!!,
|
||||
showVideo = true,
|
||||
showBottomDiviser = false,
|
||||
accountViewModel = accountViewModel,
|
||||
nav = nav
|
||||
)
|
||||
} else if (noteEvent is FileHeaderEvent) {
|
||||
FileHeaderDisplay(baseNote, accountViewModel)
|
||||
} else if (noteEvent is FileStorageHeaderEvent) {
|
||||
@@ -492,3 +459,142 @@ fun NoteMaster(
|
||||
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderClassifiedsReaderForThread(
|
||||
noteEvent: ClassifiedsEvent,
|
||||
note: Note,
|
||||
accountViewModel: AccountViewModel
|
||||
) {
|
||||
val image = remember(noteEvent) { noteEvent.image() }
|
||||
val title = remember(noteEvent) { noteEvent.title() }
|
||||
val summary =
|
||||
remember(noteEvent) { noteEvent.summary() ?: noteEvent.content.take(200).ifBlank { null } }
|
||||
val price = remember(noteEvent) { noteEvent.price() }
|
||||
val location = remember(noteEvent) { noteEvent.location() }
|
||||
|
||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||
Column {
|
||||
Row() {
|
||||
image?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} ?: CreateImageHeader(note, accountViewModel)
|
||||
}
|
||||
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.body1,
|
||||
maxLines = 1,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
price?.let {
|
||||
val priceTag = remember(noteEvent) {
|
||||
if (price.frequency != null && price.currency != null) {
|
||||
"${price.amount} ${price.currency}/${price.frequency}"
|
||||
} else if (price.currency != null) {
|
||||
"${price.amount} ${price.currency}"
|
||||
} else {
|
||||
price.amount
|
||||
}
|
||||
}
|
||||
|
||||
Text(
|
||||
text = priceTag,
|
||||
maxLines = 1,
|
||||
color = MaterialTheme.colors.primary,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = remember {
|
||||
Modifier
|
||||
.clip(SmallBorder)
|
||||
.background(Color.Black)
|
||||
.padding(start = 5.dp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
summary?.let {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.caption,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = Color.Gray,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
location?.let {
|
||||
Row(
|
||||
Modifier.padding(start = 10.dp, end = 10.dp, top = 5.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = Color.Gray,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RenderLongFormHeaderForThread(noteEvent: LongTextNoteEvent) {
|
||||
Row(modifier = Modifier.padding(start = 12.dp, end = 12.dp, bottom = 12.dp)) {
|
||||
Column {
|
||||
noteEvent.image()?.let {
|
||||
AsyncImage(
|
||||
model = it,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
it
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
noteEvent.title()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 28.sp,
|
||||
fontWeight = FontWeight.Light,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
noteEvent.summary()?.let {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,24 @@ class AccountViewModel(val account: Account) : ViewModel() {
|
||||
val userFollows: LiveData<UserState> = account.userProfile().live().follows.map { it }
|
||||
val userRelays: LiveData<UserState> = account.userProfile().live().relays.map { it }
|
||||
|
||||
fun updateAutomaticallyStartPlayback(
|
||||
automaticallyStartPlayback: Boolean?
|
||||
) {
|
||||
account.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
|
||||
}
|
||||
|
||||
fun updateAutomaticallyShowUrlPreview(
|
||||
automaticallyShowUrlPreview: Boolean?
|
||||
) {
|
||||
account.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
|
||||
}
|
||||
|
||||
fun updateAutomaticallyShowImages(
|
||||
automaticallyShowImages: Boolean?
|
||||
) {
|
||||
account.updateAutomaticallyShowImages(automaticallyShowImages)
|
||||
}
|
||||
|
||||
fun isWriteable(): Boolean {
|
||||
return account.isWriteable()
|
||||
}
|
||||
|
||||
@@ -632,7 +632,8 @@ private fun ShowVideoStreaming(
|
||||
}
|
||||
|
||||
ZoomableContentView(
|
||||
content = zoomableUrlVideo
|
||||
content = zoomableUrlVideo,
|
||||
accountViewModel = accountViewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -884,7 +885,7 @@ private fun ShortChannelActionOptions(
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
@@ -947,7 +948,7 @@ private fun LiveChannelActionOptions(
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
}
|
||||
|
||||
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
LikeReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel, nav)
|
||||
Spacer(modifier = StdHorzSpacer)
|
||||
ZapReaction(baseNote = it, grayTint = MaterialTheme.colors.onSurface, accountViewModel = accountViewModel)
|
||||
}
|
||||
|
||||
@@ -51,11 +51,17 @@ import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrVideoFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NotificationViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) {
|
||||
fun MainScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
accountStateViewModel: AccountStateViewModel,
|
||||
themeViewModel: ThemeViewModel,
|
||||
startingPage: String? = null
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val navController = rememberNavController()
|
||||
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
|
||||
@@ -211,6 +217,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
||||
userReactionsStatsModel = userReactionsStatsModel,
|
||||
navController = navController,
|
||||
accountViewModel = accountViewModel,
|
||||
themeViewModel = themeViewModel,
|
||||
nextPage = startingPage
|
||||
)
|
||||
}
|
||||
|
||||
@@ -599,7 +599,7 @@ private fun ProfileHeader(
|
||||
var zoomImageDialogOpen by remember { mutableStateOf(false) }
|
||||
|
||||
Box {
|
||||
DrawBanner(baseUser)
|
||||
DrawBanner(baseUser, accountViewModel)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
@@ -688,7 +688,7 @@ private fun ProfileHeader(
|
||||
|
||||
val profilePic = baseUser.profilePicture()
|
||||
if (zoomImageDialogOpen && profilePic != null) {
|
||||
ZoomableImageDialog(figureOutMimeType(profilePic), onDismiss = { zoomImageDialogOpen = false })
|
||||
ZoomableImageDialog(figureOutMimeType(profilePic), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1260,7 +1260,7 @@ private fun WatchAndRenderBadgeImage(
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
public fun DrawBanner(baseUser: User) {
|
||||
fun DrawBanner(baseUser: User, accountViewModel: AccountViewModel) {
|
||||
val userState by baseUser.live().metadata.observeAsState()
|
||||
val banner = remember(userState) { userState?.user?.info?.banner }
|
||||
|
||||
@@ -1284,7 +1284,7 @@ public fun DrawBanner(baseUser: User) {
|
||||
)
|
||||
|
||||
if (zoomImageDialogOpen) {
|
||||
ZoomableImageDialog(imageUrl = figureOutMimeType(banner), onDismiss = { zoomImageDialogOpen = false })
|
||||
ZoomableImageDialog(imageUrl = figureOutMimeType(banner), onDismiss = { zoomImageDialogOpen = false }, accountViewModel = accountViewModel)
|
||||
}
|
||||
} else {
|
||||
Image(
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.vitorpamplona.amethyst.ui.screen.loggedIn
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ExposedDropdownMenuBox
|
||||
import androidx.compose.material.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
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
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.intl.Locale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer
|
||||
import com.vitorpamplona.amethyst.ui.theme.StdPadding
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import java.io.IOException
|
||||
|
||||
fun Context.getLocaleListFromXml(): LocaleListCompat {
|
||||
val tagsList = mutableListOf<CharSequence>()
|
||||
try {
|
||||
val xpp: XmlPullParser = resources.getXml(R.xml.locales_config)
|
||||
while (xpp.eventType != XmlPullParser.END_DOCUMENT) {
|
||||
if (xpp.eventType == XmlPullParser.START_TAG) {
|
||||
if (xpp.name == "locale") {
|
||||
tagsList.add(xpp.getAttributeValue(0))
|
||||
}
|
||||
}
|
||||
xpp.next()
|
||||
}
|
||||
} catch (e: XmlPullParserException) {
|
||||
e.printStackTrace()
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
return LocaleListCompat.forLanguageTags(tagsList.joinToString(","))
|
||||
}
|
||||
|
||||
fun Context.getLangPreferenceDropdownEntries(): Map<String, String> {
|
||||
val localeList = getLocaleListFromXml()
|
||||
val map = mutableMapOf<String, String>()
|
||||
|
||||
for (a in 0 until localeList.size()) {
|
||||
localeList[a].let {
|
||||
map.put(it!!.getDisplayName(it).replaceFirstChar { char -> char.uppercase() }, it.toLanguageTag())
|
||||
}
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
fun getLanguageIndex(languageEntries: Map<String, String>): Int {
|
||||
var languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.toLanguageTag())
|
||||
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf(Locale.current.language)
|
||||
if (languageIndex == -1) languageIndex = languageEntries.values.toTypedArray().indexOf("en")
|
||||
return languageIndex
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
accountViewModel: AccountViewModel,
|
||||
themeViewModel: ThemeViewModel
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val selectedItens = persistentListOf(
|
||||
stringResource(R.string.always),
|
||||
stringResource(R.string.wifi_only),
|
||||
stringResource(R.string.never).replaceFirstChar {
|
||||
it.uppercase()
|
||||
}
|
||||
)
|
||||
val settings = accountViewModel.account.settings
|
||||
val index = if (settings.automaticallyShowImages == null) { 0 } else {
|
||||
if (settings.automaticallyShowImages == true) 1 else 2
|
||||
}
|
||||
val videoIndex = if (settings.automaticallyStartPlayback == null) { 0 } else {
|
||||
if (settings.automaticallyShowImages == true) 1 else 2
|
||||
}
|
||||
val linkIndex = if (settings.automaticallyShowUrlPreview == null) { 0 } else {
|
||||
if (settings.automaticallyShowUrlPreview == true) 1 else 2
|
||||
}
|
||||
|
||||
val themeItens = persistentListOf(
|
||||
stringResource(R.string.system),
|
||||
stringResource(R.string.light),
|
||||
stringResource(R.string.dark)
|
||||
)
|
||||
val themeIndex = themeViewModel.theme.value ?: 0
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val languageEntries = context.getLangPreferenceDropdownEntries()
|
||||
val languageList = languageEntries.keys.toImmutableList()
|
||||
val languageIndex = getLanguageIndex(languageEntries)
|
||||
|
||||
Column(
|
||||
StdPadding
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Section(stringResource(R.string.application_preferences))
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(R.string.language),
|
||||
placeholder = languageList[languageIndex],
|
||||
options = languageList,
|
||||
onSelect = {
|
||||
scope.launch(Dispatchers.IO) {
|
||||
val locale = languageEntries[languageList[it]]
|
||||
accountViewModel.account.settings.preferredLanguage = locale
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]])
|
||||
AppCompatDelegate.setApplicationLocales(appLocale)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(R.string.theme),
|
||||
placeholder = themeItens[themeIndex],
|
||||
options = themeItens,
|
||||
onSelect = {
|
||||
themeViewModel.onChange(it)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
LocalPreferences.updateTheme(it)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(R.string.automatically_load_images_gifs),
|
||||
placeholder = selectedItens[index],
|
||||
options = selectedItens,
|
||||
onSelect = {
|
||||
val automaticallyShowImages = when (it) {
|
||||
1 -> true
|
||||
2 -> false
|
||||
else -> null
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages)
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(R.string.automatically_play_videos),
|
||||
placeholder = selectedItens[videoIndex],
|
||||
options = selectedItens,
|
||||
onSelect = {
|
||||
val automaticallyStartPlayback = when (it) {
|
||||
1 -> true
|
||||
2 -> false
|
||||
else -> null
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback)
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
TextSpinner(
|
||||
label = stringResource(R.string.automatically_show_url_preview),
|
||||
placeholder = selectedItens[linkIndex],
|
||||
options = selectedItens,
|
||||
onSelect = {
|
||||
val automaticallyShowUrlPreview = when (it) {
|
||||
1 -> true
|
||||
2 -> false
|
||||
else -> null
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview)
|
||||
LocalPreferences.saveToEncryptedStorage(accountViewModel.account)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp))
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun DropDownSettings(
|
||||
selectedItem: MutableState<String>,
|
||||
listItems: Array<String>,
|
||||
title: String
|
||||
) {
|
||||
var expanded by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
ExposedDropdownMenuBox(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
expanded = expanded,
|
||||
onExpandedChange = {
|
||||
expanded = !expanded
|
||||
}
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = selectedItem.value,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text(text = title) },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(
|
||||
expanded = expanded
|
||||
)
|
||||
},
|
||||
colors = ExposedDropdownMenuDefaults.textFieldColors()
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
listItems.forEach { selectedOption ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
selectedItem.value = selectedOption
|
||||
expanded = false
|
||||
}
|
||||
) {
|
||||
Text(text = selectedOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Section(text: String) {
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 20.sp
|
||||
)
|
||||
Spacer(modifier = DoubleVertSpacer)
|
||||
}
|
||||
@@ -396,7 +396,7 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
|
||||
BoostReaction(baseNote, accountViewModel, iconSize = 40.dp) {
|
||||
wantsToQuote = baseNote
|
||||
}*/
|
||||
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
|
||||
LikeReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, nav, iconSize = 40.dp, heartSize = Size35dp, 28.sp)
|
||||
ZapReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, accountViewModel, iconSize = 40.dp, animationSize = Size35dp)
|
||||
ViewCountReaction(baseNote, grayTint = MaterialTheme.colors.onBackground, barChartSize = 39.dp, viewCountColorFilter = MaterialTheme.colors.onBackgroundColorFilter)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.material.darkColors
|
||||
import androidx.compose.material.lightColors
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -27,6 +28,7 @@ import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel
|
||||
|
||||
private val DarkColorPalette = darkColors(
|
||||
primary = Purple200,
|
||||
@@ -304,12 +306,14 @@ val Colors.innerPostModifier: Modifier
|
||||
get() = if (isLight) LightInnerPostBorderModifier else DarkInnerPostBorderModifier
|
||||
|
||||
@Composable
|
||||
fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
|
||||
val colors = if (darkTheme) {
|
||||
DarkColorPalette
|
||||
} else {
|
||||
LightColorPalette
|
||||
fun AmethystTheme(themeViewModel: ThemeViewModel, content: @Composable () -> Unit) {
|
||||
val theme = themeViewModel.theme.observeAsState()
|
||||
val darkTheme = when (theme.value) {
|
||||
2 -> true
|
||||
1 -> false
|
||||
else -> isSystemInDarkTheme()
|
||||
}
|
||||
val colors = if (darkTheme) DarkColorPalette else LightColorPalette
|
||||
|
||||
MaterialTheme(
|
||||
colors = colors,
|
||||
@@ -319,10 +323,14 @@ fun AmethystTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composab
|
||||
)
|
||||
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode && darkTheme) {
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
window.statusBarColor = colors.background.toArgb()
|
||||
if (darkTheme) {
|
||||
window.statusBarColor = colors.background.toArgb()
|
||||
} else {
|
||||
window.statusBarColor = colors.primary.toArgb()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M388,880L368,754Q349,747 328,735Q307,723 291,710L173,764L80,600L188,521Q186,512 185.5,500.5Q185,489 185,480Q185,471 185.5,459.5Q186,448 188,439L80,360L173,196L291,250Q307,237 328,225Q349,213 368,207L388,80L572,80L592,206Q611,213 632.5,224.5Q654,236 669,250L787,196L880,360L772,437Q774,447 774.5,458.5Q775,470 775,480Q775,490 774.5,501Q774,512 772,522L880,600L787,764L669,710Q653,723 632.5,735.5Q612,748 592,754L572,880L388,880ZM480,610Q534,610 572,572Q610,534 610,480Q610,426 572,388Q534,350 480,350Q426,350 388,388Q350,426 350,480Q350,534 388,572Q426,610 480,610Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,462 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<string name="point_to_the_qr_code">QRコードに向けてください</string>
|
||||
<string name="show_qr">QRコードを表示</string>
|
||||
<string name="profile_image">プロフィール画像</string>
|
||||
<string name="scan_qr">QRコードをスキャン</string>
|
||||
<string name="show_anyway">それでも表示する</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">"このユーザによって投稿が不適切なものとしてマークされています: "</string>
|
||||
<string name="post_not_found">投稿が見つかりません</string>
|
||||
<string name="channel_image">チャンネル画像</string>
|
||||
<string name="referenced_event_not_found">参照先のイベントが見つかりません</string>
|
||||
<string name="could_not_decrypt_the_message">メッセージが復号できませんでした</string>
|
||||
<string name="group_picture">グループ画像</string>
|
||||
<string name="explicit_content">露骨なコンテンツ</string>
|
||||
<string name="spam">スパム</string>
|
||||
<string name="impersonation">なりすまし</string>
|
||||
<string name="illegal_behavior">不法行為</string>
|
||||
<string name="unknown">不明</string>
|
||||
<string name="relay_icon">リレーアイコン</string>
|
||||
<string name="unknown_author">不明な作者</string>
|
||||
<string name="copy_text">テキストをコピー</string>
|
||||
<string name="copy_user_pubkey">ユーザIDをコピー</string>
|
||||
<string name="copy_note_id">投稿IDをコピー</string>
|
||||
<string name="broadcast">ブロードキャスト</string>
|
||||
<string name="request_deletion">削除をリクエスト</string>
|
||||
<string name="block_report">ブロック / 通報</string>
|
||||
<string name="block_hide_user">ユーザをブロックして非表示</string>
|
||||
<string name="report_spam_scam">スパム / 詐欺を通報</string>
|
||||
<string name="report_impersonation">なりすましを通報</string>
|
||||
<string name="report_explicit_content">露骨なコンテンツを通報</string>
|
||||
<string name="report_illegal_behaviour">不法行為を通報</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">秘密鍵でログインするとリプライできます</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">秘密鍵でログインすると投稿をブーストできます</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">秘密鍵でログインするといいねができます</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">ザップ額が未設定です。長押しで変更</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">秘密鍵でログインするとザップすることができます</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_follow">秘密鍵でログインするとフォローすることができます</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_unfollow">秘密鍵でログインするとフォロー解除することができます</string>
|
||||
<string name="zaps">ザップ</string>
|
||||
<string name="view_count">表示回数</string>
|
||||
<string name="boost">ブースト</string>
|
||||
<string name="boosted">ブーストされました</string>
|
||||
<string name="quote">引用</string>
|
||||
<string name="new_amount_in_sats">新しいザップ額 (sats)</string>
|
||||
<string name="add">追加</string>
|
||||
<string name="replying_to">"リプライ先: "</string>
|
||||
<string name="and">" と "</string>
|
||||
<string name="in_channel">"in channel "</string>
|
||||
<string name="profile_banner">プロフィールバナー</string>
|
||||
<string name="following">" フォロー"</string>
|
||||
<string name="followers">" フォロワー"</string>
|
||||
<string name="profile">プロフィール</string>
|
||||
<string name="security_filters">セキュリティフィルタ</string>
|
||||
<string name="log_out">ログアウト</string>
|
||||
<string name="show_more">続きを表示</string>
|
||||
<string name="lightning_invoice">Lightningインボイス</string>
|
||||
<string name="pay">支払う</string>
|
||||
<string name="lightning_tips">Lightningチップ</string>
|
||||
<string name="note_to_receiver">Note to Receiver</string>
|
||||
<string name="thank_you_so_much">ありがとうございます!</string>
|
||||
<string name="amount_in_sats">金額 (sats)</string>
|
||||
<string name="send_sats">satsを送る</string>
|
||||
<string name="error_parsing_preview_for">%1$sのプレビューのパースに失敗しました: %2$s</string>
|
||||
<string name="preview_card_image_for">%1$sのプレビューカード画像</string>
|
||||
<string name="new_channel">新しいチャンネル</string>
|
||||
<string name="channel_name">チャンネル名</string>
|
||||
<string name="my_awesome_group">私の素晴らしいグループ</string>
|
||||
<string name="picture_url">画像URL</string>
|
||||
<string name="description">説明</string>
|
||||
<string name="about_us">"私たちについて… "</string>
|
||||
<string name="what_s_on_your_mind">何か思いついた?</string>
|
||||
<string name="post">送信</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="create">作成</string>
|
||||
<string name="cancel">キャンセル</string>
|
||||
<string name="failed_to_upload_the_image">画像のアップロードに失敗しました</string>
|
||||
<string name="relay_address">リレーアドレス</string>
|
||||
<string name="posts">投稿</string>
|
||||
<string name="bytes">バイト</string>
|
||||
<string name="errors">エラー</string>
|
||||
<string name="home_feed">ホームフィード</string>
|
||||
<string name="private_message_feed">非公開メッセージフィード</string>
|
||||
<string name="public_chat_feed">公開チャットフィード</string>
|
||||
<string name="global_feed">グローバルフィード</string>
|
||||
<string name="search_feed">検索フィード</string>
|
||||
<string name="add_a_relay">リレーを追加</string>
|
||||
<string name="display_name">表示名</string>
|
||||
<string name="my_display_name">わたしの表示名</string>
|
||||
<string name="username">ユーザ名</string>
|
||||
<string name="my_username">わたしのユーザ名</string>
|
||||
<string name="about_me">自己紹介</string>
|
||||
<string name="avatar_url">アバターURL</string>
|
||||
<string name="banner_url">バナーURL</string>
|
||||
<string name="website_url">ウェブサイトURL</string>
|
||||
<string name="ln_address">LNアドレス</string>
|
||||
<string name="ln_url_outdated">LN URL (旧式)</string>
|
||||
<string name="image_saved_to_the_gallery">画像を保存しました</string>
|
||||
<string name="failed_to_save_the_image">画像の保存に失敗しました</string>
|
||||
<string name="upload_image">画像をアップロード</string>
|
||||
<string name="uploading">アップロード中…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">satsを受け取るためのLightningアドレスが設定されていません</string>
|
||||
<string name="reply_here">"ここに返信… "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">投稿IDをクリップボードにコピー</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">チャンネルIDをクリップボードにコピー</string>
|
||||
<string name="edits_the_channel_metadata">チャンネルのメタデータを編集</string>
|
||||
<string name="join">参加</string>
|
||||
<string name="known">知り合い</string>
|
||||
<string name="new_requests">新しいリクエスト</string>
|
||||
<string name="blocked_users">ブロック済みユーザ</string>
|
||||
<string name="new_threads">新規スレッド</string>
|
||||
<string name="conversations">会話</string>
|
||||
<string name="notes">投稿</string>
|
||||
<string name="replies">リプライ</string>
|
||||
<string name="follows">フォロー</string>
|
||||
<string name="reports">通報</string>
|
||||
<string name="more_options">詳細なオプション</string>
|
||||
<string name="relays">" リレー"</string>
|
||||
<string name="website">ウェブサイト</string>
|
||||
<string name="lightning_address">Lightningアドレス</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">バックアップ用に nsec ID (パスワード) をクリップボードにコピー</string>
|
||||
<string name="copy_private_key_to_the_clipboard">クリップボードに秘密鍵をコピー</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">公開鍵をクリップボードにコピー</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">公開鍵 (NPub) をクリップボードにコピー</string>
|
||||
<string name="send_a_direct_message">ダイレクトメッセージを送信</string>
|
||||
<string name="edits_the_user_s_metadata">ユーザのメタデータを編集</string>
|
||||
<string name="follow">フォロー</string>
|
||||
<string name="follow_back">フォローバック</string>
|
||||
<string name="unblock">ブロック解除</string>
|
||||
<string name="copy_user_id">ユーザIDをコピー</string>
|
||||
<string name="unblock_user">ユーザのブロックを解除</string>
|
||||
<string name="npub_hex_username">npub、ユーザ名、テキスト</string>
|
||||
<string name="clear">消去</string>
|
||||
<string name="app_logo">アプリのロゴ</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec… または npub…</string>
|
||||
<string name="show_password">パスワードを表示</string>
|
||||
<string name="hide_password">パスワードを非表示</string>
|
||||
<string name="invalid_key">無効な鍵</string>
|
||||
<string name="i_accept_the">"同意する: "</string>
|
||||
<string name="terms_of_use">利用規約</string>
|
||||
<string name="acceptance_of_terms_is_required">利用規約の同意が必要です</string>
|
||||
<string name="key_is_required">鍵が必要です</string>
|
||||
<string name="login">ログイン</string>
|
||||
<string name="generate_a_new_key">新しい鍵を生成</string>
|
||||
<string name="loading_feed">フィードを読み込み中</string>
|
||||
<string name="error_loading_replies">"リプライ読み込みエラー: "</string>
|
||||
<string name="try_again">後ほどお試しください</string>
|
||||
<string name="feed_is_empty">フィードが空です</string>
|
||||
<string name="refresh">更新</string>
|
||||
<string name="created">が作成しました:</string>
|
||||
<string name="with_description_of">説明:</string>
|
||||
<string name="and_picture">画像:</string>
|
||||
<string name="changed_chat_name_to">がチャンネル情報を変更しました:</string>
|
||||
<string name="description_to">説明:</string>
|
||||
<string name="and_picture_to">画像:</string>
|
||||
<string name="leave">退出</string>
|
||||
<string name="unfollow">フォロー解除</string>
|
||||
<string name="channel_created">チャンネルが作成されました</string>
|
||||
<string name="channel_information_changed_to">チャンネル情報が次のように変更されました:</string>
|
||||
<string name="public_chat">公開チャット</string>
|
||||
<string name="posts_received">投稿を受信</string>
|
||||
<string name="remove">削除</string>
|
||||
<string name="translations_auto">自動</string>
|
||||
<string name="translations_translated_from">翻訳元:</string>
|
||||
<string name="translations_to">翻訳先:</string>
|
||||
<string name="translations_show_in_lang_first">最初に%1$sで表示</string>
|
||||
<string name="translations_always_translate_to_lang">常に%1$sに翻訳</string>
|
||||
<string name="translations_never_translate_from_lang">%1$sを翻訳しない</string>
|
||||
<string name="never">never</string>
|
||||
<string name="now">今</string>
|
||||
<string name="h">時間</string>
|
||||
<string name="m">分</string>
|
||||
<string name="d">日</string>
|
||||
<string name="nudity">ヌード</string>
|
||||
<string name="profanity_hateful_speech">誹謗中傷 / ヘイトスピーチ</string>
|
||||
<string name="report_hateful_speech">ヘイトスピーチを通報</string>
|
||||
<string name="report_nudity_porn">ヌード / ポルノを通報</string>
|
||||
<string name="others">その他</string>
|
||||
<string name="mark_all_known_as_read">すべての「知り合い」を既読にする</string>
|
||||
<string name="mark_all_new_as_read">すべての「新規リクエスト」を既読にする</string>
|
||||
<string name="mark_all_as_read">すべて既読にする</string>
|
||||
<string name="backup_keys">鍵をバックアップ</string>
|
||||
<string name="account_backup_tips_md" tools:ignore="Typos">
|
||||
## 鍵のバックアップと安全に利用するためのTips
|
||||
\n\nあなたのアカウントは秘密鍵によって保護されています。秘密鍵は **nsec1** から始まる長いランダムな文字列です。秘密鍵にアクセスできる人は、あなたのアカウントを使用してコンテンツを公開することができます。
|
||||
\n\n- 信頼できないウェブサイトやソフトウェアには、秘密鍵を **渡さない** でください。
|
||||
\n- Amethystの開発者があなたに秘密鍵をお聞きすることは **ありません** 。
|
||||
\n- アカウント復旧のために、秘密鍵を安全な方法で **バックアップして** ください 。パスワードマネージャーの利用を推奨しています。
|
||||
</string>
|
||||
<string name="secret_key_copied_to_clipboard">秘密鍵 (nsec) をクリップボードにコピーしました</string>
|
||||
<string name="copy_my_secret_key">秘密鍵をコピー</string>
|
||||
<string name="biometric_authentication_failed">認証に失敗しました</string>
|
||||
<string name="biometric_error">エラー</string>
|
||||
<string name="badge_created_by">%1$s によって作成</string>
|
||||
<string name="badge_award_image_for">%1$sのバッジ授与画像</string>
|
||||
<string name="new_badge_award_notif">新しいバッジが授与されました</string>
|
||||
<string name="award_granted_to">バッジ授与が承認されました:</string>
|
||||
<string name="copied_note_text_to_clipboard">テキストがクリップボードにコピーされました</string>
|
||||
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">\@npubをクリップボードにコピーしました</string>
|
||||
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">投稿ID (@note1) をクリップボードにコピーしました</string>
|
||||
<string name="select_text_dialog_top">テキストを選択</string>
|
||||
<string name="private_conversation_notification">"<プライベートメッセージの復号に失敗しました>\n\n%1$s と %2$s のプライベート/暗号化された会話であなたが言及されました。"</string>
|
||||
<string name="account_switch_add_account_dialog_title">アカウントを追加</string>
|
||||
<string name="drawer_accounts">アカウント</string>
|
||||
<string name="account_switch_select_account">アカウントを選択</string>
|
||||
<string name="account_switch_add_account_btn">新規アカウントを追加</string>
|
||||
<string name="account_switch_active_account">アクティブアカウント</string>
|
||||
<string name="account_switch_has_private_key">秘密鍵あり</string>
|
||||
<string name="account_switch_pubkey_only">読み取り専用、秘密鍵なし</string>
|
||||
<string name="back">戻る</string>
|
||||
<string name="quick_action_select">選択</string>
|
||||
<string name="quick_action_share_browser_link">ブラウザリンクを共有</string>
|
||||
<string name="quick_action_share">共有</string>
|
||||
<string name="quick_action_copy_user_id">ユーザID</string>
|
||||
<string name="quick_action_copy_note_id">投稿ID</string>
|
||||
<string name="quick_action_copy_text">テキストをコピー</string>
|
||||
<string name="quick_action_delete">削除</string>
|
||||
<string name="quick_action_unfollow">フォロー解除</string>
|
||||
<string name="quick_action_follow">フォロー</string>
|
||||
<string name="quick_action_request_deletion_alert_title">削除をリクエスト</string>
|
||||
<string name="quick_action_request_deletion_alert_body">Amethystは、現在接続しているリレーから投稿を削除するようリクエストします。これらのリレーや、投稿が保存されている可能性のある他のリレーから、あなたの投稿が永久に削除される保証はありません。</string>
|
||||
<string name="quick_action_block_dialog_btn">ブロック</string>
|
||||
<string name="quick_action_delete_dialog_btn">削除</string>
|
||||
<string name="quick_action_block">ブロック</string>
|
||||
<string name="quick_action_report">通報</string>
|
||||
<string name="quick_action_delete_button">削除</string>
|
||||
<string name="quick_action_dont_show_again_button">再度表示しない</string>
|
||||
<string name="report_dialog_spam">スパムまたは詐欺</string>
|
||||
<string name="report_dialog_profanity">誹謗中傷またはヘイトスピーチ</string>
|
||||
<string name="report_dialog_impersonation">悪質ななりすまし</string>
|
||||
<string name="report_dialog_nudity">ヌードまたは生々しいコンテンツ</string>
|
||||
<string name="report_dialog_illegal">不法行為</string>
|
||||
<string name="report_dialog_blocking_a_user">ユーザーをブロックするとアプリ内でそのユーザーのコンテンツが非表示になります。ブロックされたユーザーは、あなたの投稿を今までどおり閲覧可能です。ブロック済みユーザーはセキュリティフィルタ画面に表示されます。</string>
|
||||
<string name="report_dialog_block_hide_user_btn">ユーザをブロックして非表示</string>
|
||||
<string name="report_dialog_report_btn">通報</string>
|
||||
<string name="report_dialog_reminder_public">投稿したすべての通報は誰もが閲覧可能です。</string>
|
||||
<string name="report_dialog_additional_reason_placeholder">任意でレポートに関する追加情報を提供する</string>
|
||||
<string name="report_dialog_additional_reason_label">追加の情報</string>
|
||||
<string name="report_dialog_select_reason_label">理由</string>
|
||||
<string name="report_dialog_select_reason_placeholder">理由を選択…</string>
|
||||
<string name="report_dialog_post_report_btn">通報を送信</string>
|
||||
<string name="report_dialog_title">ブロックと通報</string>
|
||||
<string name="block_only">ブロック</string>
|
||||
|
||||
<string name="bookmarks">ブックマーク</string>
|
||||
<string name="private_bookmarks">非公開ブックマーク</string>
|
||||
<string name="public_bookmarks">公開ブックマーク</string>
|
||||
<string name="add_to_private_bookmarks">非公開ブックマークに追加</string>
|
||||
<string name="add_to_public_bookmarks">公開ブックマークに追加</string>
|
||||
<string name="remove_from_private_bookmarks">非公開ブックマークから削除</string>
|
||||
<string name="remove_from_public_bookmarks">公開ブックマークから削除</string>
|
||||
|
||||
<string name="wallet_connect_service">Wallet Connectサービス</string>
|
||||
<string name="wallet_connect_service_explainer">アプリから離脱することなくザップするためにNostrシークレットを設定できます。シークレットは安全に保管し、可能であればプライベートリレーを使用してください</string>
|
||||
<string name="wallet_connect_service_pubkey">Wallet Connectの公開鍵</string>
|
||||
<string name="wallet_connect_service_relay">Wallet Connectのリレー</string>
|
||||
<string name="wallet_connect_service_secret">Wallet Connectの秘密鍵</string>
|
||||
<string name="wallet_connect_service_show_secret">秘密鍵を表示</string>
|
||||
<string name="wallet_connect_service_secret_placeholder">nsec または hex 形式の秘密鍵</string>
|
||||
|
||||
<string name="pledge_amount_in_sats">誓約額 (sats)</string>
|
||||
<string name="post_poll">投票を送信</string>
|
||||
<string name="poll_heading_required">必須のフィールド:</string>
|
||||
<string name="poll_zap_recipients">ザップ受取人</string>
|
||||
<string name="poll_primary_description">投票の説明…</string>
|
||||
<string name="poll_option_index">選択肢%s</string>
|
||||
<string name="poll_option_description">投票の選択肢の説明</string>
|
||||
<string name="poll_heading_optional">任意のフィールド:</string>
|
||||
<string name="poll_zap_value_min">最小ザップ額</string>
|
||||
<string name="poll_zap_value_max">最大ザップ額</string>
|
||||
<string name="poll_consensus_threshold">コンセンサス</string>
|
||||
<string name="poll_consensus_threshold_percent">(0–100)%</string>
|
||||
<string name="poll_closing_time">終了まで</string>
|
||||
<string name="poll_closing_time_days">日</string>
|
||||
<string name="poll_is_closed">投票は締め切られました</string>
|
||||
<string name="poll_zap_amount">ザップ額</string>
|
||||
<string name="one_vote_per_user_on_atomic_votes">このタイプの投票では、1ユーザにつき1回のみ投票可能です</string>
|
||||
|
||||
<string name="looking_for_event">イベント %1$s を探しています</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message">公開メッセージを追加</string>
|
||||
<string name="custom_zaps_add_a_message_private">プライベートメッセージを追加</string>
|
||||
<string name="custom_zaps_add_a_message_nonzap">インボイスメッセージを追加</string>
|
||||
|
||||
<string name="custom_zaps_add_a_message_example">おつかれさまでした!</string>
|
||||
|
||||
<string name="lightning_create_and_add_invoice">作成して追加</string>
|
||||
<string name="poll_author_no_vote">投票の作成者が投票することはできません。</string>
|
||||
|
||||
<string name="hash_verification_passed">画像は投稿時と同じです</string>
|
||||
<string name="hash_verification_failed">画像が変更されています。作者は変更に気づいていないかもしれません</string>
|
||||
|
||||
<string name="content_description_add_image">画像を追加</string>
|
||||
<string name="content_description_add_video">動画を追加</string>
|
||||
<string name="content_description_add_document">ドキュメントを追加</string>
|
||||
|
||||
<string name="add_content">メッセージに追加</string>
|
||||
<string name="content_description">コンテンツの説明</string>
|
||||
<string name="content_description_example">夕暮れの白い砂浜に浮かぶ青いボート</string>
|
||||
|
||||
<string name="zap_type">ザップの種類</string>
|
||||
<string name="zap_type_explainer">ザップ種別のオプション</string>
|
||||
|
||||
<string name="zap_type_public">公開</string>
|
||||
<string name="zap_type_public_explainer">誰もが取引とメッセージを閲覧できます</string>
|
||||
|
||||
<string name="zap_type_private">プライベート</string>
|
||||
<string name="zap_type_private_explainer">送金者と受取人だけが互いにメッセージを閲覧できます</string>
|
||||
|
||||
<string name="zap_type_anonymous">匿名</string>
|
||||
<string name="zap_type_anonymous_explainer">受取人を含め、すべての人は送金者を知ることができません</string>
|
||||
|
||||
<string name="zap_type_nonzap">非ザップ</string>
|
||||
<string name="zap_type_nonzap_explainer">Nostrを経由せずにLightning支払いを行います</string>
|
||||
|
||||
<string name="file_server">ファイルサーバ</string>
|
||||
<string name="zap_forward_lnAddress">LNアドレス または @ユーザ名</string>
|
||||
|
||||
<string name="upload_server_imgur">imgur.com - 信頼済み</string>
|
||||
<string name="upload_server_imgur_explainer">Imgurはファイルの変更が可能です</string>
|
||||
|
||||
<string name="upload_server_nostrimg">nostrimg.com - 信頼済み</string>
|
||||
<string name="upload_server_nostrimg_explainer">NostrImgはファイルの変更が可能です</string>
|
||||
|
||||
<string name="upload_server_nostrbuild">nostr.build - 信頼済み</string>
|
||||
<string name="upload_server_nostrbuild_explainer">Nostr.buildはファイルの変更が可能です</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev">nostrfiles.dev - 信頼済み</string>
|
||||
<string name="upload_server_nostrfilesdev_explainer">Nostrfiles.devはファイルの変更が可能です</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme">nostrcheck.me - 信頼済み</string>
|
||||
<string name="upload_server_nostrcheckme_explainer">nostrcheck.meはファイルの変更が可能です</string>
|
||||
|
||||
<string name="upload_server_imgur_nip94">検証可能な Imgur (NIP-94)</string>
|
||||
<string name="upload_server_imgur_nip94_explainer">Imgurによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
|
||||
<string name="upload_server_nostrimg_nip94">検証可能な NostrImg (NIP-94)</string>
|
||||
<string name="upload_server_nostrimg_nip94_explainer">NostrImgによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
|
||||
<string name="upload_server_nostrbuild_nip94">検証可能な Nostr.build (NIP-94)</string>
|
||||
<string name="upload_server_nostrbuild_nip94_explainer">Nostr.buildによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
|
||||
<string name="upload_server_nostrfilesdev_nip94">検証可能な Nostrfiles.dev (NIP-94)</string>
|
||||
<string name="upload_server_nostrfilesdev_nip94_explainer">Nostrfiles.devによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
|
||||
<string name="upload_server_nostrcheckme_nip94">検証可能な Nostrcheck.me (NIP-94)</string>
|
||||
<string name="upload_server_nostrcheckme_nip94_explainer">Nostrcheck.meによるファイルの変更をチェックします。新規NIP: 他のクライアントでは表示できない可能性があります</string>
|
||||
|
||||
<string name="upload_server_relays_nip95">あなたのリレー (NIP-95)</string>
|
||||
<string name="upload_server_relays_nip95_explainer">ファイルはあなたのリレーによってホストされます。新規NIP: リレーがサポートしているか確認してください</string>
|
||||
|
||||
<string name="connect_via_tor_short">Tor/Orbotを設定</string>
|
||||
<string name="connect_via_tor">Orbotの設定を通して接続</string>
|
||||
|
||||
<string name="do_you_really_want_to_disable_tor_title">Orbot/Torから切断しますか?</string>
|
||||
<string name="do_you_really_want_to_disable_tor_text">データはすぐに通常のネットワークで送信されます</string>
|
||||
<string name="yes">はい</string>
|
||||
<string name="no">いいえ</string>
|
||||
|
||||
<string name="follow_list_selection">フォローリスト</string>
|
||||
<string name="follow_list_kind3follows">すべてのフォロー</string>
|
||||
<string name="follow_list_global">グローバル</string>
|
||||
<string name="connect_through_your_orbot_setup_markdown">
|
||||
## Orbotを利用してTor経由で接続する
|
||||
\n\n1. [Orbot](https://play.google.com/store/apps/details?id=org.torproject.android)をインストール
|
||||
\n2. Orbotを立ち上げる
|
||||
\n3. OrbotでSocksポート番号を確認する。デフォルトは9050
|
||||
\n4. 必要に応じてOrbotでポート番号を変更する
|
||||
\n5. この画面でSocksポート番号を設定する
|
||||
\n6. アクティベートボタンを押して、Orbotをプロキシとして使用する
|
||||
</string>
|
||||
<string name="orbot_socks_port">OrbotのSocksポート番号</string>
|
||||
<string name="invalid_port_number">無効なポート番号</string>
|
||||
<string name="use_orbot">Orbotを使用</string>
|
||||
<string name="disconnect_from_your_orbot_setup">Tor/Orbotを切断</string>
|
||||
|
||||
<string name="app_notification_dms_channel_name">プライベートメッセージ</string>
|
||||
<string name="app_notification_dms_channel_description">プライベートメッセージが届いたら通知する</string>
|
||||
|
||||
<string name="app_notification_zaps_channel_name">ザップを受け取りました</string>
|
||||
<string name="app_notification_zaps_channel_description">誰かがあなたにザップしたら通知する</string>
|
||||
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||
<string name="app_notification_zaps_channel_message_from">%1$sから</string>
|
||||
<string name="app_notification_zaps_channel_message_for">%1$sに</string>
|
||||
|
||||
<string name="reply_notify">"通知: "</string>
|
||||
|
||||
<string name="channel_list_join_conversation">会話に参加</string>
|
||||
<string name="channel_list_user_or_group_id">ユーザIDまたはグループID</string>
|
||||
<string name="channel_list_user_or_group_id_demo">npub、nevent または hex</string>
|
||||
<string name="channel_list_create_channel">作成</string>
|
||||
<string name="channel_list_join_channel">参加</string>
|
||||
|
||||
<string name="today">今日</string>
|
||||
|
||||
<string name="content_warning">コンテンツ警告</string>
|
||||
<string name="content_warning_explanation">この投稿には、一部の人々が不快や不穏に感じる可能性のあるセンシティブな内容が含まれています</string>
|
||||
<string name="content_warning_hide_all_sensitive_content">センシティブなコンテンツを常に非表示</string>
|
||||
<string name="content_warning_show_all_sensitive_content">センシティブなコンテンツを常に表示</string>
|
||||
<string name="content_warning_see_warnings">コンテンツ警告を常に表示</string>
|
||||
|
||||
<string name="recommended_apps">"推奨: "</string>
|
||||
<string name="filter_spam_from_strangers">見知らぬユーザからのスパムをフィルタリングする</string>
|
||||
<string name="warn_when_posts_have_reports_from_your_follows">あなたがフォローしている人に通報された投稿を警告する</string>
|
||||
|
||||
<string name="new_reaction_symbol">新しいリアクションシンボル</string>
|
||||
<string name="no_reaction_type_setup_long_press_to_change">リアクションタイプが選択されていません。長押しして変更</string>
|
||||
|
||||
<string name="zapraiser">Zapraiser</string>
|
||||
<string name="zapraiser_explainer">この投稿の目標募金額を追加します。サポートするクライアントは、寄付を促すためにこれをプログレスバーとして表示することができます</string>
|
||||
<string name="zapraiser_target_amount_in_sats">目標募金額 (sats)</string>
|
||||
|
||||
<string name="sats_to_complete">Zapraiser at %1$s. %2$s sats to goal</string>
|
||||
<string name="read_from_relay">リレーから読み込み</string>
|
||||
<string name="write_to_relay">リレーに書き込み</string>
|
||||
<string name="an_error_ocurred_trying_to_get_relay_information">%1$sからリレー情報を取得する際にエラーが発生しました</string>
|
||||
<string name="owner">管理者</string>
|
||||
<string name="version">バージョン</string>
|
||||
<string name="software">ソフトウェア</string>
|
||||
<string name="contact">連絡先</string>
|
||||
<string name="supports">サポートしているNIP</string>
|
||||
<string name="admission_fees">利用料</string>
|
||||
<string name="payments_url">決済URL</string>
|
||||
<string name="limitations">制限</string>
|
||||
<string name="countries">国</string>
|
||||
<string name="languages">言語</string>
|
||||
<string name="tags">タグ</string>
|
||||
<string name="posting_policy">投稿ポリシー</string>
|
||||
<string name="message_length">メッセージ長</string>
|
||||
<string name="subscriptions">サブスクリプション</string>
|
||||
<string name="filters">フィルタ</string>
|
||||
<string name="subscription_id_length">サブスクリプションID長</string>
|
||||
<string name="minimum_prefix">最小プレフィックス</string>
|
||||
<string name="maximum_event_tags">最大タグ数</string>
|
||||
<string name="content_length">コンテンツ長</string>
|
||||
<string name="minimum_pow">最小PoW</string>
|
||||
<string name="auth">認証</string>
|
||||
<string name="payment">決済</string>
|
||||
<string name="cashu">Cashuトークン</string>
|
||||
<string name="cashu_redeem">償還</string>
|
||||
|
||||
<string name="live_stream_live_tag">ライブ</string>
|
||||
<string name="live_stream_offline_tag">オフライン</string>
|
||||
<string name="live_stream_ended_tag">配信終了</string>
|
||||
<string name="live_stream_planned_tag">配信予定</string>
|
||||
|
||||
<string name="live_stream_is_offline">ライブ配信はオフラインです</string>
|
||||
<string name="live_stream_has_ended">ライブ配信は終了しました</string>
|
||||
<string name="are_you_sure_you_want_to_log_out">ログアウトするとローカル情報はすべて削除されます。アカウントを紛失しないよう、秘密鍵のバックアップがあることを確認してください。続行しますか?</string>
|
||||
<string name="followed_tags">フォロー済みタグ</string>
|
||||
|
||||
<string name="relay_setup">リレー</string>
|
||||
|
||||
<string name="discover_live">ライブ</string>
|
||||
<string name="discover_community">コミュニティ</string>
|
||||
<string name="discover_chat">チャット</string>
|
||||
<string name="community_approved_posts">承認済みの投稿</string>
|
||||
|
||||
<string name="groups_no_descriptor">このグループには説明またはルールがありません。追加するには管理者に相談してください</string>
|
||||
<string name="community_no_descriptor">このコミュニティには説明がありません。追加するには管理者に相談してください</string>
|
||||
|
||||
<string name="add_sensitive_content_label">センシティブなコンテンツ</string>
|
||||
<string name="add_sensitive_content_description">コンテンツを表示する前に、センシティブなコンテンツであることを示す警告を表示します</string>
|
||||
</resources>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Amethyst" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<style name="Theme.Amethyst" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/purple_700</item>
|
||||
<item name="android:windowBackground">@color/black</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>\
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<string name="copy_note_id">Copy Note ID</string>
|
||||
<string name="broadcast">Broadcast</string>
|
||||
<string name="request_deletion">Request Deletion</string>
|
||||
<string name="block_report">Block / Report</string>
|
||||
<string name="block_hide_user"><![CDATA[Block & Hide User]]></string>
|
||||
<string name="report_spam_scam">Report Spam / Scam</string>
|
||||
<string name="report_impersonation">Report Impersonation</string>
|
||||
@@ -483,4 +484,17 @@
|
||||
|
||||
<string name="add_sensitive_content_label">Sensitive Content</string>
|
||||
<string name="add_sensitive_content_description">Adds sensitive content warning before showing this content</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="always">Always</string>
|
||||
<string name="wifi_only">Wifi-only</string>
|
||||
<string name="system">System</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="application_preferences">Application preferences</string>
|
||||
<string name="language">Language</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="automatically_load_images_gifs">Automatically load images/gifs</string>
|
||||
<string name="automatically_play_videos">Automatically play videos</string>
|
||||
<string name="automatically_show_url_preview">Automatically show url preview</string>
|
||||
<string name="load_image">Load Image</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<style name="Theme.Amethyst" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<style name="Theme.Amethyst" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@color/purple_700</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode" tools:ignore="NewApi">shortEdges</item>
|
||||
</style>
|
||||
|
||||
@@ -7,17 +7,17 @@
|
||||
<locale android:name="fa"/>
|
||||
<locale android:name="fr"/>
|
||||
<locale android:name="hu"/>
|
||||
<locale android:name="night"/>
|
||||
<locale android:name="nl"/>
|
||||
<locale android:name="pr-rBR"/>
|
||||
<locale android:name="pt-BR"/>
|
||||
<locale android:name="ru"/>
|
||||
<locale android:name="sv-rSE"/>
|
||||
<locale android:name="sv-SE"/>
|
||||
<locale android:name="ta"/>
|
||||
<locale android:name="tr"/>
|
||||
<locale android:name="uk"/>
|
||||
<locale android:name="zh"/>
|
||||
<locale android:name="zh-rHK"/>
|
||||
<locale android:name="zh-rTW"/>
|
||||
<locale android:name="zh-HK"/>
|
||||
<locale android:name="zh-TW"/>
|
||||
<locale android:name="en"/>
|
||||
<locale android:name="en-GB"/>
|
||||
</locale-config>
|
||||
<locale android:name="ja"/>
|
||||
</locale-config>
|
||||
|
||||
@@ -20,6 +20,11 @@ class CharsetTest {
|
||||
Assert.assertEquals("\uD83E\uDDD1\uD83C\uDFFE", "\uD83E\uDDD1\uD83C\uDFFEHi".firstFullChar())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testUTF32JoinChar2() {
|
||||
Assert.assertEquals("\uD83E\uDDD1\uD83C\uDFFE", "\uD83E\uDDD1\uD83C\uDFFEHi".firstFullChar())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAsciiWithUTF32Char() {
|
||||
Assert.assertEquals("H", "Hi\uD83E\uDDD1\uD83C\uDFFEHi".firstFullChar())
|
||||
@@ -54,4 +59,9 @@ class CharsetTest {
|
||||
fun testVariation() {
|
||||
Assert.assertEquals("\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC68", "\uD83D\uDC68\u200D\u2764\uFE0F\u200D\uD83D\uDC8B\u200D\uD83D\uDC68ddd".firstFullChar())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMultipleEmoji() {
|
||||
Assert.assertEquals("\uD83E\uDEC2\uD83E\uDEC2", "\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2\uD83E\uDEC2".firstFullChar())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user