From 3e7fa7bad64f545accfc185d03e4588cb27e8469 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:18:21 -0600 Subject: [PATCH 1/2] Add unified support conversations endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wires `/wpcom/v2/mobile-support/unified-conversations` into `WpComApiClient`: list, get one, and reply. Message/attachment shape differs from the older `/mobile-support/conversations` endpoints, and reference-link attachments carry a float `score` in metadata — using `JsonValue` for metadata values rather than extending the Eq-bound `AttachmentMetadataValue`. --- wp_api/src/wp_com/client.rs | 12 +- wp_api/src/wp_com/endpoint.rs | 1 + .../unified_conversations_endpoint.rs | 70 ++ wp_api/src/wp_com/mod.rs | 1 + wp_api/src/wp_com/unified_conversations.rs | 99 +++ .../conversation-list.json | 632 ++++++++++++++++++ .../unified_conversations/reply-response.json | 223 ++++++ .../single-conversation.json | 223 ++++++ 8 files changed, 1259 insertions(+), 2 deletions(-) create mode 100644 wp_api/src/wp_com/endpoint/unified_conversations_endpoint.rs create mode 100644 wp_api/src/wp_com/unified_conversations.rs create mode 100644 wp_api/tests/wpcom/unified_conversations/conversation-list.json create mode 100644 wp_api/tests/wpcom/unified_conversations/reply-response.json create mode 100644 wp_api/tests/wpcom/unified_conversations/single-conversation.json diff --git a/wp_api/src/wp_com/client.rs b/wp_api/src/wp_com/client.rs index 69014e138..8c94711d2 100644 --- a/wp_api/src/wp_com/client.rs +++ b/wp_api/src/wp_com/client.rs @@ -48,6 +48,9 @@ use super::endpoint::{ SupportEligibilityRequestBuilder, SupportEligibilityRequestExecutor, }, support_tickets_endpoint::{SupportTicketsRequestBuilder, SupportTicketsRequestExecutor}, + unified_conversations_endpoint::{ + UnifiedConversationsRequestBuilder, UnifiedConversationsRequestExecutor, + }, }; use crate::{ api_client::WpApiClientDelegate, @@ -97,6 +100,7 @@ pub struct WpComApiRequestBuilder { support_bots: Arc, support_eligibility: Arc, support_tickets: Arc, + unified_conversations: Arc, } impl WpComApiRequestBuilder { @@ -137,7 +141,8 @@ impl WpComApiRequestBuilder { subscribers, support_bots, support_eligibility, - support_tickets + support_tickets, + unified_conversations ) } } @@ -190,6 +195,7 @@ pub struct WpComApiClient { support_bots: Arc, support_eligibility: Arc, support_tickets: Arc, + unified_conversations: Arc, } impl WpComApiClient { @@ -231,7 +237,8 @@ impl WpComApiClient { subscribers, support_bots, support_eligibility, - support_tickets + support_tickets, + unified_conversations ) } } @@ -267,3 +274,4 @@ api_client_generate_endpoint_impl!(WpComApi, subscribers); api_client_generate_endpoint_impl!(WpComApi, support_bots); api_client_generate_endpoint_impl!(WpComApi, support_eligibility); api_client_generate_endpoint_impl!(WpComApi, support_tickets); +api_client_generate_endpoint_impl!(WpComApi, unified_conversations); diff --git a/wp_api/src/wp_com/endpoint.rs b/wp_api/src/wp_com/endpoint.rs index 5c91832bc..5c8abe88e 100644 --- a/wp_api/src/wp_com/endpoint.rs +++ b/wp_api/src/wp_com/endpoint.rs @@ -39,6 +39,7 @@ pub mod subscribers_endpoint; pub mod support_bots_endpoint; pub mod support_eligibility_endpoint; pub mod support_tickets_endpoint; +pub mod unified_conversations_endpoint; #[derive(uniffi::Object)] pub struct WpComDotOrgApiUrlResolver { diff --git a/wp_api/src/wp_com/endpoint/unified_conversations_endpoint.rs b/wp_api/src/wp_com/endpoint/unified_conversations_endpoint.rs new file mode 100644 index 000000000..631b4de49 --- /dev/null +++ b/wp_api/src/wp_com/endpoint/unified_conversations_endpoint.rs @@ -0,0 +1,70 @@ +use wp_derive_request_builder::WpDerivedRequest; + +use crate::{ + request::endpoint::{AsNamespace, DerivedRequest}, + wp_com::{ + WpComNamespace, + support_tickets::ConversationId, + unified_conversations::{ + ReplyToUnifiedConversationParams, UnifiedConversation, UnifiedConversationSummary, + }, + }, +}; + +#[derive(WpDerivedRequest)] +enum UnifiedConversationsRequest { + #[get(url = "/mobile-support/unified-conversations", output = Vec)] + GetUnifiedConversationList, + #[get(url = "/mobile-support/unified-conversations/", output = UnifiedConversation)] + GetUnifiedConversation, + #[post(url = "/mobile-support/unified-conversations/", params = &ReplyToUnifiedConversationParams, output = UnifiedConversation)] + ReplyToUnifiedConversation, +} + +impl DerivedRequest for UnifiedConversationsRequest { + fn namespace() -> impl AsNamespace { + WpComNamespace::V2 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + request::endpoint::ApiUrlResolver, + wp_com::endpoint::tests::{fixture_wp_com_api_url_resolver, validate_wp_com_v2_endpoint}, + }; + use rstest::*; + use std::sync::Arc; + + #[rstest] + fn get_unified_conversation_list(endpoint: UnifiedConversationsRequestEndpoint) { + validate_wp_com_v2_endpoint( + endpoint.get_unified_conversation_list(), + "/mobile-support/unified-conversations", + ); + } + + #[rstest] + fn get_unified_conversation(endpoint: UnifiedConversationsRequestEndpoint) { + validate_wp_com_v2_endpoint( + endpoint.get_unified_conversation(&ConversationId(4396575)), + "/mobile-support/unified-conversations/4396575", + ); + } + + #[rstest] + fn reply_to_unified_conversation(endpoint: UnifiedConversationsRequestEndpoint) { + validate_wp_com_v2_endpoint( + endpoint.reply_to_unified_conversation(&ConversationId(4396575)), + "/mobile-support/unified-conversations/4396575", + ); + } + + #[fixture] + fn endpoint( + fixture_wp_com_api_url_resolver: Arc, + ) -> UnifiedConversationsRequestEndpoint { + UnifiedConversationsRequestEndpoint::new(fixture_wp_com_api_url_resolver) + } +} diff --git a/wp_api/src/wp_com/mod.rs b/wp_api/src/wp_com/mod.rs index 846329a27..2afae29ee 100644 --- a/wp_api/src/wp_com/mod.rs +++ b/wp_api/src/wp_com/mod.rs @@ -35,6 +35,7 @@ pub mod subscribers; pub mod support_bots; pub mod support_eligibility; pub mod support_tickets; +pub mod unified_conversations; impl_as_query_value_for_new_type!(WpComSiteId); uniffi::custom_newtype!(WpComSiteId, u64); diff --git a/wp_api/src/wp_com/unified_conversations.rs b/wp_api/src/wp_com/unified_conversations.rs new file mode 100644 index 000000000..c39b146c1 --- /dev/null +++ b/wp_api/src/wp_com/unified_conversations.rs @@ -0,0 +1,99 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +use crate::{JsonValue, date::WpGmtDateTime, wp_com::support_tickets::ConversationId}; + +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, uniffi::Record)] +pub struct UnifiedConversationSummary { + pub id: ConversationId, + pub title: String, + pub description: String, + pub status: String, + pub can_accept_reply: bool, + pub created_at: WpGmtDateTime, + pub updated_at: WpGmtDateTime, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)] +pub struct UnifiedConversation { + pub id: ConversationId, + pub title: String, + pub description: String, + pub status: String, + pub can_accept_reply: bool, + pub created_at: WpGmtDateTime, + pub updated_at: WpGmtDateTime, + pub messages: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)] +pub struct UnifiedMessage { + pub id: u64, + pub message: String, + pub author_role: String, + pub author_name: String, + pub created_at: WpGmtDateTime, + pub attachments: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, uniffi::Record)] +pub struct UnifiedAttachment { + pub id: u64, + pub filename: String, + pub content_type: String, + pub size: u64, + pub url: String, + pub metadata: HashMap, +} + +#[derive(Debug, PartialEq, Eq, Serialize, uniffi::Record)] +pub struct ReplyToUnifiedConversationParams { + pub message: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unified_conversation_list_deserialization() { + let json = include_str!("../../tests/wpcom/unified_conversations/conversation-list.json"); + let list: Vec = + serde_json::from_str(json).expect("Failed to deserialize unified conversation list"); + assert_eq!(list.len(), 70); + // Spot-check the first entry (a conversation that can still accept replies). + assert_eq!(list[0].id, ConversationId(11121776)); + assert!(list[0].can_accept_reply); + assert_eq!(list[0].status, "new"); + } + + #[test] + fn test_unified_conversation_deserialization() { + let json = include_str!("../../tests/wpcom/unified_conversations/single-conversation.json"); + let conversation: UnifiedConversation = + serde_json::from_str(json).expect("Failed to deserialize unified conversation"); + assert_eq!(conversation.id, ConversationId(4396575)); + assert_eq!(conversation.messages.len(), 10); + assert_eq!(conversation.messages[0].author_role, "user"); + assert_eq!(conversation.messages[1].author_role, "bot"); + // Bot responses include reference attachments with a float `score` in metadata. + let bot_attachments = &conversation.messages[7].attachments; + assert!(!bot_attachments.is_empty()); + assert!(matches!( + bot_attachments[0].metadata.get("score"), + Some(JsonValue::Float(_)) + )); + } + + #[test] + fn test_unified_conversation_reply_response_deserialization() { + // Replies return the full conversation, so the reply-response fixture + // uses the same shape as the single-conversation fixture. + let json = include_str!("../../tests/wpcom/unified_conversations/reply-response.json"); + let conversation: UnifiedConversation = + serde_json::from_str(json).expect("Failed to deserialize reply response"); + assert_eq!(conversation.id, ConversationId(4396575)); + assert!(!conversation.messages.is_empty()); + } +} diff --git a/wp_api/tests/wpcom/unified_conversations/conversation-list.json b/wp_api/tests/wpcom/unified_conversations/conversation-list.json new file mode 100644 index 000000000..87f425394 --- /dev/null +++ b/wp_api/tests/wpcom/unified_conversations/conversation-list.json @@ -0,0 +1,632 @@ +[ + { + "id": 11121776, + "title": "Bot Conversation Followup", + "description": "User has requested followup on bot conversation", + "status": "new", + "can_accept_reply": true, + "created_at": "2026-04-16T16:56:21+00:00", + "updated_at": "2026-04-16T16:56:21+00:00" + }, + { + "id": 11121769, + "title": "Bot Conversation Followup", + "description": "User has requested followup on bot conversation", + "status": "new", + "can_accept_reply": true, + "created_at": "2026-04-16T16:54:51+00:00", + "updated_at": "2026-04-16T16:54:51+00:00" + }, + { + "id": 4396575, + "title": "Hey bot, is this thing working?\n", + "description": "Hey bot, is this thing working?\n", + "status": "bot", + "can_accept_reply": true, + "created_at": "2026-04-15T00:08:14+00:00", + "updated_at": "2026-04-15T00:08:14+00:00" + }, + { + "id": 10730554, + "title": "Bot Conversation Followup", + "description": "Followup for bot conversation", + "status": "closed", + "can_accept_reply": false, + "created_at": "2026-01-15T17:15:41+00:00", + "updated_at": "2026-01-25T18:11:24+00:00" + }, + { + "id": 10722353, + "title": "test", + "description": "test - please ignore", + "status": "closed", + "can_accept_reply": false, + "created_at": "2026-01-14T00:23:16+00:00", + "updated_at": "2026-01-14T08:02:19+00:00" + }, + { + "id": 10622332, + "title": "This is a routing test", + "description": "Let's see if it works!", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-12-15T17:51:49+00:00", + "updated_at": "2025-12-25T18:13:18+00:00" + }, + { + "id": 10622354, + "title": "This is a routing test", + "description": "Let's see if it works!", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-12-15T17:54:48+00:00", + "updated_at": "2025-12-17T19:02:12+00:00" + }, + { + "id": 10622498, + "title": "This is a routing test", + "description": "Let's see if it works.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-12-15T18:30:56+00:00", + "updated_at": "2025-12-17T19:02:11+00:00" + }, + { + "id": 10509186, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:58:21+00:00", + "updated_at": "2025-11-29T22:07:48+00:00" + }, + { + "id": 10509176, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:54:18+00:00", + "updated_at": "2025-11-29T22:07:46+00:00" + }, + { + "id": 10509157, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:47:16+00:00", + "updated_at": "2025-11-29T22:07:46+00:00" + }, + { + "id": 10509107, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:30:32+00:00", + "updated_at": "2025-11-29T22:07:46+00:00" + }, + { + "id": 10509139, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:41:02+00:00", + "updated_at": "2025-11-29T22:07:44+00:00" + }, + { + "id": 10509022, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:03:30+00:00", + "updated_at": "2025-11-29T22:07:44+00:00" + }, + { + "id": 10509192, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T22:00:02+00:00", + "updated_at": "2025-11-22T00:01:54+00:00" + }, + { + "id": 10509202, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T22:01:52+00:00", + "updated_at": "2025-11-22T00:01:53+00:00" + }, + { + "id": 10509197, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T22:00:28+00:00", + "updated_at": "2025-11-22T00:01:53+00:00" + }, + { + "id": 10509013, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-19T21:02:04+00:00", + "updated_at": "2025-11-21T22:02:28+00:00" + }, + { + "id": 10448176, + "title": "A test of the Jetpack app", + "description": "This is my test message. Let's keep this conversation open to test stuff.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-04T22:36:14+00:00", + "updated_at": "2025-11-14T23:04:15+00:00" + }, + { + "id": 10442600, + "title": "test", + "description": "test", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-03T19:11:19+00:00", + "updated_at": "2025-11-06T20:02:33+00:00" + }, + { + "id": 10448214, + "title": "test creating conversations with media", + "description": "This is another test reply", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-04T22:48:32+00:00", + "updated_at": "2025-11-05T08:02:32+00:00" + }, + { + "id": 10442233, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-03T17:48:47+00:00", + "updated_at": "2025-11-03T21:04:33+00:00" + }, + { + "id": 10442215, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-11-03T17:44:26+00:00", + "updated_at": "2025-11-03T19:02:24+00:00" + }, + { + "id": 10431833, + "title": "Support Attachment Test", + "description": "This is a test message designed to demonstrate how the system communicates and responds. It serves no functional purpose beyond confirming that the message can be sent, received, and displayed correctly. By treating this as a simple test, we can ensure that all components involved are working as expected without requiring any additional action or special consideration.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-31T21:33:52+00:00", + "updated_at": "2025-11-01T19:02:53+00:00" + }, + { + "id": 10426469, + "title": "This is a test", + "description": "Presse disregard", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-30T18:38:15+00:00", + "updated_at": "2025-10-31T05:02:25+00:00" + }, + { + "id": 10427118, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-30T21:21:01+00:00", + "updated_at": "2025-10-31T05:02:24+00:00" + }, + { + "id": 10427402, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-30T22:48:30+00:00", + "updated_at": "2025-10-31T05:02:23+00:00" + }, + { + "id": 10427333, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-30T22:29:11+00:00", + "updated_at": "2025-10-31T05:02:23+00:00" + }, + { + "id": 10414675, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-28T15:37:20+00:00", + "updated_at": "2025-10-28T18:03:14+00:00" + }, + { + "id": 10410983, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-27T21:30:43+00:00", + "updated_at": "2025-10-28T08:02:16+00:00" + }, + { + "id": 10410756, + "title": "Test", + "description": "Please delete", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-27T20:36:22+00:00", + "updated_at": "2025-10-27T23:02:14+00:00" + }, + { + "id": 10340334, + "title": "testing ios", + "description": "please close this issue", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-09T20:58:24+00:00", + "updated_at": "2025-10-10T07:02:19+00:00" + }, + { + "id": 10335951, + "title": "this is a test message", + "description": "Please feel free to delete it", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-08T21:20:53+00:00", + "updated_at": "2025-10-09T07:02:25+00:00" + }, + { + "id": 10336271, + "title": "Testing iOS", + "description": "\n\n\n\n\n\n\n\n\n\n

