Skip to content

Commit

Permalink
Merge pull request #21114 from wordpress-mobile/feature/feedback-form…
Browse files Browse the repository at this point in the history
…-attachments

Feature: Feedback form attachments
  • Loading branch information
nbradbury authored Aug 8, 2024
2 parents 4d9631b + bd868ee commit 27b43d3
Show file tree
Hide file tree
Showing 14 changed files with 759 additions and 59 deletions.
4 changes: 1 addition & 3 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ class ZendeskHelper(
selectedSite: SiteModel?,
extraTags: List<String>?,
requestDescription: String,
callback: CreateRequestCallback
callback: CreateRequestCallback,
attachmentTokens: List<String> = emptyList()
) {
require(isZendeskEnabled) {
zendeskNeedsToBeEnabledError
Expand All @@ -186,6 +187,7 @@ class ZendeskHelper(
ticketFormId = TicketFieldIds.form
tags = buildZendeskTags(siteStore.sites, selectedSite, origin ?: Origin.UNKNOWN, extraTags)
.plus("DocsBot")
attachments = attachmentTokens
}

Support.INSTANCE.provider()?.requestProvider()?.createRequest(request, object : ZendeskCallback<Request>() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.wordpress.android.support

import com.zendesk.service.ErrorResponse
import com.zendesk.service.ZendeskCallback
import kotlinx.coroutines.suspendCancellableCoroutine
import org.wordpress.android.util.AppLog
import org.wordpress.android.util.AppLog.T
import org.wordpress.android.util.extensions.mimeType
import zendesk.support.Support
import zendesk.support.UploadResponse
import java.io.File
import java.io.IOException
import javax.inject.Inject
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

/**
* https://zendesk.github.io/mobile_sdk_javadocs/supportv2/v301/index.html?zendesk/support/UploadProvider.html
*/
class ZendeskUploadHelper @Inject constructor() {
/**
* Uploads multiple file attachments to Zendesk and returns a list of their tokens when
* all uploads have completed
*/
suspend fun uploadFileAttachments(
files: List<File>
) = suspendCancellableCoroutine { continuation ->
val uploadProvider = Support.INSTANCE.provider()?.uploadProvider()
if (uploadProvider == null) {
continuation.resumeWithException(IOException("Unable to upload attachments (null provider)"))
return@suspendCancellableCoroutine
}

val tokens = ArrayList<String>()
var numAttachments = files.size

val callback = object : ZendeskCallback<UploadResponse>() {
override fun onSuccess(result: UploadResponse) {
if (continuation.isActive) {
result.token?.let {
tokens.add(it)
}
numAttachments--
if (numAttachments <= 0) {
continuation.resume(tokens)
}
}
}

override fun onError(errorResponse: ErrorResponse?) {
if (continuation.isActive) {
continuation.resumeWithException(
IOException("Uploading to Zendesk failed with ${errorResponse?.reason}")
)
}
}
}
for (file in files) {
uploadProvider.uploadAttachment(
file.name,
file,
file.mimeType() ?: "",
callback
)
}
}

/**
* Deletes an attachment from Zendesk. This is currently used only during development.
*/
@Suppress("unused")
fun deleteAttachment(token: String) {
val uploadProvider = Support.INSTANCE.provider()?.uploadProvider()
if (uploadProvider == null) {
AppLog.e(T.SUPPORT, "Upload provider is null")
return
}
uploadProvider.deleteAttachment(token, object : ZendeskCallback<Void>() {
override fun onSuccess(result: Void) {
AppLog.i(T.SUPPORT, "Successfully deleted Zendesk attachment")
}

override fun onError(errorResponse: ErrorResponse?) {
AppLog.e(T.SUPPORT, "Unable to delete Zendesk attachment: ${errorResponse?.reason}")
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package org.wordpress.android.ui.compose.components

import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import kotlinx.coroutines.flow.Flow
import org.wordpress.android.R
import org.wordpress.android.ui.compose.theme.M3Theme

@Composable
fun ProgressDialog(progressDialogState: Flow<ProgressDialogState?>) {
progressDialogState.collectAsState(initial = null).value?.let {
ProgressDialog(progressDialogState = it)
}
}

@Composable
fun ProgressDialog(progressDialogState: ProgressDialogState) {
M3Theme {
val dialogProps = DialogProperties(
dismissOnBackPress = progressDialogState.dismissible,
dismissOnClickOutside = progressDialogState.dismissible,
usePlatformDefaultWidth = false
)
Dialog(onDismissRequest = progressDialogState.onDismiss, dialogProps) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(8.dp))
) {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.defaultMinSize(100.dp, 100.dp)
) {
if (progressDialogState.progress != null) {
CircularProgressIndicator(progress = {
progressDialogState.progress
}, Modifier.padding(16.dp))
} else {
CircularProgressIndicator(Modifier.padding(16.dp))
}
if (progressDialogState.message != null) {
Text(
text = LocalContext.current.getString(progressDialogState.message),
Modifier.padding(16.dp, 0.dp, 16.dp, 16.dp),
color = MaterialTheme.colorScheme.onSurface
)
}
if (progressDialogState.showCancel) {
OutlinedButton(
onClick = progressDialogState.onDismiss,
shape = RoundedCornerShape(20.dp),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)),
modifier = Modifier.padding(16.dp, 0.dp, 16.dp, 16.dp)
) {
Text(
text = stringResource(id = R.string.cancel),
Modifier.padding(2.dp),
color = MaterialTheme.colorScheme.onSurface
)
}
}
}
}
}
}
}

data class ProgressDialogState(
@StringRes val message: Int? = null,
val progress: Float? = null,
val showCancel: Boolean = false,
val dismissible: Boolean = true,
val onDismiss: () -> Unit = {}
)

@Preview(
name = "Light Mode",
showBackground = true
)
@Preview(
name = "Dark Mode",
showBackground = true,
uiMode = Configuration.UI_MODE_NIGHT_YES,
)
@Composable
fun ProgressDialogPreview() {
ProgressDialog(
progressDialogState = ProgressDialogState(
message = R.string.uploading,
showCancel = true,
progress = 50f / 100f,
)
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.wordpress.android.ui.main.feedbackform

import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.activity.viewModels
Expand All @@ -8,6 +9,7 @@ import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import dagger.hilt.android.AndroidEntryPoint
import org.wordpress.android.ui.LocaleAwareActivity
import org.wordpress.android.ui.RequestCodes

@AndroidEntryPoint
class FeedbackFormActivity : LocaleAwareActivity() {
Expand All @@ -25,7 +27,8 @@ class FeedbackFormActivity : LocaleAwareActivity() {
setContent {
FeedbackFormScreen(
messageText = viewModel.messageText.collectAsState(),
isProgressShowing = viewModel.isProgressShowing.collectAsState(),
progressDialogState = viewModel.progressDialogState.collectAsState(),
attachments = viewModel.attachments.collectAsState(),
onMessageChanged = {
viewModel.updateMessageText(it)
},
Expand All @@ -34,10 +37,27 @@ class FeedbackFormActivity : LocaleAwareActivity() {
},
onCloseClick = {
viewModel.onCloseClick(this@FeedbackFormActivity)
},
onChooseMediaClick = {
viewModel.onChooseMediaClick(this@FeedbackFormActivity)
},
onRemoveMediaClick = {
viewModel.onRemoveMediaClick(it)
}
)
}
}
)
}

@Deprecated("Deprecated in Java")
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)

if (requestCode == RequestCodes.PHOTO_PICKER) {
data?.let {
viewModel.onPhotoPickerResult(this, it)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.wordpress.android.ui.main.feedbackform

import android.net.Uri
import java.io.File

data class FeedbackFormAttachment(
val uri: Uri,
val tempFile: File,
val displayName: String,
val mimeType: String,
val attachmentType: FeedbackFormAttachmentType,
val size: Long,
)

enum class FeedbackFormAttachmentType {
IMAGE,
VIDEO,
}
Loading

0 comments on commit 27b43d3

Please sign in to comment.