mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 18:15:22 +00:00
Update New Message Screen
This commit is contained in:
parent
e25b90b229
commit
f89e4705d5
@ -360,23 +360,23 @@ dependencies {
|
|||||||
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
testImplementation 'org.robolectric:shadows-multidex:4.4'
|
||||||
|
|
||||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||||
implementation 'androidx.compose.ui:ui:1.6.2'
|
implementation "androidx.compose.ui:ui:$composeVersion"
|
||||||
implementation 'androidx.compose.animation:animation:1.6.2'
|
implementation "androidx.compose.animation:animation:$composeVersion"
|
||||||
implementation 'androidx.compose.ui:ui-tooling:1.6.2'
|
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
|
||||||
implementation "androidx.compose.runtime:runtime-livedata:1.6.2"
|
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
|
||||||
implementation 'androidx.compose.foundation:foundation-layout:1.6.2'
|
implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
|
||||||
implementation 'androidx.compose.material:material:1.6.2'
|
implementation "androidx.compose.material:material:$composeVersion"
|
||||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4-android:1.6.2'
|
androidTestImplementation "androidx.compose.ui:ui-test-junit4-android:$composeVersion"
|
||||||
debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.2'
|
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
|
||||||
|
|
||||||
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.33.1-alpha"
|
||||||
implementation "com.google.accompanist:accompanist-pager:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-pager:0.33.1-alpha"
|
||||||
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-pager-indicators:0.33.1-alpha"
|
||||||
implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"
|
implementation "com.google.accompanist:accompanist-permissions:0.33.1-alpha"
|
||||||
|
|
||||||
implementation "androidx.camera:camera-camera2:1.3.1"
|
implementation "androidx.camera:camera-camera2:1.3.2"
|
||||||
implementation "androidx.camera:camera-lifecycle:1.3.1"
|
implementation "androidx.camera:camera-lifecycle:1.3.2"
|
||||||
implementation "androidx.camera:camera-view:1.3.1"
|
implementation "androidx.camera:camera-view:1.3.2"
|
||||||
|
|
||||||
implementation 'com.google.firebase:firebase-core:21.1.1'
|
implementation 'com.google.firebase:firebase-core:21.1.1'
|
||||||
implementation "com.google.mlkit:barcode-scanning:17.2.0"
|
implementation "com.google.mlkit:barcode-scanning:17.2.0"
|
||||||
|
@ -0,0 +1,7 @@
|
|||||||
|
package org.thoughtcrime.securesms.dms
|
||||||
|
|
||||||
|
interface Callbacks {
|
||||||
|
fun onChange(value: String) {}
|
||||||
|
fun onContinue() {}
|
||||||
|
fun onScan(value: String) {}
|
||||||
|
}
|
@ -27,9 +27,7 @@ class EnterPublicKeyFragment : Fragment() {
|
|||||||
var delegate: EnterPublicKeyDelegate? = null
|
var delegate: EnterPublicKeyDelegate? = null
|
||||||
|
|
||||||
private val hexEncodedPublicKey: String
|
private val hexEncodedPublicKey: String
|
||||||
get() {
|
get() = TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||||
return TextSecurePreferences.getLocalNumber(requireContext())!!
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||||
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
||||||
|
@ -1,84 +1,85 @@
|
|||||||
package org.thoughtcrime.securesms.dms
|
package org.thoughtcrime.securesms.dms
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
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.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
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.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 androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.FragmentNewMessageBinding
|
|
||||||
import nl.komponents.kovenant.ui.failUi
|
|
||||||
import nl.komponents.kovenant.ui.successUi
|
|
||||||
import org.session.libsession.snode.SnodeAPI
|
|
||||||
import org.session.libsession.utilities.Address
|
import org.session.libsession.utilities.Address
|
||||||
import org.session.libsession.utilities.recipients.Recipient
|
import org.session.libsession.utilities.recipients.Recipient
|
||||||
import org.session.libsignal.utilities.PublicKeyValidation
|
|
||||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
||||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||||
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
import org.thoughtcrime.securesms.dependencies.DatabaseComponent
|
||||||
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.BorderlessButtonSecondary
|
||||||
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
|
import org.thoughtcrime.securesms.ui.PreviewTheme
|
||||||
|
import org.thoughtcrime.securesms.ui.ThemeResPreviewParameterProvider
|
||||||
|
import org.thoughtcrime.securesms.ui.components.AppBar
|
||||||
|
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class NewMessageFragment : Fragment() {
|
class NewMessageFragment : Fragment() {
|
||||||
|
|
||||||
private lateinit var binding: FragmentNewMessageBinding
|
val viewModel: NewMessageViewModel by viewModels()
|
||||||
|
|
||||||
lateinit var delegate: NewConversationDelegate
|
lateinit var delegate: NewConversationDelegate
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
viewModel.event.collect {
|
||||||
|
when (it) {
|
||||||
|
is Event.Success -> createPrivateChat(it.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater, container: ViewGroup?,
|
inflater: LayoutInflater, container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View {
|
): View = ComposeView(requireContext()).apply {
|
||||||
binding = FragmentNewMessageBinding.inflate(inflater)
|
setContent {
|
||||||
return binding.root
|
AppTheme {
|
||||||
}
|
val uiState by viewModel.state.collectAsState(State())
|
||||||
|
NewMessage(
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
uiState,
|
||||||
super.onViewCreated(view, savedInstanceState)
|
viewModel,
|
||||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
onClose = { delegate.onDialogClosePressed() },
|
||||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
onBack = { delegate.onDialogBackPressed() },
|
||||||
val onsOrPkDelegate = { onsNameOrPublicKey: String -> createPrivateChatIfPossible(onsNameOrPublicKey)}
|
onHelp = { Toast.makeText(requireContext(), "todo, not implemented.", Toast.LENGTH_LONG).show() }
|
||||||
val adapter = NewMessageFragmentAdapter(
|
|
||||||
parentFragment = this,
|
|
||||||
enterPublicKeyDelegate = onsOrPkDelegate,
|
|
||||||
scanPublicKeyDelegate = onsOrPkDelegate
|
|
||||||
)
|
)
|
||||||
binding.viewPager.adapter = adapter
|
|
||||||
val mediator = TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, pos ->
|
|
||||||
tab.text = when (pos) {
|
|
||||||
0 -> getString(R.string.activity_create_private_chat_enter_session_id_tab_title)
|
|
||||||
1 -> getString(R.string.activity_create_private_chat_scan_qr_code_tab_title)
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mediator.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {
|
|
||||||
if (PublicKeyValidation.isValid(onsNameOrPublicKey, isPrefixRequired = false)) {
|
|
||||||
if (PublicKeyValidation.hasValidPrefix(onsNameOrPublicKey)) {
|
|
||||||
createPrivateChat(onsNameOrPublicKey)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(requireContext(), R.string.accountIdErrorInvalid, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This could be an ONS name
|
|
||||||
showLoader()
|
|
||||||
SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
|
|
||||||
hideLoader()
|
|
||||||
createPrivateChat(hexEncodedPublicKey)
|
|
||||||
}.failUi { exception ->
|
|
||||||
hideLoader()
|
|
||||||
val message = when (exception) {
|
|
||||||
is SnodeAPI.Error.Generic -> "We couldn’t recognize this ONS. Please check and try again."
|
|
||||||
else -> exception.localizedMessage ?: getString(R.string.fragment_enter_public_key_error_message)
|
|
||||||
}
|
|
||||||
Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -92,19 +93,72 @@ class NewMessageFragment : Fragment() {
|
|||||||
}.let(requireContext()::startActivity)
|
}.let(requireContext()::startActivity)
|
||||||
delegate.onDialogClosePressed()
|
delegate.onDialogClosePressed()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showLoader() {
|
|
||||||
binding.loader.visibility = View.VISIBLE
|
|
||||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideLoader() {
|
@Preview
|
||||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
@Composable
|
||||||
|
private fun PreviewNewMessage(
|
||||||
|
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||||
|
) {
|
||||||
|
PreviewTheme(themeResId) {
|
||||||
|
NewMessage(State(), object: Callbacks {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
|
||||||
super.onAnimationEnd(animation)
|
|
||||||
binding.loader.visibility = View.GONE
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
}
|
@Composable
|
||||||
})
|
private fun NewMessage(
|
||||||
|
state: State,
|
||||||
|
callbacks: Callbacks,
|
||||||
|
onClose: () -> Unit = {},
|
||||||
|
onBack: () -> Unit = {},
|
||||||
|
onHelp: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val pagerState = rememberPagerState { TITLES.size }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.background(MaterialTheme.colors.background)) {
|
||||||
|
AppBar("New Message", onClose = { onClose() }, onBack = { onBack() })
|
||||||
|
SessionTabRow(pagerState, TITLES)
|
||||||
|
HorizontalPager(pagerState) {
|
||||||
|
when (TITLES[it]) {
|
||||||
|
R.string.enter_account_id -> EnterAccountId(state, callbacks, onHelp)
|
||||||
|
R.string.qrScan -> MaybeScanQrCode()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EnterAccountId(
|
||||||
|
state: State,
|
||||||
|
callbacks: Callbacks,
|
||||||
|
onHelp: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
SessionOutlinedTextField(
|
||||||
|
text = state.newMessageIdOrOns,
|
||||||
|
modifier = Modifier.padding(horizontal = 64.dp),
|
||||||
|
placeholder = "Enter account ID or ONS",
|
||||||
|
onChange = callbacks::onChange,
|
||||||
|
onContinue = callbacks::onContinue,
|
||||||
|
error = state.error?.string()
|
||||||
|
)
|
||||||
|
BorderlessButtonSecondary(text = "Start a new conversation by entering your friend's Account ID, ONS or scanning their QR code.") { onHelp() }
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
OutlineButton(
|
||||||
|
text = stringResource(id = R.string.continue_2),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(horizontal = 64.dp, vertical = 20.dp)
|
||||||
|
.width(200.dp),
|
||||||
|
loading = state.loading
|
||||||
|
) { callbacks.onContinue() }
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,24 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.dms
|
|
||||||
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragment
|
|
||||||
import org.thoughtcrime.securesms.util.ScanQRCodeWrapperFragmentDelegate
|
|
||||||
|
|
||||||
class NewMessageFragmentAdapter(
|
|
||||||
private val parentFragment: Fragment,
|
|
||||||
private val enterPublicKeyDelegate: EnterPublicKeyDelegate,
|
|
||||||
private val scanPublicKeyDelegate: ScanQRCodeWrapperFragmentDelegate
|
|
||||||
) : FragmentStateAdapter(parentFragment) {
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = 2
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
return when (position) {
|
|
||||||
0 -> EnterPublicKeyFragment().apply { delegate = enterPublicKeyDelegate }
|
|
||||||
1 -> ScanQRCodeWrapperFragment().apply { delegate = scanPublicKeyDelegate }
|
|
||||||
else -> throw IllegalStateException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,86 @@
|
|||||||
|
package org.thoughtcrime.securesms.dms
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
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 nl.komponents.kovenant.ui.failUi
|
||||||
|
import nl.komponents.kovenant.ui.successUi
|
||||||
|
import org.session.libsession.snode.SnodeAPI
|
||||||
|
import org.session.libsignal.utilities.PublicKeyValidation
|
||||||
|
import org.thoughtcrime.securesms.ui.GetString
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class NewMessageViewModel @Inject constructor(
|
||||||
|
private val application: Application
|
||||||
|
): AndroidViewModel(application), Callbacks {
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(
|
||||||
|
State()
|
||||||
|
)
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
private val _event = Channel<Event>()
|
||||||
|
val event = _event.receiveAsFlow()
|
||||||
|
|
||||||
|
override fun onChange(value: String) {
|
||||||
|
_state.update { it.copy(
|
||||||
|
newMessageIdOrOns = value,
|
||||||
|
error = null
|
||||||
|
) }
|
||||||
|
}
|
||||||
|
override fun onContinue() {
|
||||||
|
createPrivateChatIfPossible(state.value.newMessageIdOrOns)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScan(value: String) {
|
||||||
|
createPrivateChatIfPossible(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createPrivateChatIfPossible(onsNameOrPublicKey: String) {
|
||||||
|
if (PublicKeyValidation.isValid(onsNameOrPublicKey, isPrefixRequired = false)) {
|
||||||
|
if (PublicKeyValidation.hasValidPrefix(onsNameOrPublicKey)) {
|
||||||
|
onPublicKey(onsNameOrPublicKey)
|
||||||
|
} else {
|
||||||
|
_state.update { it.copy(error = GetString(R.string.accountIdErrorInvalid), loading = false) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This could be an ONS name
|
||||||
|
_state.update { it.copy(error = null, loading = true) }
|
||||||
|
|
||||||
|
SnodeAPI.getSessionID(onsNameOrPublicKey).successUi { hexEncodedPublicKey ->
|
||||||
|
_state.update { it.copy(loading = false) }
|
||||||
|
onPublicKey(onsNameOrPublicKey)
|
||||||
|
}.failUi { exception ->
|
||||||
|
_state.update { it.copy(loading = false, error = GetString(exception) { it.toMessage() }) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onPublicKey(onsNameOrPublicKey: String) {
|
||||||
|
viewModelScope.launch { _event.send(Event.Success(onsNameOrPublicKey)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Exception.toMessage() = when (this) {
|
||||||
|
is SnodeAPI.Error.Generic -> "We couldn’t recognize this ONS. Please check and try again."
|
||||||
|
else -> localizedMessage ?: application.getString(R.string.fragment_enter_public_key_error_message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class State(
|
||||||
|
val newMessageIdOrOns: String = "",
|
||||||
|
val error: GetString? = null,
|
||||||
|
val loading: Boolean = false
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface Event {
|
||||||
|
data class Success(val key: String): Event
|
||||||
|
}
|
@ -3,82 +3,48 @@ package org.thoughtcrime.securesms.onboarding
|
|||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.camera.core.CameraSelector
|
|
||||||
import androidx.camera.core.ExperimentalGetImage
|
import androidx.camera.core.ExperimentalGetImage
|
||||||
import androidx.camera.core.ImageAnalysis
|
|
||||||
import androidx.camera.core.ImageAnalysis.Analyzer
|
import androidx.camera.core.ImageAnalysis.Analyzer
|
||||||
import androidx.camera.core.ImageProxy
|
import androidx.camera.core.ImageProxy
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
||||||
import androidx.camera.view.PreviewView
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.OutlinedTextField
|
|
||||||
import androidx.compose.material.Scaffold
|
|
||||||
import androidx.compose.material.Snackbar
|
|
||||||
import androidx.compose.material.SnackbarHost
|
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
import androidx.compose.material.rememberScaffoldState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
import androidx.compose.ui.platform.ComposeView
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
|
||||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
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.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|
||||||
import com.google.accompanist.permissions.isGranted
|
|
||||||
import com.google.accompanist.permissions.rememberPermissionState
|
|
||||||
import com.google.accompanist.permissions.shouldShowRationale
|
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
|
||||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
|
||||||
import com.google.mlkit.vision.barcode.common.Barcode
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
import com.google.mlkit.vision.common.InputImage
|
import com.google.mlkit.vision.common.InputImage
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.session.libsession.utilities.TextSecurePreferences
|
import org.session.libsession.utilities.TextSecurePreferences
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||||
import org.thoughtcrime.securesms.ui.AppTheme
|
import org.thoughtcrime.securesms.ui.AppTheme
|
||||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
import org.thoughtcrime.securesms.ui.baseBold
|
import org.thoughtcrime.securesms.ui.baseBold
|
||||||
|
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
|
||||||
|
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
|
||||||
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
import org.thoughtcrime.securesms.ui.components.SessionTabRow
|
||||||
import org.thoughtcrime.securesms.ui.outlinedTextFieldColors
|
|
||||||
import java.util.concurrent.Executors
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val TAG = "LinkDeviceActivity"
|
private const val TAG = "LinkDeviceActivity"
|
||||||
@ -94,11 +60,6 @@ class LinkDeviceActivity : BaseActionBarActivity() {
|
|||||||
|
|
||||||
val viewModel: LinkDeviceViewModel by viewModels()
|
val viewModel: LinkDeviceViewModel by viewModels()
|
||||||
|
|
||||||
val preview = androidx.camera.core.Preview.Builder().build()
|
|
||||||
val selector = CameraSelector.Builder()
|
|
||||||
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
supportActionBar?.setTitle(R.string.activity_link_load_account)
|
supportActionBar?.setTitle(R.string.activity_link_load_account)
|
||||||
@ -124,16 +85,12 @@ class LinkDeviceActivity : BaseActionBarActivity() {
|
|||||||
}.let(::setContentView)
|
}.let(::setContentView)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadAccountScreen(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
fun LoadAccountScreen(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
||||||
val pagerState = rememberPagerState { TITLES.size }
|
val pagerState = rememberPagerState { TITLES.size }
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
val localContext = LocalContext.current
|
|
||||||
val cameraProvider = remember { ProcessCameraProvider.getInstance(localContext) }
|
|
||||||
SessionTabRow(pagerState, TITLES)
|
SessionTabRow(pagerState, TITLES)
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
@ -141,114 +98,13 @@ class LinkDeviceActivity : BaseActionBarActivity() {
|
|||||||
) { page ->
|
) { page ->
|
||||||
val title = TITLES[page]
|
val title = TITLES[page]
|
||||||
|
|
||||||
val options = BarcodeScannerOptions.Builder()
|
|
||||||
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
|
||||||
.build()
|
|
||||||
val scanner = BarcodeScanning.getClient(options)
|
|
||||||
|
|
||||||
runCatching {
|
|
||||||
cameraProvider.get().unbindAll()
|
|
||||||
if (title == R.string.qrScan) {
|
|
||||||
LocalSoftwareKeyboardController.current?.hide()
|
|
||||||
cameraProvider.get().bindToLifecycle(
|
|
||||||
LocalLifecycleOwner.current,
|
|
||||||
selector,
|
|
||||||
preview,
|
|
||||||
buildAnalysisUseCase(scanner, viewModel::scan)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.onFailure { Log.e(TAG, "error binding camera", it) }
|
|
||||||
when (title) {
|
when (title) {
|
||||||
R.string.sessionRecoveryPassword -> RecoveryPassword(state, onChange, onContinue)
|
R.string.sessionRecoveryPassword -> RecoveryPassword(state, onChange, onContinue)
|
||||||
R.string.qrScan -> MaybeScanQrCode()
|
R.string.qrScan -> MaybeScanQrCode(viewModel.qrErrorsFlow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
|
||||||
@Composable
|
|
||||||
fun MaybeScanQrCode() {
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
|
||||||
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
|
|
||||||
|
|
||||||
if (cameraPermissionState.status.isGranted) {
|
|
||||||
ScanQrCode(preview, viewModel.qrErrorsFlow)
|
|
||||||
} else if (cameraPermissionState.status.shouldShowRationale) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.Center)
|
|
||||||
.padding(horizontal = 60.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.activity_link_camera_permission_permanently_denied_configure_in_settings),
|
|
||||||
textAlign = TextAlign.Center
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.size(20.dp))
|
|
||||||
OutlineButton(
|
|
||||||
text = stringResource(R.string.sessionSettings),
|
|
||||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
|
||||||
) {
|
|
||||||
Intent(ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
|
||||||
data = Uri.fromParts("package", packageName, null)
|
|
||||||
}.let(::startActivity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
OutlineButton(
|
|
||||||
text = stringResource(R.string.cameraGrantAccess),
|
|
||||||
modifier = Modifier.align(Alignment.Center)
|
|
||||||
) {
|
|
||||||
cameraPermissionState.run { launchPermissionRequest() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ScanQrCode(preview: androidx.camera.core.Preview, errors: Flow<String>) {
|
|
||||||
val scaffoldState = rememberScaffoldState()
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
errors.collect { error ->
|
|
||||||
lifecycleScope.launch {
|
|
||||||
scaffoldState.snackbarHostState.showSnackbar(message = error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
|
||||||
scaffoldState = scaffoldState,
|
|
||||||
snackbarHost = {
|
|
||||||
SnackbarHost(
|
|
||||||
hostState = scaffoldState.snackbarHostState,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
) { data ->
|
|
||||||
Snackbar(
|
|
||||||
snackbarData = data,
|
|
||||||
modifier = Modifier.padding(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Box(modifier = Modifier.padding(padding)) {
|
|
||||||
AndroidView(
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } }
|
|
||||||
)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
Modifier
|
|
||||||
.aspectRatio(1f)
|
|
||||||
.padding(20.dp)
|
|
||||||
.clip(shape = RoundedCornerShape(20.dp))
|
|
||||||
.background(Color(0x33ffffff))
|
|
||||||
.align(Alignment.Center)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@ -272,20 +128,13 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
|
|||||||
Spacer(Modifier.size(28.dp))
|
Spacer(Modifier.size(28.dp))
|
||||||
Text(stringResource(R.string.activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings))
|
Text(stringResource(R.string.activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings))
|
||||||
Spacer(Modifier.size(24.dp))
|
Spacer(Modifier.size(24.dp))
|
||||||
OutlinedTextField(
|
SessionOutlinedTextField(
|
||||||
value = state.recoveryPhrase,
|
text = state.recoveryPhrase,
|
||||||
onValueChange = { onChange(it) },
|
placeholder = stringResource(R.string.recoveryPasswordEnter),
|
||||||
placeholder = { Text(stringResource(R.string.recoveryPasswordEnter)) },
|
onChange = onChange,
|
||||||
colors = outlinedTextFieldColors(state.error != null),
|
onContinue = onContinue,
|
||||||
singleLine = true,
|
error = state.error,
|
||||||
keyboardActions = KeyboardActions(
|
modifier = Modifier.padding(horizontal = 64.dp)
|
||||||
onDone = { onContinue() },
|
|
||||||
onGo = { onContinue() },
|
|
||||||
onSearch = { onContinue() },
|
|
||||||
onSend = { onContinue() },
|
|
||||||
),
|
|
||||||
isError = state.error != null,
|
|
||||||
shape = RoundedCornerShape(12.dp)
|
|
||||||
)
|
)
|
||||||
Spacer(Modifier.size(12.dp))
|
Spacer(Modifier.size(12.dp))
|
||||||
state.error?.let {
|
state.error?.let {
|
||||||
@ -306,16 +155,6 @@ fun Context.startLinkDeviceActivity() {
|
|||||||
Intent(this, LinkDeviceActivity::class.java).let(::startActivity)
|
Intent(this, LinkDeviceActivity::class.java).let(::startActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("UnsafeOptInUsageError")
|
|
||||||
private fun buildAnalysisUseCase(
|
|
||||||
scanner: BarcodeScanner,
|
|
||||||
onBarcodeScanned: (String) -> Unit
|
|
||||||
): ImageAnalysis = ImageAnalysis.Builder()
|
|
||||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
||||||
.build().apply {
|
|
||||||
setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned))
|
|
||||||
}
|
|
||||||
|
|
||||||
class Analyzer(
|
class Analyzer(
|
||||||
private val scanner: BarcodeScanner,
|
private val scanner: BarcodeScanner,
|
||||||
private val onBarcodeScanned: (String) -> Unit
|
private val onBarcodeScanned: (String) -> Unit
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.ui
|
package org.thoughtcrime.securesms.ui
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.ScrollState
|
import androidx.compose.foundation.ScrollState
|
||||||
@ -25,7 +26,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.ButtonColors
|
import androidx.compose.material.ButtonColors
|
||||||
import androidx.compose.material.ButtonDefaults
|
import androidx.compose.material.ButtonDefaults
|
||||||
import androidx.compose.material.Card
|
import androidx.compose.material.Card
|
||||||
|
import androidx.compose.material.CircularProgressIndicator
|
||||||
import androidx.compose.material.Colors
|
import androidx.compose.material.Colors
|
||||||
|
import androidx.compose.material.ContentAlpha
|
||||||
import androidx.compose.material.Icon
|
import androidx.compose.material.Icon
|
||||||
import androidx.compose.material.MaterialTheme
|
import androidx.compose.material.MaterialTheme
|
||||||
import androidx.compose.material.OutlinedButton
|
import androidx.compose.material.OutlinedButton
|
||||||
@ -68,6 +71,7 @@ fun OutlineButton(
|
|||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
color: Color = LocalExtraColors.current.prominentButtonColor,
|
color: Color = LocalExtraColors.current.prominentButtonColor,
|
||||||
|
loading: Boolean = false,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
@ -80,7 +84,14 @@ fun OutlineButton(
|
|||||||
backgroundColor = Color.Unspecified
|
backgroundColor = Color.Unspecified
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(text = text)
|
AnimatedVisibility(loading) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
color = color,
|
||||||
|
strokeWidth = 2.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(!loading) { Text(text = text) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,20 +110,35 @@ fun FilledButton(text: String, modifier: Modifier = Modifier, onClick: () -> Uni
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BorderlessButtonSecondary(
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
BorderlessButton(
|
||||||
|
text,
|
||||||
|
contentColor = MaterialTheme.colors.onSurface.copy(ContentAlpha.medium),
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BorderlessButton(
|
fun BorderlessButton(
|
||||||
text: String,
|
text: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
fontSize: TextUnit = TextUnit.Unspecified,
|
fontSize: TextUnit = TextUnit.Unspecified,
|
||||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||||
onClick: () -> Unit) {
|
contentColor: Color = MaterialTheme.colors.onBackground,
|
||||||
|
backgroundColor: Color = Color.Transparent,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = RoundedCornerShape(50), // = 50% percent
|
shape = RoundedCornerShape(percent = 50),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
contentColor = MaterialTheme.colors.onBackground,
|
contentColor = contentColor,
|
||||||
backgroundColor = MaterialTheme.colors.background
|
backgroundColor = backgroundColor
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
@ -271,7 +297,8 @@ fun Modifier.contentDescription(id: Int?): Modifier {
|
|||||||
@Composable
|
@Composable
|
||||||
fun OutlineButton(text: GetString, contentDescription: GetString? = text, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
fun OutlineButton(text: GetString, contentDescription: GetString? = text, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
modifier = modifier.size(108.dp, 34.dp)
|
modifier = modifier
|
||||||
|
.size(108.dp, 34.dp)
|
||||||
.contentDescription(contentDescription),
|
.contentDescription(contentDescription),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
|
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
|
||||||
@ -403,7 +430,8 @@ fun RowScope.SessionShieldIcon() {
|
|||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.session_shield),
|
painter = painterResource(R.drawable.session_shield),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.align(Alignment.CenterVertically)
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterVertically)
|
||||||
.wrapContentSize(unbounded = true)
|
.wrapContentSize(unbounded = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ sealed class GetString {
|
|||||||
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
|
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
|
||||||
fun GetString(string: String) = GetString.FromString(string)
|
fun GetString(string: String) = GetString.FromString(string)
|
||||||
fun GetString(function: (Context) -> String) = GetString.FromFun(function)
|
fun GetString(function: (Context) -> String) = GetString.FromFun(function)
|
||||||
fun <T> GetString(value: T, function: (Context, T) -> String) = GetString.FromMap(value, function)
|
fun <T> GetString(value: T, function: Context.(T) -> String) = GetString.FromMap(value, function)
|
||||||
fun GetString(duration: Duration) = GetString.FromMap(duration, ExpirationUtil::getExpirationDisplayValue)
|
fun GetString(duration: Duration) = GetString.FromMap(duration, ExpirationUtil::getExpirationDisplayValue)
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.Icon
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
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.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import network.loki.messenger.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppBar(title: String, onClose: () -> Unit = {}, onBack: (() -> Unit)? = null) {
|
||||||
|
Row(modifier = Modifier.height(64.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(64.dp)) {
|
||||||
|
onBack?.let {
|
||||||
|
IconButton(onClick = it) {
|
||||||
|
Icon(painter = painterResource(id = R.drawable.ic_prev), contentDescription = "back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Text(text = title, style = MaterialTheme.typography.h4)
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(64.dp)) {
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Icon(painter = painterResource(id = R.drawable.ic_x), contentDescription = "back")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
195
app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt
Normal file
195
app/src/main/java/org/thoughtcrime/securesms/ui/components/QR.kt
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui.components
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.camera.core.CameraSelector
|
||||||
|
import androidx.camera.core.ImageAnalysis
|
||||||
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||||
|
import androidx.camera.view.PreviewView
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Scaffold
|
||||||
|
import androidx.compose.material.Snackbar
|
||||||
|
import androidx.compose.material.SnackbarHost
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.rememberScaffoldState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||||
|
import com.google.accompanist.permissions.isGranted
|
||||||
|
import com.google.accompanist.permissions.rememberPermissionState
|
||||||
|
import com.google.accompanist.permissions.shouldShowRationale
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
|
||||||
|
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||||
|
import com.google.mlkit.vision.barcode.common.Barcode
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
|
import network.loki.messenger.R
|
||||||
|
import org.session.libsignal.utilities.Log
|
||||||
|
import org.thoughtcrime.securesms.onboarding.Analyzer
|
||||||
|
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
typealias CameraPreview = androidx.camera.core.Preview
|
||||||
|
|
||||||
|
private const val TAG = "NewMessageFragment"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MaybeScanQrCode(errors: Flow<String> = emptyFlow()) {
|
||||||
|
LocalContext.current.run {
|
||||||
|
MaybeScanQrCode(onScan = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MaybeScanQrCode(
|
||||||
|
errors: Flow<String> = emptyFlow(),
|
||||||
|
onClickSettings: () -> Unit = LocalContext.current.run { {
|
||||||
|
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = Uri.fromParts("package", packageName, null)
|
||||||
|
}.let(::startActivity)
|
||||||
|
} },
|
||||||
|
onScan: (String) -> Unit = {}
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LocalSoftwareKeyboardController.current?.hide()
|
||||||
|
|
||||||
|
val cameraPermissionState = rememberPermissionState(Manifest.permission.CAMERA)
|
||||||
|
|
||||||
|
if (cameraPermissionState.status.isGranted) {
|
||||||
|
ScanQrCode(errors, onScan)
|
||||||
|
} else if (cameraPermissionState.status.shouldShowRationale) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(horizontal = 60.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.activity_link_camera_permission_permanently_denied_configure_in_settings),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(20.dp))
|
||||||
|
OutlineButton(
|
||||||
|
text = stringResource(R.string.sessionSettings),
|
||||||
|
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||||
|
onClick = onClickSettings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OutlineButton(
|
||||||
|
text = stringResource(R.string.cameraGrantAccess),
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
) {
|
||||||
|
cameraPermissionState.run { launchPermissionRequest() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScanQrCode(errors: Flow<String>, onScan: (String) -> Unit) {
|
||||||
|
val localContext = LocalContext.current
|
||||||
|
val cameraProvider = remember { ProcessCameraProvider.getInstance(localContext) }
|
||||||
|
|
||||||
|
val preview = androidx.camera.core.Preview.Builder().build()
|
||||||
|
val selector = CameraSelector.Builder()
|
||||||
|
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
runCatching {
|
||||||
|
cameraProvider.get().unbindAll()
|
||||||
|
|
||||||
|
val options = BarcodeScannerOptions.Builder()
|
||||||
|
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
|
||||||
|
.build()
|
||||||
|
val scanner = BarcodeScanning.getClient(options)
|
||||||
|
|
||||||
|
cameraProvider.get().bindToLifecycle(
|
||||||
|
LocalLifecycleOwner.current,
|
||||||
|
selector,
|
||||||
|
preview,
|
||||||
|
buildAnalysisUseCase(scanner, onScan)
|
||||||
|
)
|
||||||
|
}.onFailure { Log.e(TAG, "error binding camera", it) }
|
||||||
|
|
||||||
|
DisposableEffect(key1 = cameraProvider) {
|
||||||
|
onDispose {
|
||||||
|
cameraProvider.get().unbindAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val scaffoldState = rememberScaffoldState()
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
errors.collect { error ->
|
||||||
|
scaffoldState.snackbarHostState.showSnackbar(message = error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
scaffoldState = scaffoldState,
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(
|
||||||
|
hostState = scaffoldState.snackbarHostState,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) { data ->
|
||||||
|
Snackbar(
|
||||||
|
snackbarData = data,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Box(modifier = Modifier.padding(padding)) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
factory = { PreviewView(it).apply { preview.setSurfaceProvider(surfaceProvider) } }
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.padding(20.dp)
|
||||||
|
.clip(shape = RoundedCornerShape(20.dp))
|
||||||
|
.background(Color(0x33ffffff))
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("UnsafeOptInUsageError")
|
||||||
|
private fun buildAnalysisUseCase(
|
||||||
|
scanner: BarcodeScanner,
|
||||||
|
onBarcodeScanned: (String) -> Unit
|
||||||
|
): ImageAnalysis = ImageAnalysis.Builder()
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build().apply {
|
||||||
|
setAnalyzer(Executors.newSingleThreadExecutor(), Analyzer(scanner, onBarcodeScanned))
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.thoughtcrime.securesms.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.OutlinedTextField
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.thoughtcrime.securesms.ui.baseBold
|
||||||
|
import org.thoughtcrime.securesms.ui.outlinedTextFieldColors
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SessionOutlinedTextField(
|
||||||
|
text: String,
|
||||||
|
placeholder: String,
|
||||||
|
onChange: (String) -> Unit,
|
||||||
|
onContinue: () -> Unit,
|
||||||
|
error: String? = null,
|
||||||
|
modifier: Modifier
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { onChange(it) },
|
||||||
|
placeholder = { Text(placeholder) },
|
||||||
|
colors = outlinedTextFieldColors(error != null),
|
||||||
|
singleLine = true,
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onDone = { onContinue() },
|
||||||
|
onGo = { onContinue() },
|
||||||
|
onSearch = { onContinue() },
|
||||||
|
onSend = { onContinue() },
|
||||||
|
),
|
||||||
|
isError = error != null,
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
error?.let {
|
||||||
|
Text(
|
||||||
|
it,
|
||||||
|
modifier = Modifier.padding(top = 12.dp),
|
||||||
|
style = MaterialTheme.typography.baseBold,
|
||||||
|
color = MaterialTheme.colors.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1082,4 +1082,5 @@
|
|||||||
<string name="activity_link_camera_permission_permanently_denied_configure_in_settings">Camera Permission permanently denied. Configure in settings.</string>
|
<string name="activity_link_camera_permission_permanently_denied_configure_in_settings">Camera Permission permanently denied. Configure in settings.</string>
|
||||||
<string name="activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings">Enter your recovery password to load your account. If you haven\'t saved it, you can find it in your app settings.</string>
|
<string name="activity_link_enter_your_recovery_password_to_load_your_account_if_you_haven_t_saved_it_you_can_find_it_in_your_app_settings">Enter your recovery password to load your account. If you haven\'t saved it, you can find it in your app settings.</string>
|
||||||
<string name="accountIdErrorInvalid">This Account ID is invalid. Please check and try again.</string>
|
<string name="accountIdErrorInvalid">This Account ID is invalid. Please check and try again.</string>
|
||||||
|
<string name="enter_account_id">Enter Account ID</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -22,6 +22,7 @@ kotlinVersion=1.8.21
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
appcompatVersion=1.6.1
|
appcompatVersion=1.6.1
|
||||||
coreVersion=1.8.0
|
coreVersion=1.8.0
|
||||||
|
composeVersion=1.6.4
|
||||||
coroutinesVersion=1.6.4
|
coroutinesVersion=1.6.4
|
||||||
curve25519Version=0.6.0
|
curve25519Version=0.6.0
|
||||||
daggerVersion=2.46.1
|
daggerVersion=2.46.1
|
||||||
@ -30,7 +31,7 @@ jacksonDatabindVersion=2.9.8
|
|||||||
junitVersion=4.13.2
|
junitVersion=4.13.2
|
||||||
kotlinxJsonVersion=1.3.3
|
kotlinxJsonVersion=1.3.3
|
||||||
kovenantVersion=3.3.0
|
kovenantVersion=3.3.0
|
||||||
lifecycleVersion=2.5.1
|
lifecycleVersion=2.7.0
|
||||||
materialVersion=1.8.0
|
materialVersion=1.8.0
|
||||||
mockitoKotlinVersion=4.1.0
|
mockitoKotlinVersion=4.1.0
|
||||||
okhttpVersion=3.12.1
|
okhttpVersion=3.12.1
|
||||||
|
Loading…
Reference in New Issue
Block a user