mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-22 15:52:22 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0da542fede | ||
|
|
d883cc32f5 | ||
|
|
64debb79fc | ||
|
|
4ee6759957 | ||
|
|
723db61e8d | ||
|
|
cb39281268 | ||
|
|
baa1b3f69f | ||
|
|
c7bf4240dd | ||
|
|
fb7edd1797 | ||
|
|
4448e38100 | ||
|
|
716ebd01df | ||
|
|
58521b27de | ||
|
|
f4db64fe7f | ||
|
|
d704352f1f | ||
|
|
511cc86025 | ||
|
|
58e8e0591b | ||
|
|
57fc149051 | ||
|
|
55cc780c08 | ||
|
|
01a7ef3785 | ||
|
|
b77e1cf960 | ||
|
|
c6eb5a7282 | ||
|
|
bf48428fe3 | ||
|
|
f4b366c4bd | ||
|
|
51ce424352 | ||
|
|
01b8b2a5fa | ||
|
|
459e503c8a | ||
|
|
708f6b2e75 | ||
|
|
b7b7f4df39 | ||
|
|
855b2ff5b1 | ||
|
|
67e6349877 | ||
|
|
5eb3b2062c | ||
|
|
c649d0820c | ||
|
|
07fd8fc5ac | ||
|
|
e6afa7fb9a | ||
|
|
e0596dc3b9 | ||
|
|
381db713ab | ||
|
|
b6f5cc9ae5 | ||
|
|
df43e730ca | ||
|
|
95423bdd94 | ||
|
|
a18479e721 | ||
|
|
f68f32f6e5 | ||
|
|
ddc1e9fd69 | ||
|
|
3f99b602a7 | ||
|
|
b09577f816 | ||
|
|
3780ed001a | ||
|
|
f8e49df126 | ||
|
|
55e31d445e | ||
|
|
5300d7bbe4 | ||
|
|
77fbb6f4f6 | ||
|
|
b570d80ac4 | ||
|
|
a7e17c810d | ||
|
|
256de32b7c |
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[BUG]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Video and Screenshots**
|
||||
If applicable, add a video and/or screenshots to help explain your problem.
|
||||
|
||||
**Device (please complete the following information):**
|
||||
- Phone Brand/Model [e.g. Pixel 7 Pro]:
|
||||
- Android Version [e.g. 33]:
|
||||
- App Version [e.g. v0.20.3]:
|
||||
|
||||
**Bounty (in Bitcoin sats) offered for a solution**
|
||||
Incentivize developers to work on your issue. Describe clear milestones to claim payment.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[FEATURE]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Bounty (in Bitcoin sats) offered for the implementation**
|
||||
Incentivize developers to work on your feature. Describe clear milestones to claim payment.
|
||||
|
||||
**Additional context**
|
||||
Add any other context, video, or screenshots about the feature request here.
|
||||
1
.idea/.name
generated
Normal file
1
.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
Amethyst
|
||||
3
.idea/inspectionProfiles/Project_Default.xml
generated
3
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -5,6 +5,9 @@
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
||||
6
.idea/kotlinc.xml
generated
Normal file
6
.idea/kotlinc.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.8.10" />
|
||||
</component>
|
||||
</project>
|
||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
|
||||
45
README.md
45
README.md
@@ -6,31 +6,48 @@ Amethyst brings the best social network to your Android phone. Just insert your
|
||||
|
||||
# Current Features
|
||||
|
||||
- [x] Account Management
|
||||
- [x] Event Builders / WebSocket Subscriptions (NIP-01, NIP-15)
|
||||
- [x] Home Feed
|
||||
- [x] Notifications Feed
|
||||
- [x] Global Feed
|
||||
- [x] Reactions (like, boost, reply)
|
||||
- [x] Image/Video Preview (gifs, svgs, etc)
|
||||
- [x] Url Preview
|
||||
- [x] Reactions (likes NIP-25, boost, reply)
|
||||
- [x] Image/Video/Url/LnInvoice Previews
|
||||
- [x] View Threads
|
||||
- [x] Private Messages (NIP-04)
|
||||
- [x] User Profiles (follow/unfollow)
|
||||
- [x] User Profiles (edit/follow/unfollow - NIP-02)
|
||||
- [x] Public Chats (NIP-28)
|
||||
- [x] Bech Encoding support (NIP-19)
|
||||
- [x] Notification Dots
|
||||
- [x] Reporting and Hide User capability
|
||||
- [x] Reporting and Hide User capability (NIP-56)
|
||||
- [x] Automatic Translations
|
||||
- [x] Profile Edit
|
||||
- [x] Relay Edit (separate relay sets for home, dms, public chats, global)
|
||||
- [x] Dropdown to Link Users when writing
|
||||
- [x] Relay Sets (home, dms, public chats, global)
|
||||
- [x] User/Note Tagging (NIP-08, NIP-10)
|
||||
- [x] Lightning Tips
|
||||
- [x] Zaps
|
||||
- [x] Zaps (NIP-57)
|
||||
- [x] URI Support (NIP-21)
|
||||
- [x] Event Deletion (NIP-09: like, boost, text notes and reports)
|
||||
- [x] Identity Verification (NIP-05)
|
||||
- [ ] Local Database
|
||||
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
|
||||
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
|
||||
- [ ] Sensitive Content (NIP-36)
|
||||
- [ ] Relay Pages (NIP-11)
|
||||
- [ ] Relay Authentication (NIP-42)
|
||||
- [ ] Generic Tags (NIP-12)
|
||||
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
|
||||
- [ ] Events with a Subject (NIP-14)
|
||||
- [ ] Long-form Content (NIP-23)
|
||||
- [ ] Online Relay Search (NIP-50)
|
||||
- [ ] Workspaces
|
||||
- [ ] Expiration Support (NIP-40)
|
||||
- [ ] Internationalization
|
||||
- [ ] Infinity Scroll
|
||||
- [ ] Identity Verification (NIP-05)
|
||||
- [ ] Account Creation / Backup Guidance
|
||||
- [ ] Message Sent feedback
|
||||
- [ ] Infinity Scroll
|
||||
- [ ] Badges (NIP-58)
|
||||
- [ ] Relay List Metadata (NIP-65)
|
||||
- [ ] Signing Requests (NIP-46)
|
||||
- [ ] Delegated Event Signing (NIP-26)
|
||||
- [ ] Account Creation / Backup Guidance (NIP-06)
|
||||
- [ ] Message Sent feedback (NIP-20)
|
||||
|
||||
# Development Overview
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ android {
|
||||
applicationId "com.vitorpamplona.amethyst"
|
||||
minSdk 26
|
||||
targetSdk 33
|
||||
versionCode 76
|
||||
versionName "0.20.2"
|
||||
versionCode 83
|
||||
versionName "0.22.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
@@ -130,6 +130,11 @@ dependencies {
|
||||
implementation 'androidx.camera:camera-lifecycle:1.2.1'
|
||||
implementation 'androidx.camera:camera-view:1.2.1'
|
||||
|
||||
// Markdown
|
||||
implementation "com.halilibo.compose-richtext:richtext-ui:0.16.0"
|
||||
implementation "com.halilibo.compose-richtext:richtext-ui-material:0.16.0"
|
||||
implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
|
||||
|
||||
// For QR Scanning
|
||||
implementation 'com.google.mlkit:vision-common:17.3.0'
|
||||
|
||||
|
||||
@@ -44,12 +44,10 @@ class NotificationLiveData(val cache: NotificationCache): LiveData<NotificationS
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
fun invalidateData() {
|
||||
if (!hasActiveObservers()) return
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
package com.vitorpamplona.amethyst
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.util.LruCache
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import java.util.UUID
|
||||
import name.neuhalfen.projects.android.robohash.RoboHash
|
||||
import name.neuhalfen.projects.android.robohash.buckets.VariableSizeHashing
|
||||
import name.neuhalfen.projects.android.robohash.handle.Handle
|
||||
import name.neuhalfen.projects.android.robohash.handle.HandleFactory
|
||||
import name.neuhalfen.projects.android.robohash.paths.Configuration
|
||||
import name.neuhalfen.projects.android.robohash.repository.ImageRepository
|
||||
|
||||
object RoboHashCache {
|
||||
|
||||
lateinit var robots: RoboHash
|
||||
lateinit var robots: MyRoboHash
|
||||
|
||||
lateinit var defaultAvatar: ImageBitmap
|
||||
|
||||
@Synchronized
|
||||
fun get(context: Context, hash: String): ImageBitmap {
|
||||
if (!this::robots.isInitialized) {
|
||||
robots = RoboHash(context)
|
||||
//robots.useCache(LruCache(100));
|
||||
robots = MyRoboHash(context)
|
||||
|
||||
defaultAvatar = robots.imageForHandle(robots.calculateHandleFromUUID(UUID.nameUUIDFromBytes("aaaa".toByteArray()))).asImageBitmap()
|
||||
}
|
||||
@@ -24,4 +31,139 @@ object RoboHashCache {
|
||||
return defaultAvatar
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Recreates RoboHash to use a custom configuration
|
||||
*/
|
||||
class MyRoboHash(context: Context) {
|
||||
private val configuration: Configuration = ModifiedSet1Configuration()
|
||||
private val repository: ImageRepository
|
||||
private val hashing = VariableSizeHashing(configuration.bucketSizes)
|
||||
|
||||
// Optional
|
||||
private var memoryCache: LruCache<String, Bitmap>? = null
|
||||
|
||||
init {
|
||||
repository = ImageRepository(context.assets)
|
||||
}
|
||||
|
||||
fun useCache(memoryCache: LruCache<String, Bitmap>?) {
|
||||
this.memoryCache = memoryCache
|
||||
}
|
||||
|
||||
fun calculateHandleFromUUID(uuid: UUID?): Handle {
|
||||
val data = hashing.createBuckets(uuid)
|
||||
return handleFactory.calculateHandle(data)
|
||||
}
|
||||
|
||||
fun imageForHandle(handle: Handle): Bitmap {
|
||||
if (null != memoryCache) {
|
||||
val cached = memoryCache!![handle.toString()]
|
||||
if (null != cached) return cached
|
||||
}
|
||||
val bucketValues = handle.bucketValues()
|
||||
val paths = configuration.convertToFacetParts(bucketValues)
|
||||
val sampleSize = 1
|
||||
val buffer = repository.createBuffer(configuration.width(), configuration.height())
|
||||
val target = buffer.copy(Bitmap.Config.ARGB_8888, true)
|
||||
val merged = Canvas(target)
|
||||
val paint = Paint(0)
|
||||
|
||||
// The first image is not added as copy form the buffer
|
||||
for (i in paths.indices) {
|
||||
merged.drawBitmap(repository.getInto(buffer, paths[i], sampleSize), 0f, 0f, paint)
|
||||
}
|
||||
repository.returnBuffer(buffer)
|
||||
if (null != memoryCache) {
|
||||
memoryCache!!.put(handle.toString(), target)
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val handleFactory = HandleFactory()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom configuration to avoid the use of String.format in the GeneratePath
|
||||
* This uses the default location and ends up encoding number in the local language
|
||||
*/
|
||||
class ModifiedSet1Configuration : Configuration {
|
||||
override fun convertToFacetParts(bucketValues: ByteArray): Array<String> {
|
||||
require(bucketValues.size == BUCKET_COUNT)
|
||||
val color = INT_TO_COLOR[bucketValues[BUCKET_COLOR].toInt()]
|
||||
val paths = mutableListOf<String>()
|
||||
|
||||
// e.g.
|
||||
// blue face #2
|
||||
// blue nose #7
|
||||
// blue
|
||||
val firstFacetBucket = BUCKET_COLOR + 1
|
||||
for (facet in 0 until FACET_COUNT) {
|
||||
val bucketValue = bucketValues[firstFacetBucket + facet].toInt()
|
||||
paths.add(generatePath(FACET_PATH_TEMPLATES[facet], color, bucketValue))
|
||||
}
|
||||
return paths.toTypedArray()
|
||||
}
|
||||
|
||||
private fun generatePath(facetPathTemplate: String, color: String, bucketValue: Int): String {
|
||||
// TODO: Make more efficient
|
||||
return facetPathTemplate.replace("#ROOT#", ROOT).replace("#COLOR#".toRegex(), color)
|
||||
.replace("#ITEM#".toRegex(), (bucketValue + 1).toString().padStart(2, '0'))
|
||||
}
|
||||
|
||||
override fun getBucketSizes(): ByteArray {
|
||||
return BUCKET_SIZES
|
||||
}
|
||||
|
||||
override fun width(): Int {
|
||||
return 300
|
||||
}
|
||||
|
||||
override fun height(): Int {
|
||||
return 300
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ROOT = "sets/set1"
|
||||
private const val BUCKET_COLOR = 0
|
||||
private const val COLOR_COUNT = 10
|
||||
private const val BODY_COUNT = 10
|
||||
private const val FACE_COUNT = 10
|
||||
private const val MOUTH_COUNT = 10
|
||||
private const val EYES_COUNT = 10
|
||||
private const val ACCESSORY_COUNT = 10
|
||||
private const val BUCKET_COUNT = 6
|
||||
private const val FACET_COUNT = 5
|
||||
private val BUCKET_SIZES = byteArrayOf(
|
||||
COLOR_COUNT.toByte(),
|
||||
BODY_COUNT.toByte(),
|
||||
FACE_COUNT.toByte(),
|
||||
MOUTH_COUNT.toByte(),
|
||||
EYES_COUNT.toByte(),
|
||||
ACCESSORY_COUNT.toByte()
|
||||
)
|
||||
private val INT_TO_COLOR = arrayOf(
|
||||
"blue",
|
||||
"brown",
|
||||
"green",
|
||||
"grey",
|
||||
"orange",
|
||||
"pink",
|
||||
"purple",
|
||||
"red",
|
||||
"white",
|
||||
"yellow"
|
||||
)
|
||||
private val FACET_PATH_TEMPLATES = arrayOf(
|
||||
"#ROOT#/#COLOR#/01Body/#COLOR#_body-#ITEM#.png",
|
||||
"#ROOT#/#COLOR#/02Face/#COLOR#_face-#ITEM#.png",
|
||||
"#ROOT#/#COLOR#/Mouth/#COLOR#_mouth-#ITEM#.png",
|
||||
"#ROOT#/#COLOR#/Eyes/#COLOR#_eyes-#ITEM#.png",
|
||||
"#ROOT#/#COLOR#/Accessory/#COLOR#_accessory-#ITEM#.png"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ object ServiceManager {
|
||||
NostrAccountDataSource.account = myAccount
|
||||
NostrHomeDataSource.account = myAccount
|
||||
NostrChatroomListDataSource.account = myAccount
|
||||
NostrGlobalDataSource.account = myAccount
|
||||
|
||||
// Notification Elements
|
||||
NostrAccountDataSource.start()
|
||||
|
||||
@@ -605,11 +605,9 @@ class Account(
|
||||
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -67,12 +67,10 @@ class AntiSpamLiveData(val cache: AntiSpamFilter): LiveData<AntiSpamState>(AntiS
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
fun invalidateData() {
|
||||
if (!hasActiveObservers()) return
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -23,7 +23,6 @@ class Channel(val idHex: String) {
|
||||
return info.name ?: idDisplayNote()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addNote(note: Note) {
|
||||
notes[note.idHex] = note
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
@@ -184,7 +185,52 @@ object LocalCache {
|
||||
refreshObservers()
|
||||
}
|
||||
|
||||
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
||||
fun consume(event: LongTextNoteEvent, relay: Relay?) {
|
||||
if (antiSpam.isSpam(event)) {
|
||||
relay?.let {
|
||||
it.spamCounter++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val note = getOrCreateNote(event.id.toHex())
|
||||
val author = getOrCreateUser(event.pubKey.toHexKey())
|
||||
|
||||
if (relay != null) {
|
||||
author.addRelayBeingUsed(relay, event.createdAt)
|
||||
note.addRelay(relay)
|
||||
}
|
||||
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val mentions = event.mentions.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val replyTo = replyToWithoutCitations(event).mapNotNull { checkGetOrCreateNote(it) }
|
||||
|
||||
note.loadEvent(event, author, mentions, replyTo)
|
||||
|
||||
//Log.d("TN", "New Note (${notes.size},${users.size}) ${note.author?.toBestDisplayName()} ${note.event?.content?.take(100)} ${formattedDateTime(event.createdAt)}")
|
||||
|
||||
// Prepares user's profile view.
|
||||
author.addNote(note)
|
||||
|
||||
// Adds notifications to users.
|
||||
mentions.forEach {
|
||||
it.addTaggedPost(note)
|
||||
}
|
||||
replyTo.forEach {
|
||||
it.author?.addTaggedPost(note)
|
||||
}
|
||||
|
||||
// Counts the replies
|
||||
replyTo.forEach {
|
||||
it.addReply(note)
|
||||
}
|
||||
|
||||
refreshObservers()
|
||||
}
|
||||
|
||||
private fun findCitations(event: Event): Set<String> {
|
||||
var citations = mutableSetOf<String>()
|
||||
// Removes citations from replies:
|
||||
val matcher = tagSearch.matcher(event.content)
|
||||
@@ -198,6 +244,17 @@ object LocalCache {
|
||||
|
||||
}
|
||||
}
|
||||
return citations
|
||||
}
|
||||
|
||||
private fun replyToWithoutCitations(event: TextNoteEvent): List<String> {
|
||||
val citations = findCitations(event)
|
||||
|
||||
return event.replyTos.filter { it !in citations }
|
||||
}
|
||||
|
||||
private fun replyToWithoutCitations(event: LongTextNoteEvent): List<String> {
|
||||
val citations = findCitations(event)
|
||||
|
||||
return event.replyTos.filter { it !in citations }
|
||||
}
|
||||
@@ -408,8 +465,8 @@ object LocalCache {
|
||||
// Already processed this event.
|
||||
if (note.event != null) return
|
||||
|
||||
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it) }
|
||||
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it) }
|
||||
val mentions = event.reportedAuthor.mapNotNull { checkGetOrCreateUser(it.key) }
|
||||
val repliesTo = event.reportedPost.mapNotNull { checkGetOrCreateNote(it.key) }
|
||||
|
||||
note.loadEvent(event, author, mentions, repliesTo)
|
||||
|
||||
@@ -735,16 +792,14 @@ class LocalCacheLiveData(val cache: LocalCache): LiveData<LocalCacheState>(Local
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
fun invalidateData() {
|
||||
if (!hasActiveObservers()) return
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
delay(100)
|
||||
delay(500)
|
||||
refresh()
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
|
||||
@@ -18,8 +18,10 @@ import java.util.regex.Pattern
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nostr.postr.events.Event
|
||||
|
||||
val tagSearch = Pattern.compile("(?:\\s|\\A)\\#\\[([0-9]+)\\]")
|
||||
@@ -318,17 +320,20 @@ class NoteLiveData(val note: Note): LiveData<NoteState>(NoteState(note)) {
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
fun invalidateData() {
|
||||
if (!hasActiveObservers()) return
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
delay(100)
|
||||
refresh()
|
||||
handlerWaiting.set(false)
|
||||
try {
|
||||
delay(100)
|
||||
refresh()
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
handlerWaiting.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -289,7 +289,7 @@ class User(val pubkeyHex: String) {
|
||||
|
||||
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
|
||||
return reports[loggedIn]?.firstOrNull() {
|
||||
it.event is ReportEvent && (it.event as ReportEvent).reportType.contains(type)
|
||||
it.event is ReportEvent && (it.event as ReportEvent).reportedAuthor.any { it.reportType == type }
|
||||
} != null
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ class UserMetadata {
|
||||
|
||||
fun anyNameStartsWith(prefix: String): Boolean {
|
||||
return listOfNotNull(name, username, display_name, displayName, nip05, lud06, lud16)
|
||||
.filter { it.startsWith(prefix, true) }.isNotEmpty()
|
||||
.any { it.startsWith(prefix, true) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,11 +379,9 @@ class UserLiveData(val user: User): LiveData<UserState>(UserState(user)) {
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
@@ -17,11 +18,14 @@ import com.vitorpamplona.amethyst.service.relays.Subscription
|
||||
import com.vitorpamplona.amethyst.service.relays.hasValidSignature
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import nostr.postr.events.ContactListEvent
|
||||
import nostr.postr.events.DeletionEvent
|
||||
import nostr.postr.events.Event
|
||||
@@ -42,6 +46,7 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val clientListener = object : Client.Listener() {
|
||||
override fun onEvent(event: Event, subscriptionId: String, relay: Relay) {
|
||||
if (subscriptionId in subscriptions.keys) {
|
||||
@@ -86,12 +91,13 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
ChannelMessageEvent.kind -> LocalCache.consume(ChannelMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
||||
ChannelHideMessageEvent.kind -> LocalCache.consume(ChannelHideMessageEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||
ChannelMuteUserEvent.kind -> LocalCache.consume(ChannelMuteUserEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig))
|
||||
|
||||
LongTextNoteEvent.kind -> LocalCache.consume(LongTextNoteEvent(event.id, event.pubKey, event.createdAt, event.tags, event.content, event.sig), relay)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,10 +129,12 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
}
|
||||
|
||||
open fun start() {
|
||||
println("DataSource: ${this.javaClass.simpleName} Start")
|
||||
resetFilters()
|
||||
}
|
||||
|
||||
open fun stop() {
|
||||
println("DataSource: ${this.javaClass.simpleName} Stop")
|
||||
subscriptions.values.forEach { channel ->
|
||||
Client.close(channel.id)
|
||||
channel.typedFilters = null
|
||||
@@ -144,17 +152,24 @@ abstract class NostrDataSource(val debugName: String) {
|
||||
subscriptions = subscriptions.minus(subscription.id)
|
||||
}
|
||||
|
||||
var handlerWaiting = false
|
||||
@Synchronized
|
||||
fun invalidateFilters() {
|
||||
if (handlerWaiting) return
|
||||
// Refreshes observers in batches.
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
fun invalidateFilters() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
println("DataSource: ${this.javaClass.simpleName} InvalidateFilters")
|
||||
|
||||
handlerWaiting = true
|
||||
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||
scope.launch {
|
||||
delay(200)
|
||||
resetFiltersSuspend()
|
||||
handlerWaiting = false
|
||||
try {
|
||||
delay(200)
|
||||
resetFiltersSuspend()
|
||||
} finally {
|
||||
withContext(NonCancellable) {
|
||||
handlerWaiting.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
@@ -8,7 +7,6 @@ import nostr.postr.JsonFilter
|
||||
import nostr.postr.events.TextNoteEvent
|
||||
|
||||
object NostrGlobalDataSource: NostrDataSource("GlobalFeed") {
|
||||
lateinit var account: Account
|
||||
fun createGlobalFilter() = TypedFilter(
|
||||
types = setOf(FeedType.GLOBAL),
|
||||
filter = JsonFilter(
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service
|
||||
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.UserState
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.relays.FeedType
|
||||
import com.vitorpamplona.amethyst.service.relays.TypedFilter
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -47,7 +48,7 @@ object NostrHomeDataSource: NostrDataSource("HomeFeed") {
|
||||
return TypedFilter(
|
||||
types = setOf(FeedType.FOLLOWS),
|
||||
filter = JsonFilter(
|
||||
kinds = listOf(TextNoteEvent.kind),
|
||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind),
|
||||
authors = followSet,
|
||||
limit = 400
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.vitorpamplona.amethyst.service.model
|
||||
|
||||
import java.util.Date
|
||||
import nostr.postr.Utils
|
||||
import nostr.postr.events.Event
|
||||
|
||||
class LongTextNoteEvent(
|
||||
id: ByteArray,
|
||||
pubKey: ByteArray,
|
||||
createdAt: Long,
|
||||
tags: List<List<String>>,
|
||||
content: String,
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
@Transient val replyTos: List<String>
|
||||
@Transient val mentions: List<String>
|
||||
|
||||
@Transient val title: String?
|
||||
@Transient val image: String?
|
||||
@Transient val summary: String?
|
||||
@Transient val publishedAt: Long?
|
||||
@Transient val topics: List<String>
|
||||
|
||||
init {
|
||||
replyTos = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
mentions = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
|
||||
topics = tags.filter { it.firstOrNull() == "t" }.mapNotNull { it.getOrNull(1) }
|
||||
title = tags.filter { it.firstOrNull() == "title" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
image = tags.filter { it.firstOrNull() == "image" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
summary = tags.filter { it.firstOrNull() == "summary" }.mapNotNull { it.getOrNull(1) }.firstOrNull()
|
||||
publishedAt = try {
|
||||
tags.filter { it.firstOrNull() == "published_at" }.mapNotNull { it.getOrNull(1) }.firstOrNull()?.toLong()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val kind = 30023
|
||||
|
||||
fun create(msg: String, replyTos: List<String>?, mentions: List<String>?, privateKey: ByteArray, createdAt: Long = Date().time / 1000): LongTextNoteEvent {
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val tags = mutableListOf<List<String>>()
|
||||
replyTos?.forEach {
|
||||
tags.add(listOf("e", it))
|
||||
}
|
||||
mentions?.forEach {
|
||||
tags.add(listOf("p", it))
|
||||
}
|
||||
val id = generateId(pubKey, createdAt, kind, tags, msg)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return LongTextNoteEvent(id, pubKey, createdAt, tags, msg, sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import nostr.postr.Utils
|
||||
import nostr.postr.events.Event
|
||||
import nostr.postr.toHex
|
||||
|
||||
data class ReportedKey(val key: String, val reportType: ReportEvent.ReportType)
|
||||
|
||||
// NIP 56 event.
|
||||
class ReportEvent (
|
||||
id: ByteArray,
|
||||
@@ -15,14 +17,37 @@ class ReportEvent (
|
||||
sig: ByteArray
|
||||
): Event(id, pubKey, createdAt, kind, tags, content, sig) {
|
||||
|
||||
@Transient val reportType: List<ReportType>
|
||||
@Transient val reportedPost: List<String>
|
||||
@Transient val reportedAuthor: List<String>
|
||||
@Transient val reportedPost: List<ReportedKey>
|
||||
@Transient val reportedAuthor: List<ReportedKey>
|
||||
|
||||
init {
|
||||
reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }
|
||||
reportedPost = tags.filter { it.firstOrNull() == "e" }.mapNotNull { it.getOrNull(1) }
|
||||
reportedAuthor = tags.filter { it.firstOrNull() == "p" }.mapNotNull { it.getOrNull(1) }
|
||||
// Works with old and new structures for report.
|
||||
|
||||
var reportType = tags.filter { it.firstOrNull() == "report" }.mapNotNull { it.getOrNull(1) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
|
||||
if (reportType == null) {
|
||||
reportType = tags.mapNotNull { it.getOrNull(2) }.map { ReportType.valueOf(it.toUpperCase()) }.firstOrNull()
|
||||
}
|
||||
if (reportType == null) {
|
||||
reportType = ReportType.SPAM
|
||||
}
|
||||
|
||||
reportedPost = tags
|
||||
.filter { it.firstOrNull() == "e" && it.getOrNull(1) != null }
|
||||
.map {
|
||||
ReportedKey(
|
||||
it[1],
|
||||
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType
|
||||
)
|
||||
}
|
||||
|
||||
reportedAuthor = tags
|
||||
.filter { it.firstOrNull() == "p" && it.getOrNull(1) != null }
|
||||
.map {
|
||||
ReportedKey(
|
||||
it[1],
|
||||
it.getOrNull(2)?.toUpperCase()?.let { it1 -> ReportType.valueOf(it1) }?: reportType
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -31,12 +56,11 @@ class ReportEvent (
|
||||
fun create(reportedPost: Event, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
||||
val content = ""
|
||||
|
||||
val reportTypeTag = listOf("report", type.name.toLowerCase())
|
||||
val reportPostTag = listOf("e", reportedPost.id.toHex())
|
||||
val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex())
|
||||
val reportPostTag = listOf("e", reportedPost.id.toHex(), type.name.toLowerCase())
|
||||
val reportAuthorTag = listOf("p", reportedPost.pubKey.toHex(), type.name.toLowerCase())
|
||||
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val tags:List<List<String>> = listOf(reportTypeTag, reportPostTag, reportAuthorTag)
|
||||
val tags:List<List<String>> = listOf(reportPostTag, reportAuthorTag)
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -45,11 +69,10 @@ class ReportEvent (
|
||||
fun create(reportedUser: String, type: ReportType, privateKey: ByteArray, createdAt: Long = Date().time / 1000): ReportEvent {
|
||||
val content = ""
|
||||
|
||||
val reportTypeTag = listOf("report", type.name.toLowerCase())
|
||||
val reportAuthorTag = listOf("p", reportedUser)
|
||||
val reportAuthorTag = listOf("p", reportedUser, type.name.toLowerCase())
|
||||
|
||||
val pubKey = Utils.pubkeyCreate(privateKey)
|
||||
val tags:List<List<String>> = listOf(reportTypeTag, reportAuthorTag)
|
||||
val tags:List<List<String>> = listOf(reportAuthorTag)
|
||||
val id = generateId(pubKey, createdAt, kind, tags, content)
|
||||
val sig = Utils.sign(id, privateKey)
|
||||
return ReportEvent(id, pubKey, createdAt, tags, content, sig)
|
||||
@@ -57,9 +80,11 @@ class ReportEvent (
|
||||
}
|
||||
|
||||
enum class ReportType() {
|
||||
EXPLICIT,
|
||||
EXPLICIT, // Not used anymore.
|
||||
ILLEGAL,
|
||||
SPAM,
|
||||
IMPERSONATION
|
||||
IMPERSONATION,
|
||||
NUDITY,
|
||||
PROFANITY,
|
||||
}
|
||||
}
|
||||
@@ -54,7 +54,7 @@ class Relay(
|
||||
if (socket != null) return
|
||||
|
||||
try {
|
||||
val request = Request.Builder().url(url.trim()).build()
|
||||
val request = Request.Builder().header("Origin", "amethyst.social").url(url.trim()).build()
|
||||
val listener = object : WebSocketListener() {
|
||||
|
||||
override fun onOpen(webSocket: WebSocket, response: Response) {
|
||||
@@ -88,7 +88,7 @@ class Relay(
|
||||
"NOTICE" -> listeners.forEach {
|
||||
//Log.w("Relay", "Relay onNotice $url, $channel")
|
||||
// "channel" being the second string in the string array ...
|
||||
it.onError(this@Relay, channel, Error("Relay sent notice: $channel"))
|
||||
it.onError(this@Relay, channel, Error("Relay sent notice: " + channel))
|
||||
}
|
||||
"OK" -> listeners.forEach {
|
||||
//Log.w("Relay", "Relay onOK $url, $channel")
|
||||
|
||||
@@ -94,7 +94,6 @@ object RelayPool: Relay.Listener {
|
||||
fun onSendResponse(eventId: String, success: Boolean, message: String, relay: Relay)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun onEvent(relay: Relay, subscriptionId: String, event: Event) {
|
||||
listeners.forEach { it.onEvent(event, subscriptionId, relay) }
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountScreen
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.AmethystTheme
|
||||
import java.util.Locale
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.provider.MediaStore
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import okhttp3.*
|
||||
import okio.BufferedSource
|
||||
import okio.IOException
|
||||
@@ -49,7 +50,7 @@ object ImageSaver {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val contentType = response.header("Content-Type")
|
||||
checkNotNull(contentType) {
|
||||
"Can't find out the content type"
|
||||
context.getString(R.string.can_t_find_out_the_content_type)
|
||||
}
|
||||
|
||||
saveContentQ(
|
||||
@@ -57,6 +58,7 @@ object ImageSaver {
|
||||
contentType = contentType,
|
||||
contentSource = response.body.source(),
|
||||
contentResolver = context.contentResolver,
|
||||
context = context
|
||||
)
|
||||
} else {
|
||||
saveContentDefault(
|
||||
@@ -80,6 +82,7 @@ object ImageSaver {
|
||||
contentType: String,
|
||||
contentSource: BufferedSource,
|
||||
contentResolver: ContentResolver,
|
||||
context: Context
|
||||
) {
|
||||
val contentValues = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
@@ -93,13 +96,13 @@ object ImageSaver {
|
||||
val uri =
|
||||
contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
checkNotNull(uri) {
|
||||
"Can't insert the new content"
|
||||
context.getString(R.string.can_t_insert_the_new_content)
|
||||
}
|
||||
|
||||
try {
|
||||
val outputStream = contentResolver.openOutputStream(uri)
|
||||
checkNotNull(outputStream) {
|
||||
"Can't open the content output stream"
|
||||
context.getString(R.string.can_t_open_the_content_output_stream)
|
||||
}
|
||||
|
||||
outputStream.use {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.vitorpamplona.amethyst.ui.actions
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import java.io.IOException
|
||||
@@ -8,6 +9,7 @@ import java.util.UUID
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.MediaType
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.OkHttpClient
|
||||
@@ -23,6 +25,7 @@ object ImageUploader {
|
||||
contentResolver: ContentResolver,
|
||||
onSuccess: (String) -> Unit,
|
||||
onError: (Throwable) -> Unit,
|
||||
context : Context
|
||||
) {
|
||||
val contentType = contentResolver.getType(uri)
|
||||
|
||||
@@ -40,7 +43,7 @@ object ImageUploader {
|
||||
override fun writeTo(sink: BufferedSink) {
|
||||
val imageInputStream = contentResolver.openInputStream(uri)
|
||||
checkNotNull(imageInputStream) {
|
||||
"Can't open the image input stream"
|
||||
context.getString(R.string.can_t_open_the_image_input_stream)
|
||||
}
|
||||
|
||||
imageInputStream.source().use(sink::writeAll)
|
||||
@@ -63,7 +66,7 @@ object ImageUploader {
|
||||
val tree = jacksonObjectMapper().readTree(body.string())
|
||||
val url = tree?.get("data")?.get("link")?.asText()
|
||||
checkNotNull(url) {
|
||||
"There must be an uploaded image URL in the response"
|
||||
context.getString(R.string.there_must_be_an_uploaded_image_url_in_the_response)
|
||||
}
|
||||
|
||||
onSuccess(url)
|
||||
|
||||
@@ -17,12 +17,15 @@ import androidx.compose.runtime.Composable
|
||||
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.TextStyle
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
|
||||
@@ -67,13 +70,13 @@ fun NewChannelView(onClose: () -> Unit, account: Account, channel: Channel? = nu
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Channel Name") },
|
||||
label = { Text(text = stringResource(R.string.channel_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.channelName.value,
|
||||
onValueChange = { postViewModel.channelName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "My Awesome Group",
|
||||
text = stringResource(R.string.my_awesome_group),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -86,7 +89,7 @@ fun NewChannelView(onClose: () -> Unit, account: Account, channel: Channel? = nu
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Picture Url") },
|
||||
label = { Text(text = stringResource(R.string.picture_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.channelPicture.value,
|
||||
onValueChange = { postViewModel.channelPicture.value = it },
|
||||
@@ -101,13 +104,13 @@ fun NewChannelView(onClose: () -> Unit, account: Account, channel: Channel? = nu
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Description") },
|
||||
label = { Text(text = stringResource(R.string.description)) },
|
||||
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||
value = postViewModel.channelDescription.value,
|
||||
onValueChange = { postViewModel.channelDescription.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "About us.. ",
|
||||
text = stringResource(R.string.about_us),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -136,7 +137,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "What's on your mind?",
|
||||
text = stringResource(R.string.what_s_on_your_mind),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -221,7 +222,7 @@ fun CloseButton(onCancel: () -> Unit) {
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = "Cancel", color = Color.White)
|
||||
Text(text = stringResource(R.string.cancel), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,7 +241,7 @@ fun PostButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier =
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = "Post", color = Color.White)
|
||||
Text(text = stringResource(R.string.post), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +260,7 @@ fun SaveButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier =
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = "Save", color = Color.White)
|
||||
Text(text = stringResource(R.string.save), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,7 +279,7 @@ fun CreateButton(onPost: () -> Unit = {}, isActive: Boolean, modifier: Modifier
|
||||
backgroundColor = if (isActive) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = "Create", color = Color.White)
|
||||
Text(text = stringResource(R.string.create), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.model.parseDirtyWordForKey
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.components.isValidURL
|
||||
import com.vitorpamplona.amethyst.ui.components.noProtocolUrlValidator
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -133,9 +134,10 @@ class NewPostViewModel: ViewModel() {
|
||||
onError = {
|
||||
isUploadingImage = false
|
||||
viewModelScope.launch {
|
||||
imageUploadingError.emit("Failed to upload the image")
|
||||
imageUploadingError.emit(context.getString(R.string.failed_to_upload_the_image))
|
||||
}
|
||||
}
|
||||
},
|
||||
context = context
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -141,7 +142,7 @@ fun ServerConfigHeader() {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = "Relay Address",
|
||||
text = stringResource(R.string.relay_address),
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -154,7 +155,7 @@ fun ServerConfigHeader() {
|
||||
Spacer(modifier = Modifier.size(25.dp))
|
||||
|
||||
Text(
|
||||
text = "Posts",
|
||||
text = stringResource(R.string.posts),
|
||||
maxLines = 1,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -164,7 +165,7 @@ fun ServerConfigHeader() {
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
text = "Posts",
|
||||
text = stringResource(id = R.string.posts),
|
||||
maxLines = 1,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -174,7 +175,7 @@ fun ServerConfigHeader() {
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text(
|
||||
text = "Errors",
|
||||
text = stringResource(R.string.errors),
|
||||
maxLines = 1,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -227,7 +228,9 @@ fun ServerConfig(
|
||||
Icon(
|
||||
imageVector = Icons.Default.Cancel,
|
||||
null,
|
||||
modifier = Modifier.padding(end = 5.dp).size(15.dp),
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = Color.Red
|
||||
)
|
||||
}
|
||||
@@ -252,8 +255,10 @@ fun ServerConfig(
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_home),
|
||||
"Home Feed",
|
||||
modifier = Modifier.padding(end = 5.dp).size(15.dp),
|
||||
stringResource(R.string.home_feed),
|
||||
modifier = Modifier
|
||||
.padding(end = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.feedTypes.contains(FeedType.FOLLOWS)) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
@@ -265,8 +270,10 @@ fun ServerConfig(
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.ic_dm),
|
||||
"Private Message Feed",
|
||||
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||
stringResource(R.string.private_message_feed),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.feedTypes.contains(FeedType.PRIVATE_DMS)) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
@@ -278,8 +285,10 @@ fun ServerConfig(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Groups,
|
||||
"Public Chat Feed",
|
||||
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||
stringResource(R.string.public_chat_feed),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.feedTypes.contains(FeedType.PUBLIC_CHATS)) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
@@ -291,8 +300,10 @@ fun ServerConfig(
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Public,
|
||||
"Global Feed",
|
||||
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||
stringResource(R.string.global_feed),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.feedTypes.contains(FeedType.GLOBAL)) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
@@ -310,7 +321,9 @@ fun ServerConfig(
|
||||
Icon(
|
||||
imageVector = Icons.Default.Download,
|
||||
null,
|
||||
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.read) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
@@ -332,7 +345,9 @@ fun ServerConfig(
|
||||
Icon(
|
||||
imageVector = Icons.Default.Upload,
|
||||
null,
|
||||
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.write) Color.Green else MaterialTheme.colors.onSurface.copy(
|
||||
alpha = 0.32f
|
||||
)
|
||||
@@ -350,7 +365,9 @@ fun ServerConfig(
|
||||
Icon(
|
||||
imageVector = Icons.Default.SyncProblem,
|
||||
null,
|
||||
modifier = Modifier.padding(horizontal = 5.dp).size(15.dp),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp)
|
||||
.size(15.dp),
|
||||
tint = if (item.errorCount > 0) Color.Yellow else Color.Green
|
||||
)
|
||||
|
||||
@@ -396,7 +413,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Add a Relay") },
|
||||
label = { Text(text = stringResource(R.string.add_a_relay)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
@@ -449,7 +466,7 @@ fun EditableServerConfig(relayToAdd: String, onNewRelay: (RelaySetupInfo) -> Uni
|
||||
backgroundColor = if (url.isNotBlank()) MaterialTheme.colors.primary else MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
) {
|
||||
Text(text = "Add", color = Color.White)
|
||||
Text(text = stringResource(id = R.string.add), color = Color.White)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,11 +17,13 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@@ -69,13 +71,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(1f), verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Display Name") },
|
||||
label = { Text(text = stringResource(R.string.display_name)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.displayName.value,
|
||||
onValueChange = { postViewModel.displayName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "My display name",
|
||||
text = stringResource(R.string.my_display_name),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -88,13 +90,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Text("@", Modifier.padding(5.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Username") },
|
||||
label = { Text(text = stringResource(R.string.username)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
value = postViewModel.userName.value,
|
||||
onValueChange = { postViewModel.userName.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "My username",
|
||||
text = stringResource(R.string.my_username),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -105,7 +107,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "About me") },
|
||||
label = { Text(text = stringResource(R.string.about_me)) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(100.dp),
|
||||
@@ -113,7 +115,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
onValueChange = { postViewModel.about.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "About me",
|
||||
text = stringResource(id = R.string.about_me),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -126,7 +128,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Avatar URL") },
|
||||
label = { Text(text = stringResource(R.string.avatar_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.picture.value,
|
||||
onValueChange = { postViewModel.picture.value = it },
|
||||
@@ -142,7 +144,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Banner URL") },
|
||||
label = { Text(text = stringResource(R.string.banner_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.banner.value,
|
||||
onValueChange = { postViewModel.banner.value = it },
|
||||
@@ -158,7 +160,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Website URL") },
|
||||
label = { Text(text = stringResource(R.string.website_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.website.value,
|
||||
onValueChange = { postViewModel.website.value = it },
|
||||
@@ -174,7 +176,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "NIP-05") },
|
||||
label = { Text(text = stringResource(R.string.nip_05)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.nip05.value,
|
||||
onValueChange = { postViewModel.nip05.value = it },
|
||||
@@ -189,7 +191,7 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "LN Address") },
|
||||
label = { Text(text = stringResource(R.string.ln_address)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.lnAddress.value,
|
||||
onValueChange = { postViewModel.lnAddress.value = it },
|
||||
@@ -205,13 +207,13 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "LN URL (outdated)") },
|
||||
label = { Text(text = stringResource(R.string.ln_url_outdated)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = postViewModel.lnURL.value,
|
||||
onValueChange = { postViewModel.lnURL.value = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "LNURL...",
|
||||
text = stringResource(R.string.lnurl),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
|
||||
@@ -11,10 +11,12 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
@@ -29,6 +31,7 @@ fun SaveToGallery(url: String) {
|
||||
val localContext = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
|
||||
fun saveImage() {
|
||||
ImageSaver.saveImage(
|
||||
context = localContext,
|
||||
@@ -37,7 +40,7 @@ fun SaveToGallery(url: String) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
localContext,
|
||||
"Image saved to the gallery",
|
||||
localContext.getString(R.string.image_saved_to_the_gallery),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
@@ -47,7 +50,7 @@ fun SaveToGallery(url: String) {
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
localContext,
|
||||
"Failed to save the image",
|
||||
localContext.getString(R.string.failed_to_save_the_image),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
@@ -78,6 +81,6 @@ fun SaveToGallery(url: String) {
|
||||
backgroundColor = Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = "Save", color = Color.White)
|
||||
Text(text = stringResource(id = R.string.save), color = Color.White)
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,12 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
@@ -54,9 +56,9 @@ fun UploadFromGallery(
|
||||
}
|
||||
) {
|
||||
if (!isUploading) {
|
||||
Text("Upload Image")
|
||||
Text(stringResource(R.string.upload_image))
|
||||
} else {
|
||||
Text("Uploading…")
|
||||
Text(stringResource(R.string.uploading))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -68,9 +70,9 @@ fun UploadFromGallery(
|
||||
enabled = !isUploading,
|
||||
) {
|
||||
if (!isUploading) {
|
||||
Text("Upload Image")
|
||||
Text(stringResource(R.string.upload_image))
|
||||
} else {
|
||||
Text("Uploading…")
|
||||
Text(stringResource(R.string.uploading))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
|
||||
|
||||
@@ -38,7 +40,7 @@ fun NewChannelButton(account: Account) {
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Add,
|
||||
contentDescription = "New Channel",
|
||||
contentDescription = stringResource(R.string.new_channel),
|
||||
modifier = Modifier.size(26.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
|
||||
@@ -16,7 +16,17 @@ import coil.compose.AsyncImagePainter
|
||||
import coil.compose.LocalImageLoader
|
||||
import java.util.Base64
|
||||
|
||||
data class ResizeImage(val url: String?, val size: Dp)
|
||||
data class ResizeImage(val url: String?, val size: Dp) {
|
||||
fun proxyUrl(): String? {
|
||||
if (url == null) return null
|
||||
|
||||
// Fixes Image size to reduce pings to servers for each size used in the app
|
||||
val imgPx = 200 // with(LocalDensity.current) { model.size.toPx().toInt() }
|
||||
val base64 = Base64.getUrlEncoder().encodeToString(url.toByteArray())
|
||||
|
||||
return "https://d12fidohs5rlxk.cloudfront.net/preset:sharp/rs:fit:$imgPx:$imgPx:0/gravity:sm/$base64"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
@@ -40,7 +50,6 @@ fun AsyncImageProxy(
|
||||
AsyncImage(
|
||||
model = model.url,
|
||||
contentDescription = contentDescription,
|
||||
imageLoader = LocalImageLoader.current,
|
||||
modifier = modifier,
|
||||
placeholder = placeholder,
|
||||
error = error,
|
||||
@@ -55,14 +64,9 @@ fun AsyncImageProxy(
|
||||
filterQuality = filterQuality
|
||||
)
|
||||
} else {
|
||||
val imgPx = with(LocalDensity.current) { model.size.toPx().toInt() }
|
||||
val base64 = Base64.getUrlEncoder().encodeToString(model.url.toByteArray())
|
||||
val extension = model.url.split(".").lastOrNull()
|
||||
|
||||
AsyncImage(
|
||||
model = "https://d12fidohs5rlxk.cloudfront.net/preset:sharp/rs:fit:$imgPx:$imgPx:0/gravity:sm/$base64",
|
||||
model = model.proxyUrl(),
|
||||
contentDescription = contentDescription,
|
||||
imageLoader = LocalImageLoader.current,
|
||||
modifier = modifier,
|
||||
placeholder = placeholder,
|
||||
error = error,
|
||||
|
||||
@@ -25,6 +25,8 @@ import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
||||
@Composable
|
||||
fun ExpandableRichTextViewer(
|
||||
@@ -67,7 +69,7 @@ fun ExpandableRichTextViewer(
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Show More", color = Color.White)
|
||||
Text(text = stringResource(R.string.show_more), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -68,7 +69,7 @@ fun InvoicePreview(lnInvoice: String) {
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Lightning Invoice",
|
||||
text = stringResource(R.string.lightning_invoice),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
@@ -79,7 +80,9 @@ fun InvoicePreview(lnInvoice: String) {
|
||||
|
||||
amount?.let {
|
||||
Text(
|
||||
text = "${NumberFormat.getInstance().format(amount)} sats",
|
||||
text = "${
|
||||
NumberFormat.getInstance().format(amount)
|
||||
} ${stringResource(id = R.string.sats)}",
|
||||
fontSize = 25.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier
|
||||
@@ -90,7 +93,9 @@ fun InvoicePreview(lnInvoice: String) {
|
||||
|
||||
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 10.dp),
|
||||
onClick = {
|
||||
runCatching {
|
||||
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("lightning:$lnInvoice"))
|
||||
@@ -102,7 +107,7 @@ fun InvoicePreview(lnInvoice: String) {
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(text = "Pay", color = Color.White, fontSize = 20.sp)
|
||||
Text(text = stringResource(R.string.pay), color = Color.White, fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@@ -73,7 +74,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "Lightning Tips",
|
||||
text = stringResource(R.string.lightning_tips),
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
modifier = Modifier.padding(start = 10.dp)
|
||||
@@ -86,13 +87,13 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
|
||||
var amount by remember { mutableStateOf(1000L) }
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Note to Receiver") },
|
||||
label = { Text(text = stringResource(R.string.note_to_receiver)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = message,
|
||||
onValueChange = { message = it },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "Thank you so much!",
|
||||
text = stringResource(R.string.thank_you_so_much),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -103,7 +104,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "Amount in Sats") },
|
||||
label = { Text(text = stringResource(R.string.amount_in_sats)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = amount.toString(),
|
||||
onValueChange = {
|
||||
@@ -152,7 +153,7 @@ fun InvoiceRequest(lud16: String, toUserPubKeyHex: String, account: Account, onC
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(text = "Send Sats", color = Color.White, fontSize = 20.sp)
|
||||
Text(text = stringResource(R.string.send_sats), color = Color.White, fontSize = 20.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,24 +2,44 @@ package com.vitorpamplona.amethyst.ui.components
|
||||
|
||||
import android.util.Patterns
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.text.Paragraph
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.halilibo.richtext.markdown.Markdown
|
||||
import com.halilibo.richtext.markdown.MarkdownParseOptions
|
||||
import com.halilibo.richtext.ui.RichText
|
||||
import com.halilibo.richtext.ui.RichTextStyle
|
||||
import com.halilibo.richtext.ui.currentRichTextStyle
|
||||
import com.halilibo.richtext.ui.material.MaterialRichText
|
||||
import com.halilibo.richtext.ui.resolveDefaults
|
||||
import com.halilibo.richtext.ui.string.RichTextString
|
||||
import com.halilibo.richtext.ui.string.RichTextStringStyle
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LnInvoiceUtil
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.service.Nip19
|
||||
@@ -60,61 +80,82 @@ fun RichTextViewer(
|
||||
accountViewModel: AccountViewModel,
|
||||
navController: NavController,
|
||||
) {
|
||||
|
||||
Column(modifier = modifier.animateContentSize()) {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
content.split('\n').forEach { paragraph ->
|
||||
|
||||
FlowRow() {
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
if (content.startsWith("# ") || content.contains("##") || content.contains("```")) {
|
||||
var richTextStyle by remember { mutableStateOf(RichTextStyle().resolveDefaults()) }
|
||||
|
||||
if (canPreview) {
|
||||
// Explicit URL
|
||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||
if (lnInvoice != null) {
|
||||
InvoicePreview(lnInvoice)
|
||||
} else if (isValidURL(word)) {
|
||||
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
||||
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||
ZoomableImageView(word)
|
||||
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
||||
VideoView(word)
|
||||
MaterialRichText(
|
||||
style = RichTextStyle().resolveDefaults().copy(
|
||||
stringStyle = richTextStyle.stringStyle?.copy(
|
||||
linkStyle = SpanStyle(
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
)
|
||||
),
|
||||
) {
|
||||
Markdown(
|
||||
content = content,
|
||||
markdownParseOptions = MarkdownParseOptions.Default,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||
content.split('\n').forEach { paragraph ->
|
||||
FlowRow() {
|
||||
paragraph.split(' ').forEach { word: String ->
|
||||
|
||||
if (canPreview) {
|
||||
// Explicit URL
|
||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||
if (lnInvoice != null) {
|
||||
InvoicePreview(lnInvoice)
|
||||
} else if (isValidURL(word)) {
|
||||
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
||||
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||
ZoomableImageView(word)
|
||||
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
||||
VideoView(word)
|
||||
} else {
|
||||
UrlPreview(word, word)
|
||||
}
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
ClickableEmail(word)
|
||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||
ClickablePhone(word)
|
||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||
UrlPreview("https://$word", word)
|
||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||
} else if (isBechLink(word)) {
|
||||
BechLink(word, navController)
|
||||
} else {
|
||||
UrlPreview(word, word)
|
||||
Text(
|
||||
text = "$word ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
}
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
ClickableEmail(word)
|
||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||
ClickablePhone(word)
|
||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||
UrlPreview("https://$word", word)
|
||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||
} else if (isBechLink(word)) {
|
||||
BechLink(word, navController)
|
||||
} else {
|
||||
Text(
|
||||
text = "$word ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (isValidURL(word)) {
|
||||
ClickableUrl("$word ", word)
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
ClickableEmail(word)
|
||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||
ClickablePhone(word)
|
||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||
ClickableUrl(word, "https://$word")
|
||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||
} else if (isBechLink(word)) {
|
||||
BechLink(word, navController)
|
||||
} else {
|
||||
Text(
|
||||
text = "$word ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
if (isValidURL(word)) {
|
||||
ClickableUrl("$word ", word)
|
||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||
ClickableEmail(word)
|
||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||
ClickablePhone(word)
|
||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||
ClickableUrl(word, "https://$word")
|
||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
|
||||
} else if (isBechLink(word)) {
|
||||
BechLink(word, navController)
|
||||
} else {
|
||||
Text(
|
||||
text = "$word ",
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -34,6 +35,7 @@ import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.os.ConfigurationCompat
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||
import com.vitorpamplona.amethyst.service.lang.ResultOrError
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -110,17 +112,17 @@ fun TranslateableRichTextViewer(
|
||||
val annotatedTranslationString = buildAnnotatedString {
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("langSettings", true.toString())
|
||||
append("Auto")
|
||||
append(stringResource(R.string.auto))
|
||||
}
|
||||
|
||||
append("-translated from ")
|
||||
append("-${stringResource(R.string.translated_from)} ")
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", true.toString())
|
||||
append(Locale(source).displayName)
|
||||
}
|
||||
|
||||
append(" to ")
|
||||
append(" ${stringResource(R.string.to)} ")
|
||||
|
||||
withStyle(clickableTextStyle) {
|
||||
pushStringAnnotation("showOriginal", false.toString())
|
||||
@@ -163,7 +165,7 @@ fun TranslateableRichTextViewer(
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text("Never translate from ${Locale(source).displayName}")
|
||||
Text(stringResource(R.string.never_translate_from) + "${Locale(source).displayName}")
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = {
|
||||
@@ -181,7 +183,14 @@ fun TranslateableRichTextViewer(
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text("Show in ${Locale(source).displayName} first")
|
||||
// TODO : Rashed translate this
|
||||
Text(
|
||||
"${stringResource(R.string.show_in)} ${Locale(source).displayName} ${
|
||||
stringResource(
|
||||
R.string.first
|
||||
)
|
||||
}"
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.prefer(source, target, target)
|
||||
@@ -198,7 +207,7 @@ fun TranslateableRichTextViewer(
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text("Show in ${Locale(target).displayName} first")
|
||||
Text("${stringResource(id = R.string.show_in)} ${Locale(target).displayName} ${R.string.first}")
|
||||
}
|
||||
Divider()
|
||||
|
||||
@@ -221,7 +230,7 @@ fun TranslateableRichTextViewer(
|
||||
|
||||
Spacer(modifier = Modifier.size(10.dp))
|
||||
|
||||
Text("Always translate to ${lang.displayName}")
|
||||
Text("${stringResource(R.string.always_translate_to)}${lang.displayName}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,32 +8,45 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import com.baha.url.preview.IUrlPreviewCallback
|
||||
import com.baha.url.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.UrlCachedPreviewer
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
|
||||
@Composable
|
||||
fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
|
||||
var urlPreviewState by remember { mutableStateOf<UrlPreviewState>(UrlPreviewState.Loading) }
|
||||
fun UrlPreview(url: String, urlText: String) {
|
||||
val default = UrlCachedPreviewer.cache[url]?.let { UrlPreviewState.Loaded(it) } ?: UrlPreviewState.Loading
|
||||
var context = LocalContext.current
|
||||
|
||||
var urlPreviewState by remember { mutableStateOf(default) }
|
||||
|
||||
// Doesn't use a viewModel because of viewModel reusing issues (too many UrlPreview are created).
|
||||
LaunchedEffect(url) {
|
||||
withContext(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url, object : IUrlPreviewCallback {
|
||||
override fun onComplete(urlInfo: UrlInfoItem) {
|
||||
if (urlInfo.allFetchComplete() && urlInfo.url == url)
|
||||
urlPreviewState = UrlPreviewState.Loaded(urlInfo)
|
||||
else
|
||||
urlPreviewState = UrlPreviewState.Empty
|
||||
}
|
||||
if (urlPreviewState == UrlPreviewState.Loading) {
|
||||
withContext(Dispatchers.IO) {
|
||||
UrlCachedPreviewer.previewInfo(url, object : IUrlPreviewCallback {
|
||||
override fun onComplete(urlInfo: UrlInfoItem) {
|
||||
if (urlInfo.allFetchComplete() && urlInfo.url == url)
|
||||
urlPreviewState = UrlPreviewState.Loaded(urlInfo)
|
||||
else
|
||||
urlPreviewState = UrlPreviewState.Empty
|
||||
}
|
||||
|
||||
override fun onFailed(throwable: Throwable) {
|
||||
urlPreviewState = UrlPreviewState.Error("Error parsing preview for ${url}: ${throwable.message}")
|
||||
}
|
||||
})
|
||||
override fun onFailed(throwable: Throwable) {
|
||||
urlPreviewState = UrlPreviewState.Error(
|
||||
context.getString(
|
||||
R.string.error_parsing_preview_for,
|
||||
url,
|
||||
throwable.message
|
||||
)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,9 +56,7 @@ fun UrlPreview(url: String, urlText: String, showUrlIfError: Boolean = true) {
|
||||
UrlPreviewCard(url, state.previewInfo)
|
||||
}
|
||||
else -> {
|
||||
if (showUrlIfError) {
|
||||
ClickableUrl(urlText, url)
|
||||
}
|
||||
ClickableUrl(urlText, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.baha.url.preview.UrlInfoItem
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
@Composable
|
||||
fun UrlPreviewCard(
|
||||
@@ -36,7 +38,7 @@ fun UrlPreviewCard(
|
||||
Column {
|
||||
AsyncImage(
|
||||
model = previewInfo.image,
|
||||
contentDescription = "Preview Card Image for ${url}",
|
||||
contentDescription = stringResource(R.string.preview_card_image_for, url),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
@@ -17,7 +17,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
@Composable
|
||||
fun ZoomableAsyncImage(imageUrl: String) {
|
||||
@@ -46,14 +48,16 @@ fun ZoomableAsyncImage(imageUrl: String) {
|
||||
) {
|
||||
AsyncImage(
|
||||
model = imageUrl,
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxSize().graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = offsetX,
|
||||
translationY = offsetY
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer(
|
||||
scaleX = scale,
|
||||
scaleY = scale,
|
||||
translationX = offsetX,
|
||||
translationY = offsetY
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@ object GlobalFeedFilter: FeedFilter<Note>() {
|
||||
lateinit var account: Account
|
||||
|
||||
override fun feed() = LocalCache.notes.values
|
||||
.filter { account.isAcceptable(it) }
|
||||
.filter {
|
||||
(it.event is TextNoteEvent && (it.event as TextNoteEvent).replyTos.isEmpty()) ||
|
||||
(it.event is ChannelMessageEvent && (it.event as ChannelMessageEvent).replyTos.isEmpty())
|
||||
}
|
||||
.filter { account.isAcceptable(it) }
|
||||
.sortedBy { it.event?.createdAt }
|
||||
.reversed()
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ object HomeConversationsFeedFilter: FeedFilter<Note>() {
|
||||
(it.event is TextNoteEvent || it.event is RepostEvent)
|
||||
&& it.author in user.follows
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
&& it.author?.let { !HomeNewThreadFeedFilter.account.isHidden(it) } ?: true
|
||||
&& !it.isNewThread()
|
||||
}
|
||||
.sortedBy { it.event?.createdAt }
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.vitorpamplona.amethyst.ui.dal
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.LocalCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
import nostr.postr.events.TextNoteEvent
|
||||
|
||||
@@ -14,9 +15,10 @@ object HomeNewThreadFeedFilter: FeedFilter<Note>() {
|
||||
|
||||
return LocalCache.notes.values
|
||||
.filter {
|
||||
(it.event is TextNoteEvent || it.event is RepostEvent)
|
||||
(it.event is TextNoteEvent || it.event is RepostEvent || it.event is LongTextNoteEvent)
|
||||
&& it.author in user.follows
|
||||
// && account.isAcceptable(it) // This filter follows only. No need to check if acceptable
|
||||
&& it.author?.let { !account.isHidden(it) } ?: true
|
||||
&& it.isNewThread()
|
||||
}
|
||||
.sortedBy { it.event?.createdAt }
|
||||
|
||||
@@ -25,16 +25,16 @@ object NotificationFeedFilter: FeedFilter<Note>() {
|
||||
}
|
||||
.filter {
|
||||
it.event !is TextNoteEvent
|
||||
||
|
||||
(
|
||||
it.event is TextNoteEvent
|
||||
&&
|
||||
(
|
||||
it.replyTo?.lastOrNull()?.author == account.userProfile()
|
||||
||
|
||||
account.userProfile() in it.directlyCiteUsers()
|
||||
)
|
||||
)
|
||||
||
|
||||
(
|
||||
it.event is TextNoteEvent
|
||||
&&
|
||||
(
|
||||
it.replyTo?.any { it.author == account.userProfile() } == true
|
||||
||
|
||||
account.userProfile() in it.directlyCiteUsers()
|
||||
)
|
||||
)
|
||||
}
|
||||
.filter {
|
||||
it.event !is ReactionEvent
|
||||
|
||||
@@ -36,6 +36,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavHostController
|
||||
@@ -201,7 +202,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(34.dp)
|
||||
.height(34.dp)
|
||||
|
||||
@@ -51,6 +51,8 @@ import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
@Composable
|
||||
fun DrawerContent(navController: NavHostController,
|
||||
@@ -111,7 +113,7 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
|
||||
if (banner != null && banner.isNotBlank()) {
|
||||
AsyncImageProxy(
|
||||
model = ResizeImage(banner, 150.dp),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -120,7 +122,7 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = "Profile Banner",
|
||||
contentDescription = stringResource(R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -131,7 +133,7 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
|
||||
Column(modifier = modifier) {
|
||||
AsyncImageProxy(
|
||||
model = ResizeImage(accountUser.profilePicture(), 100.dp),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, accountUser.pubkeyHex)),
|
||||
@@ -187,11 +189,11 @@ fun ProfileContent(baseAccountUser: User, modifier: Modifier = Modifier, scaffol
|
||||
})) {
|
||||
Row() {
|
||||
Text("${accountUserFollows.follows.size}", fontWeight = FontWeight.Bold)
|
||||
Text(" Following")
|
||||
Text(stringResource(R.string.following))
|
||||
}
|
||||
Row(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Text("${accountUserFollows.followers.size}", fontWeight = FontWeight.Bold)
|
||||
Text(" Followers")
|
||||
Text(stringResource(R.string.followers))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -216,7 +218,7 @@ fun ListContent(
|
||||
scaffoldState,
|
||||
"User/${accountUser.pubkeyHex}",
|
||||
Route.Profile.icon,
|
||||
"Profile"
|
||||
stringResource(R.string.profile)
|
||||
)
|
||||
|
||||
Divider(
|
||||
@@ -231,14 +233,14 @@ fun ListContent(
|
||||
}
|
||||
})) {
|
||||
Text(
|
||||
text = "Security Filters",
|
||||
text = stringResource(R.string.security_filters),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = W500
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.clickable(onClick = { accountViewModel.logOff() })) {
|
||||
Text(
|
||||
text = "Log out",
|
||||
text = stringResource(R.string.log_out),
|
||||
modifier = Modifier.padding(vertical = 15.dp),
|
||||
fontSize = 18.sp,
|
||||
fontWeight = W500
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -14,9 +15,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
|
||||
@@ -29,13 +32,14 @@ fun BlankNote(modifier: Modifier = Modifier, isQuote: Boolean = false) {
|
||||
modifier = Modifier.padding(
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
bottom = 25.dp,
|
||||
bottom = 8.dp,
|
||||
top = 15.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "Referenced post not found",
|
||||
text = stringResource(R.string.post_not_found),
|
||||
modifier = Modifier.padding(30.dp),
|
||||
color = Color.Gray,
|
||||
)
|
||||
@@ -59,15 +63,13 @@ fun HiddenNote(reports: Set<Note>, loggedIn: User, modifier: Modifier = Modifier
|
||||
Row(
|
||||
modifier = Modifier.padding(
|
||||
start = 20.dp,
|
||||
end = 20.dp,
|
||||
bottom = 25.dp,
|
||||
top = 15.dp
|
||||
end = 20.dp
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(30.dp)) {
|
||||
Text(
|
||||
text = "Post was flagged as inappropriate by",
|
||||
text = stringResource(R.string.post_was_flagged_as_inappropriate_by),
|
||||
color = Color.Gray,
|
||||
)
|
||||
FlowRow(modifier = Modifier.padding(top = 10.dp)) {
|
||||
@@ -91,13 +93,12 @@ fun HiddenNote(reports: Set<Note>, loggedIn: User, modifier: Modifier = Modifier
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Show Anyway", color = Color.White)
|
||||
Text(text = stringResource(R.string.show_anyway), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(vertical = 10.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
@@ -38,6 +39,7 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
@@ -71,9 +73,9 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
||||
val noteEvent = note.event
|
||||
|
||||
val description = if (noteEvent is ChannelCreateEvent) {
|
||||
"Channel created"
|
||||
stringResource(R.string.channel_created)
|
||||
} else if (noteEvent is ChannelMetadataEvent) {
|
||||
"Channel Information changed to "
|
||||
"${stringResource(R.string.channel_information_changed_to)} "
|
||||
} else {
|
||||
noteEvent?.content
|
||||
}
|
||||
@@ -97,7 +99,7 @@ fun ChatroomCompose(baseNote: Note, accountViewModel: AccountViewModel, navContr
|
||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
|
||||
)
|
||||
Text(
|
||||
" Public Chat",
|
||||
" ${stringResource(R.string.public_chat)}",
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -158,7 +160,7 @@ fun ChannelName(
|
||||
placeholder = channelPicturePlaceholder,
|
||||
fallback = channelPicturePlaceholder,
|
||||
error = channelPicturePlaceholder,
|
||||
contentDescription = "Channel Image",
|
||||
contentDescription = stringResource(R.string.channel_image),
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.height(55.dp)
|
||||
@@ -182,6 +184,8 @@ fun ChannelName(
|
||||
hasNewMessages: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
|
||||
val context = LocalContext.current
|
||||
Column(modifier = Modifier.clickable(onClick = onClick) ) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = 12.dp, end = 12.dp, top = 10.dp)
|
||||
@@ -198,7 +202,7 @@ fun ChannelName(
|
||||
|
||||
channelLastTime?.let {
|
||||
Text(
|
||||
timeAgo(channelLastTime),
|
||||
timeAgo(channelLastTime, context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f)
|
||||
)
|
||||
}
|
||||
@@ -217,7 +221,7 @@ fun ChannelName(
|
||||
)
|
||||
else
|
||||
Text(
|
||||
"Referenced event not found",
|
||||
stringResource(R.string.referenced_event_not_found),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.52f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -45,6 +45,7 @@ import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -53,6 +54,7 @@ import androidx.navigation.NavController
|
||||
import coil.compose.AsyncImage
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||
@@ -193,7 +195,7 @@ fun ChatroomMessageCompose(
|
||||
placeholder = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(context, author.pubkeyHex)),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(25.dp)
|
||||
.height(25.dp)
|
||||
@@ -238,15 +240,19 @@ fun ChatroomMessageCompose(
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val event = note.event
|
||||
if (event is ChannelCreateEvent) {
|
||||
Text(text = "${note.author?.toBestDisplayName()} created " +
|
||||
"${event.channelInfo.name ?: ""} with " +
|
||||
"description of '${event.channelInfo.about ?: ""}', " +
|
||||
"and picture '${event.channelInfo.picture ?: ""}'")
|
||||
Text(text = note.author?.toBestDisplayName()
|
||||
.toString() + " ${stringResource(R.string.created)} " + (event.channelInfo.name
|
||||
?: "") +" ${stringResource(R.string.with_description_of)} '" + (event.channelInfo.about
|
||||
?: "") + "', ${stringResource(R.string.and_picture)} '" + (event.channelInfo.picture
|
||||
?: "") + "'"
|
||||
)
|
||||
} else if (event is ChannelMetadataEvent) {
|
||||
Text(text = "${note.author?.toBestDisplayName()} changed " +
|
||||
"chat name to '${event.channelInfo.name ?: ""}', " +
|
||||
"description to '${event.channelInfo.about ?: ""}', " +
|
||||
"and picture to '${event.channelInfo.picture ?: ""}'")
|
||||
Text(text = note.author?.toBestDisplayName()
|
||||
.toString() + " ${stringResource(R.string.changed_chat_name_to)} '" + (event.channelInfo.name
|
||||
?: "") + "$', {stringResource(R.string.description_to)} '" + (event.channelInfo.about
|
||||
?: "") + "', ${stringResource(R.string.and_picture_to)} '" + (event.channelInfo.picture
|
||||
?: "") + "'"
|
||||
)
|
||||
} else {
|
||||
val eventContent = accountViewModel.decrypt(note)
|
||||
|
||||
@@ -266,7 +272,7 @@ fun ChatroomMessageCompose(
|
||||
)
|
||||
} else {
|
||||
TranslateableRichTextViewer(
|
||||
"Could Not decrypt the message",
|
||||
stringResource(R.string.could_not_decrypt_the_message),
|
||||
true,
|
||||
Modifier,
|
||||
note.event?.tags,
|
||||
@@ -289,7 +295,7 @@ fun ChatroomMessageCompose(
|
||||
) {
|
||||
Row() {
|
||||
Text(
|
||||
timeAgoShort(note.event?.createdAt),
|
||||
timeAgoShort(note.event?.createdAt, context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
fontSize = 12.sp
|
||||
)
|
||||
@@ -343,7 +349,7 @@ private fun RelayBadges(baseNote: Note) {
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
contentDescription = "Relay Icon",
|
||||
contentDescription = stringResource(id = R.string.relay_icon),
|
||||
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bolt
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.MultiSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun MultiSetCompose(multiSetCard: MultiSetCard, modifier: Modifier = Modifier, routeForLastRead: String, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
val noteState by multiSetCard.note.live().metadata.observeAsState()
|
||||
val note = noteState?.note
|
||||
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val context = LocalContext.current.applicationContext
|
||||
|
||||
val noteEvent = note?.event
|
||||
var popupExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
if (note == null) {
|
||||
BlankNote(Modifier, false)
|
||||
} else {
|
||||
var isNew by remember { mutableStateOf<Boolean>(false) }
|
||||
|
||||
LaunchedEffect(key1 = multiSetCard) {
|
||||
isNew = multiSetCard.createdAt > NotificationCache.load(routeForLastRead, context)
|
||||
|
||||
NotificationCache.markAsRead(routeForLastRead, multiSetCard.createdAt, context)
|
||||
}
|
||||
|
||||
var backgroundColor = if (isNew) {
|
||||
MaterialTheme.colors.primary.copy(0.12f).compositeOver(MaterialTheme.colors.background)
|
||||
} else {
|
||||
MaterialTheme.colors.background
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(backgroundColor)
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (noteEvent !is ChannelMessageEvent) {
|
||||
navController.navigate("Note/${note.idHex}") {
|
||||
launchSingleTop = true
|
||||
}
|
||||
} else {
|
||||
note.channel?.let {
|
||||
navController.navigate("Channel/${it.idHex}")
|
||||
}
|
||||
}
|
||||
},
|
||||
onLongClick = { popupExpanded = true }
|
||||
)
|
||||
) {
|
||||
Row(modifier = Modifier
|
||||
.padding(
|
||||
start = 12.dp,
|
||||
end = 12.dp,
|
||||
top = 10.dp)
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth()) {
|
||||
|
||||
if (multiSetCard.zapEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
// Draws the like picture outside the boosted card.
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(0.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
tint = BitcoinOrange,
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
multiSetCard.zapEvents.forEach {
|
||||
NoteAuthorPicture(
|
||||
note = it.key,
|
||||
navController = navController,
|
||||
userAccount = account.userProfile(),
|
||||
size = 35.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (multiSetCard.boostEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 4.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_retweeted),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
multiSetCard.boostEvents.forEach {
|
||||
NoteAuthorPicture(
|
||||
note = it,
|
||||
navController = navController,
|
||||
userAccount = account.userProfile(),
|
||||
size = 35.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (multiSetCard.likeEvents.isNotEmpty()) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(55.dp)
|
||||
.padding(end = 5.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_liked),
|
||||
null,
|
||||
modifier = Modifier
|
||||
.size(16.dp)
|
||||
.align(Alignment.TopEnd),
|
||||
tint = Color.Unspecified
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
FlowRow() {
|
||||
multiSetCard.likeEvents.forEach {
|
||||
NoteAuthorPicture(
|
||||
note = it,
|
||||
navController = navController,
|
||||
userAccount = account.userProfile(),
|
||||
size = 35.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Box(modifier = Modifier
|
||||
.width(65.dp)
|
||||
.padding(0.dp)) {
|
||||
}
|
||||
|
||||
NoteCompose(
|
||||
baseNote = note,
|
||||
routeForLastRead = null,
|
||||
modifier = Modifier.padding(top = 5.dp),
|
||||
isBoostedNote = true,
|
||||
parentBackgroundColor = backgroundColor,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController
|
||||
)
|
||||
|
||||
NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,8 @@ fun ObserveDisplayNip05Status(baseUser: User) {
|
||||
|
||||
user.nip05()?.let { nip05 ->
|
||||
if (nip05.split("@").size == 2) {
|
||||
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||
|
||||
val nip05Verified by nip05VerificationAsAState(user.info!!, user.pubkeyHex)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
if (nip05.split("@")[0] != "_")
|
||||
Text(
|
||||
|
||||
@@ -18,12 +18,15 @@ import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.ColorMatrix
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
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.dp
|
||||
import androidx.navigation.NavController
|
||||
@@ -35,6 +38,7 @@ import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||
@@ -42,9 +46,11 @@ import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
|
||||
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
|
||||
import com.vitorpamplona.amethyst.ui.components.ResizeImage
|
||||
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||
import com.vitorpamplona.amethyst.ui.components.UrlPreviewCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||
import kotlin.time.ExperimentalTime
|
||||
import nostr.postr.events.PrivateDmEvent
|
||||
import nostr.postr.events.TextNoteEvent
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@@ -185,7 +191,7 @@ fun NoteCompose(
|
||||
placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
contentDescription = "Group Picture",
|
||||
contentDescription = stringResource(R.string.group_picture),
|
||||
modifier = Modifier
|
||||
.width(30.dp)
|
||||
.height(30.dp)
|
||||
@@ -234,7 +240,7 @@ fun NoteCompose(
|
||||
}
|
||||
|
||||
Text(
|
||||
timeAgo(noteEvent.createdAt),
|
||||
timeAgo(noteEvent.createdAt, context = context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
maxLines = 1
|
||||
)
|
||||
@@ -257,7 +263,7 @@ fun NoteCompose(
|
||||
if (note.author != null && !makeItShort) {
|
||||
ObserveDisplayNip05Status(note.author!!)
|
||||
}
|
||||
|
||||
|
||||
Spacer(modifier = Modifier.height(3.dp))
|
||||
|
||||
if (noteEvent is TextNoteEvent && (note.replyTo != null || note.mentions != null)) {
|
||||
@@ -315,26 +321,81 @@ fun NoteCompose(
|
||||
)
|
||||
}
|
||||
} else if (noteEvent is ReportEvent) {
|
||||
val reportType = noteEvent.reportType.map {
|
||||
when (it) {
|
||||
ReportEvent.ReportType.EXPLICIT -> "Explicit Content"
|
||||
ReportEvent.ReportType.SPAM -> "Spam"
|
||||
ReportEvent.ReportType.IMPERSONATION -> "Impersonation"
|
||||
ReportEvent.ReportType.ILLEGAL -> "Illegal Behavior"
|
||||
else -> "Unknown"
|
||||
val reportType = (noteEvent.reportedPost + noteEvent.reportedAuthor).map {
|
||||
when (it.reportType) {
|
||||
ReportEvent.ReportType.EXPLICIT -> stringResource(R.string.explicit_content)
|
||||
ReportEvent.ReportType.NUDITY -> stringResource(R.string.nudity)
|
||||
ReportEvent.ReportType.PROFANITY -> stringResource(R.string.profanity_hateful_speech)
|
||||
ReportEvent.ReportType.SPAM -> stringResource(R.string.spam)
|
||||
ReportEvent.ReportType.IMPERSONATION -> stringResource(R.string.impersonation)
|
||||
ReportEvent.ReportType.ILLEGAL -> stringResource(R.string.illegal_behavior)
|
||||
else -> stringResource(R.string.unknown)
|
||||
}
|
||||
}.joinToString(", ")
|
||||
}.toSet().joinToString(", ")
|
||||
|
||||
Text(
|
||||
text = reportType
|
||||
)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 40.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
} else if (noteEvent is LongTextNoteEvent) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(shape = RoundedCornerShape(15.dp))
|
||||
.border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(15.dp))
|
||||
) {
|
||||
Column {
|
||||
noteEvent.image?.let {
|
||||
AsyncImage(
|
||||
model = noteEvent.image,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
noteEvent.image
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
noteEvent.title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.body2,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, end = 10.dp, top = 10.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
|
||||
noteEvent.summary?.let {
|
||||
Text(
|
||||
text = it,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReactionsRow(note, accountViewModel)
|
||||
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 10.dp),
|
||||
thickness = 0.25.dp
|
||||
)
|
||||
} else {
|
||||
val eventContent = noteEvent.content
|
||||
val eventContent = accountViewModel.decrypt(note)
|
||||
|
||||
val canPreview = note.author == account.userProfile()
|
||||
|| (note.author?.let { account.userProfile().isFollowing(it) } ?: true )
|
||||
|| !noteForReports.hasAnyReports()
|
||||
@@ -391,7 +452,7 @@ private fun RelayBadges(baseNote: Note) {
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, url)),
|
||||
contentDescription = "Relay Icon",
|
||||
contentDescription = stringResource(R.string.relay_icon),
|
||||
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(1f)
|
||||
@@ -460,7 +521,7 @@ fun NoteAuthorPicture(
|
||||
if (author == null) {
|
||||
Image(
|
||||
painter = BitmapPainter(RoboHashCache.get(ctx, "ohnothisauthorisnotfound")),
|
||||
contentDescription = "Unknown Author",
|
||||
contentDescription = stringResource(R.string.unknown_author),
|
||||
modifier = pictureModifier
|
||||
.fillMaxSize(1f)
|
||||
.clip(shape = CircleShape)
|
||||
@@ -485,13 +546,15 @@ fun UserPicture(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun UserPicture(
|
||||
baseUser: User,
|
||||
baseUserAccount: User,
|
||||
size: Dp,
|
||||
pictureModifier: Modifier = Modifier,
|
||||
onClick: ((User) -> Unit)? = null
|
||||
onClick: ((User) -> Unit)? = null,
|
||||
onLongClick: ((User) -> Unit)? = null
|
||||
) {
|
||||
val userState by baseUser.live().metadata.observeAsState()
|
||||
val user = userState?.user ?: return
|
||||
@@ -505,7 +568,7 @@ fun UserPicture(
|
||||
|
||||
AsyncImageProxy(
|
||||
model = ResizeImage(user.profilePicture(), size),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
|
||||
@@ -514,7 +577,9 @@ fun UserPicture(
|
||||
.clip(shape = CircleShape)
|
||||
.background(MaterialTheme.colors.background)
|
||||
.run {
|
||||
if (onClick != null)
|
||||
if (onClick != null && onLongClick != null)
|
||||
this.combinedClickable(onClick = { onClick(user) }, onLongClick = { onLongClick(user) } )
|
||||
else if (onClick != null)
|
||||
this.clickable(onClick = { onClick(user) } )
|
||||
else
|
||||
this
|
||||
@@ -544,7 +609,7 @@ fun UserPicture(
|
||||
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_verified),
|
||||
"Following",
|
||||
stringResource(id = R.string.following),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
tint = Following
|
||||
)
|
||||
@@ -564,17 +629,17 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
|
||||
Text("Copy Text")
|
||||
Text(stringResource(R.string.copy_text))
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.author?.pubkeyNpub() ?: "")); onDismiss() }) {
|
||||
Text("Copy User PubKey")
|
||||
Text(stringResource(R.string.copy_user_pubkey))
|
||||
}
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) {
|
||||
Text("Copy Note ID")
|
||||
Text(stringResource(R.string.copy_note_id))
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
|
||||
Text("Broadcast")
|
||||
Text(stringResource(R.string.broadcast))
|
||||
}
|
||||
if (note.author == accountViewModel.accountLiveData.value?.account?.userProfile()) {
|
||||
Divider()
|
||||
@@ -592,7 +657,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
)
|
||||
}; onDismiss()
|
||||
}) {
|
||||
Text("Block & Hide Author")
|
||||
Text(stringResource(R.string.block_hide_user))
|
||||
}
|
||||
Divider()
|
||||
DropdownMenuItem(onClick = {
|
||||
@@ -600,28 +665,35 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
|
||||
note.author?.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Spam / Scam")
|
||||
Text(stringResource(R.string.report_spam_scam))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(note, ReportEvent.ReportType.PROFANITY);
|
||||
note.author?.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.report_hateful_speech))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(note, ReportEvent.ReportType.IMPERSONATION);
|
||||
note.author?.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Impersonation")
|
||||
Text(stringResource(R.string.report_impersonation))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(note, ReportEvent.ReportType.EXPLICIT);
|
||||
accountViewModel.report(note, ReportEvent.ReportType.NUDITY);
|
||||
note.author?.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Explicit Content")
|
||||
Text(stringResource(R.string.report_nudity_porn))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(note, ReportEvent.ReportType.ILLEGAL);
|
||||
note.author?.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Illegal Behaviour")
|
||||
Text(stringResource(R.string.report_illegal_behaviour))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
@@ -141,7 +142,7 @@ fun ReplyReaction(
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Login with a Private key to be able to reply",
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_reply),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@@ -193,7 +194,7 @@ private fun BoostReaction(
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Login with a Private key to be able to boost posts",
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_boost_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@@ -264,7 +265,7 @@ fun LikeReaction(
|
||||
scope.launch {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Login with a Private key to like Posts",
|
||||
context.getString(R.string.login_with_a_private_key_to_like_posts),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
@@ -329,7 +330,7 @@ fun ZapReaction(
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
"No Zap Amount Setup. Long Press to change",
|
||||
context.getString(R.string.no_zap_amount_setup_long_press_to_change),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
@@ -339,7 +340,7 @@ fun ZapReaction(
|
||||
Toast
|
||||
.makeText(
|
||||
context,
|
||||
"Login with a Private key to be able to send Zaps",
|
||||
context.getString(R.string.login_with_a_private_key_to_be_able_to_send_zaps),
|
||||
Toast.LENGTH_SHORT
|
||||
)
|
||||
.show()
|
||||
@@ -386,14 +387,14 @@ fun ZapReaction(
|
||||
if (zappedNote?.isZappedBy(account.userProfile()) == true) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
contentDescription = stringResource(R.string.zaps),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = BitcoinOrange
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = grayTint
|
||||
)
|
||||
@@ -433,7 +434,7 @@ private fun ViewCountReaction(baseNote: Note, textModifier: Modifier = Modifier)
|
||||
.diskCachePolicy(CachePolicy.DISABLED)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.build(),
|
||||
contentDescription = "View count",
|
||||
contentDescription = stringResource(R.string.view_count),
|
||||
modifier = Modifier.height(24.dp),
|
||||
colorFilter = ColorFilter.tint(grayTint)
|
||||
)
|
||||
@@ -461,7 +462,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text("Boost", color = Color.White, textAlign = TextAlign.Center)
|
||||
Text(stringResource(R.string.boost), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
|
||||
Button(
|
||||
@@ -473,7 +474,7 @@ private fun BoostTypeChoicePopup(baseNote: Note, accountViewModel: AccountViewMo
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text("Quote", color = Color.White, textAlign = TextAlign.Center)
|
||||
Text(stringResource(R.string.quote), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -640,7 +641,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
label = { Text(text = "New Amount in Sats") },
|
||||
label = { Text(text = stringResource(R.string.new_amount_in_sats)) },
|
||||
value = postViewModel.nextAmount,
|
||||
onValueChange = {
|
||||
postViewModel.nextAmount = it
|
||||
@@ -656,7 +657,9 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.padding(end = 10.dp).weight(1f)
|
||||
modifier = Modifier
|
||||
.padding(end = 10.dp)
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Button(
|
||||
@@ -667,7 +670,7 @@ fun UpdateZapAmountDialog(onClose: () -> Unit, account: Account) {
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(text = "Add", color = Color.White)
|
||||
Text(text = stringResource(R.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ 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.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.RelayInfo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import java.time.Instant
|
||||
@@ -38,6 +41,8 @@ fun RelayCompose(
|
||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
Column() {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -58,13 +63,13 @@ fun RelayCompose(
|
||||
)
|
||||
|
||||
Text(
|
||||
timeAgo(relay.lastEvent),
|
||||
timeAgo(relay.lastEvent, context = context),
|
||||
maxLines = 1
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
"${relay.counter} posts received",
|
||||
"${relay.counter} ${stringResource(R.string.posts_received)}",
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
@@ -99,7 +104,7 @@ fun AddRelayButton(onClick: () -> Unit) {
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Add", color = Color.White)
|
||||
Text(text = stringResource(id = R.string.add), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +120,7 @@ fun RemoveRelayButton(onClick: () -> Unit) {
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Remove", color = Color.White)
|
||||
Text(text = stringResource(R.string.remove), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -15,6 +18,7 @@ import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -37,7 +41,7 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
|
||||
val repliesToDisplay = if (expanded) mentions else mentions.take(2)
|
||||
|
||||
Text(
|
||||
"replying to ",
|
||||
stringResource(R.string.replying_to),
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
@@ -62,7 +66,7 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
|
||||
)
|
||||
} else if (idx < repliesToDisplay.size - 1) {
|
||||
Text(
|
||||
" and ",
|
||||
" ${stringResource(R.string.and)} ",
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
@@ -76,7 +80,7 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
|
||||
)
|
||||
} else if (idx < repliesToDisplay.size) {
|
||||
Text(
|
||||
" and ",
|
||||
" ${stringResource(R.string.and)} ",
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
@@ -88,7 +92,7 @@ fun ReplyInformation(replyTo: List<Note>?, dupMentions: List<User>?, account: Ac
|
||||
)
|
||||
|
||||
Text(
|
||||
" others",
|
||||
" ${stringResource(R.string.others)}",
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
@@ -128,7 +132,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?,
|
||||
|
||||
FlowRow() {
|
||||
Text(
|
||||
"in channel ",
|
||||
stringResource(R.string.in_channel),
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
@@ -142,7 +146,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?,
|
||||
if (mentions != null && mentions.isNotEmpty()) {
|
||||
if (replyTo != null && replyTo.isNotEmpty()) {
|
||||
Text(
|
||||
"replying to ",
|
||||
stringResource(id = R.string.replying_to),
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
@@ -168,7 +172,7 @@ fun ReplyInformationChannel(replyTo: List<Note>?,
|
||||
)
|
||||
} else if (idx < mentionSet.size - 1) {
|
||||
Text(
|
||||
" and ",
|
||||
" ${stringResource(id = R.string.add)} ",
|
||||
fontSize = 13.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.vitorpamplona.amethyst.ui.note
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.DateUtils
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
fun timeAgo(mills: Long?): String {
|
||||
fun timeAgo(mills: Long?, context : Context): String {
|
||||
if (mills == null) return " "
|
||||
if (mills == 0L) return " • never"
|
||||
if (mills == 0L) return " • ${context.getString(R.string.never)}"
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
mills * 1000,
|
||||
@@ -13,16 +15,16 @@ fun timeAgo(mills: Long?): String {
|
||||
DateUtils.FORMAT_ABBREV_ALL
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = "now";
|
||||
humanReadable = context.getString(R.string.now);
|
||||
}
|
||||
|
||||
return " • " + humanReadable
|
||||
.replace(" hr. ago", "h")
|
||||
.replace(" min. ago", "m")
|
||||
.replace(" days ago", "d")
|
||||
.replace(" hr. ago", context.getString(R.string.h))
|
||||
.replace(" min. ago", context.getString(R.string.m))
|
||||
.replace(" days ago", context.getString(R.string.d))
|
||||
}
|
||||
|
||||
fun timeAgoShort(mills: Long?): String {
|
||||
fun timeAgoShort(mills: Long?, context: Context): String {
|
||||
if (mills == null) return " "
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
@@ -32,13 +34,13 @@ fun timeAgoShort(mills: Long?): String {
|
||||
DateUtils.FORMAT_ABBREV_ALL
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = "now";
|
||||
humanReadable = context.getString(R.string.now);
|
||||
}
|
||||
|
||||
return humanReadable
|
||||
}
|
||||
|
||||
fun timeAgoLong(mills: Long?): String {
|
||||
fun timeAgoLong(mills: Long?, context: Context): String {
|
||||
if (mills == null) return " "
|
||||
|
||||
var humanReadable = DateUtils.getRelativeTimeSpanString(
|
||||
@@ -48,7 +50,7 @@ fun timeAgoLong(mills: Long?): String {
|
||||
DateUtils.FORMAT_SHOW_TIME
|
||||
).toString()
|
||||
if (humanReadable.startsWith("In") || humanReadable.startsWith("0")) {
|
||||
humanReadable = "now";
|
||||
humanReadable = context.getString(R.string.now);
|
||||
}
|
||||
|
||||
return humanReadable
|
||||
|
||||
@@ -15,11 +15,14 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
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.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -94,7 +97,7 @@ fun ZapNoteCompose(baseNote: Pair<Note, Note>, accountViewModel: AccountViewMode
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
showAmount(amount) + " sats",
|
||||
"${showAmount(amount)} ${stringResource(R.string.sats)}",
|
||||
color = BitcoinOrange,
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.W500,
|
||||
|
||||
@@ -24,10 +24,12 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
import com.vitorpamplona.amethyst.NotificationCache
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||
import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -94,9 +96,11 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, modifier: Modifier = Modifier, isInner
|
||||
.padding(0.dp)) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Zaps",
|
||||
contentDescription = stringResource(id = R.string.zaps),
|
||||
tint = BitcoinOrange,
|
||||
modifier = Modifier.size(25.dp).align(Alignment.TopEnd)
|
||||
modifier = Modifier
|
||||
.size(25.dp)
|
||||
.align(Alignment.TopEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,11 +28,15 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import coil.compose.AsyncImage
|
||||
import coil.compose.rememberAsyncImagePainter
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
import com.vitorpamplona.amethyst.ui.actions.CloseButton
|
||||
@@ -83,7 +87,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, user.pubkeyHex)),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(100.dp)
|
||||
.height(100.dp)
|
||||
@@ -129,7 +133,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(text = "Scan QR")
|
||||
Text(text = stringResource(R.string.scan_qr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +141,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
|
||||
Row(horizontalArrangement = Arrangement.Center, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
"Point to the QR Code",
|
||||
stringResource(R.string.point_to_the_qr_code),
|
||||
modifier = Modifier.padding(top = 7.dp),
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 25.sp
|
||||
@@ -167,7 +171,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
|
||||
backgroundColor = MaterialTheme.colors.primary
|
||||
)
|
||||
) {
|
||||
Text(text = "Show QR")
|
||||
Text(text = stringResource(R.string.show_qr))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,6 +32,19 @@ class ZapSetCard(val note: Note, val zapEvents: Map<Note, Note>): Card() {
|
||||
override fun id() = note.idHex + "Z" + createdAt
|
||||
}
|
||||
|
||||
class MultiSetCard(val note: Note, val boostEvents: List<Note>, val likeEvents: List<Note>, val zapEvents: Map<Note, Note>): Card() {
|
||||
val createdAt = maxOf(
|
||||
zapEvents.maxOfOrNull { it.value.event?.createdAt ?: 0 } ?: 0 ,
|
||||
likeEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0 ,
|
||||
boostEvents.maxOfOrNull { it.event?.createdAt ?: 0 } ?: 0
|
||||
)
|
||||
|
||||
override fun createdAt(): Long {
|
||||
return createdAt
|
||||
}
|
||||
override fun id() = note.idHex + "X" + createdAt
|
||||
}
|
||||
|
||||
class BoostSetCard(val note: Note, val boostEvents: List<Note>): Card() {
|
||||
val createdAt = boostEvents.maxOf { it.event?.createdAt ?: 0 }
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.vitorpamplona.amethyst.ui.note.BoostSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.LikeSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.MultiSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.note.ZapSetCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
@@ -120,6 +121,12 @@ private fun FeedLoaded(
|
||||
navController = navController,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
is MultiSetCard -> MultiSetCompose(
|
||||
item,
|
||||
accountViewModel = accountViewModel,
|
||||
navController = navController,
|
||||
routeForLastRead = routeForLastRead
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>): ViewModel() {
|
||||
reactionsPerEvent.getOrPut(reactedPost, { mutableListOf() }).add(it)
|
||||
}
|
||||
|
||||
val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) }
|
||||
//val reactionCards = reactionsPerEvent.map { LikeSetCard(it.key, it.value) }
|
||||
|
||||
val zapsPerEvent = mutableMapOf<Note, MutableMap<Note, Note>>()
|
||||
notes
|
||||
@@ -84,7 +84,7 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>): ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
val zapCards = zapsPerEvent.map { ZapSetCard(it.key, it.value) }
|
||||
//val zapCards = zapsPerEvent.map { ZapSetCard(it.key, it.value) }
|
||||
|
||||
val boostsPerEvent = mutableMapOf<Note, MutableList<Note>>()
|
||||
notes
|
||||
@@ -95,11 +95,20 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>): ViewModel() {
|
||||
boostsPerEvent.getOrPut(boostedPost, { mutableListOf() }).add(it)
|
||||
}
|
||||
|
||||
val boostCards = boostsPerEvent.map { BoostSetCard(it.key, it.value) }
|
||||
//val boostCards = boostsPerEvent.map { BoostSetCard(it.key, it.value) }
|
||||
|
||||
val allBaseNotes = zapsPerEvent.keys + boostsPerEvent.keys + reactionsPerEvent.keys
|
||||
val multiCards = allBaseNotes.map {
|
||||
MultiSetCard(it,
|
||||
boostsPerEvent.get(it) ?: emptyList(),
|
||||
reactionsPerEvent.get(it) ?: emptyList(),
|
||||
zapsPerEvent.get(it) ?: emptyMap()
|
||||
)
|
||||
}
|
||||
|
||||
val textNoteCards = notes.filter { it.event !is ReactionEvent && it.event !is RepostEvent && it.event !is LnZapEvent }.map { NoteCard(it) }
|
||||
|
||||
return (reactionCards + boostCards + zapCards + textNoteCards).sortedBy { it.createdAt() }.reversed()
|
||||
return (multiCards + textNoteCards).sortedBy { it.createdAt() }.reversed()
|
||||
}
|
||||
|
||||
private fun updateFeed(notes: List<Card>) {
|
||||
@@ -120,11 +129,9 @@ open class CardFeedViewModel(val dataSource: FeedFilter<Note>): ViewModel() {
|
||||
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
private fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -22,10 +22,12 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.note.NoteCompose
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
|
||||
@@ -122,7 +124,7 @@ fun LoadingFeed() {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text("Loading feed")
|
||||
Text(stringResource(R.string.loading_feed))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,12 +137,12 @@ fun FeedError(errorMessage: String, onRefresh: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text("Error loading replies: $errorMessage")
|
||||
Text("${stringResource(R.string.error_loading_replies)} $errorMessage")
|
||||
Button(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
onClick = onRefresh
|
||||
) {
|
||||
Text(text = "Try again")
|
||||
Text(text = stringResource(R.string.try_again))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,9 +156,9 @@ fun FeedEmpty(onRefresh: () -> Unit) {
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text("Feed is empty.")
|
||||
Text(stringResource(R.string.feed_is_empty))
|
||||
OutlinedButton(onClick = onRefresh) {
|
||||
Text(text = "Refresh")
|
||||
Text(text = stringResource(R.string.refresh))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,11 +87,10 @@ abstract class FeedViewModel(val localFilter: FeedFilter<Note>): ViewModel() {
|
||||
}
|
||||
|
||||
private var handlerWaiting = AtomicBoolean()
|
||||
@Synchronized
|
||||
|
||||
fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -61,11 +61,9 @@ open class LnZapFeedViewModel(val dataSource: FeedFilter<Pair<Note, Note>>): Vie
|
||||
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
private fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -83,11 +83,9 @@ class RelayFeedViewModel: ViewModel() {
|
||||
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
private fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -33,6 +33,7 @@ import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.navigation.NavController
|
||||
import com.google.accompanist.swiperefresh.SwipeRefresh
|
||||
@@ -50,6 +51,14 @@ import com.vitorpamplona.amethyst.ui.note.ReactionsRow
|
||||
import com.vitorpamplona.amethyst.ui.note.timeAgo
|
||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.model.LongTextNoteEvent
|
||||
|
||||
@Composable
|
||||
fun ThreadFeedView(noteId: String, viewModel: FeedViewModel, accountViewModel: AccountViewModel, navController: NavController) {
|
||||
@@ -192,6 +201,8 @@ fun NoteMaster(baseNote: Note,
|
||||
|
||||
var showHiddenNote by remember { mutableStateOf(false) }
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
var moreActionsExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
val noteEvent = note?.event
|
||||
@@ -232,7 +243,7 @@ fun NoteMaster(baseNote: Note,
|
||||
NoteUsernameDisplay(baseNote, Modifier.weight(1f))
|
||||
|
||||
Text(
|
||||
timeAgo(noteEvent.createdAt),
|
||||
timeAgo(noteEvent.createdAt, context = context),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
maxLines = 1
|
||||
)
|
||||
@@ -256,6 +267,41 @@ fun NoteMaster(baseNote: Note,
|
||||
}
|
||||
}
|
||||
|
||||
if (noteEvent is LongTextNoteEvent) {
|
||||
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||
Column {
|
||||
noteEvent.image?.let {
|
||||
AsyncImage(
|
||||
model = noteEvent.image,
|
||||
contentDescription = stringResource(
|
||||
R.string.preview_card_image_for,
|
||||
noteEvent.image
|
||||
),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
|
||||
noteEvent.title?.let {
|
||||
Text(
|
||||
text = it,
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 10.dp)
|
||||
)
|
||||
}
|
||||
|
||||
noteEvent.summary?.let {
|
||||
Text(
|
||||
text = it
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row(modifier = Modifier.padding(horizontal = 12.dp)) {
|
||||
Column() {
|
||||
val eventContent = note.event?.content
|
||||
@@ -286,4 +332,4 @@ fun NoteMaster(baseNote: Note,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,11 +65,9 @@ open class UserFeedViewModel(val dataSource: FeedFilter<User>): ViewModel() {
|
||||
|
||||
var handlerWaiting = AtomicBoolean()
|
||||
|
||||
@Synchronized
|
||||
private fun invalidateData() {
|
||||
if (handlerWaiting.getAndSet(true)) return
|
||||
|
||||
handlerWaiting.set(true)
|
||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||
scope.launch {
|
||||
try {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.map
|
||||
import com.vitorpamplona.amethyst.service.lnurl.LightningAddressResolver
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.AccountState
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
@@ -51,7 +52,7 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
||||
val lud16 = note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
|
||||
|
||||
if (lud16.isNullOrBlank()) {
|
||||
onError("User does not have a lightning address setup to receive sats")
|
||||
onError(context.getString(R.string.user_does_not_have_a_lightning_address_setup_to_receive_sats))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
@@ -59,6 +60,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.model.Channel
|
||||
@@ -183,7 +185,7 @@ fun ChannelScreen(channelId: String?, accountViewModel: AccountViewModel, accoun
|
||||
modifier = Modifier.weight(1f, true),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "reply here.. ",
|
||||
text = stringResource(R.string.reply_here),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -226,7 +228,7 @@ fun ChannelHeader(baseChannel: Channel, account: Account, accountStateViewModel:
|
||||
placeholder = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(context, channel.idHex)),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = context.getString(R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(35.dp)
|
||||
.height(35.dp)
|
||||
@@ -302,7 +304,7 @@ private fun NoteCopyButton(
|
||||
Icon(
|
||||
tint = Color.White,
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "Copies the Note ID to the clipboard for sharing"
|
||||
contentDescription = stringResource(R.string.copies_the_note_id_to_the_clipboard_for_sharing)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
@@ -310,7 +312,7 @@ private fun NoteCopyButton(
|
||||
onDismissRequest = { popupExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); popupExpanded = false }) {
|
||||
Text("Copy Channel ID (Note) to the Clipboard")
|
||||
Text(stringResource(R.string.copy_channel_id_note_to_the_clipboard))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -339,7 +341,7 @@ private fun EditButton(account: Account, channel: Channel) {
|
||||
Icon(
|
||||
tint = Color.White,
|
||||
imageVector = Icons.Default.EditNote,
|
||||
contentDescription = "Edits the Channel Metadata"
|
||||
contentDescription = stringResource(R.string.edits_the_channel_metadata)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -359,7 +361,7 @@ private fun JoinButton(account: Account, channel: Channel, navController: NavCon
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Join", color = Color.White)
|
||||
Text(text = stringResource(R.string.join), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,6 +380,6 @@ private fun LeaveButton(account: Account, channel: Channel, navController: NavCo
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Leave", color = Color.White)
|
||||
Text(text = stringResource(R.string.leave), color = Color.White)
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -32,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import com.vitorpamplona.amethyst.R
|
||||
|
||||
@OptIn(ExperimentalPagerApi::class)
|
||||
@Composable
|
||||
@@ -57,7 +59,7 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
|
||||
selected = pagerState.currentPage == 0,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
||||
text = {
|
||||
Text(text = "Known")
|
||||
Text(text = stringResource(R.string.known))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,7 +67,7 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
|
||||
selected = pagerState.currentPage == 1,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||
text = {
|
||||
Text(text = "New Requests")
|
||||
Text(text = stringResource(R.string.new_requests))
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -87,31 +89,11 @@ fun TabKnown(accountViewModel: AccountViewModel, navController: NavController) {
|
||||
ChatroomListKnownFeedFilter.account = account
|
||||
val feedViewModel: NostrChatroomListKnownFeedViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
NostrChatroomListDataSource.resetFilters()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { source, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Chatroom List Start")
|
||||
NostrChatroomListDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Chatroom List Stop")
|
||||
NostrChatroomListDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
@@ -129,29 +111,9 @@ fun TabNew(accountViewModel: AccountViewModel, navController: NavController) {
|
||||
ChatroomListNewFeedFilter.account = account
|
||||
val feedViewModel: NostrChatroomListNewFeedViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
NostrChatroomListDataSource.resetFilters()
|
||||
feedViewModel.refresh() // refresh view
|
||||
}
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { source, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Chatroom List Start")
|
||||
NostrChatroomListDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Chatroom List Stop")
|
||||
NostrChatroomListDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
feedViewModel.invalidateData() // refresh view
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextDirection
|
||||
@@ -47,6 +48,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.RoboHashCache
|
||||
import com.vitorpamplona.amethyst.model.Note
|
||||
import com.vitorpamplona.amethyst.model.User
|
||||
@@ -76,7 +78,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
||||
val feedViewModel: NostrChatRoomFeedViewModel = viewModel()
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(userId) {
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
@@ -169,7 +171,7 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "reply here.. ",
|
||||
text = stringResource(id = R.string.reply_here),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -216,7 +218,7 @@ fun ChatroomHeader(baseUser: User, accountViewModel: AccountViewModel, navContro
|
||||
placeholder = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)),
|
||||
fallback = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)),
|
||||
error = BitmapPainter(RoboHashCache.get(ctx, author.pubkeyHex)),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
modifier = Modifier
|
||||
.width(35.dp)
|
||||
.height(35.dp)
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.navigation.NavController
|
||||
@@ -20,6 +21,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.ui.dal.HiddenAccountsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrHiddenAccountsFeedViewModel
|
||||
import com.vitorpamplona.amethyst.ui.screen.UserFeedView
|
||||
@@ -55,7 +57,7 @@ fun FiltersScreen(accountViewModel: AccountViewModel, navController: NavControll
|
||||
selected = pagerState.currentPage == 0,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
||||
text = {
|
||||
Text(text = "Blocked Users")
|
||||
Text(text = stringResource(R.string.blocked_users))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
@@ -25,6 +26,7 @@ import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.pagerTabIndicatorOffset
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.service.NostrHomeDataSource
|
||||
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
|
||||
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
||||
@@ -49,34 +51,13 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
|
||||
val pagerState = rememberPagerState()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
NostrHomeDataSource.resetFilters()
|
||||
|
||||
feedViewModel.invalidateData()
|
||||
feedViewModelReplies.invalidateData()
|
||||
}
|
||||
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
DisposableEffect(accountViewModel) {
|
||||
val observer = LifecycleEventObserver { source, event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
println("Home Start")
|
||||
NostrHomeDataSource.start()
|
||||
feedViewModel.invalidateData()
|
||||
feedViewModelReplies.invalidateData()
|
||||
}
|
||||
if (event == Lifecycle.Event.ON_PAUSE) {
|
||||
println("Home Stop")
|
||||
NostrHomeDataSource.stop()
|
||||
}
|
||||
}
|
||||
|
||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
Column(Modifier.fillMaxHeight()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 0.dp)
|
||||
@@ -95,7 +76,7 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
|
||||
selected = pagerState.currentPage == 0,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
||||
text = {
|
||||
Text(text = "New Threads")
|
||||
Text(text = stringResource(R.string.new_threads))
|
||||
}
|
||||
)
|
||||
|
||||
@@ -103,7 +84,7 @@ fun HomeScreen(accountViewModel: AccountViewModel, navController: NavController)
|
||||
selected = pagerState.currentPage == 1,
|
||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
||||
text = {
|
||||
Text(text = "Conversations")
|
||||
Text(text = stringResource(R.string.conversations))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ fun NotificationScreen(accountViewModel: AccountViewModel, navController: NavCon
|
||||
NotificationFeedFilter.account = account
|
||||
val feedViewModel: NotificationViewModel = viewModel()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
LaunchedEffect(accountViewModel) {
|
||||
feedViewModel.refresh()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -179,34 +180,34 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
||||
) {
|
||||
val tabs = listOf<@Composable() (() -> Unit)?>(
|
||||
{
|
||||
Text(text = "Notes")
|
||||
Text(text = stringResource(R.string.notes))
|
||||
},
|
||||
{
|
||||
Text(text = "Replies")
|
||||
Text(text = stringResource(R.string.replies))
|
||||
},
|
||||
{
|
||||
val userState by baseUser.live().follows.observeAsState()
|
||||
val userFollows = userState?.user?.follows?.size ?: "--"
|
||||
|
||||
Text(text = "$userFollows Follows")
|
||||
Text(text = "$userFollows ${stringResource(R.string.follows)}")
|
||||
},
|
||||
{
|
||||
val userState by baseUser.live().follows.observeAsState()
|
||||
val userFollows = userState?.user?.followers?.size ?: "--"
|
||||
val userFollowers = userState?.user?.followers?.size ?: "--"
|
||||
|
||||
Text(text = "$userFollows Followers")
|
||||
Text(text = "$userFollowers ${stringResource(id = R.string.followers)}")
|
||||
},
|
||||
{
|
||||
val userState by baseUser.live().zaps.observeAsState()
|
||||
val userZaps = userState?.user?.zappedAmount()
|
||||
|
||||
Text(text = "${showAmount(userZaps)} Zaps")
|
||||
Text(text = "${showAmount(userZaps)} ${stringResource(id = R.string.zaps)}")
|
||||
},
|
||||
{
|
||||
val userState by baseUser.live().reports.observeAsState()
|
||||
val userReports = userState?.user?.reports?.values?.flatten()?.count()
|
||||
|
||||
Text(text = "${userReports} Reports")
|
||||
Text(text = "${userReports.toString()} ${stringResource(R.string.reports)}")
|
||||
},
|
||||
{
|
||||
val userState by baseUser.live().relays.observeAsState()
|
||||
@@ -215,7 +216,7 @@ fun ProfileScreen(userId: String?, accountViewModel: AccountViewModel, navContro
|
||||
val userStateRelayInfo by baseUser.live().relayInfo.observeAsState()
|
||||
val userRelays = userStateRelayInfo?.user?.relays?.size ?: "--"
|
||||
|
||||
Text(text = "$userRelaysBeingUsed / $userRelays Relays")
|
||||
Text(text = "$userRelaysBeingUsed / $userRelays ${stringResource(R.string.relays)}")
|
||||
}
|
||||
)
|
||||
|
||||
@@ -264,7 +265,8 @@ private fun ProfileHeader(
|
||||
val accountUser = accountUserState?.user ?: return
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
Box {
|
||||
DrawBanner(baseUser)
|
||||
|
||||
@@ -288,7 +290,7 @@ private fun ProfileHeader(
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More Options",
|
||||
contentDescription = stringResource(R.string.more_options),
|
||||
)
|
||||
|
||||
UserProfileDropDownMenu(baseUser, popupExpanded, { popupExpanded = false }, accountViewModel)
|
||||
@@ -309,12 +311,23 @@ private fun ProfileHeader(
|
||||
) {
|
||||
|
||||
UserPicture(
|
||||
baseUser, navController, account.userProfile(), 100.dp,
|
||||
baseUser = baseUser,
|
||||
baseUserAccount = account.userProfile(),
|
||||
size = 100.dp,
|
||||
pictureModifier = Modifier.border(
|
||||
3.dp,
|
||||
MaterialTheme.colors.background,
|
||||
CircleShape
|
||||
)
|
||||
CircleShape),
|
||||
onClick = {
|
||||
navController.navigate("User/${it.pubkeyHex}")
|
||||
},
|
||||
onLongClick = {
|
||||
ResizeImage(it.info?.picture, 100.dp).proxyUrl()?.let { it1 ->
|
||||
clipboardManager.setText(
|
||||
AnnotatedString(it1)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
@@ -386,7 +399,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
|
||||
Icon(
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
|
||||
imageVector = Icons.Default.Link,
|
||||
contentDescription = "Website",
|
||||
contentDescription = stringResource(R.string.website),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
@@ -408,7 +421,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
|
||||
Icon(
|
||||
tint = BitcoinOrange,
|
||||
imageVector = Icons.Default.Bolt,
|
||||
contentDescription = "Lightning Address",
|
||||
contentDescription = stringResource(R.string.lightning_address),
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
|
||||
@@ -440,26 +453,33 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DrawBanner(baseUser: User) {
|
||||
val userState by baseUser.live().metadata.observeAsState()
|
||||
val user = userState?.user ?: return
|
||||
|
||||
val banner = user.info?.banner
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
|
||||
if (banner != null && banner.isNotBlank()) {
|
||||
AsyncImageProxy(
|
||||
model = ResizeImage(banner, 125.dp),
|
||||
contentDescription = "Profile Image",
|
||||
contentDescription = stringResource(id = R.string.profile_image),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(125.dp)
|
||||
.height(125.dp).combinedClickable(
|
||||
onClick = {},
|
||||
onLongClick = {
|
||||
clipboardManager.setText(AnnotatedString(banner))
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.profile_banner),
|
||||
contentDescription = "Profile Banner",
|
||||
contentDescription = stringResource(id = R.string.profile_banner),
|
||||
contentScale = ContentScale.FillWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -638,7 +658,7 @@ private fun NSecCopyButton(
|
||||
Icon(
|
||||
tint = Color.White,
|
||||
imageVector = Icons.Default.Key,
|
||||
contentDescription = "Copies the Nsec ID (your password) to the clipboard for backup"
|
||||
contentDescription = stringResource(R.string.copies_the_nsec_id_your_password_to_the_clipboard_for_backup)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
@@ -646,7 +666,7 @@ private fun NSecCopyButton(
|
||||
onDismissRequest = { popupExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(onClick = { account.loggedIn.privKey?.let { clipboardManager.setText(AnnotatedString(it.toNsec())) }; popupExpanded = false }) {
|
||||
Text("Copy Private Key to the Clipboard")
|
||||
Text(stringResource(R.string.copy_private_key_to_the_clipboard))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,7 +693,7 @@ private fun NPubCopyButton(
|
||||
Icon(
|
||||
tint = Color.White,
|
||||
imageVector = Icons.Default.Share,
|
||||
contentDescription = "Copies the public key to the clipboard for sharing"
|
||||
contentDescription = stringResource(R.string.copies_the_public_key_to_the_clipboard_for_sharing)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
@@ -681,7 +701,7 @@ private fun NPubCopyButton(
|
||||
onDismissRequest = { popupExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); popupExpanded = false }) {
|
||||
Text("Copy Public Key (NPub) to the Clipboard")
|
||||
Text(stringResource(R.string.copy_public_key_npub_to_the_clipboard))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -702,7 +722,7 @@ private fun MessageButton(user: User, navController: NavController) {
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_dm),
|
||||
"Send a Direct Message",
|
||||
stringResource(R.string.send_a_direct_message),
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
@@ -732,7 +752,7 @@ private fun EditButton(account: Account) {
|
||||
Icon(
|
||||
tint = Color.White,
|
||||
imageVector = Icons.Default.EditNote,
|
||||
contentDescription = "Edits the User's Metadata"
|
||||
contentDescription = stringResource(R.string.edits_the_user_s_metadata)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -749,7 +769,7 @@ fun UnfollowButton(onClick: () -> Unit) {
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Unfollow", color = Color.White)
|
||||
Text(text = stringResource(R.string.unfollow), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -765,7 +785,7 @@ fun FollowButton(onClick: () -> Unit) {
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Follow", color = Color.White, textAlign = TextAlign.Center)
|
||||
Text(text = stringResource(R.string.follow), color = Color.White, textAlign = TextAlign.Center)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -781,7 +801,7 @@ fun ShowUserButton(onClick: () -> Unit) {
|
||||
),
|
||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||
) {
|
||||
Text(text = "Unblock", color = Color.White)
|
||||
Text(text = stringResource(R.string.unblock), color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -799,7 +819,7 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
onDismissRequest = onDismiss
|
||||
) {
|
||||
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); onDismiss() }) {
|
||||
Text("Copy User ID")
|
||||
Text(stringResource(R.string.copy_user_id))
|
||||
}
|
||||
|
||||
if ( account.userProfile() != user) {
|
||||
@@ -813,11 +833,11 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
)
|
||||
}; onDismiss()
|
||||
}) {
|
||||
Text("Unblock User")
|
||||
Text(stringResource(R.string.unblock_user))
|
||||
}
|
||||
} else {
|
||||
DropdownMenuItem(onClick = { user.let { accountViewModel.hide(it, context) }; onDismiss() }) {
|
||||
Text("Block & Hide User")
|
||||
Text(stringResource(id = R.string.block_hide_user))
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
@@ -826,28 +846,35 @@ fun UserProfileDropDownMenu(user: User, popupExpanded: Boolean, onDismiss: () ->
|
||||
user.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Spam / Scam")
|
||||
Text(stringResource(id = R.string.report_spam_scam))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.PROFANITY);
|
||||
user.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text(stringResource(R.string.report_hateful_speech))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.IMPERSONATION);
|
||||
user.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Impersonation")
|
||||
Text(stringResource(id = R.string.report_impersonation))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.EXPLICIT);
|
||||
accountViewModel.report(user, ReportEvent.ReportType.NUDITY);
|
||||
user.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Explicit Content")
|
||||
Text(stringResource(R.string.report_nudity_porn))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
accountViewModel.report(user, ReportEvent.ReportType.ILLEGAL);
|
||||
user.let { accountViewModel.hide(it, context) }
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("Report Illegal Behaviour")
|
||||
Text(stringResource(id = R.string.report_illegal_behaviour))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
@@ -67,6 +68,7 @@ import com.vitorpamplona.amethyst.ui.screen.FeedView
|
||||
import com.vitorpamplona.amethyst.ui.screen.NostrGlobalFeedViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
@@ -81,12 +83,13 @@ fun SearchScreen(accountViewModel: AccountViewModel, navController: NavControlle
|
||||
val account = accountState?.account ?: return
|
||||
|
||||
GlobalFeedFilter.account = account
|
||||
NostrGlobalDataSource.account = account
|
||||
val feedViewModel: NostrGlobalFeedViewModel = viewModel()
|
||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
feedViewModel.invalidateData()
|
||||
delay(500)
|
||||
feedViewModel.invalidateData()
|
||||
}
|
||||
|
||||
DisposableEffect(accountViewModel) {
|
||||
@@ -200,7 +203,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
|
||||
.defaultMinSize(minHeight = 20.dp),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "npub, hex, username ",
|
||||
text = stringResource(R.string.npub_hex_username),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -218,7 +221,7 @@ private fun SearchBar(accountViewModel: AccountViewModel, navController: NavCont
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Clear,
|
||||
contentDescription = "Clear"
|
||||
contentDescription = stringResource(R.string.clear)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,8 +38,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
@@ -60,6 +62,7 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
val acceptedTerms = remember { mutableStateOf(false) }
|
||||
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
||||
val uri = LocalUriHandler.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
@@ -81,7 +84,7 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
|
||||
Image(
|
||||
painterResource(id = R.drawable.amethyst),
|
||||
contentDescription = "App Logo",
|
||||
contentDescription = stringResource(R.string.app_logo),
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentScale = ContentScale.Inside
|
||||
)
|
||||
@@ -97,12 +100,12 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
onValueChange = { key.value = it },
|
||||
keyboardOptions = KeyboardOptions(
|
||||
autoCorrect = false,
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Go
|
||||
),
|
||||
placeholder = {
|
||||
Text(
|
||||
text = "nsec / npub / hex private key",
|
||||
text = stringResource(R.string.nsec_npub_hex_private_key),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
|
||||
)
|
||||
},
|
||||
@@ -110,7 +113,8 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
IconButton(onClick = { showPassword = !showPassword }) {
|
||||
Icon(
|
||||
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription = if (showPassword) "Show Password" else "Hide Password"
|
||||
contentDescription = if (showPassword) stringResource(R.string.show_password) else stringResource(
|
||||
R.string.hide_password)
|
||||
)
|
||||
}
|
||||
},
|
||||
@@ -120,7 +124,7 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
try {
|
||||
accountViewModel.login(key.value.text)
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Invalid key"
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -141,10 +145,10 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
onCheckedChange = { acceptedTerms.value = it }
|
||||
)
|
||||
|
||||
Text(text = "I accept the ")
|
||||
Text(text = stringResource(R.string.i_accept_the))
|
||||
|
||||
ClickableText(
|
||||
text = AnnotatedString("terms of use"),
|
||||
text = AnnotatedString(stringResource(R.string.terms_of_use)),
|
||||
onClick = { runCatching { uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md") } },
|
||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
|
||||
)
|
||||
@@ -164,18 +168,18 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
Button(
|
||||
onClick = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired = "Acceptance of terms is required"
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
|
||||
if (key.value.text.isBlank()) {
|
||||
errorMessage = "Key is required"
|
||||
errorMessage = context.getString(R.string.key_is_required)
|
||||
}
|
||||
|
||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||
try {
|
||||
accountViewModel.login(key.value.text)
|
||||
} catch (e: Exception) {
|
||||
errorMessage = "Invalid key"
|
||||
errorMessage = context.getString(R.string.invalid_key)
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -188,14 +192,14 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
backgroundColor = if (acceptedTerms.value) MaterialTheme.colors.primary else Color.Gray
|
||||
)
|
||||
) {
|
||||
Text(text = "Login")
|
||||
Text(text = stringResource(R.string.login))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The last child is glued to the bottom.
|
||||
ClickableText(
|
||||
text = AnnotatedString("Generate a new key"),
|
||||
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.fillMaxWidth(),
|
||||
@@ -203,7 +207,7 @@ fun LoginPage(accountViewModel: AccountStateViewModel) {
|
||||
if (acceptedTerms.value) {
|
||||
accountViewModel.newKey()
|
||||
} else {
|
||||
termsAcceptanceIsRequired = "Acceptance of terms is required"
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
},
|
||||
style = TextStyle(
|
||||
|
||||
176
app/src/main/res/values-ar/strings.xml
Normal file
176
app/src/main/res/values-ar/strings.xml
Normal file
@@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<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">Relay Icon</string>
|
||||
<string name="unknown_author">كاتب غير معروف</string>
|
||||
<string name="copy_text">نسخ النص</string>
|
||||
<string name="copy_user_pubkey">نسخ المفتاح العام</string>
|
||||
<string name="copy_note_id">نسخ معرف </string>
|
||||
<string name="broadcast">بث</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">لا يوجد إعداد لمبلغ Zap. اضغط لفترة طويلة للتغيير</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">قم بتسجيل الدخول باستخدام مفتاح خاص لتتمكن من إرسال Zaps</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">مشاهدة العد</string>
|
||||
<string name="boost">تعزيز</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">"في القناة "</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">ملاحظة للمستقبل</string>
|
||||
<string name="thank_you_so_much">شكرا جزيلا!</string>
|
||||
<string name="amount_in_sats">المبلغ في Sats</string>
|
||||
<string name="send_sats">إرسال Sats</string>
|
||||
<string name="never_translate_from">"لا تترجم ابداً من "</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="can_t_find_out_the_content_type">لا يمكن معرفة نوع المحتوى</string>
|
||||
<string name="can_t_insert_the_new_content">لا يمكن إدراج المحتوى الجديد</string>
|
||||
<string name="can_t_open_the_content_output_stream">لا يمكن فتح دفق إخراج المحتوى</string>
|
||||
<string name="can_t_open_the_image_input_stream">لا يمكن فتح دفق إدخال الصورة</string>
|
||||
<string name="there_must_be_an_uploaded_image_url_in_the_response">يجب أن يكون هناك عنوان URL للصورة التي تم تحميلها في الاستجابة</string>
|
||||
<string name="channel_name">اسم القناة</string>
|
||||
<string name="my_awesome_group">مجموعتي الرائعة</string>
|
||||
<string name="picture_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">Relay عنوان</string>
|
||||
<string name="posts">المنشورات</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="add_a_relay">أضف 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">رابط الصورة الزمزية</string>
|
||||
<string name="banner_url">رابط الشعار</string>
|
||||
<string name="website_url">رابط الموقع</string>
|
||||
<string name="ln_address">LN عنوان</string>
|
||||
<string name="ln_url_outdated">LN رابط (قديم)</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">لا يمتلك المستخدم إعداد عنوان lighting لاستقبال sats</string>
|
||||
<string name="reply_here">"الرد هنا.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">نسخ معرف الملاحظة إلى الحافظة للمشاركة</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">انسخ معرف القناة (ملاحظة) إلى الحافظة</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">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 (كلمة المرور الخاصة بك) إلى الحافظة للنسخ الاحتياطي</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">القيام بتحرير metadata للمستخدم</string>
|
||||
<string name="follow">متابعة</string>
|
||||
<string name="unblock">رفع الحظر</string>
|
||||
<string name="copy_user_id">نسخ معرف المستخدم</string>
|
||||
<string name="unblock_user">إلغاء حظر عن مستخدم</string>
|
||||
<string name="npub_hex_username">npub, hex, username </string>
|
||||
<string name="clear">نظف</string>
|
||||
<string name="app_logo">شعار التطبيق</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex private key</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="auto">تلقائي</string>
|
||||
<string name="translated_from">مترجم من</string>
|
||||
<string name="to">الى</string>
|
||||
<string name="show_in">تظهر في</string>
|
||||
<string name="first">اولا</string>
|
||||
<string name="always_translate_to">ترجمة إلى</string>
|
||||
<string name="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>
|
||||
</resources>
|
||||
176
app/src/main/res/values-pt-rBR/strings.xml
Normal file
176
app/src/main/res/values-pt-rBR/strings.xml
Normal file
@@ -0,0 +1,176 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="point_to_the_qr_code">Aponte para o código QR</string>
|
||||
<string name="show_qr">Mostrar QR</string>
|
||||
<string name="profile_image">Imagem de perfil</string>
|
||||
<string name="scan_qr">Escanear QR</string>
|
||||
<string name="show_anyway">Mostrar de qualquer maneira</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">A postagem foi sinalizada como imprópria por</string>
|
||||
<string name="post_not_found">postagem não encontrada</string>
|
||||
<string name="channel_image">Imagem do Canal</string>
|
||||
<string name="referenced_event_not_found">Evento referenciado não encontrado</string>
|
||||
<string name="could_not_decrypt_the_message">Não foi possível descriptografar a mensagem</string>
|
||||
<string name="group_picture">Imagem do grupo</string>
|
||||
<string name="explicit_content">Conteúdo explícito</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="impersonation">Representação</string>
|
||||
<string name="illegal_behavior">Comportamento ilegal</string>
|
||||
<string name="unknown">Desconhecido</string>
|
||||
<string name="relay_icon">Ícone do relay</string>
|
||||
<string name="unknown_author">Autor desconhecido</string>
|
||||
<string name="copy_text">Copiar texto</string>
|
||||
<string name="copy_user_pubkey">Copiar PubKey do usuário</string>
|
||||
<string name="copy_note_id">Copiar ID da Nota</string>
|
||||
<string name="broadcast">Transmitir</string>
|
||||
<string name="block_hide_user">Bloquear e ocultar usuário</string>
|
||||
<string name="report_spam_scam">Denunciar spam/fraude</string>
|
||||
<string name="report_impersonation">Denunciar representação</string>
|
||||
<string name="report_explicit_content">Denunciar conteúdo explícito</string>
|
||||
<string name="report_illegal_behaviour">Denunciar comportamento ilegal</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Faça login com uma chave privada para poder responder</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Faça login com uma chave privada para poder impulsionar postagens</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Faça login com uma chave privada para curtir postagens</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">Nenhuma configuração de quantidade de Zap. Pressione e segure para alterar</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Faça login com uma chave privada para poder enviar Zaps</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">Contagem de visualizações</string>
|
||||
<string name="boost">Impulsionar</string>
|
||||
<string name="quote">Citar</string>
|
||||
<string name="new_amount_in_sats">Novo Valor em Sats</string>
|
||||
<string name="add">Adicionar</string>
|
||||
<string name="replying_to">"respondendo para "</string>
|
||||
<string name="and">" e "</string>
|
||||
<string name="in_channel">"no canal "</string>
|
||||
<string name="profile_banner">Banner do perfil</string>
|
||||
<string name="following">" Seguindo"</string>
|
||||
<string name="followers">" Seguidores"</string>
|
||||
<string name="profile">Perfil</string>
|
||||
<string name="security_filters">Filtros de segurança</string>
|
||||
<string name="log_out">Sair</string>
|
||||
<string name="show_more">Mostar mais</string>
|
||||
<string name="lightning_invoice">Fatura Lightning</string>
|
||||
<string name="pay">Pagar</string>
|
||||
<string name="lightning_tips">Doação Lightning</string>
|
||||
<string name="note_to_receiver">Nota ao Destinatário</string>
|
||||
<string name="thank_you_so_much">Obrigado!</string>
|
||||
<string name="amount_in_sats">Quantidade em Sats</string>
|
||||
<string name="send_sats">Enviar Sats</string>
|
||||
<string name="never_translate_from">"Nunca traduzir de "</string>
|
||||
<string name="error_parsing_preview_for">Erro ao analisar a visualização para %1$s: %2$s</string>
|
||||
<string name="preview_card_image_for">Visualize a imagem do cartão para %1$s</string>
|
||||
<string name="new_channel">Novo Canal</string>
|
||||
<string name="can_t_find_out_the_content_type">Não é possível descobrir o tipo de conteúdo</string>
|
||||
<string name="can_t_insert_the_new_content">Não é possível inserir o novo conteúdo</string>
|
||||
<string name="can_t_open_the_content_output_stream">Não é possível abrir o fluxo de saída de conteúdo</string>
|
||||
<string name="can_t_open_the_image_input_stream">Não é possível abrir o fluxo de entrada de imagem</string>
|
||||
<string name="there_must_be_an_uploaded_image_url_in_the_response">Deve haver um URL de imagem na resposta</string>
|
||||
<string name="channel_name">Nome do Canal</string>
|
||||
<string name="my_awesome_group">Meu grupo</string>
|
||||
<string name="picture_url">Url da foto</string>
|
||||
<string name="description">Descrição</string>
|
||||
<string name="about_us">"Sobre nós.. "</string>
|
||||
<string name="what_s_on_your_mind">O que você está pensando?</string>
|
||||
<string name="post">Salvar</string>
|
||||
<string name="save">Salvar</string>
|
||||
<string name="create">Criar</string>
|
||||
<string name="cancel">Cancelar</string>
|
||||
<string name="failed_to_upload_the_image">Falha ao enviar imagem</string>
|
||||
<string name="relay_address">Endereço do Relay</string>
|
||||
<string name="posts">Posts</string>
|
||||
<string name="errors">Erros</string>
|
||||
<string name="home_feed">Feed principal</string>
|
||||
<string name="private_message_feed">Feed de mensagens privadas</string>
|
||||
<string name="public_chat_feed">Feed Chat Público</string>
|
||||
<string name="global_feed">Feed Global</string>
|
||||
<string name="add_a_relay">Adicionar um Relay</string>
|
||||
<string name="display_name">Nome de Exibição</string>
|
||||
<string name="my_display_name">Meu nome de exibição</string>
|
||||
<string name="username">Usuário</string>
|
||||
<string name="my_username">Meu usuário</string>
|
||||
<string name="about_me">Sobre mim</string>
|
||||
<string name="avatar_url">URL da foto de perfil</string>
|
||||
<string name="banner_url">URL do Banner</string>
|
||||
<string name="website_url">URL do Site</string>
|
||||
<string name="ln_address">Endereço LN</string>
|
||||
<string name="ln_url_outdated">LN URL (desatualizado)</string>
|
||||
<string name="image_saved_to_the_gallery">Imagem salva para a galeria</string>
|
||||
<string name="failed_to_save_the_image">Falha ao salvar a imagem</string>
|
||||
<string name="upload_image">Enviar Imagem</string>
|
||||
<string name="uploading">Enviando...</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Usuário não tem um endereço lightning configurado para receber sats</string>
|
||||
<string name="reply_here">"responda aqui.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copia o ID do canal (note) para compartilhar</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Copiar ID do canal (Note)</string>
|
||||
<string name="edits_the_channel_metadata">Editar os dados do canal</string>
|
||||
<string name="join">Entrar</string>
|
||||
<string name="known">Conhecidos</string>
|
||||
<string name="new_requests">Novas Solicitações</string>
|
||||
<string name="blocked_users">Usuários</string>
|
||||
<string name="new_threads">Novos Tópicos</string>
|
||||
<string name="conversations">Conversas</string>
|
||||
<string name="notes">Notas</string>
|
||||
<string name="replies">Respostas</string>
|
||||
<string name="follows">Seguindo</string>
|
||||
<string name="reports">Denúncias</string>
|
||||
<string name="more_options">Mais opções</string>
|
||||
<string name="relays">" Relays"</string>
|
||||
<string name="website">Site</string>
|
||||
<string name="lightning_address">Endereço Lightning</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copiar a chave Nsec (sua senha) para backup</string>
|
||||
<string name="copy_private_key_to_the_clipboard">Copiar Chave Privada</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copia a chave public para ser compartilhada</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">Copiar chave pública (NPub)</string>
|
||||
<string name="send_a_direct_message">Enviar uma mensagem direta</string>
|
||||
<string name="edits_the_user_s_metadata">Muda os dados do usuário</string>
|
||||
<string name="follow">Seguir</string>
|
||||
<string name="unblock">Desbloquear</string>
|
||||
<string name="copy_user_id">Copiar ID do usuário</string>
|
||||
<string name="unblock_user">Desbloquear Usuário</string>
|
||||
<string name="npub_hex_username">"npub, hex, usuário "</string>
|
||||
<string name="clear">Limpar</string>
|
||||
<string name="app_logo">Logo do aplicativo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex chave privada</string>
|
||||
<string name="show_password">Mostrar senha</string>
|
||||
<string name="hide_password">Esconder senha</string>
|
||||
<string name="invalid_key">Chave inválida</string>
|
||||
<string name="i_accept_the">"Eu aceito os "</string>
|
||||
<string name="terms_of_use">termos de uso</string>
|
||||
<string name="acceptance_of_terms_is_required">É necessário aceitar os termos de uso</string>
|
||||
<string name="key_is_required">Chave é obrigatória</string>
|
||||
<string name="login">Entrar</string>
|
||||
<string name="generate_a_new_key">Gerar uma nova chave</string>
|
||||
<string name="loading_feed">Carregando feed</string>
|
||||
<string name="error_loading_replies">Erro ao carregar respostas</string>
|
||||
<string name="try_again">Tente novamente</string>
|
||||
<string name="feed_is_empty">Feed está vazio</string>
|
||||
<string name="refresh">Atualizar</string>
|
||||
<string name="created">criado</string>
|
||||
<string name="with_description_of">com descrição de</string>
|
||||
<string name="and_picture">e foto</string>
|
||||
<string name="changed_chat_name_to">mudou nome do canal para</string>
|
||||
<string name="description_to">descrição para</string>
|
||||
<string name="and_picture_to">e foto para</string>
|
||||
<string name="leave">Sair</string>
|
||||
<string name="unfollow">Remover seguidor</string>
|
||||
<string name="channel_created">Canal criado</string>
|
||||
<string name="channel_information_changed_to">Informação do canal mudou para</string>
|
||||
<string name="public_chat">Chat público</string>
|
||||
<string name="posts_received">postagens recebidas</string>
|
||||
<string name="remove">Remover</string>
|
||||
<string name="auto">"Automaticamente "</string>
|
||||
<string name="translated_from">"traduzido de "</string>
|
||||
<string name="to">para</string>
|
||||
<string name="show_in">Mostrar em</string>
|
||||
<string name="first">primeiro</string>
|
||||
<string name="always_translate_to">"Sempre traduzir para "</string>
|
||||
<string name="never">nunca</string>
|
||||
<string name="now">agora</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Nudez</string>
|
||||
<string name="profanity_hateful_speech">Palavrões / Discurso de ódio</string>
|
||||
<string name="report_hateful_speech">Denunciar discurso de ódio</string>
|
||||
<string name="report_nudity_porn">Denunciar nudez / pornografia</string>
|
||||
<string name="others">outros</string>
|
||||
</resources>
|
||||
@@ -1,4 +1,180 @@
|
||||
<resources>
|
||||
<string name="app_name_release">Amethyst</string>
|
||||
<string name="app_name_debug">Amethyst Debug</string>
|
||||
<string name="app_name_release" translatable="false">Amethyst</string>
|
||||
<string name="app_name_debug" translatable="false">Amethyst Debug</string>
|
||||
<string name="point_to_the_qr_code">Point to the QR Code</string>
|
||||
<string name="show_qr">Show QR</string>
|
||||
<string name="profile_image">Profile Image</string>
|
||||
<string name="scan_qr">Scan QR</string>
|
||||
<string name="show_anyway">Show Anyway</string>
|
||||
<string name="post_was_flagged_as_inappropriate_by">Post was flagged as inappropriate by</string>
|
||||
<string name="post_not_found">post not found</string>
|
||||
<string name="channel_image">Channel Image</string>
|
||||
<string name="referenced_event_not_found">Referenced event not found</string>
|
||||
<string name="could_not_decrypt_the_message">Could Not decrypt the message</string>
|
||||
<string name="group_picture">Group Picture</string>
|
||||
<string name="explicit_content">Explicit Content</string>
|
||||
<string name="spam">Spam</string>
|
||||
<string name="impersonation">Impersonation</string>
|
||||
<string name="illegal_behavior">Illegal Behavior</string>
|
||||
<string name="unknown">Unknown</string>
|
||||
<string name="relay_icon">Relay Icon</string>
|
||||
<string name="unknown_author">Unknown Author</string>
|
||||
<string name="copy_text">Copy Text</string>
|
||||
<string name="copy_user_pubkey">Copy User PubKey</string>
|
||||
<string name="copy_note_id">Copy Note ID</string>
|
||||
<string name="broadcast">Broadcast</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>
|
||||
<string name="report_explicit_content">Report Explicit Content</string>
|
||||
<string name="report_illegal_behaviour">Report Illegal Behaviour</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_reply">Login with a Private key to be able to reply</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_boost_posts">Login with a Private key to be able to boost posts</string>
|
||||
<string name="login_with_a_private_key_to_like_posts">Login with a Private key to like Posts</string>
|
||||
<string name="no_zap_amount_setup_long_press_to_change">No Zap Amount Setup. Long Press to change</string>
|
||||
<string name="login_with_a_private_key_to_be_able_to_send_zaps">Login with a Private key to be able to send Zaps</string>
|
||||
<string name="zaps">Zaps</string>
|
||||
<string name="view_count">View count</string>
|
||||
<string name="boost">Boost</string>
|
||||
<string name="quote">Quote</string>
|
||||
<string name="new_amount_in_sats">New Amount in Sats</string>
|
||||
<string name="add">Add</string>
|
||||
<string name="replying_to">"replying to "</string>
|
||||
<string name="and">" and "</string>
|
||||
<string name="in_channel">"in channel "</string>
|
||||
<string name="profile_banner">Profile Banner</string>
|
||||
<string name="following">" Following"</string>
|
||||
<string name="followers">" Followers"</string>
|
||||
<string name="profile">Profile</string>
|
||||
<string name="security_filters">Security Filters</string>
|
||||
<string name="log_out">Log out</string>
|
||||
<string name="show_more">Show More</string>
|
||||
<string name="lightning_invoice">Lightning Invoice</string>
|
||||
<string name="pay">Pay</string>
|
||||
<string name="lightning_tips">Lightning Tips</string>
|
||||
<string name="note_to_receiver">Note to Receiver</string>
|
||||
<string name="thank_you_so_much">Thank you so much!</string>
|
||||
<string name="amount_in_sats">Amount in Sats</string>
|
||||
<string name="send_sats">Send Sats</string>
|
||||
<string name="never_translate_from">"Never translate from "</string>
|
||||
<string name="error_parsing_preview_for">"Error parsing preview for %1$s : %2$s"</string>
|
||||
<string name="preview_card_image_for">"Preview Card Image for %1$s"</string>
|
||||
<string name="new_channel">New Channel</string>
|
||||
<string name="can_t_find_out_the_content_type">Can\'t find out the content type</string>
|
||||
<string name="can_t_insert_the_new_content">Can\'t insert the new content</string>
|
||||
<string name="can_t_open_the_content_output_stream">Can\'t open the content output stream</string>
|
||||
<string name="can_t_open_the_image_input_stream">Can\'t open the image input stream</string>
|
||||
<string name="there_must_be_an_uploaded_image_url_in_the_response">There must be an uploaded image URL in the response</string>
|
||||
<string name="channel_name">Channel Name</string>
|
||||
<string name="my_awesome_group">My Awesome Group</string>
|
||||
<string name="picture_url">Picture Url</string>
|
||||
<string name="description">Description</string>
|
||||
<string name="about_us">"About us.. "</string>
|
||||
<string name="what_s_on_your_mind">What\'s on your mind?</string>
|
||||
<string name="post">Post</string>
|
||||
<string name="save">Save</string>
|
||||
<string name="create">Create</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="failed_to_upload_the_image">Failed to upload the image</string>
|
||||
<string name="relay_address">Relay Address</string>
|
||||
<string name="posts">Posts</string>
|
||||
<string name="errors">Errors</string>
|
||||
<string name="home_feed">Home Feed</string>
|
||||
<string name="private_message_feed">Private Message Feed</string>
|
||||
<string name="public_chat_feed">Public Chat Feed</string>
|
||||
<string name="global_feed">Global Feed</string>
|
||||
<string name="add_a_relay">Add a Relay</string>
|
||||
<string name="display_name">Display Name</string>
|
||||
<string name="my_display_name">My display name</string>
|
||||
<string name="username">Username</string>
|
||||
<string name="my_username">My username</string>
|
||||
<string name="about_me">About me</string>
|
||||
<string name="avatar_url">Avatar URL</string>
|
||||
<string name="banner_url">Banner URL</string>
|
||||
<string name="website_url">Website URL</string>
|
||||
<string name="ln_address">LN Address</string>
|
||||
<string name="ln_url_outdated">LN URL (outdated)</string>
|
||||
<string name="image_saved_to_the_gallery">Image saved to the gallery</string>
|
||||
<string name="failed_to_save_the_image">Failed to save the image</string>
|
||||
<string name="upload_image">Upload Image</string>
|
||||
<string name="uploading">Uploading…</string>
|
||||
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">User does not have a lightning address setup to receive sats</string>
|
||||
<string name="reply_here">"reply here.. "</string>
|
||||
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Copies the Note ID to the clipboard for sharing</string>
|
||||
<string name="copy_channel_id_note_to_the_clipboard">Copy Channel ID (Note) to the Clipboard</string>
|
||||
<string name="edits_the_channel_metadata">Edits the Channel Metadata</string>
|
||||
<string name="join">Join</string>
|
||||
<string name="known">Known</string>
|
||||
<string name="new_requests">New Requests</string>
|
||||
<string name="blocked_users">Blocked Users</string>
|
||||
<string name="new_threads">New Threads</string>
|
||||
<string name="conversations">Conversations</string>
|
||||
<string name="notes">Notes</string>
|
||||
<string name="replies">Replies</string>
|
||||
<string name="follows">"Follows"</string>
|
||||
<string name="reports">"Reports"</string>
|
||||
<string name="more_options">More Options</string>
|
||||
<string name="relays">" Relays"</string>
|
||||
<string name="website">Website</string>
|
||||
<string name="lightning_address">Lightning Address</string>
|
||||
<string name="copies_the_nsec_id_your_password_to_the_clipboard_for_backup">Copies the Nsec ID (your password) to the clipboard for backup</string>
|
||||
<string name="copy_private_key_to_the_clipboard">Copy Private Key to the Clipboard</string>
|
||||
<string name="copies_the_public_key_to_the_clipboard_for_sharing">Copies the public key to the clipboard for sharing</string>
|
||||
<string name="copy_public_key_npub_to_the_clipboard">Copy Public Key (NPub) to the Clipboard</string>
|
||||
<string name="send_a_direct_message">Send a Direct Message</string>
|
||||
<string name="edits_the_user_s_metadata">Edits the User\'s Metadata</string>
|
||||
<string name="follow">Follow</string>
|
||||
<string name="unblock">Unblock</string>
|
||||
<string name="copy_user_id">Copy User ID</string>
|
||||
<string name="unblock_user">Unblock User</string>
|
||||
<string name="npub_hex_username">"npub, hex, username "</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="app_logo">App Logo</string>
|
||||
<string name="nsec_npub_hex_private_key">nsec / npub / hex private key</string>
|
||||
<string name="show_password">Show Password</string>
|
||||
<string name="hide_password">Hide Password</string>
|
||||
<string name="invalid_key">Invalid key</string>
|
||||
<string name="i_accept_the">"I accept the "</string>
|
||||
<string name="terms_of_use">terms of use</string>
|
||||
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
||||
<string name="key_is_required">Key is required</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="generate_a_new_key">Generate a new key</string>
|
||||
<string name="loading_feed">Loading feed</string>
|
||||
<string name="error_loading_replies">"Error loading replies: "</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="feed_is_empty">Feed is empty.</string>
|
||||
<string name="refresh">Refresh</string>
|
||||
<string name="created">created</string>
|
||||
<string name="with_description_of">with description of</string>
|
||||
<string name="and_picture">and picture</string>
|
||||
<string name="changed_chat_name_to">changed chat name to</string>
|
||||
<string name="description_to">description to</string>
|
||||
<string name="and_picture_to">and picture to</string>
|
||||
<string name="leave">Leave</string>
|
||||
<string name="unfollow">Unfollow</string>
|
||||
<string name="channel_created">Channel created</string>
|
||||
<string name="channel_information_changed_to">"Channel Information changed to"</string>
|
||||
<string name="public_chat">Public Chat</string>
|
||||
<string name="posts_received">posts received</string>
|
||||
<string name="remove">Remove</string>
|
||||
<string name="sats" translatable="false">sats</string>
|
||||
<string name="auto">Auto</string>
|
||||
<string name="translated_from">translated from</string>
|
||||
<string name="to">to</string>
|
||||
<string name="show_in">Show in</string>
|
||||
<string name="first">first</string>
|
||||
<string name="always_translate_to">Always translate to</string>
|
||||
<string name="nip_05" translatable="false">NIP-05</string>
|
||||
<string name="lnurl" translatable="false">LNURL...</string>
|
||||
<string name="never">never</string>
|
||||
<string name="now">now</string>
|
||||
<string name="h">h</string>
|
||||
<string name="m">m</string>
|
||||
<string name="d">d</string>
|
||||
<string name="nudity">Nudity</string>
|
||||
<string name="profanity_hateful_speech">Profanity / Hateful speech</string>
|
||||
<string name="report_hateful_speech">Report Hateful speech</string>
|
||||
<string name="report_nudity_porn">Report Nudity / Porn</string>
|
||||
<string name="others">others</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user