Please feel free to close this upon receipt. 

\n\n\n", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-08T23:12:21+00:00", + "updated_at": "2025-10-09T05:21:31+00:00" + }, + { + "id": 10336250, + "title": "Testing iOS", + "description": "\n\n\n\n\n\n\n\n\n\n

Please just close this message if you receive it. 

\n\n\n", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-08T23:04:48+00:00", + "updated_at": "2025-10-09T05:21:31+00:00" + }, + { + "id": 10336076, + "title": "This is an HTML test", + "description": "It's a **bold** choice.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-08T21:56:53+00:00", + "updated_at": "2025-10-09T05:21:30+00:00" + }, + { + "id": 10335957, + "title": "This is a JSON test", + "description": "you can feel free to close it :) ", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-08T21:22:43+00:00", + "updated_at": "2025-10-09T05:21:30+00:00" + }, + { + "id": 10331714, + "title": "This is a test", + "description": "you can feel free to close it :) ", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-07T21:41:17+00:00", + "updated_at": "2025-10-08T03:02:01+00:00" + }, + { + "id": 10331692, + "title": "This is a test", + "description": "you can feel free to delete it :) ", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-07T21:35:46+00:00", + "updated_at": "2025-10-08T03:02:01+00:00" + }, + { + "id": 10331740, + "title": "This is a test", + "description": "you can feel free to close it :) ", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-10-07T21:50:20+00:00", + "updated_at": "2025-10-08T03:02:00+00:00" + }, + { + "id": 9823827, + "title": "Your Jetpack.com Purchase [Action Required]", + "description": "User jkmassel ( jeremy.massel@gmail.com ) has purchased Jetpack Complete ( jetpack_complete ).\n\nBlog ID: 244953345\nSite URL: siteless.jetpack.com/GID1wjidNf7jG9QRetxBboaX\nLicense Key: jetpack_complete_sAVA8D5QpXN3JSbEfZ4Ytxu4C\n\nPayments Admin: https://mc.a8c.com/payments-admin/view-user-product-ownership.php?id=742098\nStore Admin: https://wordpress.com/wp-admin/network/admin.php?page=store-admin&action=search&username=742098\n", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-28T00:04:03+00:00", + "updated_at": "2025-05-30T01:01:47+00:00" + }, + { + "id": 9809452, + "title": "Test Subject", + "description": "Test Message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:06:21+00:00", + "updated_at": "2025-05-27T07:02:13+00:00" + }, + { + "id": 9809446, + "title": "Test Subject", + "description": "Test Message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:04:35+00:00", + "updated_at": "2025-05-27T07:02:13+00:00" + }, + { + "id": 9809519, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:22:38+00:00", + "updated_at": "2025-05-27T07:02:12+00:00" + }, + { + "id": 9809497, + "title": "Test Subject", + "description": "Test Message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:15:51+00:00", + "updated_at": "2025-05-27T07:02:12+00:00" + }, + { + "id": 9809475, + "title": "Test Subject", + "description": "Test Message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:11:55+00:00", + "updated_at": "2025-05-27T07:02:12+00:00" + }, + { + "id": 9809470, + "title": "Test Subject", + "description": "Test Message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:10:04+00:00", + "updated_at": "2025-05-27T07:02:12+00:00" + }, + { + "id": 9809538, + "title": "Mobile Support Test Message", + "description": "This is a test – it can be deleted without replying.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-23T17:29:37+00:00", + "updated_at": "2025-05-27T07:02:11+00:00" + }, + { + "id": 9796958, + "title": "Jetpack for iOS Support", + "description": "this is a test. please ignore", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-21T01:03:10+00:00", + "updated_at": "2025-05-24T22:02:02+00:00" + }, + { + "id": 9806076, + "title": "Jetpack for iOS Support", + "description": "This is a new test – is it working?", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:39:39+00:00", + "updated_at": "2025-05-24T22:01:50+00:00" + }, + { + "id": 9806058, + "title": "Jetpack for iOS Support", + "description": "This is a new test – is it working?", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:32:56+00:00", + "updated_at": "2025-05-24T22:01:50+00:00" + }, + { + "id": 9806056, + "title": "Jetpack for iOS Support", + "description": "This is a new test – is it working?", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:31:36+00:00", + "updated_at": "2025-05-24T22:01:50+00:00" + }, + { + "id": 9806121, + "title": "Jetpack for iOS Support", + "description": "This is a new test – is it working?", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:53:30+00:00", + "updated_at": "2025-05-23T10:02:30+00:00" + }, + { + "id": 9806054, + "title": "Jetpack for iOS Support", + "description": "I'm wondering if this is being routed correctly?", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:30:55+00:00", + "updated_at": "2025-05-22T21:31:03+00:00" + }, + { + "id": 9806050, + "title": "Jetpack for iOS Support", + "description": "This is a test by @jkmassel. Please ignore", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:30:13+00:00", + "updated_at": "2025-05-22T21:30:20+00:00" + }, + { + "id": 9806033, + "title": "Jetpack for iOS Support", + "description": "This is a test by @jkmassel. Please ignore", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-22T21:22:08+00:00", + "updated_at": "2025-05-22T21:22:17+00:00" + }, + { + "id": 9766014, + "title": "testing ", + "description": "this is. test please disregard ", + "status": "closed", + "can_accept_reply": false, + "created_at": "2025-05-12T23:53:07+00:00", + "updated_at": "2025-05-20T01:01:25+00:00" + }, + { + "id": 8527923, + "title": "The PayPal Express Checkout plugin has been deprecated", + "description": "Hi there,\n\n\n\nWe’re writing to let you know that, due to the author’s request, the PayPal Express Checkout plugin was deprecated on July 9, 2024.\n\n\n\n**What does this mean for you?**\n\n\n\nThe plugin is no longer available for download. As it looks like you may have been using it, we wanted to inform you that as of July 29th, if the plugin is:\n\n- **Disabled on your site**, it’ll be removed\n- **Enabled on your site,** it’ll be left in place, but it won’t be possible to automatically or manually update it going forward\n\nIf you’re looking for an alternative, we’d suggest using the WooCommerce PayPal Payments plugin.\n\n\n\nThanks for your time,\n\n\n\nThe WordPress.com Team\n\n\n\nP.S. If you have any questions or concerns, feel free to get in touch by replying directly to this email.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2024-07-25T14:58:23+00:00", + "updated_at": "2024-08-06T16:04:03+00:00" + }, + { + "id": 8491006, + "title": "another test", + "description": "More stuff", + "status": "closed", + "can_accept_reply": false, + "created_at": "2024-07-16T18:34:41+00:00", + "updated_at": "2024-07-20T23:02:23+00:00" + }, + { + "id": 8470219, + "title": "This is a test", + "description": "you can ignore it 🙃", + "status": "closed", + "can_accept_reply": false, + "created_at": "2024-07-11T16:52:40+00:00", + "updated_at": "2024-07-15T21:04:49+00:00" + }, + { + "id": 8466093, + "title": "This is a test", + "description": "This is the message I am writing", + "status": "closed", + "can_accept_reply": false, + "created_at": "2024-07-10T20:16:53+00:00", + "updated_at": "2024-07-15T01:02:27+00:00" + }, + { + "id": 8420673, + "title": "Jetpack for iOS Support", + "description": "this is a test message. please disregard", + "status": "closed", + "can_accept_reply": false, + "created_at": "2024-07-02T05:40:53+00:00", + "updated_at": "2024-07-09T08:02:33+00:00" + }, + { + "id": 8420696, + "title": "Jetpack for iOS Support", + "description": "Jetpack Mobile Bot transcript:\n>\nQuestion:\n>\nI don't know what I need\n>\nAnswer:\n>\nNo worries! I'm here to help. Could you please provide a bit more detail about what you're trying to do or what issue you're facing? This will help me assist you better. If you're having trouble with logging in, setting up, or using the Jetpack Mobile app, let me know the specifics.\n\nIf you prefer, you can also click on the \"Contact Support\" button below to create a ticket, and a Happiness Engineer will assist you further.\n\n[Contact Support](#)\n>\nQuestion:\n>\nI don't know what you're saying \n>\nAnswer:\n>\nOf course! I'd be happy to help you. Could you please provide more details about what you're trying to figure out? Are you looking for assistance with setting up or using the Jetpack Mobile app, or do you need help with your WordPress.com account? Let me know so I can provide the most relevant information.\n>\nQuestion:\n>\nI'm confused, can I talk to a human?\n>\nAnswer:\n>\nSure! Please click on the \"Contact Support\" button below and share with us as much information as you can provide. A Happiness Engineer will assist you further with your request.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2024-07-02T05:45:16+00:00", + "updated_at": "2024-07-02T05:45:26+00:00" + }, + { + "id": 4293254, + "title": "WooCommerce for iOS Support", + "description": "This is a test message. Please disregard and delete it -@jkmassel", + "status": "closed", + "can_accept_reply": false, + "created_at": "2021-09-13T03:28:34+00:00", + "updated_at": "2021-09-13T05:02:42+00:00" + }, + { + "id": 3683708, + "title": "WordPress for Android Support", + "description": "This is a test message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2021-01-26T06:31:57+00:00", + "updated_at": "2021-01-30T09:24:53+00:00" + }, + { + "id": 3670604, + "title": "WordPress for Android Support", + "description": "This is a wordpress test", + "status": "closed", + "can_accept_reply": false, + "created_at": "2021-01-21T18:49:03+00:00", + "updated_at": "2021-01-30T09:24:10+00:00" + }, + { + "id": 3670191, + "title": "WooCommerce for Android Support", + "description": "This is a test for Android pis notifications", + "status": "closed", + "can_accept_reply": false, + "created_at": "2021-01-21T16:58:13+00:00", + "updated_at": "2021-01-30T09:23:57+00:00" + }, + { + "id": 3557971, + "title": "WordPress for iOS Support", + "description": "This is a test for encrypted logging.", + "status": "closed", + "can_accept_reply": false, + "created_at": "2020-12-08T20:42:31+00:00", + "updated_at": "2020-12-16T00:01:48+00:00" + }, + { + "id": 3365817, + "title": "WordPress for iOS Support", + "description": "This is a test message", + "status": "closed", + "can_accept_reply": false, + "created_at": "2020-10-01T13:58:02+00:00", + "updated_at": "2020-10-08T15:02:33+00:00" + }, + { + "id": 2302960, + "title": "WordPress for iOS Support", + "description": "This is a test", + "status": "closed", + "can_accept_reply": false, + "created_at": "2019-08-26T21:51:40+00:00", + "updated_at": "2019-09-03T05:04:20+00:00" + } +] diff --git a/wp_api/tests/wpcom/unified_conversations/reply-response.json b/wp_api/tests/wpcom/unified_conversations/reply-response.json new file mode 100644 index 000000000..4d57265c2 --- /dev/null +++ b/wp_api/tests/wpcom/unified_conversations/reply-response.json @@ -0,0 +1,223 @@ +{ + "id": 4396575, + "title": "Would it be possible to chat with a human about this?", + "description": "Would it be possible to chat with a human about this?", + "status": "new", + "can_accept_reply": true, + "created_at": "2026-04-15T00:08:14+00:00", + "updated_at": "2026-04-16T18:01:05+00:00", + "messages": [ + { + "id": 17661874, + "message": "Would it be possible to chat with a human about this?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T16:54:23+00:00", + "attachments": [] + }, + { + "id": 17661877, + "message": "Sure! Help is on the way!", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T16:54:24+00:00", + "attachments": [] + }, + { + "id": 17661906, + "message": "Would it be possible to chat with a human about this?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T16:54:49+00:00", + "attachments": [] + }, + { + "id": 17661908, + "message": "Sure! Help is on the way!", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T16:54:50+00:00", + "attachments": [] + }, + { + "id": 17662078, + "message": "Would it be possible to chat with a human about this?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T16:56:18+00:00", + "attachments": [] + }, + { + "id": 17662081, + "message": "Sure! Help is on the way!", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T16:56:20+00:00", + "attachments": [] + }, + { + "id": 17668954, + "message": "What's the best way to use the app?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T17:54:09+00:00", + "attachments": [] + }, + { + "id": 17668974, + "message": "Great question! The Jetpack app is perfect for managing your site on the go - you can create posts, check your stats, respond to comments, and handle basic site management.\n\nWhat's your main goal with the app? Are you looking to publish content, track your site's performance, or manage something specific?", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T17:54:18+00:00", + "attachments": [ + { + "id": 20115252203262, + "filename": "Get help with the Jetpack app", + "content_type": "text/html", + "size": 2260, + "url": "http://jetpack.com/?p=203262", + "metadata": { + "score": 0.27002066 + } + }, + { + "id": 1080686167000, + "filename": "Getting Started", + "content_type": "text/html", + "size": 16835, + "url": "https://apps.wordpress.com/support/mobile/getting-started/", + "metadata": { + "score": 0.25811708 + } + }, + { + "id": 1080686168999, + "filename": "How can I create a Post?", + "content_type": "text/html", + "size": 1392, + "url": "https://apps.wordpress.com/support/mobile/posts-and-pages/how-can-i-create-a-post/", + "metadata": { + "score": 0.26508412 + } + }, + { + "id": 1080686169071, + "filename": "How to Install and Uninstall WordPress and Jetpack Apps", + "content_type": "text/html", + "size": 1061, + "url": "https://apps.wordpress.com/support/mobile/login-signup/install-uninstal-jetpack-worpress-app/", + "metadata": { + "score": 0.2639684 + } + }, + { + "id": 1080686165225, + "filename": "Mobile App Support", + "content_type": "text/html", + "size": 34, + "url": "https://apps.wordpress.com/support/mobile/", + "metadata": { + "score": 0.26189515 + } + }, + { + "id": 20115252202482, + "filename": "Switch to the Jetpack app", + "content_type": "text/html", + "size": 2275, + "url": "http://jetpack.com/?p=202482", + "metadata": { + "score": 0.2606449 + } + }, + { + "id": 1080686169013, + "filename": "How to find App Logs in Jetpack App", + "content_type": "text/html", + "size": 1305, + "url": "https://apps.wordpress.com/support/mobile/login-signup/how-to-find-app-logs-in-jetpack-app/", + "metadata": { + "score": 0.2566855 + } + }, + { + "id": 1080686165623, + "filename": "Image, Video, and Audio", + "content_type": "text/html", + "size": 314, + "url": "https://apps.wordpress.com/support/mobile/images-video-and-audio/", + "metadata": { + "score": 0.25576228 + } + } + ] + }, + { + "id": 17669825, + "message": "What's the best way to use the app?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T18:00:55+00:00", + "attachments": [] + }, + { + "id": 17669849, + "message": "The Jetpack app works best for day-to-day site management - creating posts, checking stats, managing comments, and updating content while you're away from your computer.\n\nWhat type of site are you managing, and what do you most want to accomplish with the app?", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T18:01:05+00:00", + "attachments": [ + { + "id": 1080686161843, + "filename": "Contribute", + "content_type": "text/html", + "size": 3388, + "url": "https://apps.wordpress.com/contribute/", + "metadata": { + "score": 0.27968985 + } + }, + { + "id": 1080686165225, + "filename": "Mobile App Support", + "content_type": "text/html", + "size": 34, + "url": "https://apps.wordpress.com/support/mobile/", + "metadata": { + "score": 0.2944926 + } + }, + { + "id": 20115252203262, + "filename": "Get help with the Jetpack app", + "content_type": "text/html", + "size": 2260, + "url": "http://jetpack.com/?p=203262", + "metadata": { + "score": 0.29033327 + } + }, + { + "id": 1080686169013, + "filename": "How to find App Logs in Jetpack App", + "content_type": "text/html", + "size": 1305, + "url": "https://apps.wordpress.com/support/mobile/login-signup/how-to-find-app-logs-in-jetpack-app/", + "metadata": { + "score": 0.28003126 + } + }, + { + "id": 1080686167000, + "filename": "Getting Started", + "content_type": "text/html", + "size": 16835, + "url": "https://apps.wordpress.com/support/mobile/getting-started/", + "metadata": { + "score": 0.28022778 + } + } + ] + } + ] +} diff --git a/wp_api/tests/wpcom/unified_conversations/single-conversation.json b/wp_api/tests/wpcom/unified_conversations/single-conversation.json new file mode 100644 index 000000000..4d57265c2 --- /dev/null +++ b/wp_api/tests/wpcom/unified_conversations/single-conversation.json @@ -0,0 +1,223 @@ +{ + "id": 4396575, + "title": "Would it be possible to chat with a human about this?", + "description": "Would it be possible to chat with a human about this?", + "status": "new", + "can_accept_reply": true, + "created_at": "2026-04-15T00:08:14+00:00", + "updated_at": "2026-04-16T18:01:05+00:00", + "messages": [ + { + "id": 17661874, + "message": "Would it be possible to chat with a human about this?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T16:54:23+00:00", + "attachments": [] + }, + { + "id": 17661877, + "message": "Sure! Help is on the way!", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T16:54:24+00:00", + "attachments": [] + }, + { + "id": 17661906, + "message": "Would it be possible to chat with a human about this?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T16:54:49+00:00", + "attachments": [] + }, + { + "id": 17661908, + "message": "Sure! Help is on the way!", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T16:54:50+00:00", + "attachments": [] + }, + { + "id": 17662078, + "message": "Would it be possible to chat with a human about this?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T16:56:18+00:00", + "attachments": [] + }, + { + "id": 17662081, + "message": "Sure! Help is on the way!", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T16:56:20+00:00", + "attachments": [] + }, + { + "id": 17668954, + "message": "What's the best way to use the app?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T17:54:09+00:00", + "attachments": [] + }, + { + "id": 17668974, + "message": "Great question! The Jetpack app is perfect for managing your site on the go - you can create posts, check your stats, respond to comments, and handle basic site management.\n\nWhat's your main goal with the app? Are you looking to publish content, track your site's performance, or manage something specific?", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T17:54:18+00:00", + "attachments": [ + { + "id": 20115252203262, + "filename": "Get help with the Jetpack app", + "content_type": "text/html", + "size": 2260, + "url": "http://jetpack.com/?p=203262", + "metadata": { + "score": 0.27002066 + } + }, + { + "id": 1080686167000, + "filename": "Getting Started", + "content_type": "text/html", + "size": 16835, + "url": "https://apps.wordpress.com/support/mobile/getting-started/", + "metadata": { + "score": 0.25811708 + } + }, + { + "id": 1080686168999, + "filename": "How can I create a Post?", + "content_type": "text/html", + "size": 1392, + "url": "https://apps.wordpress.com/support/mobile/posts-and-pages/how-can-i-create-a-post/", + "metadata": { + "score": 0.26508412 + } + }, + { + "id": 1080686169071, + "filename": "How to Install and Uninstall WordPress and Jetpack Apps", + "content_type": "text/html", + "size": 1061, + "url": "https://apps.wordpress.com/support/mobile/login-signup/install-uninstal-jetpack-worpress-app/", + "metadata": { + "score": 0.2639684 + } + }, + { + "id": 1080686165225, + "filename": "Mobile App Support", + "content_type": "text/html", + "size": 34, + "url": "https://apps.wordpress.com/support/mobile/", + "metadata": { + "score": 0.26189515 + } + }, + { + "id": 20115252202482, + "filename": "Switch to the Jetpack app", + "content_type": "text/html", + "size": 2275, + "url": "http://jetpack.com/?p=202482", + "metadata": { + "score": 0.2606449 + } + }, + { + "id": 1080686169013, + "filename": "How to find App Logs in Jetpack App", + "content_type": "text/html", + "size": 1305, + "url": "https://apps.wordpress.com/support/mobile/login-signup/how-to-find-app-logs-in-jetpack-app/", + "metadata": { + "score": 0.2566855 + } + }, + { + "id": 1080686165623, + "filename": "Image, Video, and Audio", + "content_type": "text/html", + "size": 314, + "url": "https://apps.wordpress.com/support/mobile/images-video-and-audio/", + "metadata": { + "score": 0.25576228 + } + } + ] + }, + { + "id": 17669825, + "message": "What's the best way to use the app?", + "author_role": "user", + "author_name": "jkmassel", + "created_at": "2026-04-16T18:00:55+00:00", + "attachments": [] + }, + { + "id": 17669849, + "message": "The Jetpack app works best for day-to-day site management - creating posts, checking stats, managing comments, and updating content while you're away from your computer.\n\nWhat type of site are you managing, and what do you most want to accomplish with the app?", + "author_role": "bot", + "author_name": "bot", + "created_at": "2026-04-16T18:01:05+00:00", + "attachments": [ + { + "id": 1080686161843, + "filename": "Contribute", + "content_type": "text/html", + "size": 3388, + "url": "https://apps.wordpress.com/contribute/", + "metadata": { + "score": 0.27968985 + } + }, + { + "id": 1080686165225, + "filename": "Mobile App Support", + "content_type": "text/html", + "size": 34, + "url": "https://apps.wordpress.com/support/mobile/", + "metadata": { + "score": 0.2944926 + } + }, + { + "id": 20115252203262, + "filename": "Get help with the Jetpack app", + "content_type": "text/html", + "size": 2260, + "url": "http://jetpack.com/?p=203262", + "metadata": { + "score": 0.29033327 + } + }, + { + "id": 1080686169013, + "filename": "How to find App Logs in Jetpack App", + "content_type": "text/html", + "size": 1305, + "url": "https://apps.wordpress.com/support/mobile/login-signup/how-to-find-app-logs-in-jetpack-app/", + "metadata": { + "score": 0.28003126 + } + }, + { + "id": 1080686167000, + "filename": "Getting Started", + "content_type": "text/html", + "size": 16835, + "url": "https://apps.wordpress.com/support/mobile/getting-started/", + "metadata": { + "score": 0.28022778 + } + } + ] + } + ] +} From bc3f97d43a40e23cdfcf010ec65271928d61e9c3 Mon Sep 17 00:00:00 2001 From: Jeremy Massel <1123407+jkmassel@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:25:50 -0600 Subject: [PATCH 2/2] Wire unified conversations into Kotlin and Swift example apps - Swift: expose `unifiedConversations` on `WPComApiClient`; add a read-only "Unified Conversations" row to the WPCom root list showing title, status, and created date. - Kotlin: add a list screen (tappable) and a detail screen showing messages, author role/name, and attachment count. Wired through `WpComSiteScreen` and `App.kt` navigation. Conversation ID passed through remembered state to match the existing pattern for post type / taxonomy navigation. --- .../kotlin/rs/wordpress/example/shared/App.kt | 37 +++++ .../shared/ui/wpcom/WpComSiteScreen.kt | 10 ++ .../wpcom/WpComUnifiedConversationScreen.kt | 146 ++++++++++++++++++ .../wpcom/WpComUnifiedConversationsScreen.kt | 133 ++++++++++++++++ .../Example/Services/WPComService.swift | 13 ++ .../wordpress-api/WPComApiClient.swift | 4 + 6 files changed, 343 insertions(+) create mode 100644 native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationScreen.kt create mode 100644 native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationsScreen.kt diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt index cb6da0a7d..d25fef25b 100644 --- a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/App.kt @@ -46,6 +46,9 @@ import rs.wordpress.example.shared.ui.wpcom.WpComBotConversationsScreen import rs.wordpress.example.shared.ui.wpcom.WpComMeScreen import rs.wordpress.example.shared.ui.wpcom.WpComSiteScreen import rs.wordpress.example.shared.ui.wpcom.WpComSupportConversationsScreen +import rs.wordpress.example.shared.ui.wpcom.WpComUnifiedConversationScreen +import rs.wordpress.example.shared.ui.wpcom.WpComUnifiedConversationsScreen +import uniffi.wp_api.ConversationId import uniffi.wp_api.PostEndpointType import uniffi.wp_api.TermEndpointType import uniffi.wp_mobile.Account @@ -65,6 +68,7 @@ fun App(authenticationEnabled: Boolean, authenticateSite: (String, onSuccess: () var currentSiteViewModel by remember { mutableStateOf(null) } var currentPostType by remember { mutableStateOf(null) } var currentTaxonomy by remember { mutableStateOf(null) } + var currentUnifiedConversationId by remember { mutableStateOf(null) } MaterialTheme { NavHost(navController, startDestination = "welcome") { @@ -364,6 +368,7 @@ fun App(authenticationEnabled: Boolean, authenticateSite: (String, onSuccess: () WpComSiteScreen( onMeClicked = { navController.navigate("wpcom_me") }, onSupportConversationsClicked = { navController.navigate("wpcom_support") }, + onUnifiedConversationsClicked = { navController.navigate("wpcom_unified") }, onBotConversationsClicked = { navController.navigate("wpcom_bots") }, onBackClicked = { navController.popBackStack() } ) @@ -401,6 +406,38 @@ fun App(authenticationEnabled: Boolean, authenticateSite: (String, onSuccess: () onBackClicked = { navController.popBackStack() } ) } + composable("wpcom_unified") { + val wpComClient = currentWpComClient + if (wpComClient == null) { + ErrorMessage("No WordPress.com account connected") + return@composable + } + WpComUnifiedConversationsScreen( + wpComApiClient = wpComClient, + onConversationClicked = { conversationId -> + currentUnifiedConversationId = conversationId + navController.navigate("wpcom_unified_detail") + }, + onBackClicked = { navController.popBackStack() } + ) + } + composable("wpcom_unified_detail") { + val wpComClient = currentWpComClient + val conversationId = currentUnifiedConversationId + if (wpComClient == null) { + ErrorMessage("No WordPress.com account connected") + return@composable + } + if (conversationId == null) { + ErrorMessage("No conversation selected") + return@composable + } + WpComUnifiedConversationScreen( + wpComApiClient = wpComClient, + conversationId = conversationId, + onBackClicked = { navController.popBackStack() } + ) + } } } } diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComSiteScreen.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComSiteScreen.kt index 576746b93..36dbb051c 100644 --- a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComSiteScreen.kt +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComSiteScreen.kt @@ -24,6 +24,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview fun WpComSiteScreen( onMeClicked: () -> Unit, onSupportConversationsClicked: () -> Unit, + onUnifiedConversationsClicked: () -> Unit, onBotConversationsClicked: () -> Unit, onBackClicked: () -> Unit = {} ) { @@ -60,6 +61,15 @@ fun WpComSiteScreen( modifier = Modifier.clickable(onClick = onSupportConversationsClicked) ) } + item { + ListItem( + headlineContent = { Text("Unified Conversations") }, + trailingContent = { + Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = null) + }, + modifier = Modifier.clickable(onClick = onUnifiedConversationsClicked) + ) + } item { ListItem( headlineContent = { Text("Bot Conversations") }, diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationScreen.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationScreen.kt new file mode 100644 index 000000000..ccb959764 --- /dev/null +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationScreen.kt @@ -0,0 +1,146 @@ +package rs.wordpress.example.shared.ui.wpcom + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.ConversationId +import uniffi.wp_api.UnifiedConversation +import uniffi.wp_api.UnifiedMessage + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Preview +fun WpComUnifiedConversationScreen( + wpComApiClient: WpComApiClient, + conversationId: ConversationId, + onBackClicked: () -> Unit = {} +) { + var conversation by remember { mutableStateOf(null) } + var error by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(true) } + + LaunchedEffect(conversationId) { + when ( + val result = wpComApiClient.request { + it.unifiedConversations().getUnifiedConversation(conversationId) + } + ) { + is WpRequestResult.Success -> { + conversation = result.response.data + isLoading = false + } + else -> { + error = result.toString() + isLoading = false + } + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(conversation?.title ?: "Conversation") }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + } + ) { paddingValues -> + when { + isLoading -> { + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + error != null -> { + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Text( + text = "Error: $error", + color = MaterialTheme.colorScheme.error + ) + } + } + conversation != null -> { + val messages = conversation!!.messages + LazyColumn( + modifier = Modifier.fillMaxSize().padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(messages) { message -> + MessageRow(message) + HorizontalDivider() + } + } + } + } + } +} + +@Composable +private fun MessageRow(message: UnifiedMessage) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + ) { + Text( + text = "${message.authorName} (${message.authorRole})", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = message.message, + style = MaterialTheme.typography.bodyMedium + ) + if (message.attachments.isNotEmpty()) { + Text( + text = "Attachments: ${message.attachments.size}", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.padding(top = 4.dp) + ) + } + Text( + text = formatUnifiedDate(message.createdAt), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.outline, + modifier = Modifier.padding(top = 4.dp) + ) + } +} diff --git a/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationsScreen.kt b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationsScreen.kt new file mode 100644 index 000000000..223754965 --- /dev/null +++ b/native/kotlin/example/composeApp/src/commonMain/kotlin/rs/wordpress/example/shared/ui/wpcom/WpComUnifiedConversationsScreen.kt @@ -0,0 +1,133 @@ +package rs.wordpress.example.shared.ui.wpcom + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import org.jetbrains.compose.ui.tooling.preview.Preview +import rs.wordpress.api.kotlin.WpComApiClient +import rs.wordpress.api.kotlin.WpRequestResult +import uniffi.wp_api.ConversationId +import uniffi.wp_api.UnifiedConversationSummary +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +@Preview +fun WpComUnifiedConversationsScreen( + wpComApiClient: WpComApiClient, + onConversationClicked: (ConversationId) -> Unit, + onBackClicked: () -> Unit = {} +) { + var conversations by remember { mutableStateOf>(emptyList()) } + var error by remember { mutableStateOf(null) } + var isLoading by remember { mutableStateOf(true) } + + LaunchedEffect(Unit) { + when ( + val result = wpComApiClient.request { + it.unifiedConversations().getUnifiedConversationList() + } + ) { + is WpRequestResult.Success -> { + conversations = result.response.data + isLoading = false + } + else -> { + error = result.toString() + isLoading = false + } + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text("Unified Conversations") }, + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + ) + } + ) { paddingValues -> + when { + isLoading -> { + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + error != null -> { + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Text( + text = "Error: $error", + color = MaterialTheme.colorScheme.error + ) + } + } + conversations.isEmpty() -> { + Box( + modifier = Modifier.fillMaxSize().padding(paddingValues), + contentAlignment = Alignment.Center + ) { + Text("No unified conversations found.") + } + } + else -> { + LazyColumn( + modifier = Modifier.fillMaxSize().padding(paddingValues) + ) { + items(conversations) { conversation -> + ListItem( + headlineContent = { Text(conversation.title) }, + supportingContent = { + Text("${conversation.status} · ${formatUnifiedDate(conversation.updatedAt)}") + }, + modifier = Modifier.clickable { + onConversationClicked(conversation.id) + } + ) + } + } + } + } + } +} + +internal fun formatUnifiedDate(date: Date): String { + return try { + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(date) + } catch (e: Exception) { + date.toString() + } +} diff --git a/native/swift/Example/Example/Services/WPComService.swift b/native/swift/Example/Example/Services/WPComService.swift index 37cfa4b6c..9d8b10751 100644 --- a/native/swift/Example/Example/Services/WPComService.swift +++ b/native/swift/Example/Example/Services/WPComService.swift @@ -27,6 +27,19 @@ final class WPComService: ObservableObject { } }, + RootListData(name: "Unified Conversations", category: .system) { + let client = try await WPComApiClient.instance(loginManager: self.loginManager) + return try await client.unifiedConversations.getUnifiedConversationList().data + .map { + ListViewData( + id: String($0.id), + title: $0.title, + subtitle: "\($0.status) · \($0.createdAt.formatted())", + fields: [:] + ) + } + }, + RootListData( name: "Bot Conversations", category: .system, diff --git a/native/swift/Sources/wordpress-api/WPComApiClient.swift b/native/swift/Sources/wordpress-api/WPComApiClient.swift index 09468c00d..044674faf 100644 --- a/native/swift/Sources/wordpress-api/WPComApiClient.swift +++ b/native/swift/Sources/wordpress-api/WPComApiClient.swift @@ -100,6 +100,10 @@ public final class WPComApiClient: Sendable { internalClient.supportTickets() } + public var unifiedConversations: UnifiedConversationsRequestExecutor { + internalClient.unifiedConversations() + } + public var statsVisits: StatsVisitsRequestExecutor { internalClient.statsVisits() }