android: Voice messages UI changes (#1422)

* android: Voice messages UI changes

* Alert for groups

Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko
2022-11-25 19:31:04 +03:00
committed by GitHub
parent 60fedbf5d2
commit f8cf35879f
12 changed files with 125 additions and 24 deletions

View File

@@ -1052,7 +1052,7 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a
val file = cItem.file val file = cItem.file
if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) { if (cItem.content.msgContent is MsgContent.MCImage && file != null && file.fileSize <= MAX_IMAGE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) {
withApi { receiveFile(file.fileId) } withApi { receiveFile(file.fileId) }
} else if (cItem.content.msgContent is MsgContent.MCVoice && file != null && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && appPrefs.privacyAcceptImages.get()) { } else if (cItem.content.msgContent is MsgContent.MCVoice && file != null && file.fileSize <= MAX_VOICE_SIZE_AUTO_RCV && file.fileSize > MAX_VOICE_SIZE_FOR_SENDING && appPrefs.privacyAcceptImages.get()) {
withApi { receiveFile(file.fileId) } // TODO check inlineFileMode != IFMSent withApi { receiveFile(file.fileId) } // TODO check inlineFileMode != IFMSent
} }
if (!cItem.chatDir.sent && !cItem.isCall && !cItem.isMutedMemberEvent && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) { if (!cItem.chatDir.sent && !cItem.isCall && !cItem.isMutedMemberEvent && (!isAppOnForeground(appContext) || chatModel.chatId.value != cInfo.id)) {

View File

@@ -135,7 +135,7 @@ fun TerminalLayout(
topBar = { CloseSheetBar(close) }, topBar = { CloseSheetBar(close) },
bottomBar = { bottomBar = {
Box(Modifier.padding(horizontal = 8.dp)) { Box(Modifier.padding(horizontal = 8.dp)) {
SendMsgView(composeState, false, sendCommand, ::onMessageChange, { _, _, _ -> }, textStyle) SendMsgView(composeState, false, false, false, sendCommand, ::onMessageChange, { _, _, _ -> }, {}, {}, textStyle)
} }
}, },
modifier = Modifier.navigationBarsWithImePadding() modifier = Modifier.navigationBarsWithImePadding()

View File

@@ -64,7 +64,7 @@ fun ChatInfoView(
}, },
openPreferences = { openPreferences = {
ModalManager.shared.showModal(true) { ModalManager.shared.showModal(true) {
ContactPreferencesView(chatModel, chatModel.currentUser.value ?: return@showModal, contact) ContactPreferencesView(chatModel, chatModel.currentUser.value ?: return@showModal, contact.contactId)
} }
}, },
deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) }, deleteContact = { deleteContactDialog(chat.chatInfo, chatModel, close) },

View File

@@ -455,6 +455,29 @@ fun ComposeView(
composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished)) composeState.value = composeState.value.copy(preview = ComposePreview.VoicePreview(filePath, durationMs, finished))
} }
fun allowVoiceToContact() {
val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return
val featuresAllowed = contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)
withApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed).copy(voice = ChatPreference(FeatureAllowed.YES))
val toContact = chatModel.controller.apiSetContactPrefs(contact.contactId, prefs)
if (toContact != null) {
chatModel.updateContact(toContact)
}
}
}
fun showDisabledVoiceAlert() {
AlertManager.shared.showAlertMsg(
title = generalGetString(R.string.voice_messages_prohibited),
text = generalGetString(
if (chat.chatInfo is ChatInfo.Direct)
R.string.ask_your_contact_to_enable_voice
else
R.string.only_group_owners_can_enable_voice
)
)
}
fun cancelLinkPreview() { fun cancelLinkPreview() {
val uri = composeState.value.linkPreview?.uri val uri = composeState.value.linkPreview?.uri
if (uri != null) { if (uri != null) {
@@ -549,15 +572,35 @@ fun ComposeView(
.clip(CircleShape) .clip(CircleShape)
) )
} }
val allowedVoiceByPrefs = remember(chat.chatInfo) {
when (chat.chatInfo) {
is ChatInfo.Direct -> chat.chatInfo.contact.mergedPreferences.voice.enabled.forUser
is ChatInfo.Group -> chat.chatInfo.groupInfo.fullGroupPreferences.voice.enable == GroupFeatureEnabled.ON
else -> false
}
}
val needToAllowVoiceToContact = remember(chat.chatInfo) {
when (chat.chatInfo) {
is ChatInfo.Direct -> with(chat.chatInfo.contact.mergedPreferences.voice) {
((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) &&
contactPreference.allow == FeatureAllowed.YES
}
else -> false
}
}
SendMsgView( SendMsgView(
composeState, composeState,
allowVoiceRecord = true, showVoiceRecordIcon = true,
allowedVoiceByPrefs = allowedVoiceByPrefs,
needToAllowVoiceToContact = needToAllowVoiceToContact,
sendMessage = { sendMessage = {
sendMessage() sendMessage()
resetLinkPreview() resetLinkPreview()
}, },
::onMessageChange, ::onMessageChange,
::onAudioAdded, ::onAudioAdded,
::allowVoiceToContact,
::showDisabledVoiceAlert,
textStyle textStyle
) )
} }

