Update settings

This commit is contained in:
Andrew 2024-04-11 14:20:32 +09:30
parent 85c7a23235
commit c589bed249
12 changed files with 447 additions and 637 deletions

View File

@ -373,6 +373,7 @@ dependencies {
implementation "com.google.accompanist:accompanist-pager:0.33.1-alpha"
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"
implementation "com.google.accompanist:accompanist-drawablepainter:0.33.1-alpha"
implementation "androidx.camera:camera-camera2:1.3.2"
implementation "androidx.camera:camera-lifecycle:1.3.2"

View File

@ -161,7 +161,7 @@ import org.thoughtcrime.securesms.mms.MediaConstraints
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity
import org.thoughtcrime.securesms.onboarding.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment
import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment
@ -176,6 +176,7 @@ import org.thoughtcrime.securesms.util.isScrolledToBottom
import org.thoughtcrime.securesms.util.isScrolledToWithin30dpOfBottom
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.toPx
import java.lang.ref.WeakReference
import java.util.Locale
@ -1602,7 +1603,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val userPublicKey = textSecurePreferences.getLocalNumber()
val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey)
if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) {
startRecoveryPasswordActivity()
start<RecoveryPasswordActivity>()
}
// Create the message
val message = VisibleMessage().applyExpiryMode(viewModel.threadId)

View File

@ -92,7 +92,7 @@ import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.notifications.PushRegistry
import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity
import org.thoughtcrime.securesms.onboarding.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.showMuteDialog
@ -111,6 +111,7 @@ import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.start
import org.thoughtcrime.securesms.util.themeState
import java.io.IOException
import java.util.Locale
@ -374,7 +375,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
stringResource(R.string.continue_2),
Modifier.align(Alignment.CenterVertically),
contentDescription = GetString(R.string.AccessibilityId_reveal_recovery_phrase_button)
) { startRecoveryPasswordActivity() }
) { start<RecoveryPasswordActivity>() }
}
}
}

View File

@ -1,27 +1,21 @@
package org.thoughtcrime.securesms.onboarding.recoverypassword
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.os.Bundle
import androidx.activity.viewModels
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -30,10 +24,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
@ -52,6 +43,7 @@ import org.thoughtcrime.securesms.ui.SessionShieldIcon
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.classicDarkColors
import org.thoughtcrime.securesms.ui.colorDestructive
import org.thoughtcrime.securesms.ui.components.QrImageCard
import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.h8
import org.thoughtcrime.securesms.ui.small
@ -68,7 +60,6 @@ class RecoveryPasswordActivity : BaseActionBarActivity() {
setContent {
RecoveryPassword(
viewModel.seed,
viewModel.qrBitmap,
{ viewModel.copySeed(context) }
) { onHide() }
}
@ -113,7 +104,6 @@ fun PreviewRecoveryPassword(
@Composable
fun RecoveryPassword(
seed: String = "",
qrBitmap: Bitmap? = null,
copySeed:() -> Unit = {},
onHide:() -> Unit = {}
) {
@ -124,14 +114,14 @@ fun RecoveryPassword(
.verticalScroll(rememberScrollState())
.padding(bottom = 16.dp)
) {
RecoveryPasswordCell(seed, qrBitmap, copySeed)
RecoveryPasswordCell(seed, copySeed)
HideRecoveryPasswordCell(onHide)
}
}
}
@Composable
fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:() -> Unit = {}) {
fun RecoveryPasswordCell(seed: String, copySeed:() -> Unit = {}) {
val showQr = remember {
mutableStateOf(false)
}
@ -163,21 +153,15 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:(
)
}
AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) {
Card(
backgroundColor = LocalExtraColors.current.lightCell,
elevation = 0.dp,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(vertical = 24.dp)
) {
qrBitmap?.let {
QrImage(
bitmap = it,
contentDescription = "QR code of your recovery password",
)
}
}
AnimatedVisibility(
showQr.value,
modifier = Modifier.align(Alignment.CenterHorizontally).padding(vertical = 24.dp)
) {
QrImageCard(
seed,
contentDescription = "QR code of your recovery password",
icon = R.drawable.session_shield
)
}
AnimatedVisibility(!showQr.value) {
@ -205,29 +189,6 @@ fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:(
}
}
@Composable
fun QrImage(bitmap: Bitmap, contentDescription: String, icon: Int = R.drawable.session_shield) {
Box {
Image(
bitmap = bitmap.asImageBitmap(),
contentDescription = contentDescription,
colorFilter = ColorFilter.tint(LocalExtraColors.current.onLightCell)
)
Icon(
painter = painterResource(id = icon),
contentDescription = "",
tint = LocalExtraColors.current.onLightCell,
modifier = Modifier
.align(Alignment.Center)
.width(46.dp)
.height(56.dp)
.background(color = LocalExtraColors.current.lightCell)
.padding(horizontal = 3.dp, vertical = 1.dp)
)
}
}
private fun MutableState<Boolean>.toggle() { value = !value }
@Composable
@ -247,7 +208,3 @@ fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) {
}
}
}
fun Context.startRecoveryPasswordActivity() {
Intent(this, RecoveryPasswordActivity::class.java).also(::startActivity)
}

