Merge remote-tracking branch 'origin/HEAD' into less_memory_test_branch

This commit is contained in:
Vitor Pamplona
2023-03-14 17:46:29 -04:00
20 changed files with 413 additions and 112 deletions

View File

@@ -28,10 +28,19 @@ jobs:
- name: Build AAB - name: Build AAB
run: ./gradlew clean bundleRelease --stacktrace run: ./gradlew clean bundleRelease --stacktrace
- name: Sign AAB - name: Sign AAB (Google Play)
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/bundle/release releaseDirectory: app/build/outputs/bundle/playRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Sign AAB (F-Droid)
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/bundle/fdroidRelease
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@@ -40,10 +49,19 @@ jobs:
- name: Build APK - name: Build APK
run: ./gradlew assembleRelease --stacktrace --no-daemon run: ./gradlew assembleRelease --stacktrace --no-daemon
- name: Sign APK - name: Sign APK (Google Play)
uses: r0adkll/sign-android-release@v1 uses: r0adkll/sign-android-release@v1
with: with:
releaseDirectory: app/build/outputs/apk/release releaseDirectory: app/build/outputs/apk/play/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Sign APK (F-Droid)
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/fdroid/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }} signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.KEY_ALIAS }} alias: ${{ secrets.KEY_ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }} keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
@@ -60,24 +78,96 @@ jobs:
draft: false draft: false
prerelease: false prerelease: false
- name: Upload APK Asset # Google Play APK
id: upload-release-asset-apk - name: Upload Play APK Universal Asset
id: upload-release-asset-play-universal-apk
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/release/app-release-unsigned-signed.apk asset_path: app/build/outputs/apk/play/release/app-play-universal-release-unsigned-signed.apk
asset_name: app-release-${{ github.ref_name }}.apk asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip asset_content_type: application/zip
- name: Upload AAB Asset - name: Upload Play APK x86 Asset
id: upload-release-asset-aab id: upload-release-asset-play-x86-apk
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
upload_url: ${{ steps.create_release.outputs.upload_url }} upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/release/app-release.aab asset_path: app/build/outputs/apk/play/release/app-play-x86-release-unsigned-signed.apk
asset_name: app-release-${{ github.ref_name }}.aab asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload Play APK x86_64 Asset
id: upload-release-asset-play-x86-64-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/play/release/app-play-x86_64-release-unsigned-signed.apk
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
# F-Droid APK
- name: Upload F-Droid APK Universal Asset
id: upload-release-asset-fdroid-universal-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-universal-release-unsigned-signed.apk
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload F-Droid APK x86 Asset
id: upload-release-asset-fdroid-x86-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
asset_content_type: application/zip
- name: Upload F-Droid APK x86_64 Asset
id: upload-release-asset-fdroid-x86-64-apk
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/fdroid/release/app-fdroid-x86_64-release-unsigned-signed.apk
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
asset_content_type: application/zip
# Google Play AAB
- name: Upload Google Play AAB Asset
id: upload-release-asset-play-aab
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/playRelease/app-play-release.aab
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
asset_content_type: application/zip
# FDroid AAB
- name: Upload F-Droid AAB Asset
id: upload-release-asset-fdroid-aab
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/bundle/fdroidRelease/app-fdroid-release.aab
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
asset_content_type: application/zip asset_content_type: application/zip

View File

