Skip to content

Commit

Permalink
Implement media management api (without delete)
Browse files Browse the repository at this point in the history
  • Loading branch information
AleksandarIlic committed Nov 13, 2024
1 parent fa95f0c commit 8329e9e
Show file tree
Hide file tree
Showing 20 changed files with 334 additions and 134 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.primal.android.attachments.domain

data class CdnResource(
val eventId: String,
val url: String,
val contentType: String? = null,
val variants: List<CdnResourceVariant>? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ enum class PrimalVerb(val identifier: String) {
EXPLORE_PEOPLE("explore_people"),
EXPLORE_ZAPS("explore_zaps"),
CLIENT_CONFIG("client_config"),
MEDIA_MANAGEMENT_STATS("membership_media_management_stats"),
MEDIA_MANAGEMENT_UPLOADS("membership_media_management_uploads"),
MEDIA_MANAGEMENT_DELETE("membership_media_management_delete"),
WALLET("wallet"),
WALLET_MONITOR("wallet_monitor_2"),
WALLET_MEMBERSHIP_NAME_AVAILABLE("membership_name_available"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,8 @@ private fun List<PrimalEvent>.flatMapNotNullAsContentPrimalEventResources() =
fun List<PrimalEvent>.flatMapNotNullAsCdnResource() =
flatMapNotNullAsContentPrimalEventResources()
.flatMap {
val eventId = it.eventId
it.resources.map { eventResource ->
CdnResource(
eventId = eventId,
contentType = eventResource.mimeType,
url = eventResource.url,
variants = eventResource.variants.map { variant ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ enum class NostrEventKind(val value: Int) {
PrimalDvmFeedMetadata(value = 10_000_159),
PrimalTrendingTopics(value = 10_000_160),
PrimalClientConfig(value = 10_000_162),
PrimalUserMediaStorageStats(value = 10_000_163),
PrimalUserUploadInfo(value = 10_000_164),
PrimalWalletOperation(value = 10_000_300),
PrimalWalletBalance(value = 10_000_301),
PrimalWalletDepositInvoice(value = 10_000_302),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import kotlinx.serialization.Serializable

@Serializable
data class ContentPrimalEventResources(
@SerialName("event_id") val eventId: String,
val resources: List<EventResource>,
@SerialName("thumbnails") val videoThumbnails: Map<String, String> = emptyMap(),
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package net.primal.android.premium.manage.media

import net.primal.android.premium.manage.media.model.MediaUiItem
import net.primal.android.premium.manage.media.ui.MediaUiItem

interface PremiumMediaManagementContract {
data class UiState(
Expand All @@ -10,7 +10,6 @@ interface PremiumMediaManagementContract {
val videosInBytes: Long? = null,
val otherInBytes: Long? = null,
val mediaItems: List<MediaUiItem> = emptyList(),

val calculating: Boolean = true,
)
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
package net.primal.android.premium.manage.media

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import android.content.ClipData
import android.content.ClipboardManager
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import net.primal.android.R
import net.primal.android.core.compose.PrimalDivider
import net.primal.android.core.compose.PrimalTopAppBar
import net.primal.android.core.compose.icons.PrimalIcons
import net.primal.android.core.compose.icons.primaliconpack.ArrowBack
import net.primal.android.core.utils.ifNotNull
import net.primal.android.premium.manage.ui.MediaTable
import net.primal.android.premium.manage.ui.UsedStorageBreakdown
import net.primal.android.premium.manage.media.ui.MediaListItem
import net.primal.android.premium.manage.media.ui.TableHeader
import net.primal.android.premium.manage.media.ui.UsedStorageBreakdown
import net.primal.android.theme.AppTheme

@Composable
fun PremiumMediaManagementScreen(viewModel: PremiumMediaManagementViewModel, onClose: () -> Unit) {
Expand All @@ -31,6 +39,7 @@ fun PremiumMediaManagementScreen(viewModel: PremiumMediaManagementViewModel, onC
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PremiumMediaManagementScreen(state: PremiumMediaManagementContract.UiState, onClose: () -> Unit) {
val context = LocalContext.current
Scaffold(
topBar = {
PrimalTopAppBar(
Expand All @@ -40,27 +49,58 @@ fun PremiumMediaManagementScreen(state: PremiumMediaManagementContract.UiState,
)
},
) { paddingValues ->
Column(
LazyColumn(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 24.dp)
.padding(paddingValues),
verticalArrangement = Arrangement.spacedBy(24.dp),
.padding(horizontal = 16.dp)
.padding(bottom = 16.dp)
.padding(paddingValues)
.clip(
AppTheme.shapes.large.copy(
topStart = CornerSize(0.dp),
topEnd = CornerSize(0.dp),
),
),
) {
ifNotNull(state.usedStorageInBytes, state.maxStorageInBytes) { used, max ->
UsedStorageBreakdown(
usedStorageInBytes = used.times(600_000),
maxStorageInBytes = max,
imagesInBytes = state.imagesInBytes,
videosInBytes = state.videosInBytes,
otherInBytes = state.otherInBytes,
calculating = state.calculating,
item(key = "storageBreakdown") {
UsedStorageBreakdown(
modifier = Modifier.padding(bottom = 24.dp, top = 24.dp),
usedStorageInBytes = used,
maxStorageInBytes = max,
imagesInBytes = state.imagesInBytes,
videosInBytes = state.videosInBytes,
otherInBytes = state.otherInBytes,
calculating = state.calculating,
)
}
}

item(key = "tableHeader") {
TableHeader(
modifier = Modifier.clip(
AppTheme.shapes.large.copy(
bottomStart = CornerSize(0.dp),
bottomEnd = CornerSize(0.dp),
),
),
)
}
MediaTable(

items(
items = state.mediaItems,
onCopyClick = {},
onDeleteClick = {},
)
key = { it.mediaId },
) { item ->
PrimalDivider()
MediaListItem(
item = item,
onCopyClick = {
val clipboard = context.getSystemService(ClipboardManager::class.java)
val clip = ClipData.newPlainText("", item.mediaUrl)
clipboard.setPrimaryClip(clip)
},
onDeleteClick = {},
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,94 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import java.time.Instant
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.launch
import net.primal.android.networking.sockets.errors.WssException
import net.primal.android.premium.manage.media.PremiumMediaManagementContract.UiState
import net.primal.android.premium.manage.media.model.MediaType
import net.primal.android.premium.manage.media.model.MediaUiItem
import net.primal.android.premium.manage.media.repository.MediaManagementRepository
import net.primal.android.premium.manage.media.ui.MediaType
import net.primal.android.premium.manage.media.ui.MediaUiItem
import net.primal.android.user.accounts.active.ActiveAccountStore
import timber.log.Timber

@HiltViewModel
class PremiumMediaManagementViewModel @Inject constructor(
private val activeAccountStore: ActiveAccountStore,
private val mediaManagementRepository: MediaManagementRepository,
) : ViewModel() {

private val _state = MutableStateFlow(UiState())
val state = _state.asStateFlow()
private fun setState(reducer: UiState.() -> UiState) = _state.getAndUpdate { it.reducer() }

init {
setMaxAndUsedStorage()
fetchMediaBreakdown()
observeActiveAccount()
fetchMediaStats()
fetchMediaUploads()
}

private fun setMaxAndUsedStorage() =
private fun fetchMediaUploads() {
viewModelScope.launch {
val premiumMembership = activeAccountStore.activeUserAccount().premiumMembership
setState {
copy(
usedStorageInBytes = premiumMembership?.usedStorageInBytes,
maxStorageInBytes = premiumMembership?.maxStorageInBytes,
)
try {
val mediaUploads = mediaManagementRepository.fetchMediaUploads(
userId = activeAccountStore.activeUserId(),
) ?: emptyList()

val uploads = mediaUploads.map { mediaInfo ->
val cdnVariant = mediaInfo.cdnResource?.variants?.minByOrNull { it.width }
MediaUiItem(
mediaId = mediaInfo.url,
thumbnailUrl = cdnVariant?.mediaUrl,
mediaUrl = mediaInfo.url,
sizeInBytes = mediaInfo.sizeInBytes,
type = when {
mediaInfo.mimetype?.contains("image") == true -> MediaType.Image
mediaInfo.mimetype?.contains("video") == true -> MediaType.Video
else -> MediaType.Other
},
createdAt = mediaInfo.createdAt?.let(Instant::ofEpochSecond),
)
}

setState { copy(mediaItems = uploads) }
} catch (error: WssException) {
Timber.e(error)
}
}
}

private fun fetchMediaBreakdown() =
private fun fetchMediaStats() {
viewModelScope.launch {
val imgUrl = "https://images.stockcake.com/public/9/e/0/9e0955ab-0177-" +
"48a2-a346-1525da173e28_medium/verdant-grass-field-stockcake.jpg"
delay(1.seconds)
setState {
copy(
calculating = false,
imagesInBytes = 35_791_394_133,
videosInBytes = 17_791_394_133,
otherInBytes = 12_791_394_133,
mediaItems = listOf(
MediaUiItem(
mediaId = "asdf",
thumbnailUrl = imgUrl,
mediaUrl = imgUrl,
sizeInBytes = 1_000_000,
type = MediaType.Image,
date = Instant.now(),
),
MediaUiItem(
mediaId = "asdf1",
thumbnailUrl = imgUrl,
mediaUrl = imgUrl,
sizeInBytes = 1_000_000,
type = MediaType.Video,
date = Instant.now(),
),
MediaUiItem(
mediaId = "asdf2",
thumbnailUrl = imgUrl,
mediaUrl = imgUrl,
sizeInBytes = 2_600_000,
type = MediaType.Image,
date = Instant.now(),
),
MediaUiItem(
mediaId = "asdf3",
thumbnailUrl = imgUrl,
mediaUrl = imgUrl,
sizeInBytes = 3_000_000,
type = MediaType.Video,
date = Instant.now(),
),
MediaUiItem(
mediaId = "asdf4",
thumbnailUrl = imgUrl,
mediaUrl = imgUrl,
sizeInBytes = 1_000_000,
type = MediaType.Image,
date = Instant.now(),
),
),
)
setState { copy(calculating = true) }
try {
val stats = mediaManagementRepository.fetchMediaStats(userId = activeAccountStore.activeUserId())
setState {
copy(
imagesInBytes = stats.imagesInBytes,
videosInBytes = stats.videosInBytes,
otherInBytes = stats.otherFilesInBytes,
)
}
} catch (error: WssException) {
Timber.e(error)
} finally {
setState { copy(calculating = false) }
}
}
}

private fun observeActiveAccount() {
viewModelScope.launch {
activeAccountStore.activeUserAccount.collect {
setState {
copy(
usedStorageInBytes = it.premiumMembership?.usedStorageInBytes,
maxStorageInBytes = it.premiumMembership?.maxStorageInBytes,
)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package net.primal.android.premium.manage.media.api

import net.primal.android.premium.manage.media.api.model.MediaStorageStats
import net.primal.android.premium.manage.media.api.model.MediaUploadsResponse

interface MediaManagementApi {

suspend fun getMediaStats(userId: String): MediaStorageStats

suspend fun getMediaUploads(userId: String): MediaUploadsResponse

suspend fun deleteMedia(userId: String)
}
Loading

0 comments on commit 8329e9e

Please sign in to comment.