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

Implement Gravatar QuickEditor #21312

Merged
merged 10 commits into from
Nov 15, 2024
4 changes: 3 additions & 1 deletion WordPress/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ android {
buildConfigField "boolean", "VOICE_TO_CONTENT", "false"
buildConfigField "boolean", "READER_FLOATING_BUTTON", "false"
buildConfigField "boolean", "ENABLE_SELF_HOSTED_USERS", "false"
buildConfigField "boolean", "GRAVATAR_QUICK_EDITOR", "true"

// Override these constants in jetpack product flavor to enable/ disable features
buildConfigField "boolean", "ENABLE_SITE_CREATION", "true"
Expand Down Expand Up @@ -406,6 +407,7 @@ dependencies {
}
implementation(libs.wordpress.persistent.edittext)
implementation("$gradle.ext.gravatarBinaryPath:${libs.versions.gravatar.get()}")
implementation("$gradle.ext.gravatarQuickEditorBinaryPath:${libs.versions.gravatar.get()}")

implementation(libs.google.play.app.update)

Expand Down Expand Up @@ -454,7 +456,7 @@ dependencies {
implementation(libs.apache.commons.text)
implementation(libs.airbnb.lottie.main)
implementation(libs.facebook.shimmer)
implementation(libs.yalantis.ucrop) {
implementation(libs.automattic.ucrop) {
exclude group: 'androidx.core', module: 'core'
exclude group: 'androidx.constraintlayout', module: 'constraintlayout'
exclude group: 'androidx.appcompat', module: 'appcompat'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.widget.NestedScrollView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.gravatar.AvatarQueryOptions
import com.gravatar.AvatarUrl
import com.gravatar.quickeditor.GravatarQuickEditor
import com.gravatar.quickeditor.ui.editor.AuthenticationMethod
import com.gravatar.quickeditor.ui.editor.AvatarPickerContentLayout
import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorParams
import com.gravatar.services.AvatarService
import com.gravatar.services.GravatarResult
import com.gravatar.types.Email
Expand Down Expand Up @@ -70,6 +76,7 @@ import org.wordpress.android.util.StringUtils
import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.WPAvatarUtils
import org.wordpress.android.util.WPMediaUtils
import org.wordpress.android.util.config.GravatarQuickEditorFeatureConfig
import org.wordpress.android.util.extensions.getColorFromAttribute
import org.wordpress.android.util.extensions.redirectContextClickToLongPressListener
import org.wordpress.android.util.image.ImageManager
Expand Down Expand Up @@ -134,6 +141,9 @@ class SignupEpilogueFragment : LoginBaseFormFragment<SignupEpilogueListener?>(),
@Inject
lateinit var mAvatarService: AvatarService

@Inject
lateinit var gravatarQuickEditorFeatureConfig: GravatarQuickEditorFeatureConfig

@LayoutRes
override fun getContentLayout(): Int {
return 0 // no content layout; entire view is inflated in createMainView
Expand Down Expand Up @@ -163,7 +173,33 @@ class SignupEpilogueFragment : LoginBaseFormFragment<SignupEpilogueListener?>(),
headerAvatarLayout.isEnabled = mIsEmailSignup
headerAvatarLayout.setOnClickListener {
mUnifiedLoginTracker.trackClick(UnifiedLoginTracker.Click.SELECT_AVATAR)
mMediaPickerLauncher.showGravatarPicker(this@SignupEpilogueFragment)
if (gravatarQuickEditorFeatureConfig.isEnabled()) {
GravatarQuickEditor.show(
fragment = this,
gravatarQuickEditorParams = GravatarQuickEditorParams {
email = Email(mEmailAddress)
avatarPickerContentLayout = AvatarPickerContentLayout.Horizontal
},
authenticationMethod = AuthenticationMethod.Bearer(mAccount.accessToken.orEmpty()),
onAvatarSelected = {
mPhotoUrl = AvatarUrl(
email = Email(mEmailAddress),
avatarQueryOptions = AvatarQueryOptions {
preferredSize = resources.getDimensionPixelSize(R.dimen.avatar_sz_large)
}
).url(cacheBuster = System.currentTimeMillis().toString()).toString()
mImageManager.loadIntoCircle(
mHeaderAvatar,
ImageType.AVATAR_WITHOUT_BACKGROUND,
mPhotoUrl
)
mHeaderAvatarAdd.visibility = View.GONE
mIsAvatarAdded = true
},
)
} else {
mMediaPickerLauncher.showGravatarPicker(this@SignupEpilogueFragment)
}
}
headerAvatarLayout.setOnLongClickListener {
ToastUtils.showToast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.content.ContextCompat
import androidx.lifecycle.compose.LocalLifecycleOwner
import org.wordpress.android.ui.compose.theme.AppThemeM2
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.camera.core.Preview as CameraPreview

@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.gravatar.quickeditor.GravatarQuickEditor
import com.gravatar.quickeditor.ui.editor.AuthenticationMethod
import com.gravatar.quickeditor.ui.editor.AvatarPickerContentLayout
import com.gravatar.quickeditor.ui.editor.GravatarQuickEditorParams
import com.gravatar.services.AvatarService
import com.gravatar.services.GravatarResult
import com.gravatar.types.Email
Expand Down Expand Up @@ -82,6 +86,7 @@ import org.wordpress.android.util.ToastUtils
import org.wordpress.android.util.ToastUtils.Duration.SHORT
import org.wordpress.android.util.WPMediaUtils
import org.wordpress.android.util.config.DomainManagementFeatureConfig
import org.wordpress.android.util.config.GravatarQuickEditorFeatureConfig
import org.wordpress.android.util.config.QRCodeAuthFlowFeatureConfig
import org.wordpress.android.util.config.RecommendTheAppFeatureConfig
import org.wordpress.android.util.extensions.getColorFromAttribute
Expand Down Expand Up @@ -131,6 +136,9 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {
@Inject
lateinit var qrCodeAuthFlowFeatureConfig: QRCodeAuthFlowFeatureConfig

@Inject
lateinit var gravatarQuickEditorFeatureConfig: GravatarQuickEditorFeatureConfig

@Inject
lateinit var jetpackBrandingUtils: JetpackBrandingUtils

Expand All @@ -156,6 +164,7 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {

private val shouldShowDomainButton
get() = BuildConfig.IS_JETPACK_APP && domainManagementFeatureConfig.isEnabled() && accountStore.hasAccessToken()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
(requireActivity().application as WordPress).component().inject(this)
Expand Down Expand Up @@ -192,7 +201,21 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {

val showPickerListener = OnClickListener {
AnalyticsTracker.track(ME_GRAVATAR_TAPPED)
showPhotoPickerForGravatar()
if (gravatarQuickEditorFeatureConfig.isEnabled()) {
GravatarQuickEditor.show(
fragment = this@MeFragment,
gravatarQuickEditorParams = GravatarQuickEditorParams {
email = Email(accountStore.account.email)
avatarPickerContentLayout = AvatarPickerContentLayout.Horizontal
},
authenticationMethod = AuthenticationMethod.Bearer(accountStore.accessToken.orEmpty()),
onAvatarSelected = {
loadAvatar(null, true)
},
)
} else {
showPhotoPickerForGravatar()
}
}
avatarContainer.setOnClickListener(showPickerListener)
rowMyProfile.setOnClickListener {
Expand Down Expand Up @@ -470,11 +493,12 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {
isUpdatingGravatar = isUpdating
}

private fun MeFragmentBinding.loadAvatar(injectFilePath: String?) {
private fun MeFragmentBinding.loadAvatar(injectFilePath: String?, forceRefresh: Boolean = false) {
val newAvatarUploaded = !injectFilePath.isNullOrEmpty()
val avatarUrl = meGravatarLoader.constructGravatarUrl(accountStore.account.avatarUrl)
val newAvatarSelected = newAvatarUploaded || forceRefresh
meGravatarLoader.load(
newAvatarUploaded,
newAvatarSelected,
avatarUrl,
injectFilePath,
meAvatar,
Expand Down Expand Up @@ -508,7 +532,7 @@ class MeFragment : Fragment(R.layout.me_fragment), OnScrollToTopListener {
resource: Drawable,
model: Any?
) {
if (newAvatarUploaded && resource is BitmapDrawable) {
if (newAvatarSelected && resource is BitmapDrawable) {
var bitmap = resource.bitmap
// create a copy since the original bitmap may by automatically recycled
bitmap = bitmap.copy(bitmap.config, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ class MeGravatarLoader @Inject constructor(
private val resourseProvider: ResourceProvider
) {
fun load(
newAvatarUploaded: Boolean,
newAvatarSelected: Boolean,
avatarUrl: String,
injectFilePath: String?,
imageView: ImageView,
imageType: ImageType,
listener: RequestListener<Drawable>? = null
) {
if (newAvatarUploaded) {
if (newAvatarSelected) {
// invalidate the specific gravatar entry from the bitmap cache. It will be updated via the injected
// request cache.
WordPress.getBitmapCache().removeSimilar(avatarUrl)
Expand All @@ -45,19 +45,22 @@ class MeGravatarLoader @Inject constructor(
imageManager.loadIntoCircle(
imageView,
imageType,
if (newAvatarUploaded && injectFilePath != null) {
if (newAvatarSelected && injectFilePath != null) {
injectFilePath
} else {
avatarUrl
// If new avatar selected we force refresh the avatar
val constructGravatarUrl = constructGravatarUrl(avatarUrl, newAvatarSelected)
constructGravatarUrl
},
listener,
appPrefsWrapper.avatarVersion
)
}
}

fun constructGravatarUrl(rawAvatarUrl: String): String {
fun constructGravatarUrl(rawAvatarUrl: String, forceRefresh: Boolean = false): String {
val avatarSz = resourseProvider.getDimensionPixelSize(R.dimen.avatar_sz_extra_small)
return WPAvatarUtils.rewriteAvatarUrl(rawAvatarUrl, avatarSz)
val cacheBuster = if (forceRefresh) System.currentTimeMillis().toString() else null
return WPAvatarUtils.rewriteAvatarUrl(rawAvatarUrl, avatarSz, cacheBuster)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
Expand All @@ -37,6 +36,7 @@ import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
Expand Down Expand Up @@ -283,28 +283,22 @@ private fun ReadingPreferencesPreviewFeedback(
end = endIndex,
)

addStringAnnotation(
tag = "url",
annotation = "feedback",
addLink(
clickable = LinkAnnotation.Clickable(
tag = "url",
linkInteractionListener = {
onSendFeedbackClick()
}
),
start = startIndex,
end = endIndex,
)
}

val buttonLabel = stringResource(R.string.reader_preferences_screen_preview_text_feedback_label)
@Suppress("DEPRECATION")
ClickableText(
Text(
text = annotatedString,
style = textStyle,
onClick = { offset ->
annotatedString.getStringAnnotations(tag = "url", start = offset, end = offset)
.firstOrNull()
?.let { annotation ->
if (annotation.item == "feedback") {
onSendFeedbackClick()
}
}
},
modifier = Modifier.semantics {
onClick(
label = buttonLabel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ private fun Loaded(uiState: UiState.Loaded) {

Column(
modifier = Modifier
.animateItem(fadeInSpec = null, fadeOutSpec = null)
.animateItem()
.fillMaxWidth()
.padding(
top = Margin.Large.value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class WPAvatarUtils {
private WPAvatarUtils() {
throw new IllegalStateException("Utility class");
}

public static final DefaultAvatarOption DEFAULT_AVATAR = MysteryPerson.INSTANCE;

/**
Expand All @@ -35,7 +36,8 @@ private WPAvatarUtils() {
* @return the fixed url
*/
public static String rewriteAvatarUrl(@NonNull final String imageUrl, int avatarSz,
@Nullable DefaultAvatarOption defaultImage) {
@Nullable DefaultAvatarOption defaultImage,
@Nullable String cacheBuster) {
if (TextUtils.isEmpty(imageUrl)) {
return "";
}
Expand All @@ -47,17 +49,27 @@ public static String rewriteAvatarUrl(@NonNull final String imageUrl, int avatar
try {
return new AvatarUrl(new URL(imageUrl),
new AvatarQueryOptions.Builder()
.setPreferredSize(avatarSz)
.setDefaultAvatarOption(defaultImage)
.build()
).url(null).toString();
.setPreferredSize(avatarSz)
.setDefaultAvatarOption(defaultImage)
.build()
).url(cacheBuster).toString();
} catch (MalformedURLException | IllegalArgumentException e) {
return "";
}
}
}

public static String rewriteAvatarUrl(@NonNull final String imageUrl, int avatarSz) {
return rewriteAvatarUrl(imageUrl, avatarSz, DEFAULT_AVATAR);
return rewriteAvatarUrl(imageUrl, avatarSz, DEFAULT_AVATAR, null);
}

public static String rewriteAvatarUrl(@NonNull final String imageUrl, int avatarSz,
@Nullable DefaultAvatarOption defaultImage) {
return rewriteAvatarUrl(imageUrl, avatarSz, defaultImage, null);
}

public static String rewriteAvatarUrl(@NonNull final String imageUrl, int avatarSz,
@Nullable String cacheBuster) {
return rewriteAvatarUrl(imageUrl, avatarSz, DEFAULT_AVATAR, cacheBuster);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.wordpress.android.util.config

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

@Feature(GravatarQuickEditorFeatureConfig.GRAVATAR_QUICK_EDITOR_REMOTE_FIELD, true)
class GravatarQuickEditorFeatureConfig @Inject constructor(appConfig: AppConfig) : FeatureConfig(
appConfig,
BuildConfig.GRAVATAR_QUICK_EDITOR,
GRAVATAR_QUICK_EDITOR_REMOTE_FIELD
) {
override fun isEnabled(): Boolean {
return super.isEnabled() && BuildConfig.GRAVATAR_QUICK_EDITOR
}

companion object {
const val GRAVATAR_QUICK_EDITOR_REMOTE_FIELD = "gravatar_quick_editor"
}
}
1 change: 1 addition & 0 deletions config/gradle/included_builds.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ gradle.ext.aztecAndroidGlideLoaderPath = "org.wordpress.aztec:glide-loader"
gradle.ext.aztecAndroidPicassoLoaderPath = "org.wordpress.aztec:picasso-loader"
gradle.ext.aboutAutomatticBinaryPath = "com.automattic:about"
gradle.ext.gravatarBinaryPath = "com.gravatar:gravatar"
gradle.ext.gravatarQuickEditorBinaryPath = "com.gravatar:gravatar-quickeditor"

def localBuilds = new File("${rootDir}/local-builds.gradle")
if (localBuilds.exists()) {
Expand Down
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ wordpress-lint = '2.1.0'
wordpress-persistent-edittext = '1.0.2'
wordpress-rs = 'trunk-50f703a7f677084157d02f05d4d477d7eaf960b1'
wordpress-utils = '3.14.0'
yalantis-ucrop = '2.2.9'
automattic-ucrop = '2.2.10'
zendesk = '5.1.2'

[libraries]
Expand Down Expand Up @@ -260,7 +260,7 @@ wordpress-lint = { group = "org.wordpress", name = "lint", version.ref = "wordpr
wordpress-persistent-edittext = { group = "org.wordpress", name = "persistentedittext", version.ref = "wordpress-persistent-edittext" }
wordpress-rs-android = { group = "rs.wordpress.api", name = "android", version.ref = "wordpress-rs" }
wordpress-utils = { group = "org.wordpress", name = "utils", version.ref = "wordpress-utils" }
yalantis-ucrop = { group = "com.github.yalantis", name = "ucrop", version.ref = "yalantis-ucrop" }
automattic-ucrop = { group = "com.automattic", name = "ucrop", version.ref = "automattic-ucrop" }
zendesk-support = { group = "com.zendesk", name = "support", version.ref = "zendesk" }

[plugins]
Expand Down
2 changes: 1 addition & 1 deletion libs/image-editor/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.main)
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
implementation(libs.androidx.lifecycle.livedata.core)
implementation(libs.yalantis.ucrop) {
implementation(libs.automattic.ucrop) {
exclude group: 'com.squareup.okhttp3'
exclude group: 'androidx.core', module: 'core'
exclude group: 'androidx.constraintlayout', module: 'constraintlayout'
Expand Down