View File

@ -42,13 +42,4 @@ class RecoveryPasswordViewModel @Inject constructor(
MnemonicCodec { MnemonicUtilities.loadFileContents(application, it) }
.encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english)
}
val qrBitmap by lazy {
QRCodeUtilities.encode(
data = seed,
size = toPx(280, application.resources),
isInverted = false,
hasTransparentBackground = true
)
}
}

View File

@ -2,15 +2,14 @@ package org.thoughtcrime.securesms.preferences
import android.Manifest
import android.app.Activity
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.AsyncTask
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.SparseArray
import android.view.ActionMode
@ -20,9 +19,38 @@ import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Card
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import network.loki.messenger.BuildConfig
import network.loki.messenger.R
import network.loki.messenger.databinding.ActivitySettingsBinding
@ -34,23 +62,37 @@ import nl.komponents.kovenant.ui.successUi
import org.session.libsession.avatars.AvatarHelper
import org.session.libsession.avatars.ProfileContactPhoto
import org.session.libsession.messaging.MessagingModuleConfiguration
import org.session.libsession.snode.OnionRequestAPI
import org.session.libsession.snode.SnodeAPI
import org.session.libsession.utilities.*
import org.session.libsession.utilities.Address
import org.session.libsession.utilities.ProfileKeyUtil
import org.session.libsession.utilities.ProfilePictureUtilities
import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsession.utilities.recipients.Recipient
import org.session.libsession.utilities.truncateIdForDisplay
import org.session.libsignal.utilities.Log
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
import org.thoughtcrime.securesms.avatar.AvatarSelection
import org.thoughtcrime.securesms.components.ProfilePictureView
import org.thoughtcrime.securesms.dependencies.ConfigFactory
import org.thoughtcrime.securesms.home.PathActivity
import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity
import org.thoughtcrime.securesms.onboarding.recoverypassword.RecoveryPasswordActivity
import org.thoughtcrime.securesms.permissions.Permissions
import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity
import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.AppTheme
import org.thoughtcrime.securesms.ui.BorderlessButton
import org.thoughtcrime.securesms.ui.Cell
import org.thoughtcrime.securesms.ui.Divider
import org.thoughtcrime.securesms.ui.ItemButton
import org.thoughtcrime.securesms.ui.ItemButtonWithDrawable
import org.thoughtcrime.securesms.ui.OutlineButton
import org.thoughtcrime.securesms.ui.PreviewTheme
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
import org.thoughtcrime.securesms.ui.destructiveButtonColors
import org.thoughtcrime.securesms.util.BitmapDecodingException
import org.thoughtcrime.securesms.util.BitmapUtil
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
@ -61,6 +103,8 @@ import java.io.File
import java.security.SecureRandom
import javax.inject.Inject
private const val TAG = "SettingsActivity"
@AndroidEntryPoint
class SettingsActivity : PassphraseRequiredActionBarActivity() {
@ -69,21 +113,14 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
@Inject
lateinit var prefs: TextSecurePreferences
private lateinit var binding: ActivitySettingsBinding
private var displayNameEditActionMode: ActionMode? = null
set(value) { field = value; handleDisplayNameEditActionModeChanged() }
private lateinit var glide: GlideRequests
private var tempFile: File? = null
private val hexEncodedPublicKey: String
get() {
return TextSecurePreferences.getLocalNumber(this)!!
}
private val hexEncodedPublicKey: String get() = TextSecurePreferences.getLocalNumber(this)!!
companion object {
const val updatedProfileResultCode = 1234
private const val SCROLL_STATE = "SCROLL_STATE"
}
@ -92,7 +129,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
super.onCreate(savedInstanceState, isReady)
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
glide = GlideApp.with(this)
binding.composeView.setContent {
AppTheme {
Buttons()
}
}
}
override fun onStart() {
@ -104,21 +146,6 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) }
btnGroupNameDisplay.text = getDisplayName()
publicKeyTextView.text = hexEncodedPublicKey
copyButton.setOnClickListener { copyPublicKey() }
shareButton.setOnClickListener { sharePublicKey() }
pathButton.setOnClickListener { showPath() }
pathContainer.disableClipping()
privacyButton.setOnClickListener { showPrivacySettings() }
notificationsButton.setOnClickListener { showNotificationSettings() }
messageRequestsButton.setOnClickListener { showMessageRequests() }
chatsButton.setOnClickListener { showChatSettings() }
appearanceButton.setOnClickListener { showAppearanceSettings() }
inviteFriendButton.setOnClickListener { sendInvitation() }
helpButton.setOnClickListener { showHelp() }
passwordDivider.isGone = prefs.getHidePassword()
passwordButton.isGone = prefs.getHidePassword()
passwordButton.setOnClickListener { showPassword() }
clearAllDataButton.setOnClickListener { clearAllData() }
versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
}
@ -167,30 +194,22 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode != Activity.RESULT_OK) return
when (requestCode) {
AvatarSelection.REQUEST_CODE_AVATAR -> {
if (resultCode != Activity.RESULT_OK) {
return
}
val outputFile = Uri.fromFile(File(cacheDir, "cropped"))
var inputFile: Uri? = data?.data
if (inputFile == null && tempFile != null) {
inputFile = Uri.fromFile(tempFile)
}
val inputFile: Uri? = data?.data ?: tempFile?.let(Uri::fromFile)
AvatarSelection.circularCropImage(this, inputFile, outputFile, R.string.CropImageActivity_profile_avatar)
}
AvatarSelection.REQUEST_CODE_CROP_IMAGE -> {
if (resultCode != Activity.RESULT_OK) {
return
}
AsyncTask.execute {
lifecycleScope.launch(Dispatchers.IO) {
try {
val profilePictureToBeUploaded = BitmapUtil.createScaledBytes(this@SettingsActivity, AvatarSelection.getResultUri(data), ProfileMediaConstraints()).bitmap
Handler(Looper.getMainLooper()).post {
launch(Dispatchers.Main) {
updateProfile(true, profilePictureToBeUploaded)
}
} catch (e: BitmapDecodingException) {
e.printStackTrace()
Log.e(TAG, e)
}
}
}
@ -205,10 +224,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
// region Updating
private fun handleDisplayNameEditActionModeChanged() {
val isEditingDisplayName = this.displayNameEditActionMode !== null
val isEditingDisplayName = this.displayNameEditActionMode != null
binding.btnGroupNameDisplay.visibility = if (isEditingDisplayName) View.INVISIBLE else View.VISIBLE
binding.displayNameEditText.visibility = if (isEditingDisplayName) View.VISIBLE else View.INVISIBLE
binding.btnGroupNameDisplay.isInvisible = isEditingDisplayName
binding.displayNameEditText.isInvisible = !isEditingDisplayName
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
if (isEditingDisplayName) {
@ -255,12 +274,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
MessagingModuleConfiguration.shared.storage.clearUserPic()
}
}
val compoundPromise = all(promises)
compoundPromise.successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
all(promises) successUi { // Do this on the UI thread so that it happens before the alwaysUi clause below
val userConfig = configFactory.user
if (isUpdatingProfilePicture) {
AvatarHelper.setAvatar(this, Address.fromSerialized(TextSecurePreferences.getLocalNumber(this)!!), profilePicture)
TextSecurePreferences.setProfileAvatarId(this, profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
prefs.setProfileAvatarId(profilePicture?.let { SecureRandom().nextInt() } ?: 0 )
ProfileKeyUtil.setEncodedProfileKey(this, encodedProfileKey)
// new config
val url = TextSecurePreferences.getProfilePictureURL(this)
@ -275,8 +293,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
configFactory.persist(userConfig, SnodeAPI.nowWithOffset)
}
ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@SettingsActivity)
}
compoundPromise.alwaysUi {
} alwaysUi {
if (displayName != null) {
binding.btnGroupNameDisplay.text = displayName
}
@ -318,23 +335,23 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
title(R.string.activity_settings_set_display_picture)
view(R.layout.dialog_change_avatar)
button(R.string.activity_settings_upload) { startAvatarSelection() }
if (TextSecurePreferences.getProfileAvatarId(context) != 0) {
if (prefs.getProfileAvatarId() != 0) {
button(R.string.activity_settings_remove) { removeAvatar() }
}
cancelButton()
}.apply {
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
?.also(::setupProfilePictureView)
val profilePic = findViewById<ProfilePictureView>(R.id.profile_picture_view)
?.also(::setupProfilePictureView)
val pictureIcon = findViewById<View>(R.id.ic_pictures)
val pictureIcon = findViewById<View>(R.id.ic_pictures)
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
val recipient = Recipient.from(context, Address.fromSerialized(hexEncodedPublicKey), false)
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
val photoSet = (recipient.contactPhoto as ProfileContactPhoto).avatarObject !in setOf("0", "")
profilePic?.isVisible = photoSet
pictureIcon?.isVisible = !photoSet
}
profilePic?.isVisible = photoSet
pictureIcon?.isVisible = !photoSet
}
}
private fun removeAvatar() {
@ -359,65 +376,21 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
}
private fun sharePublicKey() {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
intent.type = "text/plain"
val chooser = Intent.createChooser(intent, getString(R.string.share))
startActivity(chooser)
}
private fun showPrivacySettings() {
val intent = Intent(this, PrivacySettingsActivity::class.java)
push(intent)
}
private fun showNotificationSettings() {
val intent = Intent(this, NotificationSettingsActivity::class.java)
push(intent)
}
private fun showMessageRequests() {
val intent = Intent(this, MessageRequestsActivity::class.java)
push(intent)
}
private fun showChatSettings() {
val intent = Intent(this, ChatSettingsActivity::class.java)
push(intent)
}
private fun showAppearanceSettings() {
val intent = Intent(this, AppearanceSettingsActivity::class.java)
push(intent)
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, hexEncodedPublicKey)
type = "text/plain"
}.let { Intent.createChooser(it, getString(R.string.share)) }
.let(::startActivity)
}
private fun sendInvitation() {
val intent = Intent()
intent.action = Intent.ACTION_SEND
val invitation = "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey !"
intent.putExtra(Intent.EXTRA_TEXT, invitation)
intent.type = "text/plain"
val chooser = Intent.createChooser(intent, getString(R.string.activity_settings_invite_button_title))
startActivity(chooser)
}
private fun showHelp() {
val intent = Intent(this, HelpSettingsActivity::class.java)
push(intent)
}
private fun showPath() {
val intent = Intent(this, PathActivity::class.java)
show(intent)
}
private fun showPassword() {
startRecoveryPasswordActivity()
}
private fun clearAllData() {
ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog")
Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, "Hey, I've been using Session to chat with complete privacy and security. Come join me! Download it at https://getsession.org/. My Session ID is $hexEncodedPublicKey !")
type = "text/plain"
}.let { Intent.createChooser(it, getString(R.string.activity_settings_invite_button_title)) }
.let(::startActivity)
}
// endregion
@ -451,4 +424,88 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() {
return false;
}
}
@Composable
fun Buttons() {
Column {
Row(
modifier = Modifier.padding(horizontal = 24.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
OutlineButton(
modifier = Modifier.weight(1f),
onClick = { sharePublicKey() }
) { Text(stringResource(R.string.share)) }
OutlineButton(
modifier = Modifier.weight(1f),
onClick = { copyPublicKey() },
temporaryContent = { Text(stringResource(R.string.copied)) }
) {
Text(stringResource(R.string.copy))
}
}
Spacer(modifier = Modifier.height(24.dp))
var hasPaths by remember {
mutableStateOf(false)
}
CheckPaths { hasPaths = it }
Cell {
Column {
ItemButtonWithDrawable(R.string.activity_path_title, icon = if (hasPaths) R.drawable.ic_status else R.drawable.ic_path_yellow) { show<PathActivity>() }
Divider()
ItemButton(R.string.activity_settings_privacy_button_title, icon = R.drawable.ic_privacy_icon) { show<PrivacySettingsActivity>() }
Divider()
ItemButton(R.string.activity_settings_notifications_button_title, icon = R.drawable.ic_speaker, contentDescription = R.string.AccessibilityId_notifications) { show<NotificationSettingsActivity>() }
Divider()
ItemButton(R.string.activity_settings_conversations_button_title, icon = R.drawable.ic_conversations, contentDescription = R.string.AccessibilityId_conversations) { show<ChatSettingsActivity>() }
Divider()
ItemButton(R.string.activity_settings_message_requests_button_title, icon = R.drawable.ic_message_requests, contentDescription = R.string.AccessibilityId_message_requests) { show<MessageRequestsActivity>() }
Divider()
ItemButton(R.string.activity_settings_message_appearance_button_title, icon = R.drawable.ic_appearance, contentDescription = R.string.AccessibilityId_appearance) { show<AppearanceSettingsActivity>() }
Divider()
ItemButton(R.string.activity_settings_invite_button_title, icon = R.drawable.ic_invite_friend, contentDescription = R.string.AccessibilityId_invite_friend) { sendInvitation() }
Divider()
if (!prefs.getHidePassword()) {
ItemButton(R.string.sessionRecoveryPassword, icon = R.drawable.ic_recovery_phrase, contentDescription = R.string.AccessibilityId_recovery_password_menu_item) { show<RecoveryPasswordActivity>() }
Divider()
}
ItemButton(R.string.activity_settings_help_button, icon = R.drawable.ic_help, contentDescription = R.string.AccessibilityId_help) { show<HelpSettingsActivity>() }
Divider()
ItemButton(R.string.activity_settings_clear_all_data_button_title, colors = destructiveButtonColors(), icon = R.drawable.ic_clear_data, contentDescription = R.string.AccessibilityId_clear_data) { ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") }
}
}
}
}
@Composable
fun CheckPaths(setHasPaths: (Boolean) -> Unit) {
val context = LocalContext.current
val manager = LocalBroadcastManager.getInstance(context)
fun update() {
lifecycleScope.launch {
val paths = withContext(Dispatchers.IO) { OnionRequestAPI.paths }
setHasPaths(paths.isNotEmpty())
}
}
fun addReceiver(action: String): BroadcastReceiver = createReceiver { update() }.also { manager.registerReceiver(it, IntentFilter(action)) }
val receivers = listOf("buildingPaths", "pathsBuilt").map(::addReceiver)
DisposableEffect(Unit) {
onDispose {
receivers.forEach(manager::unregisterReceiver)
}
}
}
}
fun createReceiver(update: () -> Unit) = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { update() }
}