@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk 26 minSdk 26
targetSdk 33 targetSdk 33
versionCode 91 versionCode 95
versionName "0.24.2" versionName "0.25.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -33,19 +33,45 @@ android {
resValue "string", "app_name", "@string/app_name_debug" resValue "string", "app_name", "@string/app_name_debug"
} }
} }
flavorDimensions "channel"
productFlavors {
play {
dimension "channel"
}
fdroid {
dimension "channel"
}
}
splits {
abi {
enable true
reset()
include "x86", "x86_64"
universalApk true
}
}
compileOptions { compileOptions {
sourceCompatibility JavaVersion.VERSION_11 sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11
} }
kotlinOptions { kotlinOptions {
jvmTarget = '11' jvmTarget = '11'
} }
buildFeatures { buildFeatures {
compose true compose true
} }
composeOptions { composeOptions {
kotlinCompilerExtensionVersion "1.4.3" kotlinCompilerExtensionVersion "1.4.3"
} }
packagingOptions { packagingOptions {
resources { resources {
excludes += '/META-INF/{AL2.0,LGPL2.1}' excludes += '/META-INF/{AL2.0,LGPL2.1}'
@@ -66,10 +92,10 @@ dependencies {
implementation("androidx.navigation:navigation-compose:$nav_version") implementation("androidx.navigation:navigation-compose:$nav_version")
// Observe Live data as State // Observe Live data as State
implementation 'androidx.compose.runtime:runtime-livedata:1.4.0-beta02' implementation "androidx.compose.runtime:runtime-livedata:$compose_ui_version"
implementation 'androidx.compose.material:material:1.4.0-beta02' implementation "androidx.compose.material:material:$compose_ui_version"
implementation "androidx.compose.material:material-icons-extended:1.4.0-beta02" implementation "androidx.compose.material:material-icons-extended:$compose_ui_version"
// Lifecycle // Lifecycle
implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"
@@ -135,10 +161,10 @@ dependencies {
implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0" implementation "com.halilibo.compose-richtext:richtext-commonmark:0.16.0"
// Local model for language identification // Local model for language identification
implementation 'com.google.mlkit:language-id:17.0.4' playImplementation 'com.google.mlkit:language-id:17.0.4'
// Google services model the translate text // Google services model the translate text
implementation 'com.google.mlkit:translate:17.0.1' playImplementation 'com.google.mlkit:translate:17.0.1'
// Automatic memory leak detection // Automatic memory leak detection
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.10'

View File

@@ -0,0 +1,26 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.navigation.NavController
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun TranslateableRichTextViewer(
content: String,
canPreview: Boolean,
modifier: Modifier = Modifier,
tags: List<List<String>>?,
backgroundColor: Color,
accountViewModel: AccountViewModel,
navController: NavController
) = ExpandableRichTextViewer(
content,
canPreview,
modifier,
tags,
backgroundColor,
accountViewModel,
navController
)

View File

@@ -79,7 +79,7 @@ object LocalPreferences {
} }
val prefs = encryptedPreferences() val prefs = encryptedPreferences()
prefs.edit().apply { prefs.edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma)) putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply() }.apply()
} }
@@ -101,7 +101,7 @@ object LocalPreferences {
if (accounts.remove(npub)) { if (accounts.remove(npub)) {
val prefs = encryptedPreferences() val prefs = encryptedPreferences()
prefs.edit().apply { prefs.edit().apply {
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma)) putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(comma).ifBlank { null })
}.apply() }.apply()
} }
} }

View File

@@ -319,7 +319,7 @@ class Account(
LocalCache.consume(signedEvent, null) LocalCache.consume(signedEvent, null)
} }
fun sendPrivateMeesage(message: String, toUser: String, replyingTo: Note? = null) { fun sendPrivateMessage(message: String, toUser: String, replyingTo: Note? = null) {
if (!isWriteable()) return if (!isWriteable()) return
val user = LocalCache.users[toUser] ?: return val user = LocalCache.users[toUser] ?: return

View File

@@ -218,7 +218,9 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
Row(modifier = Modifier.fillMaxWidth()) { Row(modifier = Modifier.fillMaxWidth()) {
UploadFromGallery( UploadFromGallery(
isUploading = postViewModel.isUploadingImage isUploading = postViewModel.isUploadingImage,
tint = MaterialTheme.colors.primary,
modifier = Modifier.padding(bottom = 10.dp)
) { ) {
postViewModel.upload(it, context) postViewModel.upload(it, context)
} }

View File

@@ -4,23 +4,38 @@ import android.net.Uri
import android.os.Build import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddPhotoAlternate
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState import com.google.accompanist.permissions.rememberPermissionState
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import java.util.concurrent.atomic.AtomicBoolean
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
fun UploadFromGallery( fun UploadFromGallery(
isUploading: Boolean, isUploading: Boolean,
tint: Color,
modifier: Modifier,
onImageChosen: (Uri) -> Unit onImageChosen: (Uri) -> Unit
) { ) {
val cameraPermissionState = val cameraPermissionState =
@@ -43,66 +58,113 @@ fun UploadFromGallery(
} }
} }
) )
} else { }
Box() {
TextButton( UploadBoxButton(isUploading, tint, modifier) {
modifier = Modifier
.align(Alignment.TopCenter),
enabled = !isUploading,
onClick = {
showGallerySelect = true showGallerySelect = true
} }
} else {
UploadBoxButton(isUploading, tint, modifier) {
cameraPermissionState.launchPermissionRequest()
}
}
}
@Composable
private fun UploadBoxButton(
isUploading: Boolean,
tint: Color,
modifier: Modifier,
onClick: () -> Unit
) { ) {
Box() {
IconButton(
modifier = modifier.align(Alignment.Center),
enabled = !isUploading,
onClick = {
onClick()
}
) {
if (!isUploading) {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_add_photo), imageVector = Icons.Default.AddPhotoAlternate,
contentDescription = stringResource(id = R.string.upload_image), contentDescription = stringResource(id = R.string.upload_image),
modifier = Modifier modifier = Modifier.height(25.dp),
.height(20.dp) tint = tint
.padding(end = 8.dp), )
tint = MaterialTheme.colors.primary } else {
LoadingAnimation()
}
}
}
}
@Composable
fun LoadingAnimation(
indicatorSize: Dp = 20.dp,
circleColors: List<Color> = listOf(
Color(0xFF5851D8),
Color(0xFF833AB4),
Color(0xFFC13584),
Color(0xFFE1306C),
Color(0xFFFD1D1D),
Color(0xFFF56040),
Color(0xFFF77737),
Color(0xFFFCAF45),
Color(0xFFFFDC80),
Color(0xFF5851D8)
),
animationDuration: Int = 1000
) {
val infiniteTransition = rememberInfiniteTransition()
val rotateAnimation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = animationDuration,
easing = LinearEasing
)
)
) )
if (!isUploading) { CircularProgressIndicator(
Text(stringResource(R.string.upload_image)) modifier = Modifier
} else { .size(size = indicatorSize)
Text(stringResource(R.string.uploading)) .rotate(degrees = rotateAnimation)
} .border(
} width = 4.dp,
} brush = Brush.sweepGradient(circleColors),
} shape = CircleShape
} else { ),
Column { progress = 1f,
Button( strokeWidth = 1.dp,
onClick = { cameraPermissionState.launchPermissionRequest() }, color = MaterialTheme.colors.background // Set background color
enabled = !isUploading )
) {
if (!isUploading) {
Text(stringResource(R.string.upload_image))
} else {
Text(stringResource(R.string.uploading))
}
}
}
}
} }
@Composable @Composable
fun GallerySelect( fun GallerySelect(
onImageUri: (Uri?) -> Unit = { } onImageUri: (Uri?) -> Unit = { }
) { ) {
var hasLaunched by remember { mutableStateOf(AtomicBoolean(false)) }
val launcher = rememberLauncherForActivityResult( val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(), contract = ActivityResultContracts.GetContent(),
onResult = { uri: Uri? -> onResult = { uri: Uri? ->
onImageUri(uri) onImageUri(uri)
hasLaunched.set(false)
} }
) )
@Composable @Composable
fun LaunchGallery() { fun LaunchGallery() {
SideEffect { SideEffect {
if (!hasLaunched.getAndSet(true)) {
launcher.launch("image/*") launcher.launch("image/*")
} }
} }
}
LaunchGallery() LaunchGallery()
} }

View File

