Skip to content

Commit

Permalink
Sync backend changes: Migrate image upload from Firebase (#17)
Browse files Browse the repository at this point in the history
Fixes #16
  • Loading branch information
shank03 authored Oct 11, 2023
2 parents 591b2a8 + 411b572 commit acee31b
Show file tree
Hide file tree
Showing 19 changed files with 195 additions and 96 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mnnit.moticlubs.data.network

import com.mnnit.moticlubs.data.network.api.AvatarApi
import com.mnnit.moticlubs.data.network.api.ChannelsApi
import com.mnnit.moticlubs.data.network.api.ClubApi
import com.mnnit.moticlubs.data.network.api.GithubApi
Expand All @@ -10,6 +11,7 @@ import com.mnnit.moticlubs.data.network.api.UserApi
import com.mnnit.moticlubs.data.network.api.ViewsApi

interface ApiService :
AvatarApi,
UserApi,
ClubApi,
PostsApi,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.mnnit.moticlubs.data.network.api

import com.mnnit.moticlubs.data.network.dto.ClubDto
import com.mnnit.moticlubs.data.network.dto.ImageUrlDto
import com.mnnit.moticlubs.data.network.dto.UserDto
import com.mnnit.moticlubs.domain.util.Constants.AUTHORIZATION_HEADER
import com.mnnit.moticlubs.domain.util.Constants.AVATAR_ROUTE
import com.mnnit.moticlubs.domain.util.Constants.CLUB_ID_CLAIM
import okhttp3.MultipartBody
import retrofit2.Response
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
import retrofit2.http.Query

interface AvatarApi {

@POST("$AVATAR_ROUTE/user")
@Multipart
suspend fun updateUserAvatar(
@Header(AUTHORIZATION_HEADER) auth: String?,
@Part file: MultipartBody.Part,
): Response<UserDto?>

@POST("$AVATAR_ROUTE/club")
@Multipart
suspend fun updateClubAvatar(
@Header(AUTHORIZATION_HEADER) auth: String?,
@Query(CLUB_ID_CLAIM) clubId: Long,
@Part file: MultipartBody.Part,
): Response<ClubDto?>

@POST("$AVATAR_ROUTE/post")
@Multipart
suspend fun uploadPostImage(
@Header(AUTHORIZATION_HEADER) auth: String?,
@Query(CLUB_ID_CLAIM) clubId: Long,
@Part file: MultipartBody.Part,
): Response<ImageUrlDto?>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.mnnit.moticlubs.data.network.api

import com.mnnit.moticlubs.data.network.dto.ClubModel
import com.mnnit.moticlubs.data.network.dto.ClubDto
import com.mnnit.moticlubs.data.network.dto.UpdateClubDto
import com.mnnit.moticlubs.domain.util.Constants.AUTHORIZATION_HEADER
import com.mnnit.moticlubs.domain.util.Constants.CLUB_ID_CLAIM
Expand All @@ -19,13 +19,13 @@ interface ClubApi {
suspend fun getClubs(
@Header(AUTHORIZATION_HEADER) auth: String?,
@Header(STAMP_HEADER) stamp: Long,
): Response<List<ClubModel>?>
): Response<List<ClubDto>?>

@PUT("$CLUB_ROUTE/{$CLUB_ID_CLAIM}")
suspend fun updateClub(
@Header(AUTHORIZATION_HEADER) auth: String?,
@Header(STAMP_HEADER) stamp: Long,
@Path(CLUB_ID_CLAIM) clubId: Long,
@Body data: UpdateClubDto,
): Response<ClubModel?>
): Response<ClubDto?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.mnnit.moticlubs.data.network.dto.AdminDetailDto
import com.mnnit.moticlubs.data.network.dto.FCMDto
import com.mnnit.moticlubs.data.network.dto.FCMTokenDto
import com.mnnit.moticlubs.data.network.dto.SaveUserDto
import com.mnnit.moticlubs.data.network.dto.UpdateUserAvatarDto
import com.mnnit.moticlubs.data.network.dto.UpdateUserContactDto
import com.mnnit.moticlubs.data.network.dto.UserDto
import com.mnnit.moticlubs.domain.util.Constants.AUTHORIZATION_HEADER
Expand Down Expand Up @@ -47,13 +46,6 @@ interface UserApi {
@Path(USER_ID_CLAIM) userId: Long,
): Response<UserDto?>

@PUT("$USER_ROUTE/avatar")
suspend fun setProfilePicUrl(
@Header(AUTHORIZATION_HEADER) auth: String?,
@Header(STAMP_HEADER) stamp: Long,
@Body avatar: UpdateUserAvatarDto,
): Response<UserDto?>

@PUT("$USER_ROUTE/contact")
suspend fun setContact(
@Header(AUTHORIZATION_HEADER) auth: String?,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize

@Parcelize
data class ClubModel(
data class ClubDto(
@SerializedName("cid")
@Expose
var clubId: Long,
Expand All @@ -33,10 +33,6 @@ data class UpdateClubDto(
@Expose
var description: String,

@SerializedName("avatar")
@Expose
var avatar: String,

@SerializedName("summary")
@Expose
var summary: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.mnnit.moticlubs.data.network.dto

import com.google.gson.annotations.Expose
import com.google.gson.annotations.SerializedName

data class ImageUrlDto(
@SerializedName("url")
@Expose
var url: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import com.mnnit.moticlubs.domain.util.ResponseStamp
import com.mnnit.moticlubs.domain.util.mapToDomain
import com.mnnit.moticlubs.domain.util.networkResource
import kotlinx.coroutines.flow.Flow
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File

class UpdateClub(private val repository: Repository) {

Expand All @@ -22,12 +25,26 @@ class UpdateClub(private val repository: Repository) {
club.clubId,
UpdateClubDto(
description = club.description,
avatar = club.avatar,
summary = club.summary,
),
)
},
saveResponse = { _, new -> repository.insertOrUpdateClub(new.mapToDomain()) },
remoteRequired = true,
)

operator fun invoke(clubId: Long, file: File): Flow<Resource<Club>> = repository.networkResource(
"Error updating club",
stampKey = ResponseStamp.CLUB,
query = { repository.getClub(clubID = clubId) },
apiCall = { apiService, auth, _ ->
apiService.updateClubAvatar(
auth,
clubId,
MultipartBody.Part.createFormData("file", file.name, file.asRequestBody()),
)
},
saveResponse = { _, new -> repository.insertOrUpdateClub(new.mapToDomain()) },
remoteRequired = true,
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.mnnit.moticlubs.domain.usecase.user

import com.mnnit.moticlubs.data.network.dto.UpdateUserAvatarDto
import com.mnnit.moticlubs.data.network.dto.UpdateUserContactDto
import com.mnnit.moticlubs.domain.model.User
import com.mnnit.moticlubs.domain.repository.Repository
Expand All @@ -9,18 +8,21 @@ import com.mnnit.moticlubs.domain.util.ResponseStamp
import com.mnnit.moticlubs.domain.util.mapToDomain
import com.mnnit.moticlubs.domain.util.networkResource
import kotlinx.coroutines.flow.Flow
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import java.io.File

class UpdateUser(private val repository: Repository) {

operator fun invoke(user: User): Flow<Resource<User>> = repository.networkResource(
operator fun invoke(userId: Long, file: File): Flow<Resource<User>> = repository.networkResource(
"Unable to update user",
stampKey = ResponseStamp.USER,
query = { user },
apiCall = { apiService, auth, stamp ->
apiService.setProfilePicUrl(
stampKey = object : ResponseStamp.StampKey("UserAvatar") {},
query = { repository.getUser(userId) ?: User() },
apiCall = { apiService, auth, _ ->
apiService.updateUserAvatar(
auth,
stamp,
UpdateUserAvatarDto(user.avatar),
// stamp,
MultipartBody.Part.createFormData("file", file.name, file.asRequestBody()),
)
},
saveResponse = { _, new -> repository.insertOrUpdateUser(new.mapToDomain()) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,16 @@ import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import android.widget.Toast
import com.google.firebase.storage.StorageReference
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream

object ImageUploadManager {

/**
* Uploads the given [imageUri] to Firebase [storageRef]
*
* @param onSuccess allows caller to handle the downloadUrl upon upload
*/
fun uploadImageToFirebase(
fun prepareImage(
context: Context,
imageUri: Uri,
loading: PublishedState<Boolean>,
storageRef: StorageReference,
onSuccess: (downloadUrl: String) -> Unit,
onSuccess: (file: File) -> Unit,
) {
if (!context.connectionAvailable()) {
Toast.makeText(context, "You're Offline", Toast.LENGTH_SHORT).show()
Expand All @@ -32,26 +26,25 @@ object ImageUploadManager {
val bitmap = compressBitmap(imageUri, context)
bitmap ?: return

val boas = ByteArrayOutputStream()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 100, boas)
} else {
bitmap.compress(Bitmap.CompressFormat.WEBP, 100, boas)
}
storageRef.putBytes(boas.toByteArray()).continueWithTask { task ->
if (!task.isSuccessful) {
Toast.makeText(context, "Error ${task.exception?.message}", Toast.LENGTH_SHORT).show()
loading.value = false
try {
val file = File(context.cacheDir, "tmp.webp")
if (!file.exists() && !file.createNewFile()) {
throw Exception("Unable to create temp webp file")
}
storageRef.downloadUrl
}.addOnCompleteListener { task ->
if (task.isSuccessful) {
val downloadUrl = task.result.toString()
onSuccess(downloadUrl)

val fos = FileOutputStream(file)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 100, fos)
} else {
Toast.makeText(context, "Error ${task.exception?.message}", Toast.LENGTH_SHORT).show()
loading.value = false
bitmap.compress(Bitmap.CompressFormat.WEBP, 100, fos)
}
bitmap.recycle()
fos.close()

onSuccess(file)
} catch (e: Exception) {
Toast.makeText(context, "Error ${e.message}", Toast.LENGTH_SHORT).show()
loading.value = false
}
}

Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/com/mnnit/moticlubs/domain/util/Mapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.mnnit.moticlubs.domain.util

import com.mnnit.moticlubs.data.network.dto.AdminDetailDto
import com.mnnit.moticlubs.data.network.dto.ChannelDto
import com.mnnit.moticlubs.data.network.dto.ClubModel
import com.mnnit.moticlubs.data.network.dto.ClubDto
import com.mnnit.moticlubs.data.network.dto.PostDto
import com.mnnit.moticlubs.data.network.dto.ReplyDto
import com.mnnit.moticlubs.data.network.dto.UrlResponseModel
Expand Down Expand Up @@ -40,7 +40,7 @@ fun AdminDetailDto.mapToDomain(): User =
contact = this.contact,
)

fun ClubModel.mapToDomain(): Club =
fun ClubDto.mapToDomain(): Club =
Club(
clubId = this.clubId,
name = this.name,
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/mnnit/moticlubs/domain/util/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ object Constants {

const val BASE_URL = "https://sac.mnnit.ac.in/moticlubs/"
private const val URL_PREFIX = "api/v1"
const val AVATAR_ROUTE = "$URL_PREFIX/avatar"
const val CHANNEL_ROUTE = "$URL_PREFIX/channel"
const val CLUB_ROUTE = "$URL_PREFIX/clubs"
const val POST_ROUTE = "$URL_PREFIX/posts"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ private fun Context.getUrlPainter(url: String, profile: Boolean): Painter {
it.load(url).placeholder(resID).error(resID)
},
key = url,
onError = { Log.d("TAG", "getProfileUrlPainter: network error") },
onError = { Log.d("TAG", "getProfileUrlPainter: network error: ${it.localizedMessage}") },
)
} else {
picasso.value.rememberPainter(
Expand All @@ -79,7 +79,7 @@ private fun Context.getUrlPainter(url: String, profile: Boolean): Painter {
},
key = url,
onError = {
Log.d("TAG", "getProfileUrlPainter: Error, fallback to network")
Log.d("TAG", "getProfileUrlPainter: Error, fallback to network: ${it.localizedMessage}")
error.value = true
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@ import com.canhub.cropper.CropImageContract
import com.canhub.cropper.CropImageContractOptions
import com.canhub.cropper.CropImageOptions
import com.canhub.cropper.CropImageView
import com.google.firebase.ktx.Firebase
import com.google.firebase.storage.ktx.storage
import com.mnnit.moticlubs.domain.util.ImageUploadManager
import com.mnnit.moticlubs.ui.viewmodel.ChannelScreenViewModel

Expand All @@ -47,24 +45,29 @@ fun PostTextFormatter(viewModel: ChannelScreenViewModel, modifier: Modifier = Mo
return@rememberLauncherForActivityResult
}

ImageUploadManager.uploadImageToFirebase(
ImageUploadManager.prepareImage(
context = context,
imageUri = uri,
loading = viewModel.showProgress,
storageRef = Firebase.storage.reference.child("post_images")
.child(viewModel.channelModel.channelId.toString())
.child(System.currentTimeMillis().toString()),
onSuccess = { downloadUrl ->
viewModel.showProgress.value = false
val post = viewModel.eventPostMsg.value.text
val selection = viewModel.eventPostMsg.value.selection
val urlLink = "\n<img src=\"$downloadUrl\">\n"
val msgLink = "\n[image_${viewModel.eventImageReplacerMap.value.size}]\n"
viewModel.eventImageReplacerMap.value[msgLink.replace("\n", "")] = urlLink

viewModel.eventPostMsg.value = TextFieldValue(
post.replaceRange(selection.start, selection.end, msgLink),
selection = TextRange(selection.end + msgLink.length, selection.end + msgLink.length),
onSuccess = { file ->
viewModel.uploadPostImage(
file,
onSuccess = { downloadUrl ->
viewModel.showProgress.value = false
val post = viewModel.eventPostMsg.value.text
val selection = viewModel.eventPostMsg.value.selection
val urlLink = "\n<img src=\"$downloadUrl\">\n"
val msgLink = "\n[image_${viewModel.eventImageReplacerMap.value.size}]\n"
viewModel.eventImageReplacerMap.value[msgLink.replace("\n", "")] = urlLink

viewModel.eventPostMsg.value = TextFieldValue(
post.replaceRange(selection.start, selection.end, msgLink),
selection = TextRange(selection.end + msgLink.length, selection.end + msgLink.length),
)
},
onFailure = {
Toast.makeText(context, "Unable to upload: $it", Toast.LENGTH_SHORT).show()
},
)
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ fun DescriptionComponent(viewModel: ClubDetailsScreenViewModel, modifier: Modifi

viewModel.progressMsg = "Updating"
viewModel.showProgressDialog.value = true
viewModel.updateClub(
viewModel.updateClubAvatar(
description = viewModel.displayedDescription,
onResponse = {
viewModel.showProgressDialog.value = false
Expand Down
Loading

0 comments on commit acee31b

Please sign in to comment.