diff --git a/app/build.gradle b/app/build.gradle index bfb73f9fc..6e7e60605 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -5,7 +5,7 @@ buildscript { mavenCentral() } dependencies { - classpath "com.android.tools.build:gradle:$gradlePluginVersion" + classpath 'com.android.tools.build:gradle:7.4.2' classpath files('libs/gradle-witness.jar') classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlinVersion" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f2a4642b7..3ba789b60 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -106,23 +106,23 @@ android:name="org.thoughtcrime.securesms.onboarding.RegisterActivity" android:screenOrientation="portrait" android:theme="@style/Theme.Session.DayNight.FlatActionBar" /> - + Unit = {} ) = button( text, 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 70d83ef5e..60abb7cf0 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 @@ -113,7 +113,6 @@ import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companio import org.thoughtcrime.securesms.conversation.v2.MessageDetailActivity.Companion.ON_RESEND import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog -import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate @@ -163,6 +162,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.permissions.Permissions import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment @@ -1579,9 +1579,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val userPublicKey = textSecurePreferences.getLocalNumber() val isNoteToSelf = (recipient.isContactRecipient && recipient.address.toString() == userPublicKey) if (text.contains(seed) && !isNoteToSelf && !hasPermissionToSendSeed) { - val dialog = SendSeedDialog { sendTextOnlyMessage(true) } - dialog.show(supportFragmentManager, "Send Seed Dialog") - return null + startRecoveryPasswordActivity() } // Create the message val message = VisibleMessage() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 61732827f..4f2d29edb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -362,7 +362,7 @@ fun FileDetails(fileDetails: List) { fun TitledErrorText(titledText: TitledText?) { TitledText( titledText, - valueStyle = LocalTextStyle.current.copy(color = colorDestructive) + style = LocalTextStyle.current.copy(color = colorDestructive) ) } @@ -370,7 +370,7 @@ fun TitledErrorText(titledText: TitledText?) { fun TitledMonospaceText(titledText: TitledText?) { TitledText( titledText, - valueStyle = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) + style = LocalTextStyle.current.copy(fontFamily = FontFamily.Monospace) ) } @@ -378,11 +378,11 @@ fun TitledMonospaceText(titledText: TitledText?) { fun TitledText( titledText: TitledText?, modifier: Modifier = Modifier, - valueStyle: TextStyle = LocalTextStyle.current, + style: TextStyle = LocalTextStyle.current, ) { titledText?.apply { TitledView(title, modifier) { - Text(text, style = valueStyle, modifier = Modifier.fillMaxWidth()) + Text(text, style = style, modifier = Modifier.fillMaxWidth()) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt index 8188c4f7d..d5ef6434e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/ThumbnailProgressBar.kt @@ -31,7 +31,6 @@ class ThumbnailProgressBar: View { private val drawingRect = Rect() override fun dispatchDraw(canvas: Canvas) { - getDrawingRect(objectRect) drawingRect.set(objectRect) 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 f25545572..e806266ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -10,9 +10,33 @@ import android.content.Intent import android.content.IntentFilter import android.os.Build import android.os.Bundle -import android.text.SpannableString import android.widget.Toast import androidx.activity.viewModels +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredWidth +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle @@ -69,12 +93,18 @@ 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.SeedActivity -import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate +import org.thoughtcrime.securesms.onboarding.recoverypassword.startRecoveryPasswordActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.preferences.SettingsActivity import org.thoughtcrime.securesms.showMuteDialog import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.SessionShieldIcon +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.h8 +import org.thoughtcrime.securesms.ui.small import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.IP2Country @@ -89,7 +119,6 @@ import javax.inject.Inject @AndroidEntryPoint class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, - SeedReminderViewDelegate, GlobalSearchInputLayout.GlobalSearchInputLayoutListener { companion object { @@ -178,15 +207,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), binding.sessionToolbar.disableClipping() // Set up seed reminder view lifecycleScope.launchWhenStarted { - val hasViewedSeed = textSecurePreferences.getHasViewedSeed() - if (!hasViewedSeed) { - binding.seedReminderView.isVisible = true - binding.seedReminderView.title = SpannableString("You're almost finished! 80%") // Intentionally not yet translated - binding.seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_1) - binding.seedReminderView.setProgress(80, false) - binding.seedReminderView.delegate = this@HomeActivity - } else { - binding.seedReminderView.isVisible = false + binding.seedReminderView.setContent { + if (!textSecurePreferences.getHasViewedSeed()) SeedReminder() } } setupMessageRequestsBanner() @@ -203,7 +225,8 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } // Set up empty state view - binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() } + binding.emptyStateContainer.setContent { EmptyView() } + IP2Country.configureIfNeeded(this@HomeActivity) startObservingUpdates() @@ -315,6 +338,76 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), } } + @Preview + @Composable + fun PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int + ) { + PreviewTheme(themeResId) { + SeedReminder() + } + } + + @Composable + private fun SeedReminder() { + AppTheme { + Column { + Box( + Modifier + .fillMaxWidth() + .height(4.dp) + .background(MaterialTheme.colors.secondary)) + Row( + Modifier + .background(MaterialTheme.colors.surface) + .padding(horizontal = 24.dp, vertical = 16.dp) + ) { + Column(Modifier.weight(1f)) { + Row { + Text("Save your recovery password", style = MaterialTheme.typography.h8) + Spacer(Modifier.requiredWidth(8.dp)) + SessionShieldIcon() + } + Text("Save your recovery password to make sure you don't lose access to your account.", style = MaterialTheme.typography.small) + } + Spacer(Modifier.width(12.dp)) + OutlineButton( + stringResource(R.string.continue_2), + Modifier.align(Alignment.CenterVertically) + ) { startRecoveryPasswordActivity() } + } + } + } + } + + @Composable + private fun EmptyView() { + AppTheme { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(horizontal = 50.dp) + .padding(bottom = 12.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Icon( + painter = painterResource(id = R.drawable.emoji_tada), + contentDescription = null, + tint = Color.Unspecified + ) + Text("Account Created", style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) + Text("Welcome to Session", color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + Divider(modifier = Modifier.padding(vertical = 16.dp)) + Text("You don't have any conversations yet", + style = MaterialTheme.typography.h8, + textAlign = TextAlign.Center, + modifier = Modifier.padding(bottom = 12.dp)) + Text("Hit the plus button to start a chat, create a group, or join an official communitiy!", textAlign = TextAlign.Center) + Spacer(modifier = Modifier.weight(2f)) + } + } + } + override fun onInputFocusChanged(hasFocus: Boolean) { if (hasFocus) { setSearchShown(true) @@ -463,11 +556,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), super.onBackPressed() } - override fun handleSeedReminderViewContinueButtonTapped() { - val intent = Intent(this, SeedActivity::class.java) - show(intent) - } - override fun onConversationClick(thread: ThreadRecord) { val intent = Intent(this, ConversationActivityV2::class.java) intent.putExtra(ConversationActivityV2.THREAD_ID, thread.threadId) @@ -491,7 +579,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), bottomSheet.dismiss() if (!thread.recipient.isGroupRecipient && !thread.recipient.isLocalNumber) { val clip = ClipData.newPlainText("Session ID", thread.recipient.address.toString()) - val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager manager.setPrimaryClip(clip) Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } @@ -500,7 +588,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), val openGroup = DatabaseComponent.get(this@HomeActivity).lokiThreadDatabase().getOpenGroupChat(threadId) ?: return@onCopyConversationId Unit val clip = ClipData.newPlainText("Community URL", openGroup.joinURL) - val manager = getSystemService(PassphraseRequiredActionBarActivity.CLIPBOARD_SERVICE) as ClipboardManager + val manager = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager manager.setPrimaryClip(clip) Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt deleted file mode 100644 index c0699e3eb..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/DisplayNameActivity.kt +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.Intent -import android.os.Bundle -import android.view.KeyEvent -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputMethodManager -import android.widget.TextView.OnEditorActionListener -import android.widget.Toast -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityDisplayNameBinding -import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import org.session.libsession.utilities.TextSecurePreferences - -class DisplayNameActivity : BaseActionBarActivity() { - private lateinit var binding: ActivityDisplayNameBinding - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpActionBarSessionLogo() - binding = ActivityDisplayNameBinding.inflate(layoutInflater) - setContentView(binding.root) - with(binding) { - displayNameEditText.imeOptions = displayNameEditText.imeOptions or 16777216 // Always use incognito keyboard - displayNameEditText.setOnEditorActionListener( - OnEditorActionListener { _, actionID, event -> - if (actionID == EditorInfo.IME_ACTION_SEARCH || - actionID == EditorInfo.IME_ACTION_DONE || - (event.action == KeyEvent.ACTION_DOWN && - event.keyCode == KeyEvent.KEYCODE_ENTER)) { - register() - return@OnEditorActionListener true - } - false - }) - registerButton.setOnClickListener { register() } - } - } - - private fun register() { - val displayName = binding.displayNameEditText.text.toString().trim() - if (displayName.isEmpty()) { - return Toast.makeText(this, R.string.activity_display_name_display_name_missing_error, Toast.LENGTH_SHORT).show() - } - if (displayName.toByteArray().size > ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH) { - return Toast.makeText(this, R.string.activity_display_name_display_name_too_long_error, Toast.LENGTH_SHORT).show() - } - val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.hideSoftInputFromWindow(binding.displayNameEditText.windowToken, 0) - TextSecurePreferences.setProfileName(this, displayName) - val intent = Intent(this, PNModeActivity::class.java) - push(intent) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt deleted file mode 100644 index dcd4d783e..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/FakeChatView.kt +++ /dev/null @@ -1,79 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.animation.FloatEvaluator -import android.animation.ValueAnimator -import android.content.Context -import android.os.Handler -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.ScrollView -import network.loki.messenger.R -import network.loki.messenger.databinding.ViewFakeChatBinding -import org.thoughtcrime.securesms.util.disableClipping - -class FakeChatView : ScrollView { - private lateinit var binding: ViewFakeChatBinding - // region Settings - private val spacing = context.resources.getDimension(R.dimen.medium_spacing) - private val startDelay: Long = 1000 - private val delayBetweenMessages: Long = 1500 - private val animationDuration: Long = 400 - // endregion - - // region Lifecycle - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { - binding = ViewFakeChatBinding.inflate(LayoutInflater.from(context), this, true) - binding.root.disableClipping() - isVerticalScrollBarEnabled = false - } - // endregion - - // region Animation - fun startAnimating() { - listOf( binding.bubble1, binding.bubble2, binding.bubble3, binding.bubble4, binding.bubble5 ).forEach { it.alpha = 0.0f } - fun show(bubble: View) { - val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) - animation.duration = animationDuration - animation.addUpdateListener { animator -> - bubble.alpha = animator.animatedValue as Float - } - animation.start() - } - Handler().postDelayed({ - show(binding.bubble1) - Handler().postDelayed({ - show(binding.bubble2) - Handler().postDelayed({ - show(binding.bubble3) - smoothScrollTo(0, (binding.bubble1.height + spacing).toInt()) - Handler().postDelayed({ - show(binding.bubble4) - smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt()) - Handler().postDelayed({ - show(binding.bubble5) - smoothScrollTo(0, (binding.bubble1.height + spacing).toInt() + (binding.bubble2.height + spacing).toInt() + (binding.bubble3.height + spacing).toInt()) - }, delayBetweenMessages) - }, delayBetweenMessages) - }, delayBetweenMessages) - }, delayBetweenMessages) - }, startDelay) - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt index f67f0fbaa..d0f073516 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LandingActivity.kt @@ -1,41 +1,147 @@ package org.thoughtcrime.securesms.onboarding import android.content.Intent +import android.net.Uri import android.os.Bundle -import network.loki.messenger.databinding.ActivityLandingBinding +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import network.loki.messenger.R import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity import org.thoughtcrime.securesms.service.KeyCachingService -import org.thoughtcrime.securesms.util.push +import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.BorderlessButton +import org.thoughtcrime.securesms.ui.FilledButton +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.classicDarkColors +import org.thoughtcrime.securesms.ui.session_accent import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo class LandingActivity : BaseActionBarActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityLandingBinding.inflate(layoutInflater) - setContentView(binding.root) setUpActionBarSessionLogo(true) - with(binding) { - fakeChatView.startAnimating() - registerButton.setOnClickListener { register() } - restoreButton.setOnClickListener { link() } - linkButton.setOnClickListener { link() } - } + + ComposeView(this) + .apply { setContent { LandingScreen() } } + .let(::setContentView) + IdentityKeyUtil.generateIdentityKeyPair(this) TextSecurePreferences.setPasswordDisabled(this, true) // AC: This is a temporary workaround to trick the old code that the screen is unlocked. KeyCachingService.setMasterSecret(applicationContext, Object()) } - private fun register() { - val intent = Intent(this, RegisterActivity::class.java) - push(intent) + @Preview + @Composable + private fun LandingScreen() { + AppTheme { + Column(modifier = Modifier.padding(horizontal = 36.dp)) { + Spacer(modifier = Modifier.weight(1f)) + Text("Privacy in your pocket.", modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h4, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(24.dp)) + IncomingText("Welcome to Session \uD83D\uDC4B") + Spacer(modifier = Modifier.height(14.dp)) + OutgoingText("Session is engineered\nto protect your privacy.") + Spacer(modifier = Modifier.height(14.dp)) + IncomingText("You don’t even need a phone number to sign up. ") + Spacer(modifier = Modifier.height(14.dp)) + OutgoingText("Creating an account is \ninstant, free, and \nanonymous \uD83D\uDC47") + Spacer(modifier = Modifier.weight(1f)) + + OutlineButton(text = "Create account", modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally)) { startPickDisplayNameActivity() } + Spacer(modifier = Modifier.height(14.dp)) + FilledButton(text = "I have an account", modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally)) { startLinkDeviceActivity() } + Spacer(modifier = Modifier.height(8.dp)) + BorderlessButton( + text = "By using this service, you agree to our Terms of Service and Privacy Policy", + modifier = Modifier + .width(262.dp) + .align(Alignment.CenterHorizontally), + fontSize = 11.sp, + lineHeight = 13.sp + ) { openDialog() } + Spacer(modifier = Modifier.height(8.dp)) + } + } } - private fun link() { - val intent = Intent(this, LinkDeviceActivity::class.java) - push(intent) + private fun openDialog() { + showSessionDialog { + title(R.string.activity_landing_open_url_title) + text(R.string.activity_landing_open_url_explanation) + button(R.string.activity_landing_terms_of_service) { open("https://getsession.org/terms-of-service") } + button(R.string.activity_landing_privacy_policy) { open("https://getsession.org/privacy-policy") } + } } -} \ No newline at end of file + + private fun open(url: String) { + Intent(Intent.ACTION_VIEW, Uri.parse(url)).let(::startActivity) + } + + @Composable + private fun IncomingText(text: String) { + ChatText( + text, + color = classicDarkColors[2] + ) + } + + @Composable + private fun ColumnScope.OutgoingText(text: String) { + ChatText( + text, + color = session_accent, + textColor = MaterialTheme.colors.primary, + modifier = Modifier.align(Alignment.End) + ) + } + + @Composable + private fun ChatText( + text: String, + color: Color, + textColor: Color = Color.Unspecified, + modifier: Modifier = Modifier + ) { + Text( + text, + fontSize = 16.sp, + lineHeight = 19.sp, + color = textColor, + modifier = modifier + .fillMaxWidth(0.666f) + .background( + color = color, + shape = RoundedCornerShape(size = 13.dp) + ) + .padding(horizontal = 16.dp, vertical = 12.dp) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt index edd1bc274..0e06bdb93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LinkDeviceActivity.kt @@ -10,54 +10,28 @@ import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputMethodManager import android.widget.Toast -import androidx.core.view.isVisible import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter -import androidx.lifecycle.lifecycleScope -import com.google.android.material.snackbar.Snackbar import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.launch import network.loki.messenger.R import network.loki.messenger.databinding.ActivityLinkDeviceBinding import network.loki.messenger.databinding.FragmentRecoveryPhraseBinding -import org.session.libsession.snode.SnodeModule import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.hexEncodedPublicKey -import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.KeyPairUtilities import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate -import org.thoughtcrime.securesms.util.push import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import javax.inject.Inject @AndroidEntryPoint class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDelegate { - @Inject - lateinit var configFactory: ConfigFactory - private lateinit var binding: ActivityLinkDeviceBinding - internal val database: LokiAPIDatabaseProtocol - get() = SnodeModule.shared.storage - private val adapter = LinkDeviceActivityAdapter(this) - private var restoreJob: Job? = null - override fun onBackPressed() { - if (restoreJob?.isActive == true) return // Don't allow going back with a pending job - super.onBackPressed() - } + private val adapter = LinkDeviceActivityAdapter(this) // region Lifecycle override fun onCreate(savedInstanceState: Bundle?) { @@ -106,58 +80,7 @@ class LinkDeviceActivity : BaseActionBarActivity(), ScanQRCodeWrapperFragmentDel } private fun continueWithSeed(seed: ByteArray) { - - // only have one sync job running at a time (prevent QR from trying to spawn a new job) - if (restoreJob?.isActive == true) return - - restoreJob = lifecycleScope.launch { - // This is here to resolve a case where the app restarts before a user completes onboarding - // which can result in an invalid database state - database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() - - // RestoreActivity handles seed this way - val keyPairGenerationResult = KeyPairUtilities.generate(seed) - val x25519KeyPair = keyPairGenerationResult.x25519KeyPair - KeyPairUtilities.store(this@LinkDeviceActivity, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) - configFactory.keyPairChanged() - val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey - val registrationID = KeyHelper.generateRegistrationId(false) - TextSecurePreferences.setLocalRegistrationId(this@LinkDeviceActivity, registrationID) - TextSecurePreferences.setLocalNumber(this@LinkDeviceActivity, userHexEncodedPublicKey) - TextSecurePreferences.setRestorationTime(this@LinkDeviceActivity, System.currentTimeMillis()) - TextSecurePreferences.setHasViewedSeed(this@LinkDeviceActivity, true) - - binding.loader.isVisible = true - val snackBar = Snackbar.make(binding.containerLayout, R.string.activity_link_device_skip_prompt,Snackbar.LENGTH_INDEFINITE) - .setAction(R.string.registration_activity__skip) { register(true) } - - val skipJob = launch { - delay(15_000L) - snackBar.show() - } - // start polling and wait for updated message - ApplicationContext.getInstance(this@LinkDeviceActivity).apply { - startPollingIfNeeded() - } - TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect { - // handle we've synced - snackBar.dismiss() - skipJob.cancel() - register(false) - } - - binding.loader.isVisible = false - } - } - - private fun register(skipped: Boolean) { - restoreJob?.cancel() - binding.loader.isVisible = false - TextSecurePreferences.setLastConfigurationSyncTime(this, System.currentTimeMillis()) - val intent = Intent(this@LinkDeviceActivity, if (skipped) DisplayNameActivity::class.java else PNModeActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - push(intent) + startLoadingActivity(seed) } // endregion } @@ -227,3 +150,7 @@ class RecoveryPhraseFragment : Fragment() { } } // endregion + +fun Context.startLinkDeviceActivity() { + Intent(this, LinkDeviceActivity::class.java).let(::startActivity) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt new file mode 100644 index 000000000..0d053de05 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingActivity.kt @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.onboarding + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.TweenSpec +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity +import org.thoughtcrime.securesms.onboarding.pickname.startPickDisplayNameActivity +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.ProgressArc +import javax.inject.Inject + +private const val EXTRA_MNEMONIC = "mnemonic" + +@AndroidEntryPoint +class LoadingActivity: BaseActionBarActivity() { + + @Inject + lateinit var configFactory: ConfigFactory + + @Inject + lateinit var prefs: TextSecurePreferences + + private val viewModel: LoadingViewModel by viewModels() + + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + return + } + + private fun register(skipped: Boolean) { + prefs.setLastConfigurationSyncTime(System.currentTimeMillis()) + + val flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + + when { + skipped -> startPickDisplayNameActivity(true, flags) + else -> startPNModeActivity(flags) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + ComposeView(this) + .apply { setContent { LoadingScreen() } } + .let(::setContentView) + + viewModel.restore(application, intent.getByteArrayExtra(EXTRA_MNEMONIC)!!) + + lifecycleScope.launch { + viewModel.eventFlow.collect { + when (it) { + Event.TIMEOUT -> register(skipped = true) + Event.SUCCESS -> register(skipped = false) + } + } + } + } + + @Composable + fun LoadingScreen() { + val state by viewModel.stateFlow.collectAsState() + + val animatable = remember { Animatable(initialValue = 0f, visibilityThreshold = 0.005f) } + + LaunchedEffect(state) { + animatable.stop() + animatable.animateTo( + targetValue = 1f, + animationSpec = TweenSpec(durationMillis = state.duration.inWholeMilliseconds.toInt()) + ) + } + + AppTheme { + Column { + Spacer(modifier = Modifier.weight(1f)) + ProgressArc(animatable.value, modifier = Modifier.align(Alignment.CenterHorizontally)) + Text("One moment please..", modifier = Modifier.align(Alignment.CenterHorizontally), style = MaterialTheme.typography.h6) + Text("Loading your account", modifier = Modifier.align(Alignment.CenterHorizontally)) + Spacer(modifier = Modifier.weight(2f)) + } + } + } +} + +fun Context.startLoadingActivity(mnemonic: ByteArray) { + Intent(this, LoadingActivity::class.java) + .apply { putExtra(EXTRA_MNEMONIC, mnemonic) } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt new file mode 100644 index 000000000..68bc5cc12 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/LoadingViewModel.kt @@ -0,0 +1,102 @@ +package org.thoughtcrime.securesms.onboarding + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import org.session.libsession.snode.SnodeModule +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.database.LokiAPIDatabaseProtocol +import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory +import javax.inject.Inject +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds + +data class State(val duration: Duration) + +private val DONE_TIME = 1.seconds +private val DONE_ANIMATE_TIME = 500.milliseconds + +private val TOTAL_ANIMATE_TIME = 14.seconds +private val TOTAL_TIME = 15.seconds + +@HiltViewModel +class LoadingViewModel @Inject constructor( + private val configFactory: ConfigFactory, + private val prefs: TextSecurePreferences, +) : ViewModel() { + + private val state = MutableStateFlow(State(TOTAL_ANIMATE_TIME)) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow() + + private var restoreJob: Job? = null + + internal val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage + + fun restore(context: Context, seed: ByteArray) { + + // only have one sync job running at a time (prevent QR from trying to spawn a new job) + if (restoreJob?.isActive == true) return + + restoreJob = viewModelScope.launch(Dispatchers.IO) { + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + + // RestoreActivity handles seed this way + val keyPairGenerationResult = KeyPairUtilities.generate(seed) + val x25519KeyPair = keyPairGenerationResult.x25519KeyPair + KeyPairUtilities.store(context, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() + val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + prefs.apply { + setLocalRegistrationId(registrationID) + setLocalNumber(userHexEncodedPublicKey) + setRestorationTime(System.currentTimeMillis()) + setHasViewedSeed(true) + } + + val skipJob = launch(Dispatchers.IO) { + delay(TOTAL_TIME) + event.send(Event.TIMEOUT) + } + + // start polling and wait for updated message + ApplicationContext.getInstance(context).apply { startPollingIfNeeded() } + TextSecurePreferences.events.filter { it == TextSecurePreferences.CONFIGURATION_SYNCED }.collect { + // handle we've synced + skipJob.cancel() + + state.value = State(DONE_ANIMATE_TIME) + delay(DONE_TIME) + event.send(Event.SUCCESS) + } + } + } +} + +sealed interface Event { + object SUCCESS: Event + object TIMEOUT: Event +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt deleted file mode 100644 index e4e8e6a9a..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/PNModeActivity.kt +++ /dev/null @@ -1,179 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.animation.ArgbEvaluator -import android.animation.ValueAnimator -import android.content.Intent -import android.graphics.drawable.TransitionDrawable -import android.net.Uri -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.Toast -import androidx.annotation.ColorInt -import androidx.annotation.DrawableRes -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityPnModeBinding -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.ThemeUtil -import org.thoughtcrime.securesms.ApplicationContext -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.home.HomeActivity -import org.thoughtcrime.securesms.notifications.PushManager -import org.thoughtcrime.securesms.notifications.PushRegistry -import org.thoughtcrime.securesms.showSessionDialog -import org.thoughtcrime.securesms.util.GlowViewUtilities -import org.thoughtcrime.securesms.util.PNModeView -import org.thoughtcrime.securesms.util.disableClipping -import org.thoughtcrime.securesms.util.getAccentColor -import org.thoughtcrime.securesms.util.getColorWithID -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import org.thoughtcrime.securesms.util.show -import javax.inject.Inject - -@AndroidEntryPoint -class PNModeActivity : BaseActionBarActivity() { - - @Inject lateinit var pushRegistry: PushRegistry - - private lateinit var binding: ActivityPnModeBinding - private var selectedOptionView: PNModeView? = null - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpActionBarSessionLogo(true) - TextSecurePreferences.setHasSeenWelcomeScreen(this, true) - binding = ActivityPnModeBinding.inflate(layoutInflater) - setContentView(binding.root) - with(binding) { - contentView.disableClipping() - fcmOptionView.setOnClickListener { toggleFCM() } - fcmOptionView.mainColor = ThemeUtil.getThemedColor(root.context, R.attr.colorPrimary) - fcmOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) - backgroundPollingOptionView.setOnClickListener { toggleBackgroundPolling() } - backgroundPollingOptionView.mainColor = ThemeUtil.getThemedColor(root.context, R.attr.colorPrimary) - backgroundPollingOptionView.strokeColor = resources.getColorWithID(R.color.pn_option_border, theme) - registerButton.setOnClickListener { register() } - } - toggleFCM() - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.menu_pn_mode, menu) - return true - } - // endregion - - // region Animation - private fun performTransition(@DrawableRes transitionID: Int, subject: View) { - val drawable = resources.getDrawable(transitionID, theme) as TransitionDrawable - subject.background = drawable - drawable.startTransition(250) - } - // endregion - - // region Interaction - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when(item.itemId) { - R.id.learnMoreButton -> learnMore() - else -> { /* Do nothing */ } - } - return super.onOptionsItemSelected(item) - } - - private fun learnMore() { - try { - val url = "https://getsession.org/faq/#privacy" - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { - Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - } - - private fun toggleFCM() = with(binding) { - val accentColor = getAccentColor() - when (selectedOptionView) { - null -> { - performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(fcmOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - selectedOptionView = fcmOptionView - } - fcmOptionView -> { - performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = null - } - backgroundPollingOptionView -> { - performTransition(R.drawable.pn_option_background_select_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(fcmOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = fcmOptionView - } - } - } - - private fun toggleBackgroundPolling() = with(binding) { - val accentColor = getAccentColor() - when (selectedOptionView) { - null -> { - performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - selectedOptionView = backgroundPollingOptionView - } - backgroundPollingOptionView -> { - performTransition(R.drawable.pn_option_background_deselect_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(backgroundPollingOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = null - } - fcmOptionView -> { - performTransition(R.drawable.pn_option_background_select_transition, backgroundPollingOptionView) - GlowViewUtilities.animateShadowColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.transparent, theme), accentColor) - animateStrokeColorChange(backgroundPollingOptionView, resources.getColorWithID(R.color.pn_option_border, theme), accentColor) - performTransition(R.drawable.pn_option_background_deselect_transition, fcmOptionView) - GlowViewUtilities.animateShadowColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.transparent, theme)) - animateStrokeColorChange(fcmOptionView, accentColor, resources.getColorWithID(R.color.pn_option_border, theme)) - selectedOptionView = backgroundPollingOptionView - } - } - } - - private fun animateStrokeColorChange(bubble: PNModeView, @ColorInt startColor: Int, @ColorInt endColor: Int) { - val animation = ValueAnimator.ofObject(ArgbEvaluator(), startColor, endColor) - animation.duration = 250 - animation.addUpdateListener { animator -> - val color = animator.animatedValue as Int - bubble.strokeColor = color - } - animation.start() - } - - private fun register() { - if (selectedOptionView == null) { - showSessionDialog { - title(R.string.activity_pn_mode_no_option_picked_dialog_title) - button(R.string.ok) - } - return - } - - TextSecurePreferences.setPushEnabled(this, (selectedOptionView == binding.fcmOptionView)) - val application = ApplicationContext.getInstance(this) - application.startPollingIfNeeded() - pushRegistry.refresh(true) - val intent = Intent(this, HomeActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - intent.putExtra(HomeActivity.FROM_ONBOARDING, true) - show(intent) - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt deleted file mode 100644 index 051cd7542..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RecoveryPhraseRestoreActivity.kt +++ /dev/null @@ -1,114 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.Intent -import android.graphics.Typeface -import android.net.Uri -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.text.style.StyleSpan -import android.view.View -import android.widget.Toast -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityRecoveryPhraseRestoreBinding -import org.session.libsession.snode.SnodeModule -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.utilities.Hex -import org.session.libsignal.utilities.KeyHelper -import org.session.libsignal.utilities.hexEncodedPublicKey -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.KeyPairUtilities -import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import javax.inject.Inject - -@AndroidEntryPoint -class RecoveryPhraseRestoreActivity : BaseActionBarActivity() { - - @Inject - lateinit var configFactory: ConfigFactory - - private lateinit var binding: ActivityRecoveryPhraseRestoreBinding - internal val database: LokiAPIDatabaseProtocol - get() = SnodeModule.shared.storage - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setUpActionBarSessionLogo() - TextSecurePreferences.apply { - setHasViewedSeed(this@RecoveryPhraseRestoreActivity, true) - setConfigurationMessageSynced(this@RecoveryPhraseRestoreActivity, false) - setRestorationTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) - setLastProfileUpdateTime(this@RecoveryPhraseRestoreActivity, System.currentTimeMillis()) - } - binding = ActivityRecoveryPhraseRestoreBinding.inflate(layoutInflater) - setContentView(binding.root) - binding.mnemonicEditText.imeOptions = binding.mnemonicEditText.imeOptions or 16777216 // Always use incognito keyboard - binding.restoreButton.setOnClickListener { restore() } - val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") - termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(object : ClickableSpan() { - - override fun onClick(widget: View) { - openURL("https://getsession.org/terms-of-service/") - } - }, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(object : ClickableSpan() { - - override fun onClick(widget: View) { - openURL("https://getsession.org/privacy-policy/") - } - }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - binding.termsTextView.movementMethod = LinkMovementMethod.getInstance() - binding.termsTextView.text = termsExplanation - } - // endregion - - // region Interaction - private fun restore() { - val mnemonic = binding.mnemonicEditText.text.toString() - try { - // This is here to resolve a case where the app restarts before a user completes onboarding - // which can result in an invalid database state - database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() - - val loadFileContents: (String) -> String = { fileName -> - MnemonicUtilities.loadFileContents(this, fileName) - } - val hexEncodedSeed = MnemonicCodec(loadFileContents).decode(mnemonic) - val seed = Hex.fromStringCondensed(hexEncodedSeed) - val keyPairGenerationResult = KeyPairUtilities.generate(seed) - val x25519KeyPair = keyPairGenerationResult.x25519KeyPair - KeyPairUtilities.store(this, seed, keyPairGenerationResult.ed25519KeyPair, x25519KeyPair) - configFactory.keyPairChanged() - val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey - val registrationID = KeyHelper.generateRegistrationId(false) - TextSecurePreferences.setLocalRegistrationId(this, registrationID) - TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) - val intent = Intent(this, DisplayNameActivity::class.java) - push(intent) - } catch (e: Exception) { - val message = if (e is MnemonicCodec.DecodingError) e.description else MnemonicCodec.DecodingError.Generic.description - return Toast.makeText(this, message, Toast.LENGTH_SHORT).show() - } - } - - private fun openURL(url: String) { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { - Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt deleted file mode 100644 index 6e082e000..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/RegisterActivity.kt +++ /dev/null @@ -1,157 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.graphics.Typeface -import android.net.Uri -import android.os.Bundle -import android.os.Handler -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.method.LinkMovementMethod -import android.text.style.ClickableSpan -import android.text.style.StyleSpan -import android.view.View -import android.widget.Toast -import com.goterl.lazysodium.utils.KeyPair -import dagger.hilt.android.AndroidEntryPoint -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityRegisterBinding -import org.session.libsession.snode.SnodeModule -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsignal.crypto.ecc.ECKeyPair -import org.session.libsignal.database.LokiAPIDatabaseProtocol -import org.session.libsignal.utilities.KeyHelper -import org.session.libsignal.utilities.hexEncodedPublicKey -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.KeyPairUtilities -import org.thoughtcrime.securesms.dependencies.ConfigFactory -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo -import javax.inject.Inject - -@AndroidEntryPoint -class RegisterActivity : BaseActionBarActivity() { - - @Inject - lateinit var configFactory: ConfigFactory - - private lateinit var binding: ActivityRegisterBinding - internal val database: LokiAPIDatabaseProtocol - get() = SnodeModule.shared.storage - private var seed: ByteArray? = null - private var ed25519KeyPair: KeyPair? = null - private var x25519KeyPair: ECKeyPair? = null - set(value) { field = value; updatePublicKeyTextView() } - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivityRegisterBinding.inflate(layoutInflater) - setContentView(binding.root) - setUpActionBarSessionLogo() - TextSecurePreferences.apply { - setHasViewedSeed(this@RegisterActivity, false) - setConfigurationMessageSynced(this@RegisterActivity, true) - setRestorationTime(this@RegisterActivity, 0) - setLastProfileUpdateTime(this@RegisterActivity, System.currentTimeMillis()) - } - binding.registerButton.setOnClickListener { register() } - binding.copyButton.setOnClickListener { copyPublicKey() } - val termsExplanation = SpannableStringBuilder("By using this service, you agree to our Terms of Service and Privacy Policy") - termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(object : ClickableSpan() { - - override fun onClick(widget: View) { - openURL("https://getsession.org/terms-of-service/") - } - }, 40, 56, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(StyleSpan(Typeface.BOLD), 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - termsExplanation.setSpan(object : ClickableSpan() { - - override fun onClick(widget: View) { - openURL("https://getsession.org/privacy-policy/") - } - }, 61, 75, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - binding.termsTextView.movementMethod = LinkMovementMethod.getInstance() - binding.termsTextView.text = termsExplanation - updateKeyPair() - } - // endregion - - // region Updating - private fun updateKeyPair() { - val keyPairGenerationResult = KeyPairUtilities.generate() - seed = keyPairGenerationResult.seed - ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair - x25519KeyPair = keyPairGenerationResult.x25519KeyPair - } - - private fun updatePublicKeyTextView() { - val hexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey - val characterCount = hexEncodedPublicKey.count() - var count = 0 - val limit = 32 - fun animate() { - val numberOfIndexesToShuffle = 32 - count - val indexesToShuffle = (0 until characterCount).shuffled().subList(0, numberOfIndexesToShuffle) - var mangledHexEncodedPublicKey = hexEncodedPublicKey - for (index in indexesToShuffle) { - try { - mangledHexEncodedPublicKey = mangledHexEncodedPublicKey.substring(0, index) + "0123456789abcdef__".random() + mangledHexEncodedPublicKey.substring(index + 1, mangledHexEncodedPublicKey.count()) - } catch (exception: Exception) { - // Do nothing - } - } - count += 1 - if (count < limit) { - binding.publicKeyTextView.text = mangledHexEncodedPublicKey - Handler().postDelayed({ - animate() - }, 32) - } else { - binding.publicKeyTextView.text = hexEncodedPublicKey - } - } - animate() - } - // endregion - - // region Interaction - private fun register() { - // This is here to resolve a case where the app restarts before a user completes onboarding - // which can result in an invalid database state - database.clearAllLastMessageHashes() - database.clearReceivedMessageHashValues() - - KeyPairUtilities.store(this, seed!!, ed25519KeyPair!!, x25519KeyPair!!) - configFactory.keyPairChanged() - val userHexEncodedPublicKey = x25519KeyPair!!.hexEncodedPublicKey - val registrationID = KeyHelper.generateRegistrationId(false) - TextSecurePreferences.setLocalRegistrationId(this, registrationID) - TextSecurePreferences.setLocalNumber(this, userHexEncodedPublicKey) - TextSecurePreferences.setRestorationTime(this, 0) - TextSecurePreferences.setHasViewedSeed(this, false) - val intent = Intent(this, DisplayNameActivity::class.java) - push(intent) - } - - private fun copyPublicKey() { - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Session ID", x25519KeyPair!!.hexEncodedPublicKey) - clipboard.setPrimaryClip(clip) - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - - private fun openURL(url: String) { - try { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - startActivity(intent) - } catch (e: Exception) { - Toast.makeText(this, R.string.invalid_url, Toast.LENGTH_SHORT).show() - } - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt deleted file mode 100644 index 0eab58fa0..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedActivity.kt +++ /dev/null @@ -1,95 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.style.ForegroundColorSpan -import android.widget.LinearLayout -import android.widget.Toast -import network.loki.messenger.R -import network.loki.messenger.databinding.ActivitySeedBinding -import org.session.libsession.utilities.TextSecurePreferences -import org.session.libsession.utilities.getColorFromAttr -import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.utilities.hexEncodedPrivateKey -import org.thoughtcrime.securesms.BaseActionBarActivity -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.crypto.MnemonicUtilities -import org.thoughtcrime.securesms.util.getAccentColor - -class SeedActivity : BaseActionBarActivity() { - - private lateinit var binding: ActivitySeedBinding - - private val seed by lazy { - var hexEncodedSeed = IdentityKeyUtil.retrieve(this, IdentityKeyUtil.LOKI_SEED) - if (hexEncodedSeed == null) { - hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account - } - val loadFileContents: (String) -> String = { fileName -> - MnemonicUtilities.loadFileContents(this, fileName) - } - MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english) - } - - // region Lifecycle - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - binding = ActivitySeedBinding.inflate(layoutInflater) - setContentView(binding.root) - supportActionBar!!.title = resources.getString(R.string.activity_seed_title) - val seedReminderViewTitle = SpannableString("You're almost finished! 90%") // Intentionally not yet translated - seedReminderViewTitle.setSpan(ForegroundColorSpan(getAccentColor()), 24, 27, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - with(binding) { - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_2) - seedReminderView.setProgress(90, false) - seedReminderView.hideContinueButton() - var redactedSeed = seed - var index = 0 - for (character in seed) { - if (character.isLetter()) { - redactedSeed = redactedSeed.replaceRange(index, index + 1, "▆") - } - index += 1 - } - seedTextView.setTextColor(getAccentColor()) - seedTextView.text = redactedSeed - seedTextView.setOnLongClickListener { revealSeed(); true } - revealButton.setOnLongClickListener { revealSeed(); true } - copyButton.setOnClickListener { copySeed() } - } - } - // endregion - - // region Updating - private fun revealSeed() { - val seedReminderViewTitle = SpannableString("Account secured! 100%") // Intentionally not yet translated - seedReminderViewTitle.setSpan(ForegroundColorSpan(getAccentColor()), 17, 21, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - with(binding) { - seedReminderView.title = seedReminderViewTitle - seedReminderView.subtitle = resources.getString(R.string.view_seed_reminder_subtitle_3) - seedReminderView.setProgress(100, true) - val seedTextViewLayoutParams = seedTextView.layoutParams as LinearLayout.LayoutParams - seedTextViewLayoutParams.height = seedTextView.height - seedTextView.layoutParams = seedTextViewLayoutParams - seedTextView.setTextColor(getColorFromAttr(android.R.attr.textColorPrimary)) - seedTextView.text = seed - } - TextSecurePreferences.setHasViewedSeed(this, true) - } - // endregion - - // region Interaction - private fun copySeed() { - revealSeed() - val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Seed", seed) - clipboard.setPrimaryClip(clip) - Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - } - // endregion -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt deleted file mode 100644 index 28611985f..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/SeedReminderView.kt +++ /dev/null @@ -1,59 +0,0 @@ -package org.thoughtcrime.securesms.onboarding - -import android.content.Context -import android.os.Build -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import network.loki.messenger.databinding.ViewSeedReminderBinding - -class SeedReminderView : FrameLayout { - private lateinit var binding: ViewSeedReminderBinding - - var title: CharSequence - get() = binding.titleTextView.text - set(value) { binding.titleTextView.text = value } - var subtitle: CharSequence - get() = binding.subtitleTextView.text - set(value) { binding.subtitleTextView.text = value } - var delegate: SeedReminderViewDelegate? = null - - constructor(context: Context) : super(context) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) { - setUpViewHierarchy() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int, defStyleRes: Int) : super(context, attrs, defStyleAttr, defStyleRes) { - setUpViewHierarchy() - } - - private fun setUpViewHierarchy() { - binding = ViewSeedReminderBinding.inflate(LayoutInflater.from(context), this, true) - binding.button.setOnClickListener { delegate?.handleSeedReminderViewContinueButtonTapped() } - } - - fun setProgress(progress: Int, isAnimated: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.progressBar.setProgress(progress, isAnimated) - } else { - binding.progressBar.progress = progress - } - } - - fun hideContinueButton() { - binding.button.visibility = View.GONE - } -} - -interface SeedReminderViewDelegate { - - fun handleSeedReminderViewContinueButtonTapped() -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt new file mode 100644 index 000000000..42fc83f05 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsActivity.kt @@ -0,0 +1,168 @@ +package org.thoughtcrime.securesms.onboarding.messagenotifications + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.compose.foundation.BorderStroke +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.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +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 dagger.hilt.android.AndroidEntryPoint +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.home.HomeActivity +import org.thoughtcrime.securesms.notifications.PushRegistry +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider +import org.thoughtcrime.securesms.ui.h8 +import org.thoughtcrime.securesms.ui.h9 +import org.thoughtcrime.securesms.ui.session_accent +import org.thoughtcrime.securesms.ui.small +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject + +@AndroidEntryPoint +class MessageNotificationsActivity : BaseActionBarActivity() { + + @Inject lateinit var pushRegistry: PushRegistry + + private val viewModel: MessageNotificationsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpActionBarSessionLogo(true) + TextSecurePreferences.setHasSeenWelcomeScreen(this, true) + + ComposeView(this) + .apply { setContent { MessageNotifications() } } + .let(::setContentView) + } + + @Composable + private fun MessageNotifications() { + val state by viewModel.stateFlow.collectAsState() + + AppTheme { + MessageNotifications(state, viewModel::setEnabled, ::register) + } + } + + private fun register() { + TextSecurePreferences.setPushEnabled(this, viewModel.stateFlow.value.pushEnabled) + ApplicationContext.getInstance(this).startPollingIfNeeded() + pushRegistry.refresh(true) + Intent(this, HomeActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + putExtra(HomeActivity.FROM_ONBOARDING, true) + }.also(::startActivity) + } +} + +@Preview +@Composable +fun MessageNotificationsPreview( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + MessageNotifications() + } +} + +@Composable +fun MessageNotifications( + state: MessageNotificationsState = MessageNotificationsState(), + setEnabled: (Boolean) -> Unit = {}, + onContinue: () -> Unit = {} +) { + Column(Modifier.padding(horizontal = 32.dp)) { + Spacer(Modifier.weight(1f)) + Text("Message notifications", style = MaterialTheme.typography.h4) + Spacer(Modifier.height(16.dp)) + Text("There are two ways Session can notify you of new messages.") + Spacer(Modifier.height(16.dp)) + NotificationRadioButton( + R.string.activity_pn_mode_fast_mode, + R.string.activity_pn_mode_fast_mode_explanation, + R.string.activity_pn_mode_recommended_option_tag, + selected = state.pushEnabled, + onClick = { setEnabled(true) } + ) + Spacer(Modifier.height(16.dp)) + NotificationRadioButton( + R.string.activity_pn_mode_slow_mode, + R.string.activity_pn_mode_slow_mode_explanation, + selected = state.pushDisabled, + onClick = { setEnabled(false) } + ) + Spacer(Modifier.weight(1f)) + OutlineButton( + stringResource(R.string.continue_2), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(262.dp), + onClick = onContinue + ) + Spacer(modifier = Modifier.height(12.dp)) + } +} + +@Composable +fun NotificationRadioButton( + @StringRes title: Int, + @StringRes explanation: Int, + @StringRes tag: Int? = null, + selected: Boolean = false, + onClick: () -> Unit = {} +) { + Row { + OutlinedButton( + onClick = onClick, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors(backgroundColor = MaterialTheme.colors.background, contentColor = Color.White), + border = if (selected) BorderStroke(ButtonDefaults.OutlinedBorderSize, session_accent) else ButtonDefaults.outlinedBorder, + shape = RoundedCornerShape(8.dp) + ) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + Text(stringResource(title), style = MaterialTheme.typography.h8) + Text(stringResource(explanation), style = MaterialTheme.typography.small) + tag?.let { Text(stringResource(it), color = session_accent, style = MaterialTheme.typography.h9) } + } + } + RadioButton(selected = selected, modifier = Modifier.align(Alignment.CenterVertically), onClick = onClick) + } +} + +fun Context.startPNModeActivity(flags: Int = 0) { + Intent(this, MessageNotificationsActivity::class.java) + .also { it.flags = flags } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt new file mode 100644 index 000000000..f913e0444 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.onboarding.messagenotifications + +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import javax.inject.Inject + +@HiltViewModel +class MessageNotificationsViewModel @Inject constructor(): ViewModel() { + private val state = MutableStateFlow(MessageNotificationsState()) + val stateFlow = state.asStateFlow() + + fun setEnabled(enabled: Boolean) { + state.update { MessageNotificationsState(pushEnabled = enabled) } + } +} + +data class MessageNotificationsState(val pushEnabled: Boolean = true) { + val pushDisabled get() = !pushEnabled +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt new file mode 100644 index 000000000..729194a5d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameActivity.kt @@ -0,0 +1,150 @@ +package org.thoughtcrime.securesms.onboarding.pickname + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.ContentAlpha +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.onboarding.messagenotifications.startPNModeActivity +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +import org.thoughtcrime.securesms.ui.base +import org.thoughtcrime.securesms.ui.baseBold +import org.thoughtcrime.securesms.ui.colorDestructive +import org.thoughtcrime.securesms.util.setUpActionBarSessionLogo +import javax.inject.Inject + +private const val EXTRA_PICK_NEW_NAME = "extra_pick_new_name" + +@AndroidEntryPoint +class PickDisplayNameActivity : BaseActionBarActivity() { + + @Inject + lateinit var viewModelFactory: PickDisplayNameViewModel.AssistedFactory + + private val viewModel: PickDisplayNameViewModel by viewModels { + val pickNewName = intent.getBooleanExtra(EXTRA_PICK_NEW_NAME, false) + viewModelFactory.create(pickNewName) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setUpActionBarSessionLogo() + + ComposeView(this) + .apply { setContent { DisplayNameScreen(viewModel) } } + .let(::setContentView) + + lifecycleScope.launch { + viewModel.eventFlow.collect { + startPNModeActivity() + } + } + } + + @Composable + private fun DisplayNameScreen(viewModel: PickDisplayNameViewModel) { + val state = viewModel.stateFlow.collectAsState() + + AppTheme { + DisplayName(state.value, viewModel::onChange) { viewModel.onContinue(this) } + } + } + + @Preview + @Composable + fun PreviewDisplayName() { + PreviewTheme(R.style.Classic_Dark) { + DisplayName(State()) + } + } + + @Composable + fun DisplayName(state: State, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) { + Column( + verticalArrangement = Arrangement.spacedBy(20.dp), + modifier = Modifier + .padding(horizontal = 50.dp) + .padding(bottom = 12.dp) + ) { + Spacer(modifier = Modifier.weight(1f)) + Text(stringResource(state.title), style = MaterialTheme.typography.h4) + Text( + stringResource(state.description), + style = MaterialTheme.typography.base, + modifier = Modifier.padding(bottom = 12.dp)) + + OutlinedTextField( + value = state.displayName, + onValueChange = { onChange(it) }, + placeholder = { Text(stringResource(R.string.activity_display_name_edit_text_hint)) }, + colors = TextFieldDefaults.outlinedTextFieldColors( + textColor = state.error?.let { colorDestructive } ?: + LocalContentColor.current.copy(LocalContentAlpha.current), + focusedBorderColor = Color(0xff414141), + unfocusedBorderColor = Color(0xff414141), + cursorColor = LocalContentColor.current, + placeholderColor = state.error?.let { colorDestructive } + ?: MaterialTheme.colors.onSurface.copy(ContentAlpha.medium) + ), + singleLine = true, + keyboardActions = KeyboardActions( + onDone = { onContinue() }, + onGo = { onContinue() }, + onSearch = { onContinue() }, + onSend = { onContinue() }, + ), + isError = state.error != null, + shape = RoundedCornerShape(12.dp) + ) + + state.error?.let { + Text(stringResource(it), style = MaterialTheme.typography.baseBold, color = MaterialTheme.colors.error) + } + + Spacer(modifier = Modifier.weight(2f)) + + OutlineButton( + stringResource(R.string.continue_2), + modifier = Modifier + .align(Alignment.CenterHorizontally) + .width(262.dp) + ) { onContinue() } + } + } +} + +fun Context.startPickDisplayNameActivity(failedToLoad: Boolean = false, flags: Int = 0) { + Intent(this, PickDisplayNameActivity::class.java) + .apply { putExtra(EXTRA_PICK_NEW_NAME, failedToLoad) } + .also { it.flags = flags } + .also(::startActivity) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt new file mode 100644 index 000000000..9ede264ab --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/pickname/PickDisplayNameViewModel.kt @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.onboarding.pickname + +import android.content.Context +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.session.libsession.snode.SnodeModule +import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol.Companion.NAME_PADDED_LENGTH +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.database.LokiAPIDatabaseProtocol +import org.session.libsignal.utilities.KeyHelper +import org.session.libsignal.utilities.hexEncodedPublicKey +import org.thoughtcrime.securesms.crypto.KeyPairUtilities +import org.thoughtcrime.securesms.dependencies.ConfigFactory + +class PickDisplayNameViewModel( + pickNewName: Boolean, + private val prefs: TextSecurePreferences, + private val configFactory: ConfigFactory +): ViewModel() { + private val state = MutableStateFlow(if (pickNewName) pickNewNameState() else State()) + val stateFlow = state.asStateFlow() + + private val event = Channel() + val eventFlow = event.receiveAsFlow() + + private val database: LokiAPIDatabaseProtocol + get() = SnodeModule.shared.storage + + fun onContinue(context: Context) { + state.update { it.copy(displayName = it.displayName.trim()) } + + val displayName = state.value.displayName + + val keyPairGenerationResult = KeyPairUtilities.generate() + val seed = keyPairGenerationResult.seed + val ed25519KeyPair = keyPairGenerationResult.ed25519KeyPair + val x25519KeyPair = keyPairGenerationResult.x25519KeyPair + + when { + displayName.isEmpty() -> { state.update { it.copy(error = R.string.activity_display_name_display_name_missing_error) } } + displayName.length > NAME_PADDED_LENGTH -> { state.update { it.copy(error = R.string.activity_display_name_display_name_too_long_error) } } + else -> { + prefs.setProfileName(displayName) + + // This is here to resolve a case where the app restarts before a user completes onboarding + // which can result in an invalid database state + database.clearAllLastMessageHashes() + database.clearReceivedMessageHashValues() + + KeyPairUtilities.store(context, seed, ed25519KeyPair, x25519KeyPair) + configFactory.keyPairChanged() + val userHexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey + val registrationID = KeyHelper.generateRegistrationId(false) + prefs.setLocalRegistrationId(registrationID) + prefs.setLocalNumber(userHexEncodedPublicKey) + prefs.setRestorationTime(0) + prefs.setHasViewedSeed(false) + + viewModelScope.launch { event.send(Event.DONE) } + } + } + } + + fun onChange(value: String) { + state.update { state -> state.copy( + displayName = value, + error = value.takeIf { it.length > NAME_PADDED_LENGTH }?.let { R.string.activity_display_name_display_name_too_long_error } + ) + } + } + + @dagger.assisted.AssistedFactory + interface AssistedFactory { + fun create(pickNewName: Boolean): Factory + } + + @Suppress("UNCHECKED_CAST") + class Factory @AssistedInject constructor( + @Assisted private val pickNewName: Boolean, + private val prefs: TextSecurePreferences, + private val configFactory: ConfigFactory + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return PickDisplayNameViewModel(pickNewName, prefs, configFactory) as T + } + } +} + +data class State( + @StringRes val title: Int = R.string.activity_display_name_title_2, + @StringRes val description: Int = R.string.activity_display_name_explanation, + @StringRes val error: Int? = null, + val displayName: String = "" +) + +fun pickNewNameState() = State( + title = R.string.activity_display_name_pick_a_new_display_name, + description = R.string.activity_display_name_unable_to_load_new_name_explanation +) + +sealed interface Event { + object DONE: Event +} 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 new file mode 100644 index 000000000..d35e9722b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordActivity.kt @@ -0,0 +1,244 @@ +package org.thoughtcrime.securesms.onboarding.recoverypassword + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import android.widget.Toast +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 +import androidx.compose.runtime.MutableState +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.Color +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 +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import network.loki.messenger.R +import org.session.libsession.utilities.TextSecurePreferences +import org.thoughtcrime.securesms.BaseActionBarActivity +import org.thoughtcrime.securesms.showSessionDialog +import org.thoughtcrime.securesms.ui.AppTheme +import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin +import org.thoughtcrime.securesms.ui.LocalExtraColors +import org.thoughtcrime.securesms.ui.OutlineButton +import org.thoughtcrime.securesms.ui.PreviewTheme +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.h8 +import org.thoughtcrime.securesms.ui.small + +class RecoveryPasswordActivity : BaseActionBarActivity() { + + private val viewModel: RecoveryPasswordViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + supportActionBar!!.title = resources.getString(R.string.activity_recovery_password) + + ComposeView(this).apply { + setContent { + RecoveryPassword(viewModel.seed, viewModel.qrBitmap, { copySeed() }) { onHide() } + } + }.let(::setContentView) + } + + private fun revealSeed() { + TextSecurePreferences.setHasViewedSeed(this, true) + } + + private fun copySeed() { + revealSeed() + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("Seed", viewModel.seed) + clipboard.setPrimaryClip(clip) + Toast.makeText(this, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } + + private fun onHide() { + showSessionDialog { + title("Hide Recovery Password Permanently") + text("Without your recovery password, you cannot load your account on new devices.\n" + + "\n" + + "We strongly recommend you save your recovery password in a safe and secure place before continuing.") + destructiveButton(R.string.continue_2) { onHideConfirm() } + button(R.string.cancel) {} + } + } + + private fun onHideConfirm() { + showSessionDialog { + title("Hide Recovery Password Permanently") + text("Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.") + button(R.string.cancel) {} + destructiveButton(R.string.yes) { + viewModel.permanentlyHidePassword() + finish() + } + } + } +} + +@Preview +@Composable +fun PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + RecoveryPassword(seed = "Voyage urban toyed maverick peculiar tuxedo penguin tree grass building listen speak withdraw terminal plane") + } +} + +@Composable +fun RecoveryPassword( + seed: String = "", + qrBitmap: Bitmap? = null, + copySeed:() -> Unit = {}, + onHide:() -> Unit = {} +) { + AppTheme { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier.verticalScroll(rememberScrollState()) + .padding(bottom = 16.dp) + ) { + RecoveryPasswordCell(seed, qrBitmap, copySeed) + HideRecoveryPasswordCell(onHide) + } + } +} + +@Composable +fun RecoveryPasswordCell(seed: String = "", qrBitmap: Bitmap? = null, copySeed:() -> Unit = {}) { + val showQr = remember { + mutableStateOf(false) + } + + CellWithPaddingAndMargin { + Column { + Row { + Text("Recovery Password") + Spacer(Modifier.width(8.dp)) + SessionShieldIcon() + } + + Text("Use your recovery password to load your account on new devices.\n\nYour account cannot be recovered without your recovery password. Make sure it's stored somewhere safe and secure — and don't share it with anyone.") + + AnimatedVisibility(!showQr.value) { + Text( + seed, + modifier = Modifier + .padding(vertical = 24.dp) + .border( + width = 1.dp, + color = classicDarkColors[3], + shape = RoundedCornerShape(11.dp) + ) + .padding(24.dp), + style = MaterialTheme.typography.small.copy(fontFamily = FontFamily.Monospace), + color = LocalExtraColors.current.prominentButtonColor, + ) + } + + 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) + ) { + Box { + qrBitmap?.let { + Image( + bitmap = it.asImageBitmap(), + contentDescription = "QR code of your recovery password", + colorFilter = ColorFilter.tint(LocalExtraColors.current.onLightCell) + ) + } + + Icon( + painter = painterResource(id = R.drawable.session_shield), + 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) + ) + } + } + } + + AnimatedVisibility(!showQr.value) { + Row(horizontalArrangement = Arrangement.spacedBy(32.dp)) { + OutlineButton(text = stringResource(R.string.copy), modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { copySeed() } + OutlineButton(text = "View QR", modifier = Modifier.weight(1f), color = MaterialTheme.colors.onPrimary) { showQr.toggle() } + } + } + + AnimatedVisibility(showQr.value, modifier = Modifier.align(Alignment.CenterHorizontally)) { + OutlineButton( + text = "View Password", + color = MaterialTheme.colors.onPrimary, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { showQr.toggle() } + } + } + } +} + +private fun MutableState.toggle() { value = !value } + +@Composable +fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) { + CellWithPaddingAndMargin { + Row { + Column(Modifier.weight(1f)) { + Text(text = "Hide Recovery Password", style = MaterialTheme.typography.h8) + Text(text = "Permanently hide your recovery password on this device.") + } + OutlineButton( + "Hide", + modifier = Modifier.align(Alignment.CenterVertically), + color = colorDestructive + ) { onHide() } + } + } +} + +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 new file mode 100644 index 000000000..9b7ff3227 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/recoverypassword/RecoveryPasswordViewModel.kt @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.onboarding.recoverypassword + +import android.app.Application +import android.graphics.Bitmap +import androidx.lifecycle.AndroidViewModel +import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback +import dagger.hilt.android.lifecycle.HiltViewModel +import org.session.libsession.utilities.AppTextSecurePreferences +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.crypto.MnemonicCodec +import org.session.libsignal.utilities.hexEncodedPrivateKey +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.MnemonicUtilities +import org.thoughtcrime.securesms.util.QRCodeUtilities +import org.thoughtcrime.securesms.util.toPx +import javax.inject.Inject + +@HiltViewModel +class RecoveryPasswordViewModel @Inject constructor( + private val application: Application +): AndroidViewModel(application) { + + val prefs = AppTextSecurePreferences(application) + + fun permanentlyHidePassword() { + prefs.setHidePassword(true) + } + + val seed by lazy { + val hexEncodedSeed = IdentityKeyUtil.retrieve(application, IdentityKeyUtil.LOKI_SEED) + ?: IdentityKeyUtil.getIdentityKeyPair(application).hexEncodedPrivateKey // Legacy account + 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/SeedDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt deleted file mode 100644 index bae5f1960..000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SeedDialog.kt +++ /dev/null @@ -1,41 +0,0 @@ -package org.thoughtcrime.securesms.preferences - -import android.app.Dialog -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.os.Bundle -import android.widget.Toast -import androidx.fragment.app.DialogFragment -import network.loki.messenger.R -import org.session.libsignal.crypto.MnemonicCodec -import org.session.libsignal.utilities.hexEncodedPrivateKey -import org.thoughtcrime.securesms.createSessionDialog -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil -import org.thoughtcrime.securesms.crypto.MnemonicUtilities - -class SeedDialog: DialogFragment() { - private val seed by lazy { - val hexEncodedSeed = IdentityKeyUtil.retrieve(requireContext(), IdentityKeyUtil.LOKI_SEED) - ?: IdentityKeyUtil.getIdentityKeyPair(requireContext()).hexEncodedPrivateKey // Legacy account - - MnemonicCodec { fileName -> MnemonicUtilities.loadFileContents(requireContext(), fileName) } - .encode(hexEncodedSeed, MnemonicCodec.Language.Configuration.english) - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = createSessionDialog { - title(R.string.dialog_seed_title) - text(R.string.dialog_seed_explanation) - text(seed, R.style.SessionIDTextView) - button(R.string.copy, R.string.AccessibilityId_copy_recovery_phrase) { copySeed() } - button(R.string.close) { dismiss() } - } - - private fun copySeed() { - val clipboard = requireActivity().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText("Seed", seed) - clipboard.setPrimaryClip(clip) - Toast.makeText(requireContext(), R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() - dismiss() - } -} 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 5f2485576..fbecc18a6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -19,6 +19,7 @@ import android.view.MenuItem import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.Toast +import androidx.core.view.isGone import androidx.core.view.isVisible import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.BuildConfig @@ -44,6 +45,7 @@ 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.permissions.Permissions import org.thoughtcrime.securesms.preferences.appearance.AppearanceSettingsActivity import org.thoughtcrime.securesms.profiles.ProfileMediaConstraints @@ -63,6 +65,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { @Inject lateinit var configFactory: ConfigFactory + @Inject + lateinit var prefs: TextSecurePreferences + + private lateinit var binding: ActivitySettingsBinding private var displayNameEditActionMode: ActionMode? = null @@ -85,13 +91,17 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { super.onCreate(savedInstanceState, isReady) binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) - val displayName = getDisplayName() glide = GlideApp.with(this) - with(binding) { + } + + override fun onStart() { + super.onStart() + + binding.run { setupProfilePictureView(profilePictureView) profilePictureView.setOnClickListener { showEditProfilePictureUI() } ctnGroupNameSection.setOnClickListener { startActionMode(DisplayNameEditActionModeCallback()) } - btnGroupNameDisplay.text = displayName + btnGroupNameDisplay.text = getDisplayName() publicKeyTextView.text = hexEncodedPublicKey copyButton.setOnClickListener { copyPublicKey() } shareButton.setOnClickListener { sharePublicKey() } @@ -104,7 +114,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { appearanceButton.setOnClickListener { showAppearanceSettings() } inviteFriendButton.setOnClickListener { sendInvitation() } helpButton.setOnClickListener { showHelp() } - seedButton.setOnClickListener { showSeed() } + 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})") } @@ -383,8 +395,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { show(intent) } - private fun showSeed() { - SeedDialog().show(supportFragmentManager, "Recovery Phrase Dialog") + private fun showPassword() { + startRecoveryPasswordActivity() } private fun clearAllData() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt index 55bc1be62..ca7d3de7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Colors.kt @@ -1,10 +1,21 @@ package org.thoughtcrime.securesms.ui +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card import androidx.compose.material.Colors import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.primarySurface import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp val colorDestructive = Color(0xffFF453A) @@ -42,6 +53,7 @@ const val oceanLight5 = 0xffE7F3F4 const val oceanLight6 = 0xffECFAFB const val oceanLight7 = 0xffFCFFFF +val session_accent = Color(0xFF31F196) val ocean_accent = Color(0xff57C9FA) val oceanLights = arrayOf(oceanLight0, oceanLight1, oceanLight2, oceanLight3, oceanLight4, oceanLight5, oceanLight6, oceanLight7) @@ -61,3 +73,45 @@ fun transparentButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Co @Composable fun destructiveButtonColors() = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = colorDestructive) + +@Preview +@Composable +fun Context.PreviewMessageDetails( + @PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int +) { + PreviewTheme(themeResId) { + Colors() + } +} + +@Composable +private fun Colors() { + AppTheme { + Column { + Box(Modifier.background(MaterialTheme.colors.primary)) { + Text("primary") + } + Box(Modifier.background(MaterialTheme.colors.primaryVariant)) { + Text("primaryVariant") + } + Box(Modifier.background(MaterialTheme.colors.secondary)) { + Text("secondary") + } + Box(Modifier.background(MaterialTheme.colors.secondaryVariant)) { + Text("secondaryVariant") + } + Box(Modifier.background(MaterialTheme.colors.surface)) { + Text("surface") + } + Box(Modifier.background(MaterialTheme.colors.primarySurface)) { + Text("primarySurface") + } + Box(Modifier.background(MaterialTheme.colors.background)) { + Text("background") + } + Box(Modifier.background(MaterialTheme.colors.error)) { + Text("error") + } + } + } +} 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 1724bde8a..27370e048 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -1,6 +1,8 @@ package org.thoughtcrime.securesms.ui import androidx.annotation.DrawableRes +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.Canvas import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -10,26 +12,35 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ButtonColors +import androidx.compose.material.ButtonDefaults import androidx.compose.material.Card import androidx.compose.material.Colors import androidx.compose.material.Icon import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedButton import androidx.compose.material.Text import androidx.compose.material.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.google.accompanist.pager.HorizontalPagerIndicator @@ -37,6 +48,69 @@ import kotlinx.coroutines.launch import network.loki.messenger.R import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.components.ProfilePictureView +import kotlin.math.roundToInt + +@Composable +fun OutlineButton( + text: String, + modifier: Modifier = Modifier, + color: Color = LocalExtraColors.current.prominentButtonColor, + onClick: () -> Unit +) { + OutlinedButton( + modifier = modifier, + onClick = onClick, + border = BorderStroke(1.dp, color), + shape = RoundedCornerShape(50), // = 50% percent + colors = ButtonDefaults.outlinedButtonColors( + contentColor = color, + backgroundColor = Color.Unspecified + ) + ) { + Text(text = text) + } +} + +@Composable +fun FilledButton(text: String, modifier: Modifier = Modifier, onClick: () -> Unit) { + OutlinedButton( + modifier = modifier.size(108.dp, 34.dp), + onClick = onClick, + shape = RoundedCornerShape(50), // = 50% percent + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colors.background, + backgroundColor = LocalExtraColors.current.prominentButtonColor + ) + ) { + Text(text = text) + } +} + +@Composable +fun BorderlessButton( + text: String, + modifier: Modifier = Modifier, + fontSize: TextUnit = TextUnit.Unspecified, + lineHeight: TextUnit = TextUnit.Unspecified, + onClick: () -> Unit) { + TextButton( + onClick = onClick, + modifier = modifier, + shape = RoundedCornerShape(50), // = 50% percent + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colors.onBackground, + backgroundColor = MaterialTheme.colors.background + ) + ) { + Text( + text = text, + textAlign = TextAlign.Center, + fontSize = fontSize, + lineHeight = lineHeight, + modifier = Modifier.padding(horizontal = 2.dp) + ) + } +} @Composable fun ItemButton( @@ -180,3 +254,59 @@ fun RowScope.Avatar(recipient: Recipient) { ) } } + +@Composable +fun ProgressArc(progress: Float, modifier: Modifier = Modifier) { + val text = (progress * 100).roundToInt() + + Box(modifier = modifier) { + Arc(percentage = progress, modifier = Modifier.align(Alignment.Center)) + Text("${text}%", color = Color.White, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.h2) + } +} + +@Composable +fun Arc( + modifier: Modifier = Modifier, + percentage: Float = 0.25f, + fillColor: Color = session_accent, + backgroundColor: Color = classicDarkColors[3], + strokeWidth: Dp = 18.dp, + sweepAngle: Float = 310f, + startAngle: Float = (360f - sweepAngle) / 2 + 90f +) { + Canvas( + modifier = modifier + .padding(strokeWidth) + .size(186.dp) + ) { + // Background Line + drawArc( + color = backgroundColor, + startAngle, + sweepAngle, + false, + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + size = Size(size.width, size.height) + ) + + drawArc( + color = fillColor, + startAngle, + percentage * sweepAngle, + false, + style = Stroke(strokeWidth.toPx(), cap = StrokeCap.Round), + size = Size(size.width, size.height) + ) + } +} + +@Composable +fun RowScope.SessionShieldIcon() { + Icon( + painter = painterResource(R.drawable.session_shield), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterVertically) + .wrapContentSize(unbounded = true) + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt index 64bbd21d8..469b11eee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Themes.kt @@ -5,15 +5,23 @@ import androidx.annotation.AttrRes import androidx.appcompat.view.ContextThemeWrapper import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.material.LocalContentColor import androidx.compose.material.MaterialTheme +import androidx.compose.material.Shapes +import androidx.compose.material.Typography import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import com.google.accompanist.themeadapter.appcompat.AppCompatTheme +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import com.google.accompanist.themeadapter.appcompat.createAppCompatTheme import com.google.android.material.color.MaterialColors import network.loki.messenger.R @@ -22,6 +30,9 @@ val LocalExtraColors = staticCompositionLocalOf { error("No Custom data class ExtraColors( val settingsBackground: Color, + val prominentButtonColor: Color, + val lightCell: Color, + val onLightCell: Color, ) /** @@ -31,19 +42,85 @@ data class ExtraColors( fun AppTheme( content: @Composable () -> Unit ) { - val extraColors = LocalContext.current.run { + val context = LocalContext.current + + val extraColors = context.run { ExtraColors( settingsBackground = getColorFromTheme(R.attr.colorSettingsBackground), + prominentButtonColor = getColorFromTheme(R.attr.prominentButtonColor), + lightCell = getColorFromTheme(R.attr.lightCell), + onLightCell = getColorFromTheme(R.attr.onLightCell), ) } + val surface = context.getColorFromTheme(R.attr.colorSettingsBackground) + CompositionLocalProvider(LocalExtraColors provides extraColors) { - AppCompatTheme { + AppCompatTheme(surface = surface) { content() } } } +@Composable +fun AppCompatTheme( + context: Context = LocalContext.current, + readColors: Boolean = true, + typography: Typography = sessionTypography, + shapes: Shapes = MaterialTheme.shapes, + surface: Color? = null, + content: @Composable () -> Unit +) { + val themeParams = remember(context.theme) { + context.createAppCompatTheme( + readColors = readColors, + readTypography = false + ) + } + + val colors = themeParams.colors ?: MaterialTheme.colors + + MaterialTheme( + colors = colors.copy( + surface = surface ?: colors.surface + ), + typography = typography, + shapes = shapes, + ) { + // We update the LocalContentColor to match our onBackground. This allows the default + // content color to be more appropriate to the theme background + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + content = content + ) + } +} + +fun boldStyle(size: TextUnit) = TextStyle.Default.copy( + fontWeight = FontWeight.Bold, + fontSize = size +) + +fun defaultStyle(size: TextUnit) = TextStyle.Default.copy(fontSize = size) + +val sessionTypography = Typography( + h1 = boldStyle(36.sp), + h2 = boldStyle(32.sp), + h3 = boldStyle(29.sp), + h4 = boldStyle(26.sp), + h5 = boldStyle(23.sp), + h6 = boldStyle(20.sp), +) + +val Typography.base get() = defaultStyle(14.sp) +val Typography.baseBold get() = boldStyle(14.sp) +val Typography.small get() = defaultStyle(12.sp) +val Typography.extraSmall get() = defaultStyle(11.sp) + +val Typography.h7 get() = boldStyle(18.sp) +val Typography.h8 get() = boldStyle(16.sp) +val Typography.h9 get() = boldStyle(14.sp) + fun Context.getColorFromTheme(@AttrRes attr: Int, defaultValue: Int = 0x0): Color = MaterialColors.getColor(this, attr, defaultValue).let(::Color) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt index f7d1e3e8a..d70bb3be8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/QRCodeUtilities.kt @@ -9,17 +9,26 @@ import com.google.zxing.qrcode.QRCodeWriter object QRCodeUtilities { - fun encode(data: String, size: Int, isInverted: Boolean = false, hasTransparentBackground: Boolean = true): Bitmap { + fun encode( + data: String, + size: Int, + isInverted: Boolean = false, + hasTransparentBackground: Boolean = true, + dark: Int = Color.BLACK, + light: Int = Color.WHITE, + ): Bitmap { try { val hints = hashMapOf( EncodeHintType.MARGIN to 1 ) val result = QRCodeWriter().encode(data, BarcodeFormat.QR_CODE, size, size, hints) val bitmap = Bitmap.createBitmap(result.width, result.height, Bitmap.Config.ARGB_8888) + val color = if (isInverted) light else dark + val background = if (isInverted) dark else light for (y in 0 until result.height) { for (x in 0 until result.width) { if (result.get(x, y)) { - bitmap.setPixel(x, y, if (isInverted) Color.WHITE else Color.BLACK) + bitmap.setPixel(x, y, color) } else if (!hasTransparentBackground) { - bitmap.setPixel(x, y, if (isInverted) Color.BLACK else Color.WHITE) + bitmap.setPixel(x, y, background) } } } diff --git a/app/src/main/res/drawable/emoji_tada.xml b/app/src/main/res/drawable/emoji_tada.xml new file mode 100644 index 000000000..ce0f30067 --- /dev/null +++ b/app/src/main/res/drawable/emoji_tada.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/session_logo.xml b/app/src/main/res/drawable/session_logo.xml index f88a4f21a..b2f931990 100644 --- a/app/src/main/res/drawable/session_logo.xml +++ b/app/src/main/res/drawable/session_logo.xml @@ -1,9 +1,9 @@ - - - - - + + diff --git a/app/src/main/res/drawable/session_shield.xml b/app/src/main/res/drawable/session_shield.xml new file mode 100644 index 000000000..a7c6d1a24 --- /dev/null +++ b/app/src/main/res/drawable/session_shield.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout-sw400dp/activity_landing.xml b/app/src/main/res/layout-sw400dp/activity_landing.xml deleted file mode 100644 index 5e5a36704..000000000 --- a/app/src/main/res/layout-sw400dp/activity_landing.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - -