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

The redesign of comment details content #20798

Merged
Merged
Show file tree
Hide file tree
Changes from 13 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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.core.view.isGone
import org.wordpress.android.R
import org.wordpress.android.datasets.NotificationsTable
import org.wordpress.android.ui.comments.unified.CommentIdentifier
import org.wordpress.android.ui.comments.unified.CommentSource
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.ToastUtils
Expand All @@ -26,6 +27,9 @@ class NotificationCommentDetailFragment : CommentDetailFragment() {
}
}

override fun getCommentIdentifier(): CommentIdentifier =
CommentIdentifier.NotificationCommentIdentifier(mNote!!.id, mNote!!.commentId);

override fun handleHeaderVisibility() {
mBinding?.headerView?.isGone = true
}
Expand All @@ -44,8 +48,8 @@ class NotificationCommentDetailFragment : CommentDetailFragment() {
// This should not exist, we should clean that screen so a note without a site/comment can be displayed
mSite = createDummyWordPressComSite(mNote!!.siteId.toLong())
}
if (mBinding != null && mReplyBinding != null && mActionBinding != null) {
showComment(mBinding!!, mReplyBinding!!, mActionBinding!!, mSite!!, mComment, mNote)
if (mBinding != null && mReplyBinding != null) {
showComment(mBinding!!, mReplyBinding!!, mSite!!, mComment, mNote)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import android.os.Bundle
import androidx.core.view.isVisible
import org.wordpress.android.fluxc.model.CommentModel
import org.wordpress.android.fluxc.model.SiteModel
import org.wordpress.android.ui.comments.unified.CommentIdentifier
import org.wordpress.android.ui.comments.unified.CommentSource

/**
Expand All @@ -23,6 +24,10 @@ class SiteCommentDetailFragment : CommentDetailFragment() {
}
}

override fun getCommentIdentifier(): CommentIdentifier =
CommentIdentifier.SiteCommentIdentifier(mComment!!.id, mComment!!.remoteCommentId)


override fun handleHeaderVisibility() {
mBinding?.headerView?.isVisible = true
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@file:Suppress("DEPRECATION")

package org.wordpress.android.ui.comments.unified

import android.view.LayoutInflater
import android.view.View
import android.widget.PopupWindow
import org.wordpress.android.R
import org.wordpress.android.databinding.CommentActionsBinding
import org.wordpress.android.ui.comments.CommentDetailFragment
import org.wordpress.android.util.ToastUtils

object CommentActionPopupHandler {
@JvmStatic
fun show(anchorView: View, listener: CommentDetailFragment.OnActionClickListener?) {
val popupWindow = PopupWindow(anchorView.context, null, R.style.WordPress)
popupWindow.isOutsideTouchable = true
popupWindow.elevation = anchorView.context.resources.getDimension(R.dimen.popup_over_toolbar_elevation)
popupWindow.contentView = CommentActionsBinding
.inflate(LayoutInflater.from(anchorView.context))
.apply {
textUserInfo.setOnClickListener {
listener?.onUserInfoClicked()
popupWindow.dismiss()
}
textShare.setOnClickListener {
ToastUtils.showToast(it.context, "not yet implemented")
popupWindow.dismiss()
}
textEditComment.setOnClickListener {
listener?.onEditCommentClicked()
popupWindow.dismiss()
}
textChangeStatus.setOnClickListener {
ToastUtils.showToast(it.context, "not yet implemented")
popupWindow.dismiss()
}
}.root
popupWindow.showAsDropDown(anchorView)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.wordpress.android.ui.engagement

import java.io.Serializable

sealed class BottomSheetUiState {
data class UserProfileUiState(
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm wondering if we shouldn't mark some fields as nullable because not all are always available for all the cases. For example, if the user does not have an avatar URL, it should be null but not an empty string that has less straight meaning. When using the state in different places with not enough context, it looks like the state has to contain all the data (except for onSiteClickListener.)
Just leaving this note for your consideration :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I 100% agree with you, straight meaning is important for us to handle those fields correctly. I find many implementations use default values, but it'd be better if they are defined as nullable.

I suspect that when handling this data with Java in the past, there was a tendency to use defensive programming to avoid crashes. However, now with Kotlin, handling nullables has become much easier. I think we can find time to refactor this.

val userAvatarUrl: String,
Expand All @@ -12,7 +14,7 @@ sealed class BottomSheetUiState {
val siteId: Long,
val onSiteClickListener: ((siteId: Long, siteUrl: String, source: String) -> Unit)? = null,
val blogPreviewSource: String
) : BottomSheetUiState() {
) : BottomSheetUiState(), Serializable {
val hasSiteUrl: Boolean = siteUrl.isNotBlank()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,14 @@ class EngagedPeopleListFragment : Fragment() {
recycler.layoutManager = layoutManager

userProfileViewModel.onBottomSheetAction.observeEvent(viewLifecycleOwner) { state ->
var bottomSheet = childFragmentManager.findFragmentByTag(USER_PROFILE_BOTTOM_SHEET_TAG)
var bottomSheet = childFragmentManager.findFragmentByTag(UserProfileBottomSheetFragment.TAG)
as? UserProfileBottomSheetFragment

when (state) {
ShowBottomSheet -> {
if (bottomSheet == null) {
bottomSheet = UserProfileBottomSheetFragment.newInstance(USER_PROFILE_VM_KEY)
bottomSheet.show(childFragmentManager, USER_PROFILE_BOTTOM_SHEET_TAG)
bottomSheet.show(childFragmentManager, UserProfileBottomSheetFragment.TAG)
}
}

Expand Down Expand Up @@ -304,8 +304,6 @@ class EngagedPeopleListFragment : Fragment() {
private const val KEY_LIST_SCENARIO = "list_scenario"
private const val KEY_LIST_STATE = "list_state"

private const val USER_PROFILE_BOTTOM_SHEET_TAG = "USER_PROFILE_BOTTOM_SHEET_TAG"

@JvmStatic
fun newInstance(listScenario: ListScenario): EngagedPeopleListFragment {
val args = Bundle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class ListScenarioUtils @Inject constructor(
imageManager,
notificationsUtilsWrapper
)
headerNoteBlock.setIsComment(note.isCommentType)
headerNoteBlock.setReplyToComment(note.isCommentReplyType)

val spannable: Spannable = notificationsUtilsWrapper.getSpannableContentForRanges(headerNoteBlock.getHeader(0))
val spans = spannable.getSpans(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.ViewCompat
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStoreOwner
Expand All @@ -17,15 +15,14 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.wordpress.android.R
import org.wordpress.android.WordPress
import org.wordpress.android.databinding.UserProfileBottomSheetBinding
import org.wordpress.android.ui.engagement.BottomSheetUiState.UserProfileUiState
import org.wordpress.android.ui.utils.UiHelpers
import org.wordpress.android.util.PhotonUtils
import org.wordpress.android.util.PhotonUtils.Quality.HIGH
import org.wordpress.android.util.UrlUtils
import org.wordpress.android.util.WPAvatarUtils
import org.wordpress.android.util.image.ImageManager
import org.wordpress.android.util.image.ImageType.AVATAR_WITH_BACKGROUND
import org.wordpress.android.util.image.ImageType.BLAVATAR
import org.wordpress.android.util.image.ImageType
import org.wordpress.android.viewmodel.ResourceProvider
import javax.inject.Inject
import com.google.android.material.R as MaterialR
Expand All @@ -44,29 +41,44 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() {
lateinit var resourceProvider: ResourceProvider

private lateinit var viewModel: UserProfileViewModel
private var binding: UserProfileBottomSheetBinding? = null
private val state by lazy { requireArguments().getSerializable(USER_PROFILE_STATE) as? UserProfileUiState }

companion object {
const val USER_PROFILE_VIEW_MODEL_KEY = "user_profile_view_model_key"

fun newInstance(viewModelKey: String): UserProfileBottomSheetFragment {
val fragment = UserProfileBottomSheetFragment()
val bundle = Bundle()

bundle.putString(USER_PROFILE_VIEW_MODEL_KEY, viewModelKey)

fragment.arguments = bundle
private const val USER_PROFILE_VIEW_MODEL_KEY = "user_profile_view_model_key"
private const val USER_PROFILE_STATE = "user_profile_state"

const val TAG = "USER_PROFILE_BOTTOM_SHEET_TAG"

/**
* For displaying the user profile when users are from Likes
*/
fun newInstance(viewModelKey: String) = UserProfileBottomSheetFragment()
.apply {
arguments = Bundle().apply {
putString(USER_PROFILE_VIEW_MODEL_KEY, viewModelKey)
}
}

return fragment
}
/**
* For displaying the user profile when users are from Comments or Notifications
*/
@JvmStatic
fun newInstance(state: UserProfileUiState) = UserProfileBottomSheetFragment()
.apply {
arguments = Bundle().apply {
putSerializable(USER_PROFILE_STATE, state)
}
}
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.user_profile_bottom_sheet, container)
}
): View = UserProfileBottomSheetBinding.inflate(inflater, container, false)
.apply { binding = this }
.root

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -77,7 +89,8 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() {
viewModel = ViewModelProvider(parentFragment as ViewModelStoreOwner, viewModelFactory)
.get(vmKey, UserProfileViewModel::class.java)

initObservers(view)
initObservers()
state?.let { binding?.setup(it) }

dialog?.setOnShowListener { dialogInterface ->
val sheetDialog = dialogInterface as? BottomSheetDialog
Expand All @@ -94,68 +107,63 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() {
}
}

private fun initObservers(view: View) {
val userAvatar = view.findViewById<ImageView>(R.id.user_avatar)
val blavatar = view.findViewById<ImageView>(R.id.user_site_blavatar)
val userName = view.findViewById<TextView>(R.id.user_name)
val userLogin = view.findViewById<TextView>(R.id.user_login)
val userBio = view.findViewById<TextView>(R.id.user_bio)
val siteTitle = view.findViewById<TextView>(R.id.site_title)
val siteUrl = view.findViewById<TextView>(R.id.site_url)
val siteSectionHeader = view.findViewById<TextView>(R.id.site_section_header)
val siteData = view.findViewById<View>(R.id.site_data)

viewModel.bottomSheetUiState.observe(viewLifecycleOwner, { state ->
private fun initObservers() {
viewModel.bottomSheetUiState.observe(viewLifecycleOwner) { state ->
when (state) {
is UserProfileUiState -> {
val avatarSz = resourceProvider.getDimensionPixelSize(R.dimen.user_profile_bottom_sheet_avatar_sz)
val blavatarSz = resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium)

imageManager.loadIntoCircle(
userAvatar,
AVATAR_WITH_BACKGROUND,
WPAvatarUtils.rewriteAvatarUrl(state.userAvatarUrl, avatarSz)
)
userName.text = state.userName
userLogin.text = if (state.userLogin.isNotBlank()) {
getString(R.string.at_username, state.userLogin)
} else {
""
}
if (state.userBio.isNotBlank()) {
userBio.text = state.userBio
userBio.visibility = View.VISIBLE
} else {
userBio.visibility = View.GONE
}

imageManager.load(
blavatar,
BLAVATAR,
PhotonUtils.getPhotonImageUrl(state.blavatarUrl, blavatarSz, blavatarSz, HIGH)
)

if (state.hasSiteUrl) {
siteTitle.text = state.siteTitle
siteUrl.text = UrlUtils.getHost(state.siteUrl)
siteData.setOnClickListener {
state.onSiteClickListener?.invoke(
state.siteId,
state.siteUrl,
state.blogPreviewSource
)
}
siteSectionHeader.visibility = View.VISIBLE
blavatar.visibility = View.VISIBLE
siteData.visibility = View.VISIBLE
} else {
siteSectionHeader.visibility = View.GONE
blavatar.visibility = View.GONE
siteData.visibility = View.GONE
}
binding?.setup(state)
}
}
})
}
}

private fun UserProfileBottomSheetBinding.setup(state: UserProfileUiState) {
Copy link
Contributor Author

@jarvislin jarvislin May 15, 2024

Choose a reason for hiding this comment

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

I only moved the code snippets to this function, the logic is not changed.

val avatarSz =
resourceProvider.getDimensionPixelSize(R.dimen.user_profile_bottom_sheet_avatar_sz)
val blavatarSz = resourceProvider.getDimensionPixelSize(R.dimen.avatar_sz_medium)

imageManager.loadIntoCircle(
userAvatar,
ImageType.AVATAR_WITH_BACKGROUND,
WPAvatarUtils.rewriteAvatarUrl(state.userAvatarUrl, avatarSz)
)
userName.text = state.userName
userLogin.text = if (state.userLogin.isNotBlank()) {
getString(R.string.at_username, state.userLogin)
} else {
""
}
if (state.userBio.isNotBlank()) {
userBio.text = state.userBio
userBio.visibility = View.VISIBLE
} else {
userBio.visibility = View.GONE
}

imageManager.load(
userSiteBlavatar,
ImageType.BLAVATAR,
PhotonUtils.getPhotonImageUrl(state.blavatarUrl, blavatarSz, blavatarSz, PhotonUtils.Quality.HIGH)
)

if (state.hasSiteUrl) {
siteTitle.text = state.siteTitle
siteUrl.text = UrlUtils.getHost(state.siteUrl)
siteData.setOnClickListener {
state.onSiteClickListener?.invoke(
state.siteId,
state.siteUrl,
state.blogPreviewSource
)
}
siteSectionHeader.visibility = View.VISIBLE
userSiteBlavatar.visibility = View.VISIBLE
siteData.visibility = View.VISIBLE
} else {
siteSectionHeader.visibility = View.GONE
userSiteBlavatar.visibility = View.GONE
siteData.visibility = View.GONE
}
}

override fun onAttach(context: Context) {
Expand All @@ -167,4 +175,9 @@ class UserProfileBottomSheetFragment : BottomSheetDialogFragment() {
super.onCancel(dialog)
viewModel.onBottomSheetCancelled()
}

override fun onDestroyView() {
super.onDestroyView()
binding = null
}
}
Loading
Loading