From ace469a81696d24ee0dc4f586872b7ab47b3a16c Mon Sep 17 00:00:00 2001 From: Cooltey Feng Date: Thu, 14 Nov 2024 11:21:50 -0800 Subject: [PATCH] [Feature branch] The Contributions Dashboard (#5013) * [Feature branch] The Contributions Dashboard * Update target languages and due date * [Contributions dashboard] bottom nav tab rename (#5019) * - adds conditional logic to support "Contribute" tab * - adds a safeguard against potential crash - adds an open function on NavTab to preserve the order after conditional addition of Tab - replaces "Contributions" to "Edits" in the SuggestedEditsTasksFragment * - fixes CI issue * - adds conditional naming in the EDITS enum itself, removes the CONTRIBUTE enum which removes the potential crash and hidden side effects * - fixes CI issue * - removal of unused strings and some logic --------- Co-authored-by: cooltey * [Contribution dashboard] Create a donor badge view * View update * Use TextView * Add a proper padding to the chip icon for smaller text * Update padding * [Contributions dashboard] donor status on article overflow menu (#5028) * - adds UI for displaying user donor status - adds callback function for donor buttons - adds preference for user visited donor history - * - renames Preference key and variable for clarity * - rename functions for better clarity - creates a single function for navigating to MainActivity with different tabs and extra * - rename preference key * - updates donor information text with string resource * - adds developer preference for updating donor status * - fix CI issues * - replaces the XML with the custom DonorBadgeView and updates the code * - fix ci issues * - removes developer setting option for updating donor status * - fix ci issue * - adds developer setting for changing donor history --------- Co-authored-by: cooltey * Add dialog message for donationResult preference * [Contributions dashboard] survey dialog (#5044) * - adds a survey dialog * - adds a general simple alert dialog * - converts the SimpleAlertDialog function to a class with easy to use extension for AppCompatActivity * - adds contribution survey dialog to SurveyDialog object * - adds survey link cta on alert dialog positive button * - removes dialog from SurveyDialog object and adds it to the SuggestedEditsTasksFragment * - adds contributionDashboardSurveyDialogShown preference * - small code fix * - code fixes * - code fixes --------- Co-authored-by: cooltey * Follow up: survey dialog for Contributions Dashboard * [Contributions Dashboard] Donor History (#5021) * Donor History screen * Update strings * Update strings * Fix error * Some viewModel stuff * Remove old strings * Some logic of initializing the view * Build actions * Finish the layout * Datepicker * Correctly apply theme for datepicker * Show a proper time selection for the date picker * More refine to datePicker * Better selection status * Add proper ripple animation * Handle UTC issues in the date picker * Disable if no date is selected * Use tooltip view for a proper menu item color * Add preference and comment for discussion * Handle orientation properly * Update design * Update preference * Move the preference to the bottom for easier access * qq string * Add link, paddings and ripple animation * Code review comments * Remove preference * Checkbox * Code review comments * Fix datepicker text color * Fix single item text color * Add proper outline background * Wire up the survey dialog * Open link in an external browser * [Contributions Dashboard] Update contribute tab for donor history (#5048) * Initial commit for Contribute tab * Update design * Restore design * TextView for toolbar title * Use flexbox for edge case * Add comment for log out logic and update flexbox layout * Send a LoggedOutEvent to the FlowEventBus for refreshing screen purpose * Get event and refresh * Add minimal height * Slighly change the top margin * Rename layout and update the stats view to the new design * Change to linearLayout * Add donor history related view * Margins * Update/streamline layout of "last donated" row. * Revert "Update/streamline layout of "last donated" row." This reverts commit 5a453af1b1ceb778149922517cf4d4bc3814b4bc. * Update/streamline layout of "last donated" row. (#5062) * Paddings * Simplify the layout a bit * Optimize * Set date and clickable area * Hide sequential tooltips * Refine layout * Update todo * Streamline layout of title and donor button. * Revert "Streamline layout of title and donor button." This reverts commit 5917238ea32fd22759bd016cf4c26729120b7aa4. * Streamline layout of title and donor button. * Streamline layout of title and donor button. (#5066) * Update layout * Add donor status function to a proper location * Remove unused layout parameters * Fix style and wire up donor history * Add donorHistory for overflow menu --------- Co-authored-by: Dmitry Brant Co-authored-by: Dmitry Brant * Update Donor Status logic for recurring donor option * Add warning dialog for modified page * Save only if it is modified * Add preference to dev preference * [Contributions Dashboard] Entry dialogs for the feature (#5067) * [Contributions Dashboard] Entry dialogs for the feature * Update helper * Wire entry dialogs logic * Move dialog * Add paramter * Add temp variable * Survey dialogs and update logic --------- Co-authored-by: Dmitry Brant * Fix: make sure only refresh the content in the current fragment * Fix: design signoff updates for Contribute Tab and Donor history (#5089) * Fix: design signoff updates for Contribute Tab * Adding fixes for donor history * Fix entry dialog logic * Fix: prevent possible crash * Fix: set Donor badge icon to white * Fix: Overflow menu design changes (#5095) * - design fixes: reduces vertical padding for overflow menu and adds vertical padding to "update donor status" button for better accessibility * - reverts back the padding --------- Co-authored-by: cooltey * Add isRecurringDonor key to the dev preferences * Fix: Update Contribute tab for design signoff comments (#5107) * Update contribute tab for design signoff comments * Add isRecurring donor preference to dev preferences * Apply a check to avoid unnecessary loading * Remove unused launcher * Fix: donor status if taps on the already donated; replace double % with the encoded symbol * [Contributions dashboard] app icon (#5055) * - initial work and test work * - adds list preference for setting app icon - update manifest file to include activity alias for default icon - shows snackbar when app icon changes * - fixes both app icon showing on the home screen * - adds a condition for showing donor icon - adds TODO's * - fixes CI issue * - adds Bottom sheet dialog for App icon - adds recyclerView for showing different app icon - removes unused code - adds styles for circular image view - adds logic to select/de-select app icon * - modifies AppIconDialog class to support API 21 for setting foreground and optimized code - adds prefs for saving current selected app icon * - adds logic to show/hide change app icon * - fixes CI issue * - adds donor benefit icon * - replaces png with svg donor benefit launcher icon * - code and ui fixes * - ci fix * - removes displayName from launcherIcon * - adds missing logic for showing "App Icon" setting * - replaces icon and removes unused resource * - code fixes --------- Co-authored-by: cooltey * Fix translation issue * Revert "Fix translation issue" This reverts commit e91fe45b6ff8ef40b60a305e096841271b24586b. * Revert "Fix: donor status if taps on the already donated; replace double % with" This reverts commit fb19f21bebc590ee96816e10a42f3f4d525bf8b3. * Update donor status logic * [Contributions Dashboard] add isDonor preference for the optional donor (#5116) info * Use String.format() for the double percent symbol * - disabled donor badge click when shown on SuggestedEditsTasksFragment (#5119) * Fix: save the page even it is not modified * Fix: Survey dialog design and logic changes (#5115) * - design changes: centered title, icon tint to use secondary color - logic changes: 1. when user updates donor history as a non donor the action on survey dialog opens snackbar 2. user updates donor history as donor then dialog actions opens thank you dialog * - code and style fixes * - centers the alert dialog title globally --------- Co-authored-by: Cooltey Feng * Make container visibility * Fix: make sure the username item only visible for the feature * Show the snackbar for donor history saved * Use correct string resource for app icon * Design review: open AppIconDialog after clicking on "Take me there" * Fix lint * Fix: update click listener for donorHistoryUpdate button * Refine the isDonor preference * Fix: app icon "Donor Benefit" label border (#5121) * - adds border to the "DONOR BENEFIT" design * - renames file and replaces widget_color with border_color * [Contributions Dashboard] Wire up the instrumentation (#5104) * [Contributions Dashboard] Wire up the instrumentation * Fix error * Finalized instrumentation * Add todo * Pending: campaignId * Use contrib for campaignId, remove anon suffix * Remove space * Oops --------- Co-authored-by: William Rai <48931640+Williamrai@users.noreply.github.com> Co-authored-by: Dmitry Brant Co-authored-by: Dmitry Brant Co-authored-by: williamrai --- app/src/main/AndroidManifest.xml | 29 +- .../java/org/wikipedia/LauncherController.kt | 57 +++ .../org/wikipedia/activity/BaseActivity.kt | 8 +- .../ContributionsDashboardEvent.kt | 29 ++ .../eventplatform/DonorExperienceEvent.kt | 2 +- .../dataclient/donate/CampaignCollection.kt | 2 + .../wikipedia/donate/DonorHistoryActivity.kt | 214 +++++++++ .../wikipedia/donate/DonorHistoryViewModel.kt | 40 ++ .../java/org/wikipedia/donate/DonorStatus.kt | 19 + .../java/org/wikipedia/main/MainActivity.kt | 36 +- .../java/org/wikipedia/main/MainFragment.kt | 4 +- .../org/wikipedia/navtab/MenuNavTabDialog.kt | 13 +- .../main/java/org/wikipedia/navtab/NavTab.kt | 6 +- .../java/org/wikipedia/page/PageActivity.kt | 3 +- .../java/org/wikipedia/page/PageFragment.kt | 32 +- .../wikipedia/page/action/PageActionItem.kt | 3 + .../wikipedia/page/campaign/CampaignDialog.kt | 8 +- .../org/wikipedia/settings/AppIconDialog.kt | 147 +++++++ .../main/java/org/wikipedia/settings/Prefs.kt | 25 ++ .../wikipedia/settings/SettingsActivity.kt | 5 +- .../wikipedia/settings/SettingsFragment.kt | 11 +- .../settings/SettingsPreferenceLoader.kt | 19 + .../SuggestedEditsTasksFragment.kt | 254 ++++++++--- .../ContributionsDashboardHelper.kt | 133 ++++++ .../org/wikipedia/views/DonorBadgeView.kt | 69 +++ .../org/wikipedia/views/MessageCardView.kt | 29 +- .../wikipedia/views/PageActionOverflowView.kt | 34 +- ...tionView.kt => SuggestedEditsStatsView.kt} | 31 +- .../ic_baseline_volunteer_activism_24.xml | 9 + app/src/main/res/drawable/ic_feedback.xml | 9 + .../ic_launcher_donor_benefit_foreground.xml | 21 + .../main/res/drawable/outline_repeat_24.xml | 5 + .../outline_volunteer_activism_24.xml | 7 + .../drawable/round_app_registration_24.xml | 19 + .../res/drawable/rounded_6dp_stroke_1dp.xml | 10 + .../res/layout/activity_donor_history.xml | 173 ++++++++ app/src/main/res/layout/activity_main.xml | 47 +- app/src/main/res/layout/dialog_app_icon.xml | 51 +++ .../layout/fragment_suggested_edits_tasks.xml | 412 ++++++++++-------- app/src/main/res/layout/item_app_icon.xml | 15 + app/src/main/res/layout/view_donor_badge.xml | 54 +++ app/src/main/res/layout/view_message_card.xml | 1 + .../res/layout/view_page_action_overflow.xml | 50 +++ ...ion.xml => view_suggested_edits_stats.xml} | 46 +- .../ic_launcher_donor_benefit.xml | 6 + .../ic_launcher_donor_benefit.webp | Bin 0 -> 1078 bytes .../ic_launcher_donor_benefit.webp | Bin 0 -> 714 bytes .../ic_launcher_donor_benefit.webp | Bin 0 -> 1442 bytes .../ic_launcher_donor_benefit.webp | Bin 0 -> 2076 bytes .../ic_launcher_donor_benefit.webp | Bin 0 -> 2900 bytes app/src/main/res/values-sw600dp/dimens.xml | 2 - app/src/main/res/values/dimens.xml | 6 +- app/src/main/res/values/preference_keys.xml | 7 + .../main/res/values/strings_no_translate.xml | 1 + app/src/main/res/values/styles.xml | 55 ++- .../main/res/xml/developer_preferences.xml | 23 +- app/src/main/res/xml/preferences.xml | 10 +- 57 files changed, 1954 insertions(+), 347 deletions(-) create mode 100644 app/src/main/java/org/wikipedia/LauncherController.kt create mode 100644 app/src/main/java/org/wikipedia/analytics/eventplatform/ContributionsDashboardEvent.kt create mode 100644 app/src/main/java/org/wikipedia/donate/DonorHistoryActivity.kt create mode 100644 app/src/main/java/org/wikipedia/donate/DonorHistoryViewModel.kt create mode 100644 app/src/main/java/org/wikipedia/donate/DonorStatus.kt create mode 100644 app/src/main/java/org/wikipedia/settings/AppIconDialog.kt create mode 100644 app/src/main/java/org/wikipedia/usercontrib/ContributionsDashboardHelper.kt create mode 100644 app/src/main/java/org/wikipedia/views/DonorBadgeView.kt rename app/src/main/java/org/wikipedia/views/{ImageTitleDescriptionView.kt => SuggestedEditsStatsView.kt} (67%) create mode 100644 app/src/main/res/drawable/ic_baseline_volunteer_activism_24.xml create mode 100644 app/src/main/res/drawable/ic_feedback.xml create mode 100644 app/src/main/res/drawable/ic_launcher_donor_benefit_foreground.xml create mode 100644 app/src/main/res/drawable/outline_repeat_24.xml create mode 100644 app/src/main/res/drawable/outline_volunteer_activism_24.xml create mode 100644 app/src/main/res/drawable/round_app_registration_24.xml create mode 100644 app/src/main/res/drawable/rounded_6dp_stroke_1dp.xml create mode 100644 app/src/main/res/layout/activity_donor_history.xml create mode 100644 app/src/main/res/layout/dialog_app_icon.xml create mode 100644 app/src/main/res/layout/item_app_icon.xml create mode 100644 app/src/main/res/layout/view_donor_badge.xml rename app/src/main/res/layout/{view_image_title_description.xml => view_suggested_edits_stats.xml} (59%) create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_donor_benefit.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_donor_benefit.webp create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_donor_benefit.webp create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_donor_benefit.webp create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_donor_benefit.webp create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_donor_benefit.webp diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c50b855e8be..2cc3a1e1eea 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -65,7 +65,6 @@ android:fullBackupContent="@xml/full_backup_rules" android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true" - android:icon="@mipmap/launcher" android:label="@string/app_name" android:name=".WikipediaApp" android:theme="@style/AppTheme"> @@ -90,6 +89,11 @@ android:name=".main.MainActivity" android:windowSoftInputMode="adjustResize" android:theme="@style/AppTheme.Splash" + android:exported="true"/> + @@ -102,7 +106,25 @@ - + + + + + + + + + + + + + + + + + packageManager.setComponentEnabledSetting( + launcherIcon.getComponentName(context), + if (launcherIcon == icon) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } +} + +enum class LauncherIcon( + val key: String, + val background: Int, + val foreground: Int, + val label: Int, + var isSelected: Boolean = false +) { + DEFAULT( + key = "DefaultIcon", + background = R.drawable.launcher_background, + foreground = R.drawable.launcher_foreground, + label = R.string.app_name + ), + DONOR( + key = "DonorIcon", + background = R.drawable.launcher_background, + foreground = R.drawable.ic_launcher_donor_benefit_foreground, + label = R.string.app_name + ); + + fun getComponentName(context: Context): ComponentName { + return ComponentName(context.packageName, "org.wikipedia.$key") + } + + companion object { + fun initialValues(): List { + val savedAppIcon = Prefs.currentSelectedAppIcon ?: DEFAULT.key + entries.forEach { icon -> + icon.isSelected = icon.key == savedAppIcon + } + return entries + } + } +} diff --git a/app/src/main/java/org/wikipedia/activity/BaseActivity.kt b/app/src/main/java/org/wikipedia/activity/BaseActivity.kt index 2f1d39e3cde..f85a6db3fb6 100644 --- a/app/src/main/java/org/wikipedia/activity/BaseActivity.kt +++ b/app/src/main/java/org/wikipedia/activity/BaseActivity.kt @@ -48,6 +48,7 @@ import org.wikipedia.recurring.RecurringTasksExecutor import org.wikipedia.richtext.CustomHtmlParser import org.wikipedia.settings.Prefs import org.wikipedia.theme.Theme +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.DeviceUtil import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.ResourceUtil @@ -68,7 +69,12 @@ abstract class BaseActivity : AppCompatActivity(), ConnectionStateMonitor.Callba private val requestDonateActivity = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { if (it.resultCode == RESULT_OK) { ExclusiveBottomSheetPresenter.dismiss(supportFragmentManager) - FeedbackUtil.showMessage(this, R.string.donate_gpay_success_message) + if (!Prefs.contributionsDashboardEntryDialogShown && ContributionsDashboardHelper.contributionsDashboardEnabled) { + ContributionsDashboardHelper.showDonationCompletedDialog(this) + Prefs.contributionsDashboardEntryDialogShown = true + } else { + FeedbackUtil.showMessage(this, R.string.donate_gpay_success_message) + } } } diff --git a/app/src/main/java/org/wikipedia/analytics/eventplatform/ContributionsDashboardEvent.kt b/app/src/main/java/org/wikipedia/analytics/eventplatform/ContributionsDashboardEvent.kt new file mode 100644 index 00000000000..4199963de83 --- /dev/null +++ b/app/src/main/java/org/wikipedia/analytics/eventplatform/ContributionsDashboardEvent.kt @@ -0,0 +1,29 @@ +package org.wikipedia.analytics.eventplatform + +import org.wikipedia.WikipediaApp +import org.wikipedia.dataclient.donate.CampaignCollection +import org.wikipedia.settings.Prefs +import org.wikipedia.usercontrib.ContributionsDashboardHelper + +class ContributionsDashboardEvent : DonorExperienceEvent() { + + companion object { + + fun logAction( + action: String, + activeInterface: String, + wikiId: String = WikipediaApp.instance.appOrSystemLanguageCode, + campaignId: String? = null + ) { + if (ContributionsDashboardHelper.contributionsDashboardEnabled) { + submit( + action, + activeInterface, + campaignId?.let { "campaign_id: ${CampaignCollection.getFormattedCampaignId(it)}, " } + .orEmpty() + "donor_detected: ${Prefs.isDonor}", + wikiId + ) + } + } + } +} diff --git a/app/src/main/java/org/wikipedia/analytics/eventplatform/DonorExperienceEvent.kt b/app/src/main/java/org/wikipedia/analytics/eventplatform/DonorExperienceEvent.kt index 27700a2dcb4..3237fd57669 100644 --- a/app/src/main/java/org/wikipedia/analytics/eventplatform/DonorExperienceEvent.kt +++ b/app/src/main/java/org/wikipedia/analytics/eventplatform/DonorExperienceEvent.kt @@ -4,7 +4,7 @@ import org.wikipedia.WikipediaApp import org.wikipedia.dataclient.donate.CampaignCollection import org.wikipedia.settings.Prefs -class DonorExperienceEvent { +open class DonorExperienceEvent { companion object { diff --git a/app/src/main/java/org/wikipedia/dataclient/donate/CampaignCollection.kt b/app/src/main/java/org/wikipedia/dataclient/donate/CampaignCollection.kt index 70b5b2fab76..66420ac8924 100644 --- a/app/src/main/java/org/wikipedia/dataclient/donate/CampaignCollection.kt +++ b/app/src/main/java/org/wikipedia/dataclient/donate/CampaignCollection.kt @@ -50,6 +50,8 @@ object CampaignCollection { fun addDonationResult(fromWeb: Boolean = false) { Prefs.donationResults = Prefs.donationResults.plus(DonationResult(dateTime = LocalDateTime.now().toString(), fromWeb = fromWeb)) + Prefs.isDonor = true + Prefs.hasDonorHistorySaved = true } @Serializable diff --git a/app/src/main/java/org/wikipedia/donate/DonorHistoryActivity.kt b/app/src/main/java/org/wikipedia/donate/DonorHistoryActivity.kt new file mode 100644 index 00000000000..e22052b5c8e --- /dev/null +++ b/app/src/main/java/org/wikipedia/donate/DonorHistoryActivity.kt @@ -0,0 +1,214 @@ +package org.wikipedia.donate + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.format.DateUtils +import androidx.activity.viewModels +import androidx.core.view.isVisible +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointBackward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.wikipedia.Constants +import org.wikipedia.R +import org.wikipedia.activity.BaseActivity +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent +import org.wikipedia.databinding.ActivityDonorHistoryBinding +import org.wikipedia.main.MainActivity +import org.wikipedia.settings.Prefs +import org.wikipedia.usercontrib.ContributionsDashboardHelper +import org.wikipedia.util.ResourceUtil +import org.wikipedia.util.UriUtil +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.time.ZonedDateTime + +class DonorHistoryActivity : BaseActivity() { + + private lateinit var binding: ActivityDonorHistoryBinding + private val viewModel: DonorHistoryViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityDonorHistoryBinding.inflate(layoutInflater) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + supportActionBar?.title = getString(R.string.donor_history_title) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + init() + } + + override fun onBackPressed() { + if (viewModel.donorHistoryModified) { + MaterialAlertDialogBuilder(this) + .setMessage(getString(R.string.edit_abandon_confirm)) + .setPositiveButton(getString(R.string.edit_abandon_confirm_yes)) { dialog, _ -> + ContributionsDashboardEvent.logAction("cancel_click", "contrib_update") + dialog.dismiss() + if (viewModel.shouldGoBackToContributeTab) { + startActivity(MainActivity.newIntent(this).putExtra(Constants.INTENT_EXTRA_GO_TO_SE_TAB, true)) + } else { + finish() + } + } + .setNegativeButton(getString(R.string.edit_abandon_confirm_no)) { dialog, _ -> + ContributionsDashboardEvent.logAction("restart_click", "contrib_update") + dialog.dismiss() + } + .show() + return + } + super.onBackPressed() + } + + private fun init() { + + binding.donationInfoContainer.isVisible = viewModel.isDonor + + binding.donorStatus.setOnClickListener { + ContributionsDashboardEvent.logAction("update_click", "contrib_update") + showDonorStatusDialog() + } + + binding.lastDonationContainer.setOnClickListener { + showLastDonatedDatePicker() + } + + binding.recurringDonorCheckbox.isChecked = viewModel.isRecurringDonor + binding.recurringDonorCheckbox.setOnClickListener { + viewModel.donorHistoryModified = true + viewModel.isRecurringDonor = binding.recurringDonorCheckbox.isChecked + binding.recurringDonorCheckbox.isChecked = viewModel.isRecurringDonor + } + binding.recurringDonorContainer.setOnClickListener { + binding.recurringDonorCheckbox.performClick() + } + + binding.donateButton.setOnClickListener { + ContributionsDashboardEvent.logAction("donate_start_click", "contrib_update", campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + launchDonateDialog(campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + } + + binding.experimentLink.setOnClickListener { + ContributionsDashboardEvent.logAction("about_click", "contrib_update") + UriUtil.visitInExternalBrowser(this, Uri.parse(getString(R.string.contributions_dashboard_wiki_url))) + } + + binding.saveButton.setOnClickListener { + ContributionsDashboardEvent.logAction("save_click", "contrib_update") + viewModel.saveDonorHistory() + if (viewModel.shouldGoBackToContributeTab) { + startActivity( + MainActivity.newIntent(this) + .putExtra(Constants.INTENT_EXTRA_GO_TO_SE_TAB, true) + ) + return@setOnClickListener + } + finish() + } + updateDonorStatusText() + updateLastDonatedText() + } + + private fun updateDonorStatusText() { + var donorStatusTextColor = R.attr.primary_color + val donorStatusText = if (!Prefs.hasDonorHistorySaved && viewModel.currentDonorStatus == -1) { + donorStatusTextColor = R.attr.placeholder_color + R.string.donor_history_update_donor_status_default + } else if (viewModel.isDonor) { + viewModel.currentDonorStatus = 0 + R.string.donor_history_update_donor_status_donor + } else { + viewModel.currentDonorStatus = 1 + R.string.donor_history_update_donor_status_not_a_donor + } + binding.donorStatus.text = getString(donorStatusText) + binding.donorStatus.setTextColor(ResourceUtil.getThemedColorStateList(this, donorStatusTextColor)) + binding.donateButton.isVisible = viewModel.currentDonorStatus == 1 // Not a donor + binding.donationInfoContainer.isVisible = viewModel.isDonor + } + + private fun updateLastDonatedText() { + binding.lastDonationDate.isVisible = viewModel.lastDonated != null + var lastDonatedTextColor = R.attr.primary_color + val lastDonatedText = if (viewModel.lastDonated == null) { + lastDonatedTextColor = R.attr.placeholder_color + R.string.donor_history_last_donated_hint + } else { + R.string.donor_history_last_donated + } + binding.lastDonationLabel.text = getString(lastDonatedText) + binding.lastDonationLabel.setTextColor(ResourceUtil.getThemedColorStateList(this, lastDonatedTextColor)) + viewModel.lastDonated?.let { + binding.lastDonationDate.text = DateUtils.getRelativeTimeSpanString( + viewModel.dateTimeToMilli(it), + System.currentTimeMillis(), + DateUtils.DAY_IN_MILLIS + ) + } + } + + private fun showDonorStatusDialog() { + val donorStatusList = arrayOf( + getString(R.string.donor_history_update_donor_status_donor), + getString(R.string.donor_history_update_donor_status_not_a_donor) + ) + MaterialAlertDialogBuilder(this) + .setSingleChoiceItems(donorStatusList, viewModel.currentDonorStatus) { dialog, which -> + viewModel.isDonor = which == 0 + viewModel.currentDonorStatus = which + viewModel.donorHistoryModified = true + updateDonorStatusText() + updateLastDonatedText() + dialog.dismiss() + } + .show() + } + + private fun showLastDonatedDatePicker() { + // The CalendarConstraints handles date in UTC + val utcMillis = LocalDateTime.now().toInstant(ZoneOffset.UTC).toEpochMilli() + val defaultDatePickerMilli = viewModel.lastDonated?.let { + viewModel.dateTimeToMilli(it) + } ?: run { + utcMillis + } + + val calendarConstraints = CalendarConstraints.Builder() + .setEnd(utcMillis) + .setValidator(DateValidatorPointBackward.before(utcMillis)) + .build() + + MaterialDatePicker.Builder.datePicker() + .setTheme(R.style.MaterialDatePickerStyle) + .setSelection(defaultDatePickerMilli) + .setInputMode(MaterialDatePicker.INPUT_MODE_TEXT) + .setCalendarConstraints(calendarConstraints) + .build() + .apply { + addOnPositiveButtonClickListener { + // The date picker returns milliseconds in UTC timezone. + val utcDate = LocalDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC) + viewModel.lastDonated = ZonedDateTime.of(utcDate, ZoneId.systemDefault()).toLocalDateTime().toString() + viewModel.donorHistoryModified = true + updateLastDonatedText() + } + } + .show(supportFragmentManager, "datePicker") + } + + companion object { + + const val RESULT_GO_BACK_TO_CONTRIBUTE_TAB = "goBackToContributeTab" + + fun newIntent(context: Context, completedDonation: Boolean = false, goBackToContributeTab: Boolean = false): Intent { + return Intent(context, DonorHistoryActivity::class.java) + .putExtra(Constants.ARG_BOOLEAN, completedDonation) + .putExtra(RESULT_GO_BACK_TO_CONTRIBUTE_TAB, goBackToContributeTab) + } + } +} diff --git a/app/src/main/java/org/wikipedia/donate/DonorHistoryViewModel.kt b/app/src/main/java/org/wikipedia/donate/DonorHistoryViewModel.kt new file mode 100644 index 00000000000..83646e6cf5c --- /dev/null +++ b/app/src/main/java/org/wikipedia/donate/DonorHistoryViewModel.kt @@ -0,0 +1,40 @@ +package org.wikipedia.donate + +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.ViewModel +import org.wikipedia.Constants +import org.wikipedia.settings.Prefs +import org.wikipedia.usercontrib.ContributionsDashboardHelper +import java.time.LocalDateTime +import java.time.ZoneId + +class DonorHistoryViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { + + val completedDonation = savedStateHandle.get(Constants.ARG_BOOLEAN) == true + val shouldGoBackToContributeTab = savedStateHandle.get(DonorHistoryActivity.RESULT_GO_BACK_TO_CONTRIBUTE_TAB) == true + var currentDonorStatus = if (completedDonation) 0 else -1 + var isDonor = completedDonation || Prefs.hasDonorHistorySaved && Prefs.isDonor + var lastDonated = Prefs.donationResults.lastOrNull()?.dateTime + var isRecurringDonor = Prefs.isRecurringDonor + var donorHistoryModified = false + + fun saveDonorHistory() { + Prefs.hasDonorHistorySaved = true + ContributionsDashboardHelper.showSurveyDialogUI = true + if (isDonor) { + Prefs.isRecurringDonor = isRecurringDonor + lastDonated?.let { + Prefs.donationResults = Prefs.donationResults.plus(DonationResult(it, false)).distinct() + } + } else { + Prefs.isRecurringDonor = false + Prefs.donationResults = emptyList() + } + Prefs.isDonor = isDonor + donorHistoryModified = false + } + + fun dateTimeToMilli(dateTime: String): Long { + return LocalDateTime.parse(dateTime).atZone(ZoneId.systemDefault()).toInstant().toEpochMilli() + } +} diff --git a/app/src/main/java/org/wikipedia/donate/DonorStatus.kt b/app/src/main/java/org/wikipedia/donate/DonorStatus.kt new file mode 100644 index 00000000000..b230e23d744 --- /dev/null +++ b/app/src/main/java/org/wikipedia/donate/DonorStatus.kt @@ -0,0 +1,19 @@ +package org.wikipedia.donate + +import org.wikipedia.settings.Prefs + +enum class DonorStatus { + DONOR, NON_DONOR, UNKNOWN; + + companion object { + fun donorStatus(): DonorStatus { + return if (Prefs.hasDonorHistorySaved.not()) { + UNKNOWN + } else if (Prefs.isDonor) { + DONOR + } else { + NON_DONOR + } + } + } +} diff --git a/app/src/main/java/org/wikipedia/main/MainActivity.kt b/app/src/main/java/org/wikipedia/main/MainActivity.kt index c965e7950e9..060e4661338 100644 --- a/app/src/main/java/org/wikipedia/main/MainActivity.kt +++ b/app/src/main/java/org/wikipedia/main/MainActivity.kt @@ -8,22 +8,28 @@ import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.Toolbar +import androidx.core.view.isVisible import androidx.fragment.app.Fragment import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.activity.SingleFragmentActivity +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent import org.wikipedia.analytics.eventplatform.ImageRecommendationsEvent import org.wikipedia.analytics.eventplatform.PatrollerExperienceEvent +import org.wikipedia.auth.AccountUtil import org.wikipedia.databinding.ActivityMainBinding import org.wikipedia.dataclient.WikiSite +import org.wikipedia.donate.DonorStatus import org.wikipedia.feed.FeedFragment import org.wikipedia.navtab.NavTab import org.wikipedia.onboarding.InitialOnboardingActivity import org.wikipedia.page.PageActivity import org.wikipedia.settings.Prefs +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.ResourceUtil +import org.wikipedia.views.DonorBadgeView class MainActivity : SingleFragmentActivity(), MainFragment.Callback { @@ -71,21 +77,41 @@ class MainActivity : SingleFragmentActivity(), MainFragment.Callba } override fun onTabChanged(tab: NavTab) { - if (tab == NavTab.EDITS) { - ImageRecommendationsEvent.logImpression("suggested_edit_dialog") - PatrollerExperienceEvent.logImpression("suggested_edits_dialog") - } if (tab == NavTab.EXPLORE) { binding.mainToolbarWordmark.visibility = View.VISIBLE binding.mainToolbar.title = "" + binding.toolbarTitle.isVisible = false + binding.donorBadge.isVisible = false controlNavTabInFragment = false } else { + binding.toolbarTitle.isVisible = true + binding.donorBadge.isVisible = false if (tab == NavTab.SEARCH && Prefs.showSearchTabTooltip) { FeedbackUtil.showTooltip(this, fragment.binding.mainNavTabLayout.findViewById(NavTab.SEARCH.id), getString(R.string.search_tab_tooltip), aboveOrBelow = true, autoDismiss = false) Prefs.showSearchTabTooltip = false } + var titleText = getString(tab.text) + if (tab == NavTab.EDITS) { + ImageRecommendationsEvent.logImpression("suggested_edit_dialog") + PatrollerExperienceEvent.logImpression("suggested_edits_dialog") + if (ContributionsDashboardHelper.contributionsDashboardEnabled) { + titleText = if (AccountUtil.isLoggedIn) { + AccountUtil.userName + } else { + getString(R.string.contributions_dashboard_logged_out_user) + } + binding.donorBadge.disableClickForDonor() + binding.donorBadge.setup(object : DonorBadgeView.Callback { + override fun onBecomeDonorClick() { + ContributionsDashboardEvent.logAction("donate_start_click", "contrib_dashboard", campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + launchDonateDialog(campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + } + }) + binding.donorBadge.isVisible = DonorStatus.donorStatus() != DonorStatus.UNKNOWN + } + } binding.mainToolbarWordmark.visibility = View.GONE - binding.mainToolbar.setTitle(tab.text) + binding.toolbarTitle.text = titleText controlNavTabInFragment = true } fragment.requestUpdateToolbarElevation() diff --git a/app/src/main/java/org/wikipedia/main/MainFragment.kt b/app/src/main/java/org/wikipedia/main/MainFragment.kt index cb8e501c88d..aded43e3f71 100644 --- a/app/src/main/java/org/wikipedia/main/MainFragment.kt +++ b/app/src/main/java/org/wikipedia/main/MainFragment.kt @@ -467,8 +467,8 @@ class MainFragment : Fragment(), BackPressedHandler, MenuProvider, FeedFragment. } } - override fun donateClick() { - (requireActivity() as? BaseActivity)?.launchDonateDialog() + override fun donateClick(campaignId: String?) { + (requireActivity() as? BaseActivity)?.launchDonateDialog(campaignId = campaignId) } fun setBottomNavVisible(visible: Boolean) { diff --git a/app/src/main/java/org/wikipedia/navtab/MenuNavTabDialog.kt b/app/src/main/java/org/wikipedia/navtab/MenuNavTabDialog.kt index becd99c6176..868c11df305 100644 --- a/app/src/main/java/org/wikipedia/navtab/MenuNavTabDialog.kt +++ b/app/src/main/java/org/wikipedia/navtab/MenuNavTabDialog.kt @@ -10,12 +10,14 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import org.wikipedia.R import org.wikipedia.activity.FragmentUtil import org.wikipedia.analytics.eventplatform.BreadCrumbLogEvent +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent import org.wikipedia.analytics.eventplatform.DonorExperienceEvent import org.wikipedia.analytics.eventplatform.PlacesEvent import org.wikipedia.auth.AccountUtil import org.wikipedia.databinding.ViewMainDrawerBinding import org.wikipedia.page.ExtendedBottomSheetDialogFragment import org.wikipedia.places.PlacesActivity +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.DimenUtil import org.wikipedia.util.ResourceUtil.getThemedColorStateList @@ -27,7 +29,7 @@ class MenuNavTabDialog : ExtendedBottomSheetDialogFragment() { fun settingsClick() fun watchlistClick() fun contribsClick() - fun donateClick() + fun donateClick(campaignId: String? = null) } private var _binding: ViewMainDrawerBinding? = null @@ -77,9 +79,14 @@ class MenuNavTabDialog : ExtendedBottomSheetDialogFragment() { } binding.mainDrawerDonateContainer.setOnClickListener { - DonorExperienceEvent.logAction("donate_start_click", "more_menu") BreadCrumbLogEvent.logClick(requireActivity(), binding.mainDrawerDonateContainer) - callback()?.donateClick() + if (ContributionsDashboardHelper.contributionsDashboardEnabled) { + ContributionsDashboardEvent.logAction("donate_start_click", "more_menu", campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + callback()?.donateClick(campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + } else { + DonorExperienceEvent.logAction("donate_start_click", "more_menu") + callback()?.donateClick() + } dismiss() } diff --git a/app/src/main/java/org/wikipedia/navtab/NavTab.kt b/app/src/main/java/org/wikipedia/navtab/NavTab.kt index cc991fba96e..cab7358185f 100644 --- a/app/src/main/java/org/wikipedia/navtab/NavTab.kt +++ b/app/src/main/java/org/wikipedia/navtab/NavTab.kt @@ -9,6 +9,7 @@ import org.wikipedia.history.HistoryFragment import org.wikipedia.model.EnumCode import org.wikipedia.readinglist.ReadingListsFragment import org.wikipedia.suggestededits.SuggestedEditsTasksFragment +import org.wikipedia.usercontrib.ContributionsDashboardHelper enum class NavTab constructor( @StringRes val text: Int, @@ -31,7 +32,10 @@ enum class NavTab constructor( return HistoryFragment.newInstance() } }, - EDITS(R.string.nav_item_suggested_edits, R.id.nav_tab_edits, R.drawable.selector_nav_edits) { + EDITS( + if (ContributionsDashboardHelper.contributionsDashboardEnabled) R.string.nav_item_contribute + else R.string.nav_item_suggested_edits, R.id.nav_tab_edits, R.drawable.selector_nav_edits + ) { override fun newInstance(): Fragment { return SuggestedEditsTasksFragment.newInstance() } diff --git a/app/src/main/java/org/wikipedia/page/PageActivity.kt b/app/src/main/java/org/wikipedia/page/PageActivity.kt index 4c78e3a8cea..7334dc08788 100644 --- a/app/src/main/java/org/wikipedia/page/PageActivity.kt +++ b/app/src/main/java/org/wikipedia/page/PageActivity.kt @@ -59,6 +59,7 @@ import org.wikipedia.extensions.parcelableExtra import org.wikipedia.gallery.GalleryActivity import org.wikipedia.history.HistoryEntry import org.wikipedia.language.LangLinksActivity +import org.wikipedia.navtab.NavTab import org.wikipedia.notifications.AnonymousNotificationHelper import org.wikipedia.notifications.NotificationActivity import org.wikipedia.page.linkpreview.LinkPreviewDialog @@ -304,7 +305,7 @@ class PageActivity : BaseActivity(), PageFragment.Callback, LinkPreviewDialog.Lo if (app.haveMainActivity) { onBackPressed() } else { - pageFragment.goToMainTab() + pageFragment.goToMainActivity(tab = NavTab.EXPLORE, tabExtra = Constants.INTENT_EXTRA_GO_TO_MAIN_TAB) } true } else -> super.onOptionsItemSelected(item) diff --git a/app/src/main/java/org/wikipedia/page/PageFragment.kt b/app/src/main/java/org/wikipedia/page/PageFragment.kt index 40fbbf932b8..ca7ff27ddeb 100644 --- a/app/src/main/java/org/wikipedia/page/PageFragment.kt +++ b/app/src/main/java/org/wikipedia/page/PageFragment.kt @@ -46,6 +46,7 @@ import org.wikipedia.Constants.InvokeSource import org.wikipedia.LongPressHandler import org.wikipedia.R import org.wikipedia.WikipediaApp +import org.wikipedia.activity.BaseActivity import org.wikipedia.activity.FragmentUtil.getCallback import org.wikipedia.analytics.eventplatform.ArticleFindInPageInteractionEvent import org.wikipedia.analytics.eventplatform.ArticleInteractionEvent @@ -72,6 +73,7 @@ import org.wikipedia.dataclient.okhttp.HttpStatusException import org.wikipedia.dataclient.okhttp.OkHttpWebViewClient import org.wikipedia.descriptions.DescriptionEditActivity import org.wikipedia.diff.ArticleEditDetailsActivity +import org.wikipedia.donate.DonorHistoryActivity import org.wikipedia.edit.EditHandler import org.wikipedia.gallery.GalleryActivity import org.wikipedia.history.HistoryEntry @@ -98,6 +100,7 @@ import org.wikipedia.settings.Prefs import org.wikipedia.suggestededits.PageSummaryForEdit import org.wikipedia.talk.TalkTopicsActivity import org.wikipedia.theme.ThemeChooserDialog +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.ActiveTimer import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil @@ -683,13 +686,22 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi val dialog = CampaignDialog(requireActivity(), it) dialog.setCancelable(false) dialog.show() + return@launch } } + maybeShowContributionsDashboardDialog() } } } } + private fun maybeShowContributionsDashboardDialog() { + if (!Prefs.contributionsDashboardEntryDialogShown && ContributionsDashboardHelper.contributionsDashboardEnabled) { + ContributionsDashboardHelper.showEntryDialog(requireActivity()) + Prefs.contributionsDashboardEntryDialogShown = true + } + } + private fun showFindReferenceInPage(referenceAnchor: String, backLinksList: List, referenceText: String) { @@ -1263,11 +1275,11 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi PageActionOverflowView(requireContext()).show(anchor, pageActionItemCallback, currentTab, model) } - fun goToMainTab() { - startActivity(MainActivity.newIntent(requireContext()) + fun goToMainActivity(tab: NavTab, tabExtra: String) { + startActivity(MainActivity.newIntent(requireActivity()) .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) .putExtra(Constants.INTENT_RETURN_TO_MAIN, true) - .putExtra(Constants.INTENT_EXTRA_GO_TO_MAIN_TAB, NavTab.EXPLORE.code())) + .putExtra(tabExtra, tab.code())) requireActivity().finish() } @@ -1457,7 +1469,7 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi } override fun onExploreSelected() { - goToMainTab() + goToMainActivity(tab = NavTab.EXPLORE, tabExtra = Constants.INTENT_EXTRA_GO_TO_MAIN_TAB) articleInteractionEvent?.logExploreClick() metricsPlatformArticleEventToolbarInteraction.logExploreClick() } @@ -1493,6 +1505,18 @@ class PageFragment : Fragment(), BackPressedHandler, CommunicationBridge.Communi articleInteractionEvent?.logForwardClick() metricsPlatformArticleEventToolbarInteraction.logForwardClick() } + + override fun onDonorSelected() { + goToMainActivity(tab = NavTab.EDITS, tabExtra = Constants.INTENT_EXTRA_GO_TO_SE_TAB) + } + + override fun onBecomeDonorSelected() { + (requireActivity() as? BaseActivity)?.launchDonateDialog(campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + } + + override fun onUpdateDonorStatusSelected() { + startActivity(DonorHistoryActivity.newIntent(requireContext(), goBackToContributeTab = true)) + } } companion object { diff --git a/app/src/main/java/org/wikipedia/page/action/PageActionItem.kt b/app/src/main/java/org/wikipedia/page/action/PageActionItem.kt index 7346d006011..c527193c2d2 100644 --- a/app/src/main/java/org/wikipedia/page/action/PageActionItem.kt +++ b/app/src/main/java/org/wikipedia/page/action/PageActionItem.kt @@ -108,6 +108,9 @@ enum class PageActionItem constructor(val id: Int, fun onEditArticleSelected() fun onViewOnMapSelected() fun forwardClick() + fun onDonorSelected() + fun onBecomeDonorSelected() + fun onUpdateDonorStatusSelected() } companion object { diff --git a/app/src/main/java/org/wikipedia/page/campaign/CampaignDialog.kt b/app/src/main/java/org/wikipedia/page/campaign/CampaignDialog.kt index accfa5b64a8..0061f8e163c 100644 --- a/app/src/main/java/org/wikipedia/page/campaign/CampaignDialog.kt +++ b/app/src/main/java/org/wikipedia/page/campaign/CampaignDialog.kt @@ -9,6 +9,7 @@ import org.wikipedia.activity.BaseActivity import org.wikipedia.analytics.eventplatform.DonorExperienceEvent import org.wikipedia.dataclient.donate.Campaign import org.wikipedia.settings.Prefs +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.CustomTabsUtil import org.wikipedia.util.FeedbackUtil import java.time.Duration @@ -56,7 +57,12 @@ class CampaignDialog internal constructor(private val context: Context, val camp override fun onNegativeAction() { DonorExperienceEvent.logAction("already_donated_click", "article_banner", campaignId = campaign.id) - FeedbackUtil.showMessage(context as Activity, R.string.donation_campaign_donated_snackbar) + if (!Prefs.contributionsDashboardEntryDialogShown && ContributionsDashboardHelper.contributionsDashboardEnabled) { + ContributionsDashboardHelper.showDonationCompletedDialog(context) + Prefs.contributionsDashboardEntryDialogShown = true + } else { + FeedbackUtil.showMessage(context as Activity, R.string.donation_campaign_donated_snackbar) + } dismissDialog() } diff --git a/app/src/main/java/org/wikipedia/settings/AppIconDialog.kt b/app/src/main/java/org/wikipedia/settings/AppIconDialog.kt new file mode 100644 index 00000000000..b2b35b64838 --- /dev/null +++ b/app/src/main/java/org/wikipedia/settings/AppIconDialog.kt @@ -0,0 +1,147 @@ +package org.wikipedia.settings + +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import com.google.android.flexbox.AlignItems +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent +import org.wikipedia.LauncherController +import org.wikipedia.LauncherIcon +import org.wikipedia.R +import org.wikipedia.WikipediaApp +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent +import org.wikipedia.appshortcuts.AppShortcuts +import org.wikipedia.databinding.DialogAppIconBinding +import org.wikipedia.databinding.ItemAppIconBinding +import org.wikipedia.page.ExtendedBottomSheetDialogFragment +import org.wikipedia.util.DimenUtil +import org.wikipedia.util.FeedbackUtil +import org.wikipedia.util.ResourceUtil + +class AppIconDialog : ExtendedBottomSheetDialogFragment() { + private var _binding: DialogAppIconBinding? = null + private val binding get() = _binding!! + + private val appIconAdapter: AppIconAdapter by lazy { + AppIconAdapter().apply { + onItemClickListener { selectedIcon -> + if (selectedIcon == LauncherIcon.DEFAULT) { + ContributionsDashboardEvent.logAction("default_icon_select", "contrib_icon_set") + } else { + ContributionsDashboardEvent.logAction("contrib_icon_select", "contrib_icon_set") + } + Prefs.currentSelectedAppIcon = selectedIcon.key + LauncherController.setIcon(selectedIcon) + AppShortcuts.setShortcuts(requireContext()) + updateIcons(selectedIcon) + dismiss() + FeedbackUtil.makeSnackbar(requireActivity(), + WikipediaApp.instance.getString(R.string.contributions_dashboard_app_icon_updated)).show() + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogAppIconBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + ContributionsDashboardEvent.logAction("impression", "contrib_icon_set") + setupRecyclerView() + } + + private fun setupRecyclerView() { + val layoutManager = FlexboxLayoutManager(requireContext()).apply { + flexDirection = FlexDirection.ROW + justifyContent = JustifyContent.CENTER + alignItems = AlignItems.CENTER + } + binding.appIconRecyclerView.apply { + this.layoutManager = layoutManager + adapter = appIconAdapter + } + appIconAdapter.updateItems(LauncherIcon.initialValues()) + } + + private fun updateIcons(selectedIcon: LauncherIcon) { + val currentSelectedIcon = if (Prefs.currentSelectedAppIcon != null) Prefs.currentSelectedAppIcon + else selectedIcon.key + + LauncherIcon.entries.forEach { + it.isSelected = it.key == currentSelectedIcon + } + appIconAdapter.updateItems(LauncherIcon.entries) + } + + private class AppIconAdapter : RecyclerView.Adapter() { + private var list = mutableListOf() + private var onItemClickListener: ((LauncherIcon) -> Unit)? = null + + fun onItemClickListener(onItemClickListener: (LauncherIcon) -> Unit) { + this.onItemClickListener = onItemClickListener + } + + fun updateItems(newList: List) { + list.clear() + list.addAll(newList) + notifyItemRangeChanged(0, list.size) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppIconViewHolder { + val view = ItemAppIconBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return AppIconViewHolder(view) + } + + override fun getItemCount(): Int = list.size + + override fun onBindViewHolder(holder: AppIconViewHolder, position: Int) { + val item = list[position] + holder.bind(item) + } + + private inner class AppIconViewHolder(val binding: ItemAppIconBinding) : RecyclerView.ViewHolder(binding.root) { + fun bind(item: LauncherIcon) { + binding.appIcon.apply { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + foreground = ContextCompat.getDrawable(binding.root.context, item.foreground) + } else { + setImageDrawable(ContextCompat.getDrawable(binding.root.context, item.foreground)) + } + background = ContextCompat.getDrawable(binding.root.context, item.background) + setOnClickListener { + onItemClickListener?.invoke(item) + } + val strokeColor = if (item.isSelected) { + R.attr.progressive_color + } else R.attr.border_color + val newStrokeWidth = if (item.isSelected) 2f else 1f + this.strokeColor = ResourceUtil.getThemedColorStateList(context, strokeColor) + this.strokeWidth = DimenUtil.dpToPx(newStrokeWidth) + } + } + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + companion object { + fun newInstance(): AppIconDialog { + return AppIconDialog() + } + } +} diff --git a/app/src/main/java/org/wikipedia/settings/Prefs.kt b/app/src/main/java/org/wikipedia/settings/Prefs.kt index c143fd46416..288fa765ce8 100644 --- a/app/src/main/java/org/wikipedia/settings/Prefs.kt +++ b/app/src/main/java/org/wikipedia/settings/Prefs.kt @@ -5,6 +5,7 @@ import okhttp3.Cookie import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.logging.HttpLoggingInterceptor import org.wikipedia.BuildConfig +import org.wikipedia.LauncherIcon import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.analytics.SessionData @@ -738,4 +739,28 @@ object Prefs { var donationResults get() = JsonUtil.decodeFromString>(PrefsIoUtil.getString(R.string.preference_key_donation_results, null)).orEmpty() set(value) = PrefsIoUtil.setString(R.string.preference_key_donation_results, JsonUtil.encodeToString(value)) + + var hasDonorHistorySaved + get() = PrefsIoUtil.getBoolean(R.string.preference_key_donor_history_saved, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_donor_history_saved, value) + + var isDonor + get() = PrefsIoUtil.getBoolean(R.string.preference_key_is_donor, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_is_donor, value) + + var isRecurringDonor + get() = PrefsIoUtil.getBoolean(R.string.preference_key_is_recurring_donor, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_is_recurring_donor, value) + + var contributionsDashboardSurveyDialogShown + get() = PrefsIoUtil.getBoolean(R.string.preference_key_contributions_dashboard_survey_dialog_shown, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_contributions_dashboard_survey_dialog_shown, value) + + var contributionsDashboardEntryDialogShown + get() = PrefsIoUtil.getBoolean(R.string.preference_key_contributions_dashboard_entry_dialog_shown, false) + set(value) = PrefsIoUtil.setBoolean(R.string.preference_key_contributions_dashboard_entry_dialog_shown, value) + + var currentSelectedAppIcon + get() = PrefsIoUtil.getString(R.string.preference_key_current_selected_app_icon, LauncherIcon.DEFAULT.key) + set(value) = PrefsIoUtil.setString(R.string.preference_key_current_selected_app_icon, value) } diff --git a/app/src/main/java/org/wikipedia/settings/SettingsActivity.kt b/app/src/main/java/org/wikipedia/settings/SettingsActivity.kt index 8bec26e0b44..85800946bbd 100644 --- a/app/src/main/java/org/wikipedia/settings/SettingsActivity.kt +++ b/app/src/main/java/org/wikipedia/settings/SettingsActivity.kt @@ -15,7 +15,7 @@ class SettingsActivity : SingleFragmentActivity() { private val app = WikipediaApp.instance public override fun createFragment(): SettingsFragment { - return SettingsFragment.newInstance() + return SettingsFragment.newInstance(intent.getBooleanExtra(Constants.ARG_BOOLEAN, false)) } override fun onCreate(savedInstanceState: Bundle?) { @@ -42,8 +42,9 @@ class SettingsActivity : SingleFragmentActivity() { const val ACTIVITY_RESULT_FEED_CONFIGURATION_CHANGED = 2 const val ACTIVITY_RESULT_LOG_OUT = 3 - fun newIntent(ctx: Context): Intent { + fun newIntent(ctx: Context, showAppIconDialog: Boolean = false): Intent { return Intent(ctx, SettingsActivity::class.java) + .putExtra(Constants.ARG_BOOLEAN, showAppIconDialog) } } } diff --git a/app/src/main/java/org/wikipedia/settings/SettingsFragment.kt b/app/src/main/java/org/wikipedia/settings/SettingsFragment.kt index da129a86650..5abacc629fb 100644 --- a/app/src/main/java/org/wikipedia/settings/SettingsFragment.kt +++ b/app/src/main/java/org/wikipedia/settings/SettingsFragment.kt @@ -5,6 +5,7 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.core.os.bundleOf import androidx.core.view.MenuProvider import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -12,6 +13,7 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.preference.SwitchPreferenceCompat import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch +import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.concurrency.FlowEventBus import org.wikipedia.events.ReadingListsEnableSyncStatusEvent @@ -51,6 +53,9 @@ class SettingsFragment : PreferenceLoaderFragment(), MenuProvider { override fun loadPreferences() { preferenceLoader = SettingsPreferenceLoader(this) preferenceLoader.loadPreferences() + if (requireArguments().getBoolean(Constants.ARG_BOOLEAN, false)) { + preferenceLoader.showAppIconDialog() + } } override fun onResume() { @@ -96,8 +101,10 @@ class SettingsFragment : PreferenceLoaderFragment(), MenuProvider { } companion object { - fun newInstance(): SettingsFragment { - return SettingsFragment() + fun newInstance(showAppIconDialog: Boolean = false): SettingsFragment { + return SettingsFragment().apply { + arguments = bundleOf(Constants.ARG_BOOLEAN to showAppIconDialog) + } } } } diff --git a/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.kt b/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.kt index 5da2fb03e4c..b0980b3c602 100644 --- a/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.kt +++ b/app/src/main/java/org/wikipedia/settings/SettingsPreferenceLoader.kt @@ -12,11 +12,14 @@ import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.auth.AccountUtil +import org.wikipedia.donate.DonorStatus import org.wikipedia.feed.configure.ConfigureActivity import org.wikipedia.login.LoginActivity +import org.wikipedia.page.ExclusiveBottomSheetPresenter import org.wikipedia.readinglist.sync.ReadingListSyncAdapter import org.wikipedia.settings.languages.WikipediaLanguagesActivity import org.wikipedia.theme.ThemeFittingRoomActivity +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.FeedbackUtil /** UI code for app settings used by PreferenceFragment. */ @@ -47,6 +50,14 @@ internal class SettingsPreferenceLoader(fragment: PreferenceFragmentCompat) : Ba true } } + + findPreference(R.string.preference_key_app_icon).isVisible = shouldShowAppIconPreference + + findPreference(R.string.preference_key_app_icon).onPreferenceClickListener = Preference.OnPreferenceClickListener { + showAppIconDialog() + true + } + findPreference(R.string.preference_key_about_wikipedia_app).onPreferenceClickListener = Preference.OnPreferenceClickListener { activity.startActivity(Intent(activity, AboutActivity::class.java)) true @@ -71,6 +82,14 @@ internal class SettingsPreferenceLoader(fragment: PreferenceFragmentCompat) : Ba return "\n\nVersion: ${BuildConfig.VERSION_NAME} \nDevice: ${Build.BRAND} ${Build.MODEL} (SDK: ${Build.VERSION.SDK_INT})\n" } + private val shouldShowAppIconPreference get() = ContributionsDashboardHelper.contributionsDashboardEnabled && DonorStatus.donorStatus() == DonorStatus.DONOR + + fun showAppIconDialog() { + if (shouldShowAppIconPreference) { + ExclusiveBottomSheetPresenter.show(fragment.parentFragmentManager, AppIconDialog.newInstance()) + } + } + fun updateLanguagePrefSummary() { // TODO: resolve RTL vs LTR with multiple languages (e.g. list contains English and Hebrew) findPreference(R.string.preference_key_language).summary = WikipediaApp.instance.languageState.appLanguageLocalizedNames diff --git a/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt b/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt index 6ad0e453026..832e9a2dce3 100644 --- a/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt +++ b/app/src/main/java/org/wikipedia/suggestededits/SuggestedEditsTasksFragment.kt @@ -3,13 +3,11 @@ package org.wikipedia.suggestededits import android.app.Activity import android.net.Uri import android.os.Bundle +import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View -import android.view.View.GONE -import android.view.View.VISIBLE import android.view.ViewGroup import androidx.activity.result.contract.ActivityResultContracts -import androidx.constraintlayout.widget.Group import androidx.core.view.isVisible import androidx.core.widget.NestedScrollView import androidx.fragment.app.Fragment @@ -18,15 +16,18 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.wikipedia.Constants import org.wikipedia.R import org.wikipedia.WikipediaApp import org.wikipedia.analytics.eventplatform.BreadCrumbLogEvent +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent import org.wikipedia.analytics.eventplatform.ImageRecommendationsEvent import org.wikipedia.analytics.eventplatform.PatrollerExperienceEvent import org.wikipedia.analytics.eventplatform.UserContributionEvent import org.wikipedia.auth.AccountUtil +import org.wikipedia.concurrency.FlowEventBus import org.wikipedia.databinding.FragmentSuggestedEditsTasksBinding import org.wikipedia.descriptions.DescriptionEditActivity.Action.ADD_CAPTION import org.wikipedia.descriptions.DescriptionEditActivity.Action.ADD_DESCRIPTION @@ -35,10 +36,15 @@ import org.wikipedia.descriptions.DescriptionEditActivity.Action.IMAGE_RECOMMEND import org.wikipedia.descriptions.DescriptionEditActivity.Action.TRANSLATE_CAPTION import org.wikipedia.descriptions.DescriptionEditActivity.Action.TRANSLATE_DESCRIPTION import org.wikipedia.descriptions.DescriptionEditUtil +import org.wikipedia.donate.DonorHistoryActivity +import org.wikipedia.donate.DonorStatus +import org.wikipedia.events.LoggedOutEvent import org.wikipedia.login.LoginActivity import org.wikipedia.main.MainActivity +import org.wikipedia.navtab.NavTab import org.wikipedia.settings.Prefs import org.wikipedia.settings.languages.WikipediaLanguagesActivity +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.usercontrib.UserContribListActivity import org.wikipedia.usercontrib.UserContribStats import org.wikipedia.util.DateUtil @@ -46,12 +52,12 @@ import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.ReleaseUtil import org.wikipedia.util.Resource import org.wikipedia.util.ResourceUtil -import org.wikipedia.util.StringUtil import org.wikipedia.util.UriUtil import org.wikipedia.views.DefaultRecyclerAdapter import org.wikipedia.views.DefaultViewHolder import java.time.LocalDateTime import java.time.ZoneId +import java.util.Date class SuggestedEditsTasksFragment : Fragment() { private var _binding: FragmentSuggestedEditsTasksBinding? = null @@ -72,13 +78,13 @@ class SuggestedEditsTasksFragment : Fragment() { if (!isAdded) { return@Runnable } - val balloon = FeedbackUtil.getTooltip(requireContext(), binding.contributionsStatsView.tooltipText, autoDismiss = true, showDismissButton = true) - balloon.showAlignBottom(binding.contributionsStatsView.getDescriptionView()) - balloon.relayShowAlignBottom(FeedbackUtil.getTooltip(requireContext(), binding.editStreakStatsView.tooltipText, autoDismiss = true, showDismissButton = true), binding.editStreakStatsView.getDescriptionView()) - .relayShowAlignBottom(FeedbackUtil.getTooltip(requireContext(), binding.pageViewStatsView.tooltipText, autoDismiss = true, showDismissButton = true), binding.pageViewStatsView.getDescriptionView()) - .relayShowAlignBottom(FeedbackUtil.getTooltip(requireContext(), binding.editQualityStatsView.tooltipText, autoDismiss = true, showDismissButton = true), binding.editQualityStatsView.getDescriptionView()) + val balloon = FeedbackUtil.getTooltip(requireContext(), binding.editsCountStatsView.tooltipText, autoDismiss = true, showDismissButton = true) + balloon.showAlignBottom(binding.editsCountStatsView.getTitleView()) + balloon.relayShowAlignBottom(FeedbackUtil.getTooltip(requireContext(), binding.editStreakStatsView.tooltipText, autoDismiss = true, showDismissButton = true), binding.editStreakStatsView.getTitleView()) + .relayShowAlignBottom(FeedbackUtil.getTooltip(requireContext(), binding.pageViewStatsView.tooltipText, autoDismiss = true, showDismissButton = true), binding.pageViewStatsView.getTitleView()) + .relayShowAlignBottom(FeedbackUtil.getTooltip(requireContext(), binding.editQualityStatsView.tooltipText, autoDismiss = true, showDismissButton = true), binding.editQualityStatsView.getTitleView()) Prefs.showOneTimeSequentialUserStatsTooltip = false - BreadCrumbLogEvent.logTooltipShown(requireActivity(), binding.contributionsStatsView) + BreadCrumbLogEvent.logTooltipShown(requireActivity(), binding.editsCountStatsView) } private val requestAddLanguage = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { @@ -98,6 +104,10 @@ class SuggestedEditsTasksFragment : Fragment() { } } + private val requestUpdateDonorHistory = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + maybeShowDonorHistoryUpdatedSnackbar() + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { super.onCreateView(inflater, container, savedInstanceState) _binding = FragmentSuggestedEditsTasksBinding.inflate(inflater, container, false) @@ -107,11 +117,15 @@ class SuggestedEditsTasksFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) setupTestingButtons() - - binding.userStatsViewsGroup.addOnClickListener { + maybeShowDonorHistoryUpdatedSnackbar() + binding.contributionsContainer.setOnClickListener { startActivity(UserContribListActivity.newIntent(requireActivity(), AccountUtil.userName)) } + binding.donorHistoryContainer.setOnClickListener { + requestUpdateDonorHistory.launch(DonorHistoryActivity.newIntent(requireContext())) + } + binding.learnMoreCard.setOnClickListener { FeedbackUtil.showAndroidAppEditingFAQ(requireContext()) } @@ -123,7 +137,10 @@ class SuggestedEditsTasksFragment : Fragment() { binding.swipeRefreshLayout.setOnRefreshListener { refreshContents() } binding.errorView.retryClickListener = View.OnClickListener { refreshContents() } - binding.errorView.loginClickListener = View.OnClickListener { requestLogin.launch(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_SUGGESTED_EDITS)) } + binding.errorView.loginClickListener = View.OnClickListener { + ContributionsDashboardEvent.logAction("login_click", "contrib_dashboard") + requestLogin.launch(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_SUGGESTED_EDITS)) + } binding.suggestedEditsScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { _, _, scrollY, _, _ -> (requireActivity() as MainActivity).updateToolbarElevation(scrollY > 0) @@ -134,26 +151,50 @@ class SuggestedEditsTasksFragment : Fragment() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { - viewModel.uiState.collect { - when (it) { - is Resource.Loading -> onLoading() - is Resource.Success -> setFinalUIState() - is SuggestedEditsTasksFragmentViewModel.RequireLogin -> onRequireLogin() - is Resource.Error -> showError(it.throwable) + launch { + viewModel.uiState.collect { + when (it) { + is Resource.Loading -> onLoading() + is Resource.Success -> setFinalUIState() + is SuggestedEditsTasksFragmentViewModel.RequireLogin -> onRequireLogin() + is Resource.Error -> showError(it.throwable) + } + } + } + + launch { + FlowEventBus.events.collectLatest { event -> + if (event is LoggedOutEvent && + (requireActivity() as MainActivity).isCurrentFragmentSelected(this@SuggestedEditsTasksFragment)) { + refreshContents() + } } } } } } - private fun Group.addOnClickListener(listener: View.OnClickListener) { - referencedIds.forEach { id -> - binding.userStatsClickTarget.findViewById(id).setOnClickListener(listener) + private fun showDialogOrSnackBar() { + when (DonorStatus.donorStatus()) { + DonorStatus.DONOR -> { + if (ContributionsDashboardHelper.shouldShowThankYouDialog) { + ContributionsDashboardHelper.showThankYouDialog(requireContext()) + ContributionsDashboardHelper.shouldShowThankYouDialog = false + } + } + DonorStatus.NON_DONOR -> { + if (ContributionsDashboardHelper.shouldShowDonorHistorySnackbar) { + ContributionsDashboardEvent.logAction("impression", "contrib_confirm") + FeedbackUtil.showMessage(this, R.string.donor_history_updated_message_snackbar) + ContributionsDashboardHelper.shouldShowDonorHistorySnackbar = false + } + } + DonorStatus.UNKNOWN -> {} } - binding.userStatsClickTarget.setOnClickListener(listener) } fun refreshContents() { + (requireActivity() as MainActivity).onTabChanged(NavTab.EDITS) requireActivity().invalidateOptionsMenu() viewModel.fetchData() } @@ -161,6 +202,7 @@ class SuggestedEditsTasksFragment : Fragment() { override fun onResume() { super.onResume() refreshContents() + showDialogOrSnackBar() } override fun onDestroyView() { @@ -172,26 +214,35 @@ class SuggestedEditsTasksFragment : Fragment() { private fun onLoading() { binding.progressBar.isVisible = true + binding.suggestedEditsScrollView.isVisible = false } private fun onRequireLogin() { clearContents() - binding.disabledStatesView.setRequiredLogin { + binding.messageCard.setRequiredLogin { + ContributionsDashboardEvent.logAction("login_click", "contrib_dashboard") requestLogin.launch(LoginActivity.newIntent(requireContext(), LoginActivity.SOURCE_SUGGESTED_EDITS)) } - binding.disabledStatesView.isVisible = true + binding.messageCard.isVisible = true + binding.contributionsContainer.isVisible = false + binding.statsDivider.isVisible = false } private fun clearContents(shouldScrollToTop: Boolean = true) { + binding.suggestedEditsScrollView.isVisible = true binding.swipeRefreshLayout.isRefreshing = false binding.progressBar.isVisible = false binding.tasksContainer.isVisible = false binding.errorView.isVisible = false - binding.disabledStatesView.isVisible = false + binding.messageCard.isVisible = false + binding.contributionsContainer.isVisible = false + binding.statsDivider.isVisible = false if (shouldScrollToTop) { binding.suggestedEditsScrollView.scrollTo(0, 0) } binding.swipeRefreshLayout.setBackgroundColor(ResourceUtil.getThemedColor(requireContext(), R.attr.paper_color)) + + setUpDonorHistoryStatus() } private fun showError(t: Throwable) { @@ -218,34 +269,33 @@ class SuggestedEditsTasksFragment : Fragment() { binding.tasksRecyclerView.adapter!!.notifyDataSetChanged() setUserStatsViewsAndTooltips() - binding.pageViewStatsView.setTitle(viewModel.totalPageviews.toString()) + binding.pageViewStatsView.setDescription(viewModel.totalPageviews.toString()) if (viewModel.latestEditStreak < 2) { - binding.editStreakStatsView.setTitle(if (viewModel.latestEditDate.time > 0) DateUtil.getMDYDateString(viewModel.latestEditDate) else resources.getString(R.string.suggested_edits_last_edited_never)) - binding.editStreakStatsView.setDescription(resources.getString(R.string.suggested_edits_last_edited)) + binding.editStreakStatsView.setTitle(resources.getString(R.string.suggested_edits_last_edited)) + binding.editStreakStatsView.setDescription(if (viewModel.latestEditDate.time > 0) DateUtil.getMDYDateString(viewModel.latestEditDate) else resources.getString(R.string.suggested_edits_last_edited_never)) } else { - binding.editStreakStatsView.setTitle(resources.getQuantityString(R.plurals.suggested_edits_edit_streak_detail_text, + binding.editStreakStatsView.setTitle(resources.getString(R.string.suggested_edits_edit_streak_label_text)) + binding.editStreakStatsView.setDescription(resources.getQuantityString(R.plurals.suggested_edits_edit_streak_detail_text, viewModel.latestEditStreak, viewModel.latestEditStreak)) - binding.editStreakStatsView.setDescription(resources.getString(R.string.suggested_edits_edit_streak_label_text)) } if (viewModel.totalContributions == 0) { - binding.userStatsClickTarget.isEnabled = false - binding.userStatsViewsGroup.visibility = GONE - binding.onboardingImageView.visibility = VISIBLE - binding.onboardingTextView.visibility = VISIBLE - binding.onboardingTextView.text = StringUtil.fromHtml(getString(R.string.suggested_edits_onboarding_message, AccountUtil.userName)) + binding.contributionsContainer.isVisible = false + binding.statsDivider.isVisible = false + binding.messageCard.isVisible = true + binding.messageCard.setOnboarding(getString(R.string.suggested_edits_onboarding_message, AccountUtil.userName)) } else { - binding.userStatsViewsGroup.visibility = VISIBLE - binding.onboardingImageView.visibility = GONE - binding.onboardingTextView.visibility = GONE - binding.userStatsClickTarget.isEnabled = true - binding.userNameView.text = AccountUtil.userName - binding.contributionsStatsView.setTitle(viewModel.totalContributions.toString()) - binding.contributionsStatsView.setDescription(resources.getQuantityString(R.plurals.suggested_edits_contribution, viewModel.totalContributions)) - if (Prefs.showOneTimeSequentialUserStatsTooltip) { - showOneTimeSequentialUserStatsTooltips() - } + binding.contributionsContainer.isVisible = true + binding.statsDivider.isVisible = true + val contributionsStatsViewPluralRes = if (ContributionsDashboardHelper.contributionsDashboardEnabled) + R.plurals.suggested_edits_edit_frequency else R.plurals.suggested_edits_contribution + binding.editsCountStatsView.setTitle(resources.getQuantityString(contributionsStatsViewPluralRes, viewModel.totalContributions)) + binding.editsCountStatsView.setDescription(viewModel.totalContributions.toString()) + // TODO: add the sequential tooltips back after the experiment code is removed. +// if (Prefs.showOneTimeSequentialUserStatsTooltip) { +// showOneTimeSequentialUserStatsTooltips() +// } } binding.swipeRefreshLayout.setBackgroundColor(ResourceUtil.getThemedColor(requireContext(), R.attr.paper_color)) @@ -253,19 +303,19 @@ class SuggestedEditsTasksFragment : Fragment() { } private fun setUserStatsViewsAndTooltips() { - binding.contributionsStatsView.setImageDrawable(R.drawable.ic_mode_edit_white_24dp) - binding.contributionsStatsView.tooltipText = getString(R.string.suggested_edits_contributions_stat_tooltip) + binding.editsCountStatsView.setImageDrawable(R.drawable.ic_mode_edit_white_24dp) + binding.editsCountStatsView.tooltipText = getString(R.string.suggested_edits_contributions_stat_tooltip) - binding.editStreakStatsView.setDescription(resources.getString(R.string.suggested_edits_edit_streak_label_text)) - binding.editStreakStatsView.setImageDrawable(R.drawable.ic_timer_black_24dp) + binding.editStreakStatsView.setTitle(resources.getString(R.string.suggested_edits_edit_streak_label_text)) + binding.editStreakStatsView.setImageDrawable(R.drawable.ic_icon_revision_history_apps) binding.editStreakStatsView.tooltipText = getString(R.string.suggested_edits_edit_streak_stat_tooltip) - binding.pageViewStatsView.setDescription(getString(R.string.suggested_edits_views_label_text)) + binding.pageViewStatsView.setTitle(getString(R.string.suggested_edits_views_label_text)) binding.pageViewStatsView.setImageDrawable(R.drawable.ic_trending_up_black_24dp) binding.pageViewStatsView.tooltipText = getString(R.string.suggested_edits_page_views_stat_tooltip) binding.editQualityStatsView.setGoodnessState(viewModel.revertSeverity) - binding.editQualityStatsView.setDescription(getString(R.string.suggested_edits_quality_label_text)) + binding.editQualityStatsView.setTitle(getString(R.string.suggested_edits_quality_label_text)) binding.editQualityStatsView.tooltipText = getString(R.string.suggested_edits_edit_quality_stat_tooltip, UserContribStats.totalReverts) } @@ -277,8 +327,8 @@ class SuggestedEditsTasksFragment : Fragment() { private fun setIPBlockedStatus() { clearContents() - binding.disabledStatesView.setIPBlocked(viewModel.blockMessageWikipedia) - binding.disabledStatesView.visibility = VISIBLE + binding.messageCard.setIPBlocked(viewModel.blockMessageWikipedia) + binding.messageCard.isVisible = true UserContributionEvent.logIpBlock() } @@ -287,55 +337,110 @@ class SuggestedEditsTasksFragment : Fragment() { if (viewModel.totalContributions < MIN_CONTRIBUTIONS_FOR_SUGGESTED_EDITS && WikipediaApp.instance.appOrSystemLanguageCode == "en") { clearContents() - binding.disabledStatesView.setDisabled(getString(R.string.suggested_edits_gate_message, AccountUtil.userName)) - binding.disabledStatesView.setPositiveButton(R.string.suggested_edits_learn_more, { + binding.messageCard.setDisabled(getString(R.string.suggested_edits_gate_message, AccountUtil.userName)) + binding.messageCard.setPositiveButton(R.string.suggested_edits_learn_more, { UriUtil.visitInExternalBrowser(requireContext(), Uri.parse(MIN_CONTRIBUTIONS_GATE_URL)) }, true) - binding.disabledStatesView.visibility = VISIBLE + binding.messageCard.isVisible = true return true } else if (UserContribStats.isDisabled()) { // Disable the whole feature. clearContents() - binding.disabledStatesView.setDisabled(getString(R.string.suggested_edits_disabled_message, AccountUtil.userName)) - binding.disabledStatesView.visibility = VISIBLE + binding.messageCard.setDisabled(getString(R.string.suggested_edits_disabled_message, AccountUtil.userName)) + binding.messageCard.isVisible = true UserContributionEvent.logDisabled() return true } else if (pauseEndDate != null) { clearContents() val localDateTime = LocalDateTime.ofInstant(pauseEndDate.toInstant(), ZoneId.systemDefault()).toLocalDate() - binding.disabledStatesView.setPaused(getString(R.string.suggested_edits_paused_message, DateUtil.getShortDateString(localDateTime), AccountUtil.userName)) - binding.disabledStatesView.visibility = VISIBLE + binding.messageCard.setPaused(getString(R.string.suggested_edits_paused_message, DateUtil.getShortDateString(localDateTime), AccountUtil.userName)) + binding.messageCard.isVisible = true UserContributionEvent.logPaused() return true } - binding.disabledStatesView.visibility = GONE + binding.messageCard.isVisible = false return false } private fun setupTestingButtons() { if (!ReleaseUtil.isPreBetaRelease) { - binding.showIPBlockedMessage.visibility = GONE - binding.showOnboardingMessage.visibility = GONE + binding.showIPBlockedMessage.isVisible = false + binding.showOnboardingMessage.isVisible = false } binding.showIPBlockedMessage.setOnClickListener { setIPBlockedStatus() } binding.showOnboardingMessage.setOnClickListener { viewModel.totalContributions = 0; setFinalUIState() } } + private fun setUpDonorHistoryStatus() { + if (!ContributionsDashboardHelper.contributionsDashboardEnabled) { + binding.donorHistoryContainer.isVisible = false + binding.statsDivider.isVisible = false + return + } + + ContributionsDashboardEvent.logAction("impression", "contrib_dashboard") + binding.donorHistoryContainer.isVisible = true + + when (DonorStatus.donorStatus()) { + DonorStatus.DONOR -> { + Prefs.donationResults.lastOrNull()?.dateTime?.let { + val lastDonateMilli = + LocalDateTime.parse(it).atZone(ZoneId.systemDefault()).toInstant() + .toEpochMilli() + var relativeTimeSpan = DateUtils.getRelativeTimeSpanString( + lastDonateMilli, + System.currentTimeMillis(), + DateUtils.DAY_IN_MILLIS, + DateUtils.FORMAT_NUMERIC_DATE + ) + // Replace with the original dateTime string + if (relativeTimeSpan.contains("/")) { + relativeTimeSpan = DateUtil.getMDYDateString(Date(lastDonateMilli)) + } + binding.donorHistoryStatus.text = relativeTimeSpan + binding.donorHistoryStatus.isVisible = true + binding.lastDonatedChevron.isVisible = true + binding.donorHistoryUpdateButton.isVisible = false + } ?: run { + binding.donorHistoryStatus.isVisible = false + binding.lastDonatedChevron.isVisible = false + binding.donorHistoryUpdateButton.isVisible = true + } + } + + DonorStatus.NON_DONOR -> { + binding.donorHistoryStatus.text = getString(R.string.donor_history_last_donated_never) + binding.donorHistoryStatus.isVisible = true + binding.lastDonatedChevron.isVisible = true + binding.donorHistoryUpdateButton.isVisible = false + } + + DonorStatus.UNKNOWN -> { + binding.donorHistoryStatus.isVisible = false + binding.lastDonatedChevron.isVisible = false + binding.donorHistoryUpdateButton.isVisible = true + } + } + + binding.donorHistoryUpdateButton.setOnClickListener { + ContributionsDashboardEvent.logAction("update_click", "contrib_dashboard") + requestUpdateDonorHistory.launch(DonorHistoryActivity.newIntent(requireContext())) + } + } + private fun setUpTasks() { displayedTasks.clear() addImageTagsTask = SuggestedEditsTask() addImageTagsTask.title = getString(R.string.suggested_edits_image_tags) addImageTagsTask.description = getString(R.string.suggested_edits_image_tags_task_detail) - addImageTagsTask.primaryAction = getString(R.string.suggested_edits_task_action_text_add) addImageTagsTask.imageDrawable = R.drawable.ic_image_tag addImageTagsTask.primaryAction = getString(R.string.suggested_edits_task_action_text_add) addImageCaptionsTask = SuggestedEditsTask() addImageCaptionsTask.title = getString(R.string.suggested_edits_image_captions) addImageCaptionsTask.description = getString(R.string.suggested_edits_image_captions_task_detail) - addImageCaptionsTask.primaryAction = getString(R.string.suggested_edits_task_action_text_add) addImageCaptionsTask.imageDrawable = R.drawable.ic_image_caption addImageCaptionsTask.primaryAction = getString(R.string.suggested_edits_task_action_text_add) addImageCaptionsTask.secondaryAction = getString(R.string.suggested_edits_task_action_text_translate) @@ -384,6 +489,23 @@ class SuggestedEditsTasksFragment : Fragment() { } } + private fun maybeShowDonorHistoryUpdatedSnackbar() { + if (ContributionsDashboardHelper.contributionsDashboardEnabled && ContributionsDashboardHelper.showSurveyDialogUI) { + if (Prefs.hasDonorHistorySaved) { + if (!Prefs.contributionsDashboardSurveyDialogShown) { + ContributionsDashboardHelper.showSurveyDialog(requireContext(), onNegativeButtonClick = { + showDialogOrSnackBar() + }) + Prefs.contributionsDashboardSurveyDialogShown = true + } else { + ContributionsDashboardEvent.logAction("impression", "contrib_confirm") + FeedbackUtil.showMessage(this, R.string.donor_history_updated_message_snackbar) + } + ContributionsDashboardHelper.showSurveyDialogUI = false + } + } + } + private inner class TaskViewCallback : SuggestedEditsTaskView.Callback { override fun onViewClick(task: SuggestedEditsTask, secondary: Boolean) { if (WikipediaApp.instance.languageState.appLanguageCodes.size < Constants.MIN_LANGUAGES_TO_UNLOCK_TRANSLATION && secondary) { diff --git a/app/src/main/java/org/wikipedia/usercontrib/ContributionsDashboardHelper.kt b/app/src/main/java/org/wikipedia/usercontrib/ContributionsDashboardHelper.kt new file mode 100644 index 00000000000..d685f229c95 --- /dev/null +++ b/app/src/main/java/org/wikipedia/usercontrib/ContributionsDashboardHelper.kt @@ -0,0 +1,133 @@ +package org.wikipedia.usercontrib + +import android.content.Context +import android.net.Uri +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.wikipedia.R +import org.wikipedia.WikipediaApp +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent +import org.wikipedia.donate.DonorHistoryActivity +import org.wikipedia.donate.DonorStatus +import org.wikipedia.settings.SettingsActivity +import org.wikipedia.util.GeoUtil +import org.wikipedia.util.ReleaseUtil +import org.wikipedia.util.UriUtil +import java.time.LocalDate + +class ContributionsDashboardHelper { + + companion object { + + const val CAMPAIGN_ID = "contrib" + + private val enabledCountries = listOf( + "FR", "NL" + ) + + private val enabledLanguages = listOf( + "fr", "nl", "en" + ) + + private fun getSurveyDialogUrl(): String { + val surveyUrls = mapOf( + "fr" to "https://docs.google.com/forms/d/1EfPNslpWWQd1WQoA3IkFRKhWy02BTaBgTer1uCKiIHU/edit", + "nl" to "https://docs.google.com/forms/d/15GXIEfQTujFtXNU5NqfDyr9lxxET6fP0hk_p_Xz-NOk/edit", + "en" to "https://docs.google.com/forms/d/1wIJWp75MMyp2e51kSaPH9ctByUzbyhazEOaJTxQhKqs/edit" + ) + return surveyUrls[WikipediaApp.instance.languageState.appLanguageCode].orEmpty() + } + + var shouldShowDonorHistorySnackbar = false + + var shouldShowThankYouDialog = false + + var showSurveyDialogUI = false + + val contributionsDashboardEnabled get() = ReleaseUtil.isPreBetaRelease || + (enabledCountries.contains(GeoUtil.geoIPCountry.orEmpty()) && + enabledLanguages.contains(WikipediaApp.instance.languageState.appLanguageCode) && + LocalDate.now() <= LocalDate.of(2024, 12, 20)) + + fun showSurveyDialog(context: Context, onNegativeButtonClick: () -> Unit) { + ContributionsDashboardEvent.logAction("impression", "contrib_survey") + MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme_Icon_Secondary) + .setTitle(R.string.contributions_dashboard_survey_dialog_title) + .setMessage(R.string.contributions_dashboard_survey_dialog_message) + .setIcon(R.drawable.ic_feedback) + .setCancelable(false) + .setPositiveButton(R.string.contributions_dashboard_survey_dialog_ok) { _, _ -> + ContributionsDashboardEvent.logAction("enter_click", "contrib_survey") + // this should be called on button click due to logic in onResume + setEitherShowDialogOrSnackBar() + UriUtil.visitInExternalBrowser( + context, + Uri.parse(getSurveyDialogUrl()) + ) + } + .setNegativeButton(R.string.contributions_dashboard_survey_dialog_cancel) { _, _ -> + ContributionsDashboardEvent.logAction("cancel_click", "contrib_survey") + // this should be called on button click due to logic in onResume + setEitherShowDialogOrSnackBar() + onNegativeButtonClick() + } + .show() + } + + fun showThankYouDialog(context: Context) { + ContributionsDashboardEvent.logAction("impression", "contrib_icon_offer") + MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme_Icon_Secondary) + .setTitle(R.string.contributions_dashboard_donor_icon_dialog_title) + .setMessage(R.string.contributions_dashboard_donor_icon_dialog_message) + .setIcon(R.drawable.ic_heart_24) + .setPositiveButton(R.string.contributions_dashboard_donor_icon_dialog_ok) { _, _ -> + ContributionsDashboardEvent.logAction("enter_click", "contrib_icon_offer") + context.startActivity(SettingsActivity.newIntent(context, showAppIconDialog = true)) + } + .setNegativeButton(R.string.contributions_dashboard_donor_icon_dialog_cancel) { _, _ -> + ContributionsDashboardEvent.logAction("cancel_click", "contrib_icon_offer") + } + .show() + } + + fun showDonationCompletedDialog(context: Context) { + ContributionsDashboardEvent.logAction("impression", "contrib_donor_banner") + val message = String.format(context.getString(R.string.contributions_dashboard_donation_dialog_message)) + MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme_Icon_Secondary) + .setTitle(R.string.contributions_dashboard_donation_dialog_title) + .setMessage(message) + .setIcon(R.drawable.outline_volunteer_activism_24) + .setPositiveButton(R.string.contributions_dashboard_donation_dialog_ok) { _, _ -> + ContributionsDashboardEvent.logAction("contrib_enter_click", "contrib_donor_banner") + context.startActivity(DonorHistoryActivity.newIntent(context, completedDonation = true, goBackToContributeTab = true)) + } + .setNegativeButton(R.string.contributions_dashboard_donation_dialog_cancel, { _, _ -> + ContributionsDashboardEvent.logAction("contrib_enter_click", "contrib_cancel_click") + }) + .show() + } + + fun showEntryDialog(context: Context) { + ContributionsDashboardEvent.logAction("impression", "contrib_banner") + MaterialAlertDialogBuilder(context, R.style.AlertDialogTheme_Icon_Secondary) + .setTitle(R.string.contributions_dashboard_entry_dialog_title) + .setMessage(R.string.contributions_dashboard_entry_dialog_message) + .setIcon(R.drawable.outline_volunteer_activism_24) + .setPositiveButton(R.string.contributions_dashboard_entry_dialog_ok) { _, _ -> + ContributionsDashboardEvent.logAction("contrib_enter_click", "contrib_banner") + context.startActivity(DonorHistoryActivity.newIntent(context, goBackToContributeTab = true)) + } + .setNegativeButton(R.string.contributions_dashboard_entry_dialog_cancel, { _, _ -> + ContributionsDashboardEvent.logAction("contrib_cancel_click", "contrib_banner") + }) + .show() + } + + private fun setEitherShowDialogOrSnackBar() { + when (DonorStatus.donorStatus()) { + DonorStatus.DONOR -> shouldShowThankYouDialog = true + DonorStatus.NON_DONOR -> shouldShowDonorHistorySnackbar = true + DonorStatus.UNKNOWN -> {} + } + } + } +} diff --git a/app/src/main/java/org/wikipedia/views/DonorBadgeView.kt b/app/src/main/java/org/wikipedia/views/DonorBadgeView.kt new file mode 100644 index 00000000000..84d0a494d25 --- /dev/null +++ b/app/src/main/java/org/wikipedia/views/DonorBadgeView.kt @@ -0,0 +1,69 @@ +package org.wikipedia.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.view.isVisible +import org.wikipedia.databinding.ViewDonorBadgeBinding +import org.wikipedia.donate.DonorStatus +import org.wikipedia.usercontrib.ContributionsDashboardHelper + +class DonorBadgeView(context: Context, attrs: AttributeSet? = null) : FrameLayout(context, attrs) { + + interface Callback { + fun onDonorBadgeClick() { } + fun onBecomeDonorClick() { } + fun onUpdateDonorStatusClick() { } + } + val binding = ViewDonorBadgeBinding.inflate(LayoutInflater.from(context), this) + + init { + layoutParams = ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + } + + fun disableClickForDonor() { + binding.donorChip.isEnabled = false + } + + fun setup(callback: Callback) { + if (!ContributionsDashboardHelper.contributionsDashboardEnabled) { + isVisible = false + return + } + isVisible = true + binding.donorChip.isVisible = false + binding.becomeADonorChip.isVisible = false + binding.updateDonorStatusText.isVisible = false + when (DonorStatus.donorStatus()) { + DonorStatus.DONOR -> { + binding.donorChip.apply { + isVisible = true + setOnClickListener { + callback.onDonorBadgeClick() + } + } + } + DonorStatus.NON_DONOR -> { + binding.becomeADonorChip.apply { + isVisible = true + setOnClickListener { + callback.onBecomeDonorClick() + } + } + } + DonorStatus.UNKNOWN -> { + binding.updateDonorStatusText.apply { + isVisible = true + setOnClickListener { + callback.onUpdateDonorStatusClick() + } + } + } + else -> { + // Do nothing + } + } + } +} diff --git a/app/src/main/java/org/wikipedia/views/MessageCardView.kt b/app/src/main/java/org/wikipedia/views/MessageCardView.kt index e692c6a3019..eadef9a83ac 100644 --- a/app/src/main/java/org/wikipedia/views/MessageCardView.kt +++ b/app/src/main/java/org/wikipedia/views/MessageCardView.kt @@ -4,7 +4,6 @@ import android.content.Context import android.net.Uri import android.util.AttributeSet import android.view.LayoutInflater -import android.view.View import androidx.annotation.DrawableRes import androidx.annotation.StringRes import org.wikipedia.R @@ -27,10 +26,10 @@ class MessageCardView(context: Context, attrs: AttributeSet? = null) : WikiCardV fun setImageResource(@DrawableRes imageResource: Int, visible: Boolean) { if (visible) { - binding.imageView.visibility = View.VISIBLE + binding.imageView.visibility = VISIBLE binding.imageView.setImageResource(imageResource) } else { - binding.imageView.visibility = View.GONE + binding.imageView.visibility = GONE } } @@ -45,12 +44,19 @@ class MessageCardView(context: Context, attrs: AttributeSet? = null) : WikiCardV fun setNegativeButton(@StringRes stringRes: Int, listener: OnClickListener, applyListenerToContainer: Boolean) { binding.negativeButton.text = context.getString(stringRes) binding.negativeButton.setOnClickListener(listener) - binding.negativeButton.visibility = View.VISIBLE + binding.negativeButton.visibility = VISIBLE if (applyListenerToContainer) { binding.containerClickArea.setOnClickListener(listener) } } + fun setOnboarding(message: String) { + setImageResource(R.drawable.ic_suggested_edits_onboarding, true) + binding.messageTitleView.visibility = GONE + binding.messageTextView.text = StringUtil.fromHtml(message.toString()) + binding.buttonsContainer.visibility = GONE + } + fun setPaused(message: String) { setDefaultState() binding.messageTitleView.text = context.getString(R.string.suggested_edits_paused_title) @@ -82,7 +88,9 @@ class MessageCardView(context: Context, attrs: AttributeSet? = null) : WikiCardV } fun setRequiredLogin(onClickListener: OnClickListener) { - binding.imageView.visibility = View.VISIBLE + binding.imageView.visibility = VISIBLE + binding.messageTitleView.visibility = VISIBLE + binding.buttonsContainer.visibility = VISIBLE binding.messageTitleView.text = context.getString(R.string.suggested_edits_encourage_account_creation_title) binding.messageTextView.text = context.getString(R.string.suggested_edits_encourage_account_creation_message) binding.imageView.setImageResource(R.drawable.ic_require_login_header) @@ -92,10 +100,17 @@ class MessageCardView(context: Context, attrs: AttributeSet? = null) : WikiCardV } private fun setDefaultState() { - binding.imageView.visibility = View.VISIBLE + binding.imageView.visibility = VISIBLE + binding.messageTitleView.visibility = VISIBLE + binding.buttonsContainer.visibility = VISIBLE binding.positiveButton.text = context.getString(R.string.suggested_edits_learn_more) binding.positiveButton.setIconResource(R.drawable.ic_open_in_new_black_24px) binding.positiveButton.setOnClickListener { UriUtil.visitInExternalBrowser(context, Uri.parse(context.getString(R.string.android_app_edit_help_url))) } - binding.containerClickArea.setOnClickListener { UriUtil.visitInExternalBrowser(context, Uri.parse(context.getString(R.string.android_app_edit_help_url))) } + binding.containerClickArea.setOnClickListener { + UriUtil.visitInExternalBrowser( + context, + Uri.parse(context.getString(R.string.android_app_edit_help_url)) + ) + } } } diff --git a/app/src/main/java/org/wikipedia/views/PageActionOverflowView.kt b/app/src/main/java/org/wikipedia/views/PageActionOverflowView.kt index 6749c1a6a0c..002954d65e5 100644 --- a/app/src/main/java/org/wikipedia/views/PageActionOverflowView.kt +++ b/app/src/main/java/org/wikipedia/views/PageActionOverflowView.kt @@ -10,10 +10,12 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.PopupWindow import androidx.core.view.doOnDetach +import androidx.core.view.isVisible import androidx.core.widget.PopupWindowCompat import androidx.core.widget.TextViewCompat import com.google.android.material.textview.MaterialTextView import org.wikipedia.R +import org.wikipedia.analytics.eventplatform.ContributionsDashboardEvent import org.wikipedia.auth.AccountUtil import org.wikipedia.databinding.ItemCustomizeToolbarMenuBinding import org.wikipedia.databinding.ViewPageActionOverflowBinding @@ -22,9 +24,10 @@ import org.wikipedia.page.action.PageActionItem import org.wikipedia.page.customize.CustomizeToolbarActivity import org.wikipedia.page.tabs.Tab import org.wikipedia.settings.Prefs +import org.wikipedia.usercontrib.ContributionsDashboardHelper import org.wikipedia.util.ResourceUtil -class PageActionOverflowView(context: Context) : FrameLayout(context) { +class PageActionOverflowView(context: Context) : FrameLayout(context), DonorBadgeView.Callback { private var binding = ViewPageActionOverflowBinding.inflate(LayoutInflater.from(context), this, true) private var popupWindowHost: PopupWindow? = null @@ -35,6 +38,7 @@ class PageActionOverflowView(context: Context) : FrameLayout(context) { dismissPopupWindowHost() callback.forwardClick() } + loadDonorInformation() Prefs.customizeToolbarMenuOrder.forEach { val view = ItemCustomizeToolbarMenuBinding.inflate(LayoutInflater.from(context)).root val item = PageActionItem.find(it) @@ -98,10 +102,38 @@ class PageActionOverflowView(context: Context) : FrameLayout(context) { } } + private fun loadDonorInformation() { + if (ContributionsDashboardHelper.contributionsDashboardEnabled && AccountUtil.isLoggedIn) { + binding.donorContainer.isVisible = true + binding.donorUsername.text = AccountUtil.userName + binding.donorBadgeView.setup(this) + } + } + private fun dismissPopupWindowHost() { popupWindowHost?.let { it.dismiss() popupWindowHost = null } } + + override fun onDonorBadgeClick() { + // take user to Contribution Screen + callback.onDonorSelected() + dismissPopupWindowHost() + } + + override fun onBecomeDonorClick() { + // take user to the donation flow (the Donation bottom sheet). + ContributionsDashboardEvent.logAction("donate_start_click", "contrib_overflow", campaignId = ContributionsDashboardHelper.CAMPAIGN_ID) + callback.onBecomeDonorSelected() + dismissPopupWindowHost() + } + + override fun onUpdateDonorStatusClick() { + // take user to the donor history screen + ContributionsDashboardEvent.logAction("update_click", "contrib_overflow") + callback.onUpdateDonorStatusSelected() + dismissPopupWindowHost() + } } diff --git a/app/src/main/java/org/wikipedia/views/ImageTitleDescriptionView.kt b/app/src/main/java/org/wikipedia/views/SuggestedEditsStatsView.kt similarity index 67% rename from app/src/main/java/org/wikipedia/views/ImageTitleDescriptionView.kt rename to app/src/main/java/org/wikipedia/views/SuggestedEditsStatsView.kt index 6e80ffbc284..b1a613d3e9c 100644 --- a/app/src/main/java/org/wikipedia/views/ImageTitleDescriptionView.kt +++ b/app/src/main/java/org/wikipedia/views/SuggestedEditsStatsView.kt @@ -11,19 +11,19 @@ import androidx.annotation.DrawableRes import androidx.core.view.updateLayoutParams import androidx.core.widget.ImageViewCompat import org.wikipedia.R -import org.wikipedia.databinding.ViewImageTitleDescriptionBinding +import org.wikipedia.databinding.ViewSuggestedEditsStatsBinding import org.wikipedia.util.DimenUtil import org.wikipedia.util.FeedbackUtil import org.wikipedia.util.ResourceUtil -internal class ImageTitleDescriptionView constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { +internal class SuggestedEditsStatsView(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { - private val binding = ViewImageTitleDescriptionBinding.inflate(LayoutInflater.from(context), this) + private val binding = ViewSuggestedEditsStatsBinding.inflate(LayoutInflater.from(context), this) var tooltipText: String = "" init { - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - setOnLongClickListener { + layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + binding.description.setOnLongClickListener { if (tooltipText.isNotEmpty()) { FeedbackUtil.showTooltip(context as Activity, binding.description, tooltipText, aboveOrBelow = false, @@ -46,8 +46,8 @@ internal class ImageTitleDescriptionView constructor(context: Context, attrs: At binding.image.setImageResource(imageDrawable) } - fun getDescriptionView(): View { - return binding.description + fun getTitleView(): View { + return binding.title } fun setGoodnessState(severity: Int) { @@ -57,10 +57,10 @@ internal class ImageTitleDescriptionView constructor(context: Context, attrs: At val circleProgress: Double when (severity) { - 0 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.success_color; textRes = R.string.suggested_edits_quality_perfect_text; circleProgress = 100.0 } - 1 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.success_color; textRes = R.string.suggested_edits_quality_excellent_text; circleProgress = 85.0 } - 2 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.success_color; textRes = R.string.suggested_edits_quality_very_good_text; circleProgress = 75.0 } - 3 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.success_color; textRes = R.string.suggested_edits_quality_good_text; circleProgress = 55.0 } + 0 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.progressive_color; textRes = R.string.suggested_edits_quality_perfect_text; circleProgress = 100.0 } + 1 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.progressive_color; textRes = R.string.suggested_edits_quality_excellent_text; circleProgress = 85.0 } + 2 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.progressive_color; textRes = R.string.suggested_edits_quality_very_good_text; circleProgress = 75.0 } + 3 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.progressive_color; textRes = R.string.suggested_edits_quality_good_text; circleProgress = 55.0 } 4 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.highlight_color; textRes = R.string.suggested_edits_quality_okay_text; circleProgress = 40.0 } 5 -> { iconRes = R.drawable.ic_check_borderless; iconTint = R.attr.highlight_color; textRes = R.string.suggested_edits_quality_sufficient_text; circleProgress = 30.0 } else -> { iconRes = R.drawable.ic_exclamation_borderless; iconTint = R.attr.destructive_color; textRes = R.string.suggested_edits_quality_poor_text; circleProgress = 20.0 } @@ -69,18 +69,17 @@ internal class ImageTitleDescriptionView constructor(context: Context, attrs: At binding.circularProgressBar.setCurrentProgress(circleProgress) binding.circularProgressBar.progressBackgroundColor = ResourceUtil.getThemedColor(context, R.attr.paper_color) binding.circularProgressBar.progressColor = ResourceUtil.getThemedColor(context, iconTint) - binding.circularProgressBar.visibility = View.VISIBLE + binding.circularProgressBar.visibility = VISIBLE - ImageViewCompat.setImageTintList(binding.circularProgressBarOverlay, ResourceUtil.getThemedColorStateList(context, R.attr.paper_color)) - binding.circularProgressBarOverlay.visibility = View.VISIBLE + binding.circularProgressBarOverlay.visibility = VISIBLE - binding.title.text = context.getString(textRes) + binding.description.text = context.getString(textRes) binding.image.setImageResource(iconRes) ImageViewCompat.setImageTintList(binding.image, ResourceUtil.getThemedColorStateList(context, iconTint)) binding.image.updateLayoutParams { - width = DimenUtil.roundedDpToPx(DimenUtil.getDimension(R.dimen.suggested_edits_icon_size) * 3 / 4) + width = DimenUtil.roundedDpToPx(DimenUtil.getDimension(R.dimen.suggested_edits_icon_size)) height = width } binding.image.requestLayout() diff --git a/app/src/main/res/drawable/ic_baseline_volunteer_activism_24.xml b/app/src/main/res/drawable/ic_baseline_volunteer_activism_24.xml new file mode 100644 index 00000000000..753f748449a --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_volunteer_activism_24.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_feedback.xml b/app/src/main/res/drawable/ic_feedback.xml new file mode 100644 index 00000000000..67f9c6b44c8 --- /dev/null +++ b/app/src/main/res/drawable/ic_feedback.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_donor_benefit_foreground.xml b/app/src/main/res/drawable/ic_launcher_donor_benefit_foreground.xml new file mode 100644 index 00000000000..3014576fdf1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_donor_benefit_foreground.xml @@ -0,0 +1,21 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/outline_repeat_24.xml b/app/src/main/res/drawable/outline_repeat_24.xml new file mode 100644 index 00000000000..151a3e1f063 --- /dev/null +++ b/app/src/main/res/drawable/outline_repeat_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/outline_volunteer_activism_24.xml b/app/src/main/res/drawable/outline_volunteer_activism_24.xml new file mode 100644 index 00000000000..63506ad9af1 --- /dev/null +++ b/app/src/main/res/drawable/outline_volunteer_activism_24.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/drawable/round_app_registration_24.xml b/app/src/main/res/drawable/round_app_registration_24.xml new file mode 100644 index 00000000000..a99d3113f2e --- /dev/null +++ b/app/src/main/res/drawable/round_app_registration_24.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/rounded_6dp_stroke_1dp.xml b/app/src/main/res/drawable/rounded_6dp_stroke_1dp.xml new file mode 100644 index 00000000000..4f22c612ceb --- /dev/null +++ b/app/src/main/res/drawable/rounded_6dp_stroke_1dp.xml @@ -0,0 +1,10 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_donor_history.xml b/app/src/main/res/layout/activity_donor_history.xml new file mode 100644 index 00000000000..6d8ccffcdcb --- /dev/null +++ b/app/src/main/res/layout/activity_donor_history.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4654ff783bf..bc640cad911 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,7 +11,8 @@ + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize"> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_suggested_edits_tasks.xml b/app/src/main/res/layout/fragment_suggested_edits_tasks.xml index 7e270d132e8..88a6f5350c6 100644 --- a/app/src/main/res/layout/fragment_suggested_edits_tasks.xml +++ b/app/src/main/res/layout/fragment_suggested_edits_tasks.xml @@ -22,16 +22,6 @@ android:layout_height="wrap_content" android:focusableInTouchMode="true"> - - @@ -50,221 +39,258 @@ - - - + android:orientation="vertical"> - - - - - - - - - - - - - - - + + + + + + + + + + + + + android:layout_height="1dp" + android:layout_marginHorizontal="16dp" + android:background="?attr/list_divider" + android:visibility="gone"/> - - - + android:minHeight="60dp" + android:paddingVertical="16dp" + android:paddingStart="16dp" + android:paddingEnd="8dp" + android:background="?attr/selectableItemBackground" + android:clickable="true" + android:focusable="true" + android:visibility="gone"> + + + + + + + + + + + +