From 4a2d673cceccb0f7345db2cda3771a5831e346f2 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 15:02:57 -0400 Subject: [PATCH 01/17] Updates Readme for the new Private DMs --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index b1a56d722..1abf699ed 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [x] Emoji Packs (Kind:30030) - [x] Personal Emoji Lists (Kind:10030) - [x] Classifieds (Kind:30403) +- [x] Private Messages and Small Groups (NIP-24) +- [x] Gift Wraps & Seals (NIP-59) +- [x] Versioned Encrypted Payloads (NIP-44) - [ ] Marketplace (NIP-15) - [ ] Image/Video Capture in the app - [ ] Local Database From 921d52cb8c650aaf20bb1e2be830af6d469c4742 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 15:12:18 -0400 Subject: [PATCH 02/17] Moderated communities was renamed. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1abf699ed..ba010569c 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ height="80">](https://github.com/vitorpamplona/amethyst/releases) - [x] Relay Pages (NIP-11) - [x] HTTP Auth (NIP-98) - [x] Zapraiser (NIP-TBD) -- [x] Moderated Communities (NIP-172) +- [x] Moderated Communities (NIP-72) - [x] Emoji Packs (Kind:30030) - [x] Personal Emoji Lists (Kind:10030) - [x] Classifieds (Kind:30403) From 57588fd6ac8ac1e73f7214f0501f43d93d04491a Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 16:05:01 -0400 Subject: [PATCH 03/17] no need for these idea files in GitHub --- .idea/.gitignore | 3 -- .idea/.name | 1 - .idea/inspectionProfiles/Project_Default.xml | 40 -------------------- 3 files changed, 44 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/.name delete mode 100644 .idea/inspectionProfiles/Project_Default.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 26d33521a..000000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/.idea/.name b/.idea/.name deleted file mode 100644 index d39e145c1..000000000 --- a/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -Amethyst \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index d7cb58b81..000000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - \ No newline at end of file From d934e28b3a374e080257ccd613c50c6ae55274e1 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 16:10:43 -0400 Subject: [PATCH 04/17] Updates some dependencies while we wait for Android 34's Gradle plugin to be released. --- app/build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 954385bb0..e711d3577 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,7 +1,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' - id 'org.jlleitschuh.gradle.ktlint' version "11.5.0" + id 'org.jlleitschuh.gradle.ktlint' version "11.5.1" id 'com.google.gms.google-services' } @@ -179,7 +179,7 @@ dependencies { playImplementation 'com.google.mlkit:translate:17.0.1' // PushNotifications - playImplementation platform('com.google.firebase:firebase-bom:32.2.0') + playImplementation platform('com.google.firebase:firebase-bom:32.2.2') playImplementation 'com.google.firebase:firebase-messaging-ktx' // Charts @@ -204,7 +204,7 @@ dependencies { implementation 'id.zelory:compressor:3.0.1' testImplementation 'junit:junit:4.13.2' - testImplementation 'io.mockk:mockk:1.13.5' + testImplementation 'io.mockk:mockk:1.13.7' androidTestImplementation 'androidx.test.ext:junit:1.2.0-alpha01' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_ui_version" From 6ab061a5a23dfee150b78c41ad4872651d9cb8b6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 16:33:47 -0400 Subject: [PATCH 05/17] Avoids displaying a NIP-94 event without a url tag --- .../com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt | 2 ++ .../java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt index 7b9c7b301..8ae6115ee 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/FileHeaderEvent.kt @@ -26,6 +26,8 @@ class FileHeaderEvent( fun torrentInfoHash() = tags.firstOrNull { it.size > 1 && it[0] == TORRENT_INFOHASH }?.get(1) fun blurhash() = tags.firstOrNull { it.size > 1 && it[0] == BLUR_HASH }?.get(1) + fun hasUrl() = tags.any { it.size > 1 && it[0] == URL } + companion object { const val kind = 1063 diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt index 1ebc877db..fc88acdb4 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/dal/VideoFeedFilter.kt @@ -37,7 +37,7 @@ class VideoFeedFilter(val account: Account) : AdditiveFeedFilter() { return collection .asSequence() - .filter { it.event is FileHeaderEvent || it.event is FileStorageHeaderEvent } + .filter { (it.event is FileHeaderEvent && (it.event as FileHeaderEvent).hasUrl()) || it.event is FileStorageHeaderEvent } .filter { isGlobal || it.author?.pubkeyHex in followingKeySet || (it.event?.isTaggedHashes(followingTagSet) ?: false) || (it.event?.isTaggedGeoHashes(followingGeohashSet) ?: false) } .filter { isHiddenList || account.isAcceptable(it) } .filter { it.createdAt()!! <= now } From decd6ef627ca7df80f0519582df1541b6d5810bc Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 16:38:54 -0400 Subject: [PATCH 06/17] BugFix for HighlightEvent.create event. --- .../vitorpamplona/amethyst/service/model/HighlightEvent.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt index cf0e386ba..4222be0d8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/HighlightEvent.kt @@ -29,12 +29,12 @@ class HighlightEvent( msg: String, privateKey: ByteArray, createdAt: Long = TimeUtils.now() - ): PollNoteEvent { + ): HighlightEvent { val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() val tags = mutableListOf>() val id = generateId(pubKey, createdAt, kind, tags, msg) val sig = CryptoUtils.sign(id, privateKey) - return PollNoteEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + return HighlightEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) } } } From 3a8a50006d2a0cd985352568024cd0c1ff8e6159 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 21:25:12 -0400 Subject: [PATCH 07/17] Improvements to the Settings Interface --- .../amethyst/ui/components/TextSpinner.kt | 4 +- .../amethyst/ui/navigation/AppTopBar.kt | 29 +- .../amethyst/ui/screen/loggedIn/MainScreen.kt | 9 +- .../ui/screen/loggedIn/SettingsScreen.kt | 248 ++++++++++-------- app/src/main/res/values/strings.xml | 16 +- app/src/main/res/xml/locales_config.xml | 6 +- 6 files changed, 172 insertions(+), 140 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt index e09fa5cde..afa004ddd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/TextSpinner.kt @@ -35,7 +35,7 @@ import kotlinx.collections.immutable.ImmutableList @Composable fun TextSpinner( - label: String, + label: String?, placeholder: String, options: ImmutableList, explainers: ImmutableList? = null, @@ -54,7 +54,7 @@ fun TextSpinner( value = currentText, onValueChange = {}, readOnly = true, - label = { Text(label) }, + label = { label?.let { Text(it) } }, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index f5dcb29a1..2c5de3a7b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -44,10 +44,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.map import androidx.lifecycle.viewModelScope import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavHostController import coil.Coil import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account @@ -108,7 +106,8 @@ fun AppTopBar( navEntryState: State, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, - nav: (String) -> Unit + nav: (String) -> Unit, + navPopBack: () -> Unit ) { val currentRoute by remember(navEntryState.value) { derivedStateOf { @@ -122,7 +121,7 @@ fun AppTopBar( } } - RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav) + RenderTopRouteBar(currentRoute, id, followLists, scaffoldState, accountViewModel, nav, navPopBack) } @Composable @@ -132,14 +131,15 @@ private fun RenderTopRouteBar( followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, - nav: (String) -> Unit + nav: (String) -> Unit, + navPopBack: () -> Unit ) { when (currentRoute) { - // Route.Profile.route -> TopBarWithBackButton(nav) Route.Home.base -> HomeTopBar(followLists, scaffoldState, accountViewModel, nav) Route.Video.base -> StoriesTopBar(followLists, scaffoldState, accountViewModel, nav) Route.Discover.base -> DiscoveryTopBar(followLists, scaffoldState, accountViewModel, nav) Route.Notification.base -> NotificationTopBar(followLists, scaffoldState, accountViewModel, nav) + Route.Settings.base -> TopBarWithBackButton(stringResource(id = R.string.application_preferences), navPopBack) else -> { if (id != null) { when (currentRoute) { @@ -488,24 +488,23 @@ fun SimpleTextSpinner( } @Composable -fun TopBarWithBackButton(navController: NavHostController) { +fun TopBarWithBackButton(caption: String, popBack: () -> Unit) { Column() { TopAppBar( elevation = 0.dp, backgroundColor = Color(0xFFFFFF), - title = {}, + title = { + Text(caption) + }, navigationIcon = { IconButton( - onClick = { - navController.popBackStack() - }, + onClick = popBack, modifier = Modifier ) { Icon( - imageVector = Icons.Filled.ArrowBack, - null, - modifier = Modifier.size(28.dp), - tint = MaterialTheme.colors.primary + imageVector = Icons.Default.ArrowBack, + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colors.onSurface ) } }, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt index 0341b9e17..b6a9e7b83 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/MainScreen.kt @@ -95,6 +95,13 @@ fun MainScreen( } } + val navPopBack = remember(navController) { + { + navController.popBackStack() + Unit + } + } + val followLists: FollowListViewModel = viewModel( key = accountViewModel.userProfile().pubkeyHex + "FollowListViewModel", factory = FollowListViewModel.Factory(accountViewModel.account) @@ -201,7 +208,7 @@ fun MainScreen( AppBottomBar(accountViewModel, navState, navBottomRow) }, topBar = { - AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav) + AppTopBar(followLists, navState, scaffoldState, accountViewModel, nav = nav, navPopBack) }, drawerContent = { DrawerContent(nav, scaffoldState, sheetState, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt index c7108ae65..e7e18bc5a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/SettingsScreen.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.ui.screen.loggedIn import android.content.Context import androidx.appcompat.app.AppCompatDelegate +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -15,6 +16,7 @@ import androidx.compose.material.DropdownMenuItem import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExposedDropdownMenuBox import androidx.compose.material.ExposedDropdownMenuDefaults +import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.material.TextField import androidx.compose.runtime.Composable @@ -26,10 +28,12 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.os.LocaleListCompat @@ -39,7 +43,10 @@ import com.vitorpamplona.amethyst.model.ConnectivityType import com.vitorpamplona.amethyst.model.parseConnectivityType import com.vitorpamplona.amethyst.ui.screen.ThemeViewModel import com.vitorpamplona.amethyst.ui.theme.DoubleVertSpacer -import com.vitorpamplona.amethyst.ui.theme.StdPadding +import com.vitorpamplona.amethyst.ui.theme.HalfVertSpacer +import com.vitorpamplona.amethyst.ui.theme.Size10dp +import com.vitorpamplona.amethyst.ui.theme.Size20dp +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.DelicateCoroutinesApi @@ -108,16 +115,18 @@ fun SettingsScreen( stringResource(ConnectivityType.WIFI_ONLY.reourceId), stringResource(ConnectivityType.NEVER.reourceId) ) - val settings = accountViewModel.account.settings - val index = settings.automaticallyShowImages.screenCode - val videoIndex = settings.automaticallyStartPlayback.screenCode - val linkIndex = settings.automaticallyShowUrlPreview.screenCode val themeItens = persistentListOf( stringResource(R.string.system), stringResource(R.string.light), stringResource(R.string.dark) ) + + val settings = accountViewModel.account.settings + val showImagesIndex = settings.automaticallyShowImages.screenCode + val videoIndex = settings.automaticallyStartPlayback.screenCode + val linkIndex = settings.automaticallyShowUrlPreview.screenCode + val themeIndex = themeViewModel.theme.value ?: 0 val context = LocalContext.current @@ -127,123 +136,132 @@ fun SettingsScreen( val languageIndex = getLanguageIndex(languageEntries) Column( - StdPadding + Modifier + .padding(top = Size10dp, start = Size20dp, end = Size20dp) .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { - Section(stringResource(R.string.application_preferences)) - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() + SettingsRow( + R.string.language, + R.string.language_description, + languageList, + languageIndex ) { - TextSpinner( - label = stringResource(R.string.language), - placeholder = languageList[languageIndex], - options = languageList, - onSelect = { - GlobalScope.launch(Dispatchers.Main) { - val job = scope.launch(Dispatchers.IO) { - val locale = languageEntries[languageList[it]] - accountViewModel.account.settings.preferredLanguage = locale - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } - job.join() - val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]]) - AppCompatDelegate.setApplicationLocales(appLocale) - } - }, - modifier = Modifier - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) - .weight(1f) + GlobalScope.launch(Dispatchers.Main) { + val job = scope.launch(Dispatchers.IO) { + val locale = languageEntries[languageList[it]] + accountViewModel.account.settings.preferredLanguage = locale + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + job.join() + val appLocale: LocaleListCompat = LocaleListCompat.forLanguageTags(languageEntries[languageList[it]]) + AppCompatDelegate.setApplicationLocales(appLocale) + } + } + + Spacer(modifier = HalfVertSpacer) + + SettingsRow( + R.string.theme, + R.string.theme_description, + themeItens, + themeIndex + ) { + themeViewModel.onChange(it) + scope.launch(Dispatchers.IO) { + LocalPreferences.updateTheme(it) + } + } + + Spacer(modifier = HalfVertSpacer) + + SettingsRow( + R.string.automatically_load_images_gifs, + R.string.automatically_load_images_gifs_description, + selectedItens, + showImagesIndex + ) { + val automaticallyShowImages = parseConnectivityType(it) + + scope.launch(Dispatchers.IO) { + accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages) + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + } + + Spacer(modifier = HalfVertSpacer) + + SettingsRow( + R.string.automatically_play_videos, + R.string.automatically_play_videos_description, + selectedItens, + videoIndex + ) { + val automaticallyStartPlayback = parseConnectivityType(it) + + scope.launch(Dispatchers.IO) { + accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback) + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + } + + Spacer(modifier = HalfVertSpacer) + + SettingsRow( + R.string.automatically_show_url_preview, + R.string.automatically_show_url_preview_description, + selectedItens, + linkIndex + ) { + val automaticallyShowUrlPreview = parseConnectivityType(it) + + scope.launch(Dispatchers.IO) { + accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview) + LocalPreferences.saveToEncryptedStorage(accountViewModel.account) + } + } + } +} + +@Composable +fun SettingsRow( + name: Int, + description: Int, + selectedItens: ImmutableList, + selectedIndex: Int, + onSelect: (Int) -> Unit +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.weight(2.0f), + verticalArrangement = Arrangement.spacedBy(3.dp) + ) { + Text( + text = stringResource(name), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = stringResource(description), + style = MaterialTheme.typography.caption, + color = Color.Gray, + maxLines = 2, + overflow = TextOverflow.Ellipsis ) } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - TextSpinner( - label = stringResource(R.string.theme), - placeholder = themeItens[themeIndex], - options = themeItens, - onSelect = { - themeViewModel.onChange(it) - scope.launch(Dispatchers.IO) { - LocalPreferences.updateTheme(it) - } - }, - modifier = Modifier - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) - .weight(1f) - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - TextSpinner( - label = stringResource(R.string.automatically_load_images_gifs), - placeholder = selectedItens[index], - options = selectedItens, - onSelect = { - val automaticallyShowImages = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyShowImages(automaticallyShowImages) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } - }, - modifier = Modifier - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) - .weight(1f) - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - TextSpinner( - label = stringResource(R.string.automatically_play_videos), - placeholder = selectedItens[videoIndex], - options = selectedItens, - onSelect = { - val automaticallyStartPlayback = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyStartPlayback(automaticallyStartPlayback) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } - }, - modifier = Modifier - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) - .weight(1f) - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - TextSpinner( - label = stringResource(R.string.automatically_show_url_preview), - placeholder = selectedItens[linkIndex], - options = selectedItens, - onSelect = { - val automaticallyShowUrlPreview = parseConnectivityType(it) - - scope.launch(Dispatchers.IO) { - accountViewModel.updateAutomaticallyShowUrlPreview(automaticallyShowUrlPreview) - LocalPreferences.saveToEncryptedStorage(accountViewModel.account) - } - }, - modifier = Modifier - .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) - .weight(1f) - ) - } + TextSpinner( + label = "", + placeholder = selectedItens[selectedIndex], + options = selectedItens, + onSelect = onSelect, + modifier = Modifier + .windowInsetsPadding(WindowInsets(0.dp, 0.dp, 0.dp, 0.dp)) + .weight(1f) + ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 517dbe3e3..4f9bf83ac 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -493,12 +493,12 @@ System Light Dark - Application preferences + Application Preferences Language Theme - Automatically load images/gifs - Automatically play videos - Automatically show url preview + Image Preview + Video Playback + URL Preview Load Image Spammers @@ -536,4 +536,12 @@ Members of this group Explanation to members Changing the name for the new goals. + + For the App\'s Interface + Dark, Light or System theme + Automatically load images and GIFs + Automatically plays videos and GIFs + Show URL previews + When to load images + diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml index a407a07dd..64d933c4f 100644 --- a/app/src/main/res/xml/locales_config.xml +++ b/app/src/main/res/xml/locales_config.xml @@ -4,10 +4,13 @@ + + + @@ -18,7 +21,4 @@ - - - From 5d24950d7f194c50fd5e37afcc9d45c6905b1c2c Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 23:05:16 -0400 Subject: [PATCH 08/17] Starts NIP-65 implementation --- .../amethyst/model/LocalCache.kt | 21 ++++++ .../service/NostrAccountDataSource.kt | 12 ++++ .../service/model/AdvertisedRelayListEvent.kt | 70 +++++++++++++++++++ .../amethyst/service/model/EventFactory.kt | 1 + 4 files changed, 104 insertions(+) create mode 100644 app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 59e413f8d..0664e2bb5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -220,6 +220,26 @@ object LocalCache { } } + private fun consume(event: AdvertisedRelayListEvent) { + val version = getOrCreateNote(event.id) + val note = getOrCreateAddressableNote(event.address()) + val author = getOrCreateUser(event.pubKey) + + if (version.event == null) { + version.loadEvent(event, author, emptyList()) + version.moveAllReferencesTo(note) + } + + // Already processed this event. + if (note.event?.id() == event.id()) return + + if (event.createdAt > (note.createdAt() ?: 0)) { + note.loadEvent(event, author, emptyList()) + + refreshObservers(note) + } + } + fun formattedDateTime(timestamp: Long): String { return Instant.ofEpochSecond(timestamp).atZone(ZoneId.systemDefault()) .format(DateTimeFormatter.ofPattern("uuuu MMM d hh:mm a")) @@ -1497,6 +1517,7 @@ object LocalCache { try { when (event) { + is AdvertisedRelayListEvent -> consume(event) is AppDefinitionEvent -> consume(event) is AppRecommendationEvent -> consume(event) is AudioTrackEvent -> consume(event) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt index 9ce1400ac..4ed3941bc 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/NostrAccountDataSource.kt @@ -48,6 +48,17 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { ) } + fun createAccountRelayListFilter(): TypedFilter { + return TypedFilter( + types = COMMON_FEED_TYPES, + filter = JsonFilter( + kinds = listOf(AdvertisedRelayListEvent.kind), + authors = listOf(account.userProfile().pubkeyHex), + limit = 1 + ) + ) + } + fun createAccountAcceptedAwardsFilter(): TypedFilter { return TypedFilter( types = COMMON_FEED_TYPES, @@ -155,6 +166,7 @@ object NostrAccountDataSource : NostrDataSource("AccountData") { accountChannel.typedFilters = listOf( createAccountMetadataFilter(), createAccountContactListFilter(), + createAccountRelayListFilter(), createNotificationFilter(), createGiftWrapsToMeFilter(), createAccountReportsFilter(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt new file mode 100644 index 000000000..5403809fc --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/AdvertisedRelayListEvent.kt @@ -0,0 +1,70 @@ +package com.vitorpamplona.amethyst.service.model + +import androidx.compose.runtime.Immutable +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.TimeUtils +import com.vitorpamplona.amethyst.model.toHexKey +import com.vitorpamplona.amethyst.service.CryptoUtils + +@Immutable +class AdvertisedRelayListEvent( + id: HexKey, + pubKey: HexKey, + createdAt: Long, + tags: List>, + content: String, + sig: HexKey +) : Event(id, pubKey, createdAt, kind, tags, content, sig), AddressableEvent { + override fun dTag() = fixedDTag + override fun address() = ATag(kind, pubKey, dTag(), null) + + fun relays(): List { + return tags.mapNotNull { + if (it.size > 1 && it[0] == "r") { + val type = when (it.getOrNull(2)) { + "read" -> AdvertisedRelayType.READ + "write" -> AdvertisedRelayType.WRITE + else -> AdvertisedRelayType.BOTH + } + + AdvertisedRelayInfo(it[1], type) + } else { + null + } + } + } + + companion object { + const val kind = 10002 + const val fixedDTag = "" + + fun create( + list: List, + privateKey: ByteArray, + createdAt: Long = TimeUtils.now() + ): AdvertisedRelayListEvent { + val tags = list.map { + if (it.type == AdvertisedRelayType.BOTH) { + listOf(it.relayUrl) + } else { + listOf(it.relayUrl, it.type.code) + } + } + val msg = "" + val pubKey = CryptoUtils.pubkeyCreate(privateKey).toHexKey() + val id = generateId(pubKey, createdAt, kind, tags, msg) + val sig = CryptoUtils.sign(id, privateKey) + return AdvertisedRelayListEvent(id.toHexKey(), pubKey, createdAt, tags, msg, sig.toHexKey()) + } + } + + @Immutable + data class AdvertisedRelayInfo(val relayUrl: String, val type: AdvertisedRelayType) + + @Immutable + enum class AdvertisedRelayType(val code: String) { + BOTH(""), + READ("read"), + WRITE("write") + } +} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt index fcb5301c6..0e8d6d69b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/EventFactory.kt @@ -14,6 +14,7 @@ class EventFactory { sig: String, lenient: Boolean ) = when (kind) { + AdvertisedRelayListEvent.kind -> AdvertisedRelayListEvent(id, pubKey, createdAt, tags, content, sig) AppDefinitionEvent.kind -> AppDefinitionEvent(id, pubKey, createdAt, tags, content, sig) AppRecommendationEvent.kind -> AppRecommendationEvent(id, pubKey, createdAt, tags, content, sig) AudioTrackEvent.kind -> AudioTrackEvent(id, pubKey, createdAt, tags, content, sig) From d664d9ff19f985a41c76ff640a1a646c475187e6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 23:05:50 -0400 Subject: [PATCH 09/17] Moves Relay List to a surface --- .../amethyst/ui/actions/NewRelayListView.kt | 103 ++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt index d811a1e99..00c7ab118 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewRelayListView.kt @@ -7,12 +7,10 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -24,8 +22,9 @@ import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Surface +import androidx.compose.material.Scaffold import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.DeleteSweep @@ -65,13 +64,13 @@ import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.theme.ButtonBorder import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.Size35dp +import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.StdVertSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.lang.Math.round -@OptIn(ExperimentalLayoutApi::class) @Composable fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, relayToAdd: String = "", nav: (String) -> Unit) { val postViewModel: NewRelayListViewModel = viewModel() @@ -89,54 +88,68 @@ fun NewRelayListView(onClose: () -> Unit, accountViewModel: AccountViewModel, re } Dialog( - onDismissRequest = { onClose() }, - properties = DialogProperties( - decorFitsSystemWindows = false, - usePlatformDefaultWidth = false, - dismissOnClickOutside = false - ) + onDismissRequest = onClose, + properties = DialogProperties(usePlatformDefaultWidth = false) ) { - Surface(modifier = Modifier.imePadding()) { - Column( - modifier = Modifier.padding(10.dp) - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - CloseButton(onCancel = { - postViewModel.clear() - onClose() - }) + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + modifier = Modifier.fillMaxWidth().padding(end = 10.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = StdHorzSpacer) - Button( - onClick = { - postViewModel.deleteAll() - defaultRelays.forEach { - postViewModel.addRelay(it) - } - postViewModel.relays.value.forEach { item -> - loadRelayInfo(item.url, context, scope) { - postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false) + Button( + onClick = { + postViewModel.deleteAll() + defaultRelays.forEach { + postViewModel.addRelay(it) + } + postViewModel.relays.value.forEach { item -> + loadRelayInfo(item.url, context, scope) { + postViewModel.togglePaidRelay(item, it.limitation?.payment_required ?: false) + } + } } + ) { + Text(stringResource(R.string.default_relays)) } + + PostButton( + onPost = { + postViewModel.create() + onClose() + }, + true + ) } - ) { - Text(stringResource(R.string.default_relays)) - } - - PostButton( - onPost = { - postViewModel.create() + }, + navigationIcon = { + Spacer(modifier = StdHorzSpacer) + CloseButton(onCancel = { + postViewModel.clear() onClose() - }, - true - ) - } - - Spacer(modifier = StdVertSpacer) + }) + }, + backgroundColor = MaterialTheme.colors.surface, + elevation = 0.dp + ) + } + ) { pad -> + val scope = rememberCoroutineScope() + Column( + modifier = Modifier.padding( + 16.dp, + pad.calculateTopPadding(), + 16.dp, + pad.calculateBottomPadding() + ), + verticalArrangement = Arrangement.SpaceAround + ) { Row(modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { LazyColumn( contentPadding = PaddingValues( From 77bd646b1d1efbf36c7b87662c4a00e3eed2f552 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 23:06:18 -0400 Subject: [PATCH 10/17] Don't display username if display name is available. --- .../com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index cb17d00bb..faccd59d9 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -134,16 +134,18 @@ private fun UserAndUsernameDisplay( text = bestDisplayName, tags = tags, fontWeight = fontWeight, - maxLines = 1 + maxLines = 1, + modifier = modifier ) + /* CreateTextWithEmoji( text = remember { "@$bestUserName" }, tags = tags, color = MaterialTheme.colors.placeholderText, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = modifier - ) + + )*/ if (showPlayButton) { Spacer(StdHorzSpacer) DrawPlayName(bestDisplayName) From 272601112651074d8f9418c4391e9eda72d42f0e Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 23:06:33 -0400 Subject: [PATCH 11/17] Trim display names when possible --- .../com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt index faccd59d9..81e3b968f 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/UsernameDisplay.kt @@ -75,11 +75,11 @@ private fun UserNameDisplay( fontWeight: FontWeight = FontWeight.Bold ) { if (bestUserName != null && bestDisplayName != null && bestDisplayName != bestUserName) { - UserAndUsernameDisplay(bestDisplayName, tags, bestUserName, modifier, showPlayButton, fontWeight) + UserAndUsernameDisplay(bestDisplayName.trim(), tags, bestUserName.trim(), modifier, showPlayButton, fontWeight) } else if (bestDisplayName != null) { - UserDisplay(bestDisplayName, tags, modifier, showPlayButton, fontWeight) + UserDisplay(bestDisplayName.trim(), tags, modifier, showPlayButton, fontWeight) } else if (bestUserName != null) { - UserDisplay(bestUserName, tags, modifier, showPlayButton, fontWeight) + UserDisplay(bestUserName.trim(), tags, modifier, showPlayButton, fontWeight) } else { NPubDisplay(npubDisplay, modifier, fontWeight) } From dca4f827fe4617acb1447ebc350368ba768b69a6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Mon, 14 Aug 2023 23:07:08 -0400 Subject: [PATCH 12/17] Moves navigation's top bar to use the ArrowBack --- .../ui/components/SelectTextDialog.kt | 2 +- .../ui/navigation/AccountSwitchBottomSheet.kt | 8 +- .../amethyst/ui/navigation/AppTopBar.kt | 487 ++++++++++++++---- .../vitorpamplona/amethyst/ui/note/Icons.kt | 17 +- .../amethyst/ui/note/NoteCompose.kt | 26 +- .../ui/screen/loggedIn/ChannelScreen.kt | 35 +- .../ui/screen/loggedIn/ChatroomScreen.kt | 6 +- .../ui/screen/loggedIn/GeoHashScreen.kt | 4 +- .../ui/screen/loggedIn/HashtagScreen.kt | 2 +- .../ui/screen/loggedIn/ReportNoteDialog.kt | 8 +- .../vitorpamplona/amethyst/ui/theme/Shape.kt | 1 + 11 files changed, 445 insertions(+), 151 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt index aa19e1be5..926a82638 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt @@ -48,7 +48,7 @@ fun SelectTextDialog(text: String, onDismiss: () -> Unit) { IconButton( onClick = onDismiss ) { - ArrowBackIcon(Size24dp) + ArrowBackIcon() } Text(text = stringResource(R.string.select_text_dialog_top)) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt index 40098c28d..b56582716 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AccountSwitchBottomSheet.kt @@ -21,7 +21,6 @@ import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Logout import androidx.compose.material.icons.filled.RadioButtonChecked import androidx.compose.runtime.Composable @@ -52,6 +51,7 @@ import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.ui.actions.toImmutableListOfLists import com.vitorpamplona.amethyst.ui.components.CreateTextWithEmoji import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.note.toShortenHex import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -108,11 +108,7 @@ fun AccountSwitchBottomSheet( title = { Text(text = stringResource(R.string.account_switch_add_account_dialog_title)) }, navigationIcon = { IconButton(onClick = { popupExpanded = false }) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colors.onSurface - ) + ArrowBackIcon() } }, backgroundColor = Color.Transparent, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt index 2c5de3a7b..af92d985a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppTopBar.kt @@ -8,22 +8,29 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.AppBarDefaults +import androidx.compose.material.ContentAlpha import androidx.compose.material.Divider import androidx.compose.material.Icon import androidx.compose.material.IconButton +import androidx.compose.material.LocalContentAlpha import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle import androidx.compose.material.ScaffoldState +import androidx.compose.material.Surface import androidx.compose.material.Text -import androidx.compose.material.TopAppBar +import androidx.compose.material.contentColorFor import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState @@ -39,8 +46,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -49,8 +57,10 @@ import androidx.navigation.NavBackStackEntry import coil.Coil import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.model.ChatroomKey import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS import com.vitorpamplona.amethyst.model.KIND3_FOLLOWS +import com.vitorpamplona.amethyst.model.LiveActivitiesChannel import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.service.NostrAccountDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource @@ -73,22 +83,37 @@ import com.vitorpamplona.amethyst.service.model.PeopleListEvent import com.vitorpamplona.amethyst.service.relays.Client import com.vitorpamplona.amethyst.service.relays.RelayPool import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy -import com.vitorpamplona.amethyst.ui.note.CommunityHeader +import com.vitorpamplona.amethyst.ui.note.AmethystIcon +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon +import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.LoadChannel +import com.vitorpamplona.amethyst.ui.note.LoadUser +import com.vitorpamplona.amethyst.ui.note.LongCommunityHeader +import com.vitorpamplona.amethyst.ui.note.NonClickableUserPictures import com.vitorpamplona.amethyst.ui.note.SearchIcon +import com.vitorpamplona.amethyst.ui.note.ShortCommunityHeader +import com.vitorpamplona.amethyst.ui.note.UsernameDisplay import com.vitorpamplona.amethyst.ui.screen.equalImmutableLists import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChannelHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.ChatroomHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashHeader -import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.DislayGeoTagHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.GeoHashActionOptions +import com.vitorpamplona.amethyst.ui.screen.loggedIn.HashtagActionOptions import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoom import com.vitorpamplona.amethyst.ui.screen.loggedIn.LoadRoomByAuthor +import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.LongRoomHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.RoomNameOnlyDisplay +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShortChannelHeader +import com.vitorpamplona.amethyst.ui.screen.loggedIn.ShowVideoStreaming import com.vitorpamplona.amethyst.ui.screen.loggedIn.SpinnerSelectionDialog import com.vitorpamplona.amethyst.ui.theme.BottomTopHeight +import com.vitorpamplona.amethyst.ui.theme.DoubleHorzSpacer import com.vitorpamplona.amethyst.ui.theme.HeaderPictureModifier +import com.vitorpamplona.amethyst.ui.theme.Size10dp import com.vitorpamplona.amethyst.ui.theme.Size22Modifier +import com.vitorpamplona.amethyst.ui.theme.Size34dp +import com.vitorpamplona.amethyst.ui.theme.Size40dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -143,56 +168,12 @@ private fun RenderTopRouteBar( else -> { if (id != null) { when (currentRoute) { - Route.Channel.base -> LoadChannel(baseChannelHex = id) { - ChannelHeader( - baseChannel = it, - showVideo = true, - showBottomDiviser = true, - modifier = Modifier.padding(vertical = 8.dp, horizontal = 11.dp), - accountViewModel = accountViewModel, - nav = nav - ) - } - Route.RoomByAuthor.base -> LoadRoomByAuthor(authorPubKeyHex = id, accountViewModel) { - if (it != null) { - ChatroomHeader( - room = it, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 11.dp), - accountViewModel = accountViewModel, - nav = nav - ) - } else { - Spacer(BottomTopHeight) - } - } - Route.Room.base -> LoadRoom(roomId = id, accountViewModel) { - if (it != null) { - ChatroomHeader( - room = it, - modifier = Modifier.padding(vertical = 4.dp, horizontal = 11.dp), - accountViewModel = accountViewModel, - nav = nav - ) - } else { - Spacer(BottomTopHeight) - } - } - Route.Community.base -> LoadAddressableNote(aTagHex = id) { - if (it != null) { - CommunityHeader( - baseNote = it, - showBottomDiviser = true, - sendToCommunity = false, - modifier = Modifier.padding(vertical = 8.dp, horizontal = 10.dp), - accountViewModel = accountViewModel, - nav = nav - ) - } else { - Spacer(BottomTopHeight) - } - } - Route.Hashtag.base -> HashtagHeader(id, Modifier.padding(vertical = 0.dp, horizontal = 10.dp), accountViewModel) - Route.Geohash.base -> GeoHashHeader(id, Modifier.padding(vertical = 0.dp, horizontal = 10.dp), accountViewModel) + Route.Channel.base -> ChannelTopBar(id, accountViewModel, nav, navPopBack) + Route.RoomByAuthor.base -> RoomByAuthorTopBar(id, accountViewModel, nav, navPopBack) + Route.Room.base -> RoomTopBar(id, accountViewModel, nav, navPopBack) + Route.Community.base -> CommunityTopBar(id, accountViewModel, nav, navPopBack) + Route.Hashtag.base -> HashTagTopBar(id, accountViewModel, navPopBack) + Route.Geohash.base -> GeoHashTopBar(id, accountViewModel, navPopBack) else -> MainTopBar(scaffoldState, accountViewModel, nav) } } else { @@ -202,13 +183,180 @@ private fun RenderTopRouteBar( } } +@Composable +private fun GeoHashTopBar( + tag: String, + accountViewModel: AccountViewModel, + navPopBack: () -> Unit +) { + FlexibleTopBarWithBackButton( + title = { + DislayGeoTagHeader(tag, remember { Modifier.weight(1f) }) + GeoHashActionOptions(tag, accountViewModel) + }, + popBack = navPopBack + ) +} + +@Composable +private fun HashTagTopBar( + tag: String, + accountViewModel: AccountViewModel, + navPopBack: () -> Unit +) { + FlexibleTopBarWithBackButton( + title = { + Text( + remember(tag) { "#$tag" }, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f) + ) + + HashtagActionOptions(tag, accountViewModel) + }, + popBack = navPopBack + ) +} + +@Composable +private fun CommunityTopBar( + id: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + navPopBack: () -> Unit +) { + LoadAddressableNote(aTagHex = id) { baseNote -> + if (baseNote != null) { + FlexibleTopBarWithBackButton( + title = { + ShortCommunityHeader(baseNote, fontWeight = FontWeight.Medium, accountViewModel, nav) + }, + extendableRow = { + LongCommunityHeader(baseNote, accountViewModel, nav) + }, + popBack = navPopBack + ) + } else { + Spacer(BottomTopHeight) + } + } +} + +@Composable +private fun RoomTopBar( + id: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + navPopBack: () -> Unit +) { + LoadRoom(roomId = id, accountViewModel) { room -> + if (room != null) { + RenderRoomTopBar(room, accountViewModel, nav, navPopBack) + } else { + Spacer(BottomTopHeight) + } + } +} + +@Composable +private fun RoomByAuthorTopBar( + id: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + navPopBack: () -> Unit +) { + LoadRoomByAuthor(authorPubKeyHex = id, accountViewModel) { room -> + if (room != null) { + RenderRoomTopBar(room, accountViewModel, nav, navPopBack) + } else { + Spacer(BottomTopHeight) + } + } +} + +@Composable +private fun RenderRoomTopBar( + room: ChatroomKey, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + navPopBack: () -> Unit +) { + if (room.users.size == 1) { + FlexibleTopBarWithBackButton( + title = { + LoadUser(baseUserHex = room.users.first()) { baseUser -> + if (baseUser != null) { + ClickableUserPicture( + baseUser = baseUser, + accountViewModel = accountViewModel, + size = Size34dp + ) + + Spacer(modifier = DoubleHorzSpacer) + + UsernameDisplay(baseUser, Modifier.weight(1f), fontWeight = FontWeight.Medium) + } + } + }, + popBack = navPopBack + ) + } else { + FlexibleTopBarWithBackButton( + title = { + NonClickableUserPictures( + users = room.users, + accountViewModel = accountViewModel, + size = Size34dp + ) + + RoomNameOnlyDisplay(room, Modifier.padding(start = 10.dp).weight(1f), fontWeight = FontWeight.Medium, accountViewModel.userProfile()) + }, + extendableRow = { + LongRoomHeader(room, accountViewModel, nav) + }, + popBack = navPopBack + ) + } +} + +@Composable +private fun ChannelTopBar( + id: String, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + navPopBack: () -> Unit +) { + LoadChannel(baseChannelHex = id) { baseChannel -> + FlexibleTopBarWithBackButton( + prefixRow = { + if (baseChannel is LiveActivitiesChannel) { + ShowVideoStreaming(baseChannel, accountViewModel) + } + }, + title = { + ShortChannelHeader( + baseChannel = baseChannel, + accountViewModel = accountViewModel, + fontWeight = FontWeight.Medium, + nav = nav, + showFlag = true + ) + }, + extendableRow = { + LongChannelHeader(baseChannel, accountViewModel, nav) + }, + popBack = navPopBack + ) + } +} + @Composable fun NoTopBar() { } @Composable fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> val list by accountViewModel.storiesListLiveData.observeAsState(GLOBAL_FOLLOWS) FollowList( @@ -223,7 +371,7 @@ fun StoriesTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState @Composable fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> val list by accountViewModel.homeListLiveData.observeAsState(KIND3_FOLLOWS) FollowList( @@ -238,7 +386,7 @@ fun HomeTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, a @Composable fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> val list by accountViewModel.notificationListLiveData.observeAsState(GLOBAL_FOLLOWS) FollowList( @@ -253,7 +401,7 @@ fun NotificationTopBar(followLists: FollowListViewModel, scaffoldState: Scaffold @Composable fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - GenericTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> + GenericMainTopBar(scaffoldState, accountViewModel, nav) { accountViewModel -> val list by accountViewModel.discoveryListLiveData.observeAsState(GLOBAL_FOLLOWS) FollowList( @@ -268,18 +416,23 @@ fun DiscoveryTopBar(followLists: FollowListViewModel, scaffoldState: ScaffoldSta @Composable fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { - GenericTopBar(scaffoldState, accountViewModel, nav) { - AmethystIcon() + GenericMainTopBar(scaffoldState, accountViewModel, nav) { + AmethystClickableIcon() } } @OptIn(coil.annotation.ExperimentalCoilApi::class) @Composable -fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel, nav: (String) -> Unit, content: @Composable (AccountViewModel) -> Unit) { +fun GenericMainTopBar( + scaffoldState: ScaffoldState, + accountViewModel: AccountViewModel, + nav: (String) -> Unit, + content: @Composable (AccountViewModel) -> Unit +) { val coroutineScope = rememberCoroutineScope() Column(modifier = BottomTopHeight) { - TopAppBar( + MyTopAppBar( elevation = 0.dp, backgroundColor = MaterialTheme.colors.surface, title = { @@ -291,8 +444,7 @@ fun GenericTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewMod Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight() - .padding(start = 0.dp, end = 20.dp), + .fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -489,23 +641,16 @@ fun SimpleTextSpinner( @Composable fun TopBarWithBackButton(caption: String, popBack: () -> Unit) { - Column() { - TopAppBar( + Column(modifier = BottomTopHeight) { + MyTopAppBar( elevation = 0.dp, - backgroundColor = Color(0xFFFFFF), - title = { - Text(caption) - }, + title = { Text(caption) }, navigationIcon = { IconButton( onClick = popBack, modifier = Modifier ) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colors.onSurface - ) + ArrowBackIcon() } }, actions = {} @@ -515,7 +660,34 @@ fun TopBarWithBackButton(caption: String, popBack: () -> Unit) { } @Composable -fun AmethystIcon() { +fun FlexibleTopBarWithBackButton( + prefixRow: (@Composable () -> Unit)? = null, + title: @Composable RowScope.() -> Unit, + extendableRow: (@Composable () -> Unit)? = null, + popBack: () -> Unit +) { + Column() { + MyExtensibleTopAppBar( + elevation = 0.dp, + prefixRow = prefixRow, + title = title, + extendableRow = extendableRow, + navigationIcon = { + IconButton( + onClick = popBack, + modifier = Modifier + ) { + ArrowBackIcon() + } + }, + actions = {} + ) + Divider(thickness = 0.25.dp) + } +} + +@Composable +fun AmethystClickableIcon() { val context = LocalContext.current IconButton( @@ -523,12 +695,7 @@ fun AmethystIcon() { debugState(context) } ) { - Icon( - painter = painterResource(R.drawable.amethyst), - null, - modifier = Modifier.size(40.dp), - tint = Color.Unspecified - ) + AmethystIcon(Size40dp) } } @@ -578,3 +745,151 @@ fun debugState(context: Context) { Log.d("STATE DUMP", "Kind ${it.key}: \t${it.value.size} elements ") } } + +@Composable +fun MyTopAppBar( + title: @Composable RowScope.() -> Unit, + modifier: Modifier = Modifier, + navigationIcon: @Composable (() -> Unit)? = null, + actions: @Composable RowScope.() -> Unit = {}, + backgroundColor: Color = MaterialTheme.colors.surface, + contentColor: Color = contentColorFor(backgroundColor), + elevation: Dp = AppBarDefaults.TopAppBarElevation +) { + Surface( + contentColor = contentColor, + elevation = elevation, + modifier = modifier + ) { + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Row( + Modifier + .fillMaxWidth() + .padding(AppBarDefaults.ContentPadding), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + if (navigationIcon == null) { + Spacer(TitleInsetWithoutIcon) + } else { + Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) { + CompositionLocalProvider( + LocalContentAlpha provides ContentAlpha.high, + content = navigationIcon + ) + } + } + + Row( + Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + ProvideTextStyle(MaterialTheme.typography.h6) { + title() + } + } + + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } + } + } + } +} + +@Composable +fun MyExtensibleTopAppBar( + prefixRow: (@Composable () -> Unit)? = null, + title: @Composable RowScope.() -> Unit, + extendableRow: (@Composable () -> Unit)? = null, + modifier: Modifier = Modifier, + navigationIcon: @Composable (() -> Unit)? = null, + actions: @Composable RowScope.() -> Unit = {}, + backgroundColor: Color = MaterialTheme.colors.surface, + contentColor: Color = contentColorFor(backgroundColor), + elevation: Dp = AppBarDefaults.TopAppBarElevation +) { + val expanded = remember { mutableStateOf(false) } + + Surface( + color = backgroundColor, + contentColor = contentColor, + elevation = elevation, + modifier = modifier.clickable { + expanded.value = !expanded.value + } + ) { + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Column(Modifier.fillMaxWidth()) { + Row( + Modifier + .fillMaxWidth() + .padding(AppBarDefaults.ContentPadding), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + if (navigationIcon == null) { + Spacer(TitleInsetWithoutIcon) + } else { + Row(TitleIconModifier, verticalAlignment = Alignment.CenterVertically) { + CompositionLocalProvider( + LocalContentAlpha provides ContentAlpha.high, + content = navigationIcon + ) + } + } + + Row( + Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + ProvideTextStyle(MaterialTheme.typography.h6) { + title() + } + } + + CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) { + Row( + horizontalArrangement = Arrangement.End, + verticalAlignment = Alignment.CenterVertically, + content = actions + ) + } + } + + if (expanded.value && extendableRow != null) { + Row( + Modifier + .fillMaxWidth() + .padding(bottom = Size10dp), + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + extendableRow() + } + } + } + + if (prefixRow != null) { + prefixRow() + } + } + } + } +} + +private val AppBarHeight = 50.dp + +// TODO: this should probably be part of the touch target of the start and end icons, clarify this +private val AppBarHorizontalPadding = 4.dp + +// Start inset for the title when there is no navigation icon provided +private val TitleInsetWithoutIcon = Modifier.width(16.dp - AppBarHorizontalPadding) + +// Start inset for the title when there is a navigation icon provided +private val TitleIconModifier = Modifier.width(48.dp - AppBarHorizontalPadding) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt index dba6791bf..1e72fd759 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/Icons.kt @@ -35,6 +35,16 @@ import com.vitorpamplona.amethyst.ui.theme.Size30Modifier import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.subtleButton +@Composable +fun AmethystIcon(iconSize: Dp) { + Icon( + painter = painterResource(R.drawable.amethyst), + null, + modifier = Modifier.size(iconSize), + tint = Color.Unspecified + ) +} + @Composable fun FollowingIcon(iconSize: Dp) { Icon( @@ -46,12 +56,11 @@ fun FollowingIcon(iconSize: Dp) { } @Composable -fun ArrowBackIcon(iconSize: Dp) { +fun ArrowBackIcon() { Icon( imageVector = Icons.Default.ArrowBack, - contentDescription = null, - modifier = remember(iconSize) { Modifier.size(iconSize) }, - tint = MaterialTheme.colors.primary + contentDescription = stringResource(R.string.back), + tint = MaterialTheme.colors.onSurface ) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 8b1ea55fe..209654eb0 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -527,7 +527,11 @@ fun CommunityHeader( } } ) { - ShortCommunityHeader(baseNote, expanded, accountViewModel, nav) + ShortCommunityHeader( + baseNote = baseNote, + accountViewModel = accountViewModel, + nav = nav + ) if (expanded.value) { LongCommunityHeader(baseNote, accountViewModel, nav) @@ -685,7 +689,7 @@ fun LongCommunityHeader(baseNote: AddressableNote, accountViewModel: AccountView } @Composable -fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState, accountViewModel: AccountViewModel, nav: (String) -> Unit) { +fun ShortCommunityHeader(baseNote: AddressableNote, fontWeight: FontWeight = FontWeight.Bold, accountViewModel: AccountViewModel, nav: (String) -> Unit) { val noteState by baseNote.live().metadata.observeAsState() val noteEvent = remember(noteState) { noteState?.note?.event as? CommunityDefinitionEvent } ?: return @@ -710,27 +714,11 @@ fun ShortCommunityHeader(baseNote: AddressableNote, expanded: MutableState, accountViewModel: AccountViewModel, + fontWeight: FontWeight = FontWeight.Bold, nav: (String) -> Unit, showFlag: Boolean ) { @@ -691,27 +695,11 @@ private fun ShortChannelHeader( Row(verticalAlignment = Alignment.CenterVertically) { Text( text = remember(channelState) { channel.toBestDisplayName() }, - fontWeight = FontWeight.Bold, + fontWeight = fontWeight, maxLines = 1, overflow = TextOverflow.Ellipsis ) } - - val summary = remember(channelState) { - channel.summary()?.ifBlank { null } - } - - if (summary != null && !expanded.value) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = summary, - color = MaterialTheme.colors.placeholderText, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontSize = 12.sp - ) - } - } } Row( @@ -731,7 +719,7 @@ private fun ShortChannelHeader( } @Composable -private fun LongChannelHeader( +fun LongChannelHeader( baseChannel: Channel, accountViewModel: AccountViewModel, nav: (String) -> Unit @@ -965,6 +953,7 @@ fun LiveFlag() { text = stringResource(id = R.string.live_stream_live_tag), color = Color.White, fontWeight = FontWeight.Bold, + fontSize = 16.sp, modifier = remember { Modifier .clip(SmallBorder) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt index ebe79b276..f424a1a09 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ChatroomScreen.kt @@ -531,7 +531,7 @@ fun GroupChatroomHeader( ) Column(modifier = Modifier.padding(start = 10.dp)) { - RoomNameOnlyDisplay(room, Modifier, accountViewModel.userProfile()) + RoomNameOnlyDisplay(room, Modifier, FontWeight.Bold, accountViewModel.userProfile()) DisplayUserSetAsSubject(room, FontWeight.Normal) } } @@ -714,14 +714,14 @@ fun LongRoomHeader(room: ChatroomKey, accountViewModel: AccountViewModel, nav: ( } @Composable -fun RoomNameOnlyDisplay(room: ChatroomKey, modifier: Modifier, loggedInUser: User) { +fun RoomNameOnlyDisplay(room: ChatroomKey, modifier: Modifier, fontWeight: FontWeight = FontWeight.Bold, loggedInUser: User) { val roomSubject by loggedInUser.live().messages.map { it.user.privateChatrooms[room]?.subject }.distinctUntilChanged().observeAsState(loggedInUser.privateChatrooms[room]?.subject) Crossfade(targetState = roomSubject, modifier) { if (it != null && it.isNotBlank()) { - DisplayRoomSubject(it) + DisplayRoomSubject(it, fontWeight) } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt index 887c33ba2..8d0db4e3c 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/GeoHashScreen.kt @@ -119,7 +119,7 @@ fun GeoHashHeader(tag: String, modifier: Modifier = StdPadding, account: Account ) { DislayGeoTagHeader(tag, remember { Modifier.weight(1f) }) - HashtagActionOptions(tag, account) + GeoHashActionOptions(tag, account) } } @@ -154,7 +154,7 @@ fun DislayGeoTagHeader(geohash: String, modifier: Modifier) { } @Composable -private fun HashtagActionOptions( +fun GeoHashActionOptions( tag: String, accountViewModel: AccountViewModel ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt index 834a5a30b..604dcff42 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/HashtagScreen.kt @@ -130,7 +130,7 @@ fun HashtagHeader(tag: String, modifier: Modifier = StdPadding, account: Account } @Composable -private fun HashtagActionOptions( +fun HashtagActionOptions( tag: String, accountViewModel: AccountViewModel ) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt index 5f2c2e59d..072d77be5 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ReportNoteDialog.kt @@ -19,7 +19,6 @@ import androidx.compose.material.Scaffold import androidx.compose.material.Text import androidx.compose.material.TopAppBar import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Block import androidx.compose.material.icons.filled.Report import androidx.compose.runtime.Composable @@ -41,6 +40,7 @@ import androidx.compose.ui.window.DialogProperties import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.service.model.ReportEvent +import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon import com.vitorpamplona.amethyst.ui.theme.WarningColor import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Dispatchers @@ -70,11 +70,7 @@ fun ReportNoteDialog(note: Note, accountViewModel: AccountViewModel, onDismiss: title = { Text(text = stringResource(id = R.string.report_dialog_title)) }, navigationIcon = { IconButton(onClick = onDismiss) { - Icon( - imageVector = Icons.Default.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colors.onSurface - ) + ArrowBackIcon() } }, backgroundColor = MaterialTheme.colors.surface, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt index 4123cf550..e61c0de51 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/theme/Shape.kt @@ -62,6 +62,7 @@ val Size25dp = 25.dp val Size30dp = 30.dp val Size34dp = 34.dp val Size35dp = 35.dp +val Size40dp = 40.dp val Size55dp = 55.dp val Size75dp = 75.dp From 52687a48df3672c5d696958fdd042c7220e117b6 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 15 Aug 2023 11:10:17 -0400 Subject: [PATCH 13/17] Fixes image uploading tests without account. --- .../com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index d2575683b..4ead42398 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -140,7 +140,10 @@ object ImageUploader { } fun NIP98Header(url: String, method: String, body: String): String { - val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: "" + val noteJson = if (this::account.isInitialized) { + account.createHTTPAuthorization(url, method, body)?.toJson() ?: "" + } else { "" } + val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray()) return "Nostr " + encodedNIP98Event } From 1ad1d233cd7dcbaab1421cd6136314e3d9b76c95 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 15 Aug 2023 11:13:01 -0400 Subject: [PATCH 14/17] Refactoring TLV's code --- .../amethyst/service/model/ATag.kt | 35 +++---- .../amethyst/service/nip19/Nip19.kt | 87 ++++-------------- .../amethyst/service/nip19/Tlv.kt | 92 +++++++++++++++---- .../amethyst/service/TlvIntegerTest.kt | 40 ++++++++ .../vitorpamplona/amethyst/service/TlvTest.kt | 35 ------- 5 files changed, 143 insertions(+), 146 deletions(-) create mode 100644 app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt delete mode 100644 app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt index 970faa9b9..9c66b8729 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/ATag.kt @@ -2,11 +2,9 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.hexToByteArray -import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.bechToBytes import com.vitorpamplona.amethyst.service.nip19.Tlv -import com.vitorpamplona.amethyst.service.nip19.toByteArray +import com.vitorpamplona.amethyst.service.nip19.TlvBuilder import com.vitorpamplona.amethyst.service.toNAddress import fr.acinq.secp256k1.Hex @@ -15,22 +13,12 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela fun toTag() = "$kind:$pubKeyHex:$dTag" fun toNAddr(): String { - val kind = kind.toByteArray() - val author = pubKeyHex.hexToByteArray() - val dTag = dTag.toByteArray(Charsets.UTF_8) - val relay = relay?.toByteArray(Charsets.UTF_8) - - var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, dTag.size.toByte()) + dTag - - if (relay != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay - } - - fullArray = fullArray + - byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author + - byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind - - return fullArray.toNAddress() + return TlvBuilder().apply { + addString(Tlv.Type.SPECIAL, dTag) + addStringIfNotNull(Tlv.Type.RELAY, relay) + addHex(Tlv.Type.AUTHOR, pubKeyHex) + addInt(Tlv.Type.KIND, kind) + }.build().toNAddress() } companion object { @@ -63,10 +51,11 @@ data class ATag(val kind: Int, val pubKeyHex: String, val dTag: String, val rela if (key.startsWith("naddr")) { val tlv = Tlv.parse(key.bechToBytes()) - val d = tlv.get(Tlv.Type.SPECIAL.id)?.get(0)?.toString(Charsets.UTF_8) ?: "" - val relay = tlv.get(Tlv.Type.RELAY.id)?.get(0)?.toString(Charsets.UTF_8) - val author = tlv.get(Tlv.Type.AUTHOR.id)?.get(0)?.toHexKey() - val kind = tlv.get(Tlv.Type.KIND.id)?.get(0)?.let { Tlv.toInt32(it) } + + val d = tlv.firstAsString(Tlv.Type.SPECIAL) ?: "" + val relay = tlv.firstAsString(Tlv.Type.RELAY) + val author = tlv.firstAsHex(Tlv.Type.AUTHOR) + val kind = tlv.firstAsInt(Tlv.Type.KIND) if (kind != null && author != null) { return ATag(kind, author, d, relay) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt index 39a5b0cb0..937e99b01 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Nip19.kt @@ -2,7 +2,6 @@ package com.vitorpamplona.amethyst.service.nip19 import android.util.Log import androidx.compose.runtime.Immutable -import com.vitorpamplona.amethyst.model.hexToByteArray import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.service.bechToBytes import com.vitorpamplona.amethyst.service.toNEvent @@ -82,13 +81,8 @@ object Nip19 { private fun nprofile(bytes: ByteArray): Return? { val tlv = Tlv.parse(bytes) - val hex = tlv.get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toHexKey() ?: return null - - val relay = tlv.get(Tlv.Type.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) + val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null + val relay = tlv.firstAsString(Tlv.Type.RELAY) return Return(Type.USER, hex, relay) } @@ -96,30 +90,16 @@ object Nip19 { private fun nevent(bytes: ByteArray): Return? { val tlv = Tlv.parse(bytes) - val hex = tlv.get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toHexKey() ?: return null - - val relay = tlv.get(Tlv.Type.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) - - val author = tlv.get(Tlv.Type.AUTHOR.id) - ?.get(0) - ?.toHexKey() - - val kind = tlv.get(Tlv.Type.KIND.id) - ?.get(0) - ?.let { Tlv.toInt32(it) } + val hex = tlv.firstAsHex(Tlv.Type.SPECIAL) ?: return null + val relay = tlv.firstAsString(Tlv.Type.RELAY) + val author = tlv.firstAsHex(Tlv.Type.AUTHOR) + val kind = tlv.firstAsInt(Tlv.Type.KIND.id) return Return(Type.EVENT, hex, relay, author, kind) } private fun nrelay(bytes: ByteArray): Return? { - val relayUrl = Tlv.parse(bytes) - .get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toString(Charsets.UTF_8) ?: return null + val relayUrl = Tlv.parse(bytes).firstAsString(Tlv.Type.SPECIAL.id) ?: return null return Return(Type.RELAY, relayUrl) } @@ -127,53 +107,20 @@ object Nip19 { private fun naddr(bytes: ByteArray): Return? { val tlv = Tlv.parse(bytes) - val d = tlv.get(Tlv.Type.SPECIAL.id) - ?.get(0) - ?.toString(Charsets.UTF_8) ?: return null - - val relay = tlv.get(Tlv.Type.RELAY.id) - ?.get(0) - ?.toString(Charsets.UTF_8) - - val author = tlv.get(Tlv.Type.AUTHOR.id) - ?.get(0) - ?.toHexKey() - - val kind = tlv.get(Tlv.Type.KIND.id) - ?.get(0) - ?.let { Tlv.toInt32(it) } + val d = tlv.firstAsString(Tlv.Type.SPECIAL.id) ?: "" + val relay = tlv.firstAsString(Tlv.Type.RELAY.id) + val author = tlv.firstAsHex(Tlv.Type.AUTHOR.id) ?: return null + val kind = tlv.firstAsInt(Tlv.Type.KIND.id) ?: return null return Return(Type.ADDRESS, "$kind:$author:$d", relay, author, kind) } public fun createNEvent(idHex: String, author: String?, kind: Int?, relay: String?): String { - val kind = kind?.toByteArray() - val author = author?.hexToByteArray() - val idHex = idHex.hexToByteArray() - val relay = relay?.toByteArray(Charsets.UTF_8) - - var fullArray = byteArrayOf(Tlv.Type.SPECIAL.id, idHex.size.toByte()) + idHex - - if (relay != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.RELAY.id, relay.size.toByte()) + relay - } - - if (author != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.AUTHOR.id, author.size.toByte()) + author - } - - if (kind != null) { - fullArray = fullArray + byteArrayOf(Tlv.Type.KIND.id, kind.size.toByte()) + kind - } - - return fullArray.toNEvent() + return TlvBuilder().apply { + addHex(Tlv.Type.SPECIAL, idHex) + addStringIfNotNull(Tlv.Type.RELAY, relay) + addHexIfNotNull(Tlv.Type.AUTHOR, author) + addIntIfNotNull(Tlv.Type.KIND, kind) + }.build().toNEvent() } } - -fun Int.toByteArray(): ByteArray { - val bytes = ByteArray(4) - (0..3).forEach { - bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte() - } - return bytes -} diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt index 83bcc0c5e..eca8ad968 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/nip19/Tlv.kt @@ -1,9 +1,67 @@ package com.vitorpamplona.amethyst.service.nip19 +import com.vitorpamplona.amethyst.model.HexKey +import com.vitorpamplona.amethyst.model.hexToByteArray +import com.vitorpamplona.amethyst.model.toHexKey +import java.io.ByteArrayOutputStream import java.nio.ByteBuffer import java.nio.ByteOrder -object Tlv { +class TlvBuilder() { + val outputStream = ByteArrayOutputStream() + + private fun add(type: Byte, byteArray: ByteArray) { + outputStream.write(byteArrayOf(type, byteArray.size.toByte())) + outputStream.write(byteArray) + } + + fun addString(type: Byte, string: String) = add(type, string.toByteArray(Charsets.UTF_8)) + fun addHex(type: Byte, key: HexKey) = add(type, key.hexToByteArray()) + fun addInt(type: Byte, data: Int) = add(type, data.toByteArray()) + + fun addStringIfNotNull(type: Byte, data: String?) = data?.let { addString(type, it) } + fun addHexIfNotNull(type: Byte, data: HexKey?) = data?.let { addHex(type, it) } + fun addIntIfNotNull(type: Byte, data: Int?) = data?.let { addInt(type, it) } + + fun addString(type: Tlv.Type, string: String) = addString(type.id, string) + fun addHex(type: Tlv.Type, key: HexKey) = addHex(type.id, key) + fun addInt(type: Tlv.Type, data: Int) = addInt(type.id, data) + + fun addStringIfNotNull(type: Tlv.Type, data: String?) = addStringIfNotNull(type.id, data) + fun addHexIfNotNull(type: Tlv.Type, data: HexKey?) = addHexIfNotNull(type.id, data) + fun addIntIfNotNull(type: Tlv.Type, data: Int?) = addIntIfNotNull(type.id, data) + + fun build(): ByteArray { + return outputStream.toByteArray() + } +} + +fun Int.toByteArray(): ByteArray { + val bytes = ByteArray(4) + (0..3).forEach { + bytes[3 - it] = ((this ushr (8 * it)) and 0xFFFF).toByte() + } + return bytes +} + +fun ByteArray.toInt32(): Int? { + if (size != 4) return null + return ByteBuffer.wrap(this, 0, 4).order(ByteOrder.BIG_ENDIAN).int +} + +class Tlv(val data: Map>) { + fun asInt(type: Byte) = data[type]?.mapNotNull { it.toInt32() } + fun asHex(type: Byte) = data[type]?.map { it.toHexKey().intern() } + fun asString(type: Byte) = data[type]?.map { it.toString(Charsets.UTF_8) } + + fun firstAsInt(type: Byte) = data[type]?.firstOrNull()?.toInt32() + fun firstAsHex(type: Byte) = data[type]?.firstOrNull()?.toHexKey()?.intern() + fun firstAsString(type: Byte) = data[type]?.firstOrNull()?.toString(Charsets.UTF_8) + + fun firstAsInt(type: Type) = firstAsInt(type.id) + fun firstAsHex(type: Type) = firstAsHex(type.id) + fun firstAsString(type: Type) = firstAsString(type.id) + enum class Type(val id: Byte) { SPECIAL(0), RELAY(1), @@ -11,26 +69,24 @@ object Tlv { KIND(3); } - fun toInt32(bytes: ByteArray): Int { - require(bytes.size == 4) { "length must be 4, got: ${bytes.size}" } - return ByteBuffer.wrap(bytes, 0, 4).order(ByteOrder.BIG_ENDIAN).int - } + companion object { - fun parse(data: ByteArray): Map> { - val result = mutableMapOf>() - var rest = data - while (rest.isNotEmpty()) { - val t = rest[0] - val l = rest[1].toUByte().toInt() - val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) - rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) - if (v.size < l) continue + fun parse(data: ByteArray): Tlv { + val result = mutableMapOf>() + var rest = data + while (rest.isNotEmpty()) { + val t = rest[0] + val l = rest[1].toUByte().toInt() + val v = rest.sliceArray(IntRange(2, (2 + l) - 1)) + rest = rest.sliceArray(IntRange(2 + l, rest.size - 1)) + if (v.size < l) continue - if (!result.containsKey(t)) { - result[t] = mutableListOf() + if (!result.containsKey(t)) { + result[t] = mutableListOf() + } + result[t]?.add(v) } - result[t]?.add(v) + return Tlv(result) } - return result } } diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt new file mode 100644 index 000000000..cf847a33d --- /dev/null +++ b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvIntegerTest.kt @@ -0,0 +1,40 @@ +package com.vitorpamplona.amethyst.service + +import com.vitorpamplona.amethyst.service.nip19.toByteArray +import com.vitorpamplona.amethyst.service.nip19.toInt32 +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Test + +class TlvIntegerTest { + fun to_int_32_length_smaller_than_4() { + Assert.assertNull(byteArrayOfInts(1, 2, 3).toInt32()) + } + + fun to_int_32_length_bigger_than_4() { + Assert.assertNull(byteArrayOfInts(1, 2, 3, 4, 5).toInt32()) + } + + @Test() + fun to_int_32_length_4() { + val actual = byteArrayOfInts(1, 2, 3, 4).toInt32() + + assertEquals(16909060, actual) + } + + @Test() + fun backAndForth() { + assertEquals(234, 234.toByteArray().toInt32()) + assertEquals(1, 1.toByteArray().toInt32()) + assertEquals(0, 0.toByteArray().toInt32()) + assertEquals(1000, 1000.toByteArray().toInt32()) + + assertEquals(-234, (-234).toByteArray().toInt32()) + assertEquals(-1, (-1).toByteArray().toInt32()) + assertEquals(-0, (-0).toByteArray().toInt32()) + assertEquals(-1000, (-1000).toByteArray().toInt32()) + } + + private fun byteArrayOfInts(vararg ints: Int) = + ByteArray(ints.size) { pos -> ints[pos].toByte() } +} diff --git a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt b/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt deleted file mode 100644 index 9f9087036..000000000 --- a/app/src/test/java/com/vitorpamplona/amethyst/service/TlvTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.vitorpamplona.amethyst.service - -import com.vitorpamplona.amethyst.service.nip19.Tlv -import org.junit.Assert -import org.junit.Ignore -import org.junit.Test - -class TlvTest { - - @Test(expected = IllegalArgumentException::class) - fun to_int_32_length_smaller_than_4() { - Tlv.toInt32(byteArrayOfInts(1, 2, 3)) - } - - @Test(expected = IllegalArgumentException::class) - fun to_int_32_length_bigger_than_4() { - Tlv.toInt32(byteArrayOfInts(1, 2, 3, 4, 5)) - } - - @Test() - fun to_int_32_length_4() { - val actual = Tlv.toInt32(byteArrayOfInts(1, 2, 3, 4)) - - Assert.assertEquals(16909060, actual) - } - - @Ignore("Test not implemented yet") - @Test() - fun parse_TLV() { - // TODO: I don't know how to test this (?) - } - - private fun byteArrayOfInts(vararg ints: Int) = - ByteArray(ints.size) { pos -> ints[pos].toByte() } -} From f7e9898637f2e2a261b6dd1e272595fd4d953fa5 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 15 Aug 2023 11:57:28 -0400 Subject: [PATCH 15/17] Testing image uploads with Account info --- .../amethyst/ImageUploadTesting.kt | 99 ++++++------------- .../vitorpamplona/amethyst/model/Account.kt | 4 +- .../amethyst/ui/actions/ImageUploader.kt | 4 +- 3 files changed, 35 insertions(+), 72 deletions(-) diff --git a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt index bb25fe513..48ea82a2b 100644 --- a/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt +++ b/app/src/androidTest/java/com/vitorpamplona/amethyst/ImageUploadTesting.kt @@ -1,113 +1,78 @@ package com.vitorpamplona.amethyst import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.amethyst.model.Account +import com.vitorpamplona.amethyst.service.KeyPair +import com.vitorpamplona.amethyst.ui.actions.FileServer import com.vitorpamplona.amethyst.ui.actions.ImageUploader import com.vitorpamplona.amethyst.ui.actions.ImgurServer import com.vitorpamplona.amethyst.ui.actions.NostrBuildServer import com.vitorpamplona.amethyst.ui.actions.NostrFilesDevServer import com.vitorpamplona.amethyst.ui.actions.NostrImgServer -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.fail -import kotlinx.coroutines.delay import kotlinx.coroutines.runBlocking +import org.junit.Assert import org.junit.Test import org.junit.runner.RunWith import java.util.Base64 +import java.util.concurrent.CountDownLatch @RunWith(AndroidJUnit4::class) class ImageUploadTesting { val image = "R0lGODlhPQBEAPeoAJosM//AwO/AwHVYZ/z595kzAP/s7P+goOXMv8+fhw/v739/f+8PD98fH/8mJl+fn/9ZWb8/PzWlwv///6wWGbImAPgTEMImIN9gUFCEm/gDALULDN8PAD6atYdCTX9gUNKlj8wZAKUsAOzZz+UMAOsJAP/Z2ccMDA8PD/95eX5NWvsJCOVNQPtfX/8zM8+QePLl38MGBr8JCP+zs9myn/8GBqwpAP/GxgwJCPny78lzYLgjAJ8vAP9fX/+MjMUcAN8zM/9wcM8ZGcATEL+QePdZWf/29uc/P9cmJu9MTDImIN+/r7+/vz8/P8VNQGNugV8AAF9fX8swMNgTAFlDOICAgPNSUnNWSMQ5MBAQEJE3QPIGAM9AQMqGcG9vb6MhJsEdGM8vLx8fH98AANIWAMuQeL8fABkTEPPQ0OM5OSYdGFl5jo+Pj/+pqcsTE78wMFNGQLYmID4dGPvd3UBAQJmTkP+8vH9QUK+vr8ZWSHpzcJMmILdwcLOGcHRQUHxwcK9PT9DQ0O/v70w5MLypoG8wKOuwsP/g4P/Q0IcwKEswKMl8aJ9fX2xjdOtGRs/Pz+Dg4GImIP8gIH0sKEAwKKmTiKZ8aB/f39Wsl+LFt8dgUE9PT5x5aHBwcP+AgP+WltdgYMyZfyywz78AAAAAAAD///8AAP9mZv///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAKgALAAAAAA9AEQAAAj/AFEJHEiwoMGDCBMqXMiwocAbBww4nEhxoYkUpzJGrMixogkfGUNqlNixJEIDB0SqHGmyJSojM1bKZOmyop0gM3Oe2liTISKMOoPy7GnwY9CjIYcSRYm0aVKSLmE6nfq05QycVLPuhDrxBlCtYJUqNAq2bNWEBj6ZXRuyxZyDRtqwnXvkhACDV+euTeJm1Ki7A73qNWtFiF+/gA95Gly2CJLDhwEHMOUAAuOpLYDEgBxZ4GRTlC1fDnpkM+fOqD6DDj1aZpITp0dtGCDhr+fVuCu3zlg49ijaokTZTo27uG7Gjn2P+hI8+PDPERoUB318bWbfAJ5sUNFcuGRTYUqV/3ogfXp1rWlMc6awJjiAAd2fm4ogXjz56aypOoIde4OE5u/F9x199dlXnnGiHZWEYbGpsAEA3QXYnHwEFliKAgswgJ8LPeiUXGwedCAKABACCN+EA1pYIIYaFlcDhytd51sGAJbo3onOpajiihlO92KHGaUXGwWjUBChjSPiWJuOO/LYIm4v1tXfE6J4gCSJEZ7YgRYUNrkji9P55sF/ogxw5ZkSqIDaZBV6aSGYq/lGZplndkckZ98xoICbTcIJGQAZcNmdmUc210hs35nCyJ58fgmIKX5RQGOZowxaZwYA+JaoKQwswGijBV4C6SiTUmpphMspJx9unX4KaimjDv9aaXOEBteBqmuuxgEHoLX6Kqx+yXqqBANsgCtit4FWQAEkrNbpq7HSOmtwag5w57GrmlJBASEU18ADjUYb3ADTinIttsgSB1oJFfA63bduimuqKB1keqwUhoCSK374wbujvOSu4QG6UvxBRydcpKsav++Ca6G8A6Pr1x2kVMyHwsVxUALDq/krnrhPSOzXG1lUTIoffqGR7Goi2MAxbv6O2kEG56I7CSlRsEFKFVyovDJoIRTg7sugNRDGqCJzJgcKE0ywc0ELm6KBCCJo8DIPFeCWNGcyqNFE06ToAfV0HBRgxsvLThHn1oddQMrXj5DyAQgjEHSAJMWZwS3HPxT/QMbabI/iBCliMLEJKX2EEkomBAUCxRi42VDADxyTYDVogV+wSChqmKxEKCDAYFDFj4OmwbY7bDGdBhtrnTQYOigeChUmc1K3QTnAUfEgGFgAWt88hKA6aCRIXhxnQ1yg3BCayK44EWdkUQcBByEQChFXfCB776aQsG0BIlQgQgE8qO26X1h8cEUep8ngRBnOy74E9QgRgEAC8SvOfQkh7FDBDmS43PmGoIiKUUEGkMEC/PJHgxw0xH74yx/3XnaYRJgMB8obxQW6kL9QYEJ0FIFgByfIL7/IQAlvQwEpnAC7DtLNJCKUoO/w45c44GwCXiAFB/OXAATQryUxdN4LfFiwgjCNYg+kYMIEFkCKDs6PKAIJouyGWMS1FSKJOMRB/BoIxYJIUXFUxNwoIkEKPAgCBZSQHQ1A2EWDfDEUVLyADj5AChSIQW6gu10bE/JG2VnCZGfo4R4d0sdQoBAHhPjhIB94v/wRoRKQWGRHgrhGSQJxCS+0pCZbEhAAOw==" - @Test() - fun testImgurUpload() = runBlocking { + fun testBase(server: FileServer) { val bytes = Base64.getDecoder().decode(image) val inputStream = bytes.inputStream() + val countDownLatch = CountDownLatch(1) + var url: String? = null + var error: String? = null + + ImageUploader.account = Account( + KeyPair() + ) + ImageUploader.uploadImage( inputStream, bytes.size.toLong(), "image/gif", - ImgurServer(), - onSuccess = { url, contentType -> + server, + onSuccess = { newUrl, contentType -> println("Uploaded to $url") - assertNotNull(url) + url = newUrl + countDownLatch.countDown() }, onError = { println("Failed to Upload") - fail("${it.message}") + error = it.message + countDownLatch.countDown() } ) - delay(5000) + countDownLatch.await() + + Assert.assertNull(error) + Assert.assertTrue(url?.startsWith("http") == true) + } + + @Test() + fun testImgurUpload() = runBlocking { + testBase(ImgurServer()) } @Test() fun testNostrBuildUpload() = runBlocking { - val bytes = Base64.getDecoder().decode(image) - val inputStream = bytes.inputStream() - - ImageUploader.uploadImage( - inputStream, - bytes.size.toLong(), - "image/gif", - NostrBuildServer(), - onSuccess = { url, contentType -> - println("Uploaded to $url") - assertNotNull(url) - }, - onError = { - println("Failed to Upload") - fail("${it.message}") - } - ) - - delay(1000) + testBase(NostrBuildServer()) } @Test() fun testNostrImgUpload() = runBlocking { - val bytes = Base64.getDecoder().decode(image) - val inputStream = bytes.inputStream() - - ImageUploader.uploadImage( - inputStream, - bytes.size.toLong(), - "image/gif", - NostrImgServer(), - onSuccess = { url, contentType -> - println("Uploaded to $url") - assertNotNull(url) - }, - onError = { - println("Failed to Upload") - fail("${it.message}") - } - ) - - delay(1000) + testBase(NostrImgServer()) } @Test() fun testNostrFilesDevUpload() = runBlocking { - val bytes = Base64.getDecoder().decode(image) - val inputStream = bytes.inputStream() - - ImageUploader.uploadImage( - inputStream, - bytes.size.toLong(), - "image/gif", - NostrFilesDevServer(), - onSuccess = { url, contentType -> - println("Uploaded to $url") - assertNotNull(url) - }, - onError = { - println("Failed to Upload") - fail("${it.message}") - } - ) - - delay(5000) + testBase(NostrFilesDevServer()) } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index d3b78418a..e79020c3d 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -74,8 +74,8 @@ class Account( var hideBlockAlertDialog: Boolean = false, var hideNIP24WarningDialog: Boolean = false, var backupContactList: ContactListEvent? = null, - var proxy: Proxy?, - var proxyPort: Int, + var proxy: Proxy? = null, + var proxyPort: Int = 9050, var showSensitiveContent: Boolean? = null, var warnAboutPostsWithReports: Boolean = true, var filterSpamFromStrangers: Boolean = true, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt index 4ead42398..f5198a016 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/ImageUploader.kt @@ -140,9 +140,7 @@ object ImageUploader { } fun NIP98Header(url: String, method: String, body: String): String { - val noteJson = if (this::account.isInitialized) { - account.createHTTPAuthorization(url, method, body)?.toJson() ?: "" - } else { "" } + val noteJson = account.createHTTPAuthorization(url, method, body)?.toJson() ?: "" val encodedNIP98Event: String = Base64.getEncoder().encodeToString(noteJson.toByteArray()) return "Nostr " + encodedNIP98Event From a5b607b30bac7a2570525e0037b37b5b0eb1e5b0 Mon Sep 17 00:00:00 2001 From: tshinohara Date: Wed, 16 Aug 2023 02:48:50 +0900 Subject: [PATCH 16/17] fix url regax pattern. --- .../vitorpamplona/amethyst/service/CachedRichTextParser.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt index 3683221a1..7c3ec2c59 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/CachedRichTextParser.kt @@ -59,7 +59,7 @@ val noProtocolUrlValidator = try { Pattern.compile("(([\\w\\d-]+\\.)*[a-zA-Z][\\w-]+[\\.\\:]\\w+([\\/\\?\\=\\&\\#\\.]?[\\w-]+)*\\/?)(.*)") } -val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-]+(\\.[A-Za-z0-9]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE) +val HTTPRegex = "^((http|https)://)?([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE) class RichTextParser() { fun parseText( @@ -183,7 +183,7 @@ class RichTextParser() { } else if (word.contains(".") && schemelessMatcher.find()) { val url = schemelessMatcher.group(1) // url val additionalChars = schemelessMatcher.group(4) // additional chars - val pattern = "^([A-Za-z0-9-_]+(\\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\\?[^#]*)?(#.*)?".toRegex(RegexOption.IGNORE_CASE) + val pattern = """^([A-Za-z0-9-_]+(\.[A-Za-z0-9-_]+)+)(:[0-9]+)?(/[^?#]*)?(\?[^#]*)?(#.*)?""".toRegex(RegexOption.IGNORE_CASE) if (pattern.find(word) != null) { SchemelessUrlSegment(word, url, additionalChars) } else { From dd9e51e31c01b7ddaa33283c5fbafb2b1602259f Mon Sep 17 00:00:00 2001 From: David Kaspar Date: Wed, 16 Aug 2023 13:57:56 +0100 Subject: [PATCH 17/17] Added new translations for cs/de/se --- app/src/main/res/values-cs/strings.xml | 22 ++++++++++++++++++++++ app/src/main/res/values-de/strings.xml | 22 ++++++++++++++++++++++ app/src/main/res/values-sv-rSE/strings.xml | 22 ++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index a5aa1b489..3dd6ac96a 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -500,4 +500,26 @@ Přidá Geohash vaší polohy do příspěvku. Veřejnost bude vědět, že se nacházíte do 5 km od aktuální polohy Přidat varování o citlivém obsahu před zobrazením vašeho obsahu. Toto je ideální pro obsah NSFW (nebezpečné pro práci) nebo obsah, který někteří lidé mohou považovat za urážlivý nebo znepokojující + + Nová funkce + Aktivace tohoto režimu vyžaduje od Amethystu odeslání zprávy NIP-24 (GiftWrapped, Zapečetěné přímé a skupinové zprávy). NIP-24 je nový a většina klientů ho zatím neimplementovala. Ujistěte se, že příjemce používá kompatibilního klienta. + Aktivovat + + Veřejné + Soukromé + Pro + Předmět + Téma konverzace + "@Uživatel1, @Uživatel2, @Uživatel3" + + Členové této skupiny + Vysvětlení členům + Změna názvu pro nové cíle. + + Pro rozhraní aplikace + Tmavé, světlé nebo systémové téma + Automaticky načítat obrázky a GIFy + Automaticky přehrávat videa a GIFy + Zobrazit náhledy URL + Kdy načíst obrázek diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e8462631e..e22c6f1b5 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -509,4 +509,26 @@ anz der Bedingungen ist erforderlich Fügt dem Beitrag einen Geohash Ihres Standorts hinzu. Die Öffentlichkeit wird wissen, dass Sie sich innerhalb von 5 km (3 mi) vom aktuellen Standort befinden Fügt eine Warnung für sensiblen Inhalt hinzu, bevor Ihr Inhalt angezeigt wird. Dies ist ideal für NSFW-Inhalte (nicht sicher für die Arbeit) oder Inhalte, die manche Menschen als anstößig oder verstörend empfinden könnten + + Neues Feature + Um diesen Modus zu aktivieren, muss Amethyst eine NIP-24-Nachricht senden (GiftWrapped, Versiegelte Direkt- und Gruppennachrichten). NIP-24 ist neu und die meisten Clients haben es noch nicht implementiert. Stellen Sie sicher, dass der Empfänger einen kompatiblen Client verwendet. + Aktivieren + + Öffentlich + Privat + An + Betreff + Gesprächsthema + "@Benutzer1, @Benutzer2, @Benutzer3" + + Mitglieder dieser Gruppe + Erklärung an Mitglieder + Ändern des Namens für die neuen Ziele. + + Für die App-Benutzeroberfläche + Dunkles, helles oder Systemdesign + Bilder und GIFs automatisch laden + Videos und GIFs automatisch abspielen + URL-Vorschauen anzeigen + Wann Bilder geladen werden sollen \ No newline at end of file diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index 16820baec..0300e5e1c 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -497,6 +497,28 @@ Lägger till en Geohash av din plats i inlägget. Allmänheten kommer att veta att du befinner dig inom 5 km från nuvarande plats Lägger till en varning för känsligt innehåll innan ditt innehåll visas. Detta är idealiskt för NSFW-innehåll (inte säkert för arbete) eller innehåll som vissa personer kan uppleva som stötande eller störande + + Ny Funktion + För att aktivera denna funktion kräver det att Amethyst skickar ett NIP-24 meddelande (GiftWrapped, Förseglade Direkta och Gruppmeddelanden). NIP-24 är nytt och de flesta klienter har ännu inte implementerat det. Se till att mottagaren använder en kompatibel klient. + Aktivera + + Publik + Privat + Till + Ämne + Samtalsämne + "@Användare1, @Användare2, @Användare3" + + Medlemmar i denna grupp + Förklaring till medlemmar + Ändra namnet för de nya målen. + + För appens gränssnitt + Mörkt, Ljust eller Systemtema + Ladda automatiskt bilder och GIFs + Spela upp videor och GIFs automatiskt + Visa förhandsgranskning av URL + När bilder ska laddas