diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index e1eea1d..2b8a50f 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6d5302a..9aaa9ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'com.google.gms.google-services' id 'com.google.firebase.crashlytics' id 'kotlin-parcelize' + id 'kotlin-kapt' } android { @@ -14,8 +15,8 @@ android { applicationId "com.doubtless.doubtless" minSdk 28 targetSdk 33 - versionCode 6 - versionName "0.1.3" + versionCode 10 + versionName "0.1.7-poll" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" signingConfig signingConfigs.debug @@ -72,6 +73,17 @@ dependencies { implementation 'com.amplitude:analytics-android:1.+' // works with .+ and not 1.0 :/ implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' + def room_version = "2.5.2" + + implementation("androidx.room:room-runtime:$room_version") + annotationProcessor("androidx.room:room-compiler:$room_version") + + // To use Kotlin annotation processing tool (kapt) + kapt("androidx.room:room-compiler:$room_version") + + // optional - Kotlin Extensions and Coroutines support for Room + implementation("androidx.room:room-ktx:$room_version") + // server implementation 'com.squareup.retrofit2:retrofit:2.9.0' diff --git a/app/src/main/java/com/doubtless/doubtless/analytics/AnalyticsTracker.kt b/app/src/main/java/com/doubtless/doubtless/analytics/AnalyticsTracker.kt index 619be60..d9f9066 100644 --- a/app/src/main/java/com/doubtless/doubtless/analytics/AnalyticsTracker.kt +++ b/app/src/main/java/com/doubtless/doubtless/analytics/AnalyticsTracker.kt @@ -10,6 +10,16 @@ class AnalyticsTracker constructor( private val userManager: UserManager ) { + fun trackTagsFragment(tag: String) { + val map = hashMapOf() + map.putAll(getCommonAttrs()) + + map["tag"] = tag + + amplitude.track("tag_feed_viewed", map) + } + + fun trackLoginStarted() { val map = hashMapOf() map.putAll(getCommonAttrs()) @@ -58,7 +68,7 @@ class AnalyticsTracker constructor( fun trackDoubtUpVoted(doubtData: DoubtData) { val map = getCommonAttrs().toMutableMap().apply { - this["doubt_data"] = doubtData.toString() + this["doubt_data"] = doubtData.toString() } amplitude.track("doubt_upvote", map) @@ -86,7 +96,9 @@ class AnalyticsTracker constructor( map["app_version_code"] = BuildConfig.VERSION_CODE.toString() val user = userManager.getCachedUserData() ?: return map + map["user_id"] = user.id.toString() + map["user_email"] = user.email.toString() if (user.local_user_attr?.tags != null) map["user_tags"] = user.local_user_attr.tags.toString() diff --git a/app/src/main/java/com/doubtless/doubtless/constants/FirestoreCollection.kt b/app/src/main/java/com/doubtless/doubtless/constants/FirestoreCollection.kt index c4ca8ee..97fe5d2 100644 --- a/app/src/main/java/com/doubtless/doubtless/constants/FirestoreCollection.kt +++ b/app/src/main/java/com/doubtless/doubtless/constants/FirestoreCollection.kt @@ -1,7 +1,5 @@ package com.doubtless.doubtless.constants -import com.doubtless.doubtless.BuildConfig - class FirestoreCollection { companion object { @@ -20,6 +18,11 @@ class FirestoreCollection { val UPVOTE_DATA_USERS = "upvoted_users" val DOWNVOTE_DATA_USERS = "downvoted_users" + val NOTIFICATION = "notifications" + + const val TAG_MY_COLLEGE = "My College" + const val TAG_ALL = "All" + } diff --git a/app/src/main/java/com/doubtless/doubtless/constants/GamificationConstants.kt b/app/src/main/java/com/doubtless/doubtless/constants/GamificationConstants.kt new file mode 100644 index 0000000..156840d --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/constants/GamificationConstants.kt @@ -0,0 +1,5 @@ +package com.doubtless.doubtless.constants + +object GamificationConstants { + val MENTOR_XP_THRESHOLD = 1000 +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/di/AppCompositionRoot.kt b/app/src/main/java/com/doubtless/doubtless/di/AppCompositionRoot.kt index f8f7d63..d932728 100644 --- a/app/src/main/java/com/doubtless/doubtless/di/AppCompositionRoot.kt +++ b/app/src/main/java/com/doubtless/doubtless/di/AppCompositionRoot.kt @@ -9,6 +9,7 @@ import com.amplitude.android.Configuration import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.R import com.doubtless.doubtless.analytics.AnalyticsTracker +import com.doubtless.doubtless.localDatabase.AppDatabase import com.doubtless.doubtless.navigation.FragNavigator import com.doubtless.doubtless.navigation.Router import com.doubtless.doubtless.network.DoubtlessServer @@ -23,13 +24,15 @@ import com.doubtless.doubtless.screens.dashboard.usecases.DeleteAccountUseCase import com.doubtless.doubtless.screens.dashboard.usecases.FetchUserDataUseCase import com.doubtless.doubtless.screens.dashboard.usecases.FetchUserFeedByDateUseCase import com.doubtless.doubtless.screens.doubt.DoubtData -import com.doubtless.doubtless.screens.doubt.usecases.DoubtDataSharedPrefUseCase -import com.doubtless.doubtless.screens.doubt.usecases.PostDoubtUseCase -import com.doubtless.doubtless.screens.doubt.usecases.VotingUseCase +import com.doubtless.doubtless.screens.doubt.usecases.* import com.doubtless.doubtless.screens.home.entities.FeedConfig import com.doubtless.doubtless.screens.home.usecases.FetchFeedByDateUseCase import com.doubtless.doubtless.screens.home.usecases.FetchFeedByPopularityUseCase import com.doubtless.doubtless.screens.home.usecases.FetchHomeFeedUseCase +import com.doubtless.doubtless.screens.inAppNotification.dao.InAppNotificationDao +import com.doubtless.doubtless.screens.inAppNotification.usecases.FetchInAppNotificationUseCase +import com.doubtless.doubtless.screens.inAppNotification.usecases.FetchUnreadNotificationUseCase +import com.doubtless.doubtless.screens.inAppNotification.usecases.MarkInAppNotificationsReadUseCase import com.doubtless.doubtless.screens.main.MainActivity import com.doubtless.doubtless.screens.main.MainFragment import com.doubtless.doubtless.screens.onboarding.usecases.AddOnBoardingDataUseCase @@ -84,11 +87,15 @@ class AppCompositionRoot(appContext: DoubtlessApp) { } private fun getFetchFeedByPopularityUseCase(): FetchFeedByPopularityUseCase { - return FetchFeedByPopularityUseCase(FirebaseFirestore.getInstance()) + return FetchFeedByPopularityUseCase(getMentorsWhoInteractedUseCase(), FirebaseFirestore.getInstance()) } private fun getFetchFeedByDataUseCase(): FetchFeedByDateUseCase { - return FetchFeedByDateUseCase(FirebaseFirestore.getInstance()) + return FetchFeedByDateUseCase(getMentorsWhoInteractedUseCase(), FirebaseFirestore.getInstance()) + } + + private fun getMentorsWhoInteractedUseCase(): FetchInteractedMentorDataForDoubt { + return FetchInteractedMentorDataForDoubt(FirebaseFirestore.getInstance()) } // --------- Answer Screen ------------ @@ -132,14 +139,54 @@ class AppCompositionRoot(appContext: DoubtlessApp) { return PostDoubtUseCase(getServer()) } - // ------- Common -------- + // --------- InApp Notification ---------- + + fun getFetchNotificationUseCase(): FetchInAppNotificationUseCase { + return FetchInAppNotificationUseCase( + getInAppNotificationDao(), + getFetchUnreadNotificationUseCase() + ) + } + + fun getMarkInAppNotificationsReadUseCase(): MarkInAppNotificationsReadUseCase { + return MarkInAppNotificationsReadUseCase( + getInAppNotificationDao(), + FirebaseFirestore.getInstance() + ) + } + + private fun getFetchUnreadNotificationUseCase(): FetchUnreadNotificationUseCase { + return FetchUnreadNotificationUseCase(FirebaseFirestore.getInstance(), getUserManager()) + } + + private fun getInAppNotificationDao(): InAppNotificationDao { + return AppDatabase.getDbInstance().inAppNotificationDao() + } + + // ------- Doubt Preview -------- fun getAnswerVotingDoubtCase(answerData: AnswerData): VotingUseCase { - return VotingUseCase(FirebaseFirestore.getInstance(), getUserManager().getCachedUserData()!!, true, answerData, null) + return VotingUseCase( + FirebaseFirestore.getInstance(), + getUserManager().getCachedUserData()!!, + true, + answerData, + null + ) } fun getDoubtVotingDoubtCase(doubtData: DoubtData): VotingUseCase { - return VotingUseCase(FirebaseFirestore.getInstance(), getUserManager().getCachedUserData()!!, false, null, doubtData) + return VotingUseCase( + FirebaseFirestore.getInstance(), + getUserManager().getCachedUserData()!!, + false, + null, + doubtData + ) + } + + fun getFetchDoubtDataFromDoubtIdUseCase(): FetchDoubtDataFromDoubtIdUseCase { + return FetchDoubtDataFromDoubtIdUseCase(FirebaseFirestore.getInstance()) } // ------- User --------- @@ -200,21 +247,54 @@ class AppCompositionRoot(appContext: DoubtlessApp) { return null } + fun getCreateFragmentNavigator(mainActivity: MainActivity): FragNavigator? { + val createFrag = + (mainActivity.supportFragmentManager.findFragmentByTag("MainFragment") as MainFragment?) + ?.childFragmentManager?.findFragmentByTag("mainfrag_1") + + if (createFrag != null) { + return DoubtlessApp.getInstance().getAppCompRoot() + .getFragNavigator(createFrag.childFragmentManager, R.id.bottomNav_child_container) + } + + return null + } + + fun getInAppFragNavigator(mainActivity: MainActivity): FragNavigator? { + + val inAppFrag = + (mainActivity.supportFragmentManager.findFragmentByTag("MainFragment") as MainFragment?) + ?.childFragmentManager?.findFragmentByTag("mainfrag_2") + + if (inAppFrag != null) { + return DoubtlessApp.getInstance().getAppCompRoot() + .getFragNavigator(inAppFrag.childFragmentManager, R.id.bottomNav_child_container) + } + + return null + } + fun getDashboardFragNavigator(mainActivity: MainActivity): FragNavigator? { val dashboardFrag = (mainActivity.supportFragmentManager.findFragmentByTag("MainFragment") as MainFragment?) - ?.childFragmentManager?.findFragmentByTag("mainfrag_2") + ?.childFragmentManager?.findFragmentByTag("mainfrag_3") if (dashboardFrag != null) { return DoubtlessApp.getInstance().getAppCompRoot() - .getFragNavigator(dashboardFrag.childFragmentManager, R.id.bottomNav_child_container) + .getFragNavigator( + dashboardFrag.childFragmentManager, + R.id.bottomNav_child_container + ) } return null } - private fun getFragNavigator(supportFragmentManager: FragmentManager, @IdRes containerId: Int ): FragNavigator { + private fun getFragNavigator( + supportFragmentManager: FragmentManager, + @IdRes containerId: Int + ): FragNavigator { return FragNavigator( containerId, supportFragmentManager @@ -272,6 +352,7 @@ class AppCompositionRoot(appContext: DoubtlessApp) { return remoteConfig } + // --------- Dashboard ----------- fun getFetchUserDataUseCase(): FetchUserDataUseCase { return FetchUserDataUseCase( FetchUserFeedByDateUseCase(FirebaseFirestore.getInstance()), @@ -279,9 +360,17 @@ class AppCompositionRoot(appContext: DoubtlessApp) { ) } - fun getDeleteAccountUseCase() : DeleteAccountUseCase { + fun getDeleteAccountUseCase(): DeleteAccountUseCase { return DeleteAccountUseCase( FirebaseFirestore.getInstance() ) } + + // --------- Home Main Screen ----------- + + fun getFetchFilterTagsUseCase(): FetchFilterTagsUseCase { + return FetchFilterTagsUseCase( + FirebaseFirestore.getInstance() + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/localDatabase/AppDatabase.kt b/app/src/main/java/com/doubtless/doubtless/localDatabase/AppDatabase.kt new file mode 100644 index 0000000..950304a --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/localDatabase/AppDatabase.kt @@ -0,0 +1,33 @@ +package com.doubtless.doubtless.localDatabase + +import androidx.room.* +import com.doubtless.doubtless.DoubtlessApp +import com.doubtless.doubtless.screens.inAppNotification.dao.InAppNotificationDao +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity + +class AppDatabase { + @Database(entities = [InAppNotificationEntity::class], version = 1) + @TypeConverters(Converters::class) + abstract class AppDB : RoomDatabase() { + abstract fun inAppNotificationDao(): InAppNotificationDao + } + + companion object { + + private var database: AppDB? = null + + @Synchronized + fun getDbInstance(): AppDB { + + if (database == null) { + database = Room.databaseBuilder( + context = DoubtlessApp.getInstance(), + klass = AppDB::class.java, + name = "doubtless-db" + ).build() + } + + return database!! + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/localDatabase/Converter.kt b/app/src/main/java/com/doubtless/doubtless/localDatabase/Converter.kt new file mode 100644 index 0000000..fbdd0f5 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/localDatabase/Converter.kt @@ -0,0 +1,22 @@ +package com.doubtless.doubtless.localDatabase + +import androidx.room.TypeConverter +import com.google.firebase.Timestamp +import com.google.gson.Gson +import java.util.* + +class Converters { + + private val gson = Gson() + + @TypeConverter + fun longToTimestamp(value: Long): Timestamp { + return Timestamp(Date(value)) + } + + @TypeConverter + fun timestampToLong(timestamp: Timestamp): Long { + return timestamp.toDate().time + } + +} diff --git a/app/src/main/java/com/doubtless/doubtless/navigation/FragNavigator.kt b/app/src/main/java/com/doubtless/doubtless/navigation/FragNavigator.kt index 6655236..d1a5021 100644 --- a/app/src/main/java/com/doubtless/doubtless/navigation/FragNavigator.kt +++ b/app/src/main/java/com/doubtless/doubtless/navigation/FragNavigator.kt @@ -5,6 +5,7 @@ import androidx.fragment.app.FragmentManager import com.doubtless.doubtless.R import com.doubtless.doubtless.screens.answers.AnswersFragment import com.doubtless.doubtless.screens.doubt.DoubtData +import com.doubtless.doubtless.screens.poll.CreatePollFragment import com.doubtless.doubtless.screens.search.SearchFragment class FragNavigator constructor( @@ -44,4 +45,17 @@ class FragNavigator constructor( .commitAllowingStateLoss() } + fun moveToCreatePollFragment(){ + supportFragmentManager.beginTransaction() + .setCustomAnimations( + /* enter = */ R.anim.slide_in_right, + /* exit = */ R.anim.slide_out_left, + /* popEnter = */ R.anim.slide_in_left, + /* popExit = */ R.anim.slide_out_right) + .replace(containerId, CreatePollFragment()) + .addToBackStack(null) + .setReorderingAllowed(true) + .commitAllowingStateLoss() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/navigation/Router.kt b/app/src/main/java/com/doubtless/doubtless/navigation/Router.kt index b650100..9244e0f 100644 --- a/app/src/main/java/com/doubtless/doubtless/navigation/Router.kt +++ b/app/src/main/java/com/doubtless/doubtless/navigation/Router.kt @@ -5,6 +5,7 @@ import android.content.Intent import com.doubtless.doubtless.screens.onboarding.OnBoardingActivity import com.doubtless.doubtless.screens.auth.LoginActivity import com.doubtless.doubtless.screens.main.MainActivity +import com.doubtless.doubtless.screens.poll.CreatePollFragment class Router { @@ -22,5 +23,4 @@ class Router { val i = Intent(activity, OnBoardingActivity::class.java) activity.startActivity(i) } - } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerData.kt b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerData.kt index 5042fdc..c1c2576 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerData.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerData.kt @@ -1,11 +1,7 @@ package com.doubtless.doubtless.screens.answers -import android.os.Handler -import android.os.Looper -import android.widget.Toast import com.doubtless.doubtless.DoubtlessApp import com.google.errorprone.annotations.Keep -import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.PropertyName import com.google.firebase.firestore.QuerySnapshot import com.google.firebase.firestore.ServerTimestamp @@ -56,6 +52,10 @@ data class AnswerData( @get:PropertyName("created_on") @set:PropertyName("created_on") var date: Date? = null, + @SerializedName("xp_count") + @get:PropertyName("xp_count") + @set:PropertyName("xp_count") + var xpCount: Long? = 0, ) { companion object { @@ -77,7 +77,8 @@ data class AnswerData( authorYear = answerSnapshot.getField("author_year"), description = answerSnapshot.getField("description"), netVotes = (answerSnapshot.getField("net_votes") as Float?) ?: 0f, - date = answerSnapshot.getField("created_on") + date = answerSnapshot.getField("created_on"), + xpCount = (answerSnapshot.getField("xp_count") as Long?) ?: 0 ) ) } catch (e: Exception) { @@ -92,7 +93,13 @@ data class AnswerData( } fun toAnswerDoubtEntity(answerData: AnswerData): AnswerDoubtEntity { - return AnswerDoubtEntity(AnswerDoubtEntity.TYPE_ANSWER, null, answerData) + return AnswerDoubtEntity( + type = AnswerDoubtEntity.TYPE_ANSWER, + doubt = null, + answer = answerData, + answerVotingUseCase = DoubtlessApp.getInstance().getAppCompRoot() + .getAnswerVotingDoubtCase(answerData) + ) } } } @@ -112,7 +119,9 @@ data class PublishAnswerRequest( @SerializedName("author_year") var authorYear: String? = null, @SerializedName("description") - var description: String? = null + var description: String? = null, + @SerializedName("xp_count") + var xpCount: Long = 0 ) { companion object { fun toAnswerData( @@ -128,7 +137,8 @@ data class PublishAnswerRequest( authorYear = publishAnswerRequest.authorYear, description = publishAnswerRequest.description, netVotes = 0f, - date = Date() + date = Date(), + xpCount = publishAnswerRequest.xpCount ) } } @@ -169,7 +179,11 @@ data class PublishAnswerResponse( @SerializedName("net_votes") @get:PropertyName("net_votes") @set:PropertyName("net_votes") - var netVotes: Float = 0f + var netVotes: Float = 0f, + @SerializedName("xp_count") + @get:PropertyName("xp_count") + @set:PropertyName("xp_count") + var xpCount: Long = 0 ) { fun toAnswerData(): AnswerData { return AnswerData( @@ -182,7 +196,8 @@ data class PublishAnswerResponse( authorYear = this.authorYear, description = this.description, netVotes = this.netVotes, - date = null + date = null, + xpCount = this.xpCount ) } } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtEntity.kt b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtEntity.kt index 15a4c3b..5457bba 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtEntity.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtEntity.kt @@ -1,11 +1,14 @@ package com.doubtless.doubtless.screens.answers import com.doubtless.doubtless.screens.doubt.DoubtData +import com.doubtless.doubtless.screens.doubt.usecases.VotingUseCase data class AnswerDoubtEntity( val type: Int, val doubt: DoubtData? = null, - val answer: AnswerData? = null + val answer: AnswerData? = null, + val answerVotingUseCase: VotingUseCase? = null, + val doubtVotingUseCase: VotingUseCase? = null ) { companion object { const val TYPE_DOUBT = 1 diff --git a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtsAdapter.kt b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtsAdapter.kt index 89e364b..f16bfaa 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtsAdapter.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswerDoubtsAdapter.kt @@ -75,7 +75,7 @@ class AnswerDoubtsAdapter( holder.setData(doubtAnswerEntities[position].doubt!!) if (holder is AnswerViewHolder) - holder.setData(doubtAnswerEntities[position].answer!!) + holder.setData(doubtAnswerEntities[position].answer!!, doubtAnswerEntities[position].answerVotingUseCase!!) if (holder is EnterAnswerViewHolder) holder.setData(user) @@ -98,6 +98,7 @@ class AnswerDoubtsAdapter( index = 2, element = answer ) // first 2 are doubt and enter answer view + notifyItemInserted(1) } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersFragment.kt index 3636b44..f17fc90 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersFragment.kt @@ -1,16 +1,15 @@ package com.doubtless.doubtless.screens.answers import android.os.Bundle -import androidx.fragment.app.Fragment import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Toast +import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import com.doubtless.doubtless.DoubtlessApp -import com.doubtless.doubtless.R import com.doubtless.doubtless.analytics.AnalyticsTracker import com.doubtless.doubtless.databinding.FragmentAnswersBinding import com.doubtless.doubtless.navigation.FragNavigator @@ -18,8 +17,8 @@ import com.doubtless.doubtless.screens.answers.usecases.PublishAnswerUseCase import com.doubtless.doubtless.screens.answers.viewholder.EnterAnswerViewHolder import com.doubtless.doubtless.screens.auth.usecases.UserManager import com.doubtless.doubtless.screens.doubt.DoubtData -import com.doubtless.doubtless.screens.doubt.view.ViewDoubtsViewModel import com.doubtless.doubtless.screens.main.MainActivity +import com.doubtless.doubtless.screens.main.MainFragment class AnswersFragment : Fragment() { @@ -46,8 +45,23 @@ class AnswersFragment : Fragment() { userManager = DoubtlessApp.getInstance().getAppCompRoot().getUserManager() analyticsTracker = DoubtlessApp.getInstance().getAppCompRoot().getAnalyticsTracker() - val _navigator = DoubtlessApp.getInstance().getAppCompRoot() - .getHomeFragNavigator(requireActivity() as MainActivity) + + var _navigator: FragNavigator? = null + + // uuhh!! + val currentSelectedFrag = + (requireActivity() as MainActivity).getMainFragment()?.getCurrentSelectedElement() + + // encapsulate this logic + if (currentSelectedFrag is MainFragment.CurrentSelectedBottomNavFrag.HomeFrag) { + _navigator = DoubtlessApp.getInstance().getAppCompRoot() + .getHomeFragNavigator(requireActivity() as MainActivity) + } + + if (currentSelectedFrag is MainFragment.CurrentSelectedBottomNavFrag.DashboardFrag) { + _navigator = DoubtlessApp.getInstance().getAppCompRoot() + .getDashboardFragNavigator(requireActivity() as MainActivity) + } if (_navigator != null) navigator = _navigator @@ -104,7 +118,8 @@ class AnswersFragment : Fragment() { authorPhotoUrl = userManager.getCachedUserData()!!.photoUrl, authorCollege = userManager.getCachedUserData()!!.local_user_attr!!.college, authorYear = userManager.getCachedUserData()!!.local_user_attr!!.year, - description = publishAnswerDTO.description + description = publishAnswerDTO.description, + xpCount = userManager.getCachedUserData()!!.xpCount ?: 0 ) ) } @@ -122,27 +137,48 @@ class AnswersFragment : Fragment() { } viewModel.answerDoubtEntities.observe(viewLifecycleOwner) { - if (it == null) return@observe - adapter.appendAnswer(it) - viewModel.notifyAnswersConsumed() + when (it) { + is AnswersViewModel.Result.Loading -> { + binding.progressPostAnswer.visibility = View.VISIBLE + } + + is AnswersViewModel.Result.Success -> { + adapter.appendAnswer(it.data) + viewModel.notifyAnswersConsumed() + binding.progressPostAnswer.visibility = View.GONE + } + + is AnswersViewModel.Result.Error -> { + Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show() + binding.progressPostAnswer.visibility = View.GONE + } + } } viewModel.publishAnswerStatus.observe(viewLifecycleOwner) { - if (it is PublishAnswerUseCase.Result.Success) { - binding.progressPostAnswer.visibility = View.GONE - Toast.makeText(requireContext(), "Successfully posted!", Toast.LENGTH_SHORT).show() - adapter.appendAnswerAtFirst(answerData = it.answerData) - // this will increase the count across screens as the same reference was passed to the arguments. - // Its generally not a good thing to do.t(it.answerData) - doubtData.no_answers += 1 - } + when (it) { + is PublishAnswerUseCase.Result.Success -> { - if (it is PublishAnswerUseCase.Result.Error){ - binding.progressPostAnswer.visibility = View.GONE - Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show() - } - if (it is PublishAnswerUseCase.Result.Loading){ - binding.progressPostAnswer.visibility = View.VISIBLE + binding.progressPostAnswer.visibility = View.GONE + + Toast.makeText(requireContext(), "Successfully posted!", Toast.LENGTH_SHORT) + .show() + + adapter.appendAnswerAtFirst(answerData = it.answerData) + + // this will increase the count across screens as the same reference was passed to the arguments. + // Its generally not a good thing to do. + doubtData.no_answers += 1 + } + + is PublishAnswerUseCase.Result.Error -> { + binding.progressPostAnswer.visibility = View.GONE + Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show() + } + + is PublishAnswerUseCase.Result.Loading -> { + binding.progressPostAnswer.visibility = View.VISIBLE + } } } } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersViewModel.kt b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersViewModel.kt index 041f96c..1bb1464 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersViewModel.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/answers/AnswersViewModel.kt @@ -1,6 +1,7 @@ package com.doubtless.doubtless.screens.answers import androidx.lifecycle.* +import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.screens.answers.usecases.FetchAnswerUseCase import com.doubtless.doubtless.screens.answers.usecases.PublishAnswerUseCase import com.doubtless.doubtless.screens.auth.usecases.UserManager @@ -15,28 +16,55 @@ class AnswersViewModel( private val doubtData: DoubtData ) : ViewModel() { - private val _answerDoubtEntities = MutableLiveData( - listOf( - AnswerDoubtEntity(AnswerDoubtEntity.TYPE_DOUBT, doubtData, null), - AnswerDoubtEntity(AnswerDoubtEntity.TYPE_ANSWER_ENTER, null, null) + sealed class Result { + class Success(val data: List) : Result() + object Loading : Result() + class Error(val message: String) : Result() + } + + private val _answerDoubtEntities = MutableLiveData( + Result.Success( + listOf( + AnswerDoubtEntity( + type = AnswerDoubtEntity.TYPE_DOUBT, + doubt = doubtData, + answer = null, + answerVotingUseCase = null, + doubtVotingUseCase = DoubtlessApp.getInstance().getAppCompRoot() + .getDoubtVotingDoubtCase(doubtData) + ), + AnswerDoubtEntity(AnswerDoubtEntity.TYPE_ANSWER_ENTER, null, null) + ) ) ) - val answerDoubtEntities: LiveData?> = _answerDoubtEntities + val answerDoubtEntities: LiveData = _answerDoubtEntities private val _publishAnswerStatus = MutableLiveData() val publishAnswerStatus: LiveData = _publishAnswerStatus fun fetchAnswers() = viewModelScope.launch(Dispatchers.IO) { + + _answerDoubtEntities.postValue(Result.Loading) + val result = fetchAnswerUseCase.fetchAnswers() - if (result is FetchAnswerUseCase.Result.Success) { - _answerDoubtEntities.postValue(result.data.map { - AnswerData.toAnswerDoubtEntity(it) - }) - } else { - /* no-op */ + when (result) { + is FetchAnswerUseCase.Result.Success -> { + _answerDoubtEntities.postValue( + Result.Success(result.data.map { + AnswerData.toAnswerDoubtEntity(it) + }) + ) + } + else -> { + _answerDoubtEntities.postValue( + Result.Error( + (result as FetchAnswerUseCase.Result.Error).message + ) + ) + } } } @@ -61,7 +89,12 @@ class AnswersViewModel( ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return AnswersViewModel(fetchAnswerUseCase, publishAnswerUseCase, userManager, doubtData) as T + return AnswersViewModel( + fetchAnswerUseCase, + publishAnswerUseCase, + userManager, + doubtData + ) as T } } } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/answers/viewholder/AnswerViewHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/answers/viewholder/AnswerViewHolder.kt index d6c4ff5..64e1e89 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/answers/viewholder/AnswerViewHolder.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/answers/viewholder/AnswerViewHolder.kt @@ -1,21 +1,22 @@ package com.doubtless.doubtless.screens.answers.viewholder import android.view.View +import android.widget.CheckBox import android.widget.ImageView import android.widget.TextView import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.R +import com.doubtless.doubtless.constants.GamificationConstants import com.doubtless.doubtless.screens.answers.AnswerData import com.doubtless.doubtless.screens.doubt.usecases.VotingUseCase import com.doubtless.doubtless.utils.Utils +import com.doubtless.doubtless.utils.addStateListAnimation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import java.util.Date -import kotlin.math.ceil import kotlin.math.floor @@ -32,8 +33,10 @@ class AnswerViewHolder(itemView: View, private val interactionListener: Interact private val ivDp: ImageView private val tvYear: TextView private val tvVotes: TextView - private val ivUpVote: ImageView - private val ivDownVote: ImageView + private val upVote: CheckBox + private val downVote: CheckBox + private val tvCollege: TextView + private val userBadge: ImageView init { authorName = itemView.findViewById(R.id.tv_author_name) @@ -42,11 +45,13 @@ class AnswerViewHolder(itemView: View, private val interactionListener: Interact ivDp = itemView.findViewById(R.id.iv_dp_author) tvYear = itemView.findViewById(R.id.user_year) tvVotes = itemView.findViewById(R.id.tv_votes) - ivUpVote = itemView.findViewById(R.id.iv_votes) - ivDownVote = itemView.findViewById(R.id.iv_downvote) + upVote = itemView.findViewById(R.id.cb_upvote) + downVote = itemView.findViewById(R.id.cb_downvote) + tvCollege = itemView.findViewById(R.id.tv_college) + userBadge = itemView.findViewById(R.id.user_badge) } - fun setData(answerData: AnswerData) { + fun setData(answerData: AnswerData, answerVotingUseCase: VotingUseCase) { itemView.setOnClickListener { interactionListener.onAnswerClicked(answerData, adapterPosition) @@ -60,16 +65,24 @@ class AnswerViewHolder(itemView: View, private val interactionListener: Interact time.isVisible = false } + userBadge.isVisible = answerData.xpCount!! > GamificationConstants.MENTOR_XP_THRESHOLD description.text = answerData.description - tvYear.text = "| ${answerData.authorYear} Year |" - val votingUseCase = DoubtlessApp.getInstance().getAppCompRoot().getAnswerVotingDoubtCase(answerData.copy()) - setVotesUi(answerData, votingUseCase) + if (answerData.authorYear.equals("passout", ignoreCase = true)) { + tvYear.text = "| ${answerData.authorYear} |" + } else { + tvYear.text = "| ${answerData.authorYear} Year |" + } + + tvCollege.text = answerData.authorCollege + setVotesUi(answerData, answerVotingUseCase) - ivUpVote.setOnClickListener { + upVote.setOnClickListener { CoroutineScope(Dispatchers.Main).launch { + if (it.stateListAnimator == null) + it.addStateListAnimation(R.animator.scale_votes_icon) - val result = votingUseCase.upvoteDoubt() + val result = answerVotingUseCase.upvoteDoubt() if (result is VotingUseCase.Result.UpVoted) { answerData.netVotes += 1 @@ -77,14 +90,16 @@ class AnswerViewHolder(itemView: View, private val interactionListener: Interact answerData.netVotes -= 1 } - setVotesUi(answerData, votingUseCase) + setVotesUi(answerData, answerVotingUseCase) } } - ivDownVote.setOnClickListener { + downVote.setOnClickListener { CoroutineScope(Dispatchers.Main).launch { + if (it.stateListAnimator == null) + it.addStateListAnimation(R.animator.scale_votes_icon) - val result = votingUseCase.downVoteDoubt() + val result = answerVotingUseCase.downVoteDoubt() if (result is VotingUseCase.Result.DownVoted) { answerData.netVotes -= 1 @@ -92,7 +107,7 @@ class AnswerViewHolder(itemView: View, private val interactionListener: Interact answerData.netVotes += 1 } - setVotesUi(answerData, votingUseCase) + setVotesUi(answerData, answerVotingUseCase) } } @@ -102,19 +117,27 @@ class AnswerViewHolder(itemView: View, private val interactionListener: Interact } private fun setVotesUi(answerData: AnswerData, votingUseCase: VotingUseCase) { - ivDownVote.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_off_alt_24)) - ivUpVote.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_off_alt_24)) tvVotes.text = floor(answerData.netVotes).toInt().toString() CoroutineScope(Dispatchers.Main).launch { - val currentState = votingUseCase.getUserCurrentState() - if (currentState == VotingUseCase.UPVOTED) - ivUpVote.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_filled)) + when (votingUseCase.getUserCurrentState()) { - if (currentState == VotingUseCase.DOWNVOTED) - ivDownVote.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_filled)) + VotingUseCase.UPVOTED -> { + downVote.isClickable = false + upVote.isChecked = true + } + + VotingUseCase.DOWNVOTED -> { + upVote.isClickable = false + downVote.isChecked = true + } + + else -> { + downVote.isClickable = true + upVote.isClickable = true + } + } } } - } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/auth/LoginActivity.kt b/app/src/main/java/com/doubtless/doubtless/screens/auth/LoginActivity.kt index d15e4bd..ea4de4b 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/auth/LoginActivity.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/auth/LoginActivity.kt @@ -6,6 +6,7 @@ import android.view.View import android.widget.ImageView import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible import com.bumptech.glide.Glide import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.R @@ -68,6 +69,8 @@ class LoginActivity : AppCompatActivity() { private fun setupUi() { window.statusBarColor = getColor(R.color.purple) + findViewById(R.id.view_sep).isVisible = false + Glide.with(this) .load("https://lh3.googleusercontent.com/a/AGNmyxaceCDqTACCSoa1e3VimHXoAEQ4IBSYbPk8YTU-J5U=s96-c") .circleCrop() diff --git a/app/src/main/java/com/doubtless/doubtless/screens/auth/User.kt b/app/src/main/java/com/doubtless/doubtless/screens/auth/User.kt index 6e04dde..b32f4f7 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/auth/User.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/auth/User.kt @@ -29,6 +29,10 @@ data class User( @get:PropertyName("photoUrl") @set:PropertyName("photoUrl") var photoUrl: String? = null, + @SerializedName("xpCount") + @get:PropertyName("xpCount") + @set:PropertyName("xpCount") + var xpCount: Long = 0, @get:Exclude val document_id: String? = null, @get:Exclude val local_user_attr: UserAttributes? = null ) diff --git a/app/src/main/java/com/doubtless/doubtless/screens/auth/exception/UserNotFoundException.kt b/app/src/main/java/com/doubtless/doubtless/screens/auth/exception/UserNotFoundException.kt new file mode 100644 index 0000000..961485b --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/auth/exception/UserNotFoundException.kt @@ -0,0 +1,3 @@ +package com.doubtless.doubtless.screens.auth.exception + +class UserNotFoundException : Exception() \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/auth/login/LoginUtils.kt b/app/src/main/java/com/doubtless/doubtless/screens/auth/login/LoginUtils.kt new file mode 100644 index 0000000..1468d0c --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/auth/login/LoginUtils.kt @@ -0,0 +1,8 @@ +package com.doubtless.doubtless.screens.auth.login + +import android.app.Activity +import com.doubtless.doubtless.analytics.AnalyticsTracker + +interface LoginUtils { + fun logOutUser(analyticsTracker: AnalyticsTracker, activity: Activity) +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/auth/login/LoginUtilsImpl.kt b/app/src/main/java/com/doubtless/doubtless/screens/auth/login/LoginUtilsImpl.kt new file mode 100644 index 0000000..b580342 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/auth/login/LoginUtilsImpl.kt @@ -0,0 +1,39 @@ +package com.doubtless.doubtless.screens.auth.login + +import android.app.Activity +import android.widget.Toast +import com.doubtless.doubtless.DoubtlessApp +import com.doubtless.doubtless.analytics.AnalyticsTracker +import com.doubtless.doubtless.screens.auth.usecases.UserManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +object LoginUtilsImpl : LoginUtils { + + override fun logOutUser(analyticsTracker: AnalyticsTracker, activity: Activity) { + analyticsTracker.trackLogout() + CoroutineScope(Dispatchers.Main).launch { + + val result = withContext(Dispatchers.IO) { + DoubtlessApp.getInstance().getAppCompRoot().getUserManager().onUserLogoutSync() + } + + if (result is UserManager.Result.LoggedOut) { + + DoubtlessApp.getInstance().getAppCompRoot().router.moveToLoginActivity( + activity + ) + activity.finish() + + } else if (result is UserManager.Result.Error) { + + Toast.makeText( + activity, result.message, Toast.LENGTH_LONG + ).show() // encapsulate error ui handling + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/auth/usecases/UserManager.kt b/app/src/main/java/com/doubtless/doubtless/screens/auth/usecases/UserManager.kt index be038cf..7f3d6ee 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/auth/usecases/UserManager.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/auth/usecases/UserManager.kt @@ -8,7 +8,7 @@ import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser -import java.util.* +import java.util.UUID import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -19,7 +19,12 @@ class UserManager constructor( ) { sealed class Result(user: User? = null, isNewUser: Boolean?, error: String? = null) { - class Success(val user: User, val isNewUser: Boolean, val isOldUserWithNoOnboarding: Boolean) : Result(user, isNewUser) + class Success( + val user: User, + val isNewUser: Boolean, + val isOldUserWithNoOnboarding: Boolean + ) : Result(user, isNewUser) + class Error(val message: String) : Result(null, null, message) class LoggedOut : Result(null, null, null) } @@ -69,7 +74,8 @@ class UserManager constructor( if (result is UserDataServerUseCase.Result.NewUser) { userDataStorageUseCase.setUserData(result.newUser) cachedUserData = result.newUser - return Result.Success(result.newUser, + return Result.Success( + result.newUser, isNewUser = true, isOldUserWithNoOnboarding = false ) @@ -78,7 +84,8 @@ class UserManager constructor( if (result is UserDataServerUseCase.Result.OldUseWithNoOnboardingData) { userDataStorageUseCase.setUserData(result.oldUser) cachedUserData = result.oldUser - return Result.Success(result.oldUser, + return Result.Success( + result.oldUser, isNewUser = false, isOldUserWithNoOnboarding = true ) @@ -87,7 +94,8 @@ class UserManager constructor( if (result is UserDataServerUseCase.Result.OldUser) { userDataStorageUseCase.setUserData(result.oldUser) cachedUserData = result.oldUser - return Result.Success(result.oldUser, + return Result.Success( + result.oldUser, isNewUser = false, isOldUserWithNoOnboarding = false ) diff --git a/app/src/main/java/com/doubtless/doubtless/screens/common/ExtraOptionsButtonHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/common/ExtraOptionsButtonHolder.kt new file mode 100644 index 0000000..312fca3 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/common/ExtraOptionsButtonHolder.kt @@ -0,0 +1,27 @@ +package com.doubtless.doubtless.screens.common + +import android.view.View +import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView +import com.doubtless.doubtless.R +import com.doubtless.doubtless.navigation.FragNavigator + +class ExtraOptionsButtonHolder(val view: View, val interactionListener: InteractionListener) + : RecyclerView.ViewHolder(view) { + + + interface InteractionListener{ + fun onCreatePollClicked() + } + + private val createPoll: ImageView + + + init { + createPoll = view.findViewById(R.id.ic_poll) + + createPoll.setOnClickListener { + interactionListener.onCreatePollClicked() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/common/GenericFeedAdapter.kt b/app/src/main/java/com/doubtless/doubtless/screens/common/GenericFeedAdapter.kt index 339896b..98b49f8 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/common/GenericFeedAdapter.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/common/GenericFeedAdapter.kt @@ -8,10 +8,10 @@ import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.R import com.doubtless.doubtless.screens.dashboard.viewholder.UserProfileViewHolder import com.doubtless.doubtless.screens.doubt.DoubtData -import com.doubtless.doubtless.screens.doubt.usecases.VotingUseCase import com.doubtless.doubtless.screens.doubt.view.viewholder.DoubtPreviewViewHolder import com.doubtless.doubtless.screens.home.entities.FeedEntity import com.doubtless.doubtless.screens.home.viewholders.HomeSearchViewHolder +import com.doubtless.doubtless.screens.poll.ViewPollViewHolder class GenericFeedAdapter( private val genericFeedEntities: MutableList, @@ -20,11 +20,12 @@ class GenericFeedAdapter( ) : RecyclerView.Adapter() { interface InteractionListener { - fun onSearchBarClicked() fun onDoubtClicked(doubtData: DoubtData, position: Int) fun onSignOutClicked() fun onSubmitFeedbackClicked() fun onDeleteAccountClicked() + fun onCreatePollClicked() + fun onPollOptionClicked(position: Int) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -36,7 +37,9 @@ class GenericFeedAdapter( return UserProfileViewHolder( view, object : UserProfileViewHolder.InteractionListener { - override fun onDeleteAccountClicked() = interactionListener.onDeleteAccountClicked() + override fun onDeleteAccountClicked() = + interactionListener.onDeleteAccountClicked() + override fun onSignOutClicked() = interactionListener.onSignOutClicked() override fun onSubmitFeedbackClicked() = interactionListener.onSubmitFeedbackClicked() @@ -46,9 +49,8 @@ class GenericFeedAdapter( FeedEntity.TYPE_DOUBT -> { val view = LayoutInflater.from(parent.context) .inflate(R.layout.doubt_layout, parent, false) - return DoubtPreviewViewHolder( - view = view, - showVotingLayout = false, + return DoubtPreviewViewHolder(view = view, + showVotingLayout = true, interactionListener = object : DoubtPreviewViewHolder.InteractionListener { override fun onDoubtClicked(doubtData: DoubtData, position: Int) { interactionListener.onDoubtClicked(doubtData, position) @@ -56,23 +58,10 @@ class GenericFeedAdapter( }) } - FeedEntity.TYPE_SEARCH -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.layout_home_search, parent, false) - return HomeSearchViewHolder( - view, - object : HomeSearchViewHolder.InteractionListener { - override fun onLayoutClicked() { - interactionListener.onSearchBarClicked() - } - }) - } - FeedEntity.TYPE_SEARCH_RESULT -> { val view = LayoutInflater.from(parent.context) .inflate(R.layout.doubt_layout, parent, false) - return DoubtPreviewViewHolder( - view = view, + return DoubtPreviewViewHolder(view = view, showVotingLayout = false, interactionListener = object : DoubtPreviewViewHolder.InteractionListener { override fun onDoubtClicked(doubtData: DoubtData, position: Int) { @@ -80,6 +69,32 @@ class GenericFeedAdapter( } }) } + + FeedEntity.TYPE_BUTTONS -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.layout_home_buttons, parent, false) + return ExtraOptionsButtonHolder(view= view, + object : ExtraOptionsButtonHolder.InteractionListener{ + override fun onCreatePollClicked() { + interactionListener.onCreatePollClicked() + } + + } + ) + } + + FeedEntity.TYPE_POLL -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.view_poll, parent, false) + return ViewPollViewHolder( + view = view, + interactionListener = object : ViewPollViewHolder.InteractionListener { + override fun onPollOptionClicked(position: Int) { + interactionListener.onPollOptionClicked(position) + } + } + ) + } } throw Exception("type is not defined") @@ -90,11 +105,17 @@ class GenericFeedAdapter( userManager = DoubtlessApp.getInstance().getAppCompRoot().getUserManager() ) - if (holder is DoubtPreviewViewHolder && getItemViewType(position) == FeedEntity.TYPE_DOUBT) - holder.setData(genericFeedEntities[position].doubt!!) + if (holder is DoubtPreviewViewHolder && getItemViewType(position) == FeedEntity.TYPE_DOUBT) holder.setData( + genericFeedEntities[position].doubt!! + ) + + if (holder is DoubtPreviewViewHolder && getItemViewType(position) == FeedEntity.TYPE_SEARCH_RESULT) holder.setData( + genericFeedEntities[position].search_doubt!!.toDoubtData() + ) + + if (holder is ViewPollViewHolder && getItemViewType(position) == FeedEntity.TYPE_POLL) + holder.setData(genericFeedEntities[position]) - if (holder is DoubtPreviewViewHolder && getItemViewType(position) == FeedEntity.TYPE_SEARCH_RESULT) - holder.setData(genericFeedEntities[position].search_doubt!!.toDoubtData()) if (position == itemCount - 1) { onLastItemReached.invoke() diff --git a/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardContainerFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardContainerFragment.kt index d84f9ff..eaf698b 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardContainerFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardContainerFragment.kt @@ -8,6 +8,7 @@ import com.doubtless.doubtless.R import com.doubtless.doubtless.navigation.FragNavigator import com.doubtless.doubtless.navigation.OnBackPressListener import com.doubtless.doubtless.screens.main.MainActivity +import com.doubtless.doubtless.screens.main.MainFragment class DashboardContainerFragment : Fragment(R.layout.fragment_home) { @@ -28,7 +29,15 @@ class DashboardContainerFragment : Fragment(R.layout.fragment_home) { private val onBackPressListener = object : OnBackPressListener { override fun onBackPress(): Boolean { - return navigator.onBackPress() + + val backPressConsumed = navigator.onBackPress() + + return if (backPressConsumed) + true + else { + (parentFragment as? MainFragment)?.selectHomeBottomNavElement() + true + } } } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardFragment.kt index 496644e..d99e6cb 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/dashboard/DashboardFragment.kt @@ -50,6 +50,8 @@ class DashboardFragment : Fragment() { private lateinit var tracker: AnalyticsTracker + private var onCreateEventUnConsumed = true + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) tracker = DoubtlessApp.getInstance().getAppCompRoot().getAnalyticsTracker() @@ -57,10 +59,9 @@ class DashboardFragment : Fragment() { userManager = DoubtlessApp.getInstance().getAppCompRoot().getUserManager() navigator = DoubtlessApp.getInstance().getAppCompRoot() .getDashboardFragNavigator(requireActivity() as MainActivity)!! - viewModel = getViewModel() - viewModel.fetchDoubts(forPageOne = true) + onCreateEventUnConsumed = true } override fun onCreateView( @@ -75,6 +76,11 @@ class DashboardFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (onCreateEventUnConsumed) { + viewModel.fetchDoubts(forPageOne = true) + onCreateEventUnConsumed = false + } + val feedList = mutableListOf() feedList.add(FeedEntity(FeedEntity.TYPE_USER_PROFILE, null)) @@ -82,8 +88,6 @@ class DashboardFragment : Fragment() { adapter = GenericFeedAdapter(genericFeedEntities = feedList, onLastItemReached = { viewModel.fetchDoubts() }, interactionListener = object : GenericFeedAdapter.InteractionListener { - override fun onSearchBarClicked() { - } override fun onDoubtClicked(doubtData: DoubtData, position: Int) { @@ -102,6 +106,10 @@ class DashboardFragment : Fragment() { override fun onDeleteAccountClicked() { showBottomSheet() } + + override fun onCreatePollClicked() {} + + override fun onPollOptionClicked(position: Int) {} }) } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/dashboard/viewholder/UserProfileViewHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/dashboard/viewholder/UserProfileViewHolder.kt index f0d560b..328691c 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/dashboard/viewholder/UserProfileViewHolder.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/dashboard/viewholder/UserProfileViewHolder.kt @@ -8,6 +8,7 @@ import com.bumptech.glide.Glide import com.doubtless.doubtless.R import com.doubtless.doubtless.screens.auth.usecases.UserManager import com.doubtless.doubtless.theming.buttons.SecondaryButton +import com.doubtless.doubtless.utils.Utils.flatten class UserProfileViewHolder(view: View, private val interactionListener: InteractionListener) : RecyclerView.ViewHolder(view) { @@ -24,6 +25,7 @@ class UserProfileViewHolder(view: View, private val interactionListener: Interac private val signOutButton: SecondaryButton private val submitFeedback: TextView private val deleteAccount: TextView + private val tvBio: TextView init { @@ -33,6 +35,7 @@ class UserProfileViewHolder(view: View, private val interactionListener: Interac signOutButton = view.findViewById(R.id.btn_signout) submitFeedback = view.findViewById(R.id.btnFeedback) deleteAccount = view.findViewById(R.id.btn_delete_account) + tvBio = view.findViewById(R.id.tv_bio) } fun setData(userManager: UserManager) { @@ -51,6 +54,15 @@ class UserProfileViewHolder(view: View, private val interactionListener: Interac userName.text = userManager.getCachedUserData()!!.name userEmail.text = userManager.getCachedUserData()!!.email + + var tags = "" + + userManager.getCachedUserData()!!.local_user_attr!!.tags?.forEach { + tags += "#$it " + } + + tvBio.text = tags + Glide.with(userImage).load(userManager.getCachedUserData()!!.photoUrl).circleCrop() .into(userImage) } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/DoubtData.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/DoubtData.kt index cd4d8b4..861bf41 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/doubt/DoubtData.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/DoubtData.kt @@ -64,8 +64,17 @@ data class DoubtData( @SerializedName("is_trending") @get:PropertyName("is_trending") @set:PropertyName("is_trending") - var isTrending: Boolean = false -): Parcelable { + var isTrending: Boolean = false, + @SerializedName("xp_count") + @get:PropertyName("xp_count") + @set:PropertyName("xp_count") + var xpCount: Long = 0, + var mentorsDpWhoInteracted: List = mutableListOf(), + @SerializedName("iv_content") + @get:PropertyName("image_content_url") + @set:PropertyName("image_content_url") + var imageContentUrl: String? = null +) : Parcelable { companion object { fun parse(documentSnapshot: DocumentSnapshot?): DoubtData? { return try { @@ -101,5 +110,7 @@ data class PublishDoubtRequest( @SerializedName("tags") var tags: List? = null, @SerializedName("keywords") - var keywords: List? = null + var keywords: List? = null, + @SerializedName("xp_count") + var xpCount: Long = 0 ) \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/FilterTagsViewPagerAdapter.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/FilterTagsViewPagerAdapter.kt new file mode 100644 index 0000000..c8e88ac --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/FilterTagsViewPagerAdapter.kt @@ -0,0 +1,26 @@ +package com.doubtless.doubtless.screens.doubt + +import android.os.Bundle +import androidx.fragment.app.Fragment +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.doubtless.doubtless.screens.doubt.view.ViewDoubtsFragment + +class FilterTagsViewPagerAdapter( + homeMainScreenFragment: HomeMainScreenFragment, private val tagsList: List +) : FragmentStateAdapter( + homeMainScreenFragment +) { + + override fun getItemCount(): Int { + return tagsList.size + } + + override fun createFragment(position: Int): Fragment { + val fragment = ViewDoubtsFragment() + val args = Bundle() + args.putString("tag", tagsList[position]) + fragment.arguments = args + return fragment + + } +} diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/HomeMainScreenFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/HomeMainScreenFragment.kt new file mode 100644 index 0000000..6e2aeb7 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/HomeMainScreenFragment.kt @@ -0,0 +1,115 @@ +package com.doubtless.doubtless.screens.doubt + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import com.doubtless.doubtless.DoubtlessApp +import com.doubtless.doubtless.R +import com.doubtless.doubtless.analytics.AnalyticsTracker +import com.doubtless.doubtless.databinding.FragmentHomeMainScreenBinding +import com.doubtless.doubtless.navigation.FragNavigator +import com.doubtless.doubtless.screens.auth.usecases.UserManager +import com.doubtless.doubtless.screens.doubt.view.HomeMainScreenViewModel +import com.doubtless.doubtless.screens.main.MainActivity +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import java.util.Locale + + +class HomeMainScreenFragment : Fragment() { + private var _binding: FragmentHomeMainScreenBinding? = null + private val binding get() = _binding!! + private lateinit var navigator: FragNavigator + private lateinit var viewModel: HomeMainScreenViewModel + private lateinit var userManager: UserManager + private lateinit var analyticsTracker: AnalyticsTracker + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? + ): View { + _binding = FragmentHomeMainScreenBinding.inflate(inflater, container, false) + requireActivity().window.statusBarColor = requireContext().getColor(R.color.purple) + return binding.root + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + userManager = DoubtlessApp.getInstance().getAppCompRoot().getUserManager() + analyticsTracker = DoubtlessApp.getInstance().getAppCompRoot().getAnalyticsTracker() + + val _navigator = DoubtlessApp.getInstance().getAppCompRoot() + .getHomeFragNavigator(requireActivity() as MainActivity) + if (_navigator != null) navigator = _navigator + + viewModel = getViewModel() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + if (viewModel.tags.value.isNullOrEmpty()) { + viewModel.fetchTags() + + } + + viewModel.tags.observe(viewLifecycleOwner) { + if (binding.viewPager.adapter == null) { + setupViewPager(it) + } + } + binding.tvSearch.setOnClickListener { + navigator.moveToSearchFragment() + } + + } + + private fun setupViewPager(tags: List) { + val pagerAdapter = FilterTagsViewPagerAdapter(this@HomeMainScreenFragment, tags) + binding.viewPager.adapter = pagerAdapter + val capitalizedTagList = tags.map { s -> + s.replaceFirstChar { + if (it.isLowerCase()) it.uppercase( + Locale.ROOT + ) else it.toString() + } + }.toMutableList() + + TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position -> + if (position == 1) { + tab.text = "${userManager.getCachedUserData()?.local_user_attr!!.college!!} only" + return@TabLayoutMediator + } + tab.text = capitalizedTagList[position] + }.attach() + + binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab?) { + analyticsTracker.trackTagsFragment(capitalizedTagList[tab!!.position]) + } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + override fun onTabReselected(tab: TabLayout.Tab?) {} + + }) + + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun getViewModel(): HomeMainScreenViewModel { + return ViewModelProvider( + owner = this, factory = HomeMainScreenViewModel.Companion.Factory( + fetchFilterTagsUseCase = DoubtlessApp.getInstance().getAppCompRoot() + .getFetchFilterTagsUseCase() + ) + )[HomeMainScreenViewModel::class.java] + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/create/CreateDoubtFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/create/CreateDoubtFragment.kt index cd6c85c..9381330 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/doubt/create/CreateDoubtFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/create/CreateDoubtFragment.kt @@ -16,11 +16,15 @@ import androidx.lifecycle.ViewModelProvider import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.analytics.AnalyticsTracker import com.doubtless.doubtless.databinding.FragmentCreateDoubtBinding +import com.doubtless.doubtless.navigation.FragNavigator +import com.doubtless.doubtless.navigation.OnBackPressListener import com.doubtless.doubtless.screens.auth.User import com.doubtless.doubtless.screens.auth.usecases.UserManager import com.doubtless.doubtless.screens.doubt.PublishDoubtRequest import com.doubtless.doubtless.screens.doubt.usecases.DoubtDataSharedPrefUseCase import com.doubtless.doubtless.screens.doubt.usecases.PostDoubtUseCase +import com.doubtless.doubtless.screens.main.MainActivity +import com.doubtless.doubtless.screens.main.MainFragment import com.doubtless.doubtless.screens.onboarding.OnBoardingAttributes import com.doubtless.doubtless.screens.onboarding.usecases.FetchOnBoardingDataUseCase import com.google.android.material.chip.Chip @@ -34,6 +38,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.properties.Delegates +/** + * NOTE : right now this fragment only opens up from bottom nav so the navigation functionality is coded assuming that. + * change things in future if this assumption changes. + */ class CreateDoubtFragment : Fragment() { private var _binding: FragmentCreateDoubtBinding? = null @@ -44,6 +52,7 @@ class CreateDoubtFragment : Fragment() { private lateinit var analyticsTracker: AnalyticsTracker private lateinit var postDoubtUseCase: PostDoubtUseCase private lateinit var remoteConfig: FirebaseRemoteConfig + private var navigator: FragNavigator? = null private var maxHeadingCharLimit by Delegates.notNull() private var maxDescriptionCharLimit by Delegates.notNull() @@ -56,20 +65,37 @@ class CreateDoubtFragment : Fragment() { private var onBoardingAttributes: OnBoardingAttributes? = null + private val onBackPressListener = object : OnBackPressListener { + override fun onBackPress(): Boolean { + + val backPressConsumed = navigator?.onBackPress() ?: false + + return if (backPressConsumed) true + else { + (parentFragment as? MainFragment)?.selectHomeBottomNavElement() + true + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + db = Firebase.firestore - userManager = DoubtlessApp.getInstance().getAppCompRoot().getUserManager() - analyticsTracker = DoubtlessApp.getInstance().getAppCompRoot().getAnalyticsTracker() - doubtDataSharedPrefUseCase = - DoubtlessApp.getInstance().getAppCompRoot().getDoubtDataSharedPrefUseCase() - onBoardingDataUseCase = DoubtlessApp.getInstance().getAppCompRoot() - .getFetchOnBoardingDataUseCase(userManager.getCachedUserData()!!) - remoteConfig = DoubtlessApp.getInstance().getAppCompRoot().getRemoteConfig() - postDoubtUseCase = DoubtlessApp.getInstance().getAppCompRoot().getPostDoubtUseCase() + + val appComp = DoubtlessApp.getInstance().getAppCompRoot() + + userManager = appComp.getUserManager() + analyticsTracker = appComp.getAnalyticsTracker() + doubtDataSharedPrefUseCase = appComp.getDoubtDataSharedPrefUseCase() + onBoardingDataUseCase = + appComp.getFetchOnBoardingDataUseCase(userManager.getCachedUserData()!!) + remoteConfig = appComp.getRemoteConfig() + postDoubtUseCase = appComp.getPostDoubtUseCase() + navigator = appComp.getCreateFragmentNavigator(requireActivity() as MainActivity) + viewModel = ViewModelProvider( - this, - CreateDoubtViewModel.Companion.Factory(postDoubtUseCase) + this, CreateDoubtViewModel.Companion.Factory(postDoubtUseCase) )[CreateDoubtViewModel::class.java] } @@ -98,11 +124,8 @@ class CreateDoubtFragment : Fragment() { } else { isButtonClicked = false binding.postButton.alpha = 1f - Toast.makeText( - /* context = */ context, - /* text = */ - "${(result as CreateDoubtViewModel.Result.Error).message}", - /* duration = */ + Toast.makeText(/* context = */ context,/* text = */ + "${(result as CreateDoubtViewModel.Result.Error).message}",/* duration = */ Toast.LENGTH_SHORT ).show() } @@ -175,14 +198,19 @@ class CreateDoubtFragment : Fragment() { } + override fun onResume() { + super.onResume() + (requireActivity() as MainActivity).registerBackPress(onBackPressListener) + } + override fun onPause() { super.onPause() doubtDataSharedPrefUseCase.saveDoubtData( binding.doubtHeading.text.toString(), binding.doubtDescription.text.toString() ) + (requireActivity() as MainActivity).unregisterBackPress(onBackPressListener) } - private fun checkText() { val errorMessage = isEverythingValid() @@ -199,7 +227,6 @@ class CreateDoubtFragment : Fragment() { binding.postButton.alpha = 0.8f showConfirmationDialog(getSelectedTags(), keywordsEntered) - } private fun isEverythingValid(): String? { @@ -221,11 +248,9 @@ class CreateDoubtFragment : Fragment() { val size = getSelectedTags().size - if (size > 3) - return "More than 3 Tags are not allowed!" + if (size > 3) return "More than 3 Tags are not allowed!" - if (size == 0) - return "Please select a tag!" + if (size == 0) return "Please select a tag!" return null } @@ -247,7 +272,8 @@ class CreateDoubtFragment : Fragment() { createDoubt( binding.doubtHeading.text.toString(), binding.doubtDescription.text.toString(), - tags, keywords, + tags, + keywords, userManager.getCachedUserData()!! ) dialogInterface.dismiss() @@ -265,8 +291,7 @@ class CreateDoubtFragment : Fragment() { val jsonObject = Gson().fromJson(maxCharLimit, Map::class.java) as Map<*, *> maxHeadingCharLimit = (jsonObject["max_heading_char_limit"] as Double).toInt() - maxDescriptionCharLimit = - (jsonObject["max_description_char_limit"] as Double).toInt() + maxDescriptionCharLimit = (jsonObject["max_description_char_limit"] as Double).toInt() maxKeywordsLimit = (jsonObject["keywords_limit"] as Double).toInt() setMaxCharacterLimit(maxHeadingCharLimit, maxDescriptionCharLimit, maxKeywordsLimit) @@ -277,9 +302,7 @@ class CreateDoubtFragment : Fragment() { } private fun setMaxCharacterLimit( - maxHeadingCharLimit: Int, - maxDescriptionCharLimit: Int, - maxKeywordsLimit: Int + maxHeadingCharLimit: Int, maxDescriptionCharLimit: Int, maxKeywordsLimit: Int ) { binding.doubtHeading.filters = arrayOf(InputFilter.LengthFilter(maxHeadingCharLimit)) @@ -288,11 +311,7 @@ class CreateDoubtFragment : Fragment() { } private fun createDoubt( - heading: String, - description: String, - tags: List, - keywords: List, - user: User + heading: String, description: String, tags: List, keywords: List, user: User ) { viewModel.postDoubt( PublishDoubtRequest( @@ -305,7 +324,8 @@ class CreateDoubtFragment : Fragment() { description = description, netVotes = 0f, tags = tags, - keywords = keywords + keywords = keywords, + xpCount = user.xpCount ) ) } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchDoubtDataFromDoubtIdUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchDoubtDataFromDoubtIdUseCase.kt new file mode 100644 index 0000000..a010ec1 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchDoubtDataFromDoubtIdUseCase.kt @@ -0,0 +1,27 @@ +package com.doubtless.doubtless.screens.doubt.usecases + +import com.doubtless.doubtless.constants.FirestoreCollection +import com.doubtless.doubtless.screens.doubt.DoubtData +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext + +class FetchDoubtDataFromDoubtIdUseCase constructor( + private val firestore: FirebaseFirestore +) { + + suspend fun getDoubtData(doubtId: String): DoubtData? = withContext(Dispatchers.IO) { + try { + val document = firestore.collection(FirestoreCollection.AllDoubts) + .whereEqualTo("doubt_id", doubtId) + .get().await() + + return@withContext DoubtData.parse(document.documents[0]) + + } catch (e: Exception) { + return@withContext null + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchFilterTagsUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchFilterTagsUseCase.kt new file mode 100644 index 0000000..22de2c2 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchFilterTagsUseCase.kt @@ -0,0 +1,45 @@ +package com.doubtless.doubtless.screens.doubt.usecases + +import com.doubtless.doubtless.constants.FirestoreCollection +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext + +class FetchFilterTagsUseCase constructor( + private val firestore: FirebaseFirestore, +) { + private var cachedTags: List? = null + + sealed class Result { + class Success(val data: List) : Result() + class Error(val message: String) : Result() + } + + suspend fun fetchTagsFromFirebase(): Result = withContext(Dispatchers.IO) { + return@withContext try { + if (cachedTags?.isNotEmpty() == true) { + Result.Success(cachedTags!!) + } else { + val tags: MutableList = mutableListOf() + + val querySnapshot = firestore.collection(FirestoreCollection.MiscAppData) + .whereEqualTo("type", "attr_data").get().await() + + for (document in querySnapshot.documents) { + val tagsList = document.get("tags") as? List + tagsList?.let { + tags.addAll(it) + } + } + + cachedTags = tags + Result.Success(tags) + } + + } catch (e: Exception) { + Result.Error(e.message ?: "Failed to fetch tags") + } + } + +} diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchInteractedMentorDataForDoubt.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchInteractedMentorDataForDoubt.kt new file mode 100644 index 0000000..f99724d --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/usecases/FetchInteractedMentorDataForDoubt.kt @@ -0,0 +1,36 @@ +package com.doubtless.doubtless.screens.doubt.usecases + +import com.doubtless.doubtless.constants.FirestoreCollection +import com.doubtless.doubtless.screens.answers.AnswerData +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext + +class FetchInteractedMentorDataForDoubt constructor( + private val firestore: FirebaseFirestore +) { + + suspend fun fetch(doubtId: String): List = withContext(Dispatchers.IO) { + return@withContext try { + + val docx = firestore.collection(FirestoreCollection.AllDoubts) + .document(doubtId) + .collection(FirestoreCollection.DoubtAnswer) + .whereGreaterThanOrEqualTo("xp_count", 1000) + .orderBy("xp_count", Query.Direction.DESCENDING) + .limit(5) + .get().await() + + val dps = AnswerData.parse(docx)?.map { + it.authorPhotoUrl ?: "" + } + + dps ?: listOf() + + } catch (e: Exception) { + listOf() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/HomeMainScreenViewModel.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/HomeMainScreenViewModel.kt new file mode 100644 index 0000000..7e15713 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/HomeMainScreenViewModel.kt @@ -0,0 +1,51 @@ +package com.doubtless.doubtless.screens.doubt.view + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.doubtless.doubtless.constants.FirestoreCollection +import com.doubtless.doubtless.screens.doubt.usecases.FetchFilterTagsUseCase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class HomeMainScreenViewModel constructor( + private val fetchFilterTagsUseCase: FetchFilterTagsUseCase, +) : ViewModel() { + + private val _tags = MutableLiveData>() + val tags: LiveData> = _tags + + private val startTags = listOf(FirestoreCollection.TAG_ALL, FirestoreCollection.TAG_MY_COLLEGE) + + fun fetchTags() = viewModelScope.launch(Dispatchers.IO) { + + val result = fetchFilterTagsUseCase.fetchTagsFromFirebase() + + if (result is FetchFilterTagsUseCase.Result.Error) { + _tags.postValue(startTags) + return@launch + } + + result as FetchFilterTagsUseCase.Result.Success + + val tags = result.data.toMutableList() + tags.addAll( + 0, startTags + ) + _tags.postValue(tags) + } + + + companion object { + class Factory constructor( + private val fetchFilterTagsUseCase: FetchFilterTagsUseCase + ) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + return HomeMainScreenViewModel(fetchFilterTagsUseCase) as T + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsFragment.kt index 439e3e7..0034a17 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsFragment.kt @@ -1,25 +1,27 @@ package com.doubtless.doubtless.screens.doubt.view import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionInflater import com.doubtless.doubtless.DoubtlessApp -import com.doubtless.doubtless.R import com.doubtless.doubtless.analytics.AnalyticsTracker import com.doubtless.doubtless.databinding.FragmentViewDoubtsBinding import com.doubtless.doubtless.navigation.FragNavigator -import com.doubtless.doubtless.screens.common.GenericFeedAdapter +import com.doubtless.doubtless.screens.auth.exception.UserNotFoundException +import com.doubtless.doubtless.screens.auth.login.LoginUtilsImpl import com.doubtless.doubtless.screens.auth.usecases.UserManager +import com.doubtless.doubtless.screens.common.GenericFeedAdapter import com.doubtless.doubtless.screens.doubt.DoubtData import com.doubtless.doubtless.screens.home.entities.FeedConfig import com.doubtless.doubtless.screens.main.MainActivity -import com.google.firebase.firestore.FirebaseFirestore +import com.doubtless.doubtless.utils.Resource +import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.remoteconfig.FirebaseRemoteConfig import com.google.gson.Gson @@ -35,34 +37,30 @@ class ViewDoubtsFragment : Fragment() { private lateinit var navigator: FragNavigator private lateinit var remoteConfig: FirebaseRemoteConfig private lateinit var feedConfig: FeedConfig - private lateinit var firebaseFirestore: FirebaseFirestore + private lateinit var tag: String override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val inflater = TransitionInflater.from(requireContext()) - //enterTransition = inflater.inflateTransition(R.transition.slide) + // enterTransition = inflater.inflateTransition(R.transition.slide) // exitTransition = inflater.inflateTransition(R.transition.fade) + tag = arguments?.getString("tag")!! + userManager = DoubtlessApp.getInstance().getAppCompRoot().getUserManager() analyticsTracker = DoubtlessApp.getInstance().getAppCompRoot().getAnalyticsTracker() remoteConfig = DoubtlessApp.getInstance().getAppCompRoot().getRemoteConfig() - feedConfig = FeedConfig.parse(Gson(), remoteConfig) - ?: FeedConfig( - pageSize = 10, - recentPostsCount = 6, - feedDebounce = 3000, - searchDebounce = 600 - ) + feedConfig = FeedConfig.parse(Gson(), remoteConfig) ?: FeedConfig( + pageSize = 10, recentPostsCount = 6, feedDebounce = 3000, searchDebounce = 600 + ) val _navigator = DoubtlessApp.getInstance().getAppCompRoot() .getHomeFragNavigator(requireActivity() as MainActivity) - if (_navigator != null) - navigator = _navigator + if (_navigator != null) navigator = _navigator viewModel = getViewModel() - viewModel.fetchDoubts(forPageOne = true) } override fun onCreateView( @@ -75,67 +73,106 @@ class ViewDoubtsFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + if (viewModel.homeEntities.isEmpty()) { + viewModel.fetchDoubts(forPageOne = true, feedTag = tag) + } + // for debouncing var lastRefreshed = System.currentTimeMillis() - binding.llProgressBar.visibility= View.VISIBLE //show progress bar + binding.llProgressBar.visibility = View.VISIBLE //show progress bar binding.layoutSwipe.setOnRefreshListener { if (System.currentTimeMillis() - lastRefreshed < feedConfig.feedDebounce) { - Log.d("feed debounced", feedConfig.feedDebounce.toString()) binding.layoutSwipe.isRefreshing = false return@setOnRefreshListener } lastRefreshed = System.currentTimeMillis() - binding.layoutSwipe.isRefreshing = true - viewModel.refreshList() + viewModel.refreshList(tag = tag) adapter.clearCurrentList() } - adapter = GenericFeedAdapter( - genericFeedEntities = viewModel.homeEntities.toMutableList(), - onLastItemReached = { - viewModel.fetchDoubts() - }, - interactionListener = object : GenericFeedAdapter.InteractionListener { - override fun onSearchBarClicked() { - navigator.moveToSearchFragment() - } + if (!::adapter.isInitialized) { + adapter = + GenericFeedAdapter(genericFeedEntities = viewModel.homeEntities.toMutableList(), + onLastItemReached = { + viewModel.fetchDoubts(feedTag = tag) + }, + interactionListener = object : GenericFeedAdapter.InteractionListener { - override fun onDoubtClicked(doubtData: DoubtData, position: Int) { - // note that this we are not sending a copy of doubtData here, - // hence if netVotes are changed on the other screen then it will change here too. - // this solves our problem but can cause complications on long term. - navigator.moveToDoubtDetailFragment(doubtData) - } + override fun onDoubtClicked(doubtData: DoubtData, position: Int) { + // note that this we are not sending a copy of doubtData here, + // hence if netVotes are changed on the other screen then it will change here too. + // this solves our problem but can cause complications on long term. + navigator.moveToDoubtDetailFragment(doubtData) + } - override fun onSignOutClicked() { + override fun onSignOutClicked() { - } + } - override fun onSubmitFeedbackClicked() { - } + override fun onSubmitFeedbackClicked() { + } + + override fun onDeleteAccountClicked() { + } + + override fun onCreatePollClicked() { + navigator.moveToCreatePollFragment() + } + + override fun onPollOptionClicked(position: Int) { + } + }) + } - override fun onDeleteAccountClicked() { - } - }) // how is rv restoring its scroll pos when switching tabs? binding.doubtsRecyclerView.adapter = adapter binding.doubtsRecyclerView.layoutManager = LinearLayoutManager(context) - viewModel.fetchedHomeEntities.observe(viewLifecycleOwner) { - binding.llProgressBar.visibility= View.GONE //hide progress bar - if (it == null) return@observe - adapter.appendDoubts(it) - viewModel.notifyFetchedDoubtsConsumed() + viewModel.fetchedHomeEntities.observe(viewLifecycleOwner) { result -> + + binding.llProgressBar.visibility = View.GONE //hide progress bar binding.layoutSwipe.isRefreshing = false + + if (result == null) return@observe + + when (result) { + + is Resource.Success<*> -> { + if (result.data == null) return@observe + adapter.appendDoubts(result.data) + viewModel.notifyFetchedDoubtsConsumed() + } + + is Resource.Error<*> -> { + + val message = result.message ?: "some error occurred!" + + if (result.error is UserNotFoundException) { + FirebaseCrashlytics.getInstance() + .recordException(Exception("current user is null")) + + LoginUtilsImpl.logOutUser(analyticsTracker, requireActivity()) + } + + showToast(message) + } + + is Resource.Loading<*> -> {} + } } } + private fun showToast(msg: String) { + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT) + .show() + } + override fun onDestroyView() { super.onDestroyView() _binding = null @@ -143,8 +180,7 @@ class ViewDoubtsFragment : Fragment() { private fun getViewModel(): ViewDoubtsViewModel { return ViewModelProvider( - owner = this, - factory = ViewDoubtsViewModel.Companion.Factory( + owner = this, factory = ViewDoubtsViewModel.Companion.Factory( fetchHomeFeedUseCase = DoubtlessApp.getInstance().getAppCompRoot() .getFetchHomeFeedUseCase(feedConfig), analyticsTracker = analyticsTracker, diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsViewModel.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsViewModel.kt index 67c90aa..c07f714 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsViewModel.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/ViewDoubtsViewModel.kt @@ -1,18 +1,28 @@ package com.doubtless.doubtless.screens.doubt.view -import androidx.lifecycle.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import com.doubtless.doubtless.DoubtlessApp +import com.doubtless.doubtless.R import com.doubtless.doubtless.analytics.AnalyticsTracker +import com.doubtless.doubtless.screens.auth.exception.UserNotFoundException import com.doubtless.doubtless.screens.auth.usecases.UserManager import com.doubtless.doubtless.screens.home.entities.FeedEntity import com.doubtless.doubtless.screens.home.usecases.FetchHomeFeedUseCase -import com.doubtless.doubtless.screens.home.usecases.FetchHomeFeedUseCase.* +import com.doubtless.doubtless.screens.home.usecases.FetchHomeFeedUseCase.FetchHomeFeedRequest +import com.doubtless.doubtless.screens.home.usecases.FetchHomeFeedUseCase.Result +import com.doubtless.doubtless.utils.Resource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlin.collections.set class ViewDoubtsViewModel constructor( private val fetchHomeFeedUseCase: FetchHomeFeedUseCase, private val analyticsTracker: AnalyticsTracker, - private val userManager: UserManager + private val userManager: UserManager, ) : ViewModel() { private val _homeEntities = mutableListOf() @@ -22,81 +32,108 @@ class ViewDoubtsViewModel constructor( private var isLoading = false - private val _fetchedHomeEntities = MutableLiveData?>() - val fetchedHomeEntities: LiveData?> = + private val _fetchedHomeEntities = MutableLiveData?>>() + val fetchedHomeEntities: LiveData?>> = _fetchedHomeEntities // TODO : use Result here! fun notifyFetchedDoubtsConsumed() { - _fetchedHomeEntities.value = null + _fetchedHomeEntities.value = Resource.Success(data = null) } - fun fetchDoubts(forPageOne: Boolean = false) = viewModelScope.launch(Dispatchers.IO) { + fun fetchDoubts(forPageOne: Boolean = false, feedTag: String) = + viewModelScope.launch(Dispatchers.IO) { - if (isLoading) return@launch + if (isLoading) return@launch - isLoading = true + isLoading = true - val result = fetchHomeFeedUseCase.fetchFeedSync( - request = FetchHomeFeedRequest( - user = userManager.getCachedUserData()!!, - fetchFromPage1 = forPageOne + val currentUser = userManager.getCachedUserData() ?: userManager.getLoggedInUser() + + if (currentUser == null) { + // ERROR CASE + _fetchedHomeEntities.postValue( + Resource.Error( + message = DoubtlessApp.getInstance().getString(R.string.sign_in_again), + data = null, + error = UserNotFoundException() + ) + ) + isLoading = false + + return@launch + } + + val result = fetchHomeFeedUseCase.fetchFeedSync( + request = FetchHomeFeedRequest( + user = currentUser, + fetchFromPage1 = forPageOne, + tag = feedTag + ) ) - ) - if (result is Result.ListEnded || result is Result.Error) { - // ERROR CASE - _fetchedHomeEntities.postValue(null) - isLoading = false - return@launch - } + if (result is Result.ListEnded || result is Result.Error) { + // ERROR CASE + _fetchedHomeEntities.postValue(Resource.Error()) + isLoading = false + return@launch + } - result as FetchHomeFeedUseCase.Result.Success + result as Result.Success - if (!forPageOne) { - analyticsTracker.trackFeedNextPage(homeEntities.size) - } else { - analyticsTracker.trackFeedRefresh() - } + if (!forPageOne) { + analyticsTracker.trackFeedNextPage(homeEntities.size) + } else { + analyticsTracker.trackFeedRefresh() + } - val entitiesFromServer = mutableListOf() + val entitiesFromServer = mutableListOf() - result.data.forEach { doubtData -> + val pollOptions = listOf("Krishna", "Sneha", "Dhiraj") + if (_homeEntities.isEmpty()) { + entitiesFromServer.add(FeedEntity.getOptionButtons()) + entitiesFromServer.add(FeedEntity.getPollEntity(pollOptions)) + } + + + result.data.forEach { doubtData -> + _homeEntities.addAll(entitiesFromServer) + _fetchedHomeEntities.postValue(Resource.Success(entitiesFromServer)) + fetchHomeFeedUseCase.notifyDistinctDocsFetched( + docsFetched = homeEntities.size + ) - // we got the data for page 2 (lets say) now check if these posts existed on page 1 and add only unique ones. - if (_homeEntitiesIds.contains(doubtData.id) == false) { - entitiesFromServer.add(doubtData.toHomeEntity()) - _homeEntitiesIds[doubtData.id!!] = 1 + isLoading = false } - } - // for page 1 call add search entity - if (_homeEntities.isEmpty()) - entitiesFromServer.add(0, FeedEntity.getSearchEntity()) - - _homeEntities.addAll(entitiesFromServer) - _fetchedHomeEntities.postValue(entitiesFromServer) - fetchHomeFeedUseCase.notifyDistinctDocsFetched( - docsFetched = homeEntities.size - - /* subtract one for search entity, ideally should have counted Type = Doubt size */ 1 - ) - isLoading = false - } - fun refreshList() { + _homeEntities.addAll(entitiesFromServer) + _fetchedHomeEntities.postValue(Resource.Success(entitiesFromServer)) + fetchHomeFeedUseCase.notifyDistinctDocsFetched( + docsFetched = homeEntities.size + - /* subtract one for search entity, ideally should have counted Type = Doubt size */ 1 + ) + isLoading = false + } + + fun refreshList(tag: String?) { _homeEntities.clear() _homeEntitiesIds.clear() - fetchDoubts(forPageOne = true) + fetchDoubts(forPageOne = true, feedTag = tag!!) } companion object { class Factory constructor( private val fetchHomeFeedUseCase: FetchHomeFeedUseCase, private val analyticsTracker: AnalyticsTracker, - private val userManager: UserManager + private val userManager: UserManager, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return ViewDoubtsViewModel(fetchHomeFeedUseCase, analyticsTracker, userManager) as T + return ViewDoubtsViewModel( + fetchHomeFeedUseCase, + analyticsTracker, + userManager + ) as T } } } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/DoubtPreviewViewHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/DoubtPreviewViewHolder.kt index 2d37b9b..367066f 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/DoubtPreviewViewHolder.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/DoubtPreviewViewHolder.kt @@ -1,26 +1,27 @@ package com.doubtless.doubtless.screens.doubt.view.viewholder -import android.content.res.ColorStateList import android.text.util.Linkify -import android.util.Log import android.view.View +import android.widget.CheckBox import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView -import android.widget.Toast -import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.doubtless.doubtless.DoubtlessApp import com.doubtless.doubtless.R +import com.doubtless.doubtless.constants.GamificationConstants import com.doubtless.doubtless.screens.doubt.DoubtData import com.doubtless.doubtless.screens.doubt.usecases.VotingUseCase import com.doubtless.doubtless.utils.Utils import com.doubtless.doubtless.utils.Utils.flatten +import com.doubtless.doubtless.utils.Utils.toPx +import com.doubtless.doubtless.utils.addStateListAnimation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import java.util.* +import java.util.Date import kotlin.math.floor class DoubtPreviewViewHolder( @@ -40,31 +41,33 @@ class DoubtPreviewViewHolder( private val description: TextView private val ivDp: ImageView private val tvNetVotes: TextView - private val ivUpvotes: ImageView - private val ivDownvotes: ImageView + private val upvotes: CheckBox + private val downvotes: CheckBox private val tvAnswers: TextView private val tvTags: TextView private val tvCollege: TextView private val tvYear: TextView = itemView.findViewById(R.id.user_year) - private val icFire: ImageView = itemView.findViewById(R.id.ic_fire) + private val userBadge: ImageView = itemView.findViewById(R.id.user_badge) + private val llMentorsDp: LinearLayout = itemView.findViewById(R.id.ll_answered_mentor) + private val ivContent: ImageView = itemView.findViewById(R.id.iv_content) private val analyticsTracker = DoubtlessApp.getInstance().getAppCompRoot().getAnalyticsTracker() init { userName = view.findViewById(R.id.tv_username) time = view.findViewById(R.id.author_doubt_timestamp) - heading = view.findViewById(R.id.user_doubt_heading) - description = view.findViewById(R.id.user_doubt_description) + heading = view.findViewById(R.id.tv_poll_heading) + description = view.findViewById(R.id.tv_poll_description) ivDp = view.findViewById(R.id.iv_dp) tvNetVotes = view.findViewById(R.id.tv_votes) tvAnswers = view.findViewById(R.id.tv_answers) tvTags = view.findViewById(R.id.tv_tags) tvCollege = view.findViewById(R.id.user_college) - ivUpvotes = view.findViewById(R.id.iv_upvotes) - ivDownvotes = view.findViewById(R.id.iv_downvote) + upvotes = view.findViewById(R.id.cb_upvotes) + downvotes = view.findViewById(R.id.cb_downvote) - ivUpvotes.isVisible = showVotingLayout - ivDownvotes.isVisible = showVotingLayout + upvotes.isVisible = showVotingLayout + downvotes.isVisible = showVotingLayout } fun setData(doubtData: DoubtData) { @@ -92,30 +95,62 @@ class DoubtPreviewViewHolder( tvNetVotes.text = kotlin.math.floor(doubtData.netVotes).toInt().toString() - tvYear.text = "| ${doubtData.year} Year |" + if (doubtData.year.equals("passout", ignoreCase = true)) { + tvYear.text = "| ${doubtData.year} |" + } else { + tvYear.text = "| ${doubtData.year} Year |" + } description.text = doubtData.description?.trim() description.isVisible = !doubtData.description.isNullOrEmpty() tvAnswers.text = doubtData.no_answers.toString() - if (!doubtData.tags.isNullOrEmpty()) - tvTags.text = "Related to : " + doubtData.tags!!.flatten() - else + if (!doubtData.tags.isNullOrEmpty()) { + var tags = "" + + doubtData.tags?.forEach { + tags += "#$it " + } + + tvTags.text = tags + + } else tvTags.isVisible = false Glide.with(ivDp).load(doubtData.userPhotoUrl).circleCrop() .into(ivDp) - icFire.isVisible = doubtData.isTrending + // image content + if (!doubtData.imageContentUrl.isNullOrEmpty()) { + + ivContent.isVisible = true + + Glide.with(itemView.context) + .load(doubtData.imageContentUrl) + .into(ivContent) + + } else { + ivContent.isVisible = false + } + + userBadge.isVisible = doubtData.xpCount > GamificationConstants.MENTOR_XP_THRESHOLD + + + setupMentorsWhoInteractedDpUi(doubtData) + + // voting ui val votingUseCase = DoubtlessApp.getInstance().getAppCompRoot() .getDoubtVotingDoubtCase(doubtData.copy()) setVotesUi(doubtData, votingUseCase) - ivUpvotes.setOnClickListener { + + upvotes.setOnClickListener { CoroutineScope(Dispatchers.Main).launch { + if (it.stateListAnimator == null) + it.addStateListAnimation(R.animator.scale_votes_icon) val result = votingUseCase.upvoteDoubt() @@ -130,8 +165,10 @@ class DoubtPreviewViewHolder( } } - ivDownvotes.setOnClickListener { + downvotes.setOnClickListener { CoroutineScope(Dispatchers.Main).launch { + if (it.stateListAnimator == null) + it.addStateListAnimation(R.animator.scale_votes_icon) val result = votingUseCase.downVoteDoubt() @@ -147,24 +184,55 @@ class DoubtPreviewViewHolder( } } - private fun setVotesUi(doubtData: DoubtData, votingUseCase: VotingUseCase) { - ivDownvotes.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_off_alt_24)) + private fun setupMentorsWhoInteractedDpUi(doubtData: DoubtData) { - ivUpvotes.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_off_alt_24)) - // ivUpvotes.setColorFilter(ContextCompat.getColor(itemView.context, R.color.grey), android.graphics.PorterDuff.Mode.SRC_IN); + // reset view state first! + val dpsCount = llMentorsDp.childCount - 3 - tvNetVotes.text = floor(doubtData.netVotes).toInt().toString() + if (dpsCount > 0) { + repeat(dpsCount) { + llMentorsDp.removeView(llMentorsDp.getChildAt(it + 3)) // 3,4,5.. + } + } + + // then setup ui + doubtData.mentorsDpWhoInteracted.forEach { + if (it.isEmpty()) return@forEach + + val view = ImageView(itemView.context) + view.layoutParams = LinearLayout.LayoutParams(22.toPx().toInt(), 22.toPx().toInt()) + .apply { + this.marginStart = 4.toPx().toInt() + } + + Glide.with(itemView.context).load(it).circleCrop().into(view) + llMentorsDp.addView(view) + } + + llMentorsDp.isVisible = !doubtData.mentorsDpWhoInteracted.isEmpty() + + } + + private fun setVotesUi(doubtData: DoubtData, votingUseCase: VotingUseCase) { + tvNetVotes.text = floor(doubtData.netVotes).toInt().toString() CoroutineScope(Dispatchers.Main).launch { - val currentState = votingUseCase.getUserCurrentState() + when (votingUseCase.getUserCurrentState()) { + VotingUseCase.UPVOTED -> { + downvotes.isClickable = false + upvotes.isChecked = true + } - if (currentState == VotingUseCase.UPVOTED) { - ivUpvotes.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_filled)) - // ivUpvotes.setColorFilter(ContextCompat.getColor(itemView.context, R.color.purple), android.graphics.PorterDuff.Mode.SRC_IN); - } + VotingUseCase.DOWNVOTED -> { + upvotes.isClickable = false + downvotes.isChecked = true + } - if (currentState == VotingUseCase.DOWNVOTED) - ivDownvotes.setImageDrawable(itemView.context.getDrawable(R.drawable.ic_baseline_thumb_up_filled)) + else -> { + downvotes.isClickable = true + upvotes.isClickable = true + } + } } } } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/PollsViewHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/PollsViewHolder.kt new file mode 100644 index 0000000..1117e76 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/doubt/view/viewholder/PollsViewHolder.kt @@ -0,0 +1,41 @@ +package com.doubtless.doubtless.screens.doubt.view.viewholder + +import android.view.View +import android.view.ViewGroup +import android.widget.CheckBox +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.doubtless.doubtless.R + +class PollsViewHolder constructor( + private val view: View +) : ViewHolder(view) { + + private val userName: TextView + private val time: TextView + private val heading: TextView + private val ivDp: ImageView + private val tvNetVotes: TextView + private val upvotes: CheckBox + private val downvotes: CheckBox + private val tvAnswers: TextView + private val tvCollege: TextView + private val tvYear: TextView = itemView.findViewById(R.id.user_year) + private val userBadge: ImageView = itemView.findViewById(R.id.user_badge) + + init { + userName = view.findViewById(R.id.tv_username) + time = view.findViewById(R.id.author_doubt_timestamp) + heading = view.findViewById(R.id.user_doubt_heading) + ivDp = view.findViewById(R.id.iv_dp) + tvNetVotes = view.findViewById(R.id.tv_votes) + tvAnswers = view.findViewById(R.id.tv_answers) + tvCollege = view.findViewById(R.id.user_college) + upvotes = view.findViewById(R.id.cb_upvotes) + downvotes = view.findViewById(R.id.cb_downvote) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/home/HomeFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/home/HomeFragment.kt index f1fc215..c2db25f 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/home/HomeFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/home/HomeFragment.kt @@ -12,7 +12,7 @@ import com.doubtless.doubtless.R import com.doubtless.doubtless.databinding.FragmentHomeBinding import com.doubtless.doubtless.navigation.FragNavigator import com.doubtless.doubtless.navigation.OnBackPressListener -import com.doubtless.doubtless.screens.doubt.view.ViewDoubtsFragment +import com.doubtless.doubtless.screens.doubt.HomeMainScreenFragment import com.doubtless.doubtless.screens.main.MainActivity class HomeFragment : Fragment() { @@ -21,6 +21,7 @@ class HomeFragment : Fragment() { private lateinit var navigator: FragNavigator private var _binding: FragmentHomeBinding? = null + // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! @@ -30,7 +31,7 @@ class HomeFragment : Fragment() { if (savedInstanceState == null) { childFragmentManager.commit { - replace(R.id.bottomNav_child_container, ViewDoubtsFragment()) + replace(R.id.bottomNav_child_container, HomeMainScreenFragment()) } } diff --git a/app/src/main/java/com/doubtless/doubtless/screens/home/entities/FeedEntity.kt b/app/src/main/java/com/doubtless/doubtless/screens/home/entities/FeedEntity.kt index 1c455b8..9f90963 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/home/entities/FeedEntity.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/home/entities/FeedEntity.kt @@ -6,16 +6,21 @@ import com.doubtless.doubtless.screens.search.SearchResult data class FeedEntity( val type: Int, val doubt: DoubtData? = null, - val search_doubt: SearchResult? = null + val search_doubt: SearchResult? = null, + val pollOptions: List? = null ) { companion object { const val TYPE_DOUBT = 1 - const val TYPE_SEARCH = 2 - const val TYPE_SEARCH_RESULT = 3 - const val TYPE_USER_PROFILE = 4 + const val TYPE_SEARCH_RESULT = 2 + const val TYPE_USER_PROFILE = 3 + const val TYPE_BUTTONS = 4 + const val TYPE_POLL = 5 - fun getSearchEntity(): FeedEntity { - return FeedEntity(TYPE_SEARCH, null, null) + fun getOptionButtons(): FeedEntity{ + return FeedEntity(TYPE_BUTTONS, null, null) + } + fun getPollEntity(pollOptions: List): FeedEntity { + return FeedEntity(TYPE_POLL, null, null, pollOptions) } } } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByDateUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByDateUseCase.kt index 1f57813..55fe491 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByDateUseCase.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByDateUseCase.kt @@ -2,7 +2,7 @@ package com.doubtless.doubtless.screens.home.usecases import com.doubtless.doubtless.constants.FirestoreCollection import com.doubtless.doubtless.screens.doubt.DoubtData -import com.google.firebase.firestore.FieldPath +import com.doubtless.doubtless.screens.doubt.usecases.FetchInteractedMentorDataForDoubt import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query import com.google.firebase.firestore.QuerySnapshot @@ -11,6 +11,7 @@ import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext class FetchFeedByDateUseCase constructor( + private val fetchInteractedMentorDataForDoubt: FetchInteractedMentorDataForDoubt, private val firestore: FirebaseFirestore ) { @@ -25,8 +26,25 @@ class FetchFeedByDateUseCase constructor( withContext(Dispatchers.IO) { try { - var query = firestore.collection(FirestoreCollection.AllDoubts) - .orderBy("created_on", Query.Direction.DESCENDING) + + var query = when (request.tag) { + FirestoreCollection.TAG_ALL -> { + firestore.collection(FirestoreCollection.AllDoubts) + .orderBy("created_on", Query.Direction.DESCENDING) + } + + FirestoreCollection.TAG_MY_COLLEGE -> { + firestore.collection(FirestoreCollection.AllDoubts) + .whereEqualTo("author_college", request.user.local_user_attr?.college) + .orderBy("created_on", Query.Direction.DESCENDING) + } + + else -> { + firestore.collection(FirestoreCollection.AllDoubts) + .whereArrayContains("tags", request.tag) + .orderBy("created_on", Query.Direction.DESCENDING) + } + } if (lastDoubtData != null && request.fetchFromPage1 == false) { query = query.startAfter(lastDoubtData!!.date) @@ -50,17 +68,18 @@ class FetchFeedByDateUseCase constructor( } @kotlin.jvm.Throws(Exception::class) - private fun getDoubtDataList(result: QuerySnapshot?): List { + private suspend fun getDoubtDataList(result: QuerySnapshot?): List = + withContext(Dispatchers.IO) { - val doubtDataList = mutableListOf() + val doubtDataList = mutableListOf() - result!!.documents.forEach { - val doubtData = DoubtData.parse(it) ?: return@forEach - doubtDataList.add(doubtData) - } + result!!.documents.forEach { + val doubtData = DoubtData.parse(it) ?: return@forEach + doubtDataList.add(doubtData) + } - return doubtDataList - } + return@withContext doubtDataList + } } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByPopularityUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByPopularityUseCase.kt index cc2fbe6..a239c92 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByPopularityUseCase.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchFeedByPopularityUseCase.kt @@ -2,7 +2,7 @@ package com.doubtless.doubtless.screens.home.usecases import com.doubtless.doubtless.constants.FirestoreCollection import com.doubtless.doubtless.screens.doubt.DoubtData -import com.google.firebase.firestore.FieldPath +import com.doubtless.doubtless.screens.doubt.usecases.FetchInteractedMentorDataForDoubt import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query import com.google.firebase.firestore.QuerySnapshot @@ -11,6 +11,7 @@ import kotlinx.coroutines.tasks.await import kotlinx.coroutines.withContext class FetchFeedByPopularityUseCase constructor( + private val fetchInteractedMentorDataForDoubt: FetchInteractedMentorDataForDoubt, private val firestore: FirebaseFirestore ) { @@ -25,8 +26,25 @@ class FetchFeedByPopularityUseCase constructor( withContext(Dispatchers.IO) { try { - var query = firestore.collection(FirestoreCollection.AllDoubts) - .orderBy("net_votes", Query.Direction.DESCENDING) + var query = when (request.tag) { + FirestoreCollection.TAG_ALL -> { + firestore.collection(FirestoreCollection.AllDoubts) + .orderBy("net_votes", Query.Direction.DESCENDING) + } + + FirestoreCollection.TAG_MY_COLLEGE -> { + firestore.collection(FirestoreCollection.AllDoubts) + .whereEqualTo("author_college", request.user.local_user_attr?.college) + .orderBy("net_votes", Query.Direction.DESCENDING) + } + + else -> { + firestore.collection(FirestoreCollection.AllDoubts) + .whereArrayContains("tags", request.tag) + .orderBy("net_votes", Query.Direction.DESCENDING) + } + } + if (lastDoubtData != null && request.fetchFromPage1 == false) { query = query.startAfter(lastDoubtData!!.date) @@ -39,8 +57,7 @@ class FetchFeedByPopularityUseCase constructor( val result = query.get().await() val doubtDataList = getDoubtDataList(result) - if (doubtDataList.isNotEmpty()) - lastDoubtData = doubtDataList.last() + if (doubtDataList.isNotEmpty()) lastDoubtData = doubtDataList.last() return@withContext Result.Success(doubtDataList) @@ -50,17 +67,18 @@ class FetchFeedByPopularityUseCase constructor( } @kotlin.jvm.Throws(Exception::class) - private fun getDoubtDataList(result: QuerySnapshot?): List { + private suspend fun getDoubtDataList(result: QuerySnapshot?): List = + withContext(Dispatchers.IO) { - val doubtDataList = mutableListOf() + val doubtDataList = mutableListOf() - result!!.documents.forEach { - val doubtData = DoubtData.parse(it) ?: return@forEach - doubtDataList.add(doubtData) - } + result!!.documents.forEach { + val doubtData = DoubtData.parse(it) ?: return@forEach + doubtDataList.add(doubtData) + } - return doubtDataList - } + return@withContext doubtDataList + } } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchHomeFeedUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchHomeFeedUseCase.kt index f5905d0..6859705 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchHomeFeedUseCase.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/home/usecases/FetchHomeFeedUseCase.kt @@ -1,16 +1,17 @@ package com.doubtless.doubtless.screens.home.usecases +import android.os.UserManager import android.util.Log import androidx.annotation.WorkerThread import com.doubtless.doubtless.constants.FirestoreCollection import com.doubtless.doubtless.screens.auth.User import com.doubtless.doubtless.screens.doubt.DoubtData import com.doubtless.doubtless.screens.home.entities.FeedConfig -import com.google.firebase.firestore.* +import com.google.firebase.firestore.AggregateSource +import com.google.firebase.firestore.FirebaseFirestore import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.withContext -import java.lang.Math.abs import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit @@ -24,7 +25,10 @@ class FetchHomeFeedUseCase constructor( data class FetchHomeFeedRequest( val user: User, val pageSize: Int = 10, - val fetchFromPage1: Boolean = false + val fetchFromPage1: Boolean = false, + val tag: String + + ) sealed class Result { diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotifContainerFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotifContainerFragment.kt new file mode 100644 index 0000000..de036fa --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotifContainerFragment.kt @@ -0,0 +1,59 @@ +package com.doubtless.doubtless.screens.inAppNotification + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import com.doubtless.doubtless.DoubtlessApp +import com.doubtless.doubtless.R +import com.doubtless.doubtless.navigation.FragNavigator +import com.doubtless.doubtless.navigation.OnBackPressListener +import com.doubtless.doubtless.screens.dashboard.DashboardFragment +import com.doubtless.doubtless.screens.main.MainActivity +import com.doubtless.doubtless.screens.main.MainFragment + +class InAppNotificationContainerFragment: Fragment(R.layout.fragment_home) { + + private lateinit var navigator: FragNavigator + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (savedInstanceState == null) { + childFragmentManager.commit { + replace(R.id.bottomNav_child_container, InAppNotificationFragment()) + } + } + + navigator = DoubtlessApp.getInstance().getAppCompRoot() + .getInAppFragNavigator(requireActivity() as MainActivity)!! + } + + private val onBackPressListener = object : OnBackPressListener { + override fun onBackPress(): Boolean { + + val backPressConsumed = navigator.onBackPress() + + return if (backPressConsumed) + true + else { + (parentFragment as? MainFragment)?.selectHomeBottomNavElement() + true + } + } + } + + override fun onResume() { + super.onResume() + (requireActivity() as MainActivity).registerBackPress(onBackPressListener) + } + + override fun onPause() { + super.onPause() + (requireActivity() as MainActivity).unregisterBackPress(onBackPressListener) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationAdapter.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationAdapter.kt new file mode 100644 index 0000000..029308e --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationAdapter.kt @@ -0,0 +1,50 @@ +package com.doubtless.doubtless.screens.inAppNotification + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.doubtless.doubtless.R +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity + +class InAppNotificationAdapter constructor( + private val notifications: List, + private val interactionListener: InteractionListener +) : RecyclerView.Adapter() { + + interface InteractionListener { + fun onPostAnswerNotifClicked(doubtId: String) + } + + // rn there is only post answer type. + private val TYPE_POST_ANSWER = 1 + + private val data: MutableList = notifications.toMutableList() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + // fyi: passing null in the parent, renders spacings incorrectly. + val itemView = + LayoutInflater.from(parent.context).inflate(R.layout.item_post_answer_notif, parent, false) + return PostAnswerNotificationViewHolder(itemView, interactionListener) + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (holder is PostAnswerNotificationViewHolder) { + holder.bind(data[position]) + } + } + + override fun getItemViewType(position: Int): Int { + return TYPE_POST_ANSWER + } + + override fun getItemCount(): Int { + return data.size + } + + fun setNewNotifications(newNotifications: MutableList) { + data.clear() + data.addAll(newNotifications) + notifyDataSetChanged() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationFragment.kt new file mode 100644 index 0000000..09ad73e --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationFragment.kt @@ -0,0 +1,148 @@ +package com.doubtless.doubtless.screens.inAppNotification + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import com.doubtless.doubtless.DoubtlessApp +import com.doubtless.doubtless.R +import com.doubtless.doubtless.databinding.FragmentInappNotifBinding +import com.doubtless.doubtless.navigation.FragNavigator +import com.doubtless.doubtless.navigation.OnBackPressListener +import com.doubtless.doubtless.screens.doubt.usecases.FetchDoubtDataFromDoubtIdUseCase +import com.doubtless.doubtless.screens.main.MainActivity +import com.doubtless.doubtless.screens.main.MainFragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class InAppNotificationFragment : Fragment() { + + private var _binding: FragmentInappNotifBinding? = null + private val binding get() = _binding!! + + private lateinit var adapter: InAppNotificationAdapter + private lateinit var navigator: FragNavigator + private lateinit var fetchDoubtDataFromDoubtIdUseCase: FetchDoubtDataFromDoubtIdUseCase + + private val viewModel: InAppNotificationViewModel by viewModels( + factoryProducer = { + InAppNotificationViewModel.Companion.Factory( + DoubtlessApp.getInstance().getAppCompRoot().getFetchNotificationUseCase(), + DoubtlessApp.getInstance().getAppCompRoot().getMarkInAppNotificationsReadUseCase() + ) + } + ) + + private var onCreateEventUnConsumed = true + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + onCreateEventUnConsumed = true + navigator = DoubtlessApp.getInstance().getAppCompRoot() + .getInAppFragNavigator(requireActivity() as MainActivity)!! + fetchDoubtDataFromDoubtIdUseCase = + DoubtlessApp.getInstance().getAppCompRoot().getFetchDoubtDataFromDoubtIdUseCase() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentInappNotifBinding.inflate(inflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + if (onCreateEventUnConsumed) { + viewModel.fetchNotification() + onCreateEventUnConsumed = false + } + + if (::adapter.isInitialized) { + binding.rvNotif.adapter = adapter + } + + viewModel.notificationStatus.observe(viewLifecycleOwner) { + when (it) { + is InAppNotificationViewModel.Result.Success -> { + + binding.progressBar.isVisible = false + + if (::adapter.isInitialized == false) { + + adapter = InAppNotificationAdapter( + it.notifications.toMutableList(), + object : InAppNotificationAdapter.InteractionListener { + override fun onPostAnswerNotifClicked(doubtId: String) { + + CoroutineScope(Dispatchers.IO).launch { + + val doubtData = + fetchDoubtDataFromDoubtIdUseCase.getDoubtData(doubtId) + ?: return@launch + + if (!isAdded) return@launch + + navigator.moveToDoubtDetailFragment(doubtData) + } + } + }) + + binding.rvNotif.adapter = adapter + + if (it.additionalError != null) + Toast.makeText(requireContext(), it.additionalError, Toast.LENGTH_SHORT) + .show() + } else { + adapter.setNewNotifications(it.notifications.toMutableList()) + } + } + + is InAppNotificationViewModel.Result.Error -> { + binding.progressBar.isVisible = false + Toast.makeText(requireContext(), it.message, Toast.LENGTH_SHORT).show() + } + + is InAppNotificationViewModel.Result.NoData -> { + + if (it.additionalError != null) + Toast.makeText(requireContext(), it.additionalError, Toast.LENGTH_SHORT) + .show() + + binding.progressBar.isVisible = false + } + + is InAppNotificationViewModel.Result.Loading -> { + binding.progressBar.isVisible = true + } + } + } + } + + private val onBackPressListener = object : OnBackPressListener { + override fun onBackPress(): Boolean { + (parentFragment as? MainFragment)?.selectHomeBottomNavElement() + return true + } + } + + override fun onResume() { + super.onResume() + (requireActivity() as MainActivity).registerBackPress(onBackPressListener) + } + + override fun onPause() { + super.onPause() + viewModel.markNotificationsAsRead() + (requireActivity() as MainActivity).unregisterBackPress(onBackPressListener) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationViewModel.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationViewModel.kt new file mode 100644 index 0000000..d76add1 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/InAppNotificationViewModel.kt @@ -0,0 +1,89 @@ +package com.doubtless.doubtless.screens.inAppNotification + +import androidx.lifecycle.* +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity +import com.doubtless.doubtless.screens.inAppNotification.usecases.FetchInAppNotificationUseCase +import com.doubtless.doubtless.screens.inAppNotification.usecases.FetchUnreadNotificationUseCase +import com.doubtless.doubtless.screens.inAppNotification.usecases.MarkInAppNotificationsReadUseCase +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class InAppNotificationViewModel constructor( + private val fetchInAppNotificationUseCase: FetchInAppNotificationUseCase, + private val markInAppNotificationsReadUseCase: MarkInAppNotificationsReadUseCase +) : ViewModel() { + + sealed class Result { + class Success( + val notifications: List, + val additionalError: String? = null + ) : Result() + + class NoData(val additionalError: String? = null) : Result() + class Error(val message: String) : Result() + object Loading : Result() + } + + private val _notificationStatus = MutableLiveData() + val notificationStatus: LiveData = _notificationStatus + + fun fetchNotification() = CoroutineScope(Dispatchers.IO).launch { + + _notificationStatus.postValue(Result.Loading) + + val result = fetchInAppNotificationUseCase.getNotifications() + + when (result) { + is FetchInAppNotificationUseCase.Result.Success -> { + if (result.notifications.isNotEmpty()) + _notificationStatus.postValue( + Result.Success( + notifications = result.notifications.distinctBy { + it.notificationId + }.sortedByDescending { + it.createdOn?.toDate()?.time + }, + additionalError = result.error + ) + ) + else + _notificationStatus.postValue(Result.NoData(result.error)) + } + + is FetchInAppNotificationUseCase.Result.Error -> { + _notificationStatus.postValue(Result.Error(result.message)) + } + } + } + + fun markNotificationsAsRead() = CoroutineScope(Dispatchers.IO).launch { + if (_notificationStatus.value is Result.Success && _notificationStatus.value != null) { + + val notifs = + (_notificationStatus.value!! as Result.Success).notifications.toMutableList() + + markInAppNotificationsReadUseCase + .markRead(notifs) + + _notificationStatus.postValue(Result.Success(notifs.map { + it.copy(isRead = true) + })) + } + } + + companion object { + class Factory constructor( + private val fetchInAppNotificationUseCase: FetchInAppNotificationUseCase, + private val markInAppNotificationsReadUseCase: MarkInAppNotificationsReadUseCase + ) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return InAppNotificationViewModel( + fetchInAppNotificationUseCase, + markInAppNotificationsReadUseCase + ) as T + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/PostAnswerNotificationViewHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/PostAnswerNotificationViewHolder.kt new file mode 100644 index 0000000..ea38f49 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/PostAnswerNotificationViewHolder.kt @@ -0,0 +1,53 @@ +package com.doubtless.doubtless.screens.inAppNotification + +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.bumptech.glide.Glide +import com.doubtless.doubtless.R +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity +import com.doubtless.doubtless.utils.Utils + +class PostAnswerNotificationViewHolder( + itemView: View, + private val interactionListener: InAppNotificationAdapter.InteractionListener +) : ViewHolder(itemView) { + + private val tvTopDescription: TextView = itemView.findViewById(R.id.tv_top_description) + private val tvDoubtDescription: TextView = itemView.findViewById(R.id.tv_doubt_description) + private val tvUsername: TextView = itemView.findViewById(R.id.tv_username) + private val tvUserYear: TextView = itemView.findViewById(R.id.user_year) + private val tvUserCollege: TextView = itemView.findViewById(R.id.user_college) + private val tvTimestamp: TextView = itemView.findViewById(R.id.author_answer_timestamp) + private val ivDp: ImageView = itemView.findViewById(R.id.iv_dp) + private val tvAnswerDescription: TextView = itemView.findViewById(R.id.user_answer_description) + + fun bind(data: InAppNotificationEntity) { + + itemView.setOnClickListener { + interactionListener.onPostAnswerNotifClicked(data.doubtId!!) + } + + if (!data.isRead) { + itemView.setBackgroundColor(itemView.resources.getColor(R.color.light_purple)) + } else { + itemView.setBackgroundColor(itemView.resources.getColor(R.color.white)) + } + + tvDoubtDescription.text = data.doubtHeading + Glide.with(itemView.context).load(data.authorPhotoUrl).circleCrop().into(ivDp) + tvUsername.text = data.answerAuthorName + tvUserYear.text = "" + tvUserCollege.text = "" + + try { + tvTimestamp.text = Utils.getTimeAgo(data.createdOn?.toDate()!!) + } catch (e: Exception) { + + } + + tvAnswerDescription.text = data.description?.trim() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/dao/InAppNotificationDao.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/dao/InAppNotificationDao.kt new file mode 100644 index 0000000..0c9e5c1 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/dao/InAppNotificationDao.kt @@ -0,0 +1,17 @@ +package com.doubtless.doubtless.screens.inAppNotification.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity + +@Dao +interface InAppNotificationDao { + + @Query("SELECT * FROM InAppNotifications WHERE isRead = true") + fun getAllReadNotifications(): List + + @Insert + fun insertNewNotifications(notificationEntities: List) + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/model/InAppNotificationEntity.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/model/InAppNotificationEntity.kt new file mode 100644 index 0000000..dd4e95b --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/model/InAppNotificationEntity.kt @@ -0,0 +1,64 @@ +package com.doubtless.doubtless.screens.inAppNotification.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import com.google.errorprone.annotations.Keep +import com.google.firebase.Timestamp +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.Exclude +import com.google.firebase.firestore.IgnoreExtraProperties +import com.google.firebase.firestore.ServerTimestamp +import com.google.firebase.firestore.ktx.getField + +@Keep +@Entity(tableName = "InAppNotifications") +@IgnoreExtraProperties +data class InAppNotificationEntity( + @get:Exclude + @PrimaryKey + val notificationId: String, + val answerAuthorId: String? = null, + val answerAuthorName: String? = null, + val answerId: String? = null, + val authorPhotoUrl: String? = null, + @ServerTimestamp + val createdOn: Timestamp? = null, + val description: String? = null, + val doubtAuthorId: String? = null, + val doubtHeading: String? = null, + val doubtId: String? = null, + val type: String? = TYPE_POST_ANSWER, + val isRead: Boolean = false +) { + companion object { + + const val TYPE_POST_ANSWER = "postAnswer" + + fun fromDocumentSnapshot(documentSnapshot: DocumentSnapshot?): InAppNotificationEntity? { + return try { + val notif = InAppNotificationEntity( + notificationId = documentSnapshot!!.id, + answerAuthorId = documentSnapshot.getField("answer_author_id"), + answerAuthorName = documentSnapshot.getField("answer_author_name"), + answerId = documentSnapshot.getField("answer_id"), + authorPhotoUrl = documentSnapshot.getField("author_photo_url"), + createdOn = documentSnapshot.getField("created_on"), + description = documentSnapshot.getField("answer_description"), + doubtAuthorId = documentSnapshot.getField("doubt_author_id"), + doubtHeading = documentSnapshot.getField("doubt_heading"), + doubtId = documentSnapshot.getField("doubt_id"), + type = documentSnapshot.getField("type") + ) + + if (notif.type?.equals(TYPE_POST_ANSWER) == false) { + return null + } + + return notif + + } catch (e: Exception) { + null + } + } + } +} diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/FetchInAppNotificationUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/FetchInAppNotificationUseCase.kt new file mode 100644 index 0000000..1a1dfd0 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/FetchInAppNotificationUseCase.kt @@ -0,0 +1,52 @@ +package com.doubtless.doubtless.screens.inAppNotification.usecases + +import com.doubtless.doubtless.screens.inAppNotification.dao.InAppNotificationDao +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class FetchInAppNotificationUseCase constructor( + private val inAppNotificationDao: InAppNotificationDao, + private val unreadNotificationUseCase: FetchUnreadNotificationUseCase +) { + sealed class Result { + class Success(val notifications: List, val error: String? = null) : Result() + class Error(val message: String): Result() + } + + suspend fun getNotifications(): Result = withContext(Dispatchers.IO) { + try { + val notifications = mutableListOf() + + var error: String? = null + + // first fetch all unread notifications + val result = unreadNotificationUseCase.fetchNotification() + + if (result is FetchUnreadNotificationUseCase.Result.Success) { + notifications.addAll(result.notifications) + } + + if (result is FetchUnreadNotificationUseCase.Result.Error) { + error = result.message + } + + // now fetch old read notifications from db + try { + notifications.addAll(inAppNotificationDao.getAllReadNotifications()) + } catch (e: Exception) { + error = e.message + } + + if (notifications.isEmpty() && error != null) { + return@withContext Result.Error(error) + } + + return@withContext Result.Success(notifications, error) + + } catch (e: Exception) { + return@withContext Result.Error(e.message ?: "some error occurred!") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/FetchUnreadNotificationUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/FetchUnreadNotificationUseCase.kt new file mode 100644 index 0000000..7d6fd6b --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/FetchUnreadNotificationUseCase.kt @@ -0,0 +1,48 @@ +package com.doubtless.doubtless.screens.inAppNotification.usecases + +import com.doubtless.doubtless.constants.FirestoreCollection +import com.doubtless.doubtless.screens.auth.usecases.UserManager +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.withContext + +class FetchUnreadNotificationUseCase constructor( + private val firestore: FirebaseFirestore, + private val userManager: UserManager +) { + + sealed class Result { + class Success(val notifications: List) : Result() + class Error(val message: String) : Result() + } + + suspend fun fetchNotification(): Result = withContext(Dispatchers.IO) { + return@withContext try { + + val userId = userManager.getCachedUserData()!!.id + + val result = firestore.collection(FirestoreCollection.NOTIFICATION) + .whereEqualTo("doubt_author_id", userId) + .whereEqualTo("is_read", false) + .get().await() + + val notifications = mutableListOf() + + result.documents.forEach { + try { + notifications.add(InAppNotificationEntity.fromDocumentSnapshot(it)!!) + } catch (e: Exception) { + e.printStackTrace() + } + } + + return@withContext Result.Success(notifications) + + } catch (e: Exception) { + Result.Error(e.message ?: "some error occurred!") + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/MarkInAppNotificationsReadUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/MarkInAppNotificationsReadUseCase.kt new file mode 100644 index 0000000..3ed8ba5 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/inAppNotification/usecases/MarkInAppNotificationsReadUseCase.kt @@ -0,0 +1,52 @@ +package com.doubtless.doubtless.screens.inAppNotification.usecases + +import com.doubtless.doubtless.constants.FirestoreCollection +import com.doubtless.doubtless.screens.inAppNotification.dao.InAppNotificationDao +import com.doubtless.doubtless.screens.inAppNotification.model.InAppNotificationEntity +import com.google.firebase.firestore.FirebaseFirestore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class MarkInAppNotificationsReadUseCase constructor( + private val dao: InAppNotificationDao, + private val firestore: FirebaseFirestore +) { + + sealed class Result { + object Success : Result() + class Error(val message: String) : Result() + } + + suspend fun markRead(notifications: List): Result = + withContext(Dispatchers.IO) { + + try { + val markedReadNotifications = notifications.filter { + it.isRead == false + }.map { + it.copy(isRead = true) + } + + markReadOnFirestore(notifications) + + dao.insertNewNotifications(markedReadNotifications) + + return@withContext Result.Success + + } catch (e: Exception) { + return@withContext Result.Error(e.message ?: "some error occurred!") + } + + } + + private fun markReadOnFirestore(notifications: List) { + notifications.forEach { + + firestore.collection(FirestoreCollection.NOTIFICATION) + .document(it.notificationId) + .update("is_read", true) + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/main/MainActivity.kt b/app/src/main/java/com/doubtless/doubtless/screens/main/MainActivity.kt index 4827477..f1d70a8 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/main/MainActivity.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/main/MainActivity.kt @@ -25,6 +25,10 @@ class MainActivity : AppCompatActivity(), BackPressDispatcher { } } + fun getMainFragment(): MainFragment? { + return supportFragmentManager.findFragmentByTag("MainFragment") as MainFragment? + } + // ----- backpress impl ------ private val backPressListeners: MutableList = mutableListOf() diff --git a/app/src/main/java/com/doubtless/doubtless/screens/main/MainFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/main/MainFragment.kt index 336c7b5..eab986f 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/main/MainFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/main/MainFragment.kt @@ -13,6 +13,8 @@ import com.doubtless.doubtless.screens.dashboard.DashboardFragment import com.doubtless.doubtless.screens.doubt.create.CreateDoubtFragment import com.doubtless.doubtless.screens.doubt.view.ViewDoubtsFragment import com.doubtless.doubtless.screens.home.HomeFragment +import com.doubtless.doubtless.screens.inAppNotification.InAppNotificationContainerFragment +import com.doubtless.doubtless.screens.inAppNotification.InAppNotificationFragment class MainFragment : Fragment() { @@ -20,7 +22,12 @@ class MainFragment : Fragment() { private val binding get() = _binding!! private val bottomNavFragments = - listOf(HomeFragment(), CreateDoubtFragment(), DashboardContainerFragment()) + listOf( + HomeFragment(), + CreateDoubtFragment(), + InAppNotificationContainerFragment(), + DashboardContainerFragment() + ) private var areBottomNavFragmentsAdded = false @@ -76,8 +83,33 @@ class MainFragment : Fragment() { return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + fun selectHomeBottomNavElement() { + if (!binding.btmNavHome.isChecked) { + binding.btmNavHome.callOnClick() + } + } + + sealed class CurrentSelectedBottomNavFrag { + object HomeFrag : CurrentSelectedBottomNavFrag() + object CreateFrag : CurrentSelectedBottomNavFrag() + object InAppNotificationFrag : CurrentSelectedBottomNavFrag() + object DashboardFrag : CurrentSelectedBottomNavFrag() + object UnknownFrag : CurrentSelectedBottomNavFrag() + } + + fun getCurrentSelectedElement(): CurrentSelectedBottomNavFrag { + + val index = binding.retroBottomNav.getCurrentSelectedIndex() + + return when (index) { + 0 -> CurrentSelectedBottomNavFrag.HomeFrag + 1 -> CurrentSelectedBottomNavFrag.CreateFrag + 2 -> CurrentSelectedBottomNavFrag.InAppNotificationFrag + 3 -> CurrentSelectedBottomNavFrag.DashboardFrag + else -> { + CurrentSelectedBottomNavFrag.UnknownFrag + } + } } override fun onDestroyView() { diff --git a/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavData.kt b/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavData.kt index 9297c5d..218b845 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavData.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavData.kt @@ -14,4 +14,5 @@ interface OnSelectedItemChangedListener { interface BottomIntractableElement { fun onSelected() fun onUnselected() + fun onReselected() } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/RetroBottomNav.kt b/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavLayout.kt similarity index 89% rename from app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/RetroBottomNav.kt rename to app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavLayout.kt index 81404df..a052338 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/RetroBottomNav.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/main/bottomNav/BottomNavLayout.kt @@ -1,20 +1,15 @@ package com.doubtless.doubtless.screens.main.bottomNav import android.content.Context -import android.os.Bundle -import android.os.Parcelable -import android.os.Vibrator -import android.os.VibratorManager import android.util.AttributeSet import android.view.View -import android.widget.LinearLayout +import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.children -import com.doubtless.doubtless.theming.buttons.SecondaryButton -class RetroBottomNav( +class BottomNavLayout( context: Context, attributeSet: AttributeSet -) : LinearLayout(context, attributeSet) { +) : ConstraintLayout(context, attributeSet) { private var currentSelectedIndex: Int? = null private val elements: ArrayList = arrayListOf() @@ -44,11 +39,7 @@ class RetroBottomNav( private fun onNewSelectedIndex(index: Int) { onSelectedItemChangedListener?.onNewSelectedIndex(index) - elements[index].onSelected() -// -// (elements[index] as View).layoutParams = -// LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f) } private fun notifyAllUnSelectedElements() { @@ -66,7 +57,10 @@ class RetroBottomNav( clickedBtn: BottomIntractableElement ) { // proceed on only new index selection. - if (currentSelectedIndex == idx) return + if (currentSelectedIndex == idx) { + clickedBtn.onReselected() + return + } currentSelectedIndex = idx onSelectedItemChangedListener?.onNewSelectedIndex(idx) diff --git a/app/src/main/java/com/doubtless/doubtless/screens/poll/CreatePollFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/poll/CreatePollFragment.kt new file mode 100644 index 0000000..b4e1182 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/poll/CreatePollFragment.kt @@ -0,0 +1,43 @@ +package com.doubtless.doubtless.screens.poll + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import com.doubtless.doubtless.R +import com.doubtless.doubtless.databinding.FragmentCreatePollBinding + + +class CreatePollFragment : Fragment() { + + private var _binding : FragmentCreatePollBinding? = null + private val binding get() = _binding!! + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + // Inflate the layout for this fragment + _binding = FragmentCreatePollBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.topbarPoll.setOnClickListener { + requireActivity().onBackPressed() + } + } + + override fun onDestroy() { + super.onDestroy() + _binding=null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/poll/PollData.kt b/app/src/main/java/com/doubtless/doubtless/screens/poll/PollData.kt new file mode 100644 index 0000000..936c186 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/poll/PollData.kt @@ -0,0 +1,9 @@ +package com.doubtless.doubtless.screens.poll + +data class PollData( + val id: String? = null, + val pollOptions: List? = null, + val pollOptionVotes: List? = null, + val totalVotes: String? = null, + + ) \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/poll/PollFuncUseCase.kt b/app/src/main/java/com/doubtless/doubtless/screens/poll/PollFuncUseCase.kt new file mode 100644 index 0000000..2b83789 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/poll/PollFuncUseCase.kt @@ -0,0 +1,4 @@ +package com.doubtless.doubtless.screens.poll + +class PollFuncUseCase { +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/poll/ViewPollViewHolder.kt b/app/src/main/java/com/doubtless/doubtless/screens/poll/ViewPollViewHolder.kt new file mode 100644 index 0000000..4ac224a --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/screens/poll/ViewPollViewHolder.kt @@ -0,0 +1,69 @@ +package com.doubtless.doubtless.screens.poll + +import android.view.LayoutInflater +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.doubtless.doubtless.R +import com.doubtless.doubtless.screens.home.entities.FeedEntity + +class ViewPollViewHolder(val view: View, val interactionListener: InteractionListener):RecyclerView.ViewHolder(view) { + + interface InteractionListener{ + fun onPollOptionClicked(position : Int) + } + + private val userName: TextView + private val heading: TextView + private val description: TextView + private val college: TextView + private val ivDp: ImageView + private val tvOption: TextView + private val progressBar: ProgressBar + private val llOptions: LinearLayout + private val time: TextView + private val tvYear: TextView + + init { + userName = view.findViewById(R.id.tv_username_poll) + heading = view.findViewById(R.id.tv_poll_heading) + description = view.findViewById(R.id.tv_poll_description) + college = view.findViewById(R.id.tv_poll_college) + ivDp = view.findViewById(R.id.tv_user_dp_poll) + progressBar = view.findViewById(R.id.progress_poll) + tvOption = view.findViewById(R.id. tv_option) + llOptions = view.findViewById(R.id.ll_options) + time = view.findViewById(R.id.author_doubt_timestamp2) + tvYear = view.findViewById(R.id.user_year2) + + } + + fun setData(feedEntity: FeedEntity) { + llOptions.removeAllViews() + + val pollOptions = feedEntity.pollOptions + if (pollOptions != null) { + for (i in pollOptions.indices) { + val option = pollOptions[i] + val optionView = createOptionView(option, i) + llOptions.addView(optionView) + } + } + } + private fun createOptionView(option: String, position: Int): View { + val optionView = LayoutInflater.from(view.context) + .inflate(R.layout.poll_options_layout, llOptions, false) + val tvOption = optionView.findViewById(R.id.tv_option) + tvOption.text = option + + optionView.setOnClickListener { + interactionListener.onPollOptionClicked(position) + } + + return optionView + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/screens/search/SearchFragment.kt b/app/src/main/java/com/doubtless/doubtless/screens/search/SearchFragment.kt index 92c71fb..7f165c9 100644 --- a/app/src/main/java/com/doubtless/doubtless/screens/search/SearchFragment.kt +++ b/app/src/main/java/com/doubtless/doubtless/screens/search/SearchFragment.kt @@ -18,7 +18,12 @@ import com.doubtless.doubtless.screens.common.GenericFeedAdapter import com.doubtless.doubtless.screens.doubt.DoubtData import com.doubtless.doubtless.screens.main.MainActivity import com.doubtless.doubtless.screens.search.usecases.FetchSearchResultsUseCase -import kotlinx.coroutines.* +import com.doubtless.doubtless.utils.hideSoftKeyboard +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class SearchFragment : Fragment() { @@ -53,24 +58,22 @@ class SearchFragment : Fragment() { } if (!::adapter.isInitialized) { - adapter = - GenericFeedAdapter( - genericFeedEntities = mutableListOf(), - onLastItemReached = {}, - interactionListener = object : GenericFeedAdapter.InteractionListener { - override fun onSearchBarClicked() {} - - override fun onDoubtClicked(doubtData: DoubtData, position: Int) { - analyticsTracker.trackSearchedDoubtClicked(doubtData.copy()) - navigator.moveToDoubtDetailFragment(doubtData) - } + adapter = GenericFeedAdapter(genericFeedEntities = mutableListOf(), + onLastItemReached = {}, + interactionListener = object : GenericFeedAdapter.InteractionListener { + override fun onDoubtClicked(doubtData: DoubtData, position: Int) { + analyticsTracker.trackSearchedDoubtClicked(doubtData.copy()) + navigator.moveToDoubtDetailFragment(doubtData) + } - override fun onSignOutClicked() {} + override fun onSignOutClicked() {} - override fun onSubmitFeedbackClicked() {} + override fun onSubmitFeedbackClicked() {} - override fun onDeleteAccountClicked() {} - }) + override fun onDeleteAccountClicked() {} + override fun onCreatePollClicked() {} + override fun onPollOptionClicked(position: Int) {} + }) } binding.rvSearchResults.adapter = adapter @@ -86,7 +89,7 @@ class SearchFragment : Fragment() { binding.progressSearch.visibility = View.VISIBLE - if (it.toString().length <= 1){ + if (it.toString().length <= 1) { delay(1000L) binding.progressSearch.visibility = View.GONE return@launch @@ -108,11 +111,12 @@ class SearchFragment : Fragment() { Toast.makeText(requireContext(), results.message, Toast.LENGTH_SHORT).show() return@launch } - adapter.appendDoubts((results as FetchSearchResultsUseCase.Result.Success) - .searchResult.map { - it.toGenericEntity() - }) + adapter.appendDoubts((results as FetchSearchResultsUseCase.Result.Success).searchResult.map { + it.toGenericEntity() + }) binding.progressSearch.visibility = View.GONE + requireView().hideSoftKeyboard() + binding.etSearch.clearFocus() } } @@ -129,10 +133,9 @@ class SearchFragment : Fragment() { } adapter.clearCurrentList() - adapter.appendDoubts((results as FetchSearchResultsUseCase.Result.Success) - .searchResult.map { - it.toGenericEntity() - }) + adapter.appendDoubts((results as FetchSearchResultsUseCase.Result.Success).searchResult.map { + it.toGenericEntity() + }) } } diff --git a/app/src/main/java/com/doubtless/doubtless/theming/bottomNav/SoberBottomNavElementLayout.kt b/app/src/main/java/com/doubtless/doubtless/theming/bottomNav/SoberBottomNavElementLayout.kt new file mode 100644 index 0000000..2ad3246 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/theming/bottomNav/SoberBottomNavElementLayout.kt @@ -0,0 +1,24 @@ +package com.doubtless.doubtless.theming.bottomNav + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatCheckBox +import com.doubtless.doubtless.screens.main.bottomNav.BottomIntractableElement + +class SoberBottomNavElementLayout(context: Context, attributeSet: AttributeSet?) + : AppCompatCheckBox(context, attributeSet), BottomIntractableElement { + + override fun onSelected() { + isChecked = true + } + + override fun onReselected() { + // on reselection the checked icon will toggle, hence manually make it checked. + isChecked = true + } + + override fun onUnselected() { + isChecked = false + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/theming/buttons/RetroBottomNavElementLayout.kt b/app/src/main/java/com/doubtless/doubtless/theming/buttons/RetroBottomNavElementLayout.kt index 600763a..db36ecd 100644 --- a/app/src/main/java/com/doubtless/doubtless/theming/buttons/RetroBottomNavElementLayout.kt +++ b/app/src/main/java/com/doubtless/doubtless/theming/buttons/RetroBottomNavElementLayout.kt @@ -37,4 +37,8 @@ class RetroBottomNavElementLayout(context: Context, attributeSet: AttributeSet?) isCurrentlySelected = false } + override fun onReselected() { + + } + } \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/theming/buttons/SecondaryButton.kt b/app/src/main/java/com/doubtless/doubtless/theming/buttons/SecondaryButton.kt index d017866..66db7d5 100644 --- a/app/src/main/java/com/doubtless/doubtless/theming/buttons/SecondaryButton.kt +++ b/app/src/main/java/com/doubtless/doubtless/theming/buttons/SecondaryButton.kt @@ -76,6 +76,10 @@ class SecondaryButton constructor( setCardBackgroundColor(resources.getColor(R.color.cream)) } + override fun onReselected() { + + } + override fun onUnselected() { setCardBackgroundColor(resources.getColor(R.color.cream)) isCurrentlySelected = false diff --git a/app/src/main/java/com/doubtless/doubtless/utils/Extensions.kt b/app/src/main/java/com/doubtless/doubtless/utils/Extensions.kt new file mode 100644 index 0000000..1c9064a --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/utils/Extensions.kt @@ -0,0 +1,19 @@ +package com.doubtless.doubtless.utils + +import android.animation.AnimatorInflater +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.annotation.AnimatorRes + +fun View.addStateListAnimation(@AnimatorRes animation: Int) { + this.stateListAnimator = AnimatorInflater.loadStateListAnimator( + this.context, + animation + ) +} + +fun View.hideSoftKeyboard() { + val imm = this.context.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager + imm?.hideSoftInputFromWindow(this.windowToken, 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/utils/Resource.kt b/app/src/main/java/com/doubtless/doubtless/utils/Resource.kt new file mode 100644 index 0000000..1907ac5 --- /dev/null +++ b/app/src/main/java/com/doubtless/doubtless/utils/Resource.kt @@ -0,0 +1,12 @@ +package com.doubtless.doubtless.utils + +sealed class Resource( + val data: T? = null, + val message: String? = null, + val error: Exception? = null +) { + class Success(data: T) : Resource(data) + class Loading : Resource() + class Error(message: String? = null, data: T? = null, error: Exception? = null) : + Resource(data, message, error) +} \ No newline at end of file diff --git a/app/src/main/java/com/doubtless/doubtless/utils/Utils.kt b/app/src/main/java/com/doubtless/doubtless/utils/Utils.kt index c22cfc8..c7f6c9f 100644 --- a/app/src/main/java/com/doubtless/doubtless/utils/Utils.kt +++ b/app/src/main/java/com/doubtless/doubtless/utils/Utils.kt @@ -25,6 +25,12 @@ object Utils { } } + fun Int.toPx() = TypedValue.applyDimension( + /* unit = */ TypedValue.COMPLEX_UNIT_DIP, + /* value = */ this.toFloat(), + /* metrics = */ Resources.getSystem().displayMetrics + ) + fun List.flatten(): String { var string = "" diff --git a/app/src/main/res/animator/scale_votes_icon.xml b/app/src/main/res/animator/scale_votes_icon.xml new file mode 100644 index 0000000..1e2e200 --- /dev/null +++ b/app/src/main/res/animator/scale_votes_icon.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_rec_rounded.xml b/app/src/main/res/drawable/bg_rec_rounded.xml new file mode 100644 index 0000000..0e4826c --- /dev/null +++ b/app/src/main/res/drawable/bg_rec_rounded.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/create_checkable_icon.xml b/app/src/main/res/drawable/create_checkable_icon.xml new file mode 100644 index 0000000..5d42350 --- /dev/null +++ b/app/src/main/res/drawable/create_checkable_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/dashboard_checkable_icon.xml b/app/src/main/res/drawable/dashboard_checkable_icon.xml new file mode 100644 index 0000000..5742478 --- /dev/null +++ b/app/src/main/res/drawable/dashboard_checkable_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/home_checkable_icon.xml b/app/src/main/res/drawable/home_checkable_icon.xml new file mode 100644 index 0000000..bebb2a2 --- /dev/null +++ b/app/src/main/res/drawable/home_checkable_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_notifications_24.xml b/app/src/main/res/drawable/ic_baseline_notifications_24.xml new file mode 100644 index 0000000..1d038a4 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_notifications_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_add_24.xml b/app/src/main/res/drawable/ic_outline_add_24.xml new file mode 100644 index 0000000..b063a36 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_add_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_comment_24.xml b/app/src/main/res/drawable/ic_outline_comment_24.xml new file mode 100644 index 0000000..d0440be --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_comment_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_dashboard_24.xml b/app/src/main/res/drawable/ic_outline_dashboard_24.xml new file mode 100644 index 0000000..5fbe0ef --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_dashboard_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_home_24.xml b/app/src/main/res/drawable/ic_outline_home_24.xml new file mode 100644 index 0000000..678f2e0 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_home_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_notifications_24.xml b/app/src/main/res/drawable/ic_outline_notifications_24.xml new file mode 100644 index 0000000..3d20551 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_notifications_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_poll.xml b/app/src/main/res/drawable/ic_poll.xml new file mode 100644 index 0000000..2411491 --- /dev/null +++ b/app/src/main/res/drawable/ic_poll.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_arrow_24.xml b/app/src/main/res/drawable/ic_reply_arrow_24.xml new file mode 100644 index 0000000..f75e16a --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_arrow_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_verified.xml b/app/src/main/res/drawable/ic_verified.xml new file mode 100644 index 0000000..84824b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_verified.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/notif_checkable_icon.xml b/app/src/main/res/drawable/notif_checkable_icon.xml new file mode 100644 index 0000000..b199ae9 --- /dev/null +++ b/app/src/main/res/drawable/notif_checkable_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/poll_item_border.xml b/app/src/main/res/drawable/poll_item_border.xml new file mode 100644 index 0000000..5f47c34 --- /dev/null +++ b/app/src/main/res/drawable/poll_item_border.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/upvote_icon.xml b/app/src/main/res/drawable/upvote_icon.xml new file mode 100644 index 0000000..8f17949 --- /dev/null +++ b/app/src/main/res/drawable/upvote_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/user_profile_thumnail.png b/app/src/main/res/drawable/user_profile_thumnail.png new file mode 100644 index 0000000..15ca8ee Binary files /dev/null and b/app/src/main/res/drawable/user_profile_thumnail.png differ diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 3bbafb7..f555fd8 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="30dp" - app:cardCornerRadius="12dp" + app:cardCornerRadius="8dp" app:cardElevation="12dp" app:layout_constraintBottom_toBottomOf="@id/iv_banner" app:layout_constraintTop_toTopOf="parent"> diff --git a/app/src/main/res/layout/answer_layout.xml b/app/src/main/res/layout/answer_layout.xml index e7aaa65..8aa5de8 100644 --- a/app/src/main/res/layout/answer_layout.xml +++ b/app/src/main/res/layout/answer_layout.xml @@ -5,9 +5,6 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="0dp" - android:layout_marginEnd="0dp" - android:layout_marginBottom="0dp" android:padding="10dp" app:cardBackgroundColor="@color/cream" app:cardCornerRadius="0dp" @@ -16,17 +13,19 @@ + android:layout_height="8dp" + android:background="@color/separation_grey" + android:visibility="gone" /> + + - + app:layout_constraintEnd_toStartOf="@id/tv_votes" + app:layout_constraintTop_toBottomOf="@id/author_answer_description_2" /> + app:layout_constraintBottom_toBottomOf="@id/cb_upvote" + app:layout_constraintEnd_toStartOf="@id/cb_downvote" + app:layout_constraintTop_toTopOf="@id/cb_upvote" /> - diff --git a/app/src/main/res/layout/doubt_layout.xml b/app/src/main/res/layout/doubt_layout.xml index 4a0324e..9dd7409 100644 --- a/app/src/main/res/layout/doubt_layout.xml +++ b/app/src/main/res/layout/doubt_layout.xml @@ -16,7 +16,7 @@ + android:background="@color/separation_grey" /> @@ -67,12 +67,12 @@ android:id="@+id/user_year" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="4dp" + android:layout_marginStart="2dp" android:fontFamily="@font/poppins" android:text="| 4th year |" android:textColor="@color/grey" android:textSize="12dp" - app:layout_constraintStart_toEndOf="@id/tv_username" + app:layout_constraintStart_toEndOf="@+id/user_badge" app:layout_constraintTop_toTopOf="@id/tv_username" /> + app:layout_constraintTop_toBottomOf="@id/tv_poll_heading" /> + app:layout_constraintTop_toBottomOf="@id/iv_votes" + tools:text="Related to : attendance, marks, teachers" /> + app:layout_constraintTop_toBottomOf="@id/tv_poll_description" /> - + app:layout_constraintTop_toBottomOf="@id/tv_tags" /> - + app:layout_constraintStart_toEndOf="@id/cb_upvotes" + app:layout_constraintTop_toTopOf="@id/cb_upvotes" /> diff --git a/app/src/main/res/layout/enter_answer_layout.xml b/app/src/main/res/layout/enter_answer_layout.xml index f6ddc70..31a50f2 100644 --- a/app/src/main/res/layout/enter_answer_layout.xml +++ b/app/src/main/res/layout/enter_answer_layout.xml @@ -16,8 +16,9 @@ + android:layout_height="8dp" + android:visibility="gone" + android:background="@color/separation_grey" /> @@ -26,7 +27,7 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="22dp" - android:paddingTop="15dp" + android:paddingTop="24dp" android:paddingBottom="10dp"> + android:src="@drawable/ic_baseline_arrow_back_ios_24" + app:tint="@color/white" /> + android:textColor="@color/white" + android:textSize="16dp" /> diff --git a/app/src/main/res/layout/fragment_create_poll.xml b/app/src/main/res/layout/fragment_create_poll.xml new file mode 100644 index 0000000..61da021 --- /dev/null +++ b/app/src/main/res/layout/fragment_create_poll.xml @@ -0,0 +1,197 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home_main_screen.xml b/app/src/main/res/layout/fragment_home_main_screen.xml new file mode 100644 index 0000000..9816de9 --- /dev/null +++ b/app/src/main/res/layout/fragment_home_main_screen.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_inapp_notif.xml b/app/src/main/res/layout/fragment_inapp_notif.xml new file mode 100644 index 0000000..e43c4e5 --- /dev/null +++ b/app/src/main/res/layout/fragment_inapp_notif.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 75c382b..2dccd33 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -14,127 +14,62 @@ app:layout_constraintBottom_toTopOf="@id/retro_bottom_nav" app:layout_constraintTop_toTopOf="parent" /> - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_post_answer_notif.xml b/app/src/main/res/layout/item_post_answer_notif.xml new file mode 100644 index 0000000..254a777 --- /dev/null +++ b/app/src/main/res/layout/item_post_answer_notif.xml @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_home_buttons.xml b/app/src/main/res/layout/layout_home_buttons.xml new file mode 100644 index 0000000..a83c8b1 --- /dev/null +++ b/app/src/main/res/layout/layout_home_buttons.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_polls.xml b/app/src/main/res/layout/layout_polls.xml new file mode 100644 index 0000000..ff98a34 --- /dev/null +++ b/app/src/main/res/layout/layout_polls.xml @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/poll_create_layout.xml b/app/src/main/res/layout/poll_create_layout.xml new file mode 100644 index 0000000..617f11f --- /dev/null +++ b/app/src/main/res/layout/poll_create_layout.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/poll_options_layout.xml b/app/src/main/res/layout/poll_options_layout.xml new file mode 100644 index 0000000..5caafa5 --- /dev/null +++ b/app/src/main/res/layout/poll_options_layout.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/user_profile_layout.xml b/app/src/main/res/layout/user_profile_layout.xml index a29f929..5395a10 100644 --- a/app/src/main/res/layout/user_profile_layout.xml +++ b/app/src/main/res/layout/user_profile_layout.xml @@ -2,35 +2,42 @@ + android:elevation="0dp"> - + android:layout_height="wrap_content"> + + + app:cardCornerRadius="50dp" + app:cardElevation="1dp" + app:layout_constraintBottom_toBottomOf="@id/iv_thumbnail" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/iv_thumbnail"> @@ -39,19 +46,40 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" - android:layout_marginTop="4dp" - android:text="User Name" + android:layout_marginTop="14dp" + android:fontFamily="@font/poppins" android:textColor="@color/black" - android:textSize="14dp" /> + android:textSize="20dp" + app:layout_constraintStart_toStartOf="@id/cv_user_image" + app:layout_constraintTop_toBottomOf="@id/cv_user_image" + tools:text="Siddharth Sharma" /> + android:layout_marginTop="0dp" + android:fontFamily="@font/poppins" + android:text="siddharthsharma@gmail.com" + android:textSize="14dp" + app:layout_constraintStart_toStartOf="@id/tv_name" + app:layout_constraintTop_toBottomOf="@id/tv_name" /> + + + android:textSize="12dp" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + android:textSize="12dp" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + - + \ No newline at end of file diff --git a/app/src/main/res/layout/view_poll.xml b/app/src/main/res/layout/view_poll.xml new file mode 100644 index 0000000..3498cf0 --- /dev/null +++ b/app/src/main/res/layout/view_poll.xml @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d23b332..27bcd8e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -8,5 +8,7 @@ #e090c9 #fab679 - #A93AFF + #7f5eff + #FAF4FF + #99E1E1E1 \ 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 d165fb3..181b759 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,5 @@ Higher Studies Placements Please select at max 3 tags + Please sign in again! \ No newline at end of file diff --git a/app/src/main/res/values/style.xml b/app/src/main/res/values/style.xml new file mode 100644 index 0000000..c263808 --- /dev/null +++ b/app/src/main/res/values/style.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index ef567ae..a10bca0 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -14,7 +14,7 @@ @color/cream - true + false true @color/cream