Merge pull request #3025 from nextcloud/issue_2931

🔣 Translate chat message
This commit is contained in:
Andy Scherzinger 2023-05-17 19:25:56 +02:00 committed by GitHub
commit 22b7b8e68a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 866 additions and 3 deletions

View File

@ -189,6 +189,10 @@
android:name=".location.GeocodingActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".translate.TranslateActivity"
android:theme="@style/AppTheme" />
<activity
android:name=".profile.ProfileActivity"
android:theme="@style/AppTheme" />

View File

@ -44,6 +44,7 @@ import com.nextcloud.talk.models.json.search.ContactsByNumberOverall;
import com.nextcloud.talk.models.json.signaling.SignalingOverall;
import com.nextcloud.talk.models.json.signaling.settings.SignalingSettingsOverall;
import com.nextcloud.talk.models.json.status.StatusOverall;
import com.nextcloud.talk.models.json.translations.TranslationsOverall;
import com.nextcloud.talk.models.json.unifiedsearch.UnifiedSearchOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileFieldsOverall;
import com.nextcloud.talk.models.json.userprofile.UserProfileOverall;
@ -654,4 +655,12 @@ public interface NcApi {
@DELETE
Observable<GenericOverall> sendCommonDeleteRequest(@Header("Authorization") String authorization, @Url String url);
@POST
Observable<TranslationsOverall> translateMessage(@Header("Authorization") String authorization,
@Url String url,
@Query("text") String text,
@Query("toLanguage") String toLanguage,
@Nullable @Query("fromLanguage") String fromLanguage);
}

View File

@ -150,6 +150,7 @@ import com.nextcloud.talk.remotefilebrowser.activities.RemoteFileBrowserActivity
import com.nextcloud.talk.repositories.reactions.ReactionsRepository
import com.nextcloud.talk.shareditems.activities.SharedItemsActivity
import com.nextcloud.talk.signaling.SignalingMessageReceiver
import com.nextcloud.talk.translate.TranslateActivity
import com.nextcloud.talk.ui.bottom.sheet.ProfileBottomSheet
import com.nextcloud.talk.ui.dialog.AttachmentDialog
import com.nextcloud.talk.ui.dialog.MessageActionsDialog
@ -3231,6 +3232,15 @@ class ChatActivity :
clipboardManager.setPrimaryClip(clipData)
}
fun translateMessage(message: IMessage?) {
val bundle = Bundle()
bundle.putString(BundleKeys.KEY_TRANSLATE_MESSAGE, message?.text)
val intent = Intent(this, TranslateActivity::class.java)
intent.putExtras(bundle)
startActivity(intent)
}
private fun hasVisibleItems(message: ChatMessage): Boolean {
return !message.isDeleted || // copy message
message.replyable || // reply to

View File

@ -0,0 +1,37 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.translations
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class TranslateData(
@JsonField(name = ["text"])
var text: String?,
@JsonField(name = ["from"])
var fromLanguage: String?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View File

@ -0,0 +1,38 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.translations
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import com.nextcloud.talk.models.json.generic.GenericMeta
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
data class TranslateOCS( // TODO finish this model
@JsonField(name = ["meta"])
var meta: GenericMeta?,
@JsonField(name = ["data"])
var data: TranslateData?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, TranslateData())
}

View File

@ -0,0 +1,35 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.translations
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import kotlinx.parcelize.Parcelize
@Parcelize
@JsonObject
class TranslationsOverall(
@JsonField(name = ["ocs"])
var ocs: TranslateOCS?
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null)
}

View File

