From f665228683563172d6b58d9c97718b3a6735913e Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:34:56 -0600 Subject: [PATCH 1/3] Activity Log: Show MCP agent metadata in list and detail views Display "via {client}" label (e.g. "via Claude") in the Activity Log when an entry was performed by an MCP agent. Adds isMCPAgent and mcpClient fields through the full data pipeline: API response parsing, database persistence with migration, and UI rendering in both the list and detail views. Ref: AIINT-290 Co-Authored-By: Claude Opus 4.6 --- .../detail/ActivityLogDetailFragment.kt | 1 + .../detail/ActivityLogDetailModel.kt | 1 + .../activitylog/list/ActivityLogListItem.kt | 3 +- .../activitylog/list/EventItemViewHolder.kt | 11 +++++ .../activitylog/ActivityLogDetailViewModel.kt | 30 +++++++++---- .../activitylog/ActivityLogViewModel.kt | 14 +++++- .../res/layout/activity_log_item_detail.xml | 14 +++++- .../layout/activity_log_list_event_item.xml | 44 ++++++++++++++++--- WordPress/src/main/res/values/strings.xml | 2 + .../fluxc/model/activity/ActivityLogModel.kt | 4 +- .../wpcom/activity/ActivityLogRestClient.kt | 8 +++- .../rest/wpcom/dashboard/CardsRestClient.kt | 4 +- .../fluxc/persistence/ActivityLogSqlUtils.kt | 13 ++++-- .../fluxc/persistence/WellSqlConfig.kt | 7 ++- .../rest/wpcom/activity/ActivityFixtures.kt | 4 +- .../wpcom/dashboard/CardsRestClientTest.kt | 4 +- .../fluxc/store/dashboard/CardsStoreTest.kt | 4 +- 17 files changed, 137 insertions(+), 31 deletions(-) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt index ee3a5268b50e..f02c2236eb06 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailFragment.kt @@ -185,6 +185,7 @@ class ActivityLogDetailFragment : Fragment(R.layout.activity_log_item_detail) { setActorIcon(activityLogModel?.actorIconUrl, activityLogModel?.showJetpackIcon) uiHelpers.setTextOrHide(activityActorName, activityLogModel?.actorName) uiHelpers.setTextOrHide(activityActorRole, activityLogModel?.actorRole) + uiHelpers.setTextOrHide(activityActorMetadata, activityLogModel?.actorMetadata) val spannable = activityLogModel?.content?.let { notificationsUtilsWrapper.getSpannableContentForRanges( diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailModel.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailModel.kt index e9fd7bf83779..e49b1a8df00d 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/detail/ActivityLogDetailModel.kt @@ -10,6 +10,7 @@ data class ActivityLogDetailModel( val isRewindButtonVisible: Boolean, val actorName: String? = null, val actorRole: String? = null, + val actorMetadata: String? = null, val content: FormattableContent? = null, val summary: String? = null, val createdDate: String = "", diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt index 33f963428f4e..c71d3549a78b 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogListItem.kt @@ -31,7 +31,8 @@ sealed class ActivityLogListItem(val type: ViewType) { val date: Date, override val isButtonVisible: Boolean, val buttonIcon: Icon, - val isRestoreHidden: Boolean + val isRestoreHidden: Boolean, + val actorMetadata: String? = null ) : ActivityLogListItem(EVENT), IActionableItem { val formattedDate: String = date.toFormattedDateString() val icon = Icon.fromValue(gridIcon) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt index d01fc9093468..b38964fa80a2 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt @@ -25,6 +25,8 @@ class EventItemViewHolder( private val thumbnail: ImageView = itemView.findViewById(R.id.action_icon) private val container: View = itemView.findViewById(R.id.activity_content_container) private val actionButton: ImageButton = itemView.findViewById(R.id.action_button) + private val metadataSeparator: TextView = itemView.findViewById(R.id.action_metadata_separator) + private val metadata: TextView = itemView.findViewById(R.id.action_metadata) override fun updateChanges(bundle: Bundle) { if (bundle.containsKey(ActivityLogDiffCallback.LIST_ITEM_BUTTON_VISIBILITY_KEY)) { @@ -41,6 +43,15 @@ class EventItemViewHolder( summary.text = activity.title text.text = activity.description + if (activity.actorMetadata != null) { + metadataSeparator.visibility = View.VISIBLE + metadata.visibility = View.VISIBLE + metadata.text = activity.actorMetadata + } else { + metadataSeparator.visibility = View.GONE + metadata.visibility = View.GONE + } + ColorUtils.setImageResourceWithTint( actionButton, activity.buttonIcon.drawable, diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt index 48cc26a42d39..cd2b76114448 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt @@ -107,13 +107,13 @@ class ActivityLogDetailViewModel @Inject constructor( private fun findAndPostActivityLogItemDetail() { - activityLogStore + val model = activityLogStore .getActivityLogForSite(site) .find { it.activityID == activityLogId } - ?.toActivityLogDetailModel() + model?.toActivityLogDetailModel() ?.let { _item.value = it - }?: findAndPostActivityLogItemDetailViaDashboardCardsIfNeeded() + } ?: findAndPostActivityLogItemDetailViaDashboardCardsIfNeeded() } private fun findAndPostActivityLogItemDetailViaDashboardCardsIfNeeded() { @@ -138,20 +138,32 @@ class ActivityLogDetailViewModel @Inject constructor( } } - private fun ActivityLogModel.toActivityLogDetailModel() = - ActivityLogDetailModel( + private fun ActivityLogModel.toActivityLogDetailModel(): ActivityLogDetailModel { + val actorSnapshot = actor + return ActivityLogDetailModel( activityID = activityID, rewindId = rewindID, - actorIconUrl = actor?.avatarURL, - showJetpackIcon = actor?.showJetpackIcon(), + actorIconUrl = actorSnapshot?.avatarURL, + showJetpackIcon = actorSnapshot?.showJetpackIcon(), isRewindButtonVisible = rewindable ?: false, - actorName = actor?.displayName, - actorRole = actor?.role, + actorName = actorSnapshot?.displayName, + actorRole = actorSnapshot?.role, + actorMetadata = actorSnapshot?.mcpClient + ?.takeIf { client -> + actorSnapshot.isMCPAgent && client.isNotEmpty() + } + ?.let { client -> + resourceProvider.getString( + R.string.activity_log_mcp_agent_label, + client + ) + }, content = content, summary = summary, createdDate = published.toFormattedDateString(), createdTime = published.toFormattedTimeString() ) + } private fun getMultisiteMessage(): SpannableString { val clickableText = resourceProvider.getString(R.string.activity_log_visit_our_documentation_page) diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt index b5bf6b0d4f2b..6192515e90c1 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt @@ -204,7 +204,19 @@ class ActivityLogViewModel @Inject constructor( model = model, rewindDisabled = withRestoreProgressItem || withBackupDownloadProgressItem, isRestoreHidden = restoreEvent.isRestoreHidden - ) + ).let { event -> + val client = model.actor?.mcpClient + if (model.actor?.isMCPAgent == true && !client.isNullOrEmpty()) { + event.copy( + actorMetadata = resourceProvider.getString( + R.string.activity_log_mcp_agent_label, + client + ) + ) + } else { + event + } + } val lastItem = items.lastOrNull() as? ActivityLogListItem.Event if (lastItem == null || lastItem.formattedDate != currentItem.formattedDate) { items.add(ActivityLogListItem.Header(currentItem.formattedDate)) diff --git a/WordPress/src/main/res/layout/activity_log_item_detail.xml b/WordPress/src/main/res/layout/activity_log_item_detail.xml index f522881a0cf9..ef32b426faa1 100644 --- a/WordPress/src/main/res/layout/activity_log_item_detail.xml +++ b/WordPress/src/main/res/layout/activity_log_item_detail.xml @@ -37,10 +37,9 @@ + + - + android:orientation="horizontal"> + + + + + + + + diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 7ea1fbe5214c..15b0625de79d 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1253,6 +1253,8 @@ Activity Log + + via %1$s Activity icon Event Activity Log action button diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/activity/ActivityLogModel.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/activity/ActivityLogModel.kt index afb6d71a06ee..cce4d528e880 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/activity/ActivityLogModel.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/model/activity/ActivityLogModel.kt @@ -21,6 +21,8 @@ data class ActivityLogModel( val type: String?, val wpcomUserID: Long?, val avatarURL: String?, - val role: String? + val role: String?, + val isMCPAgent: Boolean = false, + val mcpClient: String? = null ) } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt index a87bf811b2b5..ea19e772c561 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt @@ -341,7 +341,9 @@ class ActivityLogRestClient @Inject constructor( it.type, it.wpcom_user_id, it.icon?.url, - it.role + it.role, + it.is_mcp_agent ?: false, + it.mcp_client ) } ) @@ -461,7 +463,9 @@ class ActivityLogRestClient @Inject constructor( val external_user_id: Long?, val wpcom_user_id: Long?, val icon: Icon?, - val role: String? + val role: String?, + val is_mcp_agent: Boolean?, + val mcp_client: String? ) class Icon(val type: String?, val url: String?, val width: Int?, val height: Int?) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt index af923415365a..637afab2d4c2 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClient.kt @@ -280,7 +280,9 @@ fun ActivitiesResponse.toActivityCardModel(): ActivityCardModel { act.type, act.wpcom_user_id, act.icon?.url, - act.role + act.role, + act.is_mcp_agent ?: false, + act.mcp_client ) } ) diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/ActivityLogSqlUtils.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/ActivityLogSqlUtils.kt index 73bfcc7010fc..77cf491843ab 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/ActivityLogSqlUtils.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/ActivityLogSqlUtils.kt @@ -196,7 +196,9 @@ class ActivityLogSqlUtils @Inject constructor(private val formattableContentMapp actorType = this.actor?.type, wpcomUserID = this.actor?.wpcomUserID, avatarURL = this.actor?.avatarURL, - role = this.actor?.role + role = this.actor?.role, + mcpAgent = this.actor?.isMCPAgent, + mcpClient = this.actor?.mcpClient ) } @@ -267,7 +269,9 @@ class ActivityLogSqlUtils @Inject constructor(private val formattableContentMapp @Column var actorType: String? = null, @Column var wpcomUserID: Long? = null, @Column var avatarURL: String? = null, - @Column var role: String? = null + @Column var role: String? = null, + @Column var mcpAgent: Boolean? = null, + @Column var mcpClient: String? = null ) : Identifiable { constructor() : this(-1, 0, 0, "", "", "", published = 0) @@ -286,7 +290,10 @@ class ActivityLogSqlUtils @Inject constructor(private val formattableContentMapp avatarURL != null || role != null ) { - ActivityLogModel.ActivityActor(displayName, actorType, wpcomUserID, avatarURL, role) + ActivityLogModel.ActivityActor( + displayName, actorType, wpcomUserID, avatarURL, role, + mcpAgent ?: false, mcpClient + ) } else { null } diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt index a8f18b82a34f..1be581dfd347 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/persistence/WellSqlConfig.kt @@ -41,7 +41,7 @@ open class WellSqlConfig : DefaultWellConfig { annotation class AddOn override fun getDbVersion(): Int { - return 211 + return 212 } override fun getDbName(): String { @@ -2089,6 +2089,11 @@ open class WellSqlConfig : DefaultWellConfig { 210 -> { db.execSQL("ALTER TABLE TermModel ADD IS_HIERARCHICAL BOOLEAN") } + + 211 -> { + db.execSQL("ALTER TABLE ActivityLog ADD MCP_AGENT BOOLEAN") + db.execSQL("ALTER TABLE ActivityLog ADD MCP_CLIENT TEXT") + } } } db.setTransactionSuccessful() diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityFixtures.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityFixtures.kt index ae079ffeb8ec..0674aa6f6d2c 100644 --- a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityFixtures.kt +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityFixtures.kt @@ -15,7 +15,9 @@ val ACTIVITY_RESPONSE = ActivityLogRestClient.ActivitiesResponse.ActivityRespons "dog.jpg", 100, 100), - "admin"), + "admin", + null, + null), "create a blog", Date(), ActivityLogRestClient.ActivitiesResponse.Generator(10.3f, 123), diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt index b846362c5629..9ea0c566fadc 100644 --- a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/network/rest/wpcom/dashboard/CardsRestClientTest.kt @@ -159,7 +159,9 @@ private val ACTIVITY_RESPONSE_ACTOR = ActivitiesResponse.Actor( 10, 15, ACTIVITY_RESPONSE_ICON, - "admin" + "admin", + null, + null ) private val ACTIVITY_RESPONSE_GENERATOR = ActivitiesResponse.Generator(10.3f, 123) private val ACTIVITY_RESPONSE_PAGE = ActivitiesResponse.ActivityResponse( diff --git a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt index 7e15e7b5c933..23a433419d38 100644 --- a/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt +++ b/libs/fluxc/src/test/java/org/wordpress/android/fluxc/store/dashboard/CardsStoreTest.kt @@ -183,7 +183,9 @@ private val ACTIVITY_RESPONSE_ACTOR = ActivitiesResponse.Actor( ACTIVITY_ACTOR_EXTERNAL_USER_ID, ACTIVITY_ACTOR_WPCOM_USER_ID, ACTIVITY_RESPONSE_ICON, - ACTIVITY_ACTOR_ROLE + ACTIVITY_ACTOR_ROLE, + null, + null ) private val ACTIVITY_RESPONSE_GENERATOR = ActivitiesResponse.Generator(10.3f, 123) private val ACTIVITY_RESPONSE_PAGE = ActivitiesResponse.ActivityResponse( From ff394ed05d86fa262bf36ad1d77fc5707a4dcc84 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:09:16 -0600 Subject: [PATCH 2/3] Fix detekt and lint issues in MCP agent metadata changes Suppress LongParameterList for Actor class and extract hardcoded dot separator to a string resource. Co-Authored-By: Claude Opus 4.6 --- WordPress/src/main/res/layout/activity_log_list_event_item.xml | 2 +- WordPress/src/main/res/values/strings.xml | 1 + .../fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/WordPress/src/main/res/layout/activity_log_list_event_item.xml b/WordPress/src/main/res/layout/activity_log_list_event_item.xml index cfc16c2f4db8..549619c159a2 100644 --- a/WordPress/src/main/res/layout/activity_log_list_event_item.xml +++ b/WordPress/src/main/res/layout/activity_log_list_event_item.xml @@ -75,7 +75,7 @@ android:layout_height="wrap_content" android:paddingStart="4dp" android:paddingEnd="4dp" - android:text="\u00B7" + android:text="@string/activity_log_metadata_separator" android:textAppearance="?attr/textAppearanceCaption" android:visibility="gone" tools:visibility="visible" /> diff --git a/WordPress/src/main/res/values/strings.xml b/WordPress/src/main/res/values/strings.xml index 15b0625de79d..c7280f431075 100644 --- a/WordPress/src/main/res/values/strings.xml +++ b/WordPress/src/main/res/values/strings.xml @@ -1255,6 +1255,7 @@ Activity Log via %1$s + \u00B7 Activity icon Event Activity Log action button diff --git a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt index ea19e772c561..64f6a1a8a302 100644 --- a/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt +++ b/libs/fluxc/src/main/java/org/wordpress/android/fluxc/network/rest/wpcom/activity/ActivityLogRestClient.kt @@ -457,6 +457,7 @@ class ActivityLogRestClient @Inject constructor( val activity_id: String? ) + @Suppress("LongParameterList") class Actor( val type: String?, val name: String?, From d6bb765d6c98d508b60245ce6845c9dc30a059a8 Mon Sep 17 00:00:00 2001 From: adalpari Date: Thu, 19 Mar 2026 12:03:51 +0100 Subject: [PATCH 3/3] Refactor MCP metadata: extract shared logic, use uiHelpers, add tests - Extract duplicated MCP formatting into ActivityActorExtensions - Remove unnecessary intermediate variable in detail ViewModel - Pass uiHelpers to EventItemViewHolder for consistent visibility handling - Add unit tests for MCP metadata in both list and detail ViewModels Co-Authored-By: Claude Opus 4.6 (1M context) --- .../ui/activitylog/list/ActivityLogAdapter.kt | 4 +- .../activitylog/list/EventItemViewHolder.kt | 16 ++--- .../activitylog/ActivityActorExtensions.kt | 18 +++++ .../activitylog/ActivityLogDetailViewModel.kt | 15 +--- .../activitylog/ActivityLogViewModel.kt | 11 +-- .../ActivityLogDetailViewModelTest.kt | 71 +++++++++++++++++++ .../activitylog/ActivityLogViewModelTest.kt | 66 +++++++++++++++++ 7 files changed, 171 insertions(+), 30 deletions(-) create mode 100644 WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityActorExtensions.kt diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt index cc173266b2da..21ab3091e902 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/ActivityLogAdapter.kt @@ -62,7 +62,9 @@ class ActivityLogAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ActivityLogViewHolder { return when (viewType) { ViewType.PROGRESS.id -> ProgressItemViewHolder(parent) - ViewType.EVENT.id -> EventItemViewHolder(parent, itemClickListener, secondaryActionClickListener) + ViewType.EVENT.id -> EventItemViewHolder( + parent, itemClickListener, secondaryActionClickListener, uiHelpers + ) ViewType.HEADER.id -> HeaderItemViewHolder(parent) ViewType.FOOTER.id -> FooterItemViewHolder(parent) ViewType.LOADING.id -> LoadingItemViewHolder(parent) diff --git a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt index b38964fa80a2..cbada1494b87 100644 --- a/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt +++ b/WordPress/src/main/java/org/wordpress/android/ui/activitylog/list/EventItemViewHolder.kt @@ -12,13 +12,15 @@ import android.widget.TextView import org.wordpress.android.R import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.Event import org.wordpress.android.ui.activitylog.list.ActivityLogListItem.SecondaryAction +import org.wordpress.android.ui.utils.UiHelpers import org.wordpress.android.util.ColorUtils import org.wordpress.android.util.extensions.getColorResIdFromAttribute class EventItemViewHolder( parent: ViewGroup, private val itemClickListener: (ActivityLogListItem) -> Unit, - private val secondaryActionClickListener: (SecondaryAction, ActivityLogListItem) -> Boolean + private val secondaryActionClickListener: (SecondaryAction, ActivityLogListItem) -> Boolean, + private val uiHelpers: UiHelpers ) : ActivityLogViewHolder(parent, R.layout.activity_log_list_event_item) { private val summary: TextView = itemView.findViewById(R.id.action_summary) private val text: TextView = itemView.findViewById(R.id.action_text) @@ -43,14 +45,10 @@ class EventItemViewHolder( summary.text = activity.title text.text = activity.description - if (activity.actorMetadata != null) { - metadataSeparator.visibility = View.VISIBLE - metadata.visibility = View.VISIBLE - metadata.text = activity.actorMetadata - } else { - metadataSeparator.visibility = View.GONE - metadata.visibility = View.GONE - } + uiHelpers.setTextOrHide(metadata, activity.actorMetadata) + uiHelpers.updateVisibility( + metadataSeparator, activity.actorMetadata != null + ) ColorUtils.setImageResourceWithTint( actionButton, diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityActorExtensions.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityActorExtensions.kt new file mode 100644 index 000000000000..59d93df9f67d --- /dev/null +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityActorExtensions.kt @@ -0,0 +1,18 @@ +package org.wordpress.android.viewmodel.activitylog + +import org.wordpress.android.R +import org.wordpress.android.fluxc.model.activity.ActivityLogModel +import org.wordpress.android.viewmodel.ResourceProvider + +fun ActivityLogModel.ActivityActor.formattedMcpMetadata( + resourceProvider: ResourceProvider +): String? { + val client = mcpClient + return if (isMCPAgent && !client.isNullOrEmpty()) { + resourceProvider.getString( + R.string.activity_log_mcp_agent_label, client + ) + } else { + null + } +} diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt index cd2b76114448..873c58a2e510 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModel.kt @@ -107,10 +107,10 @@ class ActivityLogDetailViewModel @Inject constructor( private fun findAndPostActivityLogItemDetail() { - val model = activityLogStore + activityLogStore .getActivityLogForSite(site) .find { it.activityID == activityLogId } - model?.toActivityLogDetailModel() + ?.toActivityLogDetailModel() ?.let { _item.value = it } ?: findAndPostActivityLogItemDetailViaDashboardCardsIfNeeded() @@ -148,16 +148,7 @@ class ActivityLogDetailViewModel @Inject constructor( isRewindButtonVisible = rewindable ?: false, actorName = actorSnapshot?.displayName, actorRole = actorSnapshot?.role, - actorMetadata = actorSnapshot?.mcpClient - ?.takeIf { client -> - actorSnapshot.isMCPAgent && client.isNotEmpty() - } - ?.let { client -> - resourceProvider.getString( - R.string.activity_log_mcp_agent_label, - client - ) - }, + actorMetadata = actorSnapshot?.formattedMcpMetadata(resourceProvider), content = content, summary = summary, createdDate = published.toFormattedDateString(), diff --git a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt index 6192515e90c1..e689441992ae 100644 --- a/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt +++ b/WordPress/src/main/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModel.kt @@ -205,14 +205,9 @@ class ActivityLogViewModel @Inject constructor( rewindDisabled = withRestoreProgressItem || withBackupDownloadProgressItem, isRestoreHidden = restoreEvent.isRestoreHidden ).let { event -> - val client = model.actor?.mcpClient - if (model.actor?.isMCPAgent == true && !client.isNullOrEmpty()) { - event.copy( - actorMetadata = resourceProvider.getString( - R.string.activity_log_mcp_agent_label, - client - ) - ) + val metadata = model.actor?.formattedMcpMetadata(resourceProvider) + if (metadata != null) { + event.copy(actorMetadata = metadata) } else { event } diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt index 570021d43672..6a9c686e858f 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogDetailViewModelTest.kt @@ -16,9 +16,11 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.junit.MockitoJUnitRunner import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import org.wordpress.android.BaseUnitTest +import org.wordpress.android.R import org.wordpress.android.fluxc.Dispatcher import org.wordpress.android.fluxc.model.SiteModel import org.wordpress.android.fluxc.model.activity.ActivityLogModel @@ -461,6 +463,75 @@ class ActivityLogDetailViewModelTest : BaseUnitTest() { } } + @Test + fun `given mcp agent with client, when view model starts, then metadata is shown`() { + val mcpLabel = "via Claude" + val mcpActivity = activityLogModel.copy( + actor = activityLogModel.actor?.copy( + isMCPAgent = true, + mcpClient = "Claude" + ) + ) + whenever(activityLogStore.getActivityLogForSite(site)) + .thenReturn(listOf(mcpActivity)) + whenever( + resourceProvider.getString( + eq(R.string.activity_log_mcp_agent_label), any() + ) + ).thenReturn(mcpLabel) + + startViewModel() + + assertNotNull(lastEmittedItem) + assertEquals(mcpLabel, lastEmittedItem?.actorMetadata) + } + + @Test + fun `given mcp agent with empty client, when view model starts, then metadata is null`() { + val mcpActivity = activityLogModel.copy( + actor = activityLogModel.actor?.copy( + isMCPAgent = true, + mcpClient = "" + ) + ) + whenever(activityLogStore.getActivityLogForSite(site)) + .thenReturn(listOf(mcpActivity)) + + startViewModel() + + assertNotNull(lastEmittedItem) + assertNull(lastEmittedItem?.actorMetadata) + } + + @Test + fun `given non-mcp agent with client, when view model starts, then metadata is null`() { + val mcpActivity = activityLogModel.copy( + actor = activityLogModel.actor?.copy( + isMCPAgent = false, + mcpClient = "Claude" + ) + ) + whenever(activityLogStore.getActivityLogForSite(site)) + .thenReturn(listOf(mcpActivity)) + + startViewModel() + + assertNotNull(lastEmittedItem) + assertNull(lastEmittedItem?.actorMetadata) + } + + @Test + fun `given no actor, when view model starts, then metadata is null`() { + val noActorActivity = activityLogModel.copy(actor = null) + whenever(activityLogStore.getActivityLogForSite(site)) + .thenReturn(listOf(noActorActivity)) + + startViewModel() + + assertNotNull(lastEmittedItem) + assertNull(lastEmittedItem?.actorMetadata) + } + private fun startViewModel( site: SiteModel = this.site, activityID: String = this.activityID, diff --git a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt index 97172760d576..19e270141b3b 100644 --- a/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt +++ b/WordPress/src/test/java/org/wordpress/android/viewmodel/activitylog/ActivityLogViewModelTest.kt @@ -1455,6 +1455,72 @@ class ActivityLogViewModelTest : BaseUnitTest() { assertEquals(snackbarMessages.firstOrNull(), BACKED_UP_NO_DATE) } + /* MCP METADATA */ + + @Test + fun `given mcp agent activity, when events loaded, then event has metadata`() = test { + val mcpLabel = "via Claude" + val mcpActivity = activity().copy( + actor = ActivityLogModel.ActivityActor( + displayName = "Bot", + type = "Application", + wpcomUserID = null, + avatarURL = null, + role = "admin", + isMCPAgent = true, + mcpClient = "Claude" + ) + ) + whenever(store.getActivityLogForSite(site, false, rewindableOnly)) + .thenReturn(listOf(mcpActivity)) + whenever( + resourceProvider.getString( + eq(R.string.activity_log_mcp_agent_label), any() + ) + ).thenReturn(mcpLabel) + + viewModel.reloadEvents(done = true) + + val eventItems = viewModel.events.value + ?.filterIsInstance() + assertNotNull(eventItems) + assertEquals(mcpLabel, eventItems?.first()?.actorMetadata) + } + + @Test + fun `given non-mcp activity, when events loaded, then event has no metadata`() = test { + viewModel.reloadEvents(done = true) + + val eventItems = viewModel.events.value + ?.filterIsInstance() + assertNotNull(eventItems) + assertNull(eventItems?.first()?.actorMetadata) + } + + @Test + fun `given mcp agent with empty client, when events loaded, then event has no metadata`() = test { + val mcpActivity = activity().copy( + actor = ActivityLogModel.ActivityActor( + displayName = "Bot", + type = "Application", + wpcomUserID = null, + avatarURL = null, + role = "admin", + isMCPAgent = true, + mcpClient = "" + ) + ) + whenever(store.getActivityLogForSite(site, false, rewindableOnly)) + .thenReturn(listOf(mcpActivity)) + + viewModel.reloadEvents(done = true) + + val eventItems = viewModel.events.value + ?.filterIsInstance() + assertNotNull(eventItems) + assertNull(eventItems?.first()?.actorMetadata) + } + /* PRIVATE */ private fun firstActivity() = activity()