View File

@ -1,13 +1,17 @@
package org.thoughtcrime.securesms.ui
import android.graphics.drawable.BitmapDrawable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@ -23,6 +27,7 @@ import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Button
import androidx.compose.material.ButtonColors
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Card
@ -52,10 +57,12 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextAlign
@ -64,6 +71,8 @@ import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.res.ResourcesCompat
import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@ -259,6 +268,42 @@ fun <T> OptionsCard(card: OptionsCard<T>, callbacks: Callbacks<T>) {
}
@Composable
fun ItemButton(
@StringRes textId: Int,
@DrawableRes icon: Int,
colors: ButtonColors = transparentButtonColors(),
@StringRes contentDescription: Int = textId,
onClick: () -> Unit
) {
ItemButton(stringResource(textId), icon, colors, stringResource(contentDescription), onClick)
}
@Composable
fun ItemButtonWithDrawable(
@StringRes textId: Int,
@DrawableRes icon: Int,
colors: ButtonColors = transparentButtonColors(),
@StringRes contentDescription: Int = textId,
onClick: () -> Unit
) {
val context = LocalContext.current
ItemButton(
text = stringResource(textId),
icon = {
Image(
painter = rememberDrawablePainter(drawable = context.getDrawable(icon)),
contentDescription = stringResource(contentDescription),
modifier = Modifier.align(Alignment.Center)
)
},
colors = colors,
contentDescription = stringResource(contentDescription),
onClick = onClick
)
}
@Composable
fun ItemButton(
text: String,
@ -267,24 +312,43 @@ fun ItemButton(
contentDescription: String = text,
onClick: () -> Unit
) {
TextButton(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
colors = colors,
onClick = onClick,
shape = RectangleShape,
) {
Box(modifier = Modifier
.width(80.dp)
.fillMaxHeight()) {
ItemButton(
text = text,
icon = {
Icon(
painter = painterResource(id = icon),
contentDescription = contentDescription,
modifier = Modifier.align(Alignment.Center)
)
},
colors = colors,
contentDescription = contentDescription,
onClick = onClick
)
}
@Composable
fun ItemButton(
text: String,
icon: @Composable BoxScope.() -> Unit,
colors: ButtonColors = transparentButtonColors(),
contentDescription: String = text,
onClick: () -> Unit
) {
TextButton(
modifier = Modifier
.fillMaxWidth()
.height(60.dp),
colors = colors,
onClick = onClick,
shape = RectangleShape,
) {
Box(modifier = Modifier
.width(80.dp)
.fillMaxHeight()) {
icon()
}
Text(text, modifier = Modifier.fillMaxWidth())
Text(text, modifier = Modifier.fillMaxWidth(), style = MaterialTheme.typography.h8)
}
}
@ -308,9 +372,9 @@ fun CellWithPaddingAndMargin(
shape = RoundedCornerShape(16.dp),
elevation = 0.dp,
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
.padding(horizontal = margin),
.wrapContentHeight()
.fillMaxWidth()
.padding(horizontal = margin),
) {
Box(Modifier.padding(padding)) { content() }
}
@ -321,14 +385,14 @@ fun <T> TitledRadioButton(option: RadioOption<T>, onClick: () -> Unit) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.runIf(option.enabled) { clickable { if (!option.selected) onClick() } }
.heightIn(min = 60.dp)
.padding(horizontal = 32.dp)
.contentDescription(option.contentDescription)
.runIf(option.enabled) { clickable { if (!option.selected) onClick() } }
.heightIn(min = 60.dp)
.padding(horizontal = 32.dp)
.contentDescription(option.contentDescription)
) {
Column(modifier = Modifier
.weight(1f)
.align(Alignment.CenterVertically)) {
.weight(1f)
.align(Alignment.CenterVertically)) {
Column {
Text(
text = option.title(),
@ -349,8 +413,8 @@ fun <T> TitledRadioButton(option: RadioOption<T>, onClick: () -> Unit) {
onClick = null,
enabled = option.enabled,
modifier = Modifier
.height(26.dp)
.align(Alignment.CenterVertically)
.height(26.dp)
.align(Alignment.CenterVertically)
)
}
}
@ -373,8 +437,8 @@ fun Modifier.contentDescription(id: Int?): Modifier {
fun OutlineButton(text: GetString, contentDescription: GetString? = text, modifier: Modifier = Modifier, onClick: () -> Unit) {
OutlinedButton(
modifier = modifier
.size(108.dp, 34.dp)
.contentDescription(contentDescription),
.size(108.dp, 34.dp)
.contentDescription(contentDescription),
onClick = onClick,
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
shape = RoundedCornerShape(50), // = 50% percent
@ -396,37 +460,37 @@ fun Modifier.fadingEdges(
topEdgeHeight: Dp = 0.dp,
bottomEdgeHeight: Dp = 20.dp
): Modifier = this.then(
Modifier
// adding layer fixes issue with blending gradient and content
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
drawContent()
Modifier
// adding layer fixes issue with blending gradient and content
.graphicsLayer { alpha = 0.99F }
.drawWithContent {
drawContent()
val topColors = listOf(Color.Transparent, Color.Black)
val topStartY = scrollState.value.toFloat()
val topGradientHeight = min(topEdgeHeight.toPx(), topStartY)
if (topGradientHeight > 0f) drawRect(
brush = Brush.verticalGradient(
colors = topColors,
startY = topStartY,
endY = topStartY + topGradientHeight
),
blendMode = BlendMode.DstIn
)
val topColors = listOf(Color.Transparent, Color.Black)
val topStartY = scrollState.value.toFloat()
val topGradientHeight = min(topEdgeHeight.toPx(), topStartY)
if (topGradientHeight > 0f) drawRect(
brush = Brush.verticalGradient(
colors = topColors,
startY = topStartY,
endY = topStartY + topGradientHeight
),
blendMode = BlendMode.DstIn
)
val bottomColors = listOf(Color.Black, Color.Transparent)
val bottomEndY = size.height - scrollState.maxValue + scrollState.value
val bottomGradientHeight =
min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value)
if (bottomGradientHeight > 0f) drawRect(
brush = Brush.verticalGradient(
colors = bottomColors,
startY = bottomEndY - bottomGradientHeight,
endY = bottomEndY
),
blendMode = BlendMode.DstIn
)
}
val bottomColors = listOf(Color.Black, Color.Transparent)
val bottomEndY = size.height - scrollState.maxValue + scrollState.value
val bottomGradientHeight =
min(bottomEdgeHeight.toPx(), scrollState.maxValue.toFloat() - scrollState.value)
if (bottomGradientHeight > 0f) drawRect(
brush = Brush.verticalGradient(
colors = bottomColors,
startY = bottomEndY - bottomGradientHeight,
endY = bottomEndY
),
blendMode = BlendMode.DstIn
)
}
)
@Composable
@ -440,16 +504,16 @@ fun Divider() {
fun RowScope.Avatar(recipient: Recipient) {
Box(
modifier = Modifier
.width(60.dp)
.align(Alignment.CenterVertically)
.width(60.dp)
.align(Alignment.CenterVertically)
) {
AndroidView(
factory = {
ProfilePictureView(it).apply { update(recipient) }
},
modifier = Modifier
.width(46.dp)
.height(46.dp)
.width(46.dp)
.height(46.dp)
)
}
}
@ -476,8 +540,8 @@ fun Arc(
) {
Canvas(
modifier = modifier
.padding(strokeWidth)
.size(186.dp)
.padding(strokeWidth)
.size(186.dp)
) {
// Background Line
drawArc(
@ -506,8 +570,8 @@ fun RowScope.SessionShieldIcon() {
painter = painterResource(R.drawable.session_shield),
contentDescription = null,
modifier = Modifier
.align(Alignment.CenterVertically)
.wrapContentSize(unbounded = true)
.align(Alignment.CenterVertically)
.wrapContentSize(unbounded = true)
)
}

View File

@ -0,0 +1,79 @@
package org.thoughtcrime.securesms.ui.components
import android.graphics.Bitmap
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.Card
import androidx.compose.material.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import network.loki.messenger.R
import org.thoughtcrime.securesms.ui.LocalExtraColors
import org.thoughtcrime.securesms.util.QRCodeUtilities
@Composable
fun QrImageCard(
string: String,
contentDescription: String,
modifier: Modifier = Modifier,
icon: Int = R.drawable.session_shield
) {
Card(
backgroundColor = LocalExtraColors.current.lightCell,
elevation = 0.dp,
modifier = modifier
) { QrImage(string, contentDescription, icon) }
}
@Composable
fun QrImage(string: String, contentDescription: String, icon: Int = R.drawable.session_shield) {
var bitmap: Bitmap? by remember {
mutableStateOf(null)
}
val scope = rememberCoroutineScope()
LaunchedEffect(string) {
scope.launch(Dispatchers.IO) {
bitmap = QRCodeUtilities.encode(string, 400)
}
}
Box {
bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = contentDescription,
colorFilter = ColorFilter.tint(LocalExtraColors.current.onLightCell)
)
}
Icon(
painter = painterResource(id = icon),
contentDescription = "",
tint = LocalExtraColors.current.onLightCell,
modifier = Modifier
.align(Alignment.Center)
.width(46.dp)
.height(56.dp)
.background(color = LocalExtraColors.current.lightCell)
.padding(horizontal = 3.dp, vertical = 1.dp)
)
}
}

View File

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.util
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.View
@ -90,12 +91,17 @@ fun String.getThemeStyle(): Int = when (this) {
}
@StyleRes
fun Int.getDefaultAccentColor(): Int =
if (this == R.style.Ocean_Dark || this == R.style.Ocean_Light) R.style.PrimaryBlue
else R.style.PrimaryGreen
fun Int.getDefaultAccentColor(): Int = when (this) {
R.style.Ocean_Dark, R.style.Ocean_Light -> R.style.PrimaryBlue
else -> R.style.PrimaryGreen
}
data class ThemeState (
@StyleRes val theme: Int,
@StyleRes val accentStyle: Int,
val followSystem: Boolean
)
)
inline fun <reified T: Activity> Context.start() = Intent(this, T::class.java).also(::startActivity)
inline fun <reified T: Activity> Activity.show() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) }
inline fun <reified T: Activity> Activity.push() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) }

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="28dp" android:height="28dp">
<shape android:shape="oval">
<gradient
android:type="radial"
android:startColor="#FFCE3A"
android:endColor="#00FFCE3A"
android:gradientRadius="12dp"/>
</shape>
</item>
<item android:top="6dp" android:left="6dp" android:right="6dp" android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#FFCE3A"/>
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:width="28dp" android:height="28dp">
<shape android:shape="oval">
<gradient
android:type="radial"
android:startColor="#31F196"
android:endColor="#0031F196"
android:gradientRadius="12dp"/>
</shape>
</item>
<item android:top="6dp" android:left="6dp" android:right="6dp" android:bottom="6dp">
<shape android:shape="oval">
<solid android:color="#31F196"/>
</shape>
</item>
</layer-list>

View File

@ -88,392 +88,11 @@
android:textAlignment="center"
android:contentDescription="@string/AccessibilityId_account_id"
tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" />
<LinearLayout
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/large_spacing"
android:layout_marginTop="@dimen/large_spacing"
android:layout_marginRight="@dimen/large_spacing"
android:orientation="horizontal">
<Button
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/copyButton"
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_weight="1"
android:text="@string/copy" />
<Button
style="@style/Widget.Session.Button.Common.ProminentOutline"
android:id="@+id/shareButton"
android:layout_width="0dp"
android:layout_height="@dimen/medium_button_height"
android:layout_weight="1"
android:layout_marginStart="@dimen/medium_spacing"
android:text="@string/share" />
</LinearLayout>
<LinearLayout
android:background="@drawable/preference_single_no_padding"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/large_spacing"
android:layout_marginHorizontal="@dimen/large_spacing"
android:orientation="vertical">
<!-- Path -->
<RelativeLayout
android:id="@+id/pathButton"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:layout_width="match_parent"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_height="@dimen/setting_button_height">
<FrameLayout
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:id="@+id/pathContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size">
<org.thoughtcrime.securesms.home.PathStatusView
android:layout_gravity="center"
android:layout_width="@dimen/path_status_view_size"
android:layout_height="@dimen/path_status_view_size"/>
</FrameLayout>
<TextView
android:id="@+id/pathText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/pathContainer"
android:text="@string/activity_path_title" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/privacyButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height">
<ImageView
android:id="@+id/privacyContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_privacy_icon"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/privacyText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/privacyContainer"
android:text="@string/activity_settings_privacy_button_title" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/notificationsButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_notifications">
<ImageView
android:id="@+id/notificationsContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_speaker"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/notificationsText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/notificationsContainer"
android:text="@string/activity_settings_notifications_button_title"
/>
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/chatsButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_conversations">
<ImageView
android:id="@+id/chatsContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_conversations"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/chatsText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/chatsContainer"
android:text="@string/activity_settings_conversations_button_title" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/messageRequestsButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_message_requests">
<ImageView
android:id="@+id/messageRequestsContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_message_requests"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/messageRequestsTexts"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/messageRequestsContainer"
android:text="@string/activity_settings_message_requests_button_title" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/appearanceButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_appearance">
<ImageView
android:id="@+id/appearanceContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_appearance"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/appearanceText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/appearanceContainer"
android:text="@string/activity_settings_message_appearance_button_title" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/inviteFriendButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_invite_friend">
<ImageView
android:id="@+id/inviteFriendContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_invite_friend"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/inviteFriendTexts"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/inviteFriendContainer"
android:text="@string/activity_settings_invite_button_title" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/passwordButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_recovery_password_menu_item">
<ImageView
android:id="@+id/passwordContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_recovery_phrase"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/seedText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/passwordContainer"
android:text="@string/sessionRecoveryPassword" />
</RelativeLayout>
<View
android:id="@+id/passwordDivider"
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/helpButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_help">
<ImageView
android:id="@+id/helpContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_help"
android:scaleType="centerInside"
app:tint="?android:textColorPrimary" />
<TextView
android:id="@+id/helpTexts"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="?android:textColorPrimary"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/helpContainer"
android:text="@string/activity_settings_help_button" />
</RelativeLayout>
<View
android:layout_marginHorizontal="@dimen/very_large_spacing"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?colorDividerBackground" />
<RelativeLayout
android:id="@+id/clearAllDataButton"
android:background="?selectableItemBackground"
android:paddingHorizontal="@dimen/large_spacing"
android:layout_width="match_parent"
android:layout_height="@dimen/setting_button_height"
android:contentDescription="@string/AccessibilityId_clear_data">
<ImageView
android:id="@+id/clearContainer"
android:layout_width="@dimen/small_profile_picture_size"
android:layout_height="@dimen/small_profile_picture_size"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:src="@drawable/ic_clear_data"
android:scaleType="centerInside"/>
<TextView
android:id="@+id/clearText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textColor="@color/destructive"
android:textSize="@dimen/medium_font_size"
android:layout_marginHorizontal="@dimen/medium_spacing"
android:textStyle="bold"
android:gravity="center"
android:layout_toEndOf="@+id/clearContainer"
android:text="@string/activity_settings_clear_all_data_button_title" />
</RelativeLayout>
</LinearLayout>
android:layout_height="wrap_content"/>
<ImageView
android:id="@+id/oxenLogoImageView"