@ -0,0 +1,279 @@
/*
* Nextcloud Talk application
*
* @author Julius Linus
* @author Andy Scherzinger
* Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
* Copyright (C) 2023 Andy Scherzinger <info@andy-scherzinger.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.translate
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.method.ScrollingMovementMethod
import android.util.Log
import android.view.View
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.appcompat.app.AlertDialog
import autodagger.AutoInjector
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.BaseActivity
import com.nextcloud.talk.api.NcApi
import com.nextcloud.talk.application.NextcloudTalkApplication
import com.nextcloud.talk.data.user.model.User
import com.nextcloud.talk.databinding.ActivityTranslateBinding
import com.nextcloud.talk.models.json.translations.TranslationsOverall
import com.nextcloud.talk.users.UserManager
import com.nextcloud.talk.utils.ApiUtils
import com.nextcloud.talk.utils.bundle.BundleKeys
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.json.JSONArray
import java.util.Locale
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
class TranslateActivity : BaseActivity() {
private lateinit var binding: ActivityTranslateBinding
@Inject
lateinit var ncApi: NcApi
@Inject
lateinit var userManager: UserManager
var fromLanguages = arrayOf<String>()
var toLanguages = arrayOf<String>()
var text: String? = null
var check: Int = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
binding = ActivityTranslateBinding.inflate(layoutInflater)
setupActionBar()
setContentView(binding.root)
setupSystemColors()
setupTextViews()
setupSpinners()
getLanguageOptions()
translate(null, Locale.getDefault().language)
}
private fun setupActionBar() {
setSupportActionBar(binding.translationToolbar)
binding.translationToolbar.setNavigationOnClickListener {
onBackPressed()
}
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.setDisplayShowHomeEnabled(true)
supportActionBar?.setIcon(ColorDrawable(resources!!.getColor(R.color.transparent)))
supportActionBar?.title = resources!!.getString(R.string.translation)
viewThemeUtils.material.themeToolbar(binding.translationToolbar)
}
private fun setupTextViews() {
val original = binding.originalMessageTextview
val translation = binding.translatedMessageTextview
viewThemeUtils.talk.themeIncomingMessageBubble(original, grouped = true, deleted = false)
viewThemeUtils.talk.themeIncomingMessageBubble(translation, grouped = true, deleted = false)
original.movementMethod = ScrollingMovementMethod()
translation.movementMethod = ScrollingMovementMethod()
val bundle = intent.extras
binding.originalMessageTextview.text = bundle?.getString(BundleKeys.KEY_TRANSLATE_MESSAGE)
text = bundle?.getString(BundleKeys.KEY_TRANSLATE_MESSAGE)
}
private fun getLanguageOptions() {
val currentUser: User = userManager.currentUser.blockingGet()
val json = JSONArray(CapabilitiesUtilNew.getLanguages(currentUser).toString())
val fromLanguagesSet = mutableSetOf(resources.getString(R.string.translation_detect_language))
val toLanguagesSet = mutableSetOf(resources.getString(R.string.translation_device_settings))
for (i in 0 until json.length()) {
val current = json.getJSONObject(i)
if (current.getString(FROM_ID) != Locale.getDefault().language) {
toLanguagesSet.add(current.getString(FROM_LABEL))
}
fromLanguagesSet.add(current.getString(TO_LABEL))
}
fromLanguages = fromLanguagesSet.toTypedArray()
toLanguages = toLanguagesSet.toTypedArray()
fillSpinners()
}
private fun enableSpinners(value: Boolean) {
binding.fromLanguageInputLayout.isEnabled = value
binding.toLanguageInputLayout.isEnabled = value
}
private fun translate(fromLanguage: String?, toLanguage: String) {
val currentUser: User = userManager.currentUser.blockingGet()
val credentials: String = ApiUtils.getCredentials(currentUser.username, currentUser.token)
val translateURL = ApiUtils.getUrlForTranslation(currentUser.baseUrl)
val calculatedFromLanguage = if (fromLanguage == null || fromLanguage == "") {
null
} else {
fromLanguage
}
Log.i(TAG, "Url is: $translateURL")
ncApi.translateMessage(credentials, translateURL, text, toLanguage, calculatedFromLanguage)
?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(object : Observer<TranslationsOverall> {
override fun onSubscribe(d: Disposable) {
enableSpinners(false)
binding.translatedMessageTextview.visibility = View.GONE
binding.progressBar.visibility = View.VISIBLE
}
override fun onNext(translationOverall: TranslationsOverall) {
binding.progressBar.visibility = View.GONE
binding.translatedMessageTextview.visibility = View.VISIBLE
binding.translatedMessageTextview.text = translationOverall.ocs?.data?.text
}
override fun onError(e: Throwable) {
Log.w(TAG, "Error while translating message", e)
binding.progressBar.visibility = View.GONE
val dialogBuilder = MaterialAlertDialogBuilder(this@TranslateActivity)
.setIcon(
viewThemeUtils.dialog.colorMaterialAlertDialogIcon(
context,
R.drawable.ic_warning_white
)
)
.setTitle(R.string.translation_error_title)
.setMessage(R.string.translation_error_message)
.setPositiveButton(R.string.nc_ok) { dialog, _ ->
dialog.dismiss()
}
viewThemeUtils.dialog.colorMaterialAlertDialogBackground(context, dialogBuilder)
val dialog = dialogBuilder.show()
viewThemeUtils.platform.colorTextButtons(
dialog.getButton(AlertDialog.BUTTON_POSITIVE)
)
}
override fun onComplete() {
// nothing?
}
})
enableSpinners(true)
}
private fun getISOFromLanguage(language: String): String {
if (resources.getString(R.string.translation_device_settings).equals(language)) {
return Locale.getDefault().language
}
return getISOFromServer(language)
}
private fun getISOFromServer(language: String): String {
val currentUser: User = userManager.currentUser.blockingGet()
val json = JSONArray(CapabilitiesUtilNew.getLanguages(currentUser).toString())
for (i in 0..json.length() - 1) {
val current = json.getJSONObject(i)
if (current.getString(FROM_LABEL) == language) {
return current.getString(FROM_ID)
}
}
return ""
}
private fun setupSpinners() {
viewThemeUtils.material.colorTextInputLayout(binding.fromLanguageInputLayout)
viewThemeUtils.material.colorTextInputLayout(binding.toLanguageInputLayout)
fillSpinners()
binding.fromLanguage.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (++check > 1) {
val fromLabel: String = getISOFromLanguage(parent.getItemAtPosition(position).toString())
val toLabel: String = getISOFromLanguage(binding.fromLanguage.text.toString())
Log.i(TAG, "fromLanguageSpinner :: $FROM_LABEL = $fromLabel, $TO_LABEL = $ count: $check")
translate(fromLabel, toLabel)
}
}
override fun onNothingSelected(parent: AdapterView<*>) {
// write code to perform some action
}
}
binding.toLanguage.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
if (++check > 2) {
val toLabel: String = getISOFromLanguage(parent.getItemAtPosition(position).toString())
val fromLabel: String = getISOFromLanguage(binding.fromLanguage.text.toString())
Log.i(TAG, "toLanguageSpinner :: $FROM_LABEL = $fromLabel, $TO_LABEL = $toLabel count: $check")
translate(fromLabel, toLabel)
}
}
override fun onNothingSelected(parent: AdapterView<*>) {
// write code to perform some action
}
}
}
private fun fillSpinners() {
binding.fromLanguage.setAdapter(
ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, fromLanguages)
)
if (fromLanguages.isNotEmpty()) {
binding.fromLanguage.setText(fromLanguages[0])
}
binding.toLanguage.setAdapter(
ArrayAdapter(this, android.R.layout.simple_spinner_dropdown_item, toLanguages)
)
if (toLanguages.isNotEmpty()) {
binding.toLanguage.setText(toLanguages[0])
}
}
companion object {
private val TAG = TranslateActivity::class.simpleName
private const val FROM_ID = "from"
private const val FROM_LABEL = "fromLabel"
private const val TO_LABEL = "toLabel"
}
}

View File

@ -55,6 +55,7 @@ import io.reactivex.Observer
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import org.json.JSONArray
import javax.inject.Inject
@AutoInjector(NextcloudTalkApplication::class)
@ -88,6 +89,12 @@ class MessageActionsDialog(
viewThemeUtils.platform.themeDialog(dialogMessageActionsBinding.root)
initEmojiBar(hasChatPermission)
initMenuItemCopy(!message.isDeleted)
initMenuItemTranslate(
!message.isDeleted &&
ChatMessage.MessageType.REGULAR_TEXT_MESSAGE == message.getCalculateMessageType() &&
CapabilitiesUtilNew.isTranslationsSupported(user) &&
JSONArray(CapabilitiesUtilNew.getLanguages(user).toString()).length() > 0
)
initMenuReplyToMessage(message.replyable && hasChatPermission)
initMenuReplyPrivately(
message.replyable &&
@ -296,6 +303,17 @@ class MessageActionsDialog(
dialogMessageActionsBinding.menuCopyMessage.visibility = getVisibility(visible)
}
private fun initMenuItemTranslate(visible: Boolean) {
if (visible) {
dialogMessageActionsBinding.menuTranslateMessage.setOnClickListener {
chatActivity.translateMessage(message)
dismiss()
}
}
dialogMessageActionsBinding.menuTranslateMessage.visibility = getVisibility(visible)
}
private fun getVisibility(visible: Boolean): Int {
return if (visible) {
View.VISIBLE

View File

@ -28,7 +28,6 @@ import android.graphics.drawable.Drawable
import android.graphics.drawable.LayerDrawable
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import android.widget.TextView
import androidx.annotation.ColorInt
@ -65,7 +64,7 @@ class TalkSpecificViewThemeUtils @Inject constructor(
private val appcompat: AndroidXViewThemeUtils
) :
ViewThemeUtilsBase(schemes) {
fun themeIncomingMessageBubble(bubble: ViewGroup, grouped: Boolean, deleted: Boolean) {
fun themeIncomingMessageBubble(bubble: View, grouped: Boolean, deleted: Boolean) {
val resources = bubble.resources
var bubbleResource = R.drawable.shape_incoming_message
@ -88,7 +87,7 @@ class TalkSpecificViewThemeUtils @Inject constructor(
ViewCompat.setBackground(bubble, bubbleDrawable)
}
fun themeOutgoingMessageBubble(bubble: ViewGroup, grouped: Boolean, deleted: Boolean) {
fun themeOutgoingMessageBubble(bubble: View, grouped: Boolean, deleted: Boolean) {
withScheme(bubble) { scheme ->
val bgBubbleColor = if (deleted) {
ColorUtils.setAlphaComponent(scheme.surfaceVariant, HALF_ALPHA_INT)

View File

@ -526,4 +526,8 @@ public class ApiUtils {
public static String getUrlForConversationDescription(int version, String baseUrl, String token) {
return getUrlForRoom(version, baseUrl, token) + "/description";
}
public static String getUrlForTranslation(String baseUrl) {
return baseUrl + ocsApiVersion + "/translation/translate";
}
}

View File

@ -29,6 +29,7 @@ object BundleKeys {
const val KEY_SELECTED_EMAILS = "KEY_SELECTED_EMAILS"
const val KEY_USERNAME = "KEY_USERNAME"
const val KEY_TOKEN = "KEY_TOKEN"
const val KEY_TRANSLATE_MESSAGE = "KEY_TRANSLATE_MESSAGE"
const val KEY_BASE_URL = "KEY_BASE_URL"
const val KEY_IS_ACCOUNT_IMPORT = "KEY_IS_ACCOUNT_IMPORT"
const val KEY_ORIGINAL_PROTOCOL = "KEY_ORIGINAL_PROTOCOL"

View File

@ -199,5 +199,24 @@ object CapabilitiesUtilNew {
return false
}
fun isTranslationsSupported(user: User?): Boolean {
if (user?.capabilities != null) {
val capabilities = user.capabilities
return capabilities?.spreedCapability?.config?.containsKey("chat") == true &&
capabilities.spreedCapability!!.config!!["chat"] != null &&
capabilities.spreedCapability!!.config!!["chat"]!!.containsKey("translations")
}
return false
}
fun getLanguages(user: User?): Any? {
return if (isTranslationsSupported(user)) {
user!!.capabilities!!.spreedCapability!!.config!!["chat"]!!["translations"]
} else {
null
}
}
const val DEFAULT_CHAT_SIZE = 1000
}

View File

@ -0,0 +1,25 @@
<!--
@author Google LLC
Copyright (C) 2023 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#61FFFFFF"
android:pathData="M376,720L320,664L504,480L320,296L376,240L616,480L376,720Z"/>
</vector>

View File

@ -0,0 +1,26 @@
<!--
@author Google LLC
Copyright (C) 2023 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12.87,15.07l-2.54,-2.51 0.03,-0.03c1.74,-1.94 2.98,-4.17 3.71,-6.53L17,6L17,4h-7L10,2L8,2v2L1,4v1.99h11.17C11.5,7.92 10.44,9.75 9,11.35 8.07,10.32 7.3,9.19 6.69,8h-2c0.73,1.63 1.73,3.17 2.98,4.56l-5.09,5.02L4,19l5,-5 3.11,3.11 0.76,-2.04zM18.5,10h-2L12,22h2l1.12,-3h4.75L21,22h2l-4.5,-12zM15.88,17l1.62,-4.33L19.12,17h-3.24z" />
</vector>

View File

@ -0,0 +1,25 @@
<!--
@author Google LLC
Copyright (C) 2023 Google LLC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#61000000"
android:pathData="M376,720L320,664L504,480L320,296L376,240L616,480L376,720Z"/>
</vector>

View File

@ -0,0 +1,145 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Julius Linus
~ Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/translation_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/translation_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/appbar"
android:theme="?attr/actionBarPopupTheme"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIconTint="@color/fontAppbar"
app:popupTheme="@style/appActionBarPopupMenu"
app:titleTextColor="@color/fontAppbar"
tools:title="@string/translation" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="horizontal"
android:padding="@dimen/standard_padding">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/fromLanguageInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/translation_from">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/fromLanguage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:layout_width="@dimen/standard_double_margin"
android:layout_height="match_parent"
android:contentDescription="@null"
android:src="@drawable/ic_chevron_right" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/toLanguageInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/translation_to">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/toLanguage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="@dimen/standard_padding">
<TextView
android:id="@+id/original_message_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_weight="1"
android:background="@drawable/shape_grouped_incoming_message"
android:padding="@dimen/dialog_padding"
android:scrollbars="vertical"
android:text=""
android:textColor="@color/nc_incoming_text_default"
android:textSize="@dimen/message_text_size" />
<TextView
android:id="@+id/translated_message_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_weight="1"
android:background="@drawable/shape_grouped_incoming_message"
android:padding="@dimen/dialog_padding"
android:scrollbars="vertical"
android:text=""
android:textColor="@color/nc_incoming_text_default"
android:textSize="@dimen/message_text_size"
android:visibility="visible" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,148 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Nextcloud Talk application
~
~ @author Julius Linus
~ Copyright (C) 2023 Julius Linus <julius.linus@nextcloud.com>
~
~ This program is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ at your option) any later version.
~
~ This program is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:weightSum="2">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/translation_appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/translation_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/appbar"
android:theme="?attr/actionBarPopupTheme"
app:layout_scrollFlags="scroll|enterAlways"
app:navigationIconTint="@color/fontAppbar"
app:popupTheme="@style/appActionBarPopupMenu"
app:titleTextColor="@color/fontAppbar"
tools:title="@string/translation" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:padding="@dimen/standard_padding">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/fromLanguageInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/translation_from">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/fromLanguage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:inputType="none"
android:lines="1"
android:popupTheme="@style/ThemeOverlay.AppTheme.PopupMenu" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/toLanguageInputLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:paddingTop="@dimen/standard_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:hint="@string/translation_to">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:id="@+id/toLanguage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="middle"
android:inputType="none"
android:lines="1"
android:popupTheme="@style/ThemeOverlay.AppTheme.PopupMenu" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="@dimen/standard_quarter_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/original_message_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_margin"
android:layout_weight="1"
android:background="@drawable/shape_grouped_incoming_message"
android:padding="@dimen/dialog_padding"
android:scrollbars="vertical"
android:text=""
android:textColor="@color/nc_incoming_text_default"
android:textSize="@dimen/message_text_size" />
<TextView
android:id="@+id/translated_message_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/standard_margin"
android:layout_marginEnd="@dimen/standard_margin"
android:layout_marginBottom="@dimen/standard_margin"
android:layout_weight="1"
android:background="@drawable/shape_grouped_incoming_message"
android:padding="@dimen/dialog_padding"
android:scrollbars="vertical"
android:text=""
android:textColor="@color/nc_incoming_text_default"
android:textSize="@dimen/message_text_size"
android:visibility="visible" />
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -287,6 +287,39 @@
</LinearLayout>
<LinearLayout
android:id="@+id/menu_translate_message"
android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
tools:ignore="UseCompoundDrawables">
<ImageView
android:id="@+id/menu_icon_translate_message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/zero"
android:src="@drawable/ic_baseline_translate_24"
app:tint="@color/high_emphasis_menu_icon" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/menu_text_translate_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="start|center_vertical"
android:paddingStart="@dimen/standard_double_padding"
android:paddingEnd="@dimen/standard_padding"
android:text="@string/translate"
android:textAlignment="viewStart"
android:textColor="@color/high_emphasis_text"
android:textSize="@dimen/bottom_sheet_text_size" />
</LinearLayout>
<LinearLayout
android:id="@+id/menu_delete_message"
android:layout_width="match_parent"

View File

@ -656,5 +656,13 @@ How to translate with transifex:
<string name="nc_not_allowed_to_activate_audio">You are not allowed to activate audio!</string>
<string name="nc_not_allowed_to_activate_video">You are not allowed to activate video!</string>
<string name="scroll_to_bottom">Scroll to bottom</string>
<string name="translate">Translate</string>
<string name="translation">Translation</string>
<string name="translation_from">From</string>
<string name="translation_to">To</string>
<string name="translation_detect_language">Detect language</string>
<string name="translation_device_settings">Device settings</string>
<string name="translation_error_title">Translation failed</string>
<string name="translation_error_message">Could not detect language</string>
</resources>