@@ -1,11 +1,20 @@
package com.vitorpamplona.amethyst.ui.components package com.vitorpamplona.amethyst.ui.components
import android.content.Context import android.content.Context
import android.net.Uri
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import coil.ImageLoader
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.fetch.Fetcher
import coil.fetch.SourceResult
import coil.request.ImageRequest import coil.request.ImageRequest
import java.nio.ByteBuffer import coil.request.Options
import okio.Buffer
import java.security.MessageDigest import java.security.MessageDigest
import kotlin.time.ExperimentalTime
import kotlin.time.measureTimedValue
private fun toHex(color: Color): String { private fun toHex(color: Color): String {
val argb = color.toArgb() val argb = color.toArgb()
@@ -40,8 +49,7 @@ private fun svgString(msg: String): String {
val mouth = mouths[mouthIndex] val mouth = mouths[mouthIndex]
val accessory = accessories[accIndex] val accessory = accessories[accIndex]
return """ return """<svg id="$hashHex" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
<svg id="$hashHex" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300">
<defs> <defs>
<style> <style>
.cls-bg{fill:${toHex(bgColor)}}.cls-fill-1{fill:${toHex(fgColor)};}.cls-fill-2{fill:${toHex(fgColor)};} .cls-bg{fill:${toHex(bgColor)}}.cls-fill-1{fill:${toHex(fgColor)};}.cls-fill-2{fill:${toHex(fgColor)};}
@@ -50,19 +58,45 @@ private fun svgString(msg: String): String {
</defs> </defs>
<title>Robohash $hashHex</title> <title>Robohash $hashHex</title>
${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths} ${background}${body.paths}${face.paths}${eye.paths}${mouth.paths}${accessory.paths}
</svg> </svg>"""
""".trimIndent()
} }
class HashImageFetcher(
private val context: Context,
private val data: Uri
) : Fetcher {
@OptIn(ExperimentalTime::class)
override suspend fun fetch(): SourceResult {
val (value, elapsed) = measureTimedValue {
val source = try {
Buffer().apply { write(svgString(data.toString()).toByteArray()) }
} finally {
}
SourceResult(
source = ImageSource(source, context),
mimeType = null,
dataSource = DataSource.MEMORY
)
}
println("Elapsed: $elapsed")
return value
}
class Factory : Fetcher.Factory<Uri> {
override fun create(data: Uri, options: Options, imageLoader: ImageLoader): Fetcher {
return HashImageFetcher(options.context, data)
}
}
}
object Robohash { object Robohash {
fun imageRequest(context: Context, message: String): ImageRequest { fun imageRequest(context: Context, message: String): ImageRequest {
return ImageRequest return ImageRequest
.Builder(context) .Builder(context)
.data( .data("robohash:$message")
ByteBuffer.wrap( .fetcherFactory(HashImageFetcher.Factory())
svgString(message).toByteArray()
)
)
.crossfade(100) .crossfade(100)
.build() .build()
} }

View File

@@ -99,6 +99,7 @@ fun MainTopBar(scaffoldState: ScaffoldState, accountViewModel: AccountViewModel)
Column() { Column() {
TopAppBar( TopAppBar(
elevation = 0.dp, elevation = 0.dp,
backgroundColor = MaterialTheme.colors.surface,
title = { title = {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),

View File

@@ -344,7 +344,7 @@ fun ChatroomMessageCompose(
} }
} }
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel) NoteDropDownMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
} }
} }
} }

View File

