diff --git a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt index 227ad0d76..84194c3ec 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/LocalPreferences.kt @@ -10,6 +10,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.ui.actions.NewRelayListViewModel import com.vitorpamplona.amethyst.ui.navigation.Route +import java.util.Locale import nostr.postr.Persona import nostr.postr.events.ContactListEvent import nostr.postr.events.Event @@ -26,6 +27,8 @@ class LocalPreferences(context: Context) { remove("following_channels") remove("hidden_users") remove("relays") + remove("dontTranslateFrom") + remove("translateTo") }.apply() } @@ -36,6 +39,8 @@ class LocalPreferences(context: Context) { account.followingChannels.let { putStringSet("following_channels", it) } account.hiddenUsers.let { putStringSet("hidden_users", it) } account.localRelays.let { putString("relays", gson.toJson(it)) } + account.dontTranslateFrom.let { putStringSet("dontTranslateFrom", it) } + account.translateTo.let { putString("translateTo", it) } }.apply() } @@ -43,19 +48,24 @@ class LocalPreferences(context: Context) { encryptedPreferences.apply { val privKey = getString("nostr_privkey", null) val pubKey = getString("nostr_pubkey", null) - val followingChannels = getStringSet("following_channels", null)?.toMutableSet() ?: mutableSetOf() - val hiddenUsers = getStringSet("hidden_users", emptySet())?.toMutableSet() ?: mutableSetOf() + val followingChannels = getStringSet("following_channels", null) ?: setOf() + val hiddenUsers = getStringSet("hidden_users", emptySet()) ?: setOf() val localRelays = gson.fromJson( getString("relays", "[]"), object : TypeToken>() {}.type ) ?: setOf() + val dontTranslateFrom = getStringSet("dontTranslateFrom", null) ?: setOf() + val translateTo = getString("translateTo", null) ?: Locale.getDefault().language + if (pubKey != null) { return Account( Persona(privKey = privKey?.toByteArray(), pubKey = pubKey.toByteArray()), followingChannels, hiddenUsers, - localRelays + localRelays, + dontTranslateFrom, + translateTo ) } else { return null 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 675a1bb26..067d73c41 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -1,5 +1,9 @@ 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 com.vitorpamplona.amethyst.service.relays.Constants 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.ui.actions.NewRelayListViewModel import java.util.Date +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -34,13 +40,23 @@ val DefaultChannels = setOf( "42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5" // -> Amethyst's Group ) +fun getLanguagesSpokenByUser(): Set { + val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()) + val codedList = mutableSetOf() + for (i in 0 until languageList.size()) { + languageList.get(i)?.let { codedList.add(it.language) } + } + return codedList +} + class Account( val loggedIn: Persona, var followingChannels: Set = DefaultChannels, var hiddenUsers: Set = setOf(), - var localRelays: Set = Constants.defaultRelays.toSet() + var localRelays: Set = Constants.defaultRelays.toSet(), + var dontTranslateFrom: Set = getLanguagesSpokenByUser(), + var translateTo: String = Locale.getDefault().language ) { - fun userProfile(): User { return LocalCache.getOrCreateUser(loggedIn.pubKey.toHexKey()) } @@ -265,22 +281,22 @@ class Account( fun joinChannel(idHex: String) { followingChannels = followingChannels + idHex - invalidateData() + invalidateData(live) } fun leaveChannel(idHex: String) { followingChannels = followingChannels - idHex - invalidateData() + invalidateData(live) } fun hideUser(pubkeyHex: String) { hiddenUsers = hiddenUsers + pubkeyHex - invalidateData() + invalidateData(live) } fun showUser(pubkeyHex: String) { hiddenUsers = hiddenUsers - pubkeyHex - invalidateData() + invalidateData(live) } 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? { return userProfile().relays?.map { val localFeedTypes = localRelays.firstOrNull() { localRelay -> localRelay.url == it.key }?.feedTypes ?: FeedType.values().toSet() @@ -357,19 +383,20 @@ class Account( // Observers line up here. val live: AccountLiveData = AccountLiveData(this) + val liveLanguages: AccountLiveData = AccountLiveData(this) + + var handlerWaiting = AtomicBoolean() - // Refreshes observers in batches. - var handlerWaiting = false @Synchronized - fun invalidateData() { - if (handlerWaiting) return + private fun invalidateData(live: AccountLiveData) { + if (handlerWaiting.getAndSet(true)) return - handlerWaiting = true + handlerWaiting.set(true) val scope = CoroutineScope(Job() + Dispatchers.Default) scope.launch { delay(100) live.refresh() - handlerWaiting = false + handlerWaiting.set(false) } } @@ -412,7 +439,6 @@ class Account( localRelays = value.toSet() sendNewRelayList(value.associate { it.url to ContactListEvent.ReadWrite(it.read, it.write) } ) } - } class AccountLiveData(private val account: Account): LiveData(AccountState(account)) { diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt new file mode 100644 index 000000000..0d2de04ad --- /dev/null +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/lang/LanguageTranslatorService.kt @@ -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(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 { + return languageIdentification.identifyLanguage(text) + } + + fun translate(text: String, source: String, target: String): Task { + 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>() + for (paragraph in text.split("\n")) { + tasks.add(translator.translate(paragraph)) + } + + Tasks.whenAll(tasks).continueWith { + val results: MutableList = ArrayList() + for (task in tasks) { + results.add(task.result) + } + ResultOrError(results.joinToString("\n"), source, target, null) + } + } + } + + fun autoTranslate(text: String, dontTranslateFrom: Set, translateTo: String): Task { + return identifyLanguage(text).onSuccessTask { + if (it == translateTo) { + Tasks.forCanceled() + } else if (it != "und" && !dontTranslateFrom.contains(it)) { + translate(text, it, translateTo) + } else { + Tasks.forCanceled() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt index 73496050d..d01adf36a 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/RichTextViewer.kt @@ -1,7 +1,6 @@ package com.vitorpamplona.amethyst.ui.components import android.content.res.Resources -import android.util.LruCache import android.util.Patterns import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background @@ -10,10 +9,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.style.TextDirection @@ -23,19 +24,17 @@ import androidx.compose.ui.unit.dp import androidx.core.os.ConfigurationCompat import androidx.navigation.NavController import com.google.accompanist.flowlayout.FlowRow -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 com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.lnurl.LnInvoiceUtil +import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.LocalCache import com.vitorpamplona.amethyst.model.toByteArray import com.vitorpamplona.amethyst.model.toNote 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.screen.loggedIn.AccountViewModel import nostr.postr.toNpub import java.net.MalformedURLException import java.net.URISyntaxException @@ -64,96 +63,139 @@ fun isValidURL(url: String?): Boolean { } @Composable -fun RichTextViewer(content: String, canPreview: Boolean, tags: List>?, navController: NavController) { +fun TranslateableRichTextViewer( + content: String, + canPreview: Boolean, + tags: List>?, + accountViewModel: AccountViewModel, + navController: NavController +) { val translatedTextState = remember { mutableStateOf(ResultOrError(content, null, null, null)) } var showOriginal by remember { mutableStateOf(false) } - var showFullText by remember { mutableStateOf(false) } + var langSettingsPopupExpanded by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { - LanguageTranslatorService.autoTranslate(content).addOnCompleteListener { task -> + val context = LocalContext.current + + 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) { translatedTextState.value = task.result + } else { + translatedTextState.value = ResultOrError(content, null, null, null) } } } 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)) { + ExpandableRichTextViewer( + toBeViewed, + canPreview, + tags, + navController + ) - Box(contentAlignment = Alignment.BottomCenter) { + val target = translatedTextState.value.targetLang + val source = translatedTextState.value.sourceLang - Column(Modifier.fillMaxWidth().animateContentSize()) { - // FlowRow doesn't work well with paragraphs. So we need to split them - text.split('\n').forEach { paragraph -> + if (source != null && target != null) { + if (source != target) { + Row(modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) { + val clickableTextStyle = SpanStyle(color = MaterialTheme.colors.primary.copy(alpha = 0.52f)) - FlowRow() { - paragraph.split(' ').forEach { word: String -> + val annotatedTranslationString= buildAnnotatedString { + withStyle(clickableTextStyle) { + pushStringAnnotation("langSettings", true.toString()) + append("Auto") + } - 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), - ) + append("-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 -> + if (span.tag == "showOriginal") + showOriginal = span.item.toBoolean() + else + langSettingsPopupExpanded = !langSettingsPopupExpanded + } + } + + DropdownMenu( + expanded = langSettingsPopupExpanded, + onDismissRequest = { langSettingsPopupExpanded = false } + ) { + DropdownMenuItem(onClick = { + accountViewModel.dontTranslateFrom(source, context) + langSettingsPopupExpanded = false + }) { + Text("Never translate from ${Locale(source).displayName}") + } + Divider() + val languageList = ConfigurationCompat.getLocales(Resources.getSystem().getConfiguration()) + for (i in 0 until languageList.size()) { + languageList.get(i)?.let { lang -> + DropdownMenuItem(onClick = { + accountViewModel.translateTo(lang, context) + langSettingsPopupExpanded = false + }) { + Text("Always translate to ${lang.displayName}") } } } } } } + } + } +} - if (toBeViewed.length > 350 && !showFullText) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center, - modifier = Modifier.fillMaxWidth().background( +@Composable +fun ExpandableRichTextViewer( + content: String, + canPreview: Boolean, + tags: List>?, + 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( colors = listOf( MaterialTheme.colors.background.copy(alpha = 0f), @@ -161,63 +203,98 @@ fun RichTextViewer(content: String, canPreview: Boolean, tags: List ) ) ) + ) { + 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( - 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() - } + Text(text = "Show More", color = Color.White) } } } } } +@Composable +fun RichTextViewer( + content: String, + canPreview: Boolean, + tags: List>?, + 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 { return word.startsWith("nostr:", true) || word.startsWith("npub1", true) @@ -291,89 +368,3 @@ fun TagLink(word: String, tags: List>, 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(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 { - return languageIdentification.identifyLanguage(text) - } - - fun translate(text: String, source: String, target: String): Task { - 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>() - for (paragraph in text.split("\n")) { - tasks.add(translator.translate(paragraph)) - } - - Tasks.whenAll(tasks).continueWith { - val results: MutableList = ArrayList() - for (task in tasks) { - results.add(task.result) - } - ResultOrError(results.joinToString("\n"), source, target, null) - } - } - } - - fun autoTranslate(text: String, target: String): Task { - 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 { - return autoTranslate(text, usersPreferredLanguage) - } -} - - diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt index 949714cf5..8276f5d47 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ChatroomMessageCompose.kt @@ -60,6 +60,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.RepostEvent import com.vitorpamplona.amethyst.ui.components.RichTextViewer +import com.vitorpamplona.amethyst.ui.components.TranslateableRichTextViewer import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel val ChatBubbleShapeMe = RoundedCornerShape(15.dp, 15.dp, 3.dp, 15.dp) @@ -221,17 +222,19 @@ fun ChatroomMessageCompose(baseNote: Note, routeForLastRead: String?, innerQuote || !noteForReports.hasAnyReports() if (eventContent != null) { - RichTextViewer( + TranslateableRichTextViewer( eventContent, canPreview, note.event?.tags, + accountViewModel, navController ) } else { - RichTextViewer( + TranslateableRichTextViewer( "Could Not decrypt the message", true, note.event?.tags, + accountViewModel, navController ) } 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 9c46b56c8..1213fabcf 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 @@ -66,6 +66,7 @@ import com.vitorpamplona.amethyst.service.model.ReactionEvent import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.RepostEvent 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.theme.Following import nostr.postr.events.TextNoteEvent @@ -271,7 +272,13 @@ fun NoteCompose( || !noteForReports.hasAnyReports() if (eventContent != null) { - RichTextViewer(eventContent, canPreview, note.event?.tags, navController) + TranslateableRichTextViewer( + eventContent, + canPreview, + note.event?.tags, + accountViewModel, + navController + ) } ReactionsRow(note, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt index 8ef50d9b5..b132b5221 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/ThreadFeedView.kt @@ -42,6 +42,7 @@ import com.google.accompanist.swiperefresh.SwipeRefresh import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.vitorpamplona.amethyst.model.Note 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.HiddenNote import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture @@ -226,7 +227,13 @@ fun NoteMaster(baseNote: Note, accountViewModel: AccountViewModel, navController || !noteForReports.hasAnyReports() if (eventContent != null) { - RichTextViewer(eventContent, canPreview, note.event?.tags, navController) + TranslateableRichTextViewer( + eventContent, + canPreview, + note.event?.tags, + accountViewModel, + navController + ) } ReactionsRow(note, accountViewModel) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt index d1b6094e6..cc89bff06 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/AccountViewModel.kt @@ -10,9 +10,11 @@ import com.vitorpamplona.amethyst.model.AccountState import com.vitorpamplona.amethyst.model.Note import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.model.ReportEvent +import java.util.Locale class AccountViewModel(private val account: Account): ViewModel() { val accountLiveData: LiveData = account.live.map { it } + val accountLanguagesLiveData: LiveData = account.liveLanguages.map { it } fun reactTo(note: Note) { account.reactTo(note) @@ -47,4 +49,14 @@ class AccountViewModel(private val account: Account): ViewModel() { account.showUser(user.pubkeyHex) 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) + } } \ No newline at end of file