Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Reader] Implement announcement card #20846

Merged
merged 18 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ android {
buildConfigField "boolean", "READER_READING_PREFERENCES", "false"
buildConfigField "boolean", "READER_READING_PREFERENCES_FEEDBACK", "false"
buildConfigField "boolean", "READER_TAGS_FEED", "false"
buildConfigField "boolean", "READER_ANNOUNCEMENT_CARD", "false"

// Override these constants in jetpack product flavor to enable/ disable features
buildConfigField "boolean", "ENABLE_SITE_CREATION", "true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ public enum DeletablePrefKey implements PrefKey {
SHOULD_HIDE_DYNAMIC_CARD,
PINNED_SITE_IDS,
READER_READING_PREFERENCES_JSON,
SHOULD_SHOW_READER_ANNOUNCEMENT_CARD,
}

/**
Expand Down Expand Up @@ -1780,6 +1781,14 @@ public static void setPinnedSiteLocalIds(@NonNull final String ids) {
setString(DeletablePrefKey.PINNED_SITE_IDS, ids);
}

public static boolean getShouldShowReaderAnnouncementCard() {
return prefs().getBoolean(DeletablePrefKey.SHOULD_SHOW_READER_ANNOUNCEMENT_CARD.name(), true);
}

public static void setShouldShowReaderAnnouncementCard(final boolean shouldShow) {
prefs().edit().putBoolean(DeletablePrefKey.SHOULD_SHOW_READER_ANNOUNCEMENT_CARD.name(), shouldShow).apply();
}

@Nullable
public static String getReaderReadingPreferencesJson() {
return getString(DeletablePrefKey.READER_READING_PREFERENCES_JSON, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,11 @@ class AppPrefsWrapper @Inject constructor() {

fun setBookmarkPostsPseudoIdsUpdated() = AppPrefs.setBookmarkPostsPseudoIdsUpdated()

fun shouldShowReaderAnnouncementCard(): Boolean = AppPrefs.getShouldShowReaderAnnouncementCard()

fun setShouldShowReaderAnnouncementCard(shouldShow: Boolean) =
AppPrefs.setShouldShowReaderAnnouncementCard(shouldShow)

fun getAllPrefs(): Map<String, Any?> = AppPrefs.getAllPrefs()

fun setString(prefKey: PrefKey, value: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import org.wordpress.android.ui.reader.subfilter.SubfilterCategory
import org.wordpress.android.ui.reader.subfilter.SubfilterListItem
import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel
import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState
import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCard
import org.wordpress.android.ui.reader.views.compose.ReaderTopAppBar
import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType
import org.wordpress.android.ui.utils.UiHelpers
Expand Down Expand Up @@ -99,6 +100,7 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding = ReaderFragmentLayoutBinding.bind(view).apply {
initTopAppBar()
initAnnouncementCard()
initViewModel(savedInstanceState)
}
}
Expand Down Expand Up @@ -181,6 +183,23 @@ class ReaderFragment : Fragment(R.layout.reader_fragment_layout), ScrollableView
}
}

private fun ReaderFragmentLayoutBinding.initAnnouncementCard() {
readerAnnouncementCardComposeView.apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
val announcementCardUiState by viewModel.announcementCardState.observeAsState()
val state = announcementCardUiState ?: return@setContent
AppTheme {
ReaderAnnouncementCard(
shouldShow = state.shouldShow,
items = state.items,
onAnnouncementCardDoneClick = { viewModel.onAnnouncementCardDoneClick() }
)
}
}
}
}

private fun ReaderFragmentLayoutBinding.initViewModel(savedInstanceState: Bundle?) {
viewModel = ViewModelProvider(this@ReaderFragment, viewModelFactory)[ReaderViewModel::class.java]
startReaderViewModel(savedInstanceState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
import org.wordpress.android.BuildConfig
import org.wordpress.android.R
import org.wordpress.android.analytics.AnalyticsTracker
import org.wordpress.android.fluxc.store.AccountStore
import org.wordpress.android.fluxc.store.QuickStartStore
import org.wordpress.android.fluxc.store.QuickStartStore.QuickStartTask
Expand All @@ -43,6 +44,7 @@ import org.wordpress.android.ui.reader.utils.DateProvider
import org.wordpress.android.ui.reader.utils.ReaderTopBarMenuHelper
import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState
import org.wordpress.android.ui.reader.viewmodels.ReaderViewModel.ReaderUiState.ContentUiState.TabUiState
import org.wordpress.android.ui.reader.views.compose.ReaderAnnouncementCardItemData
import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterSelectedItem
import org.wordpress.android.ui.reader.views.compose.filter.ReaderFilterType
import org.wordpress.android.ui.utils.UiString
Expand All @@ -51,6 +53,7 @@ import org.wordpress.android.util.JetpackBrandingUtils
import org.wordpress.android.util.QuickStartUtils
import org.wordpress.android.util.SnackbarSequencer
import org.wordpress.android.util.UrlUtilsWrapper
import org.wordpress.android.util.config.ReaderAnnouncementCardFeatureConfig
import org.wordpress.android.util.config.ReaderTagsFeedFeatureConfig
import org.wordpress.android.util.distinct
import org.wordpress.android.viewmodel.Event
Expand Down Expand Up @@ -81,6 +84,7 @@ class ReaderViewModel @Inject constructor(
private val urlUtilsWrapper: UrlUtilsWrapper,
private val readerTagsFeedFeatureConfig: ReaderTagsFeedFeatureConfig,
// todo: annnmarie removed this private val getFollowedTagsUseCase: GetFollowedTagsUseCase
private val readerAnnouncementCardFeatureConfig: ReaderAnnouncementCardFeatureConfig,
) : ScopedViewModel(mainDispatcher) {
private var initialized: Boolean = false
private var wasPaused: Boolean = false
Expand All @@ -93,6 +97,9 @@ class ReaderViewModel @Inject constructor(
private val _topBarUiState = MutableLiveData<TopBarUiState>()
val topBarUiState: LiveData<TopBarUiState> = _topBarUiState.distinct()

private val _announcementCardState = MutableLiveData<AnnouncementCardUiState>()
val announcementCardState: LiveData<AnnouncementCardUiState> = _announcementCardState

private val _updateTags = MutableLiveData<Event<Unit>>()
val updateTags: LiveData<Event<Unit>> = _updateTags

Expand Down Expand Up @@ -125,6 +132,7 @@ class ReaderViewModel @Inject constructor(
if (initialized) return
loadTabs(savedInstanceState)
if (jetpackBrandingUtils.shouldShowJetpackPoweredBottomSheet()) showJetpackPoweredBottomSheet()
loadAnnouncementCard()
}

fun onSaveInstanceState(out: Bundle) {
Expand All @@ -138,6 +146,40 @@ class ReaderViewModel @Inject constructor(
// _showJetpackPoweredBottomSheet.value = Event(true)
}

private fun loadAnnouncementCard() {
val items = mutableListOf<ReaderAnnouncementCardItemData>()

if (readerTagsFeedFeatureConfig.isEnabled()) {
items.add(
ReaderAnnouncementCardItemData(
iconRes = R.drawable.ic_reader_tag,
titleRes = R.string.reader_announcement_card_tags_stream_title,
descriptionRes = R.string.reader_announcement_card_tags_stream_description,
)
)
}

items.add(
ReaderAnnouncementCardItemData(
iconRes = R.drawable.ic_reader_preferences,
titleRes = R.string.reader_announcement_card_reading_preferences_title,
descriptionRes = R.string.reader_announcement_card_reading_preferences_description,
)
)

_announcementCardState.value = AnnouncementCardUiState(
shouldShow = readerAnnouncementCardFeatureConfig.isEnabled() &&
appPrefsWrapper.shouldShowReaderAnnouncementCard(),
items = items,
)
}

fun onAnnouncementCardDoneClick() {
readerTracker.track(AnalyticsTracker.Stat.READER_ANNOUNCEMENT_CARD_DISMISSED)
appPrefsWrapper.setShouldShowReaderAnnouncementCard(false)
loadAnnouncementCard()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

np: maybe I'd rename this something like setAnnouncementCardState, since in the end we use it for that and in this scope here for example we are not actually loading the cards but setting the "hide" state. wdyt?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, that threw me off as well when I worked on the code yesterday. Maybe updateAnnouncementCard would be better.

}

@JvmOverloads
fun loadTabs(savedInstanceState: Bundle? = null) {
launch {
Expand Down Expand Up @@ -563,6 +605,11 @@ class ReaderViewModel @Inject constructor(
val duration: Int = QUICK_START_PROMPT_DURATION
)

data class AnnouncementCardUiState(
val shouldShow: Boolean,
val items: List<ReaderAnnouncementCardItemData>,
)

companion object {
private const val QUICK_START_PROMPT_DURATION = 5000
private const val FILTER_UPDATE_DELAY = 50L
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package org.wordpress.android.ui.reader.views.compose

import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandIn
import androidx.compose.animation.shrinkOut
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.wordpress.android.R
import org.wordpress.android.designsystem.footnote
import org.wordpress.android.ui.compose.theme.AppColor
import org.wordpress.android.ui.compose.theme.AppTheme
import org.wordpress.android.ui.compose.unit.Margin

@Composable
fun ReaderAnnouncementCard(
shouldShow: Boolean,
items: List<ReaderAnnouncementCardItemData>,
onAnnouncementCardDoneClick: () -> Unit,
) {
val primaryColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black
val secondaryColor = if (isSystemInDarkTheme()) AppColor.Black else AppColor.White
AnimatedVisibility(
visible = shouldShow,
enter = expandIn(),
exit = shrinkOut(
shrinkTowards = Alignment.TopCenter,
),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(Margin.ExtraLarge.value),
verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value),
) {
// Title
Text(
text = stringResource(R.string.reader_announcement_card_title),
style = MaterialTheme.typography.labelLarge,
color = primaryColor,
)
// Items
Column(
modifier = Modifier
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(Margin.ExtraLarge.value)
) {
items.forEach {
ReaderAnnouncementCardItem(it)
}
}
// Done button
Button(
modifier = Modifier
.fillMaxWidth(),
onClick = { onAnnouncementCardDoneClick() },
elevation = ButtonDefaults.elevation(0.dp),
colors = ButtonDefaults.buttonColors(
backgroundColor = primaryColor,
),
) {
Text(
text = stringResource(id = R.string.reader_btn_done),
color = secondaryColor,
style = MaterialTheme.typography.labelLarge,
)
}
}
}
}

@Composable
private fun ReaderAnnouncementCardItem(data: ReaderAnnouncementCardItemData) {
val primaryColor = if (isSystemInDarkTheme()) AppColor.White else AppColor.Black
val secondaryColor = if (isSystemInDarkTheme()) AppColor.Black else AppColor.White
Row(
modifier = Modifier
.fillMaxWidth()
.defaultMinSize(minWidth = 54.dp, minHeight = 54.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val iconBackgroundColor = primaryColor
Icon(
modifier = Modifier
.padding(
start = Margin.Large.value,
end = Margin.Large.value
)
.drawBehind {
drawCircle(
color = iconBackgroundColor,
radius = this.size.maxDimension,
)
},
painter = painterResource(data.iconRes),
tint = secondaryColor,
contentDescription = null
)
Column(verticalArrangement = Arrangement.Center) {
Text(
modifier = Modifier.padding(
start = Margin.Large.value,
),
text = stringResource(data.titleRes),
style = MaterialTheme.typography.labelLarge,
color = primaryColor,
)
val secondaryElementColor = primaryColor.copy(
alpha = 0.6F
)
Text(
modifier = Modifier.padding(
start = Margin.Large.value,
),
text = stringResource(data.descriptionRes),
style = MaterialTheme.typography.footnote,
color = secondaryElementColor,
)
}
}
}

data class ReaderAnnouncementCardItemData(
@DrawableRes val iconRes: Int,
@StringRes val titleRes: Int,
@StringRes val descriptionRes: Int,
)


@Preview
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun ReaderTagsFeedPostListItemPreview() {
AppTheme {
Box(
modifier = Modifier
.fillMaxWidth()
) {
ReaderAnnouncementCard(
shouldShow = false,
items = listOf(
ReaderAnnouncementCardItemData(
iconRes = R.drawable.ic_wifi_off_24px,
titleRes = R.string.reader_tags_display_name,
descriptionRes = R.string.reader_tags_feed_loading_error_description,
),
ReaderAnnouncementCardItemData(
iconRes = R.drawable.ic_wifi_off_24px,
titleRes = R.string.reader_tags_display_name,
descriptionRes = R.string.reader_tags_feed_loading_error_description,
),
ReaderAnnouncementCardItemData(
iconRes = R.drawable.ic_wifi_off_24px,
titleRes = R.string.reader_tags_display_name,
descriptionRes = R.string.reader_tags_feed_loading_error_description,
),
),
onAnnouncementCardDoneClick = {},
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ class TagsAndCategoriesUseCase
}

private fun getIcon(type: String) =
if (type == "tag") R.drawable.ic_tag_white_24dp else R.drawable.ic_folder_white_24dp
if (type == "tag") R.drawable.ic_reader_tag else R.drawable.ic_folder_white_24dp

private fun onLinkClick() {
analyticsTracker.track(AnalyticsTracker.Stat.STATS_TAGS_AND_CATEGORIES_VIEW_MORE_TAPPED)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package org.wordpress.android.util.config

import org.wordpress.android.BuildConfig
import org.wordpress.android.annotation.Feature
import javax.inject.Inject

private const val READER_ANNOUNCEMENT_CARD_REMOTE_FIELD = "reader_announcement_card"
@Feature(remoteField = READER_ANNOUNCEMENT_CARD_REMOTE_FIELD, defaultValue = false)
class ReaderAnnouncementCardFeatureConfig @Inject constructor(
appConfig: AppConfig
) : FeatureConfig(
appConfig,
BuildConfig.READER_ANNOUNCEMENT_CARD,
READER_ANNOUNCEMENT_CARD_REMOTE_FIELD,
)
Loading
Loading