View File

@@ -57,7 +57,7 @@ fun ComposeVoiceView(
Modifier Modifier
.requiredWidth(progressBarWidth.value.dp) .requiredWidth(progressBarWidth.value.dp)
.padding(top = 58.dp) .padding(top = 58.dp)
.height(2.dp) .height(3.dp)
.background(MaterialTheme.colors.primary) .background(MaterialTheme.colors.primary)
) )
Row( Row(

View File

@@ -24,15 +24,17 @@ import chat.simplex.app.views.helpers.*
fun ContactPreferencesView( fun ContactPreferencesView(
m: ChatModel, m: ChatModel,
user: User, user: User,
contact: Contact, contactId: Long,
) { ) {
var featuresAllowed by remember { mutableStateOf(contactUserPrefsToFeaturesAllowed(contact.mergedPreferences)) } val contact = remember { derivedStateOf { (m.getContactChat(contactId)?.chatInfo as? ChatInfo.Direct)?.contact } }
var currentFeaturesAllowed by remember { mutableStateOf(featuresAllowed) } val ct = contact.value ?: return
var featuresAllowed by remember(ct) { mutableStateOf(contactUserPrefsToFeaturesAllowed(ct.mergedPreferences)) }
var currentFeaturesAllowed by remember(ct) { mutableStateOf(featuresAllowed) }
ContactPreferencesLayout( ContactPreferencesLayout(
featuresAllowed, featuresAllowed,
currentFeaturesAllowed, currentFeaturesAllowed,
user, user,
contact, ct,
applyPrefs = { prefs -> applyPrefs = { prefs ->
featuresAllowed = prefs featuresAllowed = prefs
}, },
@@ -42,7 +44,7 @@ fun ContactPreferencesView(
savePrefs = { savePrefs = {
withApi { withApi {
val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val prefs = contactFeaturesAllowedToPrefs(featuresAllowed)
val toContact = m.controller.apiSetContactPrefs(contact.contactId, prefs) val toContact = m.controller.apiSetContactPrefs(ct.contactId, prefs)
if (toContact != null) { if (toContact != null) {
m.updateContact(toContact) m.updateContact(toContact)
currentFeaturesAllowed = featuresAllowed currentFeaturesAllowed = featuresAllowed

View File

@@ -17,7 +17,6 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.* import androidx.compose.material.icons.outlined.*
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.* import androidx.compose.ui.*
@@ -25,7 +24,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.platform.* import androidx.compose.ui.platform.*
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
@@ -48,10 +46,14 @@ import java.io.*
@Composable @Composable
fun SendMsgView( fun SendMsgView(
composeState: MutableState<ComposeState>, composeState: MutableState<ComposeState>,
allowVoiceRecord: Boolean, showVoiceRecordIcon: Boolean,
allowedVoiceByPrefs: Boolean,
needToAllowVoiceToContact: Boolean,
sendMessage: () -> Unit, sendMessage: () -> Unit,
onMessageChange: (String) -> Unit, onMessageChange: (String) -> Unit,
onAudioAdded: (String, Int, Boolean) -> Unit, onAudioAdded: (String, Int, Boolean) -> Unit,
allowVoiceToContact: () -> Unit,
showDisabledVoiceAlert: () -> Unit,
textStyle: MutableState<TextStyle> textStyle: MutableState<TextStyle>
) { ) {
Column(Modifier.padding(vertical = 8.dp)) { Column(Modifier.padding(vertical = 8.dp)) {
@@ -60,7 +62,7 @@ fun SendMsgView(
val attachEnabled = !composeState.value.editing val attachEnabled = !composeState.value.editing
val filePath = rememberSaveable { mutableStateOf(null as String?) } val filePath = rememberSaveable { mutableStateOf(null as String?) }
var recordingTimeRange by rememberSaveable(saver = LongRange.saver) { mutableStateOf(0L..0L) } // since..to var recordingTimeRange by rememberSaveable(saver = LongRange.saver) { mutableStateOf(0L..0L) } // since..to
val showVoiceButton = ((cs.message.isEmpty() || recordingTimeRange.first > 0L) && allowVoiceRecord && attachEnabled && cs.preview is ComposePreview.NoPreview) || filePath.value != null val showVoiceButton = ((cs.message.isEmpty() || recordingTimeRange.first > 0L) && showVoiceRecordIcon && attachEnabled && cs.preview is ComposePreview.NoPreview) || filePath.value != null
Box(if (recordingTimeRange.first == 0L) Box(if (recordingTimeRange.first == 0L)
Modifier Modifier
else else
@@ -112,6 +114,16 @@ fun SendMsgView(
val startStopRecording: () -> Unit = { val startStopRecording: () -> Unit = {
when { when {
!permissionsState.allPermissionsGranted -> permissionsState.launchMultiplePermissionRequest() !permissionsState.allPermissionsGranted -> permissionsState.launchMultiplePermissionRequest()
needToAllowVoiceToContact -> {
AlertManager.shared.showAlertDialog(
title = generalGetString(R.string.allow_voice_messages_question),
text = generalGetString(R.string.you_need_to_allow_to_send_voice),
confirmText = generalGetString(R.string.allow_verb),
dismissText = generalGetString(R.string.cancel_verb),
onConfirm = allowVoiceToContact,
)
}
!allowedVoiceByPrefs -> showDisabledVoiceAlert()
recordingInProgress.value -> stopRecordingAndAddAudio() recordingInProgress.value -> stopRecordingAndAddAudio()
filePath.value == null -> { filePath.value == null -> {
recordingTimeRange = System.currentTimeMillis()..0L recordingTimeRange = System.currentTimeMillis()..0L
@@ -149,10 +161,15 @@ fun SendMsgView(
} }
} }
val interactionSource = interactionSourceWithTapDetection( val interactionSource = interactionSourceWithTapDetection(
// It's just a key for triggering dropping a state in the compose function. Without it
// nothing will react on changed params like needToAllowVoiceToContact or allowedVoiceByPrefs
needToAllowVoiceToContact.toString() + allowedVoiceByPrefs.toString(),
onPress = { onPress = {
if (filePath.value == null) startStopRecording() if (filePath.value == null) startStopRecording()
}, },
onClick = { onClick = {
// Voice not allowed or not granted voice record permission for the app
if (!allowedVoiceByPrefs || !permissionsState.allPermissionsGranted) return@interactionSourceWithTapDetection
if (!recordingInProgress.value && filePath.value != null) { if (!recordingInProgress.value && filePath.value != null) {
sendMessage() sendMessage()
cleanUp(false) cleanUp(false)
@@ -173,9 +190,19 @@ fun SendMsgView(
Modifier Modifier
IconButton({}, Modifier.size(36.dp), enabled = !cs.inProgress, interactionSource = interactionSource) { IconButton({}, Modifier.size(36.dp), enabled = !cs.inProgress, interactionSource = interactionSource) {
Icon( Icon(
if (recordingTimeRange.last != 0L) Icons.Outlined.ArrowUpward else if (stopRecOnNextClick) Icons.Default.Stop else Icons.Default.Mic, when {
recordingTimeRange.last != 0L -> Icons.Outlined.ArrowUpward
stopRecOnNextClick -> Icons.Filled.Stop
allowedVoiceByPrefs -> Icons.Filled.KeyboardVoice
else -> Icons.Outlined.KeyboardVoice
},
stringResource(R.string.icon_descr_record_voice_message), stringResource(R.string.icon_descr_record_voice_message),
tint = if (recordingTimeRange.last != 0L) Color.White else if (!cs.inProgress) MaterialTheme.colors.primary else HighOrLowlight, tint = when {
recordingTimeRange.last != 0L -> Color.White
stopRecOnNextClick -> MaterialTheme.colors.primary
allowedVoiceByPrefs -> MaterialTheme.colors.primary
else -> HighOrLowlight
},
modifier = Modifier modifier = Modifier
.size(36.dp) .size(36.dp)
.padding(4.dp) .padding(4.dp)
@@ -301,10 +328,14 @@ fun PreviewSendMsgView() {
SimpleXTheme { SimpleXTheme {
SendMsgView( SendMsgView(
composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) },
allowVoiceRecord = false, showVoiceRecordIcon = false,
allowedVoiceByPrefs = false,
needToAllowVoiceToContact = false,
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> }, onAudioAdded = { _, _, _ -> },
allowVoiceToContact = {},
showDisabledVoiceAlert = {},
textStyle = textStyle textStyle = textStyle
) )
} }
@@ -324,10 +355,14 @@ fun PreviewSendMsgViewEditing() {
SimpleXTheme { SimpleXTheme {
SendMsgView( SendMsgView(
composeState = remember { mutableStateOf(composeStateEditing) }, composeState = remember { mutableStateOf(composeStateEditing) },
allowVoiceRecord = false, showVoiceRecordIcon = false,
allowedVoiceByPrefs = false,
needToAllowVoiceToContact = false,
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> }, onAudioAdded = { _, _, _ -> },
allowVoiceToContact = {},
showDisabledVoiceAlert = {},
textStyle = textStyle textStyle = textStyle
) )
} }
@@ -347,10 +382,14 @@ fun PreviewSendMsgViewInProgress() {
SimpleXTheme { SimpleXTheme {
SendMsgView( SendMsgView(
composeState = remember { mutableStateOf(composeStateInProgress) }, composeState = remember { mutableStateOf(composeStateInProgress) },
allowVoiceRecord = false, showVoiceRecordIcon = false,
allowedVoiceByPrefs = false,
needToAllowVoiceToContact = false,
sendMessage = {}, sendMessage = {},
onMessageChange = { _ -> }, onMessageChange = { _ -> },
onAudioAdded = { _, _, _ -> }, onAudioAdded = { _, _, _ -> },
allowVoiceToContact = {},
showDisabledVoiceAlert = {},
textStyle = textStyle textStyle = textStyle
) )
} }

View File

@@ -5,8 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.InsertDriveFile import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -95,7 +94,7 @@ fun FramedItemView(
ciQuotedMsgView(qi) ciQuotedMsgView(qi)
} }
Icon( Icon(
if (qi.content is MsgContent.MCFile) Icons.Filled.InsertDriveFile else Icons.Filled.PlayArrow, if (qi.content is MsgContent.MCFile) Icons.Filled.InsertDriveFile else Icons.Filled.Mic,
if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message), if (qi.content is MsgContent.MCFile) stringResource(R.string.icon_descr_file) else stringResource(R.string.voice_message),
Modifier Modifier
.padding(top = 6.dp, end = 4.dp) .padding(top = 6.dp, end = 4.dp)

View File

@@ -214,8 +214,8 @@ fun interactionSourceWithDetection(onClick: () -> Unit, onLongClick: () -> Unit)
} }
@Composable @Composable
fun interactionSourceWithTapDetection(onPress: () -> Unit, onClick: () -> Unit, onCancel: () -> Unit, onRelease: ()-> Unit): MutableInteractionSource { fun interactionSourceWithTapDetection(key: Any, onPress: () -> Unit, onClick: () -> Unit, onCancel: () -> Unit, onRelease: ()-> Unit): MutableInteractionSource {
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember(key) { MutableInteractionSource() }
LaunchedEffect(interactionSource) { LaunchedEffect(interactionSource) {
var firstTapTime = 0L var firstTapTime = 0L
interactionSource.interactions.collect { interaction -> interactionSource.interactions.collect { interaction ->

View File

@@ -167,6 +167,7 @@
<string name="save_verb">Speichern</string> <string name="save_verb">Speichern</string>
<string name="edit_verb">Bearbeiten</string> <string name="edit_verb">Bearbeiten</string>
<string name="delete_verb">Löschen</string> <string name="delete_verb">Löschen</string>
<string name="allow_verb">***Allow</string>
<string name="delete_message__question">Die Nachricht löschen?</string> <string name="delete_message__question">Die Nachricht löschen?</string>
<string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden!</string> <string name="delete_message_cannot_be_undone_warning">Nachricht wird gelöscht - dies kann nicht rückgängig gemacht werden!</string>
<string name="for_me_only">Nur für mich</string> <string name="for_me_only">Nur für mich</string>
@@ -249,6 +250,11 @@
<!-- Message Actions - SendMsgView.kt --> <!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Nachricht senden</string> <string name="icon_descr_send_message">Nachricht senden</string>
<string name="icon_descr_record_voice_message">***Record voice message</string> <string name="icon_descr_record_voice_message">***Record voice message</string>
<string name="allow_voice_messages_question">***Allow voice messages?</string>
<string name="you_need_to_allow_to_send_voice">***You need to allow your contact to send voice messages to be able to send them.</string>
<string name="voice_messages_prohibited">***Voice messages prohibited!</string>
<string name="ask_your_contact_to_enable_voice">***Please ask your contact to enable sending voice messages.</string>
<string name="only_group_owners_can_enable_voice">***Only group owners can enable voice messages.</string>
<!-- General Actions / Responses --> <!-- General Actions / Responses -->
<string name="back">Zurück</string> <string name="back">Zurück</string>

View File

@@ -167,6 +167,7 @@
<string name="save_verb">Сохранить</string> <string name="save_verb">Сохранить</string>
<string name="edit_verb">Редактировать</string> <string name="edit_verb">Редактировать</string>
<string name="delete_verb">Удалить</string> <string name="delete_verb">Удалить</string>
<string name="allow_verb">Разрешить</string>
<string name="delete_message__question">Удалить сообщение?</string> <string name="delete_message__question">Удалить сообщение?</string>
<string name="delete_message_cannot_be_undone_warning">Сообщение будет удалено это действие нельзя отменить!</string> <string name="delete_message_cannot_be_undone_warning">Сообщение будет удалено это действие нельзя отменить!</string>
<string name="for_me_only">Только для меня</string> <string name="for_me_only">Только для меня</string>
@@ -249,6 +250,11 @@
<!-- Message Actions - SendMsgView.kt --> <!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Отправить сообщение</string> <string name="icon_descr_send_message">Отправить сообщение</string>
<string name="icon_descr_record_voice_message">Записать голосовое сообщение</string> <string name="icon_descr_record_voice_message">Записать голосовое сообщение</string>
<string name="allow_voice_messages_question">Разрешить голосовые сообщения?</string>
<string name="you_need_to_allow_to_send_voice">Чтобы включить отправку голосовых сообщений, разрешите их вашему контакту.</string>
<string name="voice_messages_prohibited">Голосовые сообщения запрещены!</string>
<string name="ask_your_contact_to_enable_voice">Попросите вашего контакта разрешить отправку голосовых сообщений.</string>
<string name="only_group_owners_can_enable_voice">Только владельцы группы могут разрешить голосовые сообщения.</string>
<!-- General Actions / Responses --> <!-- General Actions / Responses -->
<string name="back">Назад</string> <string name="back">Назад</string>

View File

@@ -167,6 +167,7 @@
<string name="save_verb">Save</string> <string name="save_verb">Save</string>
<string name="edit_verb">Edit</string> <string name="edit_verb">Edit</string>
<string name="delete_verb">Delete</string> <string name="delete_verb">Delete</string>
<string name="allow_verb">Allow</string>
<string name="delete_message__question">Delete message?</string> <string name="delete_message__question">Delete message?</string>
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string> <string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
<string name="for_me_only">For me only</string> <string name="for_me_only">For me only</string>
@@ -249,6 +250,11 @@
<!-- Message Actions - SendMsgView.kt --> <!-- Message Actions - SendMsgView.kt -->
<string name="icon_descr_send_message">Send Message</string> <string name="icon_descr_send_message">Send Message</string>
<string name="icon_descr_record_voice_message">Record voice message</string> <string name="icon_descr_record_voice_message">Record voice message</string>
<string name="allow_voice_messages_question">Allow voice messages?</string>
<string name="you_need_to_allow_to_send_voice">You need to allow your contact to send voice messages to be able to send them.</string>
<string name="voice_messages_prohibited">Voice messages prohibited!</string>
<string name="ask_your_contact_to_enable_voice">Please ask your contact to enable sending voice messages.</string>
<string name="only_group_owners_can_enable_voice">Only group owners can enable voice messages.</string>
<!-- General Actions / Responses --> <!-- General Actions / Responses -->
<string name="back">Back</string> <string name="back">Back</string>