mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-23 10:05:15 +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'
|
||||
|
||||
implementation 'com.github.bumptech.glide:compose:1.0.0-alpha.5'
|
||||
implementation 'androidx.compose.ui:ui:1.6.2'
|
||||
implementation 'androidx.compose.animation:animation:1.6.2'
|
||||
implementation 'androidx.compose.ui:ui-tooling:1.6.2'
|
||||
implementation "androidx.compose.runtime:runtime-livedata:1.6.2"
|
||||
implementation 'androidx.compose.foundation:foundation-layout:1.6.2'
|
||||
implementation 'androidx.compose.material:material:1.6.2'
|
||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4-android:1.6.2'
|
||||
debugImplementation 'androidx.compose.ui:ui-test-manifest:1.6.2'
|
||||
implementation "androidx.compose.ui:ui:$composeVersion"
|
||||
implementation "androidx.compose.animation:animation:$composeVersion"
|
||||
implementation "androidx.compose.ui:ui-tooling:$composeVersion"
|
||||
implementation "androidx.compose.runtime:runtime-livedata:$composeVersion"
|
||||
implementation "androidx.compose.foundation:foundation-layout:$composeVersion"
|
||||
implementation "androidx.compose.material:material:$composeVersion"
|
||||
androidTestImplementation "androidx.compose.ui:ui-test-junit4-android:$composeVersion"
|
||||
debugImplementation "androidx.compose.ui:ui-test-manifest:$composeVersion"
|
||||
|
||||
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-indicators: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-lifecycle:1.3.1"
|
||||
implementation "androidx.camera:camera-view:1.3.1"
|
||||
implementation "androidx.camera:camera-camera2:1.3.2"
|
||||
implementation "androidx.camera:camera-lifecycle:1.3.2"
|
||||
implementation "androidx.camera:camera-view:1.3.2"
|
||||
|
||||
implementation 'com.google.firebase:firebase-core:21.1.1'
|
||||
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
|
||||
|
||||
private val hexEncodedPublicKey: String
|
||||
get() {
|
||||
return TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||
}
|
||||
get() = TextSecurePreferences.getLocalNumber(requireContext())!!
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
binding = FragmentEnterPublicKeyBinding.inflate(inflater, container, false)
|
||||
|
@ -1,84 +1,85 @@
|
||||
package org.thoughtcrime.securesms.dms
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
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 com.google.android.material.tabs.TabLayoutMediator
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
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.recipients.Recipient
|
||||
import org.session.libsignal.utilities.PublicKeyValidation
|
||||
import org.thoughtcrime.securesms.conversation.start.NewConversationDelegate
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
|
||||
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
|
||||
class NewMessageFragment : Fragment() {
|
||||
|
||||
private lateinit var binding: FragmentNewMessageBinding
|
||||
val viewModel: NewMessageViewModel by viewModels()
|
||||
|
||||
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(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentNewMessageBinding.inflate(inflater)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.backButton.setOnClickListener { delegate.onDialogBackPressed() }
|
||||
binding.closeButton.setOnClickListener { delegate.onDialogClosePressed() }
|
||||
val onsOrPkDelegate = { onsNameOrPublicKey: String -> createPrivateChatIfPossible(onsNameOrPublicKey)}
|
||||
val adapter = NewMessageFragmentAdapter(
|
||||
parentFragment = this,
|
||||
enterPublicKeyDelegate = onsOrPkDelegate,
|
||||
scanPublicKeyDelegate = onsOrPkDelegate
|
||||
): View = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
AppTheme {
|
||||
val uiState by viewModel.state.collectAsState(State())
|
||||
NewMessage(
|
||||
uiState,
|
||||
viewModel,
|
||||
onClose = { delegate.onDialogClosePressed() },
|
||||
onBack = { delegate.onDialogBackPressed() },
|
||||
onHelp = { Toast.makeText(requireContext(), "todo, not implemented.", Toast.LENGTH_LONG).show() }
|
||||
)
|
||||
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)
|
||||
delegate.onDialogClosePressed()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showLoader() {
|
||||
binding.loader.visibility = View.VISIBLE
|
||||
binding.loader.animate().setDuration(150).alpha(1.0f).start()
|
||||
}
|
||||
|
||||
private fun hideLoader() {
|
||||
binding.loader.animate().setDuration(150).alpha(0.0f).setListener(object : AnimatorListenerAdapter() {
|
||||
|
||||
override fun onAnimationEnd(animation: Animator) {
|
||||
super.onAnimationEnd(animation)
|
||||
binding.loader.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PreviewNewMessage(
|
||||
@PreviewParameter(ThemeResPreviewParameterProvider::class) themeResId: Int
|
||||
) {
|
||||
PreviewTheme(themeResId) {
|
||||
NewMessage(State(), object: Callbacks {})
|
||||
}
|
||||
}
|
||||
|
||||
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
|
||||
|
||||
@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.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS
|
||||
import androidx.activity.viewModels
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageAnalysis.Analyzer
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
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.Row
|
||||
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.layout.width
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
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.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.rememberScaffoldState
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
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.BarcodeScannerOptions
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.launch
|
||||
import network.loki.messenger.R
|
||||
import org.session.libsession.utilities.TextSecurePreferences
|
||||
import org.session.libsignal.utilities.Log
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity
|
||||
import org.thoughtcrime.securesms.ui.AppTheme
|
||||
import org.thoughtcrime.securesms.ui.OutlineButton
|
||||
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.outlinedTextFieldColors
|
||||
import java.util.concurrent.Executors
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val TAG = "LinkDeviceActivity"
|
||||
@ -94,11 +60,6 @@ class LinkDeviceActivity : BaseActionBarActivity() {
|
||||
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar?.setTitle(R.string.activity_link_load_account)
|
||||
@ -124,16 +85,12 @@ class LinkDeviceActivity : BaseActionBarActivity() {
|
||||
}.let(::setContentView)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun LoadAccountScreen(state: LinkDeviceState, onChange: (String) -> Unit = {}, onContinue: () -> Unit = {}) {
|
||||
val pagerState = rememberPagerState { TITLES.size }
|
||||
|
||||
Column {
|
||||
val localContext = LocalContext.current
|
||||
val cameraProvider = remember { ProcessCameraProvider.getInstance(localContext) }
|
||||
SessionTabRow(pagerState, TITLES)
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
@ -141,114 +98,13 @@ class LinkDeviceActivity : BaseActionBarActivity() {
|
||||
) { 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) {
|
||||
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
|
||||
@ -272,20 +128,13 @@ fun RecoveryPassword(state: LinkDeviceState, onChange: (String) -> Unit = {}, on
|
||||
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))
|
||||
Spacer(Modifier.size(24.dp))
|
||||
OutlinedTextField(
|
||||
value = state.recoveryPhrase,
|
||||
onValueChange = { onChange(it) },
|
||||
placeholder = { Text(stringResource(R.string.recoveryPasswordEnter)) },
|
||||
colors = outlinedTextFieldColors(state.error != null),
|
||||
singleLine = true,
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = { onContinue() },
|
||||
onGo = { onContinue() },
|
||||
onSearch = { onContinue() },
|
||||
onSend = { onContinue() },
|
||||
),
|
||||
isError = state.error != null,
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
SessionOutlinedTextField(
|
||||
text = state.recoveryPhrase,
|
||||
placeholder = stringResource(R.string.recoveryPasswordEnter),
|
||||
onChange = onChange,
|
||||
onContinue = onContinue,
|
||||
error = state.error,
|
||||
modifier = Modifier.padding(horizontal = 64.dp)
|
||||
)
|
||||
Spacer(Modifier.size(12.dp))
|
||||
state.error?.let {
|
||||
@ -306,16 +155,6 @@ fun Context.startLinkDeviceActivity() {
|
||||
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(
|
||||
private val scanner: BarcodeScanner,
|
||||
private val onBarcodeScanned: (String) -> Unit
|
||||
|
@ -1,6 +1,7 @@
|
||||
package org.thoughtcrime.securesms.ui
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.ScrollState
|
||||
@ -25,7 +26,9 @@ 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.CircularProgressIndicator
|
||||
import androidx.compose.material.Colors
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedButton
|
||||
@ -68,6 +71,7 @@ fun OutlineButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = LocalExtraColors.current.prominentButtonColor,
|
||||
loading: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
OutlinedButton(
|
||||
@ -80,7 +84,14 @@ fun OutlineButton(
|
||||
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
|
||||
fun BorderlessButton(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
onClick: () -> Unit) {
|
||||
contentColor: Color = MaterialTheme.colors.onBackground,
|
||||
backgroundColor: Color = Color.Transparent,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
TextButton(
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(50), // = 50% percent
|
||||
shape = RoundedCornerShape(percent = 50),
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
contentColor = MaterialTheme.colors.onBackground,
|
||||
backgroundColor = MaterialTheme.colors.background
|
||||
contentColor = contentColor,
|
||||
backgroundColor = backgroundColor
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
@ -271,7 +297,8 @@ fun Modifier.contentDescription(id: Int?): Modifier {
|
||||
@Composable
|
||||
fun OutlineButton(text: GetString, contentDescription: GetString? = text, modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||
OutlinedButton(
|
||||
modifier = modifier.size(108.dp, 34.dp)
|
||||
modifier = modifier
|
||||
.size(108.dp, 34.dp)
|
||||
.contentDescription(contentDescription),
|
||||
onClick = onClick,
|
||||
border = BorderStroke(1.dp, LocalExtraColors.current.prominentButtonColor),
|
||||
@ -403,7 +430,8 @@ fun RowScope.SessionShieldIcon() {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.session_shield),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.align(Alignment.CenterVertically)
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
.wrapContentSize(unbounded = true)
|
||||
)
|
||||
}
|
||||
|
@ -48,7 +48,7 @@ sealed class GetString {
|
||||
fun GetString(@StringRes resId: Int) = GetString.FromResId(resId)
|
||||
fun GetString(string: String) = GetString.FromString(string)
|
||||
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)
|
||||
|
||||
|
||||
|
@ -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_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="enter_account_id">Enter Account ID</string>
|
||||
</resources>
|
||||
|
@ -22,6 +22,7 @@ kotlinVersion=1.8.21
|
||||
android.useAndroidX=true
|
||||
appcompatVersion=1.6.1
|
||||
coreVersion=1.8.0
|
||||
composeVersion=1.6.4
|
||||
coroutinesVersion=1.6.4
|
||||
curve25519Version=0.6.0
|
||||
daggerVersion=2.46.1
|
||||
@ -30,7 +31,7 @@ jacksonDatabindVersion=2.9.8
|
||||
junitVersion=4.13.2
|
||||
kotlinxJsonVersion=1.3.3
|
||||
kovenantVersion=3.3.0
|
||||
lifecycleVersion=2.5.1
|
||||
lifecycleVersion=2.7.0
|
||||
materialVersion=1.8.0
|
||||
mockitoKotlinVersion=4.1.0
|
||||
okhttpVersion=3.12.1
|
||||
|
Loading…
Reference in New Issue
Block a user