diff --git a/app/build.gradle b/app/build.gradle index 171c1657f8..6cd7dafde5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 87d5ea4ae7..35adb8f5bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -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() } // Create the message val message = VisibleMessage().applyExpiryMode(viewModel.threadId) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 67400a7a81..3bd695b1ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -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() } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt index 55a35a5435..500b4cea08 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt @@ -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.toggle() { value = !value } @Composable @@ -247,7 +208,3 @@ fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) { } } } - -fun Context.startRecoveryPasswordActivity() { - Intent(this, RecoveryPasswordActivity::class.java).also(::startActivity) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt index 9fee061dc4..f5648c681f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt @@ -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 - ) - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index 32b7e83cbb..4545f9b7a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -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(R.id.profile_picture_view) - ?.also(::setupProfilePictureView) + val profilePic = findViewById(R.id.profile_picture_view) + ?.also(::setupProfilePictureView) - val pictureIcon = findViewById(R.id.ic_pictures) + val pictureIcon = findViewById(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() } + Divider() + ItemButton(R.string.activity_settings_privacy_button_title, icon = R.drawable.ic_privacy_icon) { show() } + Divider() + ItemButton(R.string.activity_settings_notifications_button_title, icon = R.drawable.ic_speaker, contentDescription = R.string.AccessibilityId_notifications) { show() } + Divider() + ItemButton(R.string.activity_settings_conversations_button_title, icon = R.drawable.ic_conversations, contentDescription = R.string.AccessibilityId_conversations) { show() } + Divider() + ItemButton(R.string.activity_settings_message_requests_button_title, icon = R.drawable.ic_message_requests, contentDescription = R.string.AccessibilityId_message_requests) { show() } + Divider() + ItemButton(R.string.activity_settings_message_appearance_button_title, icon = R.drawable.ic_appearance, contentDescription = R.string.AccessibilityId_appearance) { show() } + 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() } + Divider() + } + ItemButton(R.string.activity_settings_help_button, icon = R.drawable.ic_help, contentDescription = R.string.AccessibilityId_help) { show() } + 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() } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index 19f0b15b9e..5b62a91135 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -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 OptionsCard(card: OptionsCard, callbacks: Callbacks) { } +@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 TitledRadioButton(option: RadioOption, 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 TitledRadioButton(option: RadioOption, 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) ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt new file mode 100644 index 0000000000..49b8942882 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/QrImage.kt @@ -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) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index 5ff823a15c..b543b7d807 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -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 -) \ No newline at end of file +) + +inline fun Context.start() = Intent(this, T::class.java).also(::startActivity) +inline fun Activity.show() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) } +inline fun Activity.push() = Intent(this, T::class.java).also(::startActivity).also { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) } diff --git a/app/src/main/res/drawable/ic_path_yellow.xml b/app/src/main/res/drawable/ic_path_yellow.xml new file mode 100644 index 0000000000..04f0c51545 --- /dev/null +++ b/app/src/main/res/drawable/ic_path_yellow.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_status.xml b/app/src/main/res/drawable/ic_status.xml new file mode 100644 index 0000000000..7b19ad1413 --- /dev/null +++ b/app/src/main/res/drawable/ic_status.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index a5bac6b069..364a54484c 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -88,392 +88,11 @@ android:textAlignment="center" android:contentDescription="@string/AccessibilityId_account_id" tools:text="05987d601943c267879be41830888066c6a024cbdc9a548d06813924bf3372ea78" /> - - - -