mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-10-09 23:42:32 +02:00
New options to choose which language to translate to and which languages to block translations from
This commit is contained in:
@@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.User
|
|||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
|
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.navigation.Route
|
import com.vitorpamplona.amethyst.ui.navigation.Route
|
||||||
|
import java.util.Locale
|
||||||
import nostr.postr.Persona
|
import nostr.postr.Persona
|
||||||
import nostr.postr.events.ContactListEvent
|
import nostr.postr.events.ContactListEvent
|
||||||
import nostr.postr.events.Event
|
import nostr.postr.events.Event
|
||||||
@@ -26,6 +27,8 @@ class LocalPreferences(context: Context) {
|
|||||||
remove("following_channels")
|
remove("following_channels")
|
||||||
remove("hidden_users")
|
remove("hidden_users")
|
||||||
remove("relays")
|
remove("relays")
|
||||||
|
remove("dontTranslateFrom")
|
||||||
|
remove("translateTo")
|
||||||
}.apply()
|
}.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +39,8 @@ class LocalPreferences(context: Context) {
|
|||||||
account.followingChannels.let { putStringSet("following_channels", it) }
|
account.followingChannels.let { putStringSet("following_channels", it) }
|
||||||
account.hiddenUsers.let { putStringSet("hidden_users", it) }
|
account.hiddenUsers.let { putStringSet("hidden_users", it) }
|
||||||
account.localRelays.let { putString("relays", gson.toJson(it)) }
|
account.localRelays.let { putString("relays", gson.toJson(it)) }
|
||||||
|
account.dontTranslateFrom.let { putStringSet("dontTranslateFrom", it) }
|
||||||
|
account.translateTo.let { putString("translateTo", it) }
|
||||||
}.apply()
|
}.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,19 +48,24 @@ class LocalPreferences(context: Context) {
|
|||||||
encryptedPreferences.apply {
|
encryptedPreferences.apply {
|
||||||
val privKey = getString("nostr_privkey", null)
|
val privKey = getString("nostr_privkey", null)
|
||||||
val pubKey = getString("nostr_pubkey", null)
|
val pubKey = getString("nostr_pubkey", null)
|
||||||
val followingChannels = getStringSet("following_channels", null)?.toMutableSet() ?: mutableSetOf()
|
val followingChannels = getStringSet("following_channels", null) ?: setOf()
|
||||||
val hiddenUsers = getStringSet("hidden_users", emptySet())?.toMutableSet() ?: mutableSetOf()
|
val hiddenUsers = getStringSet("hidden_users", emptySet()) ?: setOf()
|
||||||
val localRelays = gson.fromJson(
|
val localRelays = gson.fromJson(
|
||||||
getString("relays", "[]"),
|
getString("relays", "[]"),
|
||||||
object : TypeToken<Set<NewRelayListViewModel.Relay>>() {}.type
|
object : TypeToken<Set<NewRelayListViewModel.Relay>>() {}.type
|
||||||
) ?: setOf<NewRelayListViewModel.Relay>()
|
) ?: setOf<NewRelayListViewModel.Relay>()
|
||||||
|
|
||||||
|
val dontTranslateFrom = getStringSet("dontTranslateFrom", null) ?: setOf()
|
||||||
|
val translateTo = getString("translateTo", null) ?: Locale.getDefault().language
|
||||||
|
|
||||||
if (pubKey != null) {
|
if (pubKey != null) {
|
||||||
return Account(
|
return Account(
|
||||||
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
|
Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()),
|
||||||
followingChannels,
|
followingChannels,
|
||||||
hiddenUsers,
|
hiddenUsers,
|
||||||
localRelays
|
localRelays,
|
||||||
|
dontTranslateFrom,
|
||||||
|
translateTo
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return null
|
return null
|
||||||
|
@@ -1,5 +1,9 @@
|
|||||||
package com.vitorpamplona.amethyst.model
|
package com.vitorpamplona.amethyst.model
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import com.vitorpamplona.amethyst.service.relays.Constants
|
import com.vitorpamplona.amethyst.service.relays.Constants
|
||||||
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
import com.vitorpamplona.amethyst.service.model.ChannelCreateEvent
|
||||||
@@ -14,6 +18,8 @@ import com.vitorpamplona.amethyst.service.relays.Relay
|
|||||||
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
import com.vitorpamplona.amethyst.service.relays.RelayPool
|
||||||
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
|
import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -34,13 +40,23 @@ val DefaultChannels = setOf(
|
|||||||
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
|
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun getLanguagesSpokenByUser(): Set<String> {
|
||||||
|
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
|
||||||
|
val codedList = mutableSetOf<String>()
|
||||||
|
for (i in 0 until languageList.size()) {
|
||||||
|
languageList.get(i)?.let { codedList.add(it.language) }
|
||||||
|
}
|
||||||
|
return codedList
|
||||||
|
}
|
||||||
|
|
||||||
class Account(
|
class Account(
|
||||||
val loggedIn: Persona,
|
val loggedIn: Persona,
|
||||||
var followingChannels: Set<String> = DefaultChannels,
|
var followingChannels: Set<String> = DefaultChannels,
|
||||||
var hiddenUsers: Set<String> = setOf(),
|
var hiddenUsers: Set<String> = setOf(),
|
||||||
var localRelays: Set<NewRelayListViewModel.Relay> = Constants.defaultRelays.toSet()
|
var localRelays: Set<NewRelayListViewModel.Relay> = Constants.defaultRelays.toSet(),
|
||||||
|
var dontTranslateFrom: Set<String> = getLanguagesSpokenByUser(),
|
||||||
|
var translateTo: String = Locale.getDefault().language
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun userProfile(): User {
|
fun userProfile(): User {
|
||||||
return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey())
|
return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey())
|
||||||
}
|
}
|
||||||
@@ -265,22 +281,22 @@ class Account(
|
|||||||
|
|
||||||
fun joinChannel(idHex: String) {
|
fun joinChannel(idHex: String) {
|
||||||
followingChannels = followingChannels + idHex
|
followingChannels = followingChannels + idHex
|
||||||
invalidateData()
|
invalidateData(live)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun leaveChannel(idHex: String) {
|
fun leaveChannel(idHex: String) {
|
||||||
followingChannels = followingChannels - idHex
|
followingChannels = followingChannels - idHex
|
||||||
invalidateData()
|
invalidateData(live)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideUser(pubkeyHex: String) {
|
fun hideUser(pubkeyHex: String) {
|
||||||
hiddenUsers = hiddenUsers + pubkeyHex
|
hiddenUsers = hiddenUsers + pubkeyHex
|
||||||
invalidateData()
|
invalidateData(live)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showUser(pubkeyHex: String) {
|
fun showUser(pubkeyHex: String) {
|
||||||
hiddenUsers = hiddenUsers - pubkeyHex
|
hiddenUsers = hiddenUsers - pubkeyHex
|
||||||
invalidateData()
|
invalidateData(live)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
fun sendChangeChannel(name: String, about: String, picture: String, channel: Channel) {
|
||||||
@@ -332,6 +348,16 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun addDontTranslateFrom(languageCode: String) {
|
||||||
|
dontTranslateFrom = dontTranslateFrom.plus(languageCode)
|
||||||
|
invalidateData(liveLanguages)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateTranslateTo(languageCode: String) {
|
||||||
|
translateTo = languageCode
|
||||||
|
invalidateData(liveLanguages)
|
||||||
|
}
|
||||||
|
|
||||||
fun activeRelays(): Array<Relay>? {
|
fun activeRelays(): Array<Relay>? {
|
||||||
return userProfile().relays?.map {
|
return userProfile().relays?.map {
|
||||||
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
|
val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet()
|
||||||
@@ -357,19 +383,20 @@ class Account(
|
|||||||
|
|
||||||
// Observers line up here.
|
// Observers line up here.
|
||||||
val live: AccountLiveData = AccountLiveData(this)
|
val live: AccountLiveData = AccountLiveData(this)
|
||||||
|
val liveLanguages: AccountLiveData = AccountLiveData(this)
|
||||||
|
|
||||||
|
var handlerWaiting = AtomicBoolean()
|
||||||
|
|
||||||
// Refreshes observers in batches.
|
|
||||||
var handlerWaiting = false
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun invalidateData() {
|
private fun invalidateData(live: AccountLiveData) {
|
||||||
if (handlerWaiting) return
|
if (handlerWaiting.getAndSet(true)) return
|
||||||
|
|
||||||
handlerWaiting = true
|
handlerWaiting.set(true)
|
||||||
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
val scope = CoroutineScope(Job() + Dispatchers.Default)
|
||||||
scope.launch {
|
scope.launch {
|
||||||
delay(100)
|
delay(100)
|
||||||
live.refresh()
|
live.refresh()
|
||||||
handlerWaiting = false
|
handlerWaiting.set(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -412,7 +439,6 @@ class Account(
|
|||||||
localRelays = value.toSet()
|
localRelays = value.toSet()
|
||||||
sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } )
|
sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } )
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
class AccountLiveData(private val account: Account): LiveData<AccountState>(AccountState(account)) {
|
||||||
|
@@ -0,0 +1,84 @@
|
|||||||
|
package com.vitorpamplona.amethyst.service.lang
|
||||||
|
|
||||||
|
import android.util.LruCache
|
||||||
|
import com.google.android.gms.tasks.Task
|
||||||
|
import com.google.android.gms.tasks.Tasks
|
||||||
|
import com.google.mlkit.nl.languageid.LanguageIdentification
|
||||||
|
import com.google.mlkit.nl.translate.TranslateLanguage
|
||||||
|
import com.google.mlkit.nl.translate.Translation
|
||||||
|
import com.google.mlkit.nl.translate.Translator
|
||||||
|
import com.google.mlkit.nl.translate.TranslatorOptions
|
||||||
|
import java.util.ArrayList
|
||||||
|
|
||||||
|
class ResultOrError(
|
||||||
|
var result: String?,
|
||||||
|
var sourceLang: String?,
|
||||||
|
var targetLang: String?,
|
||||||
|
var error: Exception?
|
||||||
|
)
|
||||||
|
|
||||||
|
object LanguageTranslatorService {
|
||||||
|
private val languageIdentification = LanguageIdentification.getClient()
|
||||||
|
|
||||||
|
private val translators =
|
||||||
|
object : LruCache<TranslatorOptions, Translator>(10) {
|
||||||
|
override fun create(options: TranslatorOptions): Translator {
|
||||||
|
return Translation.getClient(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun entryRemoved(
|
||||||
|
evicted: Boolean,
|
||||||
|
key: TranslatorOptions,
|
||||||
|
oldValue: Translator,
|
||||||
|
newValue: Translator?
|
||||||
|
) {
|
||||||
|
oldValue.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun identifyLanguage(text: String): Task<String> {
|
||||||
|
return languageIdentification.identifyLanguage(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun translate(text: String, source: String, target: String): Task<ResultOrError> {
|
||||||
|
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
|
||||||
|
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
|
||||||
|
if (sourceLangCode == null || targetLangCode == null) {
|
||||||
|
return Tasks.forCanceled()
|
||||||
|
}
|
||||||
|
|
||||||
|
val options = TranslatorOptions.Builder()
|
||||||
|
.setSourceLanguage(sourceLangCode)
|
||||||
|
.setTargetLanguage(targetLangCode)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val translator = translators[options]
|
||||||
|
|
||||||
|
return translator.downloadModelIfNeeded().onSuccessTask {
|
||||||
|
val tasks = mutableListOf<Task<String>>()
|
||||||
|
for (paragraph in text.split("\n")) {
|
||||||
|
tasks.add(translator.translate(paragraph))
|
||||||
|
}
|
||||||
|
|
||||||
|
Tasks.whenAll(tasks).continueWith {
|
||||||
|
val results: MutableList<String> = ArrayList()
|
||||||
|
for (task in tasks) {
|
||||||
|
results.add(task.result)
|
||||||
|
}
|
||||||
|
ResultOrError(results.joinToString("\n"), source, target, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun autoTranslate(text: String, dontTranslateFrom: Set<String>, translateTo: String): Task<ResultOrError> {
|
||||||
|
return identifyLanguage(text).onSuccessTask {
|
||||||
|
if (it == translateTo) {
|
||||||
|
Tasks.forCanceled()
|
||||||
|
} else if (it != "und" && !dontTranslateFrom.contains(it)) {
|
||||||
|
translate(text, it, translateTo)
|
||||||
|
} else {
|
||||||
|
Tasks.forCanceled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -1,7 +1,6 @@
|
|||||||
package com.vitorpamplona.amethyst.ui.components
|
package com.vitorpamplona.amethyst.ui.components
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import android.util.LruCache
|
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -10,10 +9,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.foundation.text.ClickableText
|
import androidx.compose.foundation.text.ClickableText
|
||||||
import androidx.compose.material.*
|
import androidx.compose.material.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.style.TextDirection
|
import androidx.compose.ui.text.style.TextDirection
|
||||||
@@ -23,19 +24,17 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.core.os.ConfigurationCompat
|
import androidx.core.os.ConfigurationCompat
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import com.google.accompanist.flowlayout.FlowRow
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
import com.google.android.gms.tasks.Task
|
import com.vitorpamplona.amethyst.LocalPreferences
|
||||||
import com.google.android.gms.tasks.Tasks
|
|
||||||
import com.google.mlkit.nl.languageid.LanguageIdentification
|
|
||||||
import com.google.mlkit.nl.translate.TranslateLanguage
|
|
||||||
import com.google.mlkit.nl.translate.Translation
|
|
||||||
import com.google.mlkit.nl.translate.Translator
|
|
||||||
import com.google.mlkit.nl.translate.TranslatorOptions
|
|
||||||
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
|
import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil
|
||||||
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.model.LocalCache
|
import com.vitorpamplona.amethyst.model.LocalCache
|
||||||
import com.vitorpamplona.amethyst.model.toByteArray
|
import com.vitorpamplona.amethyst.model.toByteArray
|
||||||
import com.vitorpamplona.amethyst.model.toNote
|
import com.vitorpamplona.amethyst.model.toNote
|
||||||
import com.vitorpamplona.amethyst.service.Nip19
|
import com.vitorpamplona.amethyst.service.Nip19
|
||||||
|
import com.vitorpamplona.amethyst.service.lang.LanguageTranslatorService
|
||||||
|
import com.vitorpamplona.amethyst.service.lang.ResultOrError
|
||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import nostr.postr.toNpub
|
import nostr.postr.toNpub
|
||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
@@ -64,96 +63,139 @@ fun isValidURL(url: String?): Boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RichTextViewer(content: String, canPreview: Boolean, tags: List<List<String>>?, navController: NavController) {
|
fun TranslateableRichTextViewer(
|
||||||
|
content: String,
|
||||||
|
canPreview: Boolean,
|
||||||
|
tags: List<List<String>>?,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
val translatedTextState = remember {
|
val translatedTextState = remember {
|
||||||
mutableStateOf(ResultOrError(content, null, null, null))
|
mutableStateOf(ResultOrError(content, null, null, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
var showOriginal by remember { mutableStateOf(false) }
|
var showOriginal by remember { mutableStateOf(false) }
|
||||||
var showFullText by remember { mutableStateOf(false) }
|
var langSettingsPopupExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
val context = LocalContext.current
|
||||||
LanguageTranslatorService.autoTranslate(content).addOnCompleteListener { task ->
|
|
||||||
|
val accountState by accountViewModel.accountLanguagesLiveData.observeAsState()
|
||||||
|
val account = accountState?.account ?: return
|
||||||
|
|
||||||
|
LaunchedEffect(accountState) {
|
||||||
|
LanguageTranslatorService.autoTranslate(content, account.dontTranslateFrom, account.translateTo).addOnCompleteListener { task ->
|
||||||
if (task.isSuccessful) {
|
if (task.isSuccessful) {
|
||||||
translatedTextState.value = task.result
|
translatedTextState.value = task.result
|
||||||
|
} else {
|
||||||
|
translatedTextState.value = ResultOrError(content, null, null, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val toBeViewed = if (showOriginal) content else translatedTextState.value.result ?: content
|
val toBeViewed = if (showOriginal) content else translatedTextState.value.result ?: content
|
||||||
val text = if (showFullText) toBeViewed else toBeViewed.take(350)
|
|
||||||
|
|
||||||
Column(modifier = Modifier.padding(top = 5.dp)) {
|
Column(modifier = Modifier.padding(top = 5.dp)) {
|
||||||
|
ExpandableRichTextViewer(
|
||||||
|
toBeViewed,
|
||||||
|
canPreview,
|
||||||
|
tags,
|
||||||
|
navController
|
||||||
|
)
|
||||||
|
|
||||||
Box(contentAlignment = Alignment.BottomCenter) {
|
val target = translatedTextState.value.targetLang
|
||||||
|
val source = translatedTextState.value.sourceLang
|
||||||
|
|
||||||
Column(Modifier.fillMaxWidth().animateContentSize()) {
|
if (source != null && target != null) {
|
||||||
// FlowRow doesn't work well with paragraphs. So we need to split them
|
if (source != target) {
|
||||||
text.split('\n').forEach { paragraph ->
|
Row(modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) {
|
||||||
|
val clickableTextStyle = SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
|
||||||
|
|
||||||
FlowRow() {
|
val annotatedTranslationString= buildAnnotatedString {
|
||||||
paragraph.split(' ').forEach { word: String ->
|
withStyle(clickableTextStyle) {
|
||||||
|
pushStringAnnotation("langSettings", true.toString())
|
||||||
|
append("Auto")
|
||||||
|
}
|
||||||
|
|
||||||
if (canPreview) {
|
append("-translated from ")
|
||||||
// Explicit URL
|
|
||||||
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
withStyle(clickableTextStyle) {
|
||||||
if (lnInvoice != null) {
|
pushStringAnnotation("showOriginal", true.toString())
|
||||||
InvoicePreview(lnInvoice)
|
append(Locale(source).displayName)
|
||||||
} else if (isValidURL(word)) {
|
}
|
||||||
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
|
||||||
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
append(" to ")
|
||||||
ZoomableImageView(word)
|
|
||||||
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
withStyle(clickableTextStyle) {
|
||||||
VideoView(word)
|
pushStringAnnotation("showOriginal", false.toString())
|
||||||
} else {
|
append(Locale(target).displayName)
|
||||||
UrlPreview(word, word)
|
}
|
||||||
}
|
}
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
|
||||||
ClickableEmail(word)
|
ClickableText(
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
text = annotatedTranslationString,
|
||||||
ClickablePhone(word)
|
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
overflow = TextOverflow.Visible,
|
||||||
UrlPreview("https://$word", word)
|
maxLines = 3
|
||||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
) { spanOffset -> annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset)
|
||||||
TagLink(word, tags, navController)
|
.firstOrNull()
|
||||||
} else if (isBechLink(word)) {
|
?.also { span ->
|
||||||
BechLink(word, navController)
|
if (span.tag == "showOriginal")
|
||||||
} else {
|
showOriginal = span.item.toBoolean()
|
||||||
Text(
|
else
|
||||||
text = "$word ",
|
langSettingsPopupExpanded = !langSettingsPopupExpanded
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
} else {
|
DropdownMenu(
|
||||||
if (isValidURL(word)) {
|
expanded = langSettingsPopupExpanded,
|
||||||
ClickableUrl("$word ", word)
|
onDismissRequest = { langSettingsPopupExpanded = false }
|
||||||
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
) {
|
||||||
ClickableEmail(word)
|
DropdownMenuItem(onClick = {
|
||||||
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
accountViewModel.dontTranslateFrom(source, context)
|
||||||
ClickablePhone(word)
|
langSettingsPopupExpanded = false
|
||||||
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
}) {
|
||||||
ClickableUrl(word, "https://$word")
|
Text("Never translate from ${Locale(source).displayName}")
|
||||||
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
}
|
||||||
TagLink(word, tags, navController)
|
Divider()
|
||||||
} else if (isBechLink(word)) {
|
val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration())
|
||||||
BechLink(word, navController)
|
for (i in 0 until languageList.size()) {
|
||||||
} else {
|
languageList.get(i)?.let { lang ->
|
||||||
Text(
|
DropdownMenuItem(onClick = {
|
||||||
text = "$word ",
|
accountViewModel.translateTo(lang, context)
|
||||||
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
langSettingsPopupExpanded = false
|
||||||
)
|
}) {
|
||||||
|
Text("Always translate to ${lang.displayName}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (toBeViewed.length > 350 && !showFullText) {
|
@Composable
|
||||||
Row(
|
fun ExpandableRichTextViewer(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
content: String,
|
||||||
horizontalArrangement = Arrangement.Center,
|
canPreview: Boolean,
|
||||||
modifier = Modifier.fillMaxWidth().background(
|
tags: List<List<String>>?,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
var showFullText by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val text = if (showFullText) content else content.take(350)
|
||||||
|
|
||||||
|
Box(contentAlignment = Alignment.BottomCenter) {
|
||||||
|
RichTextViewer(text, canPreview, tags, navController)
|
||||||
|
|
||||||
|
if (content.length > 350 && !showFullText) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
brush = Brush.verticalGradient(
|
brush = Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
MaterialTheme.colors.background.copy(alpha = 0f),
|
MaterialTheme.colors.background.copy(alpha = 0f),
|
||||||
@@ -161,63 +203,98 @@ fun RichTextViewer(content: String, canPreview: Boolean, tags: List<List<String>
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = Modifier.padding(top = 10.dp),
|
||||||
|
onClick = { showFullText = !showFullText },
|
||||||
|
shape = RoundedCornerShape(20.dp),
|
||||||
|
colors = ButtonDefaults
|
||||||
|
.buttonColors(
|
||||||
|
backgroundColor = MaterialTheme.colors.primary
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
Button(
|
Text(text = "Show More", color = Color.White)
|
||||||
modifier = Modifier.padding(top = 10.dp),
|
|
||||||
onClick = { showFullText = !showFullText },
|
|
||||||
shape = RoundedCornerShape(20.dp),
|
|
||||||
colors = ButtonDefaults
|
|
||||||
.buttonColors(
|
|
||||||
backgroundColor = MaterialTheme.colors.primary
|
|
||||||
),
|
|
||||||
contentPadding = PaddingValues(vertical = 6.dp, horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
Text(text = "Show More", color = Color.White)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val target = translatedTextState.value.targetLang
|
|
||||||
val source = translatedTextState.value.sourceLang
|
|
||||||
|
|
||||||
if (source != null && target != null) {
|
|
||||||
if (source != target) {
|
|
||||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f))
|
|
||||||
|
|
||||||
val annotatedTranslationString= buildAnnotatedString {
|
|
||||||
append("Auto-translated from ")
|
|
||||||
|
|
||||||
withStyle(clickableTextStyle) {
|
|
||||||
pushStringAnnotation("showOriginal", true.toString())
|
|
||||||
append(Locale(source).displayName)
|
|
||||||
}
|
|
||||||
|
|
||||||
append(" to ")
|
|
||||||
|
|
||||||
withStyle(clickableTextStyle) {
|
|
||||||
pushStringAnnotation("showOriginal", false.toString())
|
|
||||||
append(Locale(target).displayName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClickableText(
|
|
||||||
text = annotatedTranslationString,
|
|
||||||
style = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)),
|
|
||||||
overflow = TextOverflow.Visible,
|
|
||||||
maxLines = 3
|
|
||||||
) { spanOffset ->
|
|
||||||
annotatedTranslationString.getStringAnnotations(spanOffset, spanOffset)
|
|
||||||
.firstOrNull()
|
|
||||||
?.also { span ->
|
|
||||||
showOriginal = span.item.toBoolean()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RichTextViewer(
|
||||||
|
content: String,
|
||||||
|
canPreview: Boolean,
|
||||||
|
tags: List<List<String>>?,
|
||||||
|
navController: NavController
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateContentSize()) {
|
||||||
|
// FlowRow doesn't work well with paragraphs. So we need to split them
|
||||||
|
content.split('\n').forEach { paragraph ->
|
||||||
|
|
||||||
|
FlowRow() {
|
||||||
|
paragraph.split(' ').forEach { word: String ->
|
||||||
|
|
||||||
|
if (canPreview) {
|
||||||
|
// Explicit URL
|
||||||
|
val lnInvoice = LnInvoiceUtil.findInvoice(word)
|
||||||
|
if (lnInvoice != null) {
|
||||||
|
InvoicePreview(lnInvoice)
|
||||||
|
} else if (isValidURL(word)) {
|
||||||
|
val removedParamsFromUrl = word.split("?")[0].toLowerCase()
|
||||||
|
if (imageExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
ZoomableImageView(word)
|
||||||
|
} else if (videoExtension.matcher(removedParamsFromUrl).matches()) {
|
||||||
|
VideoView(word)
|
||||||
|
} else {
|
||||||
|
UrlPreview(word, word)
|
||||||
|
}
|
||||||
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
|
ClickableEmail(word)
|
||||||
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
|
ClickablePhone(word)
|
||||||
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
|
UrlPreview("https://$word", word)
|
||||||
|
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(word, tags, navController)
|
||||||
|
} else if (isBechLink(word)) {
|
||||||
|
BechLink(word, navController)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (isValidURL(word)) {
|
||||||
|
ClickableUrl("$word ", word)
|
||||||
|
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
|
||||||
|
ClickableEmail(word)
|
||||||
|
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
|
||||||
|
ClickablePhone(word)
|
||||||
|
} else if (noProtocolUrlValidator.matcher(word).matches()) {
|
||||||
|
ClickableUrl(word, "https://$word")
|
||||||
|
} else if (tagIndex.matcher(word).matches() && tags != null) {
|
||||||
|
TagLink(word, tags, navController)
|
||||||
|
} else if (isBechLink(word)) {
|
||||||
|
BechLink(word, navController)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
text = "$word ",
|
||||||
|
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
fun isBechLink(word: String): Boolean {
|
fun isBechLink(word: String): Boolean {
|
||||||
return word.startsWith("nostr:", true)
|
return word.startsWith("nostr:", true)
|
||||||
|| word.startsWith("npub1", true)
|
|| word.startsWith("npub1", true)
|
||||||
@@ -291,89 +368,3 @@ fun TagLink(word: String, tags: List<List<String>>, navController: NavController
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class ResultOrError(
|
|
||||||
var result: String?,
|
|
||||||
var sourceLang: String?,
|
|
||||||
var targetLang: String?,
|
|
||||||
var error: Exception?
|
|
||||||
)
|
|
||||||
|
|
||||||
object LanguageTranslatorService {
|
|
||||||
private val languageIdentification = LanguageIdentification.getClient()
|
|
||||||
|
|
||||||
private val languagesSpokenByTheUser = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()).toLanguageTags()
|
|
||||||
private val usersPreferredLanguage = Locale.getDefault().language
|
|
||||||
|
|
||||||
init {
|
|
||||||
println("LanguagesAAA: ${languagesSpokenByTheUser}")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val translators =
|
|
||||||
object : LruCache<TranslatorOptions, Translator>(10) {
|
|
||||||
override fun create(options: TranslatorOptions): Translator {
|
|
||||||
return Translation.getClient(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun entryRemoved(
|
|
||||||
evicted: Boolean,
|
|
||||||
key: TranslatorOptions,
|
|
||||||
oldValue: Translator,
|
|
||||||
newValue: Translator?
|
|
||||||
) {
|
|
||||||
oldValue.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun identifyLanguage(text: String): Task<String> {
|
|
||||||
return languageIdentification.identifyLanguage(text)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun translate(text: String, source: String, target: String): Task<ResultOrError> {
|
|
||||||
val sourceLangCode = TranslateLanguage.fromLanguageTag(source)
|
|
||||||
val targetLangCode = TranslateLanguage.fromLanguageTag(target)
|
|
||||||
if (sourceLangCode == null || targetLangCode == null) {
|
|
||||||
return Tasks.forCanceled()
|
|
||||||
}
|
|
||||||
|
|
||||||
val options = TranslatorOptions.Builder()
|
|
||||||
.setSourceLanguage(sourceLangCode)
|
|
||||||
.setTargetLanguage(targetLangCode)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val translator = translators[options]
|
|
||||||
|
|
||||||
return translator.downloadModelIfNeeded().onSuccessTask {
|
|
||||||
|
|
||||||
val tasks = mutableListOf<Task<String>>()
|
|
||||||
for (paragraph in text.split("\n")) {
|
|
||||||
tasks.add(translator.translate(paragraph))
|
|
||||||
}
|
|
||||||
|
|
||||||
Tasks.whenAll(tasks).continueWith {
|
|
||||||
val results: MutableList<String> = ArrayList()
|
|
||||||
for (task in tasks) {
|
|
||||||
results.add(task.result)
|
|
||||||
}
|
|
||||||
ResultOrError(results.joinToString("\n"), source, target, null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun autoTranslate(text: String, target: String): Task<ResultOrError> {
|
|
||||||
return identifyLanguage(text).onSuccessTask {
|
|
||||||
if (it == target) {
|
|
||||||
Tasks.forCanceled()
|
|
||||||
} else if (it != "und" && !languagesSpokenByTheUser.contains(it)) {
|
|
||||||
translate(text, it, target)
|
|
||||||
} else {
|
|
||||||
Tasks.forCanceled()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun autoTranslate(text: String): Task<ResultOrError> {
|
|
||||||
return autoTranslate(text, usersPreferredLanguage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
@@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
|
||||||
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp)
|
||||||
@@ -221,17 +222,19 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote
|
|||||||
|| !noteForReports.hasAnyReports()
|
|| !noteForReports.hasAnyReports()
|
||||||
|
|
||||||
if (eventContent != null) {
|
if (eventContent != null) {
|
||||||
RichTextViewer(
|
TranslateableRichTextViewer(
|
||||||
eventContent,
|
eventContent,
|
||||||
canPreview,
|
canPreview,
|
||||||
note.event?.tags,
|
note.event?.tags,
|
||||||
|
accountViewModel,
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
RichTextViewer(
|
TranslateableRichTextViewer(
|
||||||
"Could Not decrypt the message",
|
"Could Not decrypt the message",
|
||||||
true,
|
true,
|
||||||
note.event?.tags,
|
note.event?.tags,
|
||||||
|
accountViewModel,
|
||||||
navController
|
navController
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent
|
|||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
import com.vitorpamplona.amethyst.service.model.RepostEvent
|
||||||
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Following
|
import com.vitorpamplona.amethyst.ui.theme.Following
|
||||||
import nostr.postr.events.TextNoteEvent
|
import nostr.postr.events.TextNoteEvent
|
||||||
@@ -271,7 +272,13 @@ fun NoteCompose(
|
|||||||
|| !noteForReports.hasAnyReports()
|
|| !noteForReports.hasAnyReports()
|
||||||
|
|
||||||
if (eventContent != null) {
|
if (eventContent != null) {
|
||||||
RichTextViewer(eventContent, canPreview, note.event?.tags, navController)
|
TranslateableRichTextViewer(
|
||||||
|
eventContent,
|
||||||
|
canPreview,
|
||||||
|
note.event?.tags,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
@@ -42,6 +42,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
|
|||||||
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
|
||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
import com.vitorpamplona.amethyst.ui.components.RichTextViewer
|
||||||
|
import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer
|
||||||
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
import com.vitorpamplona.amethyst.ui.note.BlankNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
import com.vitorpamplona.amethyst.ui.note.HiddenNote
|
||||||
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
|
||||||
@@ -226,7 +227,13 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController
|
|||||||
|| !noteForReports.hasAnyReports()
|
|| !noteForReports.hasAnyReports()
|
||||||
|
|
||||||
if (eventContent != null) {
|
if (eventContent != null) {
|
||||||
RichTextViewer(eventContent, canPreview, note.event?.tags, navController)
|
TranslateableRichTextViewer(
|
||||||
|
eventContent,
|
||||||
|
canPreview,
|
||||||
|
note.event?.tags,
|
||||||
|
accountViewModel,
|
||||||
|
navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactionsRow(note, accountViewModel)
|
ReactionsRow(note, accountViewModel)
|
||||||
|
@@ -10,9 +10,11 @@ import com.vitorpamplona.amethyst.model.AccountState
|
|||||||
import com.vitorpamplona.amethyst.model.Note
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
import com.vitorpamplona.amethyst.model.User
|
import com.vitorpamplona.amethyst.model.User
|
||||||
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
import com.vitorpamplona.amethyst.service.model.ReportEvent
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
class AccountViewModel(private val account: Account): ViewModel() {
|
class AccountViewModel(private val account: Account): ViewModel() {
|
||||||
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
val accountLiveData: LiveData<AccountState> = account.live.map { it }
|
||||||
|
val accountLanguagesLiveData: LiveData<AccountState> = account.liveLanguages.map { it }
|
||||||
|
|
||||||
fun reactTo(note: Note) {
|
fun reactTo(note: Note) {
|
||||||
account.reactTo(note)
|
account.reactTo(note)
|
||||||
@@ -47,4 +49,14 @@ class AccountViewModel(private val account: Account): ViewModel() {
|
|||||||
account.showUser(user.pubkeyHex)
|
account.showUser(user.pubkeyHex)
|
||||||
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun translateTo(lang: Locale, ctx: Context) {
|
||||||
|
account.updateTranslateTo(lang.language)
|
||||||
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dontTranslateFrom(lang: String, ctx: Context) {
|
||||||
|
account.addDontTranslateFrom(lang)
|
||||||
|
LocalPreferences(ctx).saveToEncryptedStorage(account)
|
||||||
|
}
|
||||||
}
|
}
|
Reference in New Issue
Block a user