From 668832afe81f71e3cd740046b536905367f9617b Mon Sep 17 00:00:00 2001 From: Narendra Singh Anjana Date: Fri, 25 Aug 2023 18:52:31 +0530 Subject: [PATCH 01/13] Gsoc 2023 login screen migration to compose (#16) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_login.xml to LoginScreen.kt Compose and Updated facebook Sdk version to latest and added code changes according to new methods in doc and updated gitlab-ci.yml for fbClientSecret --- .github/workflows/github_ci.yml | 1 - .gitlab-ci.yml | 12 +- app/build.gradle | 3 +- app/src/main/AndroidManifest.xml | 5 +- .../ui/fragments/auth/login/LoginFragment.kt | 219 +++++++----------- .../ui/fragments/auth/login/LoginViewModel.kt | 107 ++++++++- .../ui/screens/auth/login/LoginScreen.kt | 135 +++++++++++ .../auth/login/events/LoginScreenEvent.kt | 11 + .../screens/auth/login/events/LoginUiEvent.kt | 7 + .../ui/screens/auth/models/LoginModel.kt | 6 + .../component/PrimaryProgressSnackView.kt | 46 ++++ .../agoraandroid/utilities/AppConstants.kt | 1 + .../aossie/agoraandroid/utilities/AppUtils.kt | 18 ++ app/src/main/res/drawable/facebook_logo.xml | 11 - .../main/res/drawable/ic_facebook_logo.xml | 16 ++ app/src/main/res/layout/fragment_login.xml | 160 ------------- app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/strings.xml | 7 +- build.gradle | 3 +- 19 files changed, 442 insertions(+), 329 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryProgressSnackView.kt delete mode 100644 app/src/main/res/drawable/facebook_logo.xml create mode 100644 app/src/main/res/drawable/ic_facebook_logo.xml delete mode 100644 app/src/main/res/layout/fragment_login.xml diff --git a/.github/workflows/github_ci.yml b/.github/workflows/github_ci.yml index f8ee2fe7..d8f13395 100644 --- a/.github/workflows/github_ci.yml +++ b/.github/workflows/github_ci.yml @@ -9,7 +9,6 @@ on: - '*' env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FB_API_KEY: ${{ secrets.FB_API_KEY }} FB_API_SCHEME: ${{ secrets.FB_API_SCHEME }} SECRET_KEY: ${{ secrets.SECRET_KEY }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 02dbaf48..44404ddc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,30 +16,21 @@ before_script: - apt-get --quiet install --yes wget tar unzip lib32stdc++6 lib32z1 - apt-get --quiet install curl --yes - export ANDROID_HOME="${PWD}/android-home" - # Create a new directory at specified location - install -d $ANDROID_HOME - # Here we are installing androidSDK tools from official source, - # (the key thing here is the url from where you are downloading these sdk tool for command line, so please do note this url pattern there and here as well) - # after that unzipping those tools and - # then running a series of SDK manager commands to install necessary android SDK packages that'll allow the app to build - wget --output-document=$ANDROID_HOME/cmdline-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS}_latest.zip - # move to the archive at ANDROID_HOME - pushd $ANDROID_HOME - unzip -d cmdline-tools cmdline-tools.zip - pushd cmdline-tools - # since commandline tools version 7583922 the root folder is named "cmdline-tools" so we rename it if necessary - mv cmdline-tools tools || true - popd - popd - export PATH=$PATH:${ANDROID_HOME}/cmdline-tools/tools/bin/ - # Nothing fancy here, just checking sdkManager version - sdkmanager --version - # use yes to accept all licenses - yes | sdkmanager --licenses || true - sdkmanager "platforms;android-${ANDROID_COMPILE_SDK}" - sdkmanager "platform-tools" - sdkmanager "build-tools;${ANDROID_BUILD_TOOLS}" - - touch local.properties && echo -e "fbApiKey=$FB_API_KEY\nfbApiScheme=$FB_API_SCHEME\nsecretKey=$SECRET_KEY\nserverKey=$SERVER_KEY" > local.properties + - touch local.properties && echo -e "fbApiKey=$FB_API_KEY\nfbClientSecret=$FB_CLIENT_SECRET\nfbApiScheme=$FB_API_SCHEME\nsecretKey=$SECRET_KEY\nserverKey=$SERVER_KEY" > local.properties - touch app/google-services.json && echo $FIREBASE_JSON > app/google-services.json - touch signing.properties && echo -e "storeFile=keystore.jks\nstorePassword=$KEYSTORE_PASSWORD\nkeyAlias=$KEY_ALIAS\nkeyPassword=$KEY_PASSWORD" > signing.properties - touch app/keystore.jks && echo $KEYSTORE | base64 --decode > app/keystore.jks @@ -49,7 +40,6 @@ before_script: - apt-get --quiet install ruby ruby-dev build-essential dh-autoreconf --yes - gem install bundler:1.17.2 - bundle install - # temporarily disable checking for EPIPE error and use yes to accept all licenses - set +o pipefail - set -o pipefail diff --git a/app/build.gradle b/app/build.gradle index be62c079..154cc2ed 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ Properties properties = new Properties() properties.load(project.rootProject.file('local.properties').newDataInputStream()) def fbApiScheme = properties.getProperty('fbApiScheme') def fbApiKey = properties.getProperty('fbApiKey') +def fbClientSecret = properties.getProperty('fbClientSecret') def secretKey = properties.getProperty('secretKey') def serverKey = properties.getProperty('serverKey') @@ -30,6 +31,7 @@ android { compileSdkVersion rootProject.ext.compileSdk defaultConfig { resValue("string", "fbApiKey", fbApiKey) + resValue("string", "fbClientSecret", fbClientSecret) resValue("string", "fbApiScheme", fbApiScheme) resValue("string", "secretKey", secretKey) resValue("string", "serverKey", serverKey) @@ -121,7 +123,6 @@ dependencies { //Shimmer implementation "com.facebook.shimmer:shimmer:$facebookShimmerVersion" //Facebook Login - implementation "com.facebook.android:facebook-login:$facebookLoginVersion" implementation "com.facebook.android:facebook-android-sdk:$facebookAndroidSDK" //Pie Chart implementation "com.github.PhilJay:MPAndroidChart:v3.1.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a8db1c5f..e185c999 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,10 +67,8 @@ android:exported="true"> - - @@ -80,6 +78,9 @@ + diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt index ba59d3ed..d28452cb 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginFragment.kt @@ -1,34 +1,41 @@ package org.aossie.agoraandroid.ui.fragments.auth.login -import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import com.facebook.CallbackManager -import com.facebook.CallbackManager.Factory import com.facebook.FacebookCallback import com.facebook.FacebookException import com.facebook.login.LoginManager import com.facebook.login.LoginResult +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.aossie.agoraandroid.R import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentLoginBinding import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.disableView -import org.aossie.agoraandroid.utilities.enableView -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toggleIsEnable +import org.aossie.agoraandroid.ui.screens.auth.login.LoginScreen +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.ForgotPasswordClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.LoginFacebookClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent.OnTwoFactorAuthentication +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent.UserLoggedIn +import org.aossie.agoraandroid.ui.theme.AgoraTheme +import org.aossie.agoraandroid.utilities.navigateSafely import timber.log.Timber import javax.inject.Inject @@ -41,7 +48,7 @@ constructor( private val viewModelFactory: ViewModelProvider.Factory, private val prefs: PreferenceProvider ) : BaseFragment(viewModelFactory) { - lateinit var binding: FragmentLoginBinding + private lateinit var composeView: ComposeView private val loginViewModel: LoginViewModel by viewModels { viewModelFactory @@ -53,56 +60,83 @@ constructor( container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentLoginBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } + @OptIn(ExperimentalMaterial3Api::class) override fun onFragmentInitiated() { + composeView.setContent { - initObjects() + val loginDataState by loginViewModel.loginDataState.collectAsState() + val progressErrorState by loginViewModel.progressAndErrorState.collectAsState() - initListeners() - } + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() - private fun initObjects() { - loginViewModel.sessionExpiredListener = this - lifecycleScope.launch { - loginViewModel.getLoginStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - makeFieldsToggleEnable() + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setStatusBarColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} + } + + LaunchedEffect(key1 = Unit) { + loginViewModel.uiEvents.collectLatest { event -> + when(event) { + UserLoggedIn -> { + findNavController().navigateSafely( + LoginFragmentDirections.actionLoginFragmentToHomeFragment() + ) + } + is OnTwoFactorAuthentication -> { + findNavController().navigateSafely( + LoginFragmentDirections.actionLoginFragmentToTwoFactorAuthFragment(event.crypto) + ) + } + } + } + } + + AgoraTheme { + LoginScreen(loginDataState,progressErrorState) { event -> + when(event){ + ForgotPasswordClick -> { + findNavController() + .navigateSafely(LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()) + } + BackArrowClick ->{ + findNavController().navigateUp() } - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - makeFieldsToggleEnable() - it.message?.let { crypto -> - onTwoFactorAuthentication(crypto) - } ?: kotlin.run { - Navigation.findNavController(binding.root) - .navigate(LoginFragmentDirections.actionLoginFragmentToHomeFragment()) - } + LoginFacebookClick -> { + LoginManager + .getInstance() + .logInWithReadPermissions( + requireActivity(), + callbackManager!!, + listOf("email", "public_profile") + ) } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(it.message) - makeFieldsToggleEnable() - enableBtnFacebook() + else -> { + loginViewModel.onEvent(event) } - else -> {} } } } } + initObjects() + } - callbackManager = Factory.create() - + private fun initObjects() { + loginViewModel.sessionExpiredListener = this + callbackManager = CallbackManager.Factory.create() LoginManager.getInstance() .registerCallback( callbackManager, - object : FacebookCallback { - override fun onSuccess(loginResult: LoginResult?) { + object : FacebookCallback { + override fun onSuccess(loginResult: LoginResult) { Timber.d("Success") lifecycleScope.launch { prefs.setFacebookAccessToken(loginResult?.accessToken?.token) @@ -111,103 +145,14 @@ constructor( } override fun onCancel() { - enableBtnFacebook() - notify(resources.getString(R.string.login_cancelled)) + loginViewModel.showMessage(resources.getString(R.string.login_cancelled)) } override fun onError(exception: FacebookException) { - enableBtnFacebook() notify(exception.message.toString()) + loginViewModel.showMessage(exception.message.toString()) } } ) } - - private fun makeFieldsToggleEnable() { - binding.loginBtn.toggleIsEnable() - binding.username.toggleIsEnable() - binding.password.toggleIsEnable() - } - - private fun initListeners() { - binding.forgotPasswordTv.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(LoginFragmentDirections.actionLoginFragmentToForgotPasswordFragment()) - } - - binding.loginBtn.setOnClickListener { - val userName = binding.loginUserNameTil.editText - ?.text - .toString() - .trim { it <= ' ' } - val userPass = binding.loginPasswordTil.editText - ?.text - .toString() - .trim { it <= ' ' } - HideKeyboard.hideKeyboardInFrag(this) - loginViewModel.logInRequest(userName, userPass) - } - - binding.password.doAfterTextChanged { - updateLoginButton() - } - binding.username.doAfterTextChanged { - updateLoginButton() - } - - binding.btnFacebookLogin.setOnClickListener { - binding.btnFacebookLogin.disableView() - LoginManager.getInstance() - .logInWithReadPermissions( - activity, - listOf("email", "public_profile") - ) - } - } - - private fun updateLoginButton() { - val usernameInput: String = binding.username.text - .toString() - .trim() - val passwordInput: String = binding.password.text - .toString() - .trim() - binding.loginBtn.isEnabled = usernameInput.isNotEmpty() && passwordInput.isNotEmpty() - } - - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent? - ) { - Timber.d("Activity result") - callbackManager!!.onActivityResult(requestCode, resultCode, data) - } - - override fun onSessionExpired() { - // do nothing - } - - private fun enableBtnFacebook() { - binding.btnFacebookLogin.enableView() - } - - fun onTwoFactorAuthentication( - crypto: String - ) { - lifecycleScope.launch { - loginViewModel.getLoggedInUser() - .collect { - if (it != null) { - if (it.twoFactorAuthentication!!) { - notify(getString(R.string.otp_sent)) - val action = - LoginFragmentDirections.actionLoginFragmentToTwoFactorAuthFragment(crypto) - Navigation.findNavController(binding.root) - .navigate(action) - } - } - } - } - } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt index b18a262e..332eaf11 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/login/LoginViewModel.kt @@ -2,15 +2,25 @@ package org.aossie.agoraandroid.ui.fragments.auth.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.data.db.PreferenceProvider import org.aossie.agoraandroid.domain.model.AuthResponseModel import org.aossie.agoraandroid.domain.model.LoginDtoModel import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.domain.useCases.authentication.login.LogInUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent +import org.aossie.agoraandroid.ui.screens.auth.models.LoginModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.NoInternetException @@ -27,6 +37,15 @@ constructor( private val logInUseCases: LogInUseCases ) : ViewModel() { + private val _loginDataState = MutableStateFlow (LoginModel()) + val loginDataState = _loginDataState.asStateFlow() + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + private val _uiEvents = MutableSharedFlow() + val uiEvents = _uiEvents.asSharedFlow() + var sessionExpiredListener: SessionExpiredListener? = null private val _getLoginStateFlow: MutableStateFlow?> = @@ -40,8 +59,11 @@ constructor( password: String, trustedDevice: String? = null ) { + showLoading(R.string.authenticating) _getLoginStateFlow.value = ResponseUI.loading() if (identifier.isEmpty() || password.isEmpty()) { + hideLoading() + showMessage(AppConstants.INVALID_CREDENTIALS_MESSAGE) _getLoginStateFlow.value = ResponseUI.error(AppConstants.INVALID_CREDENTIALS_MESSAGE) return } @@ -62,19 +84,36 @@ constructor( subscribeToFCM(mail) } Timber.d(user.toString()) + hideSnackBar() + hideLoading() if (!it.twoFactorAuthentication!!) { + _uiEvents.emit(LoginUiEvent.UserLoggedIn) _getLoginStateFlow.value = ResponseUI.success() } else { - _getLoginStateFlow.value = ResponseUI.success(user.crypto!!) + logInUseCases.getUser().collectLatest { + it?.let { + it.twoFactorAuthentication?.let { + if(it){ + _uiEvents.emit(LoginUiEvent.OnTwoFactorAuthentication(user.crypto!!)) + _getLoginStateFlow.value = ResponseUI.success(user.crypto!!) + }else{ + showMessage(R.string.login_error) + } + } + } + } } } } catch (e: ApiException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: Exception) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } } @@ -102,6 +141,7 @@ constructor( } fun facebookLogInRequest() { + showLoading(R.string.facebook_login) _getLoginStateFlow.value = ResponseUI.loading() viewModelScope.launch { try { @@ -113,12 +153,15 @@ constructor( } Timber.d(authResponse.toString()) } catch (e: ApiException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: Exception) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } } @@ -137,16 +180,78 @@ constructor( logInUseCases.saveUser(user) Timber.d(authResponse.toString()) prefs.setIsFacebookUser(true) + hideLoading() + hideSnackBar() + _uiEvents.emit(LoginUiEvent.UserLoggedIn) _getLoginStateFlow.value = ResponseUI.success() } catch (e: ApiException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } catch (e: Exception) { + showMessage(e.message!!) _getLoginStateFlow.value = ResponseUI.error(e.message) } } } + + fun onEvent(event: LoginScreenEvent) { + when(event){ + is LoginScreenEvent.EnteredPassword -> { + _loginDataState.value = loginDataState.value.copy( + password = event.password + ) + } + is LoginScreenEvent.EnteredUsername -> { + _loginDataState.value = loginDataState.value.copy( + username = event.username + ) + } + LoginScreenEvent.LoginClick -> { + if(_loginDataState.value.username.isEmpty()){ + showMessage(R.string.invalid_username) + return + } + if(_loginDataState.value.password.isEmpty()){ + showMessage(R.string.invalid_password) + return + } + logInRequest(_loginDataState.value.username, _loginDataState.value.password) + } + else -> {} + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt new file mode 100644 index 00000000..e8b352e3 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/LoginScreen.kt @@ -0,0 +1,135 @@ +package org.aossie.agoraandroid.ui.screens.auth.login + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.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.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.EnteredUsername +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.ForgotPasswordClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.LoginClick +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginScreenEvent.LoginFacebookClick +import org.aossie.agoraandroid.ui.screens.auth.models.LoginModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryLabelTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryPasswordField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginScreen( + loginModel: LoginModel, + screenState: ScreensState, + onEvent:(LoginScreenEvent) -> Unit +) { + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + PrimaryTopAppBar(title = R.string.login) { + onEvent(BackArrowClick) + } + } + ) { + Box( + modifier = Modifier + .padding(it) + .imePadding() + ){ + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 25.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Image( + modifier = Modifier + .fillMaxWidth() + .height(250.dp), + painter = painterResource(id = drawable.tree), + contentDescription = "") + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredUsername(it)) + }, + value = loginModel.username, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.user_name) + ) + } + item { + PrimaryPasswordField( + onPasswordChange = { + onEvent(EnteredPassword(it)) + }, + password = loginModel.password, + label = stringResource(id = string.password) + ) + } + item { + Box(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(id = string.forgot_password), + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .align(Alignment.CenterEnd) + .clickable { + onEvent(ForgotPasswordClick) + }, + ) + } + } + item { + PrimaryButton(text = stringResource(id = string.login)) { + onEvent(LoginClick) + } + } + item { + Text( + text = stringResource(id = string.or), + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + item { + PrimaryButton( + text = stringResource(id = string.continue_with_facebook), + backgroundColor = MaterialTheme.colorScheme.background, + icon = painterResource(id = drawable.ic_facebook_logo) + ) { + onEvent(LoginFacebookClick) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt new file mode 100644 index 00000000..575f043a --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginScreenEvent.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.ui.screens.auth.login.events + +sealed class LoginScreenEvent{ + data class EnteredUsername(val username:String): LoginScreenEvent() + data class EnteredPassword(val password:String): LoginScreenEvent() + object BackArrowClick: LoginScreenEvent() + object LoginClick: LoginScreenEvent() + object LoginFacebookClick: LoginScreenEvent() + object ForgotPasswordClick: LoginScreenEvent() + +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt new file mode 100644 index 00000000..1cb268be --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/login/events/LoginUiEvent.kt @@ -0,0 +1,7 @@ +package org.aossie.agoraandroid.ui.screens.auth.login.events + +sealed class LoginUiEvent { + object UserLoggedIn: LoginUiEvent() + data class OnTwoFactorAuthentication(val crypto: String): LoginUiEvent() + +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt new file mode 100644 index 00000000..ba01fec1 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/LoginModel.kt @@ -0,0 +1,6 @@ +package org.aossie.agoraandroid.ui.screens.auth.models + +data class LoginModel( + val username: String = "", + val password: String = "" +) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryProgressSnackView.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryProgressSnackView.kt new file mode 100644 index 00000000..0624b2f6 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryProgressSnackView.kt @@ -0,0 +1,46 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState + +@Composable +fun PrimaryProgressSnackView( + screenState: ScreensState +) { + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding( + 20.dp + ), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + screenState.message.let { + if (it.second) { + when (val first = it.first) { + is String -> PrimarySnackBar(text = first.toString()) + is Int -> PrimarySnackBar(text = stringResource(id = first)) + } + } + } + screenState.loading.let { + if (it.second) { + when (val first = it.first) { + is String -> PrimaryProgressView(text = first.toString()) + is Int -> PrimaryProgressView(text = stringResource(id = first)) + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt index 8284cf9a..6ef77a7e 100644 --- a/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt @@ -44,5 +44,6 @@ object AppConstants { const val SPOTLIGHT_ANIMATION_DURATION = 500L const val SPOTLIGHT_SCROLL_DURATION = 0L const val GRAPH_ANIMATION_DURATION = 1000 + const val SNACKBAR_DURATION = 2000L const val WELCOME_SCREEN_SCROLL_DELAY = 3000L } diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt index 0812d673..fbedf36e 100644 --- a/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/AppUtils.kt @@ -13,6 +13,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.biometric.BiometricManager import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_WEAK import androidx.datastore.preferences.preferencesDataStore +import androidx.navigation.NavController +import androidx.navigation.NavDirections +import androidx.navigation.NavGraph import com.google.firebase.ktx.Firebase import com.google.firebase.messaging.ktx.messaging import com.takusemba.spotlight.OnSpotlightListener @@ -102,3 +105,18 @@ fun unsubscribeFromFCM(mail: String?) { } } } + +fun NavController.navigateSafely(direction: NavDirections) { + val currentDestination = currentDestination + if (currentDestination != null) { + val navAction = currentDestination.getAction(direction.actionId) + if (navAction != null) { + val destinationId: Int = navAction.destinationId ?: 0 + val currentNode: NavGraph? = + if (currentDestination is NavGraph) currentDestination else currentDestination.parent + if (destinationId != 0 && currentNode != null && currentNode.findNode(destinationId) != null) { + navigate(direction) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/facebook_logo.xml b/app/src/main/res/drawable/facebook_logo.xml deleted file mode 100644 index f2908cc6..00000000 --- a/app/src/main/res/drawable/facebook_logo.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_facebook_logo.xml b/app/src/main/res/drawable/ic_facebook_logo.xml new file mode 100644 index 00000000..eb931903 --- /dev/null +++ b/app/src/main/res/drawable/ic_facebook_logo.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml deleted file mode 100644 index fba9b0cf..00000000 --- a/app/src/main/res/layout/fragment_login.xml +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index da8b9856..2acfdc4e 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -34,8 +34,7 @@ + android:label="Login"> Agora Vote - CONTINUE WITH FACEBOOK + Continue With Facebook LOG IN Sign Up Agora Logo @@ -102,6 +102,7 @@ Welcome, %1$s! Link Sent, Please Check Your Emails Invalid Username + Invalid Password Something Went Wrong. Please Try Again Later Connection Established Successfully Completely secret and never shown to anyone @@ -325,5 +326,9 @@ Bar Chart Pie Chart Start time is smaller than Current time + NewActivity + Authenticating... + Facebook Login... + Login error diff --git a/build.gradle b/build.gradle index 29a6e928..c1313304 100644 --- a/build.gradle +++ b/build.gradle @@ -25,8 +25,7 @@ buildscript { navFragmentVersion = '2.5.3' navUiVersion = '2.5.3' facebookShimmerVersion = '0.5.0' - facebookLoginVersion = '8.2.0' - facebookAndroidSDK = '8.2.0' + facebookAndroidSDK = '16.0.0' robolectricVersion = '4.10.3' mockWebServerVersion = '4.11.0' mockitoCoreVersion = '5.3.1' From cac5822186e91f9c4e1c43ebf68e353b06abce1a Mon Sep 17 00:00:00 2001 From: Narendra Singh Anjana Date: Fri, 25 Aug 2023 18:53:13 +0530 Subject: [PATCH 02/13] Gsoc 2023 signup screen compose migrartion (#17) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * SignUp fragment migration to jetpack compose --- .../fragments/auth/signup/SignUpFragment.kt | 184 +++---------- .../fragments/auth/signup/SignUpViewModel.kt | 135 +++++++++- .../ui/screens/auth/models/SignUpModel.kt | 11 + .../ui/screens/auth/signup/SignUpScreen.kt | 154 +++++++++++ .../auth/signup/events/SignUpScreenEvent.kt | 14 + app/src/main/res/layout/fragment_sign_up.xml | 253 ------------------ app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/strings.xml | 2 + 8 files changed, 341 insertions(+), 415 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt delete mode 100644 app/src/main/res/layout/fragment_sign_up.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt index dc3611d5..bcdd570c 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpFragment.kt @@ -1,31 +1,24 @@ package org.aossie.agoraandroid.ui.fragments.auth.signup import android.os.Bundle -import android.util.Patterns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.AdapterView -import android.widget.AdapterView.OnItemSelectedListener -import android.widget.ArrayAdapter -import androidx.appcompat.app.AppCompatActivity -import androidx.core.widget.doAfterTextChanged +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.array -import org.aossie.agoraandroid.R.string -import org.aossie.agoraandroid.data.network.dto.SecurityQuestionDto -import org.aossie.agoraandroid.databinding.FragmentSignUpBinding -import org.aossie.agoraandroid.domain.model.NewUserDtoModel +import androidx.navigation.fragment.findNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toggleIsEnable +import org.aossie.agoraandroid.ui.screens.auth.signup.SignUpScreen +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -37,162 +30,53 @@ constructor( private val viewModelFactory: ViewModelProvider.Factory ) : BaseFragment(viewModelFactory) { - private var securityQuestionOfSignUp: String? = null - private val signUpViewModel: SignUpViewModel by viewModels { viewModelFactory } - private lateinit var binding: FragmentSignUpBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSignUpBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - signUpViewModel.sessionExpiredListener = this - - binding.signupBtn.setOnClickListener { - HideKeyboard.hideKeyboardInActivity(activity as AppCompatActivity) - validateAllFields() - } - - val adapter = ArrayAdapter.createFromResource( - requireContext(), array.security_questions, - android.R.layout.simple_spinner_item - ) - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) + composeView.setContent { - binding.signUpSecurityQuestion.adapter = adapter + val signUpDataState by signUpViewModel.signUpDataState.collectAsState() + val progressErrorState by signUpViewModel.progressAndErrorState.collectAsState() - binding.signUpSecurityQuestion.onItemSelectedListener = object : OnItemSelectedListener { - override fun onItemSelected( - adapterView: AdapterView<*>, - view: View?, - i: Int, - l: Long - ) { - securityQuestionOfSignUp = adapterView.getItemAtPosition(i) - .toString() - } + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() - override fun onNothingSelected(adapterView: AdapterView<*>?) { - securityQuestionOfSignUp = resources.getStringArray(array.security_questions)[0] + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setStatusBarColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} } - } - - // Attach text watcher to different text input edit texts - binding.etUsername.doAfterTextChanged { doAfterTextChange() } - binding.etFirstName.doAfterTextChanged { doAfterTextChange() } - binding.etLastName.doAfterTextChanged { doAfterTextChange() } - binding.etEmail.doAfterTextChanged { doAfterTextChange() } - binding.etAnswer.doAfterTextChanged { doAfterTextChange() } - binding.etPassword.doAfterTextChanged { doAfterTextChange() } - - lifecycleScope.launch { - signUpViewModel.getSignUpStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - makeFieldsToggleEnable() - } - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - notify(getString(string.verify_account)) - makeFieldsToggleEnable() + AgoraTheme { + SignUpScreen(signUpDataState,progressErrorState) { event -> + when(event) { + BackArrowClick -> { + findNavController().navigateUp() } - ResponseUI.Status.ERROR -> { - notify(it.message) - binding.progressBar.hide() - makeFieldsToggleEnable() + else -> { + signUpViewModel.onEvent(event) } - else -> {} } } } } - } - - private fun makeFieldsToggleEnable() { - binding.signupBtn.toggleIsEnable() - binding.etUsername.toggleIsEnable() - binding.etFirstName.toggleIsEnable() - binding.etLastName.toggleIsEnable() - binding.etEmail.toggleIsEnable() - binding.etPassword.toggleIsEnable() - binding.signUpSecurityQuestion.toggleIsEnable() - binding.securityAnswer.toggleIsEnable() - } - - private fun validateAllFields() { - val userName = binding.signupUserName.editText - ?.text - .toString() - val firstName = binding.signupFirstName.editText - ?.text - .toString() - val lastName = binding.signupLastName.editText - ?.text - .toString() - val userEmail = binding.signupEmail.editText - ?.text - .toString() - val userPass = binding.signupPassword.editText - ?.text - .toString() - val securityQuestionAnswer = binding.securityAnswer.editText - ?.text - .toString() - val securityQuestion = securityQuestionOfSignUp - if (!Patterns.EMAIL_ADDRESS.matcher(userEmail) - .matches() - ) { - binding.signupEmail.error = "Enter a valid email address!!!" - } else if (userPass.length < 6) { - binding.signupPassword.error = "password length must be atleast 6 !!!" - } else { - binding.signupEmail.error = null - binding.signupPassword.error = null - signUpViewModel.signUpRequest( - NewUserDtoModel( - userEmail, firstName, userName, lastName, userPass, - SecurityQuestionDto(securityQuestionAnswer, "", securityQuestion!!) - ) - ) - } - } - private fun doAfterTextChange() { - val usernameInput: String = binding.etUsername.text - .toString() - .trim() - val passwordInput: String = binding.etPassword.text - .toString() - .trim() - val firstNameInput: String = binding.etFirstName.text - .toString() - .trim() - val lastNameInput: String = binding.etLastName.text - .toString() - .trim() - val emailInput: String = binding.etEmail.text - .toString() - .trim() - val answerInput: String = binding.etAnswer.text - .toString() - .trim() - binding.signupBtn.isEnabled = usernameInput.isNotEmpty() && - passwordInput.isNotEmpty() && - firstNameInput.isNotEmpty() && - lastNameInput.isNotEmpty() && - emailInput.isNotEmpty() && - answerInput.isNotEmpty() + signUpViewModel.sessionExpiredListener = this } override fun onSessionExpired() { diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt index 8e94e369..ef5659ca 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/signup/SignUpViewModel.kt @@ -1,17 +1,31 @@ package org.aossie.agoraandroid.ui.fragments.auth.signup +import android.util.Patterns import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.data.network.dto.SecurityQuestionDto import org.aossie.agoraandroid.domain.model.NewUserDtoModel import org.aossie.agoraandroid.domain.useCases.authentication.signUp.SignUpUseCase import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.auth.models.SignUpModel +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredEmail +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredFirstName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredLastName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredSecurityAnswer +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredUsername +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SelectedSecurityQuestion +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SignUpClICK +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException import timber.log.Timber import javax.inject.Inject @@ -22,31 +36,132 @@ constructor( private val signUpUseCase: SignUpUseCase ) : ViewModel() { + private val _signUpDataState = MutableStateFlow (SignUpModel()) + val signUpDataState = _signUpDataState.asStateFlow() + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + lateinit var sessionExpiredListener: SessionExpiredListener - private val _getSignUpStateFlow: MutableStateFlow?> = MutableStateFlow(null) - val getSignUpStateFlow = _getSignUpStateFlow.asStateFlow() - fun signUpRequest( + private fun signUpRequest( userDataModel: NewUserDtoModel ) { - _getSignUpStateFlow.value = ResponseUI.loading() + showLoading("Creating account...") viewModelScope.launch { try { val call = signUpUseCase(userDataModel) Timber.d(call) - _getSignUpStateFlow.value = ResponseUI.success() + showMessage(R.string.verify_account) + hideLoading() } catch (e: ApiException) { + hideLoading() if (e.message == "409") { - _getSignUpStateFlow.value = ResponseUI.error(AppConstants.USER_ALREADY_FOUND_MESSAGE) + showMessage(AppConstants.USER_ALREADY_FOUND_MESSAGE) } else { - _getSignUpStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } catch (e: SessionExpirationException) { + hideLoading() sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - _getSignUpStateFlow.value = ResponseUI.error(e.message) + hideLoading() + showMessage(e.message!!) } catch (e: Exception) { - _getSignUpStateFlow.value = ResponseUI.error(e.message) + hideLoading() + showMessage(e.message!!) + } + } + } + + fun onEvent(event: SignUpScreenEvent) { + when(event){ + is EnteredPassword -> { + _signUpDataState.value = signUpDataState.value.copy( + password = event.password + ) + } + is EnteredUsername -> { + _signUpDataState.value = signUpDataState.value.copy( + username = event.username + ) + } + SignUpClICK -> { + validateFields(_signUpDataState.value) + } + is EnteredEmail -> { + _signUpDataState.value = signUpDataState.value.copy( + email = event.email + ) + } + is EnteredFirstName -> { + _signUpDataState.value = signUpDataState.value.copy( + firstName = event.firstName + ) + } + is EnteredLastName -> { + _signUpDataState.value = signUpDataState.value.copy( + lastName = event.lastName + ) + } + is EnteredSecurityAnswer -> { + _signUpDataState.value = signUpDataState.value.copy( + securityAnswer = event.answer + ) + } + is SelectedSecurityQuestion -> { + _signUpDataState.value = signUpDataState.value.copy( + securityQuestion = event.question + ) } + else -> {} } } + + private fun validateFields(value: SignUpModel) { + if(value.username.trim().isEmpty()){ + showMessage(R.string.invalid_username) + } else if (!Patterns.EMAIL_ADDRESS.matcher(value.email.trim()) + .matches() + ) { + showMessage("Enter a valid email address!!!") + } else if (value.password.trim().length < 6) { + showMessage("password length must be atleast 6 !!!") + } else { + signUpRequest( + NewUserDtoModel( + value.email.trim(), value.firstName.trim(), value.username.trim(), value.lastName.trim(), value.password.trim(), + SecurityQuestionDto(value.securityAnswer.trim(), "", value.securityQuestion!!) + ) + ) + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt new file mode 100644 index 00000000..98745a7c --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/models/SignUpModel.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.ui.screens.auth.models + +data class SignUpModel( + val username: String = "", + val firstName: String = "", + val lastName: String = "", + val email: String = "", + val password: String = "", + val securityQuestion: String = "", + val securityAnswer: String = "", +) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt new file mode 100644 index 00000000..52502980 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/SignUpScreen.kt @@ -0,0 +1,154 @@ +package org.aossie.agoraandroid.ui.screens.auth.signup + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.models.SignUpModel +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.BackArrowClick +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredEmail +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredFirstName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredLastName +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredSecurityAnswer +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.EnteredUsername +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SelectedSecurityQuestion +import org.aossie.agoraandroid.ui.screens.auth.signup.events.SignUpScreenEvent.SignUpClICK +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryLabelTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryPasswordField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryTitledSpinner +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SignUpScreen( + signUpModel: SignUpModel, + screenState: ScreensState, + onEvent:(SignUpScreenEvent) -> Unit +){ + + val securityQuestionsList = stringArrayResource(id = R.array.security_questions).toList() + + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + PrimaryTopAppBar(title = R.string.create_account) { + onEvent(BackArrowClick) + } + } + ) { + Box( + modifier = Modifier + .padding(it) + .imePadding() + ){ + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 25.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredUsername(it)) + }, + value = signUpModel.username, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.user_name) + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredFirstName(it)) + }, + value = signUpModel.firstName, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.first_name) + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredLastName(it)) + }, + value = signUpModel.lastName, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.last_name) + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredEmail(it)) + }, + value = signUpModel.email, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.email_address), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next, keyboardType = KeyboardType.Email) + ) + } + item { + PrimaryPasswordField( + onPasswordChange = { + onEvent(EnteredPassword(it)) + }, + password = signUpModel.password, + label = stringResource(id = string.password) + ) + } + item { + PrimaryTitledSpinner( + title = stringResource(id = string.secret_security_question), + list = securityQuestionsList, + selectedIndex = if (signUpModel.securityQuestion.isNotEmpty()) securityQuestionsList.indexOf( + signUpModel.securityQuestion + ) else 0, + onItemSelected = { + onEvent(SelectedSecurityQuestion(securityQuestionsList[it])) + } + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(EnteredSecurityAnswer(it)) + }, + value = signUpModel.securityAnswer, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.answer) + ) + } + item { + PrimaryButton(text = stringResource(id = string.sign_up)) { + onEvent(SignUpClICK) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt new file mode 100644 index 00000000..f9c65906 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/signup/events/SignUpScreenEvent.kt @@ -0,0 +1,14 @@ +package org.aossie.agoraandroid.ui.screens.auth.signup.events + +sealed class SignUpScreenEvent{ + data class EnteredUsername(val username:String): SignUpScreenEvent() + data class EnteredFirstName(val firstName:String): SignUpScreenEvent() + data class EnteredLastName(val lastName:String): SignUpScreenEvent() + data class EnteredEmail(val email:String): SignUpScreenEvent() + data class EnteredPassword(val password:String): SignUpScreenEvent() + data class SelectedSecurityQuestion(val question:String): SignUpScreenEvent() + data class EnteredSecurityAnswer(val answer:String): SignUpScreenEvent() + object BackArrowClick: SignUpScreenEvent() + object SignUpClICK: SignUpScreenEvent() + +} diff --git a/app/src/main/res/layout/fragment_sign_up.xml b/app/src/main/res/layout/fragment_sign_up.xml deleted file mode 100644 index 1fff5ae2..00000000 --- a/app/src/main/res/layout/fragment_sign_up.xml +++ /dev/null @@ -1,253 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 2acfdc4e..153f83e1 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -60,8 +60,7 @@ + android:label="Sing Up" /> Continue With Facebook LOG IN Sign Up + Create Account Agora Logo User Name Password @@ -97,6 +98,7 @@ Swipe to delete Candidate Enter Details of Voters Winner Name + Answer Please Write Your Answer Here Security Question Welcome, %1$s! From a51bfd90a2666fd4f0b31f0d59580e16b52f8fae Mon Sep 17 00:00:00 2001 From: Narendra Singh Anjana Date: Tue, 29 Aug 2023 22:05:15 +0530 Subject: [PATCH 03/13] Gsoc 2023 forgot password screen migration (#63) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Forgot Password Screen Migration to jetpack compose --- app/build.gradle | 1 - .../data/Repository/UserRepositoryImpl.kt | 2 +- .../agoraandroid/data/network/api/Api.kt | 2 +- .../domain/repository/UserRepository.kt | 2 +- .../SendForgotPasswordLinkUseCase.kt | 2 +- .../forgotpassword/ForgotPasswordFragment.kt | 70 +++++----- .../forgotpassword/ForgotPasswordViewModel.kt | 84 ++++++++++-- .../forgotPassword/ForgotPasswordScreen.kt | 93 ++++++++++++++ .../ForgotPasswordScreenEvents.kt | 8 ++ .../res/layout/fragment_forgot_password.xml | 120 ------------------ app/src/main/res/navigation/nav_graph.xml | 3 +- build.gradle | 2 +- 12 files changed, 214 insertions(+), 175 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt delete mode 100644 app/src/main/res/layout/fragment_forgot_password.xml diff --git a/app/build.gradle b/app/build.gradle index 154cc2ed..6dabdc4c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,7 +106,6 @@ dependencies { testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test:runner:$testRunnerVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoCoreVersion" - // Lifecycle components implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleRuntime" implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleExtensions" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" diff --git a/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt b/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt index dfee67b4..aecfab10 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/Repository/UserRepositoryImpl.kt @@ -79,7 +79,7 @@ class UserRepositoryImpl( .deleteAllElections() } - override suspend fun sendForgotPasswordLink(username: String?): String { + override suspend fun sendForgotPasswordLink(username: String?): List { return apiRequest { api.sendForgotPassword(username) } } diff --git a/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt b/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt index b09313bd..d1b82280 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/network/api/Api.kt @@ -39,7 +39,7 @@ interface Api { suspend fun resendOTP(@Path("userName") userName: String?): Response @POST("auth/forgotPassword/send/{userName}") - suspend fun sendForgotPassword(@Path("userName") userName: String?): Response + suspend fun sendForgotPassword(@Path("userName") userName: String?): Response> @GET("election") suspend fun getAllElections(): Response diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt b/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt index 8fb9e129..44a8eb33 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/repository/UserRepository.kt @@ -19,7 +19,7 @@ interface UserRepository { suspend fun logout() fun getUser(): Flow suspend fun deleteUser() - suspend fun sendForgotPasswordLink(username: String?): String + suspend fun sendForgotPasswordLink(username: String?): List suspend fun updateUser(updateUserData: UpdateUserDto): List suspend fun changeAvatar(url: String): List suspend fun changePassword(password: String): List diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt index 251c52cf..090fc7f5 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/authentication/forgotPassword/SendForgotPasswordLinkUseCase.kt @@ -8,7 +8,7 @@ class SendForgotPasswordLinkUseCase @Inject constructor( ) { suspend operator fun invoke( username: String? - ): String { + ): List { return repository.sendForgotPasswordLink(username) } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt index cb69727c..fd8f6218 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordFragment.kt @@ -4,19 +4,21 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R -import org.aossie.agoraandroid.databinding.FragmentForgotPasswordBinding +import androidx.navigation.fragment.findNavController +import com.google.accompanist.systemuicontroller.rememberSystemUiController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreen +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnBackIconClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -31,44 +33,44 @@ constructor( private val forgotPasswordViewModel: ForgotPasswordViewModel by viewModels { viewModelFactory } - private lateinit var binding: FragmentForgotPasswordBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentForgotPasswordBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - binding.buttonSendLink.setOnClickListener { - val userName = binding.editTextUserName.editText?.text.toString().trim() - if (userName.isEmpty()) { - notify("Please Enter User Name") - } else { - HideKeyboard.hideKeyboardInActivity(activity as AppCompatActivity) - binding.progressBar.show() - forgotPasswordViewModel.sendResetLink(userName) - } - } + composeView.setContent { - lifecycleScope.launch { - forgotPasswordViewModel.getSendResetLinkStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> binding.progressBar.show() - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - notify(getString(R.string.link_sent_please_check_your_email)) + val userNameState by forgotPasswordViewModel.userNameState.collectAsState() + val progressErrorState by forgotPasswordViewModel.progressAndErrorState.collectAsState() + + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setStatusBarColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} + } + AgoraTheme { + ForgotPasswordScreen(progressErrorState,userNameState) { event -> + when(event){ + OnBackIconClick -> { + findNavController().navigateUp() } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(it.message) + else -> { + forgotPasswordViewModel.onEvent(event) } - else -> {} } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt index 6c046235..c4c5ef25 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/forgotpassword/ForgotPasswordViewModel.kt @@ -2,14 +2,20 @@ package org.aossie.agoraandroid.ui.fragments.auth.forgotpassword import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.domain.useCases.authentication.forgotPassword.SendForgotPasswordLinkUseCase +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnSendLinkClick +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnUserNameEntered +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.SnackBarActionClick +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import javax.inject.Inject class ForgotPasswordViewModel @@ -18,26 +24,78 @@ constructor( private val sendForgotPasswordLinkUseCase: SendForgotPasswordLinkUseCase ) : ViewModel() { - private val _getSendResetLinkStateFlow: MutableStateFlow?> = - MutableStateFlow(null) - val getSendResetLinkStateFlow: StateFlow?> = - _getSendResetLinkStateFlow + private val _userNameState = MutableStateFlow ("") + val userNameState = _userNameState.asStateFlow() - fun sendResetLink(userName: String?) = viewModelScope.launch { - _getSendResetLinkStateFlow.value = ResponseUI.loading() + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + + private fun sendResetLink(userName: String?) = viewModelScope.launch { + showLoading("Sending link...") try { sendForgotPasswordLinkUseCase(userName) - _getSendResetLinkStateFlow.value = ResponseUI.success() + hideLoading() + showMessage(R.string.link_sent_please_check_your_email) } catch (e: ApiException) { if (e.message == "412") { - _getSendResetLinkStateFlow.value = ResponseUI.error(AppConstants.INVALID_USERNAME_MESSAGE) + showMessage(AppConstants.INVALID_USERNAME_MESSAGE) } else { - _getSendResetLinkStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } catch (e: NoInternetException) { - _getSendResetLinkStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _getSendResetLinkStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } + + fun onEvent(forgotPasswordScreenEvents: ForgotPasswordScreenEvents){ + when(forgotPasswordScreenEvents){ + OnSendLinkClick -> { + val userName = _userNameState.value.trim() + if (userName.isEmpty()) { + showMessage("Please Enter User Name") + } else { + sendResetLink(userName) + } + } + is OnUserNameEntered -> { + _userNameState.value = forgotPasswordScreenEvents.userName + } + SnackBarActionClick -> { + hideSnackBar() + } + else -> {} + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt new file mode 100644 index 00000000..faa9d464 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreen.kt @@ -0,0 +1,93 @@ +package org.aossie.agoraandroid.ui.screens.auth.forgotPassword + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnBackIconClick +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnSendLinkClick +import org.aossie.agoraandroid.ui.screens.auth.forgotPassword.ForgotPasswordScreenEvents.OnUserNameEntered +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryLabelTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryTopAppBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForgotPasswordScreen( + screenState: ScreensState, + userNameState:String, + onEvent:(ForgotPasswordScreenEvents) -> Unit, +){ + Scaffold( + modifier = Modifier.fillMaxSize(), + containerColor = MaterialTheme.colorScheme.background, + topBar = { + PrimaryTopAppBar(title = R.string.forgot_password) { + onEvent(OnBackIconClick) + } + } + ) { + Box( + modifier = Modifier + .padding(it) + .imePadding() + ){ + LazyColumn( + modifier = Modifier + .fillMaxSize(), + contentPadding = PaddingValues(horizontal = 25.dp, vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Image( + modifier = Modifier + .fillMaxWidth() + .height(250.dp), + painter = painterResource(id = R.drawable.annoyed), + contentDescription = "") + } + item { + Text( + text = stringResource(id = R.string.enter_user_name_reset_pass_link), + style = MaterialTheme.typography.titleLarge + ) + } + item { + PrimaryLabelTextField( + onValueChange = { + onEvent(OnUserNameEntered(it)) + }, + value = userNameState, + modifier = Modifier.fillMaxWidth(), + label = stringResource(id = string.user_name) + ) + } + item { + PrimaryButton(text = stringResource(id = string.send_link)) { + onEvent(OnSendLinkClick) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt new file mode 100644 index 00000000..c3c71e9f --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/auth/forgotPassword/ForgotPasswordScreenEvents.kt @@ -0,0 +1,8 @@ +package org.aossie.agoraandroid.ui.screens.auth.forgotPassword + +sealed class ForgotPasswordScreenEvents{ + object OnBackIconClick:ForgotPasswordScreenEvents() + object OnSendLinkClick:ForgotPasswordScreenEvents() + object SnackBarActionClick:ForgotPasswordScreenEvents() + data class OnUserNameEntered(val userName:String):ForgotPasswordScreenEvents() +} \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_forgot_password.xml b/app/src/main/res/layout/fragment_forgot_password.xml deleted file mode 100644 index 2221f924..00000000 --- a/app/src/main/res/layout/fragment_forgot_password.xml +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 153f83e1..318d88ae 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -64,8 +64,7 @@ + android:label="Forgot Password" /> Date: Wed, 30 Aug 2023 01:39:52 +0530 Subject: [PATCH 04/13] Gsoc 2023 migrating two factor auth fragment to compose (#19) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_two_factor_auth.xml to TwoFactorAuthScreen.kt in compose --- .../ui/activities/main/MainActivity.kt | 4 + .../TwoFactorAuthFragment.kt | 124 ++++-------- .../TwoFactorAuthViewModel.kt | 114 ++++++++--- .../screens/common/component/OtpTextField.kt | 95 +++++++++ .../twoFactorAuth/TwoFactorAuthScreen.kt | 180 ++++++++++++++++++ .../twoFactorAuth/TwoFactorAuthScreenEvent.kt | 7 + .../res/layout/fragment_two_factor_auth.xml | 140 -------------- app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/strings.xml | 3 + 9 files changed, 411 insertions(+), 259 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt delete mode 100644 app/src/main/res/layout/fragment_two_factor_auth.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt b/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt index 50622c39..9c22d8e1 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/activities/main/MainActivity.kt @@ -173,6 +173,10 @@ class MainActivity : AppCompatActivity() { -> { supportActionBar?.hide() } + R.id.twoFactorAuthFragment + -> { + supportActionBar?.hide() + } else -> { window.clearFlags(LayoutParams.FLAG_TRANSLUCENT_STATUS) supportActionBar?.show() diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt index dc5268e1..9851bece 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthFragment.kt @@ -4,20 +4,21 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.string -import org.aossie.agoraandroid.databinding.FragmentTwoFactorAuthBinding -import org.aossie.agoraandroid.domain.model.UserModel +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.flow.collectLatest import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.HideKeyboard -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.fragments.auth.twoFactorAuthentication.TwoFactorAuthViewModel.UiEvents.TwoFactorAuthComplete +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreen +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.OnBackClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.ResendOtpClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.VerifyOtpClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme +import org.aossie.agoraandroid.utilities.navigateSafely import javax.inject.Inject class TwoFactorAuthFragment @@ -27,102 +28,49 @@ constructor( ) : BaseFragment(viewModelFactory) { private var crypto: String? = null - private var user: UserModel? = null private val viewModel: TwoFactorAuthViewModel by viewModels { viewModelFactory } - private lateinit var binding: FragmentTwoFactorAuthBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentTwoFactorAuthBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { crypto = TwoFactorAuthFragmentArgs.fromBundle(requireArguments()).crypto viewModel.sessionExpiredListener = this - lifecycleScope.launch { - viewModel.user.collect { - if (it != null) { - user = it + composeView.setContent { + val progressErrorState by viewModel.progressAndErrorState + LaunchedEffect(key1 = viewModel) { + viewModel.uiEventsFlow.collectLatest { + when(it) { + TwoFactorAuthComplete -> { + findNavController() + .navigateSafely(TwoFactorAuthFragmentDirections.actionTwoFactorAuthFragmentToHomeFragment()) + } + } } } - } - - binding.btnVerifyOtp.setOnClickListener { - binding.progressBar.show() - val otp = binding.otpTil.editText - ?.text - .toString() - .trim { it <= ' ' } - if (otp.isEmpty()) { - notify(getString(string.enter_otp)) - binding.progressBar.hide() - } else { - HideKeyboard.hideKeyboardInActivity(activity as AppCompatActivity) - if (binding.cbTrustedDevice.isChecked) { - viewModel.verifyOTP( - otp, binding.cbTrustedDevice.isChecked, user!!.crypto!! - ) - } else { - binding.progressBar.hide() - notify(getString(string.tap_on_checkbox)) + AgoraTheme { + TwoFactorAuthScreen( + progressErrorState = progressErrorState + ) {event -> + when(event) { + ResendOtpClick -> viewModel.resendOTP() + is VerifyOtpClick -> viewModel.verifyOTP(event.otp, event.trustedDevice) + OnBackClick -> findNavController().navigateUp() + } } } } - - binding.tvResendOtp.setOnClickListener { - if (user != null) { - binding.progressBar.show() - viewModel.resendOTP(user!!.username!!) - } else { - notify(getString(string.something_went_wrong_please_try_again_later)) - } - } - - lifecycleScope.launch { - viewModel.verifyOtpResponse.collect { - handleVerifyOtp(it) - } - } - - lifecycleScope.launch { - viewModel.resendOtpResponse.collect { - handleResendOtp(it) - } - } - } - - private fun handleVerifyOtp(response: ResponseUI?) = when (response?.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - Navigation.findNavController(binding.root) - .navigate(TwoFactorAuthFragmentDirections.actionTwoFactorAuthFragmentToHomeFragment()) - } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(response?.message) - } - else -> { // Do Nothing - } - } - - private fun handleResendOtp(response: ResponseUI?) = when (response?.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - notify(getString(string.otp_sent)) - } - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(response?.message) - } - else -> { // Do Nothing - } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt index 61649d1c..8ed80515 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/auth/twoFactorAuthentication/TwoFactorAuthViewModel.kt @@ -1,18 +1,24 @@ package org.aossie.agoraandroid.ui.fragments.auth.twoFactorAuthentication +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.domain.model.VerifyOtpDtoModel import org.aossie.agoraandroid.domain.useCases.authentication.twoFactorAuthentication.TwoFactorAuthUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException import timber.log.Timber import javax.inject.Inject @@ -24,31 +30,43 @@ constructor( ) : ViewModel() { lateinit var sessionExpiredListener: SessionExpiredListener - val user = twoFactorAuthUseCases.getUser() + private var user:UserModel? = null - private val mVerifyOtpResponse = MutableStateFlow?>(null) + private val _uiEventsFlow = MutableSharedFlow() + val uiEventsFlow = _uiEventsFlow.asSharedFlow() - val verifyOtpResponse: StateFlow?> - get() = mVerifyOtpResponse + private val _progressAndErrorState = mutableStateOf(ScreensState()) + val progressAndErrorState: State = _progressAndErrorState - private val mResendOtpResponse = MutableStateFlow?>(null) - - val resendOtpResponse: StateFlow?> - get() = mResendOtpResponse + init { + viewModelScope.launch { + twoFactorAuthUseCases.getUser().collectLatest { + user = it + } + } + } fun verifyOTP( otp: String, - trustedDevice: Boolean, - crypto: String + trustedDevice: Boolean ) { if (otp.isEmpty()) { - mVerifyOtpResponse.value = ResponseUI.error(AppConstants.INVALID_OTP_MESSAGE) + showMessage(string.enter_otp) + return + } + if(!trustedDevice) { + showMessage(R.string.tap_on_checkbox) + return + } + if(user == null) { + showMessage(R.string.something_went_wrong_please_try_again_later) return } viewModelScope.launch { + showLoading(R.string.verifying_otp) try { val authResponse = - twoFactorAuthUseCases.verifyOTP(VerifyOtpDtoModel(crypto, otp, trustedDevice)) + twoFactorAuthUseCases.verifyOTP(VerifyOtpDtoModel(user!!.crypto, otp, trustedDevice)) authResponse.let { val user = UserModel( it.username, it.email, it.firstName, it.lastName, it.avatarURL, it.crypto, @@ -58,28 +76,33 @@ constructor( ) twoFactorAuthUseCases.saveUser(user) Timber.d(user.toString()) - mVerifyOtpResponse.value = ResponseUI.success() + hideLoading() + _uiEventsFlow.emit(UiEvents.TwoFactorAuthComplete) } } catch (e: ApiException) { - mVerifyOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - mVerifyOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - mVerifyOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } - fun resendOTP( - username: String, - ) { - if (username.isEmpty()) { - mResendOtpResponse.value = ResponseUI.error(AppConstants.LOGIN_AGAIN_MESSAGE) + fun resendOTP() { + if(user == null) { + showMessage(R.string.something_went_wrong_please_try_again_later) + return + } + val username = user!!.username + if (username!!.isEmpty()) { + showMessage(AppConstants.LOGIN_AGAIN_MESSAGE) return } viewModelScope.launch { + showLoading(R.string.sending_otp) try { val authResponse = twoFactorAuthUseCases.resendOTP(username) authResponse.let { @@ -90,17 +113,50 @@ constructor( it.refreshToken?.expiresOn, it.trustedDevice ) twoFactorAuthUseCases.saveUser(user) - mResendOtpResponse.value = ResponseUI.success() + showMessage(R.string.otp_sent) } } catch (e: ApiException) { - mResendOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - mResendOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - mResendOtpResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } -} + + private fun showLoading(message: Any) { + _progressAndErrorState.value=progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + private fun showMessage(message: Any) { + _progressAndErrorState.value=progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value=progressAndErrorState.value.copy( + message = Pair("",false), + ) + } + + private fun hideLoading() { + _progressAndErrorState.value=progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } + + sealed class UiEvents{ + object TwoFactorAuthComplete:UiEvents() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt new file mode 100644 index 00000000..22de63cb --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/OtpTextField.kt @@ -0,0 +1,95 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun OtpTextField( + modifier: Modifier = Modifier, + otpText: String, + otpCount: Int = 6, + onOtpTextChange: (String, Boolean) -> Unit +) { + LaunchedEffect(Unit) { + if (otpText.length > otpCount) { + throw IllegalArgumentException("Otp text value must not have more than otpCount: $otpCount characters") + } + } + + BasicTextField( + modifier = modifier, + maxLines = 1, + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), + onValueChange = { + if (it.text.length <= otpCount) { + onOtpTextChange.invoke(it.text, it.text.length == otpCount) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.NumberPassword), + decorationBox = { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly + ) { + repeat(otpCount) { index -> + CharView( + index = index, + text = otpText + ) + } + } + } + ) +} + +@Composable +private fun CharView( + index: Int, + text: String +) { + val isFocused = text.length == index + val char = when { + index == text.length -> "0" + index > text.length -> "" + else -> text[index].toString() + } + Text( + modifier = Modifier + .width(40.dp) + .border( + shape = RoundedCornerShape(8.dp), + color = if (isFocused) { + MaterialTheme.colorScheme.outlineVariant + } else { + MaterialTheme.colorScheme.outline + }, + width = 1.dp + ) + .padding(2.dp), + text = char, + style = MaterialTheme.typography.headlineMedium, + color = if (isFocused) { + MaterialTheme.colorScheme.outlineVariant + } else { + MaterialTheme.colorScheme.outline + }, + textAlign = TextAlign.Center + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt new file mode 100644 index 00000000..803d7c7e --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreen.kt @@ -0,0 +1,180 @@ +package org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowBackIos +import androidx.compose.material3.Checkbox +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import com.google.accompanist.systemuicontroller.rememberSystemUiController +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.OnBackClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.ResendOtpClick +import org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth.TwoFactorAuthScreenEvent.VerifyOtpClick +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.OtpTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun TwoFactorAuthScreen( + progressErrorState: ScreensState, + onEvent: (TwoFactorAuthScreenEvent) -> Unit +) { + + var otp by remember { + mutableStateOf("") + } + var trustDevice by remember { + mutableStateOf(false) + } + val systemUiController = rememberSystemUiController() + val useDarkIcons = !isSystemInDarkTheme() + + DisposableEffect(systemUiController, useDarkIcons) { + systemUiController.setSystemBarsColor( + color = Color.Transparent, + darkIcons = useDarkIcons + ) + onDispose {} + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = { + onEvent(OnBackClick) + }) { + Icon( + imageVector = Icons.Rounded.ArrowBackIos, + contentDescription = "" + ) + } + }, + title = {}, + colors = TopAppBarDefaults.largeTopAppBarColors( + navigationIconContentColor = MaterialTheme.colorScheme.onBackground, + containerColor = MaterialTheme.colorScheme.background + ) + ) + } + ) { + + Box(modifier = Modifier.padding(it)) { + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Text( + text = stringResource(id = string.two_factor_authentication), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.SemiBold + ) + } + item { + Text( + text = stringResource(id = string.please_enter_the_one_time_password_we_have_sent_to_your_registered_email_address), + style = MaterialTheme.typography.bodyMedium + ) + } + + item { + OtpTextField( + otpText = otp, + onOtpTextChange = { value, otpInputFilled -> + otp = value + } + ) + } + + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.didnt_receive_otp), + style = MaterialTheme.typography.bodyLarge + ) + ClickableText( + text = buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.inversePrimary)) { + append(stringResource(id = R.string.resend_otp)) + } + }, + style = MaterialTheme.typography.bodyLarge, + onClick = { + onEvent(ResendOtpClick) + } + ) + } + } + item { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(id = R.string.do_you_trust_this_device), + style = MaterialTheme.typography.titleMedium, + ) + Checkbox( + checked = trustDevice, + onCheckedChange = { + trustDevice = it + } + ) + } + } + item { + PrimaryButton(text = stringResource(id = R.string.verify)) { + onEvent(VerifyOtpClick(otp,trustDevice)) + } + } + } + + PrimaryProgressSnackView(screenState = progressErrorState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt new file mode 100644 index 00000000..a63c18f4 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/twoFactorAuth/TwoFactorAuthScreenEvent.kt @@ -0,0 +1,7 @@ +package org.aossie.agoraandroid.ui.screens.auth.twoFactorAuth + +sealed class TwoFactorAuthScreenEvent { + object OnBackClick: TwoFactorAuthScreenEvent() + object ResendOtpClick: TwoFactorAuthScreenEvent() + data class VerifyOtpClick(val otp: String, val trustedDevice:Boolean): TwoFactorAuthScreenEvent() +} diff --git a/app/src/main/res/layout/fragment_two_factor_auth.xml b/app/src/main/res/layout/fragment_two_factor_auth.xml deleted file mode 100644 index b770dacf..00000000 --- a/app/src/main/res/layout/fragment_two_factor_auth.xml +++ /dev/null @@ -1,140 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 318d88ae..e13c0139 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -345,8 +345,7 @@ + android:label="Two Factor Authentication"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b0ec1660..5b4345fc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -328,6 +328,9 @@ Bar Chart Pie Chart Start time is smaller than Current time + Verifying OTP... + Sending OTP... + Didn`t receive OTP? NewActivity Authenticating... Facebook Login... From 31463322ea7b263f702c47058c56f229421696ed Mon Sep 17 00:00:00 2001 From: Narendra Singh Anjana Date: Wed, 30 Aug 2023 01:40:41 +0530 Subject: [PATCH 05/13] Gsoc 2023 home screen migrartion (#25) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_login.xml to LoginScreen.kt Compose and Updated facebook Sdk version to latest and added code changes according to new methods in doc and updated gitlab-ci.yml for fbClientSecret * Migrated fragment_home.xml to HomeScreen.kt in compose --- app/build.gradle | 4 + .../Repository/ElectionsRepositoryImpl.kt | 5 +- .../domain/repository/ElectionsRepository.kt | 5 +- .../FetchAndSaveElectionUseCase.kt | 3 +- .../ui/fragments/home/HomeFragment.kt | 195 +++------- .../ui/fragments/home/HomeViewModel.kt | 50 ++- .../ui/screens/home/HomeScreen.kt | 213 +++++++++++ .../screens/home/events/HomeScreenEvents.kt | 10 + .../main/res/drawable/img_active_election.png | Bin 0 -> 3141 bytes .../res/drawable/img_finished_election.png | Bin 0 -> 2299 bytes .../res/drawable/img_pending_election.png | Bin 0 -> 1620 bytes .../main/res/drawable/img_total_election.png | Bin 0 -> 2882 bytes app/src/main/res/layout/fragment_home.xml | 362 ------------------ app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/strings.xml | 1 + build.gradle | 1 + 16 files changed, 349 insertions(+), 503 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt create mode 100644 app/src/main/res/drawable/img_active_election.png create mode 100644 app/src/main/res/drawable/img_finished_election.png create mode 100644 app/src/main/res/drawable/img_pending_election.png create mode 100644 app/src/main/res/drawable/img_total_election.png delete mode 100644 app/src/main/res/layout/fragment_home.xml diff --git a/app/build.gradle b/app/build.gradle index 6dabdc4c..e031aa6d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -201,6 +201,7 @@ dependencies { implementation composeBom androidTestImplementation composeBom implementation 'androidx.compose.material3:material3' + implementation 'androidx.compose.material:material' implementation 'androidx.compose.ui:ui-tooling-preview' debugImplementation 'androidx.compose.ui:ui-tooling' androidTestImplementation 'androidx.compose.ui:ui-test-junit4' @@ -213,4 +214,7 @@ dependencies { //extended Material Icons implementation "androidx.compose.material:material-icons-extended:1.4.3" + + //for live data to state + implementation("androidx.compose.runtime:runtime-livedata:$rootProject.composeLiveData") } diff --git a/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt b/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt index 662e9bbb..2794df7c 100644 --- a/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt +++ b/app/src/main/java/org/aossie/agoraandroid/data/Repository/ElectionsRepositoryImpl.kt @@ -16,6 +16,7 @@ import org.aossie.agoraandroid.data.network.dto.ElectionDto import org.aossie.agoraandroid.data.network.dto.VotersDto import org.aossie.agoraandroid.data.network.dto.WinnerDto import org.aossie.agoraandroid.data.network.responses.Ballots +import org.aossie.agoraandroid.data.network.responses.ElectionListResponse import org.aossie.agoraandroid.data.network.responses.ElectionResponse import org.aossie.agoraandroid.domain.repository.ElectionsRepository import org.aossie.agoraandroid.utilities.ApiException @@ -107,7 +108,7 @@ constructor( db.getElectionDao() .getPendingElections(currentDate) - override suspend fun fetchElections() { + override suspend fun fetchElections(): ElectionListResponse { val isNeeded = prefs.getUpdateNeeded().first() if (isNeeded) { try { @@ -117,12 +118,14 @@ constructor( elections.postValue(it) } Timber.d(response.toString()) + return response } catch (e: NoInternetException) { } catch (e: ApiException) { } catch (e: SessionExpirationException) { } catch (e: IOException) { } } + return ElectionListResponse(elections.value!!) } override fun getFinishedElections(currentDate: String): Flow> = diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt b/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt index f5dd597c..acee8d7a 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/repository/ElectionsRepository.kt @@ -7,10 +7,11 @@ import org.aossie.agoraandroid.data.network.dto.ElectionDto import org.aossie.agoraandroid.data.network.dto.VotersDto import org.aossie.agoraandroid.data.network.dto.WinnerDto import org.aossie.agoraandroid.data.network.responses.Ballots +import org.aossie.agoraandroid.data.network.responses.ElectionListResponse import org.aossie.agoraandroid.data.network.responses.ElectionResponse interface ElectionsRepository { - suspend fun fetchAndSaveElections() + suspend fun fetchAndSaveElections(): ElectionListResponse fun getElections(): Flow> fun getFinishedElectionsCount(currentDate: String): LiveData fun getPendingElectionsCount(currentDate: String): LiveData @@ -18,7 +19,7 @@ interface ElectionsRepository { fun getActiveElectionsCount(currentDate: String): LiveData suspend fun saveElections(elections: List) fun getPendingElections(currentDate: String): Flow> - suspend fun fetchElections() + suspend fun fetchElections(): ElectionListResponse fun getFinishedElections(currentDate: String): Flow> fun getActiveElections(currentDate: String): Flow> fun getElectionById(id: String): Flow diff --git a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt index a0357200..2d6485b5 100644 --- a/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt +++ b/app/src/main/java/org/aossie/agoraandroid/domain/useCases/homeFragment/FetchAndSaveElectionUseCase.kt @@ -1,12 +1,13 @@ package org.aossie.agoraandroid.domain.useCases.homeFragment +import org.aossie.agoraandroid.data.network.responses.ElectionListResponse import org.aossie.agoraandroid.domain.repository.ElectionsRepository import javax.inject.Inject class FetchAndSaveElectionUseCase @Inject constructor( private val electionsRepository: ElectionsRepository ) { - suspend operator fun invoke() { + suspend operator fun invoke(): ElectionListResponse { return electionsRepository.fetchAndSaveElections() } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt index 77e5febb..72f025df 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeFragment.kt @@ -1,28 +1,35 @@ package org.aossie.agoraandroid.ui.fragments.home -import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.doOnLayout +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import com.takusemba.spotlight.Spotlight +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.color -import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentHomeBinding import org.aossie.agoraandroid.ui.fragments.BaseFragment import org.aossie.agoraandroid.ui.fragments.auth.login.LoginViewModel -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.TargetData -import org.aossie.agoraandroid.utilities.getSpotlight -import org.aossie.agoraandroid.utilities.scrollToView +import org.aossie.agoraandroid.ui.screens.auth.login.events.LoginUiEvent.UserLoggedIn +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.home.HomeScreen +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.ActiveElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.CreateElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.FinishedElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.PendingElectionClick +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.Refresh +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.TotalElectionClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import timber.log.Timber import java.text.ParseException import java.text.SimpleDateFormat @@ -45,64 +52,68 @@ constructor( viewModelFactory } - private lateinit var binding: FragmentHomeBinding - - private var spotlight: Spotlight? = null - private var spotlightTargets: ArrayList? = null - private var currentSpotlightIndex = 0 + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentHomeBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { loginViewModel.sessionExpiredListener = this - binding.swipeRefresh.setColorSchemeResources(color.logo_yellow, color.logo_green) + composeView.setContent { + val homeScreenDataState by homeViewModel.countMediatorLiveData.observeAsState() - binding.cardViewActiveElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToActiveElectionsFragment()) - } - binding.cardViewPendingElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToPendingElectionsFragment()) - } - binding.cardViewFinishedElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToFinishedElectionsFragment()) - } - binding.cardViewTotalElections.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToElectionsFragment()) - } - binding.buttonCreateElection.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(HomeFragmentDirections.actionHomeFragmentToCreateElectionFragment()) - } - binding.swipeRefresh.setOnRefreshListener { updateUi() } + val progressErrorState1 = loginViewModel.progressAndErrorState + val progressErrorState2 = homeViewModel.progressAndErrorState - lifecycleScope.launch { - loginViewModel.getLoginStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - // Do Nothing - } - ResponseUI.Status.SUCCESS -> { + val progressErrorState by merge(progressErrorState1,progressErrorState2).collectAsState(initial = ScreensState()) + + LaunchedEffect(key1 = Unit) { + loginViewModel.uiEvents.collectLatest { event -> + when(event) { + UserLoggedIn -> { updateUi() } - ResponseUI.Status.ERROR -> { - notify(it.message) - } else -> {} } } } + + AgoraTheme { + HomeScreen(homeScreenDataState,progressErrorState){ event -> + when(event){ + ActiveElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToActiveElectionsFragment()) + } + CreateElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToCreateElectionFragment()) + } + FinishedElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToFinishedElectionsFragment()) + } + PendingElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToPendingElectionsFragment()) + } + TotalElectionClick -> { + findNavController() + .navigate(HomeFragmentDirections.actionHomeFragmentToElectionsFragment()) + } + Refresh -> { + updateUi() + } + } + } + } } lifecycleScope.launch { @@ -141,23 +152,7 @@ constructor( } } - homeViewModel.countMediatorLiveData.observe( - viewLifecycleOwner, - { - binding.textViewActiveCount.text = it[ACTIVE_ELECTION_COUNT].toString() - binding.textViewTotalCount.text = it[TOTAL_ELECTION_COUNT].toString() - binding.textViewPendingCount.text = it[PENDING_ELECTION_COUNT].toString() - binding.textViewFinishedCount.text = it[FINISHED_ELECTION_COUNT].toString() - binding.shimmerViewContainer.stopShimmer() - binding.shimmerViewContainer.visibility = View.GONE - binding.constraintLayout.visibility = View.VISIBLE - binding.swipeRefresh.isRefreshing = false // Disables the refresh icon - } - ) updateUi() - binding.root.doOnLayout { - checkIsFirstOpen() - } } override fun onNetworkConnected() { @@ -165,7 +160,6 @@ constructor( } override fun onDestroyView() { - binding.swipeRefresh.setOnRefreshListener(null) homeViewModel.sessionExpiredListener = null loginViewModel.sessionExpiredListener = null super.onDestroyView() @@ -176,70 +170,5 @@ constructor( preferenceProvider.setUpdateNeeded(true) homeViewModel.getElections() } - binding.shimmerViewContainer.startShimmer() - binding.shimmerViewContainer.visibility = View.VISIBLE - binding.constraintLayout.visibility = View.GONE - } - - private fun checkIsFirstOpen() { - lifecycleScope.launch { - if (!preferenceProvider.isDisplayed(binding.root.id.toString()) - .first() - ) { - spotlightTargets = getSpotlightTargets() - preferenceProvider.setDisplayed(binding.root.id.toString()) - showSpotlight() - } - } - } - - private fun showSpotlight() { - spotlightTargets?.let { - if (currentSpotlightIndex in it.indices) { - scrollToView(binding.scrollView, it[currentSpotlightIndex].targetView) - spotlight = requireActivity().getSpotlight( - it[currentSpotlightIndex++], - { - destroySpotlight() - }, - { - it.clear() - destroySpotlight() - }, - { - if (isAdded) { - showSpotlight() - } - } - ) - spotlight?.start() - } - } - } - - private fun getSpotlightTargets(): ArrayList { - val targetData = ArrayList() - targetData.add( - TargetData( - binding.buttonCreateElection, getString(string.Create_Election), - getString(string.create_election_spotlight) - ) - ) - return targetData - } - - private fun destroySpotlight() { - spotlight?.finish() - spotlight = null - } - - override fun onPause() { - super.onPause() - destroySpotlight() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - destroySpotlight() } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt index 17ef2419..1c309fa5 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt @@ -4,12 +4,16 @@ import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.aossie.agoraandroid.domain.useCases.homeFragment.HomeFragmentUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException +import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.NoInternetException import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException @@ -36,22 +40,35 @@ constructor( .time private val date: String = formatter.format(currentDate) + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + private val _countMediatorLiveData = MediatorLiveData>() val countMediatorLiveData = _countMediatorLiveData init { - _countMediatorLiveData.value = mutableMapOf() + _countMediatorLiveData.value = mutableMapOf( + TOTAL_ELECTION_COUNT to 0, + PENDING_ELECTION_COUNT to 0, + FINISHED_ELECTION_COUNT to 0, + ACTIVE_ELECTION_COUNT to 0 + ) addSource() } fun getElections() { + showLoading("Loading...") GlobalScope.launch { - homeViewModelUseCases.fetchAndSaveElection() + val response = homeViewModelUseCases.fetchAndSaveElection() + response.elections.let { + hideLoading() + } } } private fun addSource() { viewModelScope.launch { + _countMediatorLiveData.addSource(homeViewModelUseCases.getTotalElectionsCount()) { value -> _countMediatorLiveData.value = _countMediatorLiveData.value.apply { this?.let { @@ -107,4 +124,33 @@ constructor( } } } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt new file mode 100644 index 00000000..d2526f17 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/HomeScreen.kt @@ -0,0 +1,213 @@ +package org.aossie.agoraandroid.ui.screens.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material.pullrefresh.PullRefreshIndicator +import androidx.compose.material.pullrefresh.pullRefresh +import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FabPosition +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.fragments.home.FINISHED_ELECTION_COUNT +import org.aossie.agoraandroid.ui.fragments.home.PENDING_ELECTION_COUNT +import org.aossie.agoraandroid.ui.fragments.home.TOTAL_ELECTION_COUNT +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents +import org.aossie.agoraandroid.ui.screens.home.events.HomeScreenEvents.CreateElectionClick + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) +@Composable +fun HomeScreen( + homeScreenDataState: MutableMap?, + screenState: ScreensState, + onClickEvents: (HomeScreenEvents) -> Unit +) { + + var refreshing by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = screenState.loading.second){ + refreshing = screenState.loading.second + } + + val state = rememberPullRefreshState(refreshing, onRefresh = { + onClickEvents(HomeScreenEvents.Refresh) + }) + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground, + floatingActionButtonPosition = FabPosition.End, + floatingActionButton = { + if(!screenState.loading.second){ + ExtendedFloatingActionButton( + text = { + Text(text = stringResource(id = string.Create_Election)) + }, + icon = { + Icon(imageVector = Rounded.Add, contentDescription = "") + }, + onClick = { + onClickEvents(CreateElectionClick) + }) + } + } + ) { + Box(modifier = Modifier + .padding(it) + .pullRefresh(state)){ + LazyColumn( + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ){ + homeScreenDataState?.toList()?.sortedBy { + it.first + }?.let {list -> + itemsIndexed(list){index, item -> + HomeElectionCard( + item = item, + onClick = onClickEvents + ) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeElectionCard(item: Pair, onClick: (HomeScreenEvents) -> Unit) { + var title: String + var icon: Int + val color = when(item.first){ + TOTAL_ELECTION_COUNT -> { + title = stringResource(id = R.string.total_elections) + icon = R.drawable.img_total_election + Pair( + MaterialTheme.colorScheme.secondaryContainer, + MaterialTheme.colorScheme.onSecondaryContainer + ) + } + PENDING_ELECTION_COUNT -> { + title = stringResource(id = R.string.pending_elections) + icon = R.drawable.img_pending_election + Pair( + MaterialTheme.colorScheme.tertiaryContainer, + MaterialTheme.colorScheme.onTertiaryContainer + ) + } + FINISHED_ELECTION_COUNT -> { + title = stringResource(id = R.string.finished_elections) + icon = R.drawable.img_finished_election + Pair( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.onSurface + ) + } + else -> { + title = stringResource(id = R.string.active_elections) + icon = R.drawable.img_active_election + Pair( + MaterialTheme.colorScheme.primaryContainer, + MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + Card( + onClick = { + when(item.first){ + TOTAL_ELECTION_COUNT -> { + onClick(HomeScreenEvents.TotalElectionClick) + } + PENDING_ELECTION_COUNT -> { + onClick(HomeScreenEvents.PendingElectionClick) + } + FINISHED_ELECTION_COUNT -> { + onClick(HomeScreenEvents.FinishedElectionClick) + } + else -> { + onClick(HomeScreenEvents.ActiveElectionClick) + } + } + }, + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = color.first, + contentColor = color.second + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 10.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(25.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = "${item.second} $title" , + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = stringResource(id = R.string.view_details), + style = MaterialTheme.typography.bodyLarge + ) + } + Row( + verticalAlignment = Alignment.Bottom, + horizontalArrangement = Arrangement.spacedBy(5.dp) + ) { + Image( + painter = painterResource(id = icon), + contentDescription = "", + modifier = Modifier.size(80.dp) + ) + Icon(imageVector = Rounded.NavigateNext, contentDescription = "") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt new file mode 100644 index 00000000..3722e593 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/home/events/HomeScreenEvents.kt @@ -0,0 +1,10 @@ +package org.aossie.agoraandroid.ui.screens.home.events + +sealed class HomeScreenEvents{ + object ActiveElectionClick:HomeScreenEvents() + object TotalElectionClick:HomeScreenEvents() + object PendingElectionClick:HomeScreenEvents() + object FinishedElectionClick:HomeScreenEvents() + object CreateElectionClick:HomeScreenEvents() + object Refresh:HomeScreenEvents() +} diff --git a/app/src/main/res/drawable/img_active_election.png b/app/src/main/res/drawable/img_active_election.png new file mode 100644 index 0000000000000000000000000000000000000000..988e72880387a0dac21a60a7d16e7fd2fd1d9228 GIT binary patch literal 3141 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 fE?v5q5Z3~vOKZDa^S?89 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 gAVGoz - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index e13c0139..497333b0 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -68,8 +68,7 @@ + android:label="Home"> Authenticating... Facebook Login... Login error + View details diff --git a/build.gradle b/build.gradle index c42b2494..bc356e43 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,7 @@ buildscript { composeActivityVersion = '1.6.1' accompanist_version = '0.31.2-alpha' coilVersion = '2.4.0' + composeLiveData = '1.4.3' } repositories { From ad6df0fc049f24e8083083b6057fc50dfff86083 Mon Sep 17 00:00:00 2001 From: Narendra Singh Anjana Date: Wed, 30 Aug 2023 01:43:09 +0530 Subject: [PATCH 06/13] Gsoc 2023 migrating elections fragment to compose (#31) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_elections.xml.xml to ElectionsScreen.kt in compose --- .../fragments/elections/ElectionViewModel.kt | 60 ++++++ .../fragments/elections/ElectionsFragment.kt | 93 +++------ .../common/component/PrimaryElectionCard.kt | 189 ++++++++++++++++++ .../component/PrimaryElectionSearchBar.kt | 83 ++++++++ .../common/component/PrimaryElectionsList.kt | 40 ++++ .../ui/screens/elections/ElectionsScreen.kt | 44 ++++ .../agoraandroid/utilities/ElectionUtils.kt | 21 ++ .../main/res/drawable/ic_election_active.xml | 9 + .../res/drawable/ic_election_finished.xml | 20 ++ .../main/res/drawable/ic_election_pending.xml | 13 ++ .../main/res/layout/fragment_elections.xml | 80 -------- app/src/main/res/navigation/nav_graph.xml | 3 +- build.gradle | 2 +- 13 files changed, 510 insertions(+), 147 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/utilities/ElectionUtils.kt create mode 100644 app/src/main/res/drawable/ic_election_active.xml create mode 100644 app/src/main/res/drawable/ic_election_finished.xml create mode 100644 app/src/main/res/drawable/ic_election_pending.xml delete mode 100644 app/src/main/res/layout/fragment_elections.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt index ce9ebc88..d6111cda 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionViewModel.kt @@ -1,11 +1,19 @@ package org.aossie.agoraandroid.ui.fragments.elections +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.domain.model.ElectionModel import org.aossie.agoraandroid.domain.useCases.electionsAndCalenderView.ElectionsUseCases +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.utilities.AppConstants import java.util.Locale import javax.inject.Inject @@ -15,6 +23,58 @@ constructor( private val electionsUseCases: ElectionsUseCases ) : ViewModel() { + val elections = MutableStateFlow>(emptyList()) + val search = mutableStateOf("") + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + fun getElectionsState(query:String){ + viewModelScope.launch { + try { + electionsUseCases.getElections().collectLatest { list -> + search.value = query + if(query.isEmpty()) { + elections.emit(list) + }else{ + elections.emit(filter(list, query)) + } + } + } catch (e: IllegalStateException) { + showMessage(R.string.something_went_wrong_please_try_again_later) + } + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } + fun getElections(): Flow> { viewModelScope.launch { electionsUseCases.fetchAndSaveElection() diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt index 08e0826e..8eedd9dd 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/elections/ElectionsFragment.kt @@ -4,20 +4,15 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.data.adapters.ElectionsAdapter -import org.aossie.agoraandroid.databinding.FragmentElectionsBinding -import org.aossie.agoraandroid.domain.model.ElectionModel +import androidx.navigation.fragment.findNavController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.elections.ElectionsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject class ElectionsFragment @@ -30,78 +25,48 @@ constructor( viewModelFactory } - lateinit var mElections: ArrayList - private lateinit var electionsAdapter: ElectionsAdapter - private val onItemClicked = { _id: String -> val action = ElectionsFragmentDirections .actionElectionsFragmentToElectionDetailsFragment(_id) - Navigation.findNavController(binding.root) - .navigate(action) + findNavController().navigate(action) } - private lateinit var binding: FragmentElectionsBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentElectionsBinding.inflate(inflater) - return binding.root - } - - override fun onFragmentInitiated() { - mElections = ArrayList() - electionsAdapter = ElectionsAdapter(onItemClicked) - binding.rvTotalElections.apply { - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - adapter = electionsAdapter - } - binding.searchView.doAfterTextChanged { - filter(it.toString()) + return ComposeView(requireContext()).also { + composeView = it } } - override fun onNetworkConnected() { - bindUI() - } - - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onFragmentInitiated() { bindUI() - } - - private fun bindUI() { - lifecycleScope.launch { - try { - electionViewModel.getElections() - .collect { - if (it != null) { - addElections(it) - } - } - } catch (e: IllegalStateException) { - binding.tvSomethingWentWrong.show() + composeView.setContent { + val elections by electionViewModel.elections.collectAsState() + val progressErrorState by electionViewModel.progressAndErrorState.collectAsState() + val searchText by electionViewModel.search + AgoraTheme { + ElectionsScreen( + screenState = progressErrorState, + elections = elections, + searchText = searchText, + onSearch = { + electionViewModel.getElectionsState(it) + }, + onItemClicked = onItemClicked + ) } } } - private fun addElections(elections: List) { - if (elections.isNotEmpty()) { - mElections.addAll(elections) - electionsAdapter.submitList(elections) - } else { - binding.tvEmptyElection.show() - } + private fun bindUI() { + electionViewModel.getElectionsState(electionViewModel.search.value) } - private fun filter(query: String) { - val updatedList = electionViewModel.filter(mElections, query) - electionsAdapter.submitList(updatedList) - if (updatedList.isEmpty()) { - binding.tvEmptyElection.show() - } else { - binding.tvEmptyElection.hide() - } + override fun onNetworkConnected() { + bindUI() } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt new file mode 100644 index 00000000..3b4b5a16 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionCard.kt @@ -0,0 +1,189 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material.icons.rounded.Schedule +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.ElectionModel +import org.aossie.agoraandroid.utilities.AppConstants.Status.ACTIVE +import org.aossie.agoraandroid.utilities.AppConstants.Status.FINISHED +import org.aossie.agoraandroid.utilities.AppConstants.Status.PENDING +import org.aossie.agoraandroid.utilities.ElectionUtils +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrimaryElectionCard(election: ElectionModel, onClick: (String) -> Unit) { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) + val formattedStartingDate: Date? = formatter.parse(election.start!!) + val formattedEndingDate: Date? = formatter.parse(election.end!!) + val currentDate = Calendar.getInstance().time + val outFormat = SimpleDateFormat("dd-MM-yyyy 'at' HH:mm:ss", Locale.ENGLISH) + val endDate = outFormat.format(formattedEndingDate!!) + val startDate = outFormat.format(formattedStartingDate!!) + val status = ElectionUtils.getEventStatus(currentDate, formattedStartingDate, formattedEndingDate) + val icon:Int + val iconColor: Color + val colorContainer: Color + val colorContent: Color + + when(status){ + PENDING -> { + icon = R.drawable.ic_election_pending + iconColor = MaterialTheme.colorScheme.error + colorContainer = MaterialTheme.colorScheme.tertiaryContainer + colorContent = MaterialTheme.colorScheme.onTertiaryContainer + } + ACTIVE -> { + icon = R.drawable.ic_election_active + iconColor = Color(0xff00C537) + colorContainer = MaterialTheme.colorScheme.primaryContainer + colorContent = MaterialTheme.colorScheme.onPrimaryContainer + } + FINISHED -> { + icon = R.drawable.ic_election_finished + iconColor = Color(0xff1877F2) + colorContainer = MaterialTheme.colorScheme.secondaryContainer + colorContent = MaterialTheme.colorScheme.onSecondaryContainer + } + null -> { + icon = R.drawable.ic_election_active + iconColor = MaterialTheme.colorScheme.primary + colorContainer = MaterialTheme.colorScheme.primaryContainer + colorContent = MaterialTheme.colorScheme.onPrimaryContainer + } + } + Card( + onClick = { onClick.invoke(election._id) }, + colors = CardDefaults.cardColors( + containerColor = colorContainer, + contentColor = colorContent + ), + shape = RoundedCornerShape(15.dp), + modifier = Modifier + .fillMaxWidth(), + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(15.dp), + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = election.name!!, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Icon( + painter = painterResource(id = icon), + tint = iconColor, + contentDescription = "Election Icons") + } + Text( + text = election.description!!, + style = MaterialTheme.typography.bodyLarge, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + Column() { + Text( + text = stringResource(id = string.candidates), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + itemsIndexed(election.candidates!!) { index, item -> + AssistChip( + label = { + Text( + text = item, + style = MaterialTheme.typography.titleMedium + ) + }, + onClick = { }, + border = AssistChipDefaults.assistChipBorder( + borderWidth = 1.dp, + borderColor = colorContent + ) + ) + } + } + } + Divider( + color = MaterialTheme.colorScheme.outline, + thickness = 1.dp, + modifier = Modifier.fillMaxWidth() + ) + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column() { + TimeItem( + label = stringResource(id = string.start_at), + text = startDate + ) + TimeItem( + label = stringResource(id = string.end_at), + text = endDate + ) + } + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "Navigate Icon") + } + } + } + + +} + +@Composable +fun TimeItem(label: String, text: String) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = Rounded.Schedule, contentDescription = "") + Spacer(modifier = Modifier.width(3.dp)) + Text(text = label, fontWeight = FontWeight.SemiBold) + Text(text = text) + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt new file mode 100644 index 00000000..3b1e6882 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionSearchBar.kt @@ -0,0 +1,83 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.SearchBar +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import org.aossie.agoraandroid.R.string + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PrimaryElectionSearchBar( + searchText: String, + onSearch: (String) -> Unit, + content: @Composable() (ColumnScope.() -> Unit)? = null +) { + var text by remember { mutableStateOf("") } + var active by remember { mutableStateOf(false) } + LaunchedEffect(key1 = searchText){ + if(searchText.isNotEmpty()){ + text = searchText + onSearch(text) + } + } + SearchBar( + modifier = Modifier.fillMaxWidth(), + query = text, + onQueryChange = { + text = it + onSearch(it) + }, + onSearch = { + active = false + onSearch(it) + }, + active = active, + onActiveChange = { + active = it + }, + placeholder = { + Text(text = stringResource(id = string.search_hint)) + }, + leadingIcon = { + Icon( + imageVector = Rounded.Search, + contentDescription = "Search Icons") + }, + trailingIcon = { + if(active){ + IconButton(onClick = { + if(text.isNotEmpty()){ + text = "" + onSearch("") + }else{ + active = false + } + }) { + Icon( + imageVector = Rounded.Close, + contentDescription = "Close Icons") + } + } + }, + content = { + content?.let { it -> + it() + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt new file mode 100644 index 00000000..8c542a7d --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/PrimaryElectionsList.kt @@ -0,0 +1,40 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.ElectionModel + +@Composable +fun PrimaryElectionsList( + elections: List, + onItemClicked: (String) -> Unit +) { + if(elections.isNotEmpty()){ + LazyColumn( + verticalArrangement = Arrangement.spacedBy(20.dp, Alignment.Top), + contentPadding = PaddingValues(20.dp) + ) { + itemsIndexed(elections) { index, item -> + PrimaryElectionCard( + election = item, + onClick = onItemClicked + ) + } + } + }else{ + Box(modifier = Modifier.fillMaxSize()) { + Text(text = stringResource(id = string.empty_elections), modifier = Modifier.align(Alignment.Center)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt new file mode 100644 index 00000000..61429f11 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/elections/ElectionsScreen.kt @@ -0,0 +1,44 @@ +package org.aossie.agoraandroid.ui.screens.elections + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.aossie.agoraandroid.domain.model.ElectionModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryElectionSearchBar +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryElectionsList +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView + +@Composable +fun ElectionsScreen( + screenState: ScreensState, + elections: List, + searchText: String, + onSearch: (String) -> Unit, + onItemClicked: (String) -> Unit +) { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box(modifier = Modifier.padding(it)){ + Column { + PrimaryElectionSearchBar( + searchText = searchText, + onSearch = onSearch, + content = { + PrimaryElectionsList(elections, onItemClicked) + } + ) + PrimaryElectionsList(elections, onItemClicked) + } + PrimaryProgressSnackView(screenState = screenState) + } + } + +} + diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/ElectionUtils.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/ElectionUtils.kt new file mode 100644 index 00000000..7fae59e2 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/ElectionUtils.kt @@ -0,0 +1,21 @@ +package org.aossie.agoraandroid.utilities + +import java.util.Date + +object ElectionUtils { + + fun getEventStatus( + currentDate: Date, + formattedStartingDate: Date?, + formattedEndingDate: Date? + ): AppConstants.Status? { + return when { + currentDate.before(formattedStartingDate) -> AppConstants.Status.PENDING + currentDate.after(formattedStartingDate) && currentDate.before( + formattedEndingDate + ) -> AppConstants.Status.ACTIVE + currentDate.after(formattedEndingDate) -> AppConstants.Status.FINISHED + else -> null + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_election_active.xml b/app/src/main/res/drawable/ic_election_active.xml new file mode 100644 index 00000000..1ccb200e --- /dev/null +++ b/app/src/main/res/drawable/ic_election_active.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_election_finished.xml b/app/src/main/res/drawable/ic_election_finished.xml new file mode 100644 index 00000000..923f1fa5 --- /dev/null +++ b/app/src/main/res/drawable/ic_election_finished.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_election_pending.xml b/app/src/main/res/drawable/ic_election_pending.xml new file mode 100644 index 00000000..1611ce5d --- /dev/null +++ b/app/src/main/res/drawable/ic_election_pending.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/fragment_elections.xml b/app/src/main/res/layout/fragment_elections.xml deleted file mode 100644 index 147e4a48..00000000 --- a/app/src/main/res/layout/fragment_elections.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 497333b0..f3bc087c 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -108,8 +108,7 @@ + android:label="Elections"> Date: Wed, 30 Aug 2023 01:44:00 +0530 Subject: [PATCH 07/13] Gsoc 2023 migrating finished elections fragment to compose (#35) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_elections.xml.xml to ElectionsScreen.kt in compose * Migrated fragment_finished_elections.xmle.xml.xml to ElectionsScreen.kt in compose --- .../DisplayElectionViewModel.kt | 63 ++++++++++++- .../FinishedElectionsFragment.kt | 91 ++++++------------- .../layout/fragment_finished_elections.xml | 84 ----------------- app/src/main/res/navigation/nav_graph.xml | 3 +- 4 files changed, 90 insertions(+), 151 deletions(-) delete mode 100644 app/src/main/res/layout/fragment_finished_elections.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt index 3a8552a6..6f1dfe4b 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt @@ -1,8 +1,18 @@ package org.aossie.agoraandroid.ui.fragments.displayelections +import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.domain.model.ElectionModel import org.aossie.agoraandroid.domain.useCases.displayElection.DisplayElectionsUseCases +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.lazyDeferred import java.text.SimpleDateFormat import java.util.Calendar @@ -27,8 +37,57 @@ constructor( val pendingElections by lazyDeferred { displayElectionsUseCases.getPendingElections(date) } - val finishedElections by lazyDeferred { - displayElectionsUseCases.getFinishedElections(date) + + val finishedElections = MutableStateFlow>(emptyList()) + val search = mutableStateOf("") + + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + fun getFinishedElectionsState(query:String){ + viewModelScope.launch { + try { + displayElectionsUseCases.getFinishedElections(date).collectLatest { list -> + search.value = query + if(query.isEmpty()) { + finishedElections.emit(list) + }else{ + finishedElections.emit(filter(list, query)) + } + } + } catch (e: IllegalStateException) { + showMessage(R.string.something_went_wrong_please_try_again_later) + } + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) } fun filter( diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt index ee042eb9..e2d5ac03 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/FinishedElectionsFragment.kt @@ -4,21 +4,16 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.data.adapters.ElectionsAdapter -import org.aossie.agoraandroid.databinding.FragmentFinishedElectionsBinding -import org.aossie.agoraandroid.domain.model.ElectionModel +import androidx.navigation.fragment.findNavController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.elections.ElectionsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -34,38 +29,43 @@ constructor( viewModelFactory } - lateinit var mElections: ArrayList - private lateinit var electionsAdapter: ElectionsAdapter - private val onItemClicked = { _id: String -> val action = FinishedElectionsFragmentDirections.actionFinishedElectionsFragmentToElectionDetailsFragment( _id ) - Navigation.findNavController(binding.root) - .navigate(action) + findNavController().navigate(action) } - private lateinit var binding: FragmentFinishedElectionsBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentFinishedElectionsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - mElections = ArrayList() - electionsAdapter = ElectionsAdapter(onItemClicked) - binding.rvFinishedElections.apply { - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - adapter = electionsAdapter - } - binding.searchView.doAfterTextChanged { - filter(it.toString()) + bindUI() + composeView.setContent { + val elections by displayElectionViewModel.finishedElections.collectAsState() + val progressErrorState by displayElectionViewModel.progressAndErrorState.collectAsState() + val searchText by displayElectionViewModel.search + AgoraTheme { + ElectionsScreen( + screenState = progressErrorState, + elections = elections, + searchText = searchText, + onSearch = { + displayElectionViewModel.getFinishedElectionsState(it) + }, + onItemClicked = onItemClicked + ) + } } } @@ -73,42 +73,7 @@ constructor( bindUI() } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) - bindUI() - } - private fun bindUI() { - lifecycleScope.launch { - try { - val elections = displayElectionViewModel.finishedElections.await() - elections.collect { - if (it != null) { - addElections(it) - } - } - } catch (e: IllegalStateException) { - binding.tvSomethingWentWrong.show() - } - } - } - - private fun addElections(elections: List) { - if (elections.isNotEmpty()) { - mElections.addAll(elections) - electionsAdapter.submitList(elections) - } else { - binding.tvEmptyElection.show() - } - } - - private fun filter(query: String) { - val updatedList = displayElectionViewModel.filter(mElections, query) - electionsAdapter.submitList(updatedList) - if (updatedList.isEmpty()) { - binding.tvEmptyElection.show() - } else { - binding.tvEmptyElection.hide() - } + displayElectionViewModel.getFinishedElectionsState("") } } diff --git a/app/src/main/res/layout/fragment_finished_elections.xml b/app/src/main/res/layout/fragment_finished_elections.xml deleted file mode 100644 index 11521ff3..00000000 --- a/app/src/main/res/layout/fragment_finished_elections.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index f3bc087c..541e4357 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -202,8 +202,7 @@ + android:label="Finished Elections"> Date: Wed, 30 Aug 2023 14:49:24 +0530 Subject: [PATCH 08/13] Gsoc 2023 profile screen migration compose (#29) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_login.xml to LoginScreen.kt Compose and Updated facebook Sdk version to latest and added code changes according to new methods in doc and updated gitlab-ci.yml for fbClientSecret * Migrated fragment_home.xml to HomeScreen.kt in compose * Migrated fragment_settings.xml to SettingScreen.kt in Compose * Migrated fragment_profile.xml to ProfileScreen.kt in compose --- app/build.gradle | 4 + app/src/main/AndroidManifest.xml | 7 +- .../java/org/aossie/agoraandroid/AgoraApp.kt | 3 + .../data/db/PreferenceProvider.kt | 18 + .../agoraandroid/ui/di/models/AppContext.kt | 7 + .../agoraandroid/ui/di/modules/AppModule.kt | 7 + .../ui/fragments/home/HomeViewModel.kt | 53 +- .../ui/fragments/profile/ProfileFragment.kt | 580 ++--------------- .../ui/fragments/profile/ProfileViewModel.kt | 300 +++++++-- .../ui/fragments/settings/SettingsFragment.kt | 174 +++--- .../ui/screens/profile/ProfileScreen.kt | 281 +++++++++ .../screens/profile/ProfileScreenDataState.kt | 11 + .../ui/screens/profile/ProfileScreenEvent.kt | 16 + .../profile/component/ConfirmationDialog.kt | 52 ++ .../profile/component/IconTextSwitchButton.kt | 57 ++ .../screens/profile/component/ProfileItem.kt | 150 +++++ .../ui/screens/settings/SettingScreen.kt | 219 +++++++ .../screens/settings/SettingsScreenEvent.kt | 10 + .../component/LanguageRadioButtonItem.kt | 38 ++ .../component/LanguageUpdateDialog.kt | 91 +++ .../agoraandroid/utilities/AppConstants.kt | 1 + .../agoraandroid/utilities/LocaleUtil.kt | 11 + app/src/main/res/drawable/ic_art.xml | 12 - app/src/main/res/drawable/ic_camera.xml | 19 +- app/src/main/res/drawable/ic_contact_us.xml | 393 +----------- .../main/res/drawable/ic_fingerprint_24.xml | 5 + app/src/main/res/drawable/ic_gallery.xml | 17 + app/src/main/res/drawable/ic_info.xml | 9 + app/src/main/res/drawable/ic_logout.xml | 13 + app/src/main/res/drawable/ic_share.xml | 581 +----------------- app/src/main/res/drawable/ic_translate.xml | 9 + .../main/res/drawable/ic_two_factor_auth.xml | 12 + app/src/main/res/drawable/ic_user_new.xml | 13 + .../main/res/layout/dialog_change_avatar.xml | 83 --- app/src/main/res/layout/fragment_profile.xml | 248 -------- app/src/main/res/layout/fragment_settings.xml | 200 ------ app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values-hi/strings.xml | 330 ++++++++++ app/src/main/res/values/strings.xml | 3 + app/src/main/res/xml/provider_paths.xml | 6 +- build.gradle | 1 + 41 files changed, 1849 insertions(+), 2198 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt delete mode 100644 app/src/main/res/drawable/ic_art.xml create mode 100644 app/src/main/res/drawable/ic_fingerprint_24.xml create mode 100644 app/src/main/res/drawable/ic_gallery.xml create mode 100644 app/src/main/res/drawable/ic_info.xml create mode 100644 app/src/main/res/drawable/ic_logout.xml create mode 100644 app/src/main/res/drawable/ic_translate.xml create mode 100644 app/src/main/res/drawable/ic_two_factor_auth.xml create mode 100644 app/src/main/res/drawable/ic_user_new.xml delete mode 100644 app/src/main/res/layout/dialog_change_avatar.xml delete mode 100644 app/src/main/res/layout/fragment_profile.xml delete mode 100644 app/src/main/res/layout/fragment_settings.xml create mode 100644 app/src/main/res/values-hi/strings.xml diff --git a/app/build.gradle b/app/build.gradle index e031aa6d..377a4c5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -208,6 +208,7 @@ dependencies { debugImplementation 'androidx.compose.ui:ui-test-manifest' implementation "androidx.activity:activity-compose:$rootProject.composeActivityVersion" implementation "com.google.accompanist:accompanist-systemuicontroller:$rootProject.accompanist_version" + implementation "com.google.accompanist:accompanist-permissions:$rootProject.accompanist_version" //Coil implementation("io.coil-kt:coil-compose:$rootProject.coilVersion") @@ -217,4 +218,7 @@ dependencies { //for live data to state implementation("androidx.compose.runtime:runtime-livedata:$rootProject.composeLiveData") + + //for language change + implementation "com.github.YarikSOffice:lingver:$rootProject.languageLibrary" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e185c999..a11998c5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,14 +2,11 @@ - - - + + + securityUtil.encryptToken(_lang) + } ?: "" + } + } + + fun getAppLanguage(): Flow { + return userDataStore.data.map { + val language = it[APP_LANGUAGE]?.let { _lang -> + securityUtil.decryptToken(_lang) + } + language ?: "en" + } + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt b/app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt new file mode 100644 index 00000000..cea9c71c --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/di/models/AppContext.kt @@ -0,0 +1,7 @@ +package org.aossie.agoraandroid.ui.di.models + +import android.content.Context + +data class AppContext( + val context: Context +) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt b/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt index 655c2b49..ac70b00a 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/di/modules/AppModule.kt @@ -73,6 +73,7 @@ import org.aossie.agoraandroid.domain.useCases.profile.GetUserDataUseCase import org.aossie.agoraandroid.domain.useCases.profile.ProfileUseCases import org.aossie.agoraandroid.domain.useCases.profile.ToggleTwoFactorAuthUseCase import org.aossie.agoraandroid.domain.useCases.profile.UpdateUserUseCase +import org.aossie.agoraandroid.ui.di.models.AppContext import org.aossie.agoraandroid.utilities.AppConstants import org.aossie.agoraandroid.utilities.InternetManager import org.aossie.agoraandroid.utilities.SecurityUtil @@ -501,4 +502,10 @@ class AppModule { ): CastVoteActivityUseCases { return CastVoteActivityUseCases(castVoteUseCase, verifyVotersUseCase) } + + @Singleton + @Provides + fun provideAppContext(context: Context): AppContext { + return AppContext(context) + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt index 1c309fa5..e78dc5e3 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/home/HomeViewModel.kt @@ -1,21 +1,27 @@ package org.aossie.agoraandroid.ui.fragments.home +import android.content.Context +import android.content.Intent import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.yariksoffice.lingver.Lingver import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import org.aossie.agoraandroid.data.db.PreferenceProvider import org.aossie.agoraandroid.domain.useCases.homeFragment.HomeFragmentUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState import org.aossie.agoraandroid.utilities.ApiException import org.aossie.agoraandroid.utilities.AppConstants +import org.aossie.agoraandroid.utilities.LocaleUtil import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException import java.text.SimpleDateFormat import java.util.Calendar @@ -30,10 +36,9 @@ const val ACTIVE_ELECTION_COUNT = "activeElectionsCount" class HomeViewModel @Inject constructor( - private val homeViewModelUseCases: HomeFragmentUseCases + private val homeViewModelUseCases: HomeFragmentUseCases, + private val prefs: PreferenceProvider ) : ViewModel() { - private val _getLogoutStateFLow: MutableStateFlow?> = MutableStateFlow(null) - val getLogoutStateFlow: StateFlow?> = _getLogoutStateFLow var sessionExpiredListener: SessionExpiredListener? = null private val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) private val currentDate: Date = Calendar.getInstance() @@ -46,6 +51,15 @@ constructor( private val _countMediatorLiveData = MediatorLiveData>() val countMediatorLiveData = _countMediatorLiveData + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() + + private val _uiEvents = MutableSharedFlow() + val uiEvents = _uiEvents.asSharedFlow() + + val appLanguage = prefs.getAppLanguage() + val getSupportedLanguages = LocaleUtil.getSupportedLanguages() + init { _countMediatorLiveData.value = mutableMapOf( TOTAL_ELECTION_COUNT to 0, @@ -108,23 +122,44 @@ constructor( } fun doLogout() { - _getLogoutStateFLow.value = ResponseUI.loading() + showLoading("Logging you out...") viewModelScope.launch { try { homeViewModelUseCases.logOut() - _getLogoutStateFLow.value = ResponseUI.success() + hideSnackBar() + hideLoading() + _uiEvents.emit(UiEvents.UserLoggedOut) } catch (e: ApiException) { - _getLogoutStateFLow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _getLogoutStateFLow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _getLogoutStateFLow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } + fun changeLanguage(newLanguage: Pair, context: Context) { + viewModelScope.launch { + prefs.updateAppLanguage(newLanguage.second) + Lingver.getInstance().setLocale(context, newLanguage.second) + delay(500) + restartApp(context) + } + } + + private fun restartApp(context: Context) { + val intent = context.packageManager.getLaunchIntentForPackage(context.packageName) + intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } + + sealed class UiEvents{ + object UserLoggedOut:UiEvents() + } + private fun showLoading(message: Any) { _progressAndErrorState.value = progressAndErrorState.value.copy( loading = Pair(message,true) diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt index dea2aae1..5c632ddd 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileFragment.kt @@ -1,65 +1,35 @@ package org.aossie.agoraandroid.ui.fragments.profile -import android.Manifest -import android.app.Activity -import android.content.ContentResolver -import android.content.Intent -import android.content.pm.PackageManager -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri import android.os.Bundle -import android.provider.MediaStore -import android.text.Editable -import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog -import androidx.core.app.ActivityCompat -import androidx.core.net.toUri -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.viewModels -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import com.facebook.login.LoginManager -import com.squareup.picasso.NetworkPolicy.OFFLINE +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.merge import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R -import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.DialogChangeAvatarBinding -import org.aossie.agoraandroid.databinding.FragmentProfileBinding -import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.ui.fragments.BaseFragment import org.aossie.agoraandroid.ui.fragments.auth.login.LoginViewModel import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel -import org.aossie.agoraandroid.utilities.GetBitmapFromUri -import org.aossie.agoraandroid.utilities.HideKeyboard.hideKeyboardInFrag -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.canAuthenticateBiometric -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.isUrl -import org.aossie.agoraandroid.utilities.loadImage -import org.aossie.agoraandroid.utilities.loadImageFromMemoryNoCache -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toByteArray -import org.aossie.agoraandroid.utilities.toggleIsEnable -import java.io.File -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException +import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel.UiEvents.UserLoggedOut +import org.aossie.agoraandroid.ui.fragments.profile.ProfileViewModel.UiEvents.PasswordChanged +import org.aossie.agoraandroid.ui.fragments.profile.ProfileViewModel.UiEvents.TwoFactorAuthToggled +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject -const val CAMERA_PERMISSION_REQUEST_CODE = 1 -const val STORAGE_PERMISSION_REQUEST_CODE = 2 -const val CAMERA_INTENT_REQUEST_CODE = 3 -const val STORAGE_INTENT_REQUEST_CODE = 4 - class ProfileFragment @Inject constructor( @@ -67,240 +37,60 @@ constructor( private val prefs: PreferenceProvider ) : BaseFragment(viewModelFactory) { - private var mAvatar = MutableLiveData() - - private var encodedImage: String? = null - private val viewModel: ProfileViewModel by viewModels { viewModelFactory } private val loginViewModel: LoginViewModel by viewModels { viewModelFactory } - private val homeViewModel: HomeViewModel by viewModels { viewModelFactory } - - private lateinit var mUser: UserModel - private lateinit var binding: FragmentProfileBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentProfileBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - homeViewModel.sessionExpiredListener = this loginViewModel.sessionExpiredListener = this + viewModel.sessionExpiredListener = this - binding.firstNameTiet.doAfterTextChanged { - if (it.isNullOrEmpty()) binding.firstNameTil.error = getString(string.first_name_empty) - else binding.firstNameTil.error = null - } - binding.lastNameTiet.doAfterTextChanged { - if (it.isNullOrEmpty()) binding.lastNameTil.error = getString(string.last_name_empty) - else binding.lastNameTil.error = null - } - binding.newPasswordTiet.doAfterTextChanged { - when { - it.isNullOrEmpty() -> - binding.newPasswordTil.error = - getString(string.password_empty_warn) - else -> binding.newPasswordTil.error = null - } - checkNewPasswordAndConfirmPassword(it) - } - binding.confirmPasswordTiet.doAfterTextChanged { - when { - it.isNullOrEmpty() -> - binding.confirmPasswordTil.error = - getString(string.password_empty_warn) - it.toString() != binding.newPasswordTiet.text.toString() -> - binding.confirmPasswordTil.error = - getString(string.password_not_match_warn) - else -> binding.confirmPasswordTil.error = null - } - } - setObserver() - - binding.updateProfileBtn.setOnClickListener { - binding.progressBar.show() - toggleIsEnable() - if (binding.firstNameTil.error == null && binding.lastNameTil.error == null) { - hideKeyboardInFrag(this@ProfileFragment) - val updatedUser = mUser - updatedUser.let { - it.firstName = binding.firstNameTiet.text.toString() - it.lastName = binding.lastNameTiet.text.toString() - } - viewModel.updateUser( - updatedUser - ) - } else { - binding.progressBar.hide() - toggleIsEnable() - } - } - - binding.swBiometric.setOnCheckedChangeListener { buttonView, isChecked -> - lifecycleScope.launch { - prefs.enableBiometric(isChecked) - } - } - if (!requireContext().canAuthenticateBiometric()) binding.swBiometric.visibility = View.GONE - - binding.switchWidget.setOnClickListener { - if (binding.switchWidget.isChecked) { - AlertDialog.Builder(requireContext()) - .setTitle("Please Confirm") - .setMessage("Are you sure you want to enable two factor authentication") - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { dialog, _ -> - binding.progressBar.show() - toggleIsEnable() - viewModel.toggleTwoFactorAuth() - dialog.cancel() - } - .setNegativeButton(android.R.string.cancel) { dialog, _ -> - binding.switchWidget.isChecked = false - dialog.cancel() - } - .create() - .show() - } else { - AlertDialog.Builder(requireContext()) - .setTitle("Please Confirm") - .setMessage("Are you sure you want to disable two factor authentication") - .setCancelable(false) - .setPositiveButton(android.R.string.ok) { dialog, _ -> - binding.progressBar.show() - toggleIsEnable() - viewModel.toggleTwoFactorAuth() - dialog.cancel() - } - .setNegativeButton(android.R.string.cancel) { dialog, _ -> - binding.switchWidget.isChecked = true - dialog.cancel() - } - .create() - .show() - } - } - - binding.fabEditProfilePic.setOnClickListener { - showChangeProfileDialog() - } - - binding.changePasswordBtn.setOnClickListener { - val newPass = binding.newPasswordTiet.text.toString() - val conPass = binding.confirmPasswordTiet.text.toString() - when { - newPass.isEmpty() -> - binding.newPasswordTil.error = getString(string.password_empty_warn) - conPass.isEmpty() -> - binding.confirmPasswordTil.error = getString(string.password_empty_warn) - newPass != conPass -> - binding.confirmPasswordTil.error = getString(string.password_not_match_warn) - else -> updateUIAndChangePassword() - } - } - } - - private fun updateUIAndChangePassword() { - binding.progressBar.show() - toggleIsEnable() - hideKeyboardInFrag(this@ProfileFragment) - viewModel.changePassword(binding.newPasswordTiet.text.toString()) - } - - private fun decodeBitmap(encodedBitmap: String): Bitmap { - val decodedString = Base64.decode(encodedBitmap, Base64.NO_WRAP) - return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) - } - - private fun cacheAndSaveImage(url: String) { - binding.ivProfilePic.loadImage(url, OFFLINE) { - binding.ivProfilePic.loadImage(url) - } - } + composeView.setContent { - private fun setObserver() { - mAvatar.observe( - viewLifecycleOwner, - Observer { - binding.ivProfilePic.loadImageFromMemoryNoCache(it) - } - ) + val progressErrorState1 = viewModel.progressAndErrorState + val progressErrorState2 = homeViewModel.progressAndErrorState - lifecycleScope.launch { - viewModel.user.collect { - if (it != null) { - updateUI(it) - } - } - } + val progressErrorState by merge(progressErrorState1,progressErrorState2).collectAsState(initial = ScreensState()) - viewModel.passwordRequestCode.observe( - viewLifecycleOwner, - Observer { - handlePassword(it) - } - ) + val userData by viewModel.userModelState.collectAsState() + val profileDataState by viewModel.profileDataState.collectAsState() - lifecycleScope.launch { - loginViewModel.getLoginStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> onLoadingStarted() - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() + LaunchedEffect(key1 = viewModel) { + viewModel.uiEvents.collectLatest { + when(it) { + PasswordChanged -> { + loginViewModel.logInRequest( + userData?.username!!, profileDataState.confirmPassword, userData.trustedDevice + ) } - ResponseUI.Status.ERROR -> { - onError(it.message) + TwoFactorAuthToggled -> { + homeViewModel.doLogout() } - else -> {} } } } - } - - viewModel.userUpdateResponse.observe( - viewLifecycleOwner, - Observer { - handleUser(it) - } - ) - - viewModel.toggleTwoFactorAuthResponse.observe( - viewLifecycleOwner, - Observer { - handleTwoFactorAuthentication(it) - homeViewModel.doLogout() - } - ) - - viewModel.changeAvatarResponse.observe( - viewLifecycleOwner, - Observer { - handleChangeAvatar(it) - } - ) - lifecycleScope.launch { - homeViewModel.getLogoutStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.ERROR -> onError(it.message) - - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() + LaunchedEffect(key1 = viewModel) { + homeViewModel.uiEvents.collectLatest { + when(it) { + UserLoggedOut -> { lifecycleScope.launch { if (prefs.getIsFacebookUser().first()) { LoginManager.getInstance() @@ -308,294 +98,24 @@ constructor( } } homeViewModel.deleteUserData() - Navigation.findNavController(binding.root) - .navigate( + delay(2000) + findNavController().navigate( ProfileFragmentDirections.actionProfileFragmentToWelcomeFragment() ) } - ResponseUI.Status.LOADING -> onLoadingStarted() - else -> {} } } } - } - } - - private fun updateUI( - it: UserModel, - ) { - binding.userNameTv.text = it.username - binding.emailIdTv.text = it.email - binding.firstNameTiet.setText(it.firstName) - binding.lastNameTiet.setText(it.lastName) - binding.switchWidget.isChecked = it.twoFactorAuthentication ?: false - lifecycleScope.launch { - binding.swBiometric.isChecked = prefs.isBiometricEnabled().first() - } - mUser = it - if (it.avatarURL != null) { - if (it.avatarURL.isUrl()) - cacheAndSaveImage(it.avatarURL) - else { - val bitmap = decodeBitmap(it.avatarURL) - setAvatarFile(bitmap.toByteArray()) - } - } - } - - private fun onLoadingStarted() { - binding.progressBar.show() - toggleIsEnable() - } - - private fun onError(message: String?) { - binding.progressBar.hide() - notify(message) - toggleIsEnable() - } - - private fun showChangeProfileDialog() { - val dialogView = DialogChangeAvatarBinding.inflate(LayoutInflater.from(context)) - - val dialog = AlertDialog.Builder(requireContext()) - .setView(dialogView.root) - .create() - - dialogView.deleteProfile.setOnClickListener { - dialog.cancel() - deletePic() - } - - dialogView.cameraView.setOnClickListener { - dialog.cancel() - if (ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED - ) { - openCamera() - } else { - askCameraPermission() - } - } - - dialogView.galleryView.setOnClickListener { - dialog.cancel() - if (ActivityCompat.checkSelfPermission( - requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE - ) == PackageManager.PERMISSION_GRANTED - ) { - openGallery() - } else { - askReadStoragePermission() - } - } - - dialog.show() - } - - private fun deletePic() { - val imageUri = Uri.parse( - ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + resources.getResourcePackageName(R.drawable.ic_user1) + - '/' + resources.getResourceTypeName(R.drawable.ic_user1) + '/' + resources.getResourceEntryName( - R.drawable.ic_user1 - ) - ) - try { - val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(requireContext(), imageUri) - encodedImage = encodeJpegImage(bitmap!!) - val url = encodedImage!!.toUri() - binding.progressBar.show() - toggleIsEnable() - viewModel.changeAvatar( - url.toString(), - mUser - ) - } catch (e: FileNotFoundException) { - notify(getString(string.file_not_found)) - } - } - - private fun askReadStoragePermission() { - requestPermissions( - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), STORAGE_PERMISSION_REQUEST_CODE - ) - } - - private fun askCameraPermission() { - requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE) - } - - private fun handleChangeAvatar(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.profile_updated)) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun handleUser(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.user_updated)) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun handleTwoFactorAuthentication(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.authentication_updated)) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun handlePassword(response: ResponseUI) = when (response.status) { - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() - toggleIsEnable() - notify(getString(string.password_updated)) - loginViewModel.logInRequest( - mUser.username!!, binding.newPasswordTiet.text.toString(), mUser.trustedDevice - ) - } - ResponseUI.Status.ERROR -> onFailure(response.message) - else -> onStarted() - } - - private fun checkNewPasswordAndConfirmPassword(s: Editable?) { - if (s.toString() == binding.confirmPasswordTiet.text.toString() - .trim() - ) { - binding.confirmPasswordTil.error = null - } else { - if (!binding.confirmPasswordTiet.text.isNullOrEmpty()) { - binding.confirmPasswordTil.error = - getString(string.password_not_match_warn) - } - } - } - - private fun onStarted() { - binding.progressBar.show() - toggleIsEnable() - } - - private fun onFailure(message: String?) { - binding.progressBar.hide() - notify(message) - toggleIsEnable() - } - - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - intentData: Intent? - ) { - super.onActivityResult(requestCode, resultCode, intentData) - if (resultCode != Activity.RESULT_OK) return - - if (requestCode == STORAGE_INTENT_REQUEST_CODE && intentData?.data != null) { - val imageUri = intentData.data ?: return - try { - val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(requireContext(), imageUri) - encodedImage = encodeJpegImage(bitmap!!) - val url = encodedImage!!.toUri() - binding.progressBar.show() - toggleIsEnable() - viewModel.changeAvatar( - url.toString(), - mUser - ) - } catch (e: FileNotFoundException) { - notify(getString(string.file_not_found)) - } - } else if (requestCode == CAMERA_INTENT_REQUEST_CODE) { - val bitmap = intentData?.extras?.get("data") - if (bitmap is Bitmap) { - encodedImage = encodePngImage(bitmap) - val url = encodedImage!!.toUri() - binding.progressBar.show() - toggleIsEnable() - viewModel.changeAvatar( - url.toString(), - mUser - ) - } - } - } - - private fun openGallery() { - val galleryIntent = Intent() - galleryIntent.let { - it.type = "image/*" - it.action = Intent.ACTION_GET_CONTENT - } - startActivityForResult(galleryIntent, STORAGE_INTENT_REQUEST_CODE) - } - - private fun openCamera() { - val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - startActivityForResult(cameraIntent, CAMERA_INTENT_REQUEST_CODE) - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - if (requestCode == STORAGE_PERMISSION_REQUEST_CODE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - openGallery() - } else { - notify(getString(string.permission_denied)) - } - } else if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - openCamera() - } else { - notify(getString(string.permission_denied)) - } - } - } - - private fun encodeJpegImage(bitmap: Bitmap): String { - val bytes = bitmap.toByteArray(Bitmap.CompressFormat.JPEG) - setAvatarFile(bytes) - return Base64.encodeToString(bytes, Base64.NO_WRAP) - } - - private fun encodePngImage(bitmap: Bitmap): String { - val bytes = bitmap.toByteArray(Bitmap.CompressFormat.PNG) - setAvatarFile(bytes) - return Base64.encodeToString(bytes, Base64.NO_WRAP) - } - - private fun setAvatarFile(bytes: ByteArray) { - try { - val avatar = File(context?.cacheDir, "avatar") - if (avatar.exists()) { - avatar.delete() + AgoraTheme { + ProfileScreen( + prefs = prefs, + screenState = progressErrorState, + userData = userData, + profileDataState = profileDataState, + ){ + viewModel.onEvent(it) + } } - val fos = FileOutputStream(avatar) - fos.write(bytes) - fos.flush() - fos.close() - mAvatar.value = avatar - } catch (e: IOException) { - e.printStackTrace() - notify(getString(string.error_loading_image)) } } - - private fun toggleIsEnable() { - binding.updateProfileBtn.toggleIsEnable() - binding.changePasswordBtn.toggleIsEnable() - } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt index f1f76c42..597dd08f 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/profile/ProfileViewModel.kt @@ -1,64 +1,215 @@ package org.aossie.agoraandroid.ui.fragments.profile -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.util.Base64 +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.network.responses.AuthToken import org.aossie.agoraandroid.domain.model.UpdateUserDtoModel import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.domain.useCases.profile.ProfileUseCases +import org.aossie.agoraandroid.ui.di.models.AppContext import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenDataState +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.ChangePasswordClick +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.DeletePic +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredConfirmPassword +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredFirstName +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredLastName +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.EnteredPassword +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.ToggleTwoFactor +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.UpdateImage +import org.aossie.agoraandroid.ui.screens.profile.ProfileScreenEvent.UpdateProfileClick import org.aossie.agoraandroid.utilities.ApiException +import org.aossie.agoraandroid.utilities.AppConstants +import org.aossie.agoraandroid.utilities.GetBitmapFromUri import org.aossie.agoraandroid.utilities.NoInternetException -import org.aossie.agoraandroid.utilities.ResponseUI import org.aossie.agoraandroid.utilities.SessionExpirationException +import org.aossie.agoraandroid.utilities.toByteArray import timber.log.Timber +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException import javax.inject.Inject class ProfileViewModel @Inject constructor( - private val profileUseCases: ProfileUseCases + private val profileUseCases: ProfileUseCases, + private val appContext: AppContext ) : ViewModel() { val user = profileUseCases.getUser() - private lateinit var sessionExpiredListener: SessionExpiredListener - private val _passwordRequestCode = MutableLiveData>() + var sessionExpiredListener: SessionExpiredListener? = null - val passwordRequestCode: LiveData> - get() = _passwordRequestCode + private val _userModelState = MutableStateFlow (UserModel()) + val userModelState = _userModelState.asStateFlow() - private val _userUpdateResponse = MutableLiveData>() + private val _progressAndErrorState = MutableStateFlow (ScreensState()) + val progressAndErrorState = _progressAndErrorState.asStateFlow() - val userUpdateResponse: LiveData> - get() = _userUpdateResponse + private val _profileDataState = MutableStateFlow (ProfileScreenDataState()) + val profileDataState = _profileDataState.asStateFlow() - private val _toggleTwoFactorAuthResponse = MutableLiveData>() + private val _uiEvents = MutableSharedFlow() + val uiEvents = _uiEvents.asSharedFlow() - val toggleTwoFactorAuthResponse: LiveData> - get() = _toggleTwoFactorAuthResponse + init { + viewModelScope.launch { + user.collectLatest { + _userModelState.value = it + if(profileDataState.value.avatar==null){ + val bitmap = decodeBitmap(it.avatarURL!!) + setAvatar(bitmap) + } + _profileDataState.value = profileDataState.value.copy( + firstName = it.firstName?:"", + lastName = it.lastName?:"", + ) + } + } + } + + fun onEvent(event: ProfileScreenEvent) { + when(event) { + ChangePasswordClick -> { + val pass1 = profileDataState.value.newPassword + val pass2 = profileDataState.value.confirmPassword - private val _changeAvatarResponse = MutableLiveData>() + if(pass1.isEmpty() || pass2.isEmpty()) { + showMessage(string.password_empty_warn) + }else if(pass1 != pass2) { + showMessage(string.password_not_match_warn) + }else { + changePassword(pass2) + } + } + is EnteredConfirmPassword -> { + _profileDataState.value = profileDataState.value.copy( + confirmPassword = event.confirmPassword + ) + } + is EnteredFirstName -> { + _profileDataState.value = profileDataState.value.copy( + firstName = event.firstName + ) + } + is EnteredLastName -> { + _profileDataState.value = profileDataState.value.copy( + lastName = event.lastName + ) + } + is EnteredPassword -> { + _profileDataState.value = profileDataState.value.copy( + newPassword = event.password + ) + } + is ToggleTwoFactor -> { + showLoading("${if (event.checked) "Enabling" else "Disabling"} two factor authentication...") + toggleTwoFactorAuth() + } + UpdateProfileClick -> { + val firstName = profileDataState.value.firstName.trim() + val lastName = profileDataState.value.lastName.trim() - val changeAvatarResponse: LiveData> - get() = _changeAvatarResponse + if(firstName.isNullOrEmpty()) { + showMessage(string.first_name_empty) + }else if(lastName.isNullOrEmpty()) { + showMessage(string.last_name_empty) + }else { + val updatedUser = userModelState.value + updatedUser.let { + it.firstName = firstName + it.lastName = lastName + } + updateUser( + updatedUser + ) + } + } + is DeletePic -> { + deletePic() + } + is UpdateImage -> { + updateImage(event.uri) + } + } + } - fun changePassword(password: String) { + private fun updateImage(uri: Uri) = viewModelScope.launch { + showLoading("Updating image...") + try { + val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(appContext.context, uri) + val encodedImage = encodeJpegImage(bitmap!!) + val url = encodedImage!!.toUri() + changeAvatar( + url.toString(), + userModelState.value + ) + } catch (e: FileNotFoundException) { + showMessage(string.file_not_found) + } + } + private fun encodeJpegImage(bitmap: Bitmap): String { + val bytes = bitmap.toByteArray(Bitmap.CompressFormat.JPEG) + return Base64.encodeToString(bytes, Base64.NO_WRAP) + } + + private fun deletePic() = viewModelScope.launch { + showLoading("Deleting avatar..") + val imageUri = Uri.parse( + ContentResolver.SCHEME_ANDROID_RESOURCE + + "://" + appContext.context.resources.getResourcePackageName(R.drawable.ic_user1) + + '/' + appContext.context.resources.getResourceTypeName(R.drawable.ic_user1) + '/' + appContext.context.resources.getResourceEntryName( + R.drawable.ic_user1 + ) + ) + try { + val bitmap = GetBitmapFromUri.handleSamplingAndRotationBitmap(appContext.context, imageUri) + val encodedImage = encodeJpegImage(bitmap!!) + val url = encodedImage!!.toUri() + changeAvatar( + url.toString(), + userModelState.value + ) + } catch (e: FileNotFoundException) { + showMessage(string.file_not_found) + } + } + + fun changePassword(password: String) { + showLoading("Changing password...") viewModelScope.launch { try { profileUseCases.changePassword(password) - _passwordRequestCode.value = ResponseUI.success() + hideLoading() + showMessage(R.string.password_updated) + _uiEvents.emit(UiEvents.PasswordChanged) } catch (e: ApiException) { - _passwordRequestCode.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _passwordRequestCode.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _passwordRequestCode.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -67,11 +218,9 @@ constructor( url: String, user: UserModel ) { - viewModelScope.launch { try { profileUseCases.changeAvatar(url) - val authResponse = profileUseCases.getUserData() Timber.d(authResponse.toString()) authResponse.let { @@ -82,16 +231,18 @@ constructor( user.trustedDevice ) profileUseCases.saveUser(mUser) + showMessage(string.profile_updated) + val bitmap = decodeBitmap(mUser.avatarURL!!) + setAvatar(bitmap) } - _changeAvatarResponse.value = ResponseUI.success() } catch (e: ApiException) { - _changeAvatarResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _changeAvatarResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _changeAvatarResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -100,15 +251,16 @@ constructor( viewModelScope.launch { try { profileUseCases.toggleTwoFactorAuth() - _toggleTwoFactorAuthResponse.value = ResponseUI.success() + showMessage(R.string.authentication_updated) + _uiEvents.emit(UiEvents.TwoFactorAuthToggled) } catch (e: ApiException) { - _toggleTwoFactorAuthResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _toggleTwoFactorAuthResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _toggleTwoFactorAuthResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -116,6 +268,7 @@ constructor( fun updateUser( user: UserModel ) { + showLoading("Updating profile...") viewModelScope.launch { try { val updateUserDtoModel = UpdateUserDtoModel( @@ -130,16 +283,83 @@ constructor( ) profileUseCases.updateUser(updateUserDtoModel) profileUseCases.saveUser(user) - _userUpdateResponse.value = ResponseUI.success() + showMessage(R.string.user_updated) } catch (e: ApiException) { - _userUpdateResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { - sessionExpiredListener.onSessionExpired() + sessionExpiredListener?.onSessionExpired() } catch (e: NoInternetException) { - _userUpdateResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _userUpdateResponse.value = ResponseUI.error(e.message) + showMessage(e.message!!) + } + } + } + + private fun decodeBitmap(encodedBitmap: String): Bitmap { + val decodedString = Base64.decode(encodedBitmap, Base64.NO_WRAP) + return BitmapFactory.decodeByteArray(decodedString, 0, decodedString.size) + } + + private fun setAvatar(bitmap: Bitmap) = viewModelScope.launch { + val bytes = bitmap.toByteArray(Bitmap.CompressFormat.PNG) + try { + val avatarFolder = File(appContext.context.cacheDir, "avatars") + if (!avatarFolder.exists()) { + avatarFolder.mkdir() + } else { + avatarFolder.listFiles()?.forEach { file -> + file.delete() + } + } + val avatar = File(avatarFolder, "avatar_${System.currentTimeMillis()}.png") + if (avatar.exists()) { + avatar.delete() } + val fos = FileOutputStream(avatar) + fos.write(bytes) + fos.flush() + fos.close() + _profileDataState.value = profileDataState.value.copy( + avatar = avatar + ) + } catch (e: IOException) { + e.printStackTrace() + showMessage(string.error_loading_image) + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() } } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } + + sealed class UiEvents{ + object PasswordChanged:UiEvents() + object TwoFactorAuthToggled:UiEvents() + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt index 941ebdc6..14684a71 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/settings/SettingsFragment.kt @@ -7,32 +7,39 @@ import android.util.Base64 import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation +import androidx.navigation.fragment.findNavController import com.facebook.login.LoginManager -import com.squareup.picasso.NetworkPolicy.OFFLINE +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import org.aossie.agoraandroid.R.string import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentSettingsBinding import org.aossie.agoraandroid.domain.model.UserModel import org.aossie.agoraandroid.ui.fragments.BaseFragment import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel +import org.aossie.agoraandroid.ui.fragments.home.HomeViewModel.UiEvents.UserLoggedOut import org.aossie.agoraandroid.ui.fragments.profile.ProfileViewModel -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.hide +import org.aossie.agoraandroid.ui.screens.settings.SettingScreen +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.ChangeAppLanguage +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnAboutUsClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnAccountSettingClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnContactUsClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnLogoutClick +import org.aossie.agoraandroid.ui.screens.settings.SettingsScreenEvent.OnShareWithOthersClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import org.aossie.agoraandroid.utilities.isUrl -import org.aossie.agoraandroid.utilities.loadImage -import org.aossie.agoraandroid.utilities.loadImageFromMemoryNoCache -import org.aossie.agoraandroid.utilities.show import org.aossie.agoraandroid.utilities.toByteArray -import org.aossie.agoraandroid.utilities.toggleIsEnable import java.io.File import java.io.FileOutputStream import java.io.IOException @@ -54,7 +61,7 @@ constructor( private var mAvatar = MutableLiveData() - private lateinit var binding: FragmentSettingsBinding + private lateinit var composeView: ComposeView private lateinit var mUser: UserModel @@ -67,49 +74,28 @@ constructor( container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentSettingsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - lifecycleScope.launch { - val user = viewModel.user - user.collect { - if (it != null) { - binding.tvEmailId.text = it.email - binding.tvName.text = (it.firstName ?: "") + " " + (it.lastName ?: "") - mUser = it - if (it.avatarURL != null) { - if (it.avatarURL.isUrl()) - cacheAndSaveImage(it.avatarURL) - else { - val bitmap = decodeBitmap(it.avatarURL) - setAvatar(bitmap) - } - } - } - } - } + homeViewModel.sessionExpiredListener = this - mAvatar.observe( - viewLifecycleOwner, - Observer { - binding.imageView.loadImageFromMemoryNoCache(it) - } - ) - - lifecycleScope.launch { - homeViewModel.getLogoutStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.ERROR -> { - binding.progressBar.hide() - notify(it.message) - binding.tvLogout.toggleIsEnable() - } - ResponseUI.Status.SUCCESS -> { - binding.progressBar.hide() + composeView.setContent { + + val context = LocalContext.current + val userState by viewModel.user.collectAsState(UserModel()) + val appLanguageState by homeViewModel.appLanguage.collectAsState("") + val supportedLang = homeViewModel.getSupportedLanguages + val avatar by mAvatar.observeAsState() + val progressErrorState by homeViewModel.progressAndErrorState.collectAsState() + + LaunchedEffect(key1 = Unit) { + homeViewModel.uiEvents.collectLatest { event -> + when(event){ + UserLoggedOut -> { lifecycleScope.launch { if (prefs.getIsFacebookUser().first()) { LoginManager.getInstance() @@ -118,56 +104,58 @@ constructor( } homeViewModel.deleteUserData() notify("Logged Out") - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToWelcomeFragment() - ) + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToWelcomeFragment() + ) } - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - binding.tvLogout.toggleIsEnable() + } + } + + viewModel.user.collect { + if (it != null) { + mUser = it + if (it.avatarURL != null) { + if (!it.avatarURL.isUrl()) { + val bitmap = decodeBitmap(it.avatarURL) + setAvatar(bitmap) + } } - else -> {} } } } - } - homeViewModel.sessionExpiredListener = this - - binding.tvAccountSettings.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate(SettingsFragmentDirections.actionSettingsFragmentToProfileFragment()) - } - binding.tvShare.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToShareWithOthersFragment() - ) - } - - binding.tvAbout.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToAboutFragment() - ) - } - - binding.tvContactUs.setOnClickListener { - Navigation.findNavController(binding.root) - .navigate( - SettingsFragmentDirections.actionSettingsFragmentToContactUsFragment() - ) - } - - binding.tvLogout.setOnClickListener { - homeViewModel.doLogout() - } - } - - private fun cacheAndSaveImage(url: String) { - binding.imageView.loadImage(url, OFFLINE) { - binding.imageView.loadImage(url) + AgoraTheme { + SettingScreen(userState,avatar,appLanguageState,supportedLang,progressErrorState){ event-> + when(event){ + OnAboutUsClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToAboutFragment() + ) + } + OnAccountSettingClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToProfileFragment() + ) + } + OnContactUsClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToContactUsFragment() + ) + } + OnLogoutClick -> { + homeViewModel.doLogout() + } + OnShareWithOthersClick -> { + findNavController().navigate( + SettingsFragmentDirections.actionSettingsFragmentToShareWithOthersFragment() + ) + } + is ChangeAppLanguage -> { + homeViewModel.changeLanguage(event.language,context) + } + } + } + } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt new file mode 100644 index 00000000..abcf4f24 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreen.kt @@ -0,0 +1,281 @@ +package org.aossie.agoraandroid.ui.screens.profile + +import android.content.Context +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.rememberMultiplePermissionsState +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import org.aossie.agoraandroid.BuildConfig +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.data.db.PreferenceProvider +import org.aossie.agoraandroid.domain.model.UserModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PermissionsDialog +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryLabelTextField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryPasswordField +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.profile.component.ConfirmationDialog +import org.aossie.agoraandroid.ui.screens.profile.component.IconTextSwitchButton +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileButtonOptions.Camera +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileButtonOptions.Delete +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileButtonOptions.Gallery +import org.aossie.agoraandroid.ui.screens.profile.component.ProfileItem +import org.aossie.agoraandroid.utilities.canAuthenticateBiometric +import java.io.File +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Objects + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) +@Composable +fun ProfileScreen( + prefs: PreferenceProvider, + screenState: ScreensState, + userData: UserModel?, + profileDataState: ProfileScreenDataState, + onEvent: (ProfileScreenEvent) -> Unit +) { + + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + + val twoFactorAuth = remember { + mutableStateOf(false) + } + + val bioMetricState = remember { + mutableStateOf(false) + } + + val showTwoFactorDialog = remember { + mutableStateOf(false) + } + val storagePermissionState = rememberMultiplePermissionsState( + listOf(android.Manifest.permission.READ_EXTERNAL_STORAGE,android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + ) + val cameraPermissionState = rememberMultiplePermissionsState( + listOf( android.Manifest.permission.CAMERA) + ) + val permissionDialog = remember { mutableStateOf(Pair("",false) to storagePermissionState) } + val storagePermissionText = stringResource(id = string.storage_permission_required) + val cameraPermissionText = stringResource(id = string.camera_permission_required) + + val getImageFromGallery = rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetContent(), + ) { uri: Uri? -> + uri?.let { + onEvent(ProfileScreenEvent.UpdateImage(it)) + } + } + + val file = context.createImageFile() + val uri = FileProvider.getUriForFile( + Objects.requireNonNull(context), + BuildConfig.APPLICATION_ID + ".provider", file + ) + + val cameraLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) { + onEvent(ProfileScreenEvent.UpdateImage(uri)) + } + + LaunchedEffect(key1 = prefs) { + bioMetricState.value = prefs.isBiometricEnabled().first() + } + + LaunchedEffect(key1 = userData) { + userData?.let { + twoFactorAuth.value = it.twoFactorAuthentication?:false + } + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box(modifier = Modifier.padding(it)){ + + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(20.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + + item { + ProfileItem(userModel = userData, avatar = profileDataState.avatar) { + when(it) { + Delete -> { + onEvent(ProfileScreenEvent.DeletePic) + } + Camera -> { + if(cameraPermissionState.allPermissionsGranted){ + cameraLauncher.launch(uri) + }else{ + if (cameraPermissionState.shouldShowRationale) { + permissionDialog.value = Pair(cameraPermissionText, true) to cameraPermissionState + } else { + permissionDialog.value = Pair(cameraPermissionText, true) to cameraPermissionState + } + } + } + Gallery -> { + if(storagePermissionState.allPermissionsGranted){ + getImageFromGallery.launch("image/*") + }else{ + if (storagePermissionState.shouldShowRationale) { + permissionDialog.value = Pair(storagePermissionText, true) to storagePermissionState + } else { + permissionDialog.value = Pair(storagePermissionText, true) to storagePermissionState + } + } + } + } + } + } + item { + Spacer(modifier = Modifier.height(10.dp)) + } + item { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Box(modifier = Modifier.weight(0.45f)) { + PrimaryLabelTextField( + label = stringResource(id = string.first_name), + value = profileDataState.firstName, + onValueChange = { + onEvent(ProfileScreenEvent.EnteredFirstName(it)) + } + ) + } + Box(modifier = Modifier.weight(0.45f)) { + PrimaryLabelTextField( + label = stringResource(id = string.last_name), + value = profileDataState.lastName, + onValueChange = { + onEvent(ProfileScreenEvent.EnteredLastName(it)) + } + ) + } + } + } + item { + PrimaryButton(text = stringResource(id = string.update_profile)) { + onEvent(ProfileScreenEvent.UpdateProfileClick) + } + } + item { + PrimaryPasswordField( + label = stringResource(id = string.new_password), + password = profileDataState.newPassword, + onPasswordChange = { + onEvent(ProfileScreenEvent.EnteredPassword(it)) + } + ) + } + item { + PrimaryPasswordField( + label = stringResource(id = string.confirm_new_password), + password = profileDataState.confirmPassword, + onPasswordChange = { + onEvent(ProfileScreenEvent.EnteredConfirmPassword(it)) + } + ) + } + item { + PrimaryButton(text = stringResource(id = string.change_password)) { + onEvent(ProfileScreenEvent.ChangePasswordClick) + } + } + item { + IconTextSwitchButton( + text = stringResource(id = string.toggle_two_factor_authentication), + iconStart = drawable.ic_two_factor_auth, + checked = twoFactorAuth.value, + onCheckedChange = { + twoFactorAuth.value = it + showTwoFactorDialog.value = true + } + ) + } + if(context.canAuthenticateBiometric()){ + item { + IconTextSwitchButton( + text = stringResource(id = string.toggle_biometric_authentication), + iconStart = drawable.ic_fingerprint_24, + checked = bioMetricState.value, + onCheckedChange = { + bioMetricState.value = it + coroutineScope.launch { + prefs.enableBiometric(it) + } + } + ) + } + } + } + + if (permissionDialog.value.first.second) { + PermissionsDialog( + title = "Permission !", + description = permissionDialog.value.first.first, + onDialogConfirm = { + permissionDialog.value.second.launchMultiplePermissionRequest() + permissionDialog.value = Pair("",false) to storagePermissionState + }, + onDialogDismiss = { + permissionDialog.value = Pair("",false) to storagePermissionState + } + ) + } + + ConfirmationDialog( + showDialog = showTwoFactorDialog.value, + enableTwoFactor = twoFactorAuth.value, + onConfirm = { + showTwoFactorDialog.value = false + onEvent(ProfileScreenEvent.ToggleTwoFactor(twoFactorAuth.value)) + }) { + showTwoFactorDialog.value = false + twoFactorAuth.value = !twoFactorAuth.value + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} + +fun Context.createImageFile(): File { + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + val image = File.createTempFile( + imageFileName, + ".jpg", + externalCacheDir + ) + return image +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt new file mode 100644 index 00000000..9b3e44d6 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenDataState.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.ui.screens.profile + +import java.io.File + +data class ProfileScreenDataState( + val firstName:String = "", + val lastName:String = "", + val newPassword:String = "", + val confirmPassword:String = "", + val avatar:File? = null +) \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt new file mode 100644 index 00000000..98dcf841 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/ProfileScreenEvent.kt @@ -0,0 +1,16 @@ +package org.aossie.agoraandroid.ui.screens.profile + +import android.net.Uri + +sealed class ProfileScreenEvent{ + data class EnteredFirstName(val firstName:String): ProfileScreenEvent() + data class EnteredLastName(val lastName:String): ProfileScreenEvent() + data class EnteredPassword(val password:String): ProfileScreenEvent() + data class EnteredConfirmPassword(val confirmPassword:String): ProfileScreenEvent() + object UpdateProfileClick: ProfileScreenEvent() + object ChangePasswordClick: ProfileScreenEvent() + data class ToggleTwoFactor(val checked: Boolean): ProfileScreenEvent() + data class UpdateImage(val uri: Uri,): ProfileScreenEvent() + object DeletePic: ProfileScreenEvent() + +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt new file mode 100644 index 00000000..a4666ba2 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ConfirmationDialog.kt @@ -0,0 +1,52 @@ +package org.aossie.agoraandroid.ui.screens.profile.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ConfirmationDialog( + showDialog: Boolean, + enableTwoFactor:Boolean, + onConfirm: () -> Unit, + onCancel: () -> Unit +) { + if (showDialog) { + AlertDialog( + onDismissRequest = onCancel, + title = { + Text("Please Confirm") + }, + text = { + if(enableTwoFactor){ + Text("Are you sure you want to enable two-factor authentication?") + }else{ + Text("Are you sure you want to disable two factor authentication?") + } + }, + confirmButton = { + Button( + onClick = { + onConfirm() + } + ) { + Text("OK") + } + }, + dismissButton = { + Button( + onClick = { + onCancel() + } + ) { + Text("Cancel") + } + }, + modifier = Modifier.padding(16.dp) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt new file mode 100644 index 00000000..c0cc8a57 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/IconTextSwitchButton.kt @@ -0,0 +1,57 @@ +package org.aossie.agoraandroid.ui.screens.profile.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.material3.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 + +@Composable +fun IconTextSwitchButton( + text: String, + iconStart: Int, + checked:Boolean, + onCheckedChange: (Boolean) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Box( + modifier = Modifier + .size(44.dp) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(10.dp) + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = iconStart), + contentDescription = "", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = text, + style = MaterialTheme.typography.titleMedium + ) + } + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt new file mode 100644 index 00000000..aa8e168b --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/profile/component/ProfileItem.kt @@ -0,0 +1,150 @@ +package org.aossie.agoraandroid.ui.screens.profile.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.PhotoCamera +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +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.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest.Builder +import kotlinx.coroutines.Dispatchers +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.UserModel +import java.io.File + +enum class ProfileButtonOptions { + Delete, Camera, Gallery +} + +@Composable +fun ProfileItem(userModel: UserModel?, avatar: File?, changeAvatarClick: (ProfileButtonOptions) -> Unit) { + + val context = LocalContext.current + + val dropDownMenuState = remember { mutableStateOf(false) } + val menuItems = listOf( + stringResource(id = string.delete) to painterResource(id = drawable.ic_delete_24) to ProfileButtonOptions.Delete, + stringResource(id = string.camera) to painterResource(id = drawable.ic_camera) to ProfileButtonOptions.Camera, + stringResource(id = string.gallery) to painterResource(id = drawable.ic_gallery) to ProfileButtonOptions.Gallery + ) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Box( + modifier = Modifier.size(140.dp) + ) { + Card( + modifier = Modifier + .size(120.dp) + .align(Alignment.TopCenter), + ) { + if (userModel != null) { + AsyncImage( + model = Builder(LocalContext.current) + .data(avatar?: drawable.ic_user_new) + .dispatcher(Dispatchers.IO) + .error(drawable.ic_user_new) + .crossfade(true) + .build(), + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = "" + ) + } else { + CircularProgressIndicator() + } + } + Box( + modifier = Modifier + .size(60.dp) + .background( + color = MaterialTheme.colorScheme.background, + shape = CircleShape, + ) + .align(Alignment.BottomEnd) + ) { + Box( + modifier = Modifier + .size(50.dp) + .clip(CircleShape) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + ) + .align(Alignment.Center) + .clickable { dropDownMenuState.value = true }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Rounded.PhotoCamera, + contentDescription = "", + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + DropdownMenu( + modifier = Modifier.align(Alignment.BottomEnd), + expanded = dropDownMenuState.value, + onDismissRequest = { dropDownMenuState.value = false } + ) { + menuItems.forEach { item -> + DropdownMenuItem( + text = { + Text(text = item.first.first) + }, + leadingIcon = { + Icon( + painter = item.first.second, + contentDescription = "" + ) + }, + onClick = { + changeAvatarClick(item.second) + dropDownMenuState.value = false + } + ) + } + } + } + } + } + userModel?.let { + Text( + text = (userModel.username ?: "") , + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = userModel.email?: "", + style = MaterialTheme.typography.titleMedium + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt new file mode 100644 index 00000000..a80603f9 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingScreen.kt @@ -0,0 +1,219 @@ +package org.aossie.agoraandroid.ui.screens.settings + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import coil.compose.AsyncImage +import coil.request.ImageRequest +import kotlinx.coroutines.Dispatchers +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.domain.model.UserModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.IconTextNavigationButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.settings.component.LanguageUpdateDialog +import java.io.File + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingScreen( + userModel: UserModel, + avatar: File?, + appLangState: String, + supportedLang: List>, + screenState: ScreensState, + screenEvent: (SettingsScreenEvent) -> Unit +) { + + val languageDialogState = remember { mutableStateOf(false) } + val selectedLang = supportedLang.find { + it.second == appLangState + } + + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box(modifier = Modifier.padding(it)) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(vertical = 25.dp), + ) { + item { + ProfileItem(userModel,avatar) + } + item { + Spacer(modifier = Modifier.height(15.dp)) + Divider( + modifier = Modifier.padding(horizontal = 25.dp), + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + ) + Spacer(modifier = Modifier.height(15.dp)) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.language), + arrowText = selectedLang?.first ?: "English", + iconStart = R.drawable.ic_translate, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + languageDialogState.value = true + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.account_settings), + iconStart = R.drawable.ic_user_new, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnAccountSettingClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.about_us), + iconStart = R.drawable.ic_info, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnAboutUsClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.share_with_others), + iconStart = R.drawable.ic_share, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnShareWithOthersClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.contact_us), + iconStart = R.drawable.ic_contact_us, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnContactUsClick) + } + ) + } + item { + IconTextNavigationButton( + text = stringResource(id = R.string.logout), + iconStart = R.drawable.ic_logout, + iconEnd = { + Icon(imageVector = Icons.Rounded.NavigateNext, contentDescription = "") + }, + onClick = { + screenEvent(SettingsScreenEvent.OnLogoutClick) + } + ) + } + } + + if(languageDialogState.value){ + LanguageUpdateDialog( + languages = supportedLang, + onDismissRequest = { + languageDialogState.value = false + }, + onConfirmRequest = { + screenEvent(SettingsScreenEvent.ChangeAppLanguage(it)) + }, + selectedLang = selectedLang!!, + ) + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} + +@Composable +fun ProfileItem(userModel: UserModel, avatar: File?) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 25.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Card( + modifier = Modifier.size(100.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 10.dp + ) + ) { + AsyncImage( + model = ImageRequest + .Builder(LocalContext.current) + .data(avatar?.toUri() ?: userModel.avatarURL) + .dispatcher(Dispatchers.IO) + .error(R.drawable.ic_user) + .crossfade(true) + .build(), + modifier = Modifier + .fillMaxSize(), + contentScale = ContentScale.Crop, + contentDescription = "") + } + Column( + modifier = Modifier.fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text( + text = (userModel.firstName ?: "") + " " + (userModel.lastName ?: ""), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + text = userModel.email?: "", + style = MaterialTheme.typography.titleMedium + ) + } + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt new file mode 100644 index 00000000..48643f83 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/SettingsScreenEvent.kt @@ -0,0 +1,10 @@ +package org.aossie.agoraandroid.ui.screens.settings + +sealed class SettingsScreenEvent{ + data class ChangeAppLanguage(val language: Pair):SettingsScreenEvent() + object OnAccountSettingClick:SettingsScreenEvent() + object OnAboutUsClick:SettingsScreenEvent() + object OnShareWithOthersClick:SettingsScreenEvent() + object OnContactUsClick:SettingsScreenEvent() + object OnLogoutClick:SettingsScreenEvent() +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt new file mode 100644 index 00000000..d237cd06 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageRadioButtonItem.kt @@ -0,0 +1,38 @@ +package org.aossie.agoraandroid.ui.screens.settings.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Checkbox +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +fun LanguageRadioButtonItem( + selected: Pair, + onCheckedChange: (Pair) -> Unit, + language: Pair +){ + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp)) { + Checkbox(checked = selected.second==language.second, onCheckedChange = { + if(it){ + onCheckedChange(language) + }else{ + onCheckedChange(language) + } + }) + Text(text = language.first, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt new file mode 100644 index 00000000..bf2f590e --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/settings/component/LanguageUpdateDialog.kt @@ -0,0 +1,91 @@ +package org.aossie.agoraandroid.ui.screens.settings.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.AlertDialogDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import org.aossie.agoraandroid.R + +@Composable +fun LanguageUpdateDialog( + languages: List>, + onDismissRequest:() -> Unit, + onConfirmRequest:(Pair) -> Unit, + selectedLang: Pair, +) { + val selected = remember { + mutableStateOf(selectedLang) + } + Dialog( + onDismissRequest = onDismissRequest, + ) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = MaterialTheme.shapes.large, + tonalElevation = AlertDialogDefaults.TonalElevation + ) { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(15.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = stringResource(id = R.string.language), + style = MaterialTheme.typography.headlineMedium + ) + LazyColumn( + modifier = Modifier + .heightIn(max = 400.dp) + .fillMaxWidth() + ) { + itemsIndexed(languages) { index, item -> + LanguageRadioButtonItem( + language = item, + selected = selected.value, + onCheckedChange = { + selected.value = it + } + ) + } + } + Row(modifier = Modifier.align(Alignment.End)) { + TextButton( + onClick = onDismissRequest, + ) { + Text("Cancel") + } + TextButton( + onClick = { + if(selected.value != selectedLang){ + onConfirmRequest(selected.value!!) + }else{ + onDismissRequest.invoke() + } + }, + ) { + Text("Update") + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt index 6ef77a7e..03739b6b 100644 --- a/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/AppConstants.kt @@ -38,6 +38,7 @@ object AppConstants { const val CIPHER_TRANSFORMATION = "AES/CBC/PKCS7Padding" const val SALT = "QWlGNHNhMTJTQWZ2bGhpV3U=" const val IV = "bVQzNFNhRkQ1Njc4UUFaWA==" + const val DEFAULT_LANG = "en" enum class Status { PENDING, ACTIVE, FINISHED } diff --git a/app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt b/app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt new file mode 100644 index 00000000..bc35d284 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/utilities/LocaleUtil.kt @@ -0,0 +1,11 @@ +package org.aossie.agoraandroid.utilities + +object LocaleUtil { + + fun getSupportedLanguages(): List> { + return listOf( + Pair("English","en"), + Pair("Hindi","hi"), + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_art.xml b/app/src/main/res/drawable/ic_art.xml deleted file mode 100644 index 002b5c7d..00000000 --- a/app/src/main/res/drawable/ic_art.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_camera.xml b/app/src/main/res/drawable/ic_camera.xml index bc65ddfb..dff8eb4b 100644 --- a/app/src/main/res/drawable/ic_camera.xml +++ b/app/src/main/res/drawable/ic_camera.xml @@ -1,15 +1,10 @@ + android:width="30dp" + android:height="30dp" + android:viewportWidth="18" + android:viewportHeight="18"> - - + android:pathData="M7.333,15.75H10.667C13.007,15.75 14.178,15.75 15.019,15.199C15.382,14.961 15.694,14.654 15.938,14.296C16.5,13.471 16.5,12.321 16.5,10.023C16.5,7.724 16.5,6.575 15.938,5.75C15.694,5.392 15.382,5.085 15.019,4.847C14.479,4.492 13.802,4.366 12.767,4.321C12.272,4.321 11.847,3.953 11.75,3.477C11.676,3.128 11.484,2.816 11.206,2.592C10.929,2.368 10.582,2.248 10.226,2.25H7.774C7.034,2.25 6.395,2.764 6.25,3.477C6.153,3.953 5.728,4.321 5.234,4.321C4.199,4.366 3.522,4.493 2.981,4.847C2.619,5.085 2.307,5.392 2.063,5.75C1.5,6.575 1.5,7.724 1.5,10.023C1.5,12.321 1.5,13.47 2.062,14.296C2.305,14.653 2.617,14.96 2.981,15.199C3.822,15.75 4.993,15.75 7.333,15.75ZM9,6.955C7.274,6.955 5.875,8.328 5.875,10.022C5.875,11.717 7.274,13.09 9,13.09C10.726,13.09 12.125,11.717 12.125,10.023C12.125,8.328 10.726,6.955 9,6.955ZM9,8.182C7.965,8.182 7.125,9.006 7.125,10.023C7.125,11.039 7.965,11.863 9,11.863C10.035,11.863 10.875,11.039 10.875,10.023C10.875,9.006 10.035,8.182 9,8.182ZM12.542,7.568C12.542,7.229 12.821,6.955 13.167,6.955H14C14.344,6.955 14.625,7.229 14.625,7.568C14.623,7.732 14.557,7.889 14.44,8.004C14.322,8.119 14.164,8.183 14,8.182H13.167C13.086,8.183 13.005,8.167 12.929,8.137C12.854,8.106 12.785,8.061 12.727,8.005C12.669,7.948 12.623,7.88 12.591,7.805C12.559,7.73 12.542,7.65 12.542,7.568Z" + android:fillColor="#000000" + android:fillType="evenOdd"/> diff --git a/app/src/main/res/drawable/ic_contact_us.xml b/app/src/main/res/drawable/ic_contact_us.xml index 821bb788..8877941b 100644 --- a/app/src/main/res/drawable/ic_contact_us.xml +++ b/app/src/main/res/drawable/ic_contact_us.xml @@ -1,390 +1,9 @@ + android:width="25dp" + android:height="24dp" + android:viewportWidth="25" + android:viewportHeight="24"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:pathData="M8.42,7.54C7.62,7.2 7.28,6.21 7.76,5.49C8.73,4.05 10.35,3 12.49,3C14.84,3 16.45,4.07 17.27,5.41C17.97,6.56 18.38,8.71 17.3,10.31C16.1,12.08 14.95,12.62 14.33,13.76C14.18,14.03 14.09,14.25 14.03,14.7C13.94,15.43 13.34,16 12.6,16C11.73,16 11.02,15.25 11.12,14.38C11.18,13.87 11.3,13.34 11.58,12.84C12.35,11.45 13.83,10.63 14.69,9.4C15.6,8.11 15.09,5.7 12.51,5.7C11.34,5.7 10.58,6.31 10.11,7.04C9.76,7.61 9.03,7.79 8.42,7.54ZM14.5,20C14.5,21.1 13.6,22 12.5,22C11.4,22 10.5,21.1 10.5,20C10.5,18.9 11.4,18 12.5,18C13.6,18 14.5,18.9 14.5,20Z" + android:fillColor="#40484C"/> diff --git a/app/src/main/res/drawable/ic_fingerprint_24.xml b/app/src/main/res/drawable/ic_fingerprint_24.xml new file mode 100644 index 00000000..c2c24155 --- /dev/null +++ b/app/src/main/res/drawable/ic_fingerprint_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml new file mode 100644 index 00000000..bb083559 --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_info.xml b/app/src/main/res/drawable/ic_info.xml new file mode 100644 index 00000000..3ff53120 --- /dev/null +++ b/app/src/main/res/drawable/ic_info.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 00000000..f368b13d --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/drawable/ic_share.xml b/app/src/main/res/drawable/ic_share.xml index ae0eabbd..7a62e69e 100644 --- a/app/src/main/res/drawable/ic_share.xml +++ b/app/src/main/res/drawable/ic_share.xml @@ -1,580 +1,13 @@ + android:width="25dp" + android:height="24dp" + android:viewportWidth="25" + android:viewportHeight="24"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_translate.xml b/app/src/main/res/drawable/ic_translate.xml new file mode 100644 index 00000000..8fb6e968 --- /dev/null +++ b/app/src/main/res/drawable/ic_translate.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_two_factor_auth.xml b/app/src/main/res/drawable/ic_two_factor_auth.xml new file mode 100644 index 00000000..c6f99820 --- /dev/null +++ b/app/src/main/res/drawable/ic_two_factor_auth.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_user_new.xml b/app/src/main/res/drawable/ic_user_new.xml new file mode 100644 index 00000000..47e44bf4 --- /dev/null +++ b/app/src/main/res/drawable/ic_user_new.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/dialog_change_avatar.xml b/app/src/main/res/layout/dialog_change_avatar.xml deleted file mode 100644 index 43d75e18..00000000 --- a/app/src/main/res/layout/dialog_change_avatar.xml +++ /dev/null @@ -1,83 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml deleted file mode 100644 index 6c450ff8..00000000 --- a/app/src/main/res/layout/fragment_profile.xml +++ /dev/null @@ -1,248 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml deleted file mode 100644 index 168d30b2..00000000 --- a/app/src/main/res/layout/fragment_settings.xml +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 541e4357..eee6bd8e 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -133,8 +133,7 @@ + android:label="Settings"> + अब वोट करें + फेसबुक के साथ जारी रखें + लॉग इन करें + साइन अप करें + अब जल्द ही + उपयोगकर्ता नाम + पासवर्ड + लॉग इन करें + उपयोगकर्ता लॉगिन + उपयोगकर्ता साइन अप करें + पहला नाम + उपनाम + मेल पता + क्या आपके पास पहले से एक खाता मौजूद है \? + दाखिल करना + पासवर्ड भूल गए \? + प्रोफ़ाइल + डैशबोर्ड + बातचीत करना + एक बग रिपोर्ट करो + दूसरों के साथ साझा करें + अगोरा के बारे में + मेरे निमंत्रण + संपर्क करें + एगोरा वोट एक मतदान मंच है जहां उपयोगकर्ता चुनाव बना सकते हैं और दोस्तों को वोट डालने के लिए आमंत्रित कर सकते हैं। + यह वोटिंग एल्गोरिदम की एक विस्तृत श्रृंखला का समर्थन करता है, जिनमें से कुछ प्रमुख हैं, जैसे कि बहुमत, समतावादी, ऑस्ट्रेलियाई एसटीवी। + हमारे Gitter चैनलों पर बेझिझक हमसे संपर्क करें और Gitlab पर हमारे साथ काम करें + AOSSIE की ग्रिड + AOSSIE का GITLAB + बेझिझक कोई मुद्दा उठाएं ताकि हमारी टीम उसमें यथाशीघ्र सुधार कर सके + पासवर्ड बदलें + नया पासवर्ड + नए पासवर्ड की पुष्टि करें + चुनाव बनाएं + सक्रिय\nचुनाव + लंबित \nचुनाव + कुल\nचुनाव + ख़त्म\nचुनाव + मतदाताओं को आमंत्रित करें\? + वास्तविक समय परिणाम प्राप्त करें\? + मतपत्र कितने गुप्त होते हैं\? + मतदाताओं की सूची कौन देख सकता है\? + केवल मैं + चुनाव तक पहुंच रखने वाला हर कोई + कुछ और विकल्प + अगला + उम्मीदवार जोड़ें + उम्मीदवार का नाम + उम्मीदवार का नाम दर्ज करें + चुनाव विवरण + चुनाव का नाम + चुनाव विधि + आरंभ करने की तिथि + अंतिम तिथि + खजूर बीनने वाला + वोटिंग एल्गोरिथम चुनें + बाल्डविन + संपूर्ण मतपत्र + लॉग आउट + testemail + 0 + उपयोगकर्ता ईमेल आईडी + लिंक भेजें + सत्यापित करना + AOSSIE द्वारा ❤ के साथ विकसित किया गया + पासवर्ड रीसेट लिंक प्राप्त करने के लिए कृपया उपयोगकर्ता नाम दर्ज करें + मरीज़ की देखभाल करना, मरीज़ की देखभाल करना ज़रूरी है, लेकिन यह ऐसे समय में होगा जब बहुत काम और दर्द होगा। छोटी से छोटी बात पर आने के लिए कोई भी किसी भी तरह का काम नहीं कर सकता + उम्मीदवार + उम्मीदवार का नाम + शुरू करना: + शुरू करने की तिथि - शुरू होने की तिथि - रवाना होने की तिथि + दर्जा: + चुनाव की स्थिति + अंत: + समाप्त होने की तारीख + छवि बटन + विवरण + सक्रिय चुनाव + चुनाव ख़त्म + लंबित चुनाव + कुल चुनाव + ओपन सोर्स को और अधिक खूबसूरत जगह बनाने के लिए दूसरों के साथ साझा करें + चुनाव हटाएं + मतदाताओं को आमंत्रित करें + मतदाता + मतदान + परिणाम + मतपत्र + वोट मतपत्र + मतदाता का नाम + मतदाता ईमेल + मतदाता का ईमेल + मतदाता का नाम + मतदाता जोड़ें + वोटर को हटाने के लिए स्वाइप करें + उम्मीदवार को हटाने के लिए स्वाइप करें + मतदाताओं का विवरण दर्ज करें + विजेता का नाम + कृपया अपना उत्तर यहां लिखें + सुरक्षा प्रश्न + स्वागत है, %1$s! + लिंक भेजा गया, कृपया अपने ईमेल जांचें + अमान्य उपयोगकर्ता नाम + कुछ गलत हो गया। कृपया बाद में पुन: प्रयास करें + कनेक्शन सफलतापूर्वक स्थापित हो गया + पूरी तरह से गुप्त और कभी किसी को नहीं दिखाया गया + केवल मुझे दिखाई देता है + चुनाव तक पहुंच रखने वाले सभी लोगों के लिए दृश्यमान + ओकलाहोमा + रेंजवोटिंग + क्रमबद्ध जोड़े + संतुष्टि अनुमोदन मतदान + स्मिथसेट + क्रमशः संप्रोद्ध स्वीकृति वोटिंग + स्वीकृति + छाँट के साथ विस्तारित चुनाव + कोपलैंड + अनआवृत सेट + बोर्डा + मिनिमैक्स कोंडोर्सेट + नैंसन + केमेनी-यंग + यादृच्छिक चुनाव + आकस्मिक + तत्काल चुनाव 2 राउंड + यहाँ दिखाने के लिए कुछ नहीं है + + + //string-array for security questions + + आपकी मां का पहला नाम क्या है? + आपके पहले पालतू जानवर का नाम क्या है? + आपका उपनाम क्या है? + आपने किस प्राथमिक स्कूल में अध्ययन किया है? + आपकी गृहनगरी कहाँ है? + + + + पूरी तरह से गुप्त और कभी किसी को नहीं दिखाया गया + केवल मुझे दिखाई देता है + चुनाव तक पहुंच रखने वाले सभी लोगों के लिए दृश्यमान + + + + Oklahoma + RangeVoting + RankedPairs + Satisfaction Approval Voting + SmithSet + Sequential Proportional Approval Voting + Approval + Exhaustive ballot with dropoff + Copeland + UncoveredSet + Borda + MinimaxCondorcet + Nanson + Kemeny-Young + RandomBallot + Contingent + InstantRunoff2Round + + + //profile fragment labels + + ईमेल : + उपयोगकर्ता नाम : + पहला नाम : + अंतिम नाम : + प्रोफ़ाइल अपडेट करें + पासवर्ड खाली नहीं हो सकता + पासवर्ड मेल नहीं खा रहे हैं + पासवर्ड सफलतापूर्वक बदला गया + आपका सत्र समाप्त हो गया था। कृपया फिर से लॉगिन करें! + + एक ही ईमेल आईडी वाले मतदाता को पहले से ही जोड़ दिया गया है + उपयोगकर्ता सफलतापूर्वक अपडेट किया गया! + पासवर्ड सफलतापूर्वक अपडेट किया गया! + प्रोफ़ाइल फ़ोटो सफलतापूर्वक अपडेट की गई! + दोहरी प्रमाणीकरण अपडेट किया गया, कृपया फिर से लॉगिन करें! + पहला नाम खाली नहीं हो सकता + अंतिम नाम खाली नहीं हो सकता + ईमेल बदला नहीं जा सकता + उपयोगकर्ता नाम बदला नहीं जा सकता + + + हैलो खाली फ्रेगमेंट + शीर्षक + अधिक + चुनाव + + अवैध अनुरोध + अप्रमाणित + अवैध क्रेडेंशियल्स + सक्रिय + शुरू होगा : + शुक्रवार जनवरी 24 12:00:04 + समाप्त होगा : + लोरेम इप्सम डोलर सिट अमेट, कॉन्सेक्टेटर एडिपिसिंग एलिट उत अलिक्वाम + इस चुनाव के लिए कोई मतदाता नहीं है + खाली बैलट + खाली चुनाव + शुरू करें + एगोरा + बस वही रहने वाले मत बनिए,\nमौजूदा रहिए + चुनावों को अनुसूचित करें\nऔर मतदाताओं को आमंत्रित करें + मतदान करें और\nउम्मीदवार का चयन करें + परिणामों को दर्शाना और\nघोषित करना + गोपनीय सुरक्षा प्रश्न + स्वागत है, + ——————— या ——————— + मतदाता सूची दृश्यता + रीयल टाइम + आमंत्रित करें + जोड़ें + agora@example.com + जॉन डो + हमारे बारे में + खाता सेटिंग्स + सेटिंग्स + + \u0020 + चलिए बॉल चलाएं + हमारे Gitter चैनल पर हमसे संपर्क करें और हमारे साथ Gitlab पर काम करें + Gitlab + Gitter + होम + शेयर करें + गोपनीयता नीति + नियम और शर्तें + कैलेंडर के लिए ड्रॉप डाउन + जुलाई + सूची + दोहरी प्रमाणीकरण + कृपया अपने पंजीकृत ईमेल पते पर भेजा गया एक टाइम पासवर्ड दर्ज करें + OTP फिर से भेजें + एक समय पासवर्ड + क्या आप इस डिवाइस पर भरोसा करते हैं? + दोहरी प्रमाणीकरण टॉगल करें + बायोमेट्रिक प्रमाणीकरण सक्षम करें + [{ "include": "https://agora-frontend.herokuapp.com/.well-known/assetlinks.json" }] + उम्मीदवार + कृपया उम्मीदवार का चयन करें + मत दें + चयनित उम्मीदवार + कैमरा + कैमरा विकल्प + गैलरी विकल्प + गैलरी + ऐसा लगता है कि आपके पास कोई ब्राउज़र स्थापित नहीं है। + मतदाता हटा दिया गया + मत सफलतापूर्वक दिया गया! + यहां कुछ दिखाने के लिए कुछ नहीं है + परिणाम प्राप्त करने में असमर्थ + कृपया अपनी नेटवर्क कनेक्शन की जांच करें + अन्य + चुनाव अभी शुरू नहीं हुआ है + चुनाव समाप्त हो गया है + सक्रिय चुनावों को हटाया नहीं जा सकता + चुनाव परिणाम प्रतिशत में + चुनाव परिणाम में वोटों की संख्या + विजेता है + लॉगिन रद्द किया गया + OTP आपके पंजीकृत ईमेल पते पर भेजा गया है + आयात करें + निर्यात करें + फ़ाइल लिखने में त्रुटि + फ़ाइल नहीं मिली! + फ़ाइल उपलब्ध नहीं है! + फ़ाइल पढ़ने में त्रुटि + अनुमति अस्वीकृत + कोई शीट नहीं मिली + एक्सेल फ़ाइल नहीं + मान्य नाम और ईमेल पता दर्ज करें + कृपया मतदाता का ईमेल दर्ज करें + कृपया मान्य ईमेल पता दर्ज करें + कृपया मतदाता का नाम दर्ज करें + खोजें... + अमान्य URL + कृपया पहले एक उम्मीदवार का चयन करें + मत पुष्टि करें? + क्या आप वाकई इस चुनाव के लिए मतदान करना चाहते हैं? + पुष्टि करें + रद्द करें + एक सक्रियण लिंक पंजीकृत ईमेल आईडी पर भेजा गया है आपके खाते की पुष्टि करने के लिए + परिणाम साझा करें + वोट + एक सक्रियण लिंक पंजीकृत ईमेल आईडी पर भेजा गया है आपके खाते की पुष्टि करने के लिए + कृपया OTP दर्ज करें + कृपया आगे बढ़ने के लिए चेकबॉक्स पर टैप करें + कृपया कम से कम एक उम्मीदवार जोड़ें + कृपया सभी विवरण दर्ज करें + "समाप्ति तिथि आरंभ तिथि और समय के बाद होनी चाहिए" + आरंभ तिथि मौजूदा तिथि और समय के बाद होनी चाहिए + छवि लोड करते समय त्रुटि + मतदान आमंत्रण! + "आपको एक चुनाव के लिए मतदान के लिए आमंत्रित किया गया है" + चुनाव दृश्यता + मतदाता सूची दृश्यता + चुनाव बनाएं और मतदान के लिए उपयोगकर्ता को आमंत्रित करें + अपने चुनाव के लिए एक नाम दर्ज करें + अपने चुनाव के लिए एक विधि चुनें + अपने चुनाव के लिए एक विवरण दर्ज करें + अपने चुनाव के लिए सभी उम्मीदवार दर्ज करें + उम्मीदवारों का आयात करें + उम्मीदवारों का आयात करें\nएक्सेल शीट से + चुनाव को कौन देख सकता है चुनें + मतदाता सूची को सार्वजनिक बनाने के लिए चुनें + अपने चुनाव के लिए प्रारंभ तिथि चुनें + अपने चुनाव के लिए समाप्ति तिथि चुनें + रीयल टाइम + आमंत्रित करें + यह चुनाव की लाइव\nस्थिति दिखाता है + हटाएं + सक्रिय होने पर चुनाव को हटाया नहीं जा सकता + चुनाव समाप्त होने पर मतदाताओं को आमंत्रित नहीं किया जा सकता + चुनाव के लिए मतदाताओं को देखें + चुनाव के लिए मतपत्र देखें + चुनाव का परिणाम देखें + छोड़ें + कुछ गड़बड़ हो गई है। कृपया पुनः प्रयास करें। + प्रमाणीकरण विफल हुआ। + जीव-रहित प्रमाणीकरण + उपयोगकर्ता को ऐप का उपयोग करने से पहले प्रमाणित किया जाना चाहिए + बार चार्ट + पाई चार्ट + प्रारंभ समय वर्तमान समय से छोटा है + भाषा + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad3ba28f..2018b91a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -336,5 +336,8 @@ Facebook Login... Login error View details + Language + Storage permission required for this feature to be available. Please grant the permission. + Camera permission required for this feature to be available. Please grant the permission. diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index efdd4beb..62748bb2 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -6,7 +6,7 @@ - + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 2c17aecf..25be4524 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,7 @@ buildscript { accompanist_version = '0.31.2-alpha' coilVersion = '2.4.0' composeLiveData = '1.4.3' + languageLibrary = '1.3.0' } repositories { From 0d9e30589a5bf50e1a76608149a8fd9d4351c4f0 Mon Sep 17 00:00:00 2001 From: Narendra Singh Anjana Date: Wed, 30 Aug 2023 14:50:03 +0530 Subject: [PATCH 09/13] Gsoc 2023 migrating active elections fragment to compose (#36) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_elections.xml.xml to ElectionsScreen.kt in compose * Migrated fragment_finished_elections.xmle.xml.xml to ElectionsScreen.kt in compose * Migrated fragment_active_elections.xml.xmle.xml.xml to ElectionsScreen.kt in compose --- .../ActiveElectionsFragment.kt | 93 ++++++------------- .../DisplayElectionViewModel.kt | 21 ++++- .../res/layout/fragment_active_elections.xml | 81 ---------------- app/src/main/res/navigation/nav_graph.xml | 3 +- 4 files changed, 48 insertions(+), 150 deletions(-) delete mode 100644 app/src/main/res/layout/fragment_active_elections.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt index 18f626ab..8ffdeab0 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/ActiveElectionsFragment.kt @@ -4,21 +4,16 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.widget.doAfterTextChanged +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.launch -import org.aossie.agoraandroid.data.adapters.ElectionsAdapter -import org.aossie.agoraandroid.databinding.FragmentActiveElectionsBinding -import org.aossie.agoraandroid.domain.model.ElectionModel +import androidx.navigation.fragment.findNavController import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.show +import org.aossie.agoraandroid.ui.screens.elections.ElectionsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -33,79 +28,49 @@ constructor( private val displayElectionViewModel: DisplayElectionViewModel by viewModels { viewModelFactory } - - lateinit var mElections: ArrayList - private lateinit var electionsAdapter: ElectionsAdapter private val onItemClicked = { _id: String -> val action = ActiveElectionsFragmentDirections.actionActiveElectionsFragmentToElectionDetailsFragment(_id) - Navigation.findNavController(binding.root) - .navigate(action) + findNavController().navigate(action) } - private lateinit var binding: FragmentActiveElectionsBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentActiveElectionsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { - - mElections = ArrayList() - electionsAdapter = ElectionsAdapter(onItemClicked) - binding.rvActiveElections.apply { - layoutManager = LinearLayoutManager(context, RecyclerView.VERTICAL, false) - adapter = electionsAdapter - } - binding.searchView.doAfterTextChanged { - filter(it.toString()) + bindUI() + composeView.setContent { + val elections by displayElectionViewModel.activeElections.collectAsState() + val progressErrorState by displayElectionViewModel.progressAndErrorState.collectAsState() + val searchText by displayElectionViewModel.search + AgoraTheme { + ElectionsScreen( + screenState = progressErrorState, + elections = elections, + searchText = searchText, + onSearch = { + displayElectionViewModel.getActiveElectionsState(it) + }, + onItemClicked = onItemClicked + ) + } } - } - override fun onNetworkConnected() { - bindUI() } - override fun onActivityCreated(savedInstanceState: Bundle?) { - super.onActivityCreated(savedInstanceState) + override fun onNetworkConnected() { bindUI() } private fun bindUI() { - lifecycleScope.launch { - try { - val elections = displayElectionViewModel.activeElections.await() - elections.collect { - if (it != null) { - addElections(it) - } - } - } catch (e: IllegalStateException) { - binding.tvSomethingWentWrong.show() - } - } - } - - private fun addElections(elections: List) { - if (elections.isNotEmpty()) { - mElections.addAll(elections) - electionsAdapter.submitList(elections) - } else { - binding.tvEmptyElection.show() - } - } - - private fun filter(query: String) { - val updatedList = displayElectionViewModel.filter(mElections, query) - electionsAdapter.submitList(updatedList) - if (updatedList.isEmpty()) { - binding.tvEmptyElection.show() - } else { - binding.tvEmptyElection.hide() - } + displayElectionViewModel.getActiveElectionsState("") } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt index 6f1dfe4b..ca2ca034 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/displayelections/DisplayElectionViewModel.kt @@ -31,9 +31,7 @@ constructor( .time private val date: String = formatter.format(currentDate) - val activeElections by lazyDeferred { - displayElectionsUseCases.getActiveElections(date) - } + val activeElections = MutableStateFlow>(emptyList()) val pendingElections by lazyDeferred { displayElectionsUseCases.getPendingElections(date) } @@ -61,6 +59,23 @@ constructor( } } + fun getActiveElectionsState(query:String) { + viewModelScope.launch { + try { + displayElectionsUseCases.getActiveElections(date).collectLatest { list -> + search.value = query + if(query.isEmpty()) { + activeElections.emit(list) + }else{ + activeElections.emit(filter(list, query)) + } + } + } catch (e: IllegalStateException) { + showMessage(R.string.something_went_wrong_please_try_again_later) + } + } + } + private fun showLoading(message: Any) { _progressAndErrorState.value = progressAndErrorState.value.copy( loading = Pair(message,true) diff --git a/app/src/main/res/layout/fragment_active_elections.xml b/app/src/main/res/layout/fragment_active_elections.xml deleted file mode 100644 index 7a485d9a..00000000 --- a/app/src/main/res/layout/fragment_active_elections.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index eee6bd8e..221cbdfd 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -188,8 +188,7 @@ + android:label="Active Elections"> Date: Wed, 30 Aug 2023 14:52:03 +0530 Subject: [PATCH 10/13] Gsoc 2023 about us screen migration to compose (#44) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_about.xml to AboutScreen.kt.kt in Compose --- .../ui/fragments/about/AboutFragment.kt | 21 +- .../ui/screens/about/AboutScreen.kt | 104 ++++++ app/src/main/res/drawable/ic_about.png | Bin 0 -> 51192 bytes app/src/main/res/drawable/ic_about.xml | 330 ------------------ .../main/res/drawable/ic_privacy_policy.xml | 9 + .../main/res/drawable/ic_terms_condition.xml | 20 ++ app/src/main/res/layout/fragment_about.xml | 93 ----- app/src/main/res/navigation/nav_graph.xml | 3 +- 8 files changed, 150 insertions(+), 430 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt create mode 100644 app/src/main/res/drawable/ic_about.png delete mode 100644 app/src/main/res/drawable/ic_about.xml create mode 100644 app/src/main/res/drawable/ic_privacy_policy.xml create mode 100644 app/src/main/res/drawable/ic_terms_condition.xml delete mode 100644 app/src/main/res/layout/fragment_about.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt index 28ab47ea..10876bdf 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/about/AboutFragment.kt @@ -4,23 +4,34 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment -import org.aossie.agoraandroid.databinding.FragmentAboutBinding +import org.aossie.agoraandroid.ui.screens.about.AboutScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme /** * A simple [Fragment] subclass. */ class AboutFragment : Fragment() { - private lateinit var binding: FragmentAboutBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - // Inflate the layout for this fragment - binding = FragmentAboutBinding.inflate(layoutInflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + composeView.setContent { + AgoraTheme { + AboutScreen() + } + } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt new file mode 100644 index 00000000..679382ac --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/about/AboutScreen.kt @@ -0,0 +1,104 @@ +package org.aossie.agoraandroid.ui.screens.about + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.ui.screens.common.component.IconTextNavigationButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AboutScreen(){ + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentPadding = PaddingValues(vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ){ + item { + Image( + painter = painterResource(id = R.drawable.ic_about), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 230.dp) + ) + } + item { + Text( + text = stringResource(id = R.string.app_name), + style = MaterialTheme.typography.headlineLarge, + modifier = Modifier.padding(horizontal = 25.dp) + ) + } + item { + Text( + text = stringResource(id = R.string.about_info) +" "+ stringResource(id = R.string.about_info2), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Light, + modifier = Modifier.padding(horizontal = 25.dp) + ) + } + item { + Divider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + ) + } + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IconTextNavigationButton( + text = stringResource(id = R.string.privacy_policy), + iconStart = R.drawable.ic_privacy_policy, + iconEnd = { + Icon( + imageVector = Icons.Rounded.NavigateNext, + contentDescription = "") + }) { + + } + IconTextNavigationButton( + text = stringResource(id = R.string.terms_and_conditions), + iconStart = R.drawable.ic_terms_condition, + iconEnd = { + Icon( + imageVector = Icons.Rounded.NavigateNext, + contentDescription = "") + }) { + + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about.png b/app/src/main/res/drawable/ic_about.png new file mode 100644 index 0000000000000000000000000000000000000000..c404d7977608c07506c0caf97f8b6ed8d2bad90d GIT binary patch literal 51192 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

RGk3A7@m7+pS07hmZX}T<-diZw2yMLc>t{ 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_privacy_policy.xml b/app/src/main/res/drawable/ic_privacy_policy.xml new file mode 100644 index 00000000..5c76005e --- /dev/null +++ b/app/src/main/res/drawable/ic_privacy_policy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_terms_condition.xml b/app/src/main/res/drawable/ic_terms_condition.xml new file mode 100644 index 00000000..1a5d5cd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_terms_condition.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/layout/fragment_about.xml b/app/src/main/res/layout/fragment_about.xml deleted file mode 100644 index 66df23e2..00000000 --- a/app/src/main/res/layout/fragment_about.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 221cbdfd..0282c6d5 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -178,8 +178,7 @@ + android:label="About Agora" /> Date: Wed, 30 Aug 2023 14:52:26 +0530 Subject: [PATCH 11/13] Gsoc 2023 share fragment migration to compose (#45) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_share_with_others.xml to ShareWithOthersScreen.kt in compose --- .../share/ShareWithOthersFragment.kt | 55 +++++++------ .../ui/screens/share/ShareWithOthersScreen.kt | 59 ++++++++++++++ app/src/main/res/drawable/share.png | Bin 0 -> 52643 bytes .../res/layout/fragment_share_with_others.xml | 74 ------------------ app/src/main/res/navigation/nav_graph.xml | 3 +- 5 files changed, 93 insertions(+), 98 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/share/ShareWithOthersScreen.kt create mode 100644 app/src/main/res/drawable/share.png delete mode 100644 app/src/main/res/layout/fragment_share_with_others.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt index 9b717acb..96b4f927 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/share/ShareWithOthersFragment.kt @@ -6,47 +6,58 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import com.facebook.FacebookSdk -import org.aossie.agoraandroid.databinding.FragmentShareWithOthersBinding +import org.aossie.agoraandroid.ui.screens.share.ShareWithOthersScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme /** * A simple [Fragment] subclass. */ class ShareWithOthersFragment : Fragment() { - private lateinit var binding: FragmentShareWithOthersBinding + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentShareWithOthersBinding.inflate(layoutInflater) - binding.btnShare.setOnClickListener { - val shareIntent = Intent(Intent.ACTION_SEND) + return ComposeView(requireContext()).also { + composeView = it + } + } - // Get the app link in the Play Store - val appPackageName = FacebookSdk.getApplicationContext() - .packageName - val strAppLink: String = try { - "https://play.google.com/store/apps/details?id=$appPackageName" - } catch (activityNotFound: ActivityNotFoundException) { - "https://play.google.com/store/apps/details?id=$appPackageName" - } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + composeView.setContent { + AgoraTheme { + ShareWithOthersScreen { + val shareIntent = Intent(Intent.ACTION_SEND) - // This is the sharing part - shareIntent.type = "text/link" - val shareBody = - """ + // Get the app link in the Play Store + val appPackageName = FacebookSdk.getApplicationContext() + .packageName + val strAppLink: String = try { + "https://play.google.com/store/apps/details?id=$appPackageName" + } catch (activityNotFound: ActivityNotFoundException) { + "https://play.google.com/store/apps/details?id=$appPackageName" + } + + // This is the sharing part + shareIntent.type = "text/link" + val shareBody = + """ Hey! Download Agora Vote application for Free and create Elections right now $strAppLink """.trimIndent() - val shareSub = "APP NAME/TITLE" - shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSub) - shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody) - startActivity(Intent.createChooser(shareIntent, "Share Agora Vote Using")) + val shareSub = "APP NAME/TITLE" + shareIntent.putExtra(Intent.EXTRA_SUBJECT, shareSub) + shareIntent.putExtra(Intent.EXTRA_TEXT, shareBody) + startActivity(Intent.createChooser(shareIntent, "Share Agora Vote Using")) + } + } } - return binding.root } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/share/ShareWithOthersScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/share/ShareWithOthersScreen.kt new file mode 100644 index 00000000..80482b06 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/share/ShareWithOthersScreen.kt @@ -0,0 +1,59 @@ +package org.aossie.agoraandroid.ui.screens.share + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryButton + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareWithOthersScreen( + onShareClick:() -> Unit +){ + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(25.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ){ + Image( + painter = painterResource(id = R.drawable.share), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .padding(it) + .heightIn(max = 230.dp) + ) + Text( + text = stringResource(id = R.string.share_with_others_to_make_open_source_a_more_beautiful_place), + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Light, + ) + PrimaryButton( + text = stringResource(id = R.string.share), + icon = painterResource(id = R.drawable.ic_share) + ) { + onShareClick() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/share.png b/app/src/main/res/drawable/share.png new file mode 100644 index 0000000000000000000000000000000000000000..9240206deb81d2b914c46dca204f8cc036882bc7 GIT binary patch literal 52643 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 iPIOfZTBlQ9lK%nFU(TC7AMsxR0000 - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 0282c6d5..14bc0bba 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -182,8 +182,7 @@ + android:label="Share with Others" /> Date: Wed, 30 Aug 2023 14:54:17 +0530 Subject: [PATCH 12/13] Gsoc 2023 contact us fragment migartion to compose (#46) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_contact_us.xml.xml to ContactUsScreen.kt in compose --- .../fragments/contactUs/ContactUsFragment.kt | 47 ++++--- .../ui/screens/contactUs/ContactUsScreen.kt | 129 ++++++++++++++++++ app/src/main/res/drawable/contact_us.png | Bin 0 -> 59666 bytes .../main/res/layout/fragment_contact_us.xml | 109 --------------- app/src/main/res/navigation/nav_graph.xml | 3 +- 5 files changed, 161 insertions(+), 127 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt create mode 100644 app/src/main/res/drawable/contact_us.png delete mode 100644 app/src/main/res/layout/fragment_contact_us.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt index 4bc3a507..1c2eaceb 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/contactUs/ContactUsFragment.kt @@ -5,38 +5,53 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment +import kotlinx.coroutines.flow.MutableStateFlow import org.aossie.agoraandroid.R.string -import org.aossie.agoraandroid.databinding.FragmentContactUsBinding +import org.aossie.agoraandroid.ui.screens.contactUs.ContactUsScreen +import org.aossie.agoraandroid.ui.theme.AgoraTheme import org.aossie.agoraandroid.utilities.browse -import org.aossie.agoraandroid.utilities.snackbar /** * A simple [Fragment] subclass. */ class ContactUsFragment : Fragment() { - lateinit var binding: FragmentContactUsBinding + private lateinit var composeView: ComposeView + private val messageState = MutableStateFlow ("") override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - binding = FragmentContactUsBinding.inflate(layoutInflater) - initListeners() - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } - private fun initListeners() { - binding.btnGitter.setOnClickListener { - openUrl("https://gitter.im/aossie/home") - } - binding.btnGitlab.setOnClickListener { - openUrl("https://gitlab.com/aossie") - } - binding.btnReport.setOnClickListener { - openUrl("https://gitlab.com/aossie/agora-android/issues/new") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + composeView.setContent { + val message = messageState.collectAsState() + AgoraTheme { + ContactUsScreen( + message, + onBtnGitlabClicked = { + openUrl("https://gitlab.com/aossie") + }, + onBtnGitterClicked = { + openUrl("https://gitter.im/aossie/home") + }, + onBtnReportBugClicked = { + openUrl("https://gitlab.com/aossie/agora-android/issues/new") + } + ) { + messageState.value = "" + } + } } } @@ -44,7 +59,7 @@ class ContactUsFragment : Fragment() { try { context?.browse(url) } catch (e: ActivityNotFoundException) { - binding.root.snackbar(resources.getString(string.no_browser)) + messageState.value = resources.getString(string.no_browser) } } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt new file mode 100644 index 00000000..4fcd2960 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/contactUs/ContactUsScreen.kt @@ -0,0 +1,129 @@ +package org.aossie.agoraandroid.ui.screens.contactUs + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.NavigateNext +import androidx.compose.material3.Divider +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.common.component.IconTextNavigationButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimarySnackBar + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ContactUsScreen( + message: State, + onBtnGitlabClicked: () -> Unit, + onBtnGitterClicked: () -> Unit, + onBtnReportBugClicked: () -> Unit, + hideSnackBarClick: () -> Unit +) { + Scaffold( + containerColor = MaterialTheme.colorScheme.background, + contentColor = MaterialTheme.colorScheme.onBackground + ) { + Box { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(it), + contentPadding = PaddingValues(vertical = 20.dp), + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + item { + Image( + painter = painterResource(id = drawable.contact_us), + contentDescription = "", + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 230.dp) + ) + } + item { + Text( + text = stringResource(id = string.feel_free_to_contact_us_on_our_gitter_channel_and_work_with_us_on_gitlab), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier.padding(horizontal = 25.dp) + ) + } + item { + Divider( + thickness = 1.dp, + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier + .padding(horizontal = 25.dp) + .fillMaxWidth() + ) + } + item { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + IconTextNavigationButton( + text = stringResource(id = string.gitlab), + iconStart = drawable.ic_gitlab, + iconStartTint = Color.Unspecified, + iconEnd = { + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "" + ) + }) { + onBtnGitlabClicked.invoke() + } + IconTextNavigationButton( + text = stringResource(id = string.gitter), + iconStart = drawable.ic_gitter, + iconStartTint = Color.Unspecified, + iconEnd = { + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "" + ) + }) { + onBtnGitterClicked.invoke() + } + IconTextNavigationButton( + text = stringResource(id = string.report_a_bug), + iconStart = drawable.ic_bug, + iconStartTint = Color.Unspecified, + iconEnd = { + Icon( + imageVector = Rounded.NavigateNext, + contentDescription = "" + ) + }) { + onBtnReportBugClicked.invoke() + } + } + } + } + if(message.value.isNotEmpty()){ + PrimarySnackBar(text = message.value) { + hideSnackBarClick.invoke() + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/contact_us.png b/app/src/main/res/drawable/contact_us.png new file mode 100644 index 0000000000000000000000000000000000000000..6390994db5bf678d4e74f333a1c506aceba996e5 GIT binary patch literal 59666 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 z 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/fragment_contact_us.xml b/app/src/main/res/layout/fragment_contact_us.xml deleted file mode 100644 index a06190ba..00000000 --- a/app/src/main/res/layout/fragment_contact_us.xml +++ /dev/null @@ -1,109 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 14bc0bba..a81532c6 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -173,8 +173,7 @@ + android:label="Contact Us" /> Date: Wed, 30 Aug 2023 14:54:58 +0530 Subject: [PATCH 13/13] Gsoc 2023 migrating election details fragment to compose (#48) * Update for some issue In Deploy to Appetize * removed Github Token from ci file * Migrated fragment_election_details.xml to ElectionDetailsScreen.kt in compose --- .../ElectionDetailsFragment.kt | 351 ++++-------------- .../ElectionDetailsViewModel.kt | 122 +++++- .../component/BottomSheetIconActionButton.kt | 71 ++++ .../electionDetails/ElectionDetailsScreen.kt | 88 +++++ .../electionDetails/component/ElectionData.kt | 164 ++++++++ .../component/ElectionDetailsBottomSheet.kt | 91 +++++ .../electionDetails/component/TimeItem.kt | 28 ++ .../events/ElectionDetailsScreenEvent.kt | 9 + app/src/main/res/drawable/ic_ballot.xml | 9 + .../main/res/drawable/ic_invite_voters.xml | 20 + app/src/main/res/drawable/ic_result.xml | 9 + app/src/main/res/drawable/ic_voters.xml | 13 + .../res/layout/fragment_election_details.xml | 294 --------------- app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/strings.xml | 2 +- 15 files changed, 688 insertions(+), 586 deletions(-) create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/BottomSheetIconActionButton.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionData.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionDetailsBottomSheet.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/TimeItem.kt create mode 100644 app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/events/ElectionDetailsScreenEvent.kt create mode 100644 app/src/main/res/drawable/ic_ballot.xml create mode 100644 app/src/main/res/drawable/ic_invite_voters.xml create mode 100644 app/src/main/res/drawable/ic_result.xml create mode 100644 app/src/main/res/drawable/ic_voters.xml delete mode 100644 app/src/main/res/layout/fragment_election_details.xml diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt index 5005b796..e2a71192 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsFragment.kt @@ -1,38 +1,30 @@ package org.aossie.agoraandroid.ui.fragments.electionDetails -import android.content.res.Configuration import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.doOnLayout +import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.ComposeView import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle.State import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope -import androidx.navigation.Navigation -import com.takusemba.spotlight.Spotlight -import kotlinx.coroutines.flow.first +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import org.aossie.agoraandroid.R.drawable -import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.R import org.aossie.agoraandroid.data.db.PreferenceProvider -import org.aossie.agoraandroid.databinding.FragmentElectionDetailsBinding import org.aossie.agoraandroid.ui.fragments.BaseFragment -import org.aossie.agoraandroid.utilities.AppConstants -import org.aossie.agoraandroid.utilities.ResponseUI -import org.aossie.agoraandroid.utilities.TargetData -import org.aossie.agoraandroid.utilities.getSpotlight -import org.aossie.agoraandroid.utilities.hide -import org.aossie.agoraandroid.utilities.scrollToView -import org.aossie.agoraandroid.utilities.show -import org.aossie.agoraandroid.utilities.toggleIsEnable -import timber.log.Timber -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date -import java.util.Locale +import org.aossie.agoraandroid.ui.fragments.electionDetails.ElectionDetailsViewModel.UiEvents.ElectionDeleted +import org.aossie.agoraandroid.ui.fragments.electionDetails.ElectionDetailsViewModel.UiEvents.InviteVoters +import org.aossie.agoraandroid.ui.fragments.electionDetails.ElectionDetailsViewModel.UiEvents.ViewResults +import org.aossie.agoraandroid.ui.screens.electionDetails.ElectionDetailsScreen +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.BallotClick +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.ViewVotersClick +import org.aossie.agoraandroid.ui.theme.AgoraTheme import javax.inject.Inject /** @@ -47,24 +39,18 @@ constructor( ) : BaseFragment(viewModelFactory) { private var id: String? = null - private var status: AppConstants.Status? = null private val electionDetailsViewModel: ElectionDetailsViewModel by viewModels { viewModelFactory } - - private lateinit var binding: FragmentElectionDetailsBinding - - private var spotlight: Spotlight? = null - private var spotlightTargets: ArrayList? = null - private var currentSpotlightIndex = 0 - + private lateinit var composeView: ComposeView override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { - binding = FragmentElectionDetailsBinding.inflate(inflater) - return binding.root + return ComposeView(requireContext()).also { + composeView = it + } } override fun onFragmentInitiated() { @@ -75,272 +61,75 @@ constructor( id = args.id electionDetailsViewModel.sessionExpiredListener = this setObserver() - initListeners() - - getElectionById() - binding.root.doOnLayout { - checkIsFirstOpen() - } - } - - override fun onNetworkConnected() { - view?.let { getElectionById() } - } - - private fun setObserver() { - lifecycleScope.launch { - electionDetailsViewModel.getDeleteElectionStateFlow.collect { - if (it != null) { - when (it.status) { - ResponseUI.Status.LOADING -> { - binding.progressBar.show() - binding.buttonDelete.toggleIsEnable() - } - ResponseUI.Status.SUCCESS -> { - lifecycleScope.launch { - prefs.setUpdateNeeded(true) - } - Navigation.findNavController(binding.root) - .navigate( - ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToHomeFragment() + electionDetailsViewModel.getElectionDetailsById(id ?: "") + + composeView.setContent { + val progressErrorState by electionDetailsViewModel.progressAndErrorState + val electionDetails by electionDetailsViewModel.electionState + AgoraTheme { + ElectionDetailsScreen( + screenState = progressErrorState, + electionDetails = electionDetails + ) { event-> + when(event){ + BallotClick -> { + val action = + ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToBallotFragment( + id!! ) + findNavController().navigate(action) } - ResponseUI.Status.ERROR -> { - notify(it.message) - binding.progressBar.hide() - binding.buttonDelete.toggleIsEnable() + ViewVotersClick -> { + val action = + ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToVotersFragment( + id!! + ) + findNavController().navigate(action) } - else -> {} + else -> electionDetailsViewModel.onEvent(event) } } } } } - private fun initListeners() { - binding.buttonBallot.setOnClickListener { - val action = - ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToBallotFragment( - id!! - ) - Navigation.findNavController(binding.root) - .navigate(action) - } - binding.buttonVoters.setOnClickListener { - val action = - ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToVotersFragment( - id!! - ) - Navigation.findNavController(binding.root) - .navigate(action) - } - binding.buttonInviteVoters.setOnClickListener { - if (status == AppConstants.Status.FINISHED) { - notify(resources.getString(string.election_finished)) - } else { - val action = - ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToInviteVotersFragment( - id!! - ) - Navigation.findNavController(binding.root) - .navigate(action) - } - } - binding.buttonResult.setOnClickListener { - if (status == AppConstants.Status.PENDING) { - notify(resources.getString(string.election_not_started)) - } else { - if (isConnected) { - val action = - ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToResultFragment( - id!! - ) - Navigation.findNavController(binding.root) - .navigate(action) - } else { - notify(resources.getString(string.no_network)) - } - } - } - binding.buttonDelete.setOnClickListener { - when (status) { - AppConstants.Status.ACTIVE -> notify( - resources.getString(string.active_elections_not_started) - ) - AppConstants.Status.FINISHED -> electionDetailsViewModel.deleteElection(id) - AppConstants.Status.PENDING -> electionDetailsViewModel.deleteElection(id) - else -> {} - } - } + override fun onNetworkConnected() { + electionDetailsViewModel.getElectionDetailsById(id ?: "") } - private fun getElectionById() { - lifecycleScope.launch { - electionDetailsViewModel.getElectionById(id ?: "").collect { - if (it != null) { - Timber.d(it.toString()) - try { - binding.tvName.text = it.name - binding.tvDescription.text = it.description - val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) - val formattedStartingDate: Date = formatter.parse(it.start!!) as Date - val formattedEndingDate: Date = formatter.parse(it.end!!) as Date - val currentDate = Calendar.getInstance() - .time - val outFormat = SimpleDateFormat("dd-MM-yyyy 'at' HH:mm:ss", Locale.ENGLISH) - // set end and start date - binding.tvEndDate.text = outFormat.format(formattedEndingDate) - binding.tvStartDate.text = outFormat.format(formattedStartingDate) - // set label color and election status - binding.label.text = - getEventStatus(currentDate, formattedStartingDate, formattedEndingDate)?.name - binding.label.setBackgroundResource( - getEventColor( - currentDate, - formattedStartingDate, - formattedEndingDate + private fun setObserver() = lifecycleScope.launch { + viewLifecycleOwner.repeatOnLifecycle(state = State.STARTED) { + electionDetailsViewModel.uiEventsFlow.collectLatest { + when(it){ + ElectionDeleted -> { + lifecycleScope.launch { + prefs.setUpdateNeeded(true) + } + findNavController().navigate( + ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToHomeFragment() ) - ) - status = getEventStatus(currentDate, formattedStartingDate, formattedEndingDate) - } catch (e: ParseException) { - e.printStackTrace() } - // add candidates name - val mCandidatesName = StringBuilder() - val candidates = it.candidates - if (candidates != null) { - for (j in 0 until candidates.size) { - mCandidatesName.append(candidates[j]) - if (j != candidates.size - 1) { - mCandidatesName.append(", ") - } - } + InviteVoters -> { + val action = + ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToInviteVotersFragment( + id!! + ) + findNavController().navigate(action) } - binding.tvCandidateList.text = mCandidatesName - } - } - } - } - - private fun getEventStatus( - currentDate: Date, - formattedStartingDate: Date?, - formattedEndingDate: Date? - ): AppConstants.Status? { - return when { - currentDate.before(formattedStartingDate) -> AppConstants.Status.PENDING - currentDate.after(formattedStartingDate) && currentDate.before( - formattedEndingDate - ) -> AppConstants.Status.ACTIVE - currentDate.after(formattedEndingDate) -> AppConstants.Status.FINISHED - else -> null - } - } - - private fun getEventColor( - currentDate: Date, - formattedStartingDate: Date?, - formattedEndingDate: Date? - ): Int { - return when { - currentDate.before(formattedStartingDate) -> drawable.pending_election_label - currentDate.after(formattedStartingDate) && currentDate.before( - formattedEndingDate - ) -> drawable.active_election_label - currentDate.after(formattedEndingDate) -> drawable.finished_election_label - else -> drawable.finished_election_label - } - } - - private fun checkIsFirstOpen() { - lifecycleScope.launch { - if (!prefs.isDisplayed(binding.root.id.toString()) - .first() - ) { - spotlightTargets = getSpotlightTargets() - prefs.setDisplayed(binding.root.id.toString()) - showSpotlight() - } - } - } - - private fun showSpotlight() { - spotlightTargets?.let { - if (currentSpotlightIndex in it.indices) { - scrollToView(binding.scrollView, it[currentSpotlightIndex].targetView) - spotlight = requireActivity().getSpotlight( - it[currentSpotlightIndex++], - { - destroySpotlight() - }, - { - it.clear() - destroySpotlight() - }, - { - if (isAdded) { - showSpotlight() + ViewResults -> { + if (isConnected) { + val action = + ElectionDetailsFragmentDirections.actionElectionDetailsFragmentToResultFragment( + id!! + ) + findNavController().navigate(action) + } else { + electionDetailsViewModel.showMessage(R.string.no_network) } } - ) - spotlight?.start() + else -> {} + } } } } - - private fun getSpotlightTargets(): ArrayList { - val targetData = ArrayList() - targetData.add( - TargetData( - binding.label, getString(string.election_status), - getString(string.status_spotlight) - ) - ) - targetData.add( - TargetData( - binding.buttonDelete, getString(string.delete), - getString(string.delete_spotlight) - ) - ) - targetData.add( - TargetData( - binding.buttonInviteVoters, getString(string.invite_voter), - getString(string.invite_spotlight) - ) - ) - targetData.add( - TargetData( - binding.buttonVoters, getString(string.voters), - getString(string.voters_spotlight) - ) - ) - targetData.add( - TargetData( - binding.buttonBallot, getString(string.ballot), - getString(string.ballot_spotlight) - ) - ) - targetData.add( - TargetData( - binding.buttonResult, getString(string.result), - getString(string.result_spotlight) - ) - ) - return targetData - } - - private fun destroySpotlight() { - spotlight?.finish() - spotlight = null - } - - override fun onPause() { - super.onPause() - destroySpotlight() - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - destroySpotlight() - } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsViewModel.kt b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsViewModel.kt index 6f3e7c53..27262c8e 100644 --- a/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsViewModel.kt +++ b/app/src/main/java/org/aossie/agoraandroid/ui/fragments/electionDetails/ElectionDetailsViewModel.kt @@ -3,13 +3,19 @@ package org.aossie.agoraandroid.ui.fragments.electionDetails import android.content.Context import android.graphics.Bitmap import android.net.Uri +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf import androidx.core.content.FileProvider import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.aossie.agoraandroid.R.string @@ -19,7 +25,14 @@ import org.aossie.agoraandroid.domain.model.VotersDtoModel import org.aossie.agoraandroid.domain.model.WinnerDtoModel import org.aossie.agoraandroid.domain.useCases.electionDetails.ElectionDetailsUseCases import org.aossie.agoraandroid.ui.fragments.auth.SessionExpiredListener +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.DeleteElectionClick +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.InviteVotersClick +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent.ResultClick import org.aossie.agoraandroid.utilities.ApiException +import org.aossie.agoraandroid.utilities.AppConstants +import org.aossie.agoraandroid.utilities.ElectionUtils import org.aossie.agoraandroid.utilities.FileUtils import org.aossie.agoraandroid.utilities.NoInternetException import org.aossie.agoraandroid.utilities.ResponseUI @@ -31,6 +44,10 @@ import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale import javax.inject.Inject class ElectionDetailsViewModel @@ -49,12 +66,37 @@ constructor( private val _getResultResponseStateFlow = MutableStateFlow?>(null) var getResultResponseStateFlow: StateFlow?> = _getResultResponseStateFlow - private val _getDeleteElectionStateFlow = MutableStateFlow?>(null) - var getDeleteElectionStateFlow: StateFlow?> = - _getDeleteElectionStateFlow lateinit var sessionExpiredListener: SessionExpiredListener + private val _uiEventsFlow = MutableSharedFlow() + val uiEventsFlow = _uiEventsFlow.asSharedFlow() + + private val _progressAndErrorState = mutableStateOf(ScreensState()) + val progressAndErrorState: State = _progressAndErrorState + + private var _electionState = mutableStateOf(null) + val electionState: State = _electionState + + private var status: AppConstants.Status? = null + + fun getElectionDetailsById(id: String) = viewModelScope.launch { + showLoading("Loading election details...") + electionDetailsUseCases.getElectionById(id).collectLatest { + hideLoading() + _electionState.value = it + getStatus(it) + } + } + + private fun getStatus(election: ElectionModel) { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) + val formattedStartingDate: Date? = formatter.parse(election.start) + val formattedEndingDate: Date? = formatter.parse(election.end) + val currentDate = Calendar.getInstance().time + status = ElectionUtils.getEventStatus(currentDate, formattedStartingDate, formattedEndingDate) + } + fun getElectionById(id: String): Flow { return electionDetailsUseCases.getElectionById(id) } @@ -104,20 +146,21 @@ constructor( fun deleteElection( id: String? ) { - _getDeleteElectionStateFlow.value = ResponseUI.loading() + showLoading("Deleting election...") viewModelScope.launch { try { val response = electionDetailsUseCases.deleteElection(id) Timber.d(response.toString()) - _getDeleteElectionStateFlow.value = ResponseUI.success(response[1]) + showMessage(response[1]) + _uiEventsFlow.emit(UiEvents.ElectionDeleted) } catch (e: ApiException) { - _getDeleteElectionStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: SessionExpirationException) { sessionExpiredListener.onSessionExpired() } catch (e: NoInternetException) { - _getDeleteElectionStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } catch (e: Exception) { - _getDeleteElectionStateFlow.value = ResponseUI.error(e.message) + showMessage(e.message!!) } } } @@ -240,4 +283,67 @@ constructor( throw e } } + + fun onEvent(event: ElectionDetailsScreenEvent) = viewModelScope.launch { + when(event){ + DeleteElectionClick -> { + when (status) { + AppConstants.Status.ACTIVE -> showMessage(string.active_elections_not_started) + AppConstants.Status.FINISHED -> deleteElection(electionState.value!!._id) + AppConstants.Status.PENDING -> deleteElection(electionState.value!!._id) + else -> {} + } + } + InviteVotersClick -> { + if (status == AppConstants.Status.FINISHED) { + showMessage(string.election_finished) + } else { + _uiEventsFlow.emit(UiEvents.InviteVoters) + } + } + ResultClick -> { + if (status == AppConstants.Status.PENDING) { + showMessage(string.election_not_started) + } else { + _uiEventsFlow.emit(UiEvents.ViewResults) + } + } + else -> {} + } + } + + private fun showLoading(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair(message,true) + ) + } + + fun showMessage(message: Any) { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair(message,true), + loading = Pair("",false) + ) + viewModelScope.launch { + delay(AppConstants.SNACKBAR_DURATION) + hideSnackBar() + } + } + + private fun hideSnackBar() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + message = Pair("",false) + ) + } + + private fun hideLoading() { + _progressAndErrorState.value = progressAndErrorState.value.copy( + loading = Pair("",false) + ) + } + + sealed class UiEvents{ + object ElectionDeleted:UiEvents() + object InviteVoters:UiEvents() + object ViewResults:UiEvents() + } } diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/BottomSheetIconActionButton.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/BottomSheetIconActionButton.kt new file mode 100644 index 00000000..12c87257 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/common/component/BottomSheetIconActionButton.kt @@ -0,0 +1,71 @@ +package org.aossie.agoraandroid.ui.screens.common.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +@Composable +fun BottomSheetIconActionButton( + iconStart: ImageVector? = null, + iconStartPainter: Painter? = null, + iconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + text: String, onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable { onClick() } + .padding(horizontal = 25.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(20.dp) + ) { + Box( + modifier = Modifier + .size(40.dp) + .background( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ), + contentAlignment = Alignment.Center + ) { + iconStart?.let { + Icon( + modifier = Modifier.size(20.dp), + imageVector = it, + tint = iconColor, + contentDescription = "Bottom sheet icon" + ) + } + iconStartPainter?.let { + Icon( + modifier = Modifier.size(20.dp), + painter = it, + tint = iconColor, + contentDescription = "Bottom sheet icon" + ) + } + } + Text( + text = text, + style = MaterialTheme.typography.titleLarge + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt new file mode 100644 index 00000000..0d3fb06c --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/ElectionDetailsScreen.kt @@ -0,0 +1,88 @@ +package org.aossie.agoraandroid.ui.screens.electionDetails + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ModalBottomSheetLayout +import androidx.compose.material.ModalBottomSheetValue.HalfExpanded +import androidx.compose.material.ModalBottomSheetValue.Hidden +import androidx.compose.material.rememberModalBottomSheetState +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.domain.model.ElectionModel +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressView +import org.aossie.agoraandroid.ui.screens.common.component.PrimarySnackBar +import org.aossie.agoraandroid.ui.screens.electionDetails.component.ElectionData +import org.aossie.agoraandroid.ui.screens.electionDetails.component.ElectionDetailsBottomSheet +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun ElectionDetailsScreen( + screenState: ScreensState, + electionDetails: ElectionModel?, + onEvent: (ElectionDetailsScreenEvent) -> Unit +){ + + val modalSheetState = rememberModalBottomSheetState( + initialValue = Hidden, + confirmValueChange = { it != HalfExpanded }, + skipHalfExpanded = false + ) + val coroutineScope = rememberCoroutineScope() + + ModalBottomSheetLayout( + sheetState = modalSheetState, + sheetBackgroundColor = MaterialTheme.colorScheme.background, + sheetContentColor = MaterialTheme.colorScheme.onBackground, + sheetShape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), + sheetContent = ElectionDetailsBottomSheet(onEvent = onEvent,screenState = screenState) + ) { + Box( + modifier = Modifier.fillMaxSize() + ) { + electionDetails?.let {election -> + LazyColumn { + item { + ElectionData(election) + } + } + } + PrimaryProgressSnackView(screenState = screenState) + if(!screenState.loading.second || !screenState.message.second){ + FloatingActionButton( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(end = 20.dp, bottom = 20.dp), + onClick = { + coroutineScope.launch { + modalSheetState.show() + } + }) { + Icon( + painter = painterResource(id = R.drawable.ic_menu), + contentDescription = "") + } + } + + } + } +} + diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionData.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionData.kt new file mode 100644 index 00000000..c359391d --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionData.kt @@ -0,0 +1,164 @@ +package org.aossie.agoraandroid.ui.screens.electionDetails.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AssistChip +import androidx.compose.material3.AssistChipDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.domain.model.ElectionModel +import org.aossie.agoraandroid.utilities.AppConstants.Status.ACTIVE +import org.aossie.agoraandroid.utilities.AppConstants.Status.FINISHED +import org.aossie.agoraandroid.utilities.AppConstants.Status.PENDING +import org.aossie.agoraandroid.utilities.ElectionUtils +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ElectionData(election: ElectionModel) { + val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH) + val formattedStartingDate: Date? = formatter.parse(election.start!!) + val formattedEndingDate: Date? = formatter.parse(election.end!!) + val currentDate = Calendar.getInstance().time + val outFormat = SimpleDateFormat("dd-MM-yyyy 'at' HH:mm:ss", Locale.ENGLISH) + val endDate = outFormat.format(formattedEndingDate!!) + val startDate = outFormat.format(formattedStartingDate!!) + val status = ElectionUtils.getEventStatus(currentDate, formattedStartingDate, formattedEndingDate) + val icon:Int + val iconColor: Color + val colorContainer: Color + val colorContent: Color + + when(status){ + PENDING -> { + icon = R.drawable.ic_election_pending + iconColor = MaterialTheme.colorScheme.error + colorContainer = MaterialTheme.colorScheme.tertiaryContainer + colorContent = MaterialTheme.colorScheme.onTertiaryContainer + } + ACTIVE -> { + icon = R.drawable.ic_election_active + iconColor = Color(0xff00C537) + colorContainer = MaterialTheme.colorScheme.primaryContainer + colorContent = MaterialTheme.colorScheme.onPrimaryContainer + } + FINISHED -> { + icon = R.drawable.ic_election_finished + iconColor = Color(0xff1877F2) + colorContainer = MaterialTheme.colorScheme.secondaryContainer + colorContent = MaterialTheme.colorScheme.onSecondaryContainer + } + null -> { + icon = R.drawable.ic_election_active + iconColor = MaterialTheme.colorScheme.primary + colorContainer = MaterialTheme.colorScheme.primaryContainer + colorContent = MaterialTheme.colorScheme.onPrimaryContainer + } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(25.dp), + verticalArrangement = Arrangement.spacedBy(15.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = election.name!!, + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Box( + modifier = Modifier + .size(40.dp) + .background( + color = colorContainer, + shape = RoundedCornerShape(8.dp) + ), + contentAlignment = Alignment.Center + ) { + Icon( + modifier = Modifier.size(20.dp), + painter = painterResource(id = icon), + tint = iconColor, + contentDescription = "Election Icons" + ) + } + } + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(id = string.description) + " :-", + style = MaterialTheme.typography.labelLarge) + Text( + text = election.description!!, + style = MaterialTheme.typography.bodyLarge, + ) + } + Column( + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = stringResource(id = string.candidates) + " :-", + style = MaterialTheme.typography.labelLarge, + ) + LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + itemsIndexed(election.candidates!!) { index, item -> + AssistChip( + label = { + Text( + text = item, + style = MaterialTheme.typography.titleMedium + ) + }, + onClick = { }, + border = AssistChipDefaults.assistChipBorder( + borderWidth = 1.dp, + borderColor = colorContent + ) + ) + } + } + } + TimeItem( + label = stringResource(id = string.start_at), + text = startDate + ) + TimeItem( + label = stringResource(id = string.end_at), + text = endDate + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionDetailsBottomSheet.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionDetailsBottomSheet.kt new file mode 100644 index 00000000..9eb46b29 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/ElectionDetailsBottomSheet.kt @@ -0,0 +1,91 @@ +package org.aossie.agoraandroid.ui.screens.electionDetails.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.aossie.agoraandroid.R.drawable +import org.aossie.agoraandroid.R.string +import org.aossie.agoraandroid.ui.screens.common.Util.ScreensState +import org.aossie.agoraandroid.ui.screens.common.component.BottomSheetIconActionButton +import org.aossie.agoraandroid.ui.screens.common.component.PrimaryProgressSnackView +import org.aossie.agoraandroid.ui.screens.electionDetails.events.ElectionDetailsScreenEvent + +@Composable +fun ElectionDetailsBottomSheet( + screenState: ScreensState, + onEvent: (ElectionDetailsScreenEvent) -> Unit +): @Composable() (ColumnScope.() -> Unit) { + return { + Box { + Column() { + Box( + modifier = Modifier + .height(30.dp) + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Divider( + thickness = 3.dp, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier + .width(32.dp) + .clip(RoundedCornerShape(2.dp)) + ) + } + BottomSheetIconActionButton( + iconStart = Rounded.Delete, + text = stringResource(id = string.delete_election), + iconColor = MaterialTheme.colorScheme.error + ) { + onEvent(ElectionDetailsScreenEvent.DeleteElectionClick) + } + BottomSheetIconActionButton( + iconStartPainter = painterResource(id = drawable.ic_invite_voters), + text = stringResource(id = string.invite_voter), + iconColor = Color(0xff007BFF) + ) { + onEvent(ElectionDetailsScreenEvent.InviteVotersClick) + } + BottomSheetIconActionButton( + iconStartPainter = painterResource(id = drawable.ic_voters), + text = stringResource(id = string.voters) + ) { + onEvent(ElectionDetailsScreenEvent.ViewVotersClick) + } + BottomSheetIconActionButton( + iconStartPainter = painterResource(id = drawable.ic_ballot), + text = stringResource(id = string.ballot), + iconColor = Color(0xff6F42C1) + ) { + onEvent(ElectionDetailsScreenEvent.BallotClick) + } + BottomSheetIconActionButton( + iconStartPainter = painterResource(id = drawable.ic_result), + text = stringResource(id = string.result), + iconColor = Color(0xff28A745) + ) { + onEvent(ElectionDetailsScreenEvent.ResultClick) + } + Spacer(modifier = Modifier.height(20.dp)) + } + PrimaryProgressSnackView(screenState = screenState) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/TimeItem.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/TimeItem.kt new file mode 100644 index 00000000..c62f362a --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/component/TimeItem.kt @@ -0,0 +1,28 @@ +package org.aossie.agoraandroid.ui.screens.electionDetails.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons.Rounded +import androidx.compose.material.icons.rounded.Schedule +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun TimeItem(label: String, text: String) { + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text(text = "$label-", style = MaterialTheme.typography.labelLarge) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon(imageVector = Rounded.Schedule, contentDescription = "") + Spacer(modifier = Modifier.width(3.dp)) + Text(text = text, style = MaterialTheme.typography.bodyLarge) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/events/ElectionDetailsScreenEvent.kt b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/events/ElectionDetailsScreenEvent.kt new file mode 100644 index 00000000..7bd81689 --- /dev/null +++ b/app/src/main/java/org/aossie/agoraandroid/ui/screens/electionDetails/events/ElectionDetailsScreenEvent.kt @@ -0,0 +1,9 @@ +package org.aossie.agoraandroid.ui.screens.electionDetails.events + +sealed class ElectionDetailsScreenEvent{ + object DeleteElectionClick:ElectionDetailsScreenEvent() + object InviteVotersClick:ElectionDetailsScreenEvent() + object ViewVotersClick:ElectionDetailsScreenEvent() + object BallotClick:ElectionDetailsScreenEvent() + object ResultClick:ElectionDetailsScreenEvent() +} diff --git a/app/src/main/res/drawable/ic_ballot.xml b/app/src/main/res/drawable/ic_ballot.xml new file mode 100644 index 00000000..6ebfd3b8 --- /dev/null +++ b/app/src/main/res/drawable/ic_ballot.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_invite_voters.xml b/app/src/main/res/drawable/ic_invite_voters.xml new file mode 100644 index 00000000..6d840326 --- /dev/null +++ b/app/src/main/res/drawable/ic_invite_voters.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_result.xml b/app/src/main/res/drawable/ic_result.xml new file mode 100644 index 00000000..30f8c3ed --- /dev/null +++ b/app/src/main/res/drawable/ic_result.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_voters.xml b/app/src/main/res/drawable/ic_voters.xml new file mode 100644 index 00000000..64d8bbb8 --- /dev/null +++ b/app/src/main/res/drawable/ic_voters.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_election_details.xml b/app/src/main/res/layout/fragment_election_details.xml deleted file mode 100644 index 7e31e174..00000000 --- a/app/src/main/res/layout/fragment_election_details.xml +++ /dev/null @@ -1,294 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index a81532c6..a95c9357 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -222,8 +222,7 @@ + android:label="Election Details"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2018b91a..1623c612 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,7 +82,7 @@ Pending Elections Total Elections Share with others to make open source a more beautiful place - delete election + Delete election Invite Voters Voters Ballot