Skip to content

Commit 7b13497

Browse files
authored
CMM-885 support HE attachments (#22323)
* Adding basic UI * Renaming * Some styling * Renaming and dummy data * Using proper "new conversation icon" * Conversation details screen * Creating the reply bottomsheet * Linking to the support screen * bottomsheet fix * Mov navigation form activity to viewmodel * Adding create ticket screen * More screen adjustments * Extracting common code * Margin fix * detekt * Style * New ticket check * Creating tests * Creating repository and load conversations function * Adding createConversation function * Creating loadConversation func * Loading conversations form the viewmodel * Adding loading spinner * Pull to refresh * Proper ionitialization * Adding empty screen * Handling send new conversation * Show loading when sending * New ticket creation fix * Using snackbar for errors * Error handling * Answering conversation * Adding some test to the repository * More tests! * Compile fixes * Similarities improvements * Using snackbar in bots activity * Extracting EmptyConversationsView * Renaming * Extracting VM and UI common code * Extracting navigation common code * Renaming VMs for clarification * More refactor * Capitalise text fields * Updating rs library * Loading conversation UX * Style fix * Fixing scaffolds paddings * userID fix * Fixing the padding problem in bot chat when the keyboard is opened * Apply padding to create ticket screen when the keyboard is opened * Fixing scroll state in reply bottomsheet * Adding tests for the new common viewmodel * Fixing AIBotSupportViewModel tests * detekt * Improvements int he conversation interaction * Adding tests for HE VM * Saving draft state * Properly navigating when a ticket is selected * Error parsing improvement * accessToken suggestion improvements * General suggestions * Send message error UX improvement * Fixing tests * Converting the UI to more AndroidMaterial style * Bots screen renaming * Bots screens renaming * Make NewTicket screen more Android Material theme as well * Adding preview for EmptyConversationsView * Button fix * detekt * Ticket selection change * Supporting markdown text * detekt * Improving MarkdownUtils * Formatting text in the repository layer instead the ui * Renaming * Fixing tests * Support pagination * Triggering in the 4th element * detekt * TODO for debug purposes * Claude PR suggestions Mutex and constant * Put ConversationListView in common between bots and HE * Empty and error state * Skip site capitalization * Adding a11c labels * Adding headings labels * adding accessible labels to chat bubbles * detekt * Fixing tests * PR suggestion about bot chat bubble * Fixing tests * Updating rust * Adding attachments UI * Parsing markdown more exhaustively * New links support * Detekt * Supporting in conversation as well * Keeping the screen when select images * Add attachments to the message data class * Showing attachments in the UI * Downloading attachments * detekt * Support pagination * Triggering in the 4th element * detekt * TODO for debug purposes * Claude PR suggestions Mutex and constant * Detekt * Removing testing code * Updating RS library version * Opening images in fullscreen * Improving full screen image UX * Improving semantics * Extracting strings * Using rs PR fix * Showing attachment preview * Clearing attachments on new ticket screen close * Removing selected images limit * Unifying attachments handling inside the VM * Using a launcher instead of startActivityForResult * Remove unused parameter * Handling temp files inside the VM * Removing files * detekt * Throwing copy file error * Extracting some individual composables from HEConversation screen file * Reducing arguments * Catch file creation error * Using proper file extension * General improvements * Update RS version and some fixes * Extracting temp attachment utils * Adding new tests * Some refactoring * Removing attachments preview to open a dedicated PR * Useless changes * Useless changes * Minor refactor * String fix * CMM-885 support HE attachments i2 (#22333) * Showing attachments previews * Typo * Fixing pan issue * Passing attachments directly instead of searching for then when tapped for full screen * Compile fix * Fixing the send state message * Checking network availability * Saving message state when error * Tests * Reverting non-related commits done by mistake
1 parent 7f795cb commit 7b13497

17 files changed

+1430
-258
lines changed

WordPress/src/main/java/org/wordpress/android/support/he/model/SupportMessage.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,15 @@ data class SupportMessage(
1111
val formattedText: AnnotatedString,
1212
val createdAt: Date,
1313
val authorName: String,
14-
val authorIsUser: Boolean
14+
val authorIsUser: Boolean,
15+
val attachments: List<SupportAttachment>,
1516
)
17+
18+
data class SupportAttachment (
19+
val id: Long,
20+
val filename: String,
21+
val url: String,
22+
val type: AttachmentType,
23+
)
24+
25+
enum class AttachmentType { Image, Video, Other }

WordPress/src/main/java/org/wordpress/android/support/he/repository/HESupportRepository.kt

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import kotlinx.coroutines.withContext
55
import org.wordpress.android.fluxc.utils.AppLogWrapper
66
import org.wordpress.android.modules.IO_THREAD
77
import org.wordpress.android.networking.restapi.WpComApiClientProvider
8+
import org.wordpress.android.support.he.model.AttachmentType
9+
import org.wordpress.android.support.he.model.SupportAttachment
810
import org.wordpress.android.support.he.model.SupportConversation
911
import org.wordpress.android.support.he.model.SupportMessage
1012
import org.wordpress.android.ui.compose.utils.markdownToAnnotatedString
@@ -185,23 +187,40 @@ class HESupportRepository @Inject constructor(
185187

186188
private fun uniffi.wp_api.SupportConversation.toSupportConversation(): SupportConversation =
187189
SupportConversation(
188-
id = this.id.toLong(),
189-
title = this.title,
190-
description = this.description,
191-
lastMessageSentAt = this.updatedAt,
192-
messages = this.messages.map { it.toSupportMessage() }
190+
id = id.toLong(),
191+
title = title,
192+
description = description,
193+
lastMessageSentAt = updatedAt,
194+
messages = messages.map { it.toSupportMessage() }
193195
)
194196

195197
private fun uniffi.wp_api.SupportMessage.toSupportMessage(): SupportMessage =
196198
SupportMessage(
197-
id = this.id.toLong(),
198-
rawText = this.content,
199-
formattedText = markdownToAnnotatedString(this.content),
200-
createdAt = this.createdAt,
201-
authorName = when (this.author) {
202-
is SupportMessageAuthor.User -> (this.author as SupportMessageAuthor.User).v1.displayName
203-
is SupportMessageAuthor.SupportAgent -> (this.author as SupportMessageAuthor.SupportAgent).v1.name
199+
id = id.toLong(),
200+
rawText = content,
201+
formattedText = markdownToAnnotatedString(content),
202+
createdAt = createdAt,
203+
authorName = when (author) {
204+
is SupportMessageAuthor.User -> (author as SupportMessageAuthor.User).v1.displayName
205+
is SupportMessageAuthor.SupportAgent -> (author as SupportMessageAuthor.SupportAgent).v1.name
204206
},
205-
authorIsUser = this.authorIsCurrentUser
207+
authorIsUser = authorIsCurrentUser,
208+
attachments = attachments.map { it.toSupportAttachment() }
206209
)
210+
211+
private fun uniffi.wp_api.SupportAttachment.toSupportAttachment(): SupportAttachment =
212+
SupportAttachment(
213+
id = id.toLong(),
214+
filename = filename,
215+
url = url,
216+
type = determineAttachmentType(contentType)
217+
)
218+
219+
private fun determineAttachmentType(contentType: String): AttachmentType {
220+
return when {
221+
contentType.startsWith("image/") -> AttachmentType.Image
222+
contentType.startsWith("video/") -> AttachmentType.Video
223+
else -> AttachmentType.Other
224+
}
225+
}
207226
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package org.wordpress.android.support.he.ui
2+
3+
import android.content.res.Configuration.UI_MODE_NIGHT_YES
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.clickable
6+
import androidx.compose.foundation.gestures.detectTransformGestures
7+
import androidx.compose.foundation.layout.Arrangement
8+
import androidx.compose.foundation.layout.Box
9+
import androidx.compose.foundation.layout.Row
10+
import androidx.compose.foundation.layout.fillMaxSize
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.size
13+
import androidx.compose.foundation.shape.RoundedCornerShape
14+
import androidx.compose.material.icons.Icons
15+
import androidx.compose.material.icons.filled.Close
16+
import androidx.compose.material3.CircularProgressIndicator
17+
import androidx.compose.material3.Icon
18+
import androidx.compose.material3.IconButton
19+
import androidx.compose.material3.Surface
20+
import androidx.compose.runtime.Composable
21+
import androidx.compose.runtime.getValue
22+
import androidx.compose.runtime.mutableFloatStateOf
23+
import androidx.compose.runtime.remember
24+
import androidx.compose.runtime.setValue
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.graphics.Color
28+
import androidx.compose.ui.graphics.graphicsLayer
29+
import androidx.compose.ui.input.pointer.pointerInput
30+
import androidx.compose.ui.layout.ContentScale
31+
import androidx.compose.ui.platform.LocalContext
32+
import androidx.compose.ui.res.painterResource
33+
import androidx.compose.ui.res.stringResource
34+
import androidx.compose.ui.semantics.contentDescription
35+
import androidx.compose.ui.semantics.semantics
36+
import androidx.compose.ui.tooling.preview.Preview
37+
import androidx.compose.ui.unit.dp
38+
import androidx.compose.ui.window.Dialog
39+
import androidx.compose.ui.window.DialogProperties
40+
import coil.compose.SubcomposeAsyncImage
41+
import coil.request.ImageRequest
42+
import org.wordpress.android.R
43+
import org.wordpress.android.ui.compose.theme.AppThemeM3
44+
45+
@Composable
46+
fun AttachmentFullscreenImagePreview(
47+
imageUrl: String,
48+
onDismiss: () -> Unit,
49+
onDownload: () -> Unit = {}
50+
) {
51+
var scale by remember { mutableFloatStateOf(1f) }
52+
var offsetX by remember { mutableFloatStateOf(0f) }
53+
var offsetY by remember { mutableFloatStateOf(0f) }
54+
55+
// Load semantics
56+
val loadingImageDescription = stringResource(R.string.he_support_loading_image)
57+
val attachmentImageDescription = stringResource(R.string.he_support_attachment_image)
58+
val failedToLoadImageDescription = stringResource(R.string.he_support_failed_to_load_image)
59+
60+
Dialog(
61+
onDismissRequest = onDismiss,
62+
properties = DialogProperties(
63+
usePlatformDefaultWidth = false,
64+
dismissOnBackPress = true,
65+
dismissOnClickOutside = false
66+
)
67+
) {
68+
Surface(
69+
modifier = Modifier
70+
.fillMaxSize()
71+
.clickable(onClick = onDismiss),
72+
color = Color.Black
73+
) {
74+
Box(
75+
modifier = Modifier.fillMaxSize()
76+
) {
77+
CircularProgressIndicator(
78+
modifier = Modifier
79+
.align(Alignment.Center)
80+
.semantics {
81+
contentDescription = loadingImageDescription
82+
}
83+
)
84+
// Zoomable image
85+
Box(
86+
modifier = Modifier.fillMaxSize(),
87+
contentAlignment = Alignment.Center
88+
) {
89+
SubcomposeAsyncImage(
90+
model = ImageRequest.Builder(LocalContext.current)
91+
.data(imageUrl)
92+
.crossfade(true)
93+
.build(),
94+
contentDescription = attachmentImageDescription,
95+
modifier = Modifier
96+
.fillMaxSize()
97+
.graphicsLayer(
98+
scaleX = scale,
99+
scaleY = scale,
100+
translationX = offsetX,
101+
translationY = offsetY
102+
)
103+
.pointerInput(Unit) {
104+
detectTransformGestures { _, pan, zoom, _ ->
105+
val previousScale = scale
106+
scale = (scale * zoom).coerceIn(1f, 5f)
107+
108+
if (scale > 1f) {
109+
// Calculate max pan bounds to prevent image from going off-screen
110+
val maxOffsetX = (size.width * (scale - 1f)) / 2f
111+
val maxOffsetY = (size.height * (scale - 1f)) / 2f
112+
113+
offsetX = (offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX)
114+
offsetY = (offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY)
115+
} else if (previousScale > 1f && scale == 1f) {
116+
// Only reset when transitioning from zoomed to unzoomed
117+
offsetX = 0f
118+
offsetY = 0f
119+
}
120+
}
121+
},
122+
contentScale = ContentScale.Fit,
123+
error = {
124+
Icon(
125+
painter = painterResource(R.drawable.ic_image_white_24dp),
126+
contentDescription = failedToLoadImageDescription,
127+
tint = Color.White,
128+
modifier = Modifier.size(48.dp)
129+
)
130+
}
131+
)
132+
}
133+
134+
// Top bar with close button
135+
Row(
136+
modifier = Modifier
137+
.align(Alignment.TopEnd)
138+
.padding(16.dp)
139+
.background(
140+
color = Color.Black.copy(alpha = 0.5f),
141+
shape = RoundedCornerShape(24.dp)
142+
)
143+
.padding(4.dp),
144+
horizontalArrangement = Arrangement.spacedBy(4.dp)
145+
) {
146+
// Download button
147+
IconButton(
148+
onClick = {
149+
onDownload.invoke()
150+
onDismiss.invoke()
151+
}
152+
) {
153+
Icon(
154+
painter = painterResource(R.drawable.ic_get_app_white_24dp),
155+
contentDescription = stringResource(R.string.he_support_download_attachment),
156+
tint = Color.White,
157+
modifier = Modifier.size(24.dp)
158+
)
159+
}
160+
161+
// Close button
162+
IconButton(
163+
onClick = onDismiss
164+
) {
165+
Icon(
166+
imageVector = Icons.Filled.Close,
167+
contentDescription = stringResource(R.string.close),
168+
tint = Color.White,
169+
modifier = Modifier.size(24.dp)
170+
)
171+
}
172+
}
173+
}
174+
}
175+
}
176+
}
177+
178+
@Preview(showBackground = true, name = "Fullscreen Image Preview")
179+
@Composable
180+
private fun AttachmentFullscreenImagePreviewPreview() {
181+
AppThemeM3(isDarkTheme = false) {
182+
AttachmentFullscreenImagePreview(
183+
imageUrl = "https://via.placeholder.com/800x600",
184+
onDismiss = { },
185+
onDownload = { }
186+
)
187+
}
188+
}
189+
190+
@Preview(showBackground = true, name = "Fullscreen Image Preview - Dark", uiMode = UI_MODE_NIGHT_YES)
191+
@Composable
192+
private fun AttachmentFullscreenImagePreviewPreviewDark() {
193+
AppThemeM3(isDarkTheme = true) {
194+
AttachmentFullscreenImagePreview(
195+
imageUrl = "https://via.placeholder.com/800x600",
196+
onDismiss = { },
197+
onDownload = { }
198+
)
199+
}
200+
}

0 commit comments

Comments
 (0)