Merge branch 'main' into main
This commit is contained in:
+8
-3
@@ -2,6 +2,7 @@ plugins {
|
|||||||
id 'com.android.application'
|
id 'com.android.application'
|
||||||
id 'org.jetbrains.kotlin.android'
|
id 'org.jetbrains.kotlin.android'
|
||||||
id 'org.jlleitschuh.gradle.ktlint' version "11.3.1"
|
id 'org.jlleitschuh.gradle.ktlint' version "11.3.1"
|
||||||
|
id 'com.google.gms.google-services'
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -12,8 +13,8 @@ android {
|
|||||||
applicationId "com.vitorpamplona.amethyst"
|
applicationId "com.vitorpamplona.amethyst"
|
||||||
minSdk 26
|
minSdk 26
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 150
|
versionCode 152
|
||||||
versionName "0.43.2"
|
versionName "0.45.0"
|
||||||
|
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
@@ -86,7 +87,7 @@ android {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.core:core-ktx:1.10.0'
|
implementation 'androidx.core:core-ktx:1.10.0'
|
||||||
implementation 'androidx.activity:activity-compose:1.7.0'
|
implementation 'androidx.activity:activity-compose:1.7.1'
|
||||||
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
implementation "androidx.compose.ui:ui:$compose_ui_version"
|
||||||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
|
implementation "androidx.compose.ui:ui-tooling-preview:$compose_ui_version"
|
||||||
|
|
||||||
@@ -172,6 +173,10 @@ dependencies {
|
|||||||
// Google services model the translate text
|
// Google services model the translate text
|
||||||
playImplementation 'com.google.mlkit:translate:17.0.1'
|
playImplementation 'com.google.mlkit:translate:17.0.1'
|
||||||
|
|
||||||
|
// PushNotifications
|
||||||
|
playImplementation platform('com.google.firebase:firebase-bom:32.0.0')
|
||||||
|
playImplementation 'com.google.firebase:firebase-messaging-ktx'
|
||||||
|
|
||||||
// Automatic memory leak detection
|
// Automatic memory leak detection
|
||||||
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "768341258853",
|
||||||
|
"project_id": "amethyst-3057a",
|
||||||
|
"storage_bucket": "amethyst-3057a.appspot.com"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:768341258853:android:5d07c35a37b24ff36b8c8c",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.vitorpamplona.amethyst"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:768341258853:android:e0200680f552484d6b8c8c",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.vitorpamplona.amethyst.debug"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyB7ZxgdpgrN6R223HCFdfv4ulP8Egp7trE"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "768341258853-6um8ig59qstvio60gfo60fe5e45lnqqe.apps.googleusercontent.com",
|
||||||
|
"client_type": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.vitorpamplona.amethyst
|
||||||
|
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class CitationTests {
|
||||||
|
val json = """
|
||||||
|
{
|
||||||
|
"content": "Astral:\n\nhttps://void.cat/d/A5Fba5B1bcxwEmeyoD9nBs.webp\n\nIris:\n\nhttps://void.cat/d/44hTcVvhRps6xYYs99QsqA.webp\n\nSnort:\n\nhttps://void.cat/d/4nJD5TRePuQChM5tzteYbU.webp\n\nAmethyst agrees with Astral which I suspect are both wrong. nostr:npub13sx6fp3pxq5rl70x0kyfmunyzaa9pzt5utltjm0p8xqyafndv95q3saapa nostr:npub1v0lxxxxutpvrelsksy8cdhgfux9l6a42hsj2qzquu2zk7vc9qnkszrqj49 nostr:npub1g53mukxnjkcmr94fhryzkqutdz2ukq4ks0gvy5af25rgmwsl4ngq43drvk nostr:npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z ",
|
||||||
|
"created_at": 1683596206,
|
||||||
|
"id": "98b574c3527f0ffb30b7271084e3f07480733c7289f8de424d29eae82e36c758",
|
||||||
|
"kind": 1,
|
||||||
|
"pubkey": "46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d",
|
||||||
|
"sig": "4aa5264965018fa12a326686ad3d3bd8beae3218dcc83689b19ca1e6baeb791531943c15363aa6707c7c0c8b2d601deca1f20c32078b2872d356cdca03b04cce",
|
||||||
|
"tags": [
|
||||||
|
[
|
||||||
|
"e",
|
||||||
|
"27ac621d7dc4a932e1a79f984308e7d20656dd6fddb2ce9cdfcb6a67b9a7bcc3",
|
||||||
|
"",
|
||||||
|
"root"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"e",
|
||||||
|
"be7245af96210a0dd048cab4ad38e52dbd6c09a53ea21a7edb6be8898e5727cc",
|
||||||
|
"",
|
||||||
|
"reply"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"3f770d65d3a764a9c5cb503ae123e62ec7598ad035d836e2a810f3877a745b24"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"ec4d241c334311b3a304433ee3442be29d0e88e7ec19b85edf2bba29b93565e2"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"0fe0b18b4dbf0e0aa40fcd47209b2a49b3431fc453b460efcf45ca0bd16bd6ac"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"p",
|
||||||
|
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"seenOn": [
|
||||||
|
"wss://nostr.wine/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun parseEvent() {
|
||||||
|
val event = Event.fromJson(json, true) as TextNoteEvent
|
||||||
|
|
||||||
|
val expectedCitations = setOf(
|
||||||
|
"8c0da4862130283ff9e67d889df264177a508974e2feb96de139804ea66d6168",
|
||||||
|
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
|
||||||
|
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
|
||||||
|
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(expectedCitations, event.citedUsers())
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import com.vitorpamplona.amethyst.AccountInfo
|
||||||
|
|
||||||
|
class PushNotificationUtils {
|
||||||
|
fun init(accounts: List<AccountInfo>) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,7 @@
|
|||||||
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
android:name="com.journeyapps.barcodescanner.CaptureActivity"
|
||||||
android:screenOrientation="fullSensor"
|
android:screenOrientation="fullSensor"
|
||||||
tools:replace="screenOrientation" />
|
tools:replace="screenOrientation" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -51,6 +51,7 @@ private object PrefKeys {
|
|||||||
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
const val DEFAULT_FILE_SERVER = "defaultFileServer"
|
||||||
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
const val DEFAULT_HOME_FOLLOW_LIST = "defaultHomeFollowList"
|
||||||
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
|
const val DEFAULT_STORIES_FOLLOW_LIST = "defaultStoriesFollowList"
|
||||||
|
const val DEFAULT_NOTIFICATION_FOLLOW_LIST = "defaultNotificationFollowList"
|
||||||
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
const val ZAP_PAYMENT_REQUEST_SERVER = "zapPaymentServer"
|
||||||
const val LATEST_CONTACT_LIST = "latestContactList"
|
const val LATEST_CONTACT_LIST = "latestContactList"
|
||||||
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
const val HIDE_DELETE_REQUEST_DIALOG = "hide_delete_request_dialog"
|
||||||
@@ -206,6 +207,7 @@ object LocalPreferences {
|
|||||||
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
|
putString(PrefKeys.DEFAULT_FILE_SERVER, gson.toJson(account.defaultFileServer))
|
||||||
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
|
putString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, account.defaultHomeFollowList)
|
||||||
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList)
|
putString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, account.defaultStoriesFollowList)
|
||||||
|
putString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, account.defaultNotificationFollowList)
|
||||||
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
putString(PrefKeys.ZAP_PAYMENT_REQUEST_SERVER, gson.toJson(account.zapPaymentRequest))
|
||||||
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
putString(PrefKeys.LATEST_CONTACT_LIST, Event.gson.toJson(account.backupContactList))
|
||||||
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
putBoolean(PrefKeys.HIDE_DELETE_REQUEST_DIALOG, account.hideDeleteRequestDialog)
|
||||||
@@ -216,7 +218,13 @@ object LocalPreferences {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun loadFromEncryptedStorage(): Account? {
|
fun loadFromEncryptedStorage(): Account? {
|
||||||
encryptedPreferences(currentAccount()).apply {
|
val acc = loadFromEncryptedStorage(currentAccount())
|
||||||
|
acc?.registerObservers()
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadFromEncryptedStorage(npub: String?): Account? {
|
||||||
|
encryptedPreferences(npub).apply {
|
||||||
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
|
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
|
||||||
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
||||||
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
|
val followingChannels = getStringSet(PrefKeys.FOLLOWING_CHANNELS, null) ?: setOf()
|
||||||
@@ -230,6 +238,7 @@ object LocalPreferences {
|
|||||||
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
val translateTo = getString(PrefKeys.TRANSLATE_TO, null) ?: Locale.getDefault().language
|
||||||
val defaultHomeFollowList = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
val defaultHomeFollowList = getString(PrefKeys.DEFAULT_HOME_FOLLOW_LIST, null) ?: KIND3_FOLLOWS
|
||||||
val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
val defaultStoriesFollowList = getString(PrefKeys.DEFAULT_STORIES_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
|
val defaultNotificationFollowList = getString(PrefKeys.DEFAULT_NOTIFICATION_FOLLOW_LIST, null) ?: GLOBAL_FOLLOWS
|
||||||
|
|
||||||
val zapAmountChoices = gson.fromJson(
|
val zapAmountChoices = gson.fromJson(
|
||||||
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
getString(PrefKeys.ZAP_AMOUNTS, "[]"),
|
||||||
@@ -296,6 +305,7 @@ object LocalPreferences {
|
|||||||
defaultFileServer,
|
defaultFileServer,
|
||||||
defaultHomeFollowList,
|
defaultHomeFollowList,
|
||||||
defaultStoriesFollowList,
|
defaultStoriesFollowList,
|
||||||
|
defaultNotificationFollowList,
|
||||||
zapPaymentRequestServer,
|
zapPaymentRequestServer,
|
||||||
hideDeleteRequestDialog,
|
hideDeleteRequestDialog,
|
||||||
hideBlockAlertDialog,
|
hideBlockAlertDialog,
|
||||||
|
|||||||
@@ -38,19 +38,15 @@ object NotificationCache {
|
|||||||
|
|
||||||
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
|
class NotificationLiveData(val cache: NotificationCache) : LiveData<NotificationState>(NotificationState(cache)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Main) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||||
if (hasActiveObservers()) {
|
if (hasActiveObservers()) {
|
||||||
refresh()
|
postValue(NotificationState(cache))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
postValue(NotificationState(cache))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationState(val cache: NotificationCache)
|
class NotificationState(val cache: NotificationCache)
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class Account(
|
|||||||
var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR,
|
var defaultFileServer: ServersAvailable = ServersAvailable.IMGUR,
|
||||||
var defaultHomeFollowList: String = KIND3_FOLLOWS,
|
var defaultHomeFollowList: String = KIND3_FOLLOWS,
|
||||||
var defaultStoriesFollowList: String = GLOBAL_FOLLOWS,
|
var defaultStoriesFollowList: String = GLOBAL_FOLLOWS,
|
||||||
|
var defaultNotificationFollowList: String = GLOBAL_FOLLOWS,
|
||||||
var zapPaymentRequest: Nip47URI? = null,
|
var zapPaymentRequest: Nip47URI? = null,
|
||||||
var hideDeleteRequestDialog: Boolean = false,
|
var hideDeleteRequestDialog: Boolean = false,
|
||||||
var hideBlockAlertDialog: Boolean = false,
|
var hideBlockAlertDialog: Boolean = false,
|
||||||
@@ -754,6 +755,12 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeDefaultNotificationFollowList(name: String) {
|
||||||
|
defaultNotificationFollowList = name
|
||||||
|
live.invalidateData()
|
||||||
|
saveable.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
fun changeZapAmounts(newAmounts: List<Long>) {
|
fun changeZapAmounts(newAmounts: List<Long>) {
|
||||||
zapAmountChoices = newAmounts
|
zapAmountChoices = newAmounts
|
||||||
live.invalidateData()
|
live.invalidateData()
|
||||||
@@ -1034,14 +1041,7 @@ class Account(
|
|||||||
saveable.invalidateData()
|
saveable.invalidateData()
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
fun registerObservers() {
|
||||||
backupContactList?.let {
|
|
||||||
println("Loading saved contacts ${it.toJson()}")
|
|
||||||
if (userProfile().latestContactList == null) {
|
|
||||||
LocalCache.consume(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Observes relays to restart connections
|
// Observes relays to restart connections
|
||||||
userProfile().live().relays.observeForever {
|
userProfile().live().relays.observeForever {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
@@ -1068,6 +1068,15 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
backupContactList?.let {
|
||||||
|
println("Loading saved contacts ${it.toJson()}")
|
||||||
|
if (userProfile().latestContactList == null) {
|
||||||
|
LocalCache.consume(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountLiveData(private val account: Account) : LiveData<AccountState>(AccountState(account)) {
|
class AccountLiveData(private val account: Account) : LiveData<AccountState>(AccountState(account)) {
|
||||||
|
|||||||
@@ -55,19 +55,15 @@ class AntiSpamFilter {
|
|||||||
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
class AntiSpamLiveData(val cache: AntiSpamFilter) : LiveData<AntiSpamState>(AntiSpamState(cache)) {
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Main) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||||
if (hasActiveObservers()) {
|
if (hasActiveObservers()) {
|
||||||
refresh()
|
postValue(AntiSpamState(cache))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun invalidateData() {
|
fun invalidateData() {
|
||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
postValue(AntiSpamState(cache))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AntiSpamState(val cache: AntiSpamFilter)
|
class AntiSpamState(val cache: AntiSpamFilter)
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ class Channel(val idHex: String) {
|
|||||||
|
|
||||||
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
|
class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelState(channel)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Main) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||||
if (hasActiveObservers()) {
|
if (hasActiveObservers()) {
|
||||||
refresh()
|
postValue(ChannelState(channel))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,10 +84,6 @@ class ChannelLiveData(val channel: Channel) : LiveData<ChannelState>(ChannelStat
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
postValue(ChannelState(channel))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
super.onActive()
|
super.onActive()
|
||||||
NostrSingleChannelDataSource.add(channel.idHex)
|
NostrSingleChannelDataSource.add(channel.idHex)
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: PrivateDmEvent, relay: Relay?) {
|
fun consume(event: PrivateDmEvent, relay: Relay?): Note {
|
||||||
val note = getOrCreateNote(event.id)
|
val note = getOrCreateNote(event.id)
|
||||||
val author = getOrCreateUser(event.pubKey)
|
val author = getOrCreateUser(event.pubKey)
|
||||||
|
|
||||||
@@ -357,7 +357,7 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return note
|
||||||
|
|
||||||
val recipient = event.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }
|
val recipient = event.verifiedRecipientPubKey()?.let { getOrCreateUser(it) }
|
||||||
|
|
||||||
@@ -374,6 +374,8 @@ object LocalCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshObservers(note)
|
refreshObservers(note)
|
||||||
|
|
||||||
|
return note
|
||||||
}
|
}
|
||||||
|
|
||||||
fun consume(event: DeletionEvent) {
|
fun consume(event: DeletionEvent) {
|
||||||
@@ -736,20 +738,24 @@ object LocalCache {
|
|||||||
note.addRelay(relay)
|
note.addRelay(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val file = File(Amethyst.instance.applicationContext.externalCacheDir, "NIP95")
|
||||||
|
|
||||||
|
if (!file.exists()) {
|
||||||
|
try {
|
||||||
|
val cachePath = File(Amethyst.instance.applicationContext.externalCacheDir, "NIP95")
|
||||||
|
cachePath.mkdirs()
|
||||||
|
val stream = FileOutputStream(File(cachePath, event.id))
|
||||||
|
stream.write(event.decode())
|
||||||
|
stream.close()
|
||||||
|
Log.e("EventLogger", "Saved to disk as ${File(cachePath, event.id).toUri()}")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e("FileSotrageEvent", "FileStorageEvent save to disk error: " + event.id, e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Already processed this event.
|
// Already processed this event.
|
||||||
if (note.event != null) return
|
if (note.event != null) return
|
||||||
|
|
||||||
try {
|
|
||||||
val cachePath = File(Amethyst.instance.applicationContext.externalCacheDir, "NIP95")
|
|
||||||
cachePath.mkdirs()
|
|
||||||
val stream = FileOutputStream(File(cachePath, event.id))
|
|
||||||
stream.write(event.decode())
|
|
||||||
stream.close()
|
|
||||||
Log.e("EventLogger", "Saved to disk as ${File(cachePath, event.id).toUri()}")
|
|
||||||
} catch (e: IOException) {
|
|
||||||
Log.e("FileSotrageEvent", "FileStorageEvent save to disk error: " + event.id, e)
|
|
||||||
}
|
|
||||||
|
|
||||||
// this is an invalid event. But we don't need to keep the data in memory.
|
// this is an invalid event. But we don't need to keep the data in memory.
|
||||||
val eventNoData = FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig)
|
val eventNoData = FileStorageEvent(event.id, event.pubKey, event.createdAt, event.tags, "", event.sig)
|
||||||
|
|
||||||
@@ -907,12 +913,66 @@ object LocalCache {
|
|||||||
private fun refreshObservers(newNote: Note) {
|
private fun refreshObservers(newNote: Note) {
|
||||||
live.invalidateData(newNote)
|
live.invalidateData(newNote)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun consume(event: Event, relay: Relay?) {
|
||||||
|
if (!event.hasValidSignature()) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
when (event) {
|
||||||
|
is BadgeAwardEvent -> consume(event)
|
||||||
|
is BadgeDefinitionEvent -> consume(event)
|
||||||
|
is BadgeProfilesEvent -> consume(event)
|
||||||
|
is BookmarkListEvent -> consume(event)
|
||||||
|
is ChannelCreateEvent -> consume(event)
|
||||||
|
is ChannelHideMessageEvent -> consume(event)
|
||||||
|
is ChannelMessageEvent -> consume(event, relay)
|
||||||
|
is ChannelMetadataEvent -> consume(event)
|
||||||
|
is ChannelMuteUserEvent -> consume(event)
|
||||||
|
is ContactListEvent -> consume(event)
|
||||||
|
is DeletionEvent -> consume(event)
|
||||||
|
|
||||||
|
is FileHeaderEvent -> consume(event, relay)
|
||||||
|
is FileStorageEvent -> consume(event, relay)
|
||||||
|
is FileStorageHeaderEvent -> consume(event, relay)
|
||||||
|
is HighlightEvent -> consume(event, relay)
|
||||||
|
is LnZapEvent -> {
|
||||||
|
event.zapRequest?.let {
|
||||||
|
consume(it, relay)
|
||||||
|
}
|
||||||
|
consume(event)
|
||||||
|
}
|
||||||
|
is LnZapRequestEvent -> consume(event)
|
||||||
|
is LnZapPaymentRequestEvent -> consume(event)
|
||||||
|
is LnZapPaymentResponseEvent -> consume(event)
|
||||||
|
is LongTextNoteEvent -> consume(event, relay)
|
||||||
|
is MetadataEvent -> consume(event)
|
||||||
|
is PrivateDmEvent -> consume(event, relay)
|
||||||
|
is PeopleListEvent -> consume(event)
|
||||||
|
is ReactionEvent -> consume(event)
|
||||||
|
is RecommendRelayEvent -> consume(event)
|
||||||
|
is ReportEvent -> consume(event, relay)
|
||||||
|
is RepostEvent -> {
|
||||||
|
event.containedPost()?.let {
|
||||||
|
consume(it, relay)
|
||||||
|
}
|
||||||
|
consume(event)
|
||||||
|
}
|
||||||
|
is TextNoteEvent -> consume(event, relay)
|
||||||
|
is PollNoteEvent -> consume(event, relay)
|
||||||
|
else -> {
|
||||||
|
Log.w("Event Not Supported", event.toJson())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocalCacheLiveData : LiveData<Set<Note>>(setOf<Note>()) {
|
class LocalCacheLiveData : LiveData<Set<Note>>(setOf<Note>()) {
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledInsert<Note>(300, Dispatchers.Main)
|
private val bundler = BundledInsert<Note>(300, Dispatchers.IO)
|
||||||
|
|
||||||
fun invalidateData(newNote: Note) {
|
fun invalidateData(newNote: Note) {
|
||||||
bundler.invalidateList(newNote) { bundledNewNotes ->
|
bundler.invalidateList(newNote) { bundledNewNotes ->
|
||||||
|
|||||||
@@ -410,9 +410,9 @@ class NoteLiveSet(u: Note) {
|
|||||||
|
|
||||||
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Main) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||||
if (hasActiveObservers()) {
|
if (hasActiveObservers()) {
|
||||||
refresh()
|
postValue(NoteState(note))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,10 +420,6 @@ class NoteLiveData(val note: Note) : LiveData<NoteState>(NoteState(note)) {
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
postValue(NoteState(note))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
super.onActive()
|
super.onActive()
|
||||||
if (note is AddressableNote) {
|
if (note is AddressableNote) {
|
||||||
|
|||||||
@@ -396,9 +396,9 @@ class UserMetadata {
|
|||||||
|
|
||||||
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
||||||
// Refreshes observers in batches.
|
// Refreshes observers in batches.
|
||||||
private val bundler = BundledUpdate(300, Dispatchers.Main) {
|
private val bundler = BundledUpdate(300, Dispatchers.IO) {
|
||||||
if (hasActiveObservers()) {
|
if (hasActiveObservers()) {
|
||||||
refresh()
|
postValue(UserState(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,10 +406,6 @@ class UserLiveData(val user: User) : LiveData<UserState>(UserState(user)) {
|
|||||||
bundler.invalidate()
|
bundler.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun refresh() {
|
|
||||||
postValue(UserState(user))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActive() {
|
override fun onActive() {
|
||||||
super.onActive()
|
super.onActive()
|
||||||
NostrSingleUserDataSource.add(user)
|
NostrSingleUserDataSource.add(user)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(ReportEvent.kind),
|
kinds = listOf(ReportEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -96,12 +96,12 @@ object NostrAccountDataSource : NostrDataSource("AccountData") {
|
|||||||
),
|
),
|
||||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||||
limit = 400,
|
limit = 400,
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultNotificationFollowList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
val accountChannel = requestNewChannel { time, relayUrl ->
|
val accountChannel = requestNewChannel { time, relayUrl ->
|
||||||
latestEOSEs.addOrUpdate(account.userProfile(), relayUrl, time)
|
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultNotificationFollowList, relayUrl, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
|||||||
lateinit var account: Account
|
lateinit var account: Account
|
||||||
|
|
||||||
val latestEOSEs = EOSEAccount()
|
val latestEOSEs = EOSEAccount()
|
||||||
|
val chatRoomList = "ChatroomList"
|
||||||
|
|
||||||
fun createMessagesToMeFilter() = TypedFilter(
|
fun createMessagesToMeFilter() = TypedFilter(
|
||||||
types = setOf(FeedType.PRIVATE_DMS),
|
types = setOf(FeedType.PRIVATE_DMS),
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(PrivateDmEvent.kind),
|
kinds = listOf(PrivateDmEvent.kind),
|
||||||
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
tags = mapOf("p" to listOf(account.userProfile().pubkeyHex)),
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(PrivateDmEvent.kind),
|
kinds = listOf(PrivateDmEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind),
|
kinds = listOf(ChannelCreateEvent.kind, ChannelMetadataEvent.kind),
|
||||||
authors = listOf(account.userProfile().pubkeyHex),
|
authors = listOf(account.userProfile().pubkeyHex),
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(ChannelCreateEvent.kind),
|
kinds = listOf(ChannelCreateEvent.kind),
|
||||||
ids = account.followingChannels.toList(),
|
ids = account.followingChannels.toList(),
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
|||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(ChannelMessageEvent.kind),
|
kinds = listOf(ChannelMessageEvent.kind),
|
||||||
tags = mapOf("e" to listOf(it)),
|
tags = mapOf("e" to listOf(it)),
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList,
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(chatRoomList)?.relayList,
|
||||||
limit = 25 // Remember to consider spam that is being removed from the UI
|
limit = 25 // Remember to consider spam that is being removed from the UI
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -80,7 +81,7 @@ object NostrChatroomListDataSource : NostrDataSource("MailBoxFeed") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val chatroomListChannel = requestNewChannel { time, relayUrl ->
|
val chatroomListChannel = requestNewChannel { time, relayUrl ->
|
||||||
latestEOSEs.addOrUpdate(account.userProfile(), relayUrl, time)
|
latestEOSEs.addOrUpdate(account.userProfile(), chatRoomList, relayUrl, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
|
|||||||
@@ -3,28 +3,7 @@ package com.vitorpamplona.amethyst.service
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.service.model.*
|
import com.vitorpamplona.amethyst.service.model.*
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelHideMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelMuteUserEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ContactListEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.DeletionEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.Event
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
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.MetadataEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.RecommendRelayEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.model.TextNoteEvent
|
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.service.relays.Relay
|
import com.vitorpamplona.amethyst.service.relays.Relay
|
||||||
import com.vitorpamplona.amethyst.service.relays.Subscription
|
import com.vitorpamplona.amethyst.service.relays.Subscription
|
||||||
@@ -52,8 +31,6 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
private val clientListener = object : Client.Listener() {
|
private val clientListener = object : Client.Listener() {
|
||||||
override fun onEvent(event: Event, subscriptionId: String, relay: Relay) {
|
override fun onEvent(event: Event, subscriptionId: String, relay: Relay) {
|
||||||
if (subscriptionId in subscriptions.keys) {
|
if (subscriptionId in subscriptions.keys) {
|
||||||
if (!event.hasValidSignature()) return
|
|
||||||
|
|
||||||
val key = "$debugName $subscriptionId ${event.kind}"
|
val key = "$debugName $subscriptionId ${event.kind}"
|
||||||
val keyValue = eventCounter.get(key)
|
val keyValue = eventCounter.get(key)
|
||||||
if (keyValue != null) {
|
if (keyValue != null) {
|
||||||
@@ -62,51 +39,7 @@ abstract class NostrDataSource(val debugName: String) {
|
|||||||
eventCounter = eventCounter + Pair(key, Counter(1))
|
eventCounter = eventCounter + Pair(key, Counter(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
LocalCache.consume(event, relay)
|
||||||
when (event) {
|
|
||||||
is BadgeAwardEvent -> LocalCache.consume(event)
|
|
||||||
is BadgeDefinitionEvent -> LocalCache.consume(event)
|
|
||||||
is BadgeProfilesEvent -> LocalCache.consume(event)
|
|
||||||
is BookmarkListEvent -> LocalCache.consume(event)
|
|
||||||
is ChannelCreateEvent -> LocalCache.consume(event)
|
|
||||||
is ChannelHideMessageEvent -> LocalCache.consume(event)
|
|
||||||
is ChannelMessageEvent -> LocalCache.consume(event, relay)
|
|
||||||
is ChannelMetadataEvent -> LocalCache.consume(event)
|
|
||||||
is ChannelMuteUserEvent -> LocalCache.consume(event)
|
|
||||||
is ContactListEvent -> LocalCache.consume(event)
|
|
||||||
is DeletionEvent -> LocalCache.consume(event)
|
|
||||||
|
|
||||||
is FileHeaderEvent -> LocalCache.consume(event, relay)
|
|
||||||
is FileStorageEvent -> LocalCache.consume(event, relay)
|
|
||||||
is FileStorageHeaderEvent -> LocalCache.consume(event, relay)
|
|
||||||
is HighlightEvent -> LocalCache.consume(event, relay)
|
|
||||||
is LnZapEvent -> {
|
|
||||||
event.zapRequest?.let { onEvent(it, subscriptionId, relay) }
|
|
||||||
LocalCache.consume(event)
|
|
||||||
}
|
|
||||||
is LnZapRequestEvent -> LocalCache.consume(event)
|
|
||||||
is LnZapPaymentRequestEvent -> LocalCache.consume(event)
|
|
||||||
is LnZapPaymentResponseEvent -> LocalCache.consume(event)
|
|
||||||
is LongTextNoteEvent -> LocalCache.consume(event, relay)
|
|
||||||
is MetadataEvent -> LocalCache.consume(event)
|
|
||||||
is PrivateDmEvent -> LocalCache.consume(event, relay)
|
|
||||||
is PeopleListEvent -> LocalCache.consume(event)
|
|
||||||
is ReactionEvent -> LocalCache.consume(event)
|
|
||||||
is RecommendRelayEvent -> LocalCache.consume(event)
|
|
||||||
is ReportEvent -> LocalCache.consume(event, relay)
|
|
||||||
is RepostEvent -> {
|
|
||||||
event.containedPost()?.let { onEvent(it, subscriptionId, relay) }
|
|
||||||
LocalCache.consume(event)
|
|
||||||
}
|
|
||||||
is TextNoteEvent -> LocalCache.consume(event, relay)
|
|
||||||
is PollNoteEvent -> LocalCache.consume(event, relay)
|
|
||||||
else -> {
|
|
||||||
Log.w("Event Not Supported", event.toJson())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind),
|
kinds = listOf(TextNoteEvent.kind, LongTextNoteEvent.kind, PollNoteEvent.kind, HighlightEvent.kind),
|
||||||
authors = followSet,
|
authors = followSet,
|
||||||
limit = 400,
|
limit = 400,
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,13 @@ object NostrHomeDataSource : NostrDataSource("HomeFeed") {
|
|||||||
}.flatten()
|
}.flatten()
|
||||||
),
|
),
|
||||||
limit = 100,
|
limit = 100,
|
||||||
since = latestEOSEs.users[account.userProfile()]?.relayList
|
since = latestEOSEs.users[account.userProfile()]?.followList?.get(account.defaultHomeFollowList)?.relayList
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val followAccountChannel = requestNewChannel { time, relayUrl ->
|
val followAccountChannel = requestNewChannel { time, relayUrl ->
|
||||||
latestEOSEs.addOrUpdate(account.userProfile(), relayUrl, time)
|
latestEOSEs.addOrUpdate(account.userProfile(), account.defaultHomeFollowList, relayUrl, time)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateChannelFilters() {
|
override fun updateChannelFilters() {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ object NostrUserProfileDataSource : NostrDataSource("UserProfileFeed") {
|
|||||||
TypedFilter(
|
TypedFilter(
|
||||||
types = COMMON_FEED_TYPES,
|
types = COMMON_FEED_TYPES,
|
||||||
filter = JsonFilter(
|
filter = JsonFilter(
|
||||||
kinds = listOf(BookmarkListEvent.kind),
|
kinds = listOf(BookmarkListEvent.kind, PeopleListEvent.kind),
|
||||||
authors = listOf(it.pubkeyHex),
|
authors = listOf(it.pubkeyHex),
|
||||||
limit = 1
|
limit = 1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class PrivateDmEvent(
|
|||||||
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
|
* nip-04 EncryptedDmEvent but may omit the recipient, too. This value can be queried and used
|
||||||
* for initial messages.
|
* for initial messages.
|
||||||
*/
|
*/
|
||||||
fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }
|
private fun recipientPubKey() = tags.firstOrNull { it.size > 1 && it[0] == "p" }
|
||||||
|
|
||||||
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this[1]) }?.getOrNull()
|
fun recipientPubKeyBytes() = recipientPubKey()?.runCatching { Hex.decode(this[1]) }?.getOrNull()
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,14 @@ object Nip19 {
|
|||||||
USER, NOTE, EVENT, RELAY, ADDRESS
|
USER, NOTE, EVENT, RELAY, ADDRESS
|
||||||
}
|
}
|
||||||
|
|
||||||
val nip19regex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)(.*)", Pattern.CASE_INSENSITIVE)
|
val nip19regex = Pattern.compile("(nostr:)?@?(nsec1|npub1|nevent1|naddr1|note1|nprofile1|nrelay1)([qpzry9x8gf2tvdw0s3jn54khce6mua7l]+)([\\S]*)", Pattern.CASE_INSENSITIVE)
|
||||||
|
|
||||||
data class Return(
|
data class Return(
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val hex: String,
|
val hex: String,
|
||||||
val relay: String? = null,
|
val relay: String? = null,
|
||||||
val author: String? = null,
|
val author: String? = null,
|
||||||
val kind: Long? = null,
|
val kind: Int? = null,
|
||||||
val additionalChars: String = ""
|
val additionalChars: String = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ object Nip19 {
|
|||||||
|
|
||||||
val kind = tlv.get(Tlv.Type.KIND.id)
|
val kind = tlv.get(Tlv.Type.KIND.id)
|
||||||
?.get(0)
|
?.get(0)
|
||||||
?.let { Tlv.toInt32(it) }?.toLong()
|
?.let { Tlv.toInt32(it) }
|
||||||
|
|
||||||
return Return(Type.EVENT, hex, relay, author, kind)
|
return Return(Type.EVENT, hex, relay, author, kind)
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ object Nip19 {
|
|||||||
|
|
||||||
val kind = tlv.get(Tlv.Type.KIND.id)
|
val kind = tlv.get(Tlv.Type.KIND.id)
|
||||||
?.get(0)
|
?.get(0)
|
||||||
?.let { Tlv.toInt32(it) }?.toLong()
|
?.let { Tlv.toInt32(it) }
|
||||||
|
|
||||||
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
|
return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind)
|
||||||
}
|
}
|
||||||
|
|||||||
+99
@@ -0,0 +1,99 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendDMNotification
|
||||||
|
import com.vitorpamplona.amethyst.service.notifications.NotificationUtils.sendZapNotification
|
||||||
|
import com.vitorpamplona.amethyst.ui.note.showAmount
|
||||||
|
|
||||||
|
class EventNotificationConsumer(private val applicationContext: Context) {
|
||||||
|
fun consume(event: Event) {
|
||||||
|
// adds to database
|
||||||
|
LocalCache.consume(event, null)
|
||||||
|
|
||||||
|
when (event) {
|
||||||
|
is PrivateDmEvent -> notify(event)
|
||||||
|
is LnZapEvent -> notify(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(event: PrivateDmEvent) {
|
||||||
|
val note = LocalCache.notes[event.id] ?: return
|
||||||
|
|
||||||
|
LocalPreferences.allSavedAccounts().forEach {
|
||||||
|
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||||
|
|
||||||
|
if (acc != null && acc.userProfile().pubkeyHex == event.verifiedRecipientPubKey()) {
|
||||||
|
val followingKeySet = acc.followingKeySet()
|
||||||
|
|
||||||
|
val messagingWith = acc.userProfile().privateChatrooms.keys.filter {
|
||||||
|
(
|
||||||
|
it.pubkeyHex in followingKeySet || acc.userProfile()
|
||||||
|
.hasSentMessagesTo(it)
|
||||||
|
) && !acc.isHidden(it)
|
||||||
|
}.toSet()
|
||||||
|
|
||||||
|
if (note.author in messagingWith) {
|
||||||
|
val content = acc.decryptContent(note) ?: ""
|
||||||
|
val user = note.author?.toBestDisplayName() ?: ""
|
||||||
|
val userPicture = note.author?.profilePicture()
|
||||||
|
val noteUri = note.toNEvent()
|
||||||
|
notificationManager().sendDMNotification(content, user, userPicture, noteUri, applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notify(event: LnZapEvent) {
|
||||||
|
val noteZapEvent = LocalCache.notes[event.id] ?: return
|
||||||
|
val noteZapRequest = event.zapRequest?.id?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||||
|
val noteZapped = event.zappedPost().firstOrNull()?.let { LocalCache.checkGetOrCreateNote(it) }
|
||||||
|
|
||||||
|
LocalPreferences.allSavedAccounts().forEach {
|
||||||
|
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||||
|
|
||||||
|
if (acc != null && acc.userProfile().pubkeyHex == event.zappedAuthor().firstOrNull()) {
|
||||||
|
val amount = showAmount(event.amount)
|
||||||
|
val senderInfo = (noteZapRequest?.event as? LnZapRequestEvent)?.let {
|
||||||
|
val decryptedContent = acc.decryptZapContentAuthor(noteZapRequest)
|
||||||
|
if (decryptedContent != null) {
|
||||||
|
val author = LocalCache.getOrCreateUser(decryptedContent.pubKey)
|
||||||
|
Pair(author, decryptedContent.content)
|
||||||
|
} else if (!noteZapRequest.event?.content().isNullOrBlank()) {
|
||||||
|
Pair(noteZapRequest.author, noteZapRequest.event?.content())
|
||||||
|
} else {
|
||||||
|
Pair(noteZapRequest.author, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val zappedContent =
|
||||||
|
noteZapped?.let { it1 -> acc.decryptContent(it1)?.split("\n")?.get(0)?.take(50) }
|
||||||
|
|
||||||
|
val user = senderInfo?.first?.toBestDisplayName() ?: ""
|
||||||
|
var title = applicationContext.getString(R.string.app_notification_zaps_channel_message, amount)
|
||||||
|
senderInfo?.second?.ifBlank { null }?.let {
|
||||||
|
title += " ($it)"
|
||||||
|
}
|
||||||
|
var content = applicationContext.getString(R.string.app_notification_zaps_channel_message_from, user)
|
||||||
|
zappedContent?.let {
|
||||||
|
content += " " + applicationContext.getString(R.string.app_notification_zaps_channel_message_for, zappedContent)
|
||||||
|
}
|
||||||
|
val userPicture = senderInfo?.first?.profilePicture()
|
||||||
|
val noteUri = "nostr:Notifications"
|
||||||
|
notificationManager().sendZapNotification(content, title, userPicture, noteUri, applicationContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notificationManager(): NotificationManager {
|
||||||
|
return ContextCompat.getSystemService(applicationContext, NotificationManager::class.java) as NotificationManager
|
||||||
|
}
|
||||||
|
}
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.drawable.BitmapDrawable
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.executeBlocking
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.ui.MainActivity
|
||||||
|
|
||||||
|
object NotificationUtils {
|
||||||
|
|
||||||
|
// Notification ID.
|
||||||
|
private var notificationId = 0
|
||||||
|
|
||||||
|
private var dmChannel: NotificationChannel? = null
|
||||||
|
private var zapChannel: NotificationChannel? = null
|
||||||
|
|
||||||
|
private fun getOrCreateDMChannel(applicationContext: Context): NotificationChannel {
|
||||||
|
if (dmChannel != null) return dmChannel!!
|
||||||
|
|
||||||
|
dmChannel = NotificationChannel(
|
||||||
|
applicationContext.getString(R.string.app_notification_dms_channel_id),
|
||||||
|
applicationContext.getString(R.string.app_notification_dms_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
).apply {
|
||||||
|
description = applicationContext.getString(R.string.app_notification_dms_channel_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the channel with the system
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(dmChannel!!)
|
||||||
|
|
||||||
|
return dmChannel!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getOrCreateZapChannel(applicationContext: Context): NotificationChannel {
|
||||||
|
if (zapChannel != null) return zapChannel!!
|
||||||
|
|
||||||
|
zapChannel = NotificationChannel(
|
||||||
|
applicationContext.getString(R.string.app_notification_zaps_channel_id),
|
||||||
|
applicationContext.getString(R.string.app_notification_zaps_channel_name),
|
||||||
|
NotificationManager.IMPORTANCE_DEFAULT
|
||||||
|
).apply {
|
||||||
|
description = applicationContext.getString(R.string.app_notification_zaps_channel_description)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the channel with the system
|
||||||
|
val notificationManager: NotificationManager =
|
||||||
|
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
|
||||||
|
notificationManager.createNotificationChannel(zapChannel!!)
|
||||||
|
|
||||||
|
return zapChannel!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NotificationManager.sendZapNotification(
|
||||||
|
messageBody: String,
|
||||||
|
messageTitle: String,
|
||||||
|
pictureUrl: String?,
|
||||||
|
uri: String,
|
||||||
|
applicationContext: Context
|
||||||
|
) {
|
||||||
|
val zapChannel = getOrCreateZapChannel(applicationContext)
|
||||||
|
val channelId = applicationContext.getString(R.string.app_notification_zaps_channel_id)
|
||||||
|
|
||||||
|
sendNotification(messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NotificationManager.sendDMNotification(
|
||||||
|
messageBody: String,
|
||||||
|
messageTitle: String,
|
||||||
|
pictureUrl: String?,
|
||||||
|
uri: String,
|
||||||
|
applicationContext: Context
|
||||||
|
) {
|
||||||
|
val dmChannel = getOrCreateDMChannel(applicationContext)
|
||||||
|
val channelId = applicationContext.getString(R.string.app_notification_dms_channel_id)
|
||||||
|
|
||||||
|
sendNotification(messageBody, messageTitle, pictureUrl, uri, channelId, applicationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NotificationManager.sendNotification(
|
||||||
|
messageBody: String,
|
||||||
|
messageTitle: String,
|
||||||
|
pictureUrl: String?,
|
||||||
|
uri: String,
|
||||||
|
channelId: String,
|
||||||
|
applicationContext: Context
|
||||||
|
) {
|
||||||
|
if (pictureUrl != null) {
|
||||||
|
val request = ImageRequest.Builder(applicationContext)
|
||||||
|
.data(pictureUrl)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val imageLoader = ImageLoader(applicationContext)
|
||||||
|
val imageResult = imageLoader.executeBlocking(request)
|
||||||
|
sendNotification(
|
||||||
|
messageBody = messageBody,
|
||||||
|
messageTitle = messageTitle,
|
||||||
|
picture = imageResult.drawable as? BitmapDrawable,
|
||||||
|
uri = uri,
|
||||||
|
channelId,
|
||||||
|
applicationContext = applicationContext
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
sendNotification(
|
||||||
|
messageBody = messageBody,
|
||||||
|
messageTitle = messageTitle,
|
||||||
|
picture = null,
|
||||||
|
uri = uri,
|
||||||
|
channelId,
|
||||||
|
applicationContext = applicationContext
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun NotificationManager.sendNotification(
|
||||||
|
messageBody: String,
|
||||||
|
messageTitle: String,
|
||||||
|
picture: BitmapDrawable?,
|
||||||
|
uri: String,
|
||||||
|
channelId: String,
|
||||||
|
applicationContext: Context
|
||||||
|
) {
|
||||||
|
val contentIntent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||||
|
data = Uri.parse(uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentPendingIntent = PendingIntent.getActivity(
|
||||||
|
applicationContext,
|
||||||
|
notificationId,
|
||||||
|
contentIntent,
|
||||||
|
PendingIntent.FLAG_MUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
val builderPublic = NotificationCompat.Builder(
|
||||||
|
applicationContext,
|
||||||
|
channelId
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
|
.setContentTitle(messageTitle)
|
||||||
|
.setContentText(applicationContext.getString(R.string.app_notification_private_message))
|
||||||
|
.setLargeIcon(picture?.bitmap)
|
||||||
|
.setGroup(messageTitle)
|
||||||
|
.setContentIntent(contentPendingIntent)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
val builder = NotificationCompat.Builder(
|
||||||
|
applicationContext,
|
||||||
|
channelId
|
||||||
|
)
|
||||||
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
|
.setContentTitle(messageTitle)
|
||||||
|
.setContentText(messageBody)
|
||||||
|
.setLargeIcon(picture?.bitmap)
|
||||||
|
.setGroup(messageTitle)
|
||||||
|
.setContentIntent(contentPendingIntent)
|
||||||
|
.setPublicVersion(builderPublic.build())
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
|
||||||
|
notify(notificationId, builder.build())
|
||||||
|
|
||||||
|
notificationId++
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels all notifications.
|
||||||
|
*/
|
||||||
|
fun NotificationManager.cancelNotifications() {
|
||||||
|
cancelAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
@@ -0,0 +1,73 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.vitorpamplona.amethyst.AccountInfo
|
||||||
|
import com.vitorpamplona.amethyst.BuildConfig
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
|
import com.vitorpamplona.amethyst.service.model.RelayAuthEvent
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
|
||||||
|
class RegisterAccounts(
|
||||||
|
private val accounts: List<AccountInfo>
|
||||||
|
) {
|
||||||
|
|
||||||
|
// creates proof that it controls all accounts
|
||||||
|
private fun signEventsToProveControlOfAccounts(
|
||||||
|
accounts: List<AccountInfo>,
|
||||||
|
notificationToken: String
|
||||||
|
): List<RelayAuthEvent> {
|
||||||
|
return accounts.mapNotNull {
|
||||||
|
val acc = LocalPreferences.loadFromEncryptedStorage(it.npub)
|
||||||
|
if (acc != null) {
|
||||||
|
val relayToUse = acc.activeRelays()?.firstOrNull { it.read }
|
||||||
|
if (relayToUse != null) {
|
||||||
|
acc.createAuthEvent(relayToUse, notificationToken)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postRegistrationEvent(events: List<RelayAuthEvent>) {
|
||||||
|
try {
|
||||||
|
val jsonObject = """{
|
||||||
|
"events": [ ${events.joinToString(", ") { it.toJson() }} ]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
|
val body = jsonObject.toRequestBody(mediaType)
|
||||||
|
|
||||||
|
val request = Request.Builder()
|
||||||
|
.header("User-Agent", "Amethyst/${BuildConfig.VERSION_NAME}")
|
||||||
|
.url("https://push.amethyst.social/register")
|
||||||
|
.post(body)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val client = OkHttpClient.Builder().build()
|
||||||
|
|
||||||
|
client.newCall(request).execute()
|
||||||
|
} catch (e: java.lang.Exception) {
|
||||||
|
Log.e("FirebaseMsgService", "Unable to register with push server", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun go(notificationToken: String) {
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
scope.launch {
|
||||||
|
postRegistrationEvent(
|
||||||
|
signEventsToProveControlOfAccounts(accounts, notificationToken)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,13 +19,28 @@ class EOSERelayList(var relayList: Map<String, EOSETime> = emptyMap()) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class EOSEAccount(var users: Map<User, EOSERelayList> = emptyMap()) {
|
class EOSEFollowList(var followList: Map<String, EOSERelayList> = emptyMap()) {
|
||||||
fun addOrUpdate(user: User, relayUrl: String, time: Long) {
|
fun addOrUpdate(listCode: String, relayUrl: String, time: Long) {
|
||||||
val relayList = users[user]
|
val relayList = followList[listCode]
|
||||||
if (relayList == null) {
|
if (relayList == null) {
|
||||||
users = users + mapOf(user to EOSERelayList(mapOf(relayUrl to EOSETime(time))))
|
val newList = EOSERelayList()
|
||||||
|
newList.addOrUpdate(relayUrl, time)
|
||||||
|
followList = followList + mapOf(listCode to newList)
|
||||||
} else {
|
} else {
|
||||||
relayList.addOrUpdate(relayUrl, time)
|
relayList.addOrUpdate(relayUrl, time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EOSEAccount(var users: Map<User, EOSEFollowList> = emptyMap()) {
|
||||||
|
fun addOrUpdate(user: User, listCode: String, relayUrl: String, time: Long) {
|
||||||
|
val followList = users[user]
|
||||||
|
if (followList == null) {
|
||||||
|
val newList = EOSEFollowList()
|
||||||
|
newList.addOrUpdate(listCode, relayUrl, time)
|
||||||
|
users = users + mapOf(user to newList)
|
||||||
|
} else {
|
||||||
|
followList.addOrUpdate(listCode, relayUrl, time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ import coil.decode.SvgDecoder
|
|||||||
import com.vitorpamplona.amethyst.LocalPreferences
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.vitorpamplona.amethyst.ServiceManager
|
import com.vitorpamplona.amethyst.ServiceManager
|
||||||
import com.vitorpamplona.amethyst.VideoCache
|
import com.vitorpamplona.amethyst.VideoCache
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelMessageEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
|
||||||
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
import com.vitorpamplona.amethyst.service.nip19.Nip19
|
||||||
|
import com.vitorpamplona.amethyst.service.notifications.PushNotificationUtils
|
||||||
import com.vitorpamplona.amethyst.service.relays.Client
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.amethyst.ui.components.muted
|
import com.vitorpamplona.amethyst.ui.components.muted
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
@@ -39,15 +44,30 @@ class MainActivity : FragmentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val nip19 = Nip19.uriToRoute(intent?.data?.toString())
|
val uri = intent?.data?.toString()
|
||||||
val startingPage = when (nip19?.type) {
|
|
||||||
Nip19.Type.USER -> "User/${nip19.hex}"
|
val startingPage = if (uri.equals("nostr:Notifications", true)) {
|
||||||
Nip19.Type.NOTE -> "Note/${nip19.hex}"
|
Route.Notification.route.replace("{scrollToTop}", "true")
|
||||||
Nip19.Type.EVENT -> "Event/${nip19.hex}"
|
} else {
|
||||||
Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
|
val nip19 = Nip19.uriToRoute(uri)
|
||||||
else -> null
|
when (nip19?.type) {
|
||||||
|
Nip19.Type.USER -> "User/${nip19.hex}"
|
||||||
|
Nip19.Type.NOTE -> "Note/${nip19.hex}"
|
||||||
|
Nip19.Type.EVENT -> {
|
||||||
|
if (nip19.kind == PrivateDmEvent.kind) {
|
||||||
|
"Room/${nip19.author}"
|
||||||
|
} else if (nip19.kind == ChannelMessageEvent.kind || nip19.kind == ChannelCreateEvent.kind || nip19.kind == ChannelMetadataEvent.kind) {
|
||||||
|
"Channel/${nip19.hex}"
|
||||||
|
} else {
|
||||||
|
"Event/${nip19.hex}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Nip19.Type.ADDRESS -> "Note/${nip19.hex}"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
} ?: try {
|
} ?: try {
|
||||||
intent?.data?.toString()?.let {
|
uri?.let {
|
||||||
Nip47.parse(it)
|
Nip47.parse(it)
|
||||||
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
|
val encodedUri = URLEncoder.encode(it, StandardCharsets.UTF_8.toString())
|
||||||
Route.Home.base + "?nip47=" + encodedUri
|
Route.Home.base + "?nip47=" + encodedUri
|
||||||
@@ -100,6 +120,8 @@ class MainActivity : FragmentActivity() {
|
|||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
ServiceManager.start()
|
ServiceManager.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PushNotificationUtils().init(LocalPreferences.allSavedAccounts())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ object ChatroomListKnownFeedFilter : FeedFilter<Note>() {
|
|||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val me = account.userProfile()
|
val me = account.userProfile()
|
||||||
|
val followingKeySet = account.followingKeySet()
|
||||||
|
|
||||||
val privateChatrooms = me.privateChatrooms
|
val privateChatrooms = me.privateChatrooms
|
||||||
val messagingWith = privateChatrooms.keys.filter {
|
val messagingWith = privateChatrooms.keys.filter {
|
||||||
me.hasSentMessagesTo(it) && account.isAcceptable(it)
|
(it.pubkeyHex in followingKeySet || me.hasSentMessagesTo(it)) && !account.isHidden(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val privateMessages = messagingWith.mapNotNull { it ->
|
val privateMessages = messagingWith.mapNotNull { it ->
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ object ChatroomListNewFeedFilter : FeedFilter<Note>() {
|
|||||||
|
|
||||||
// returns the last Note of each user.
|
// returns the last Note of each user.
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val me = ChatroomListKnownFeedFilter.account.userProfile()
|
val me = account.userProfile()
|
||||||
|
val followingKeySet = ChatroomListKnownFeedFilter.account.followingKeySet()
|
||||||
|
|
||||||
val privateChatrooms = account.userProfile().privateChatrooms
|
val privateChatrooms = account.userProfile().privateChatrooms
|
||||||
val messagingWith = privateChatrooms.keys.filter {
|
val messagingWith = privateChatrooms.keys.filter {
|
||||||
!me.hasSentMessagesTo(it) && account.isAcceptable(it)
|
it.pubkeyHex !in followingKeySet && !me.hasSentMessagesTo(it) && account.isAcceptable(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
val privateMessages = messagingWith.mapNotNull { it ->
|
val privateMessages = messagingWith.mapNotNull { it ->
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.dal
|
package com.vitorpamplona.amethyst.ui.dal
|
||||||
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
|
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
@@ -18,6 +19,10 @@ object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
private fun innerApplyFilter(collection: Collection<Note>): Set<Note> {
|
||||||
|
val isGlobal = account.defaultNotificationFollowList == GLOBAL_FOLLOWS
|
||||||
|
|
||||||
|
val followingKeySet = account.selectedUsersFollowList(account.defaultNotificationFollowList) ?: emptySet()
|
||||||
|
|
||||||
val loggedInUser = account.userProfile()
|
val loggedInUser = account.userProfile()
|
||||||
val loggedInUserHex = loggedInUser.pubkeyHex
|
val loggedInUserHex = loggedInUser.pubkeyHex
|
||||||
|
|
||||||
@@ -28,6 +33,7 @@ object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
|
|||||||
it.event !is BadgeDefinitionEvent &&
|
it.event !is BadgeDefinitionEvent &&
|
||||||
it.event !is BadgeProfilesEvent &&
|
it.event !is BadgeProfilesEvent &&
|
||||||
it.author !== loggedInUser &&
|
it.author !== loggedInUser &&
|
||||||
|
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
|
||||||
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
|
||||||
(it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
(it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
|
||||||
tagsAnEventByUser(it, loggedInUser)
|
tagsAnEventByUser(it, loggedInUser)
|
||||||
|
|||||||
+4
-1
@@ -4,6 +4,8 @@ import com.vitorpamplona.amethyst.model.Account
|
|||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
|
import com.vitorpamplona.amethyst.service.model.BookmarkListEvent
|
||||||
|
import com.vitorpamplona.amethyst.service.model.PeopleListEvent
|
||||||
|
|
||||||
object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
|
object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
|
||||||
var account: Account? = null
|
var account: Account? = null
|
||||||
@@ -15,7 +17,8 @@ object UserProfileNewThreadFeedFilter : FeedFilter<Note>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun feed(): List<Note> {
|
override fun feed(): List<Note> {
|
||||||
val longFormNotes = LocalCache.addressables.values.filter { it.author == user }
|
val longFormNotes = LocalCache.addressables.values
|
||||||
|
.filter { it.author == user && (it.event !is PeopleListEvent && it.event !is BookmarkListEvent) }
|
||||||
|
|
||||||
return user?.notes
|
return user?.notes
|
||||||
?.plus(longFormNotes)
|
?.plus(longFormNotes)
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ fun AppTopBar(navController: NavHostController, scaffoldState: ScaffoldState, ac
|
|||||||
// Route.Profile.route -> TopBarWithBackButton(navController)
|
// Route.Profile.route -> TopBarWithBackButton(navController)
|
||||||
Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel)
|
Route.Home.base -> HomeTopBar(scaffoldState, accountViewModel)
|
||||||
Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel)
|
Route.Video.base -> StoriesTopBar(scaffoldState, accountViewModel)
|
||||||
|
Route.Notification.base -> NotificationTopBar(scaffoldState, accountViewModel)
|
||||||
else -> MainTopBar(scaffoldState, accountViewModel)
|
else -> MainTopBar(scaffoldState, accountViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,6 +104,15 @@ fun HomeTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NotificationTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
|
GenericTopBar(scaffoldState, accountViewModel) { account ->
|
||||||
|
FollowList(account.defaultNotificationFollowList, account.userProfile(), true) { listName ->
|
||||||
|
account.changeDefaultNotificationFollowList(listName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel) {
|
||||||
GenericTopBar(scaffoldState, accountViewModel) {
|
GenericTopBar(scaffoldState, accountViewModel) {
|
||||||
@@ -273,13 +283,13 @@ fun SimpleTextSpinner(
|
|||||||
) {
|
) {
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
var optionsShowing by remember { mutableStateOf(false) }
|
var optionsShowing by remember { mutableStateOf(false) }
|
||||||
var currentText by remember { mutableStateOf(placeholder) }
|
var currentText by remember(placeholder) { mutableStateOf(placeholder) }
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(currentText)
|
Text(placeholder)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.matchParentSize()
|
.matchParentSize()
|
||||||
|
|||||||
@@ -75,10 +75,12 @@ fun BadgeCompose(likeSetCard: BadgeCard, isInnerNote: Boolean = false, routeForL
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
routeFor(
|
scope.launch {
|
||||||
note,
|
routeFor(
|
||||||
accountViewModel.userProfile()
|
note,
|
||||||
)?.let { navController.navigate(it) }
|
accountViewModel.userProfile()
|
||||||
|
)?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -31,6 +32,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
|
import com.vitorpamplona.amethyst.ui.screen.BoostSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -45,6 +47,8 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
|
|||||||
val noteEvent = note?.event
|
val noteEvent = note?.event
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
BlankNote(Modifier, isInnerNote)
|
BlankNote(Modifier, isInnerNote)
|
||||||
} else {
|
} else {
|
||||||
@@ -65,12 +69,19 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier
|
||||||
onClick = {
|
.background(backgroundColor)
|
||||||
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
.combinedClickable(
|
||||||
},
|
onClick = {
|
||||||
onLongClick = { popupExpanded = true }
|
scope.launch {
|
||||||
)
|
routeFor(
|
||||||
|
note,
|
||||||
|
account.userProfile()
|
||||||
|
)?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { popupExpanded = true }
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -90,7 +101,9 @@ fun BoostSetCompose(boostSetCard: BoostSetCard, isInnerNote: Boolean = false, ro
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_retweeted),
|
painter = painterResource(R.drawable.ic_retweeted),
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.size(16.dp).align(Alignment.TopEnd),
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
tint = Color.Unspecified
|
tint = Color.Unspecified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -31,6 +32,7 @@ import com.vitorpamplona.amethyst.R
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
|
import com.vitorpamplona.amethyst.ui.screen.LikeSetCard
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -44,6 +46,7 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
|
|||||||
|
|
||||||
val noteEvent = note?.event
|
val noteEvent = note?.event
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
BlankNote(Modifier, isInnerNote)
|
BlankNote(Modifier, isInnerNote)
|
||||||
@@ -65,12 +68,19 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier
|
||||||
onClick = {
|
.background(backgroundColor)
|
||||||
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
.combinedClickable(
|
||||||
},
|
onClick = {
|
||||||
onLongClick = { popupExpanded = true }
|
scope.launch {
|
||||||
)
|
routeFor(
|
||||||
|
note,
|
||||||
|
account.userProfile()
|
||||||
|
)?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { popupExpanded = true }
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -90,7 +100,9 @@ fun LikeSetCompose(likeSetCard: LikeSetCard, isInnerNote: Boolean = false, route
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_liked),
|
painter = painterResource(R.drawable.ic_liked),
|
||||||
null,
|
null,
|
||||||
modifier = Modifier.size(16.dp).align(Alignment.TopEnd),
|
modifier = Modifier
|
||||||
|
.size(16.dp)
|
||||||
|
.align(Alignment.TopEnd),
|
||||||
tint = Color.Unspecified
|
tint = Color.Unspecified
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,12 @@ fun MessageSetCompose(messageSetCard: MessageSetCard, isInnerNote: Boolean = fal
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier.background(backgroundColor).combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
routeFor(note, accountViewModel.userProfile())?.let { navController.navigate(it) }
|
scope.launch {
|
||||||
|
routeFor(
|
||||||
|
note,
|
||||||
|
accountViewModel.userProfile()
|
||||||
|
)?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ fun MultiSetCompose(multiSetCard: MultiSetCard, routeForLastRead: String, accoun
|
|||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
scope.launch {
|
||||||
|
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -274,7 +274,9 @@ fun NoteComposeInner(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = {
|
onClick = {
|
||||||
routeFor(note, loggedIn)?.let { navController.navigate(it) }
|
scope.launch {
|
||||||
|
routeFor(note, loggedIn)?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClick = { popupExpanded = true }
|
onLongClick = { popupExpanded = true }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -33,6 +34,7 @@ import com.vitorpamplona.amethyst.ui.screen.ZapSetCard
|
|||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@@ -46,6 +48,7 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
|
|||||||
|
|
||||||
val noteEvent = note?.event
|
val noteEvent = note?.event
|
||||||
var popupExpanded by remember { mutableStateOf(false) }
|
var popupExpanded by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
if (note == null) {
|
if (note == null) {
|
||||||
BlankNote(Modifier, isInnerNote)
|
BlankNote(Modifier, isInnerNote)
|
||||||
@@ -67,12 +70,19 @@ fun ZapSetCompose(zapSetCard: ZapSetCard, isInnerNote: Boolean = false, routeFor
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.background(backgroundColor).combinedClickable(
|
modifier = Modifier
|
||||||
onClick = {
|
.background(backgroundColor)
|
||||||
routeFor(note, account.userProfile())?.let { navController.navigate(it) }
|
.combinedClickable(
|
||||||
},
|
onClick = {
|
||||||
onLongClick = { popupExpanded = true }
|
scope.launch {
|
||||||
)
|
routeFor(
|
||||||
|
note,
|
||||||
|
account.userProfile()
|
||||||
|
)?.let { navController.navigate(it) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { popupExpanded = true }
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ open class CardFeedViewModel(val localFilter: FeedFilter<Note>) : ViewModel() {
|
|||||||
private var lastAccount: Account? = null
|
private var lastAccount: Account? = null
|
||||||
private var lastNotes: List<Note>? = null
|
private var lastNotes: List<Note>? = null
|
||||||
|
|
||||||
private fun refresh() {
|
fun refresh() {
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
refreshSuspended()
|
refreshSuspended()
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ fun ChatroomFeedView(viewModel: FeedViewModel, accountViewModel: AccountViewMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
FeedState.Loading -> {
|
is FeedState.Loading -> {
|
||||||
LoadingFeed()
|
LoadingFeed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -72,8 +72,8 @@ fun BookmarkListScreen(accountViewModel: AccountViewModel, navController: NavCon
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
HorizontalPager(pageCount = 2, state = pagerState) {
|
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||||
when (pagerState.currentPage) {
|
when (page) {
|
||||||
0 -> FeedView(privateFeedViewModel, accountViewModel, navController, null)
|
0 -> FeedView(privateFeedViewModel, accountViewModel, navController, null)
|
||||||
1 -> FeedView(publicFeedViewModel, accountViewModel, navController, null)
|
1 -> FeedView(publicFeedViewModel, accountViewModel, navController, null)
|
||||||
}
|
}
|
||||||
|
|||||||
+63
-105
@@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -44,6 +45,7 @@ import com.vitorpamplona.amethyst.service.NostrChatroomListDataSource
|
|||||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.ChatroomListKnownFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.ChatroomListNewFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
|
import com.vitorpamplona.amethyst.ui.screen.ChatroomListFeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListKnownFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrChatroomListNewFeedViewModel
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -58,6 +60,48 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
|
|||||||
val markKnownAsRead = remember { mutableStateOf(false) }
|
val markKnownAsRead = remember { mutableStateOf(false) }
|
||||||
val markNewAsRead = remember { mutableStateOf(false) }
|
val markNewAsRead = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
|
ChatroomListKnownFeedFilter.account = account
|
||||||
|
val knownFeedViewModel: NostrChatroomListKnownFeedViewModel = viewModel()
|
||||||
|
|
||||||
|
ChatroomListNewFeedFilter.account = account
|
||||||
|
val newFeedViewModel: NostrChatroomListNewFeedViewModel = viewModel()
|
||||||
|
|
||||||
|
LaunchedEffect(accountViewModel) {
|
||||||
|
NostrChatroomListDataSource.account = account
|
||||||
|
NostrChatroomListDataSource.start()
|
||||||
|
knownFeedViewModel.invalidateData()
|
||||||
|
newFeedViewModel.invalidateData()
|
||||||
|
}
|
||||||
|
|
||||||
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
DisposableEffect(accountViewModel) {
|
||||||
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
|
if (event == Lifecycle.Event.ON_RESUME) {
|
||||||
|
NostrChatroomListDataSource.account = account
|
||||||
|
NostrChatroomListDataSource.start()
|
||||||
|
knownFeedViewModel.invalidateData()
|
||||||
|
newFeedViewModel.invalidateData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lifeCycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose {
|
||||||
|
lifeCycleOwner.lifecycle.removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val tabs by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
listOf(
|
||||||
|
ChatroomListTabItem(R.string.known, knownFeedViewModel, markKnownAsRead),
|
||||||
|
ChatroomListTabItem(R.string.new_requests, newFeedViewModel, markNewAsRead)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
Column(
|
Column(
|
||||||
@@ -68,21 +112,17 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
|
|||||||
backgroundColor = MaterialTheme.colors.background,
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
selectedTabIndex = pagerState.currentPage
|
selectedTabIndex = pagerState.currentPage
|
||||||
) {
|
) {
|
||||||
Tab(
|
tabs.forEachIndexed { index, tab ->
|
||||||
selected = pagerState.currentPage == 0,
|
Tab(
|
||||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
selected = pagerState.currentPage == index,
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringResource(R.string.known))
|
Text(text = stringResource(tab.resource))
|
||||||
}
|
},
|
||||||
)
|
onClick = {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||||
Tab(
|
}
|
||||||
selected = pagerState.currentPage == 1,
|
)
|
||||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
}
|
||||||
text = {
|
|
||||||
Text(text = stringResource(R.string.new_requests))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -107,102 +147,20 @@ fun ChatroomListScreen(accountViewModel: AccountViewModel, navController: NavCon
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
HorizontalPager(pageCount = 2, state = pagerState) {
|
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||||
when (pagerState.currentPage) {
|
ChatroomListFeedView(
|
||||||
0 -> TabKnown(accountViewModel, navController, markKnownAsRead)
|
viewModel = tabs[page].viewModel,
|
||||||
1 -> TabNew(accountViewModel, navController, markNewAsRead)
|
accountViewModel = accountViewModel,
|
||||||
}
|
navController = navController,
|
||||||
|
markAsRead = tabs[page].markAsRead
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
class ChatroomListTabItem(val resource: Int, val viewModel: FeedViewModel, val markAsRead: MutableState<Boolean>)
|
||||||
fun TabKnown(
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
navController: NavController,
|
|
||||||
markAsRead: MutableState<Boolean>
|
|
||||||
) {
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
||||||
val account = accountState?.account ?: return
|
|
||||||
|
|
||||||
ChatroomListKnownFeedFilter.account = account
|
|
||||||
val feedViewModel: NostrChatroomListKnownFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
NostrChatroomListDataSource.start()
|
|
||||||
feedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
|
||||||
DisposableEffect(accountViewModel) {
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
NostrChatroomListDataSource.start()
|
|
||||||
feedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
|
||||||
onDispose {
|
|
||||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
|
||||||
) {
|
|
||||||
ChatroomListFeedView(feedViewModel, accountViewModel, navController, markAsRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun TabNew(
|
|
||||||
accountViewModel: AccountViewModel,
|
|
||||||
navController: NavController,
|
|
||||||
markAsRead: MutableState<Boolean>
|
|
||||||
) {
|
|
||||||
val accountState by accountViewModel.accountLiveData.observeAsState()
|
|
||||||
val account = accountState?.account ?: return
|
|
||||||
|
|
||||||
ChatroomListNewFeedFilter.account = account
|
|
||||||
val feedViewModel: NostrChatroomListNewFeedViewModel = viewModel()
|
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
NostrChatroomListDataSource.start()
|
|
||||||
feedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
|
||||||
DisposableEffect(accountViewModel) {
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
|
||||||
if (event == Lifecycle.Event.ON_RESUME) {
|
|
||||||
NostrChatroomListDataSource.account = account
|
|
||||||
NostrChatroomListDataSource.start()
|
|
||||||
feedViewModel.invalidateData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lifeCycleOwner.lifecycle.addObserver(observer)
|
|
||||||
onDispose {
|
|
||||||
lifeCycleOwner.lifecycle.removeObserver(observer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
|
||||||
) {
|
|
||||||
ChatroomListFeedView(feedViewModel, accountViewModel, navController, markAsRead)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatroomTabMenu(
|
fun ChatroomTabMenu(
|
||||||
|
|||||||
+2
-2
@@ -53,8 +53,8 @@ fun HiddenUsersScreen(accountViewModel: AccountViewModel, navController: NavCont
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
HorizontalPager(pageCount = 1, state = pagerState) {
|
HorizontalPager(pageCount = 1, state = pagerState) { page ->
|
||||||
when (pagerState.currentPage) {
|
when (page) {
|
||||||
0 -> UserFeedView(feedViewModel, accountViewModel, navController)
|
0 -> UserFeedView(feedViewModel, accountViewModel, navController)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
|
|||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
import com.vitorpamplona.amethyst.ui.note.UpdateZapAmountDialog
|
||||||
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
import com.vitorpamplona.amethyst.ui.screen.FeedView
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.FeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
import com.vitorpamplona.amethyst.ui.screen.NostrHomeRepliesFeedViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
import com.vitorpamplona.amethyst.ui.screen.ScrollStateKeys
|
||||||
@@ -50,12 +51,12 @@ fun HomeScreen(
|
|||||||
nip47: String? = null
|
nip47: String? = null
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val account = accountViewModel.accountLiveData.value?.account ?: return
|
|
||||||
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
var wantsToAddNip47 by remember { mutableStateOf(nip47) }
|
||||||
|
|
||||||
val accountState = account.live.observeAsState()
|
val accountState by accountViewModel.accountLiveData.observeAsState()
|
||||||
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel, accountState.value?.account?.defaultHomeFollowList) {
|
LaunchedEffect(accountViewModel, account.defaultHomeFollowList) {
|
||||||
HomeNewThreadFeedFilter.account = account
|
HomeNewThreadFeedFilter.account = account
|
||||||
HomeConversationsFeedFilter.account = account
|
HomeConversationsFeedFilter.account = account
|
||||||
NostrHomeDataSource.resetFilters()
|
NostrHomeDataSource.resetFilters()
|
||||||
@@ -85,6 +86,15 @@ fun HomeScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val tabs by remember(homeFeedViewModel, repliesFeedViewModel) {
|
||||||
|
mutableStateOf(
|
||||||
|
listOf(
|
||||||
|
TabItem(R.string.new_threads, homeFeedViewModel, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS),
|
||||||
|
TabItem(R.string.conversations, repliesFeedViewModel, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(Modifier.fillMaxHeight()) {
|
Column(Modifier.fillMaxHeight()) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.padding(vertical = 0.dp)
|
modifier = Modifier.padding(vertical = 0.dp)
|
||||||
@@ -93,28 +103,31 @@ fun HomeScreen(
|
|||||||
backgroundColor = MaterialTheme.colors.background,
|
backgroundColor = MaterialTheme.colors.background,
|
||||||
selectedTabIndex = pagerState.currentPage
|
selectedTabIndex = pagerState.currentPage
|
||||||
) {
|
) {
|
||||||
Tab(
|
tabs.forEachIndexed { index, tab ->
|
||||||
selected = pagerState.currentPage == 0,
|
Tab(
|
||||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(0) } },
|
selected = pagerState.currentPage == index,
|
||||||
text = {
|
text = {
|
||||||
Text(text = stringResource(R.string.new_threads))
|
Text(text = stringResource(tab.resource))
|
||||||
}
|
},
|
||||||
)
|
onClick = {
|
||||||
|
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||||
Tab(
|
}
|
||||||
selected = pagerState.currentPage == 1,
|
)
|
||||||
onClick = { coroutineScope.launch { pagerState.animateScrollToPage(1) } },
|
|
||||||
text = {
|
|
||||||
Text(text = stringResource(R.string.conversations))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
HorizontalPager(pageCount = 2, state = pagerState) {
|
|
||||||
when (pagerState.currentPage) {
|
|
||||||
0 -> FeedView(homeFeedViewModel, accountViewModel, navController, Route.Home.base + "Follows", ScrollStateKeys.HOME_FOLLOWS, scrollToTop)
|
|
||||||
1 -> FeedView(repliesFeedViewModel, accountViewModel, navController, Route.Home.base + "FollowsReplies", ScrollStateKeys.HOME_REPLIES, scrollToTop)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalPager(pageCount = 2, state = pagerState) { page ->
|
||||||
|
FeedView(
|
||||||
|
viewModel = tabs[page].viewModel,
|
||||||
|
accountViewModel = accountViewModel,
|
||||||
|
navController = navController,
|
||||||
|
routeForLastRead = tabs[page].routeForLastRead,
|
||||||
|
scrollStateKey = tabs[page].scrollStateKey,
|
||||||
|
scrollToTop = scrollToTop
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TabItem(val resource: Int, val viewModel: FeedViewModel, val routeForLastRead: String, val scrollStateKey: String)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ import kotlinx.coroutines.launch
|
|||||||
@OptIn(ExperimentalMaterialApi::class)
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) {
|
fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: AccountStateViewModel, startingPage: String? = null) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
|
val scaffoldState = rememberScaffoldState(rememberDrawerState(DrawerValue.Closed))
|
||||||
val sheetState = rememberModalBottomSheetState(
|
val sheetState = rememberModalBottomSheetState(
|
||||||
@@ -69,7 +69,7 @@ fun MainScreen(accountViewModel: AccountViewModel, accountStateViewModel: Accoun
|
|||||||
drawerContent = {
|
drawerContent = {
|
||||||
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
DrawerContent(navController, scaffoldState, sheetState, accountViewModel)
|
||||||
BackHandler(enabled = scaffoldState.drawerState.isOpen) {
|
BackHandler(enabled = scaffoldState.drawerState.isOpen) {
|
||||||
coroutineScope.launch { scaffoldState.drawerState.close() }
|
scope.launch { scaffoldState.drawerState.close() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
|
|||||||
+5
-2
@@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
import com.vitorpamplona.amethyst.service.NostrAccountDataSource
|
||||||
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
|
import com.vitorpamplona.amethyst.ui.dal.NotificationFeedFilter
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
import com.vitorpamplona.amethyst.ui.screen.CardFeedView
|
import com.vitorpamplona.amethyst.ui.screen.CardFeedView
|
||||||
@@ -34,9 +35,11 @@ fun NotificationScreen(
|
|||||||
notifFeedViewModel.clear()
|
notifFeedViewModel.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(accountViewModel) {
|
LaunchedEffect(account.userProfile().pubkeyHex, account.defaultNotificationFollowList) {
|
||||||
|
NostrAccountDataSource.resetFilters()
|
||||||
NotificationFeedFilter.account = account
|
NotificationFeedFilter.account = account
|
||||||
notifFeedViewModel.invalidateData()
|
notifFeedViewModel.clear()
|
||||||
|
notifFeedViewModel.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
val lifeCycleOwner = LocalLifecycleOwner.current
|
val lifeCycleOwner = LocalLifecycleOwner.current
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ fun ProfileScreen(user: User, accountViewModel: AccountViewModel, navController:
|
|||||||
modifier = with(LocalDensity.current) {
|
modifier = with(LocalDensity.current) {
|
||||||
Modifier.height((columnSize.height - tabsSize.height).toDp())
|
Modifier.height((columnSize.height - tabsSize.height).toDp())
|
||||||
}
|
}
|
||||||
) {
|
) { page ->
|
||||||
when (pagerState.currentPage) {
|
when (page) {
|
||||||
0 -> TabNotesNewThreads(accountViewModel, navController)
|
0 -> TabNotesNewThreads(accountViewModel, navController)
|
||||||
1 -> TabNotesConversations(accountViewModel, navController)
|
1 -> TabNotesConversations(accountViewModel, navController)
|
||||||
2 -> TabFollows(baseUser, accountViewModel, navController)
|
2 -> TabFollows(baseUser, accountViewModel, navController)
|
||||||
|
|||||||
@@ -362,7 +362,6 @@
|
|||||||
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
<string name="upload_server_relays_nip95_explainer">Files are hosted by your relays. New NIP: check if they support</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<string name="follow_list_selection">Follow List</string>
|
<string name="follow_list_selection">Follow List</string>
|
||||||
<string name="follow_list_kind3follows">All Follows</string>
|
<string name="follow_list_kind3follows">All Follows</string>
|
||||||
<string name="follow_list_global">Global</string>
|
<string name="follow_list_global">Global</string>
|
||||||
@@ -378,4 +377,17 @@
|
|||||||
<string name="orbot_socks_port">Orbot Socks Port</string>
|
<string name="orbot_socks_port">Orbot Socks Port</string>
|
||||||
<string name="invalid_port_number">Invalid port number</string>
|
<string name="invalid_port_number">Invalid port number</string>
|
||||||
|
|
||||||
|
<string name="app_notification_channel_id" translatable="false">DefaultChannelID</string>
|
||||||
|
<string name="app_notification_private_message" translatable="false">New notification arrived</string>
|
||||||
|
|
||||||
|
<string name="app_notification_dms_channel_id" translatable="false">PrivateMessagesID</string>
|
||||||
|
<string name="app_notification_dms_channel_name">Private Messages</string>
|
||||||
|
<string name="app_notification_dms_channel_description">Notifies you when a private message arrives</string>
|
||||||
|
|
||||||
|
<string name="app_notification_zaps_channel_id" translatable="false">ZapsID</string>
|
||||||
|
<string name="app_notification_zaps_channel_name">Zaps Received</string>
|
||||||
|
<string name="app_notification_zaps_channel_description">Notifies you when somebody zaps you</string>
|
||||||
|
<string name="app_notification_zaps_channel_message">%1$s sats</string>
|
||||||
|
<string name="app_notification_zaps_channel_message_from">From %1$s</string>
|
||||||
|
<string name="app_notification_zaps_channel_message_for">for %1$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:name=".Amethyst">
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".service.notifications.PushNotificationReceiverService"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
|
|
||||||
|
<meta-data android:name="com.google.firebase.messaging.default_notification_icon"
|
||||||
|
android:resource="@drawable/amethyst" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_color"
|
||||||
|
android:resource="@color/purple_500" />
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.firebase.messaging.default_notification_channel_id"
|
||||||
|
android:value="@string/app_notification_channel_id" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
|
import com.vitorpamplona.amethyst.service.model.Event
|
||||||
|
|
||||||
|
class PushNotificationReceiverService : FirebaseMessagingService() {
|
||||||
|
|
||||||
|
// this is called when a message is received
|
||||||
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
|
remoteMessage.data.let {
|
||||||
|
val eventStr = remoteMessage.data["event"] ?: return
|
||||||
|
val event = Event.fromJson(eventStr, true)
|
||||||
|
EventNotificationConsumer(applicationContext).consume(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewToken(token: String) {
|
||||||
|
RegisterAccounts(LocalPreferences.allSavedAccounts()).go(token)
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.notifications
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.android.gms.tasks.OnCompleteListener
|
||||||
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
|
import com.vitorpamplona.amethyst.AccountInfo
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PushNotificationUtils {
|
||||||
|
fun init(accounts: List<AccountInfo>) {
|
||||||
|
val scope = CoroutineScope(Job() + Dispatchers.IO)
|
||||||
|
scope.launch {
|
||||||
|
// get user notification token provided by firebase
|
||||||
|
FirebaseMessaging.getInstance().token.addOnCompleteListener(
|
||||||
|
OnCompleteListener { task ->
|
||||||
|
if (!task.isSuccessful) {
|
||||||
|
Log.w("FirebaseMsgService", "Fetching FCM registration token failed", task.exception)
|
||||||
|
return@OnCompleteListener
|
||||||
|
}
|
||||||
|
// Get new FCM registration token
|
||||||
|
val notificationToken = task.result
|
||||||
|
|
||||||
|
RegisterAccounts(accounts).go(notificationToken)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ class NIP19ParserTest {
|
|||||||
assertEquals("31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far", result?.hex)
|
assertEquals("31337:27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402:xx1xulrf7wdbdlbc31far", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result?.author)
|
assertEquals("27241bb702d145a975260cfedee6265936dcd939eaecb88ea0e4071752c30402", result?.author)
|
||||||
assertEquals(31337L, result?.kind)
|
assertEquals(31337, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -71,7 +71,7 @@ class NIP19ParserTest {
|
|||||||
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result?.hex)
|
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:613f014d2911fb9df52e048aae70268c0d216790287b5814910e1e781e8e0509", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
|
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
|
||||||
assertEquals(30023L, result?.kind)
|
assertEquals(30023, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -81,7 +81,7 @@ class NIP19ParserTest {
|
|||||||
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
|
assertEquals("30023:46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d:1679509418", result?.hex)
|
||||||
assertEquals(null, result?.relay)
|
assertEquals(null, result?.relay)
|
||||||
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
|
assertEquals("46fcbe3065eaf1ae7811465924e48923363ff3f526bd6f73d7c184b16bd8ce4d", result?.author)
|
||||||
assertEquals(30023L, result?.kind)
|
assertEquals(30023, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -101,7 +101,7 @@ class NIP19ParserTest {
|
|||||||
assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex)
|
assertEquals("b60ffa7256d3dd7543d830eb717ae50d05a6c32c5f791ed15b867c2bb0b954ac", result?.hex)
|
||||||
assertEquals("wss://nostr.mom", result?.relay)
|
assertEquals("wss://nostr.mom", result?.relay)
|
||||||
assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author)
|
assertEquals("f8ff11c7a7d3478355d3b4d174e5a473797a906ea4aa61aa9b6bc0652c1ea17a", result?.author)
|
||||||
assertEquals(1L, result?.kind)
|
assertEquals(1, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -112,7 +112,7 @@ class NIP19ParserTest {
|
|||||||
assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex)
|
assertEquals("1f878e82063d80f41a781d3a2ef7bc336f1beb7942bf3b49b42aee1251eb5cf0", result?.hex)
|
||||||
assertEquals("wss://relay.damus.io", result?.relay)
|
assertEquals("wss://relay.damus.io", result?.relay)
|
||||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
|
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
|
||||||
assertEquals(1L, result?.kind)
|
assertEquals(1, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -123,7 +123,7 @@ class NIP19ParserTest {
|
|||||||
assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex)
|
assertEquals("4300ec7fa2f98a276f033908349651620aa8e282b76030ab22abca63e85e07e6", result?.hex)
|
||||||
assertEquals("wss://relay.damus.io", result?.relay)
|
assertEquals("wss://relay.damus.io", result?.relay)
|
||||||
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
|
assertEquals("460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c", result?.author)
|
||||||
assertEquals(1L, result?.kind)
|
assertEquals(1, result?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+6
-3
@@ -2,16 +2,19 @@ buildscript {
|
|||||||
ext {
|
ext {
|
||||||
fragment_version = "1.5.7"
|
fragment_version = "1.5.7"
|
||||||
lifecycle_version = '2.6.1'
|
lifecycle_version = '2.6.1'
|
||||||
compose_ui_version = '1.4.2'
|
compose_ui_version = '1.4.3'
|
||||||
nav_version = "2.5.3"
|
nav_version = "2.5.3"
|
||||||
room_version = "2.4.3"
|
room_version = "2.4.3"
|
||||||
accompanist_version = '0.30.0'
|
accompanist_version = '0.30.0'
|
||||||
coil_version = '2.3.0'
|
coil_version = '2.3.0'
|
||||||
}
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.google.gms:google-services:4.3.15'
|
||||||
|
}
|
||||||
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
}// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '8.0.0' apply false
|
id 'com.android.application' version '8.0.1' apply false
|
||||||
id 'com.android.library' version '8.0.0' apply false
|
id 'com.android.library' version '8.0.1' apply false
|
||||||
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
|
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
|
||||||
id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false
|
id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user