@@ -52,7 +52,6 @@ class AccountStateViewModel() : ViewModel() {
Account(Persona(Hex.decode(key))) Account(Persona(Hex.decode(key)))
} }
LocalPreferences.updatePrefsForLogin(account)
login(account) login(account)
} }
@@ -64,7 +63,6 @@ class AccountStateViewModel() : ViewModel() {
fun newKey() { fun newKey() {
val account = Account(Persona()) val account = Account(Persona())
LocalPreferences.updatePrefsForLogin(account)
login(account) login(account)
} }

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -65,7 +66,9 @@ import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrChannelDataSource import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.ui.actions.NewChannelView import com.vitorpamplona.amethyst.ui.actions.NewChannelView
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter import com.vitorpamplona.amethyst.ui.dal.ChannelFeedFilter
@@ -82,9 +85,10 @@ fun ChannelScreen(
) { ) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account val account = accountState?.account
val context = LocalContext.current
val channelScreenModel: NewPostViewModel = viewModel()
if (account != null && channelId != null) { if (account != null && channelId != null) {
val newPost = remember { mutableStateOf(TextFieldValue("")) }
val replyTo = remember { mutableStateOf<Note?>(null) } val replyTo = remember { mutableStateOf<Note?>(null) }
ChannelFeedFilter.loadMessagesBetween(account, channelId) ChannelFeedFilter.loadMessagesBetween(account, channelId)
@@ -98,6 +102,9 @@ fun ChannelScreen(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
feedViewModel.invalidateData() feedViewModel.invalidateData()
channelScreenModel.imageUploadingError.collect { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
} }
DisposableEffect(channelId) { DisposableEffect(channelId) {
@@ -178,8 +185,10 @@ fun ChannelScreen(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
TextField( TextField(
value = newPost.value, value = channelScreenModel.message,
onValueChange = { newPost.value = it }, onValueChange = {
channelScreenModel.updateMessage(it)
},
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences capitalization = KeyboardCapitalization.Sentences
), ),
@@ -195,15 +204,24 @@ fun ChannelScreen(
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendChannelMessage(newPost.value.text, channel.idHex, replyTo.value, null) account.sendChannelMessage(channelScreenModel.message.text, channel.idHex, replyTo.value, null)
newPost.value = TextFieldValue("") channelScreenModel.message = TextFieldValue("")
replyTo.value = null replyTo.value = null
feedViewModel.refresh() // Don't wait a full second before updating feedViewModel.refresh() // Don't wait a full second before updating
}, },
newPost.value.text.isNotBlank(), isActive = channelScreenModel.message.text.isNotBlank() && !channelScreenModel.isUploadingImage,
modifier = Modifier.padding(end = 10.dp) modifier = Modifier.padding(end = 10.dp)
) )
}, },
leadingIcon = {
UploadFromGallery(
isUploading = channelScreenModel.isUploadingImage,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
channelScreenModel.upload(it, context)
}
},
colors = TextFieldDefaults.textFieldColors( colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent unfocusedIndicatorColor = Color.Transparent

View File

@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.screen.loggedIn package com.vitorpamplona.amethyst.ui.screen.loggedIn
import android.widget.Toast
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -36,6 +37,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -50,7 +52,9 @@ import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrChatroomDataSource import com.vitorpamplona.amethyst.service.NostrChatroomDataSource
import com.vitorpamplona.amethyst.ui.actions.NewPostViewModel
import com.vitorpamplona.amethyst.ui.actions.PostButton import com.vitorpamplona.amethyst.ui.actions.PostButton
import com.vitorpamplona.amethyst.ui.actions.UploadFromGallery
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.ResizeImage import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
@@ -64,9 +68,10 @@ import com.vitorpamplona.amethyst.ui.screen.NostrChatRoomFeedViewModel
fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) { fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navController: NavController) {
val accountState by accountViewModel.accountLiveData.observeAsState() val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account val account = accountState?.account
val context = LocalContext.current
val chatRoomScreenModel: NewPostViewModel = viewModel()
if (account != null && userId != null) { if (account != null && userId != null) {
val newPost = remember { mutableStateOf(TextFieldValue("")) }
val replyTo = remember { mutableStateOf<Note?>(null) } val replyTo = remember { mutableStateOf<Note?>(null) }
ChatroomFeedFilter.loadMessagesBetween(account, userId) ChatroomFeedFilter.loadMessagesBetween(account, userId)
@@ -77,6 +82,9 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
LaunchedEffect(userId) { LaunchedEffect(userId) {
feedViewModel.refresh() feedViewModel.refresh()
chatRoomScreenModel.imageUploadingError.collect { error ->
Toast.makeText(context, error, Toast.LENGTH_SHORT).show()
}
} }
DisposableEffect(userId) { DisposableEffect(userId) {
@@ -156,8 +164,8 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
TextField( TextField(
value = newPost.value, value = chatRoomScreenModel.message,
onValueChange = { newPost.value = it }, onValueChange = { chatRoomScreenModel.updateMessage(it) },
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences capitalization = KeyboardCapitalization.Sentences
), ),
@@ -173,15 +181,24 @@ fun ChatroomScreen(userId: String?, accountViewModel: AccountViewModel, navContr
trailingIcon = { trailingIcon = {
PostButton( PostButton(
onPost = { onPost = {
account.sendPrivateMeesage(newPost.value.text, userId, replyTo.value) account.sendPrivateMessage(chatRoomScreenModel.message.text, userId, replyTo.value)
newPost.value = TextFieldValue("") chatRoomScreenModel.message = TextFieldValue("")
replyTo.value = null replyTo.value = null
feedViewModel.refresh() // Don't wait a full second before updating feedViewModel.refresh() // Don't wait a full second before updating
}, },
newPost.value.text.isNotBlank(), isActive = chatRoomScreenModel.message.text.isNotBlank() && !chatRoomScreenModel.isUploadingImage,
modifier = Modifier.padding(end = 10.dp) modifier = Modifier.padding(end = 10.dp)
) )
}, },
leadingIcon = {
UploadFromGallery(
isUploading = chatRoomScreenModel.isUploadingImage,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f),
modifier = Modifier.padding(start = 5.dp)
) {
chatRoomScreenModel.upload(it, context)
}
},
colors = TextFieldDefaults.textFieldColors( colors = TextFieldDefaults.textFieldColors(
focusedIndicatorColor = Color.Transparent, focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent unfocusedIndicatorColor = Color.Transparent

View File

@@ -25,6 +25,8 @@ import com.google.accompanist.pager.PagerState
import com.google.accompanist.pager.pagerTabIndicatorOffset import com.google.accompanist.pager.pagerTabIndicatorOffset
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.service.NostrHomeDataSource import com.vitorpamplona.amethyst.service.NostrHomeDataSource
import com.vitorpamplona.amethyst.ui.dal.HomeConversationsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.HomeNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.FeedView
import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel import com.vitorpamplona.amethyst.ui.screen.NostrHomeFeedViewModel
@@ -43,10 +45,12 @@ fun HomeScreen(
scrollToTop: Boolean = false scrollToTop: Boolean = false
) { ) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val account = accountViewModel.accountLiveData.value?.account ?: return
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
HomeNewThreadFeedFilter.account = account
HomeConversationsFeedFilter.account = account
NostrHomeDataSource.resetFilters() NostrHomeDataSource.resetFilters()
homeFeedViewModel.refresh() homeFeedViewModel.refresh()
repliesFeedViewModel.refresh() repliesFeedViewModel.refresh()
} }

View File

@@ -54,6 +54,7 @@ import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrGlobalDataSource import com.vitorpamplona.amethyst.service.NostrGlobalDataSource
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
import com.vitorpamplona.amethyst.ui.dal.GlobalFeedFilter
import com.vitorpamplona.amethyst.ui.note.ChannelName import com.vitorpamplona.amethyst.ui.note.ChannelName
import com.vitorpamplona.amethyst.ui.note.NoteCompose import com.vitorpamplona.amethyst.ui.note.NoteCompose
import com.vitorpamplona.amethyst.ui.note.UserCompose import com.vitorpamplona.amethyst.ui.note.UserCompose
@@ -81,8 +82,11 @@ fun SearchScreen(
scrollToTop: Boolean = false scrollToTop: Boolean = false
) { ) {
val lifeCycleOwner = LocalLifecycleOwner.current val lifeCycleOwner = LocalLifecycleOwner.current
val account = accountViewModel.accountLiveData.value?.account ?: return
LaunchedEffect(accountViewModel) { LaunchedEffect(accountViewModel) {
GlobalFeedFilter.account = account
NostrGlobalDataSource.resetFilters()
feedViewModel.refresh() feedViewModel.refresh()
} }

View File

@@ -8,10 +8,10 @@
<string name="show_anyway">Laat toch zien</string> <string name="show_anyway">Laat toch zien</string>
<string name="post_was_flagged_as_inappropriate_by">Bericht gemarkeerd als ongepast door</string> <string name="post_was_flagged_as_inappropriate_by">Bericht gemarkeerd als ongepast door</string>
<string name="post_not_found">Bericht niet gevonden</string> <string name="post_not_found">Bericht niet gevonden</string>
<string name="channel_image">Groepsafbeelding</string> <string name="channel_image">Kanaalafbeelding</string>
<string name="referenced_event_not_found">Verwezen event niet gevonden</string> <string name="referenced_event_not_found">Verwezen event niet gevonden</string>
<string name="could_not_decrypt_the_message">Note versleuteld met encryptie</string> <string name="could_not_decrypt_the_message">Note versleuteld met encryptie</string>
<string name="group_picture">Kanaal-afbeelding</string> <string name="group_picture">Groepafbeelding</string>
<string name="explicit_content">Expliciete inhoud</string> <string name="explicit_content">Expliciete inhoud</string>
<string name="spam">Spam</string> <string name="spam">Spam</string>
<string name="impersonation">Imitatie</string> <string name="impersonation">Imitatie</string>
@@ -53,7 +53,7 @@
<string name="lightning_invoice">Lightning invoice</string> <string name="lightning_invoice">Lightning invoice</string>
<string name="pay">Betalen</string> <string name="pay">Betalen</string>
<string name="lightning_tips">Lightning Tips</string> <string name="lightning_tips">Lightning Tips</string>
<string name="note_to_receiver">Notitie aan ontvanger</string> <string name="note_to_receiver">Note aan ontvanger</string>
<string name="thank_you_so_much">Hartelijk bedankt!</string> <string name="thank_you_so_much">Hartelijk bedankt!</string>
<string name="amount_in_sats">Bedrag in sats</string> <string name="amount_in_sats">Bedrag in sats</string>
<string name="send_sats">Verstuur sats</string> <string name="send_sats">Verstuur sats</string>
@@ -97,16 +97,16 @@
<string name="uploading">Uploaden…</string> <string name="uploading">Uploaden…</string>
<string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Gebruiker heeft geen Lightning Adress ingesteld om sats te ontvangen</string> <string name="user_does_not_have_a_lightning_address_setup_to_receive_sats">Gebruiker heeft geen Lightning Adress ingesteld om sats te ontvangen</string>
<string name="reply_here">"hier reageren.. "</string> <string name="reply_here">"hier reageren.. "</string>
<string name="copies_the_note_id_to_the_clipboard_for_sharing">Kopieert Notitie ID naar klembord om te delen</string> <string name="copies_the_note_id_to_the_clipboard_for_sharing">Kopieert note ID naar klembord om te delen</string>
<string name="copy_channel_id_note_to_the_clipboard">Kopieer kanaal ID (Notitie) naar klembord</string> <string name="copy_channel_id_note_to_the_clipboard">Kopieer kanaal ID (note) naar klembord</string>
<string name="edits_the_channel_metadata">Past de kanaal-metadata aan</string> <string name="edits_the_channel_metadata">Past de kanaal-metadata aan</string>
<string name="join">Lid worden</string> <string name="join">Lid worden</string>
<string name="known">Bekend</string> <string name="known">Bekend</string>
<string name="new_requests">Nieuw verzoek</string> <string name="new_requests">Nieuw verzoek</string>
<string name="blocked_users">Geblokeerde gebruikers</string> <string name="blocked_users">Geblokeerde gebruikers</string>
<string name="new_threads">Nieuwe notities</string> <string name="new_threads">Nieuwe notes</string>
<string name="conversations">Gesprekken</string> <string name="conversations">Gesprekken</string>
<string name="notes">Notities</string> <string name="notes">Notes</string>
<string name="replies">Reacties</string> <string name="replies">Reacties</string>
<string name="follows">"Volgend"</string> <string name="follows">"Volgend"</string>
<string name="reports">"Rapporten"</string> <string name="reports">"Rapporten"</string>
@@ -174,9 +174,9 @@
<string name="report_hateful_speech">Haatdragende taal rapporteren</string> <string name="report_hateful_speech">Haatdragende taal rapporteren</string>
<string name="report_nudity_porn">Naaktheid / porno rapporteren</string> <string name="report_nudity_porn">Naaktheid / porno rapporteren</string>
<string name="others">anderen</string> <string name="others">anderen</string>
<string name="mark_all_known_as_read">Alle bekende als gelezen markeren</string> <string name="mark_all_known_as_read">Bekende markeren als gelezen</string>
<string name="mark_all_new_as_read">Alle nieuwe als gelezen markeren</string> <string name="mark_all_new_as_read">Nieuwe markeren als gelezen</string>
<string name="mark_all_as_read">Alles als gelezen markeren</string> <string name="mark_all_as_read">Alles markeren als gelezen</string>
<string name="backup_keys">Back-up sleutels</string> <string name="backup_keys">Back-up sleutels</string>
<string name="account_backup_tips_md" tools:ignore="Typos"> <string name="account_backup_tips_md" tools:ignore="Typos">
## Sleutel back-up en veiligheidstips ## Sleutel back-up en veiligheidstips
@@ -193,19 +193,38 @@
<string name="badge_award_image_for">"Badge award afbeelding voor %1$s"</string> <string name="badge_award_image_for">"Badge award afbeelding voor %1$s"</string>
<string name="new_badge_award_notif">Je hebt een nieuwe Badge award ontvangen</string> <string name="new_badge_award_notif">Je hebt een nieuwe Badge award ontvangen</string>
<string name="award_granted_to">Badge award gegeven aan</string> <string name="award_granted_to">Badge award gegeven aan</string>
<string name="copied_note_text_to_clipboard">Notitie tekst gekopieerd naar klembord</string> <string name="copied_note_text_to_clipboard">Note tekst gekopieerd naar klembord</string>
<string name="copied_user_id_to_clipboard" tools:ignore="Typos">@npub auteur gekopieerd naar klembord</string> <string name="copied_user_id_to_clipboard" tools:ignore="Typos">@npub auteur gekopieerd naar klembord</string>
<string name="copied_note_id_to_clipboard" tools:ignore="Typos">Notitie ID (@note1) gekopieerd naar klembord</string> <string name="copied_note_id_to_clipboard" tools:ignore="Typos">Note ID (@note1) gekopieerd naar klembord</string>
<string name="select_text_dialog_top">Selecteer tekst</string> <string name="select_text_dialog_top">Selecteer tekst</string>
<string name="quick_action_select">Selecteer</string> <string name="quick_action_select">Selecteer</string>
<string name="quick_action_share_browser_link">Deel browserlink</string> <string name="quick_action_share_browser_link">Deel browserlink</string>
<string name="quick_action_share">Delen</string> <string name="quick_action_share">Delen</string>
<string name="quick_action_copy_user_id">Auteur ID</string> <string name="quick_action_copy_user_id">Auteur ID</string>
<string name="quick_action_copy_note_id">Notitie ID</string> <string name="quick_action_copy_note_id">Note ID</string>
<string name="quick_action_copy_text">Kopieer tekst</string> <string name="quick_action_copy_text">Kopieer tekst</string>
<string name="quick_action_delete">Verwijderen</string> <string name="quick_action_delete">Verwijderen</string>
<string name="quick_action_unfollow">Ontvolgen</string> <string name="quick_action_unfollow">Ontvolgen</string>
<string name="quick_action_follow">Volgen</string> <string name="quick_action_follow">Volgen</string>
<string name="quick_action_request_deletion_alert_title">Verzoek om te verwijderen</string> <string name="quick_action_request_deletion_alert_title">Verzoek om te verwijderen</string>
<string name="quick_action_request_deletion_alert_body">Amethyst zal vragen om uw notitie te verwijderen van de relays waarmee u momenteel verbonden bent. Er is geen garantie dat uw notitie permanent wordt verwijderd van deze relays, of van andere relays waar het kan worden opgeslagen.</string> <string name="quick_action_request_deletion_alert_body">Amethyst zal vragen om uw note te verwijderen van de relays waarmee u momenteel verbonden bent. Er is geen garantie dat uw note permanent wordt verwijderd van deze relays, of van andere relays waar het kan worden opgeslagen.</string>
<string name="github" translatable="false">Github Gist w/ Proof</string>
<string name="telegram" translatable="false">Telegram</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
<string name="private_conversation_notification">"&lt;Kan privébericht niet ontsleutelen&gt;\n\nJe werd genoemd in een privégesprek tussen %1$s en %2$s."</string>
<string name="quick_action_delete_button">Verwijderen</string>
<string name="quick_action_dont_show_again_button">Niet meer laten zien</string>
<string name="account_switch_add_account_dialog_title">Nieuw account toevoegen</string>
<string name="drawer_accounts">Accounts</string>
<string name="account_switch_select_account">Account selecteren</string>
<string name="account_switch_add_account_btn">Nieuw account toevoegen</string>
<string name="account_switch_active_account">Account activeren</string>
<string name="account_switch_has_private_key">Heeft privésleutel</string>
<string name="account_switch_pubkey_only">Alleen lezen, geen privésleutel</string>
<string name="back">Terug</string>
</resources> </resources>

View File

@@ -1,16 +1,16 @@
buildscript { buildscript {
ext { ext {
fragment_version = "1.5.5" fragment_version = "1.5.5"
lifecycle_version = '2.6.0-rc1' lifecycle_version = '2.6.0'
compose_ui_version = '1.4.0-beta02' compose_ui_version = '1.4.0-rc01'
nav_version = "2.5.3" nav_version = "2.5.3"
room_version = "2.4.3" room_version = "2.4.3"
accompanist_version = "0.28.0" accompanist_version = "0.28.0"
} }
}// Top-level build file where you can add configuration options common to all sub-projects/modules. }// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins { plugins {
id 'com.android.application' version '7.4.1' apply false id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.1' apply false id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false id 'org.jetbrains.kotlin.jvm' version '1.8.10' apply false
} }