ComposeActivity: use nodeinfo data for determining upload limits and markdown support

main
Alibek Omarov 5 years ago
parent b4dbee0acd
commit 919c24571d
  1. 24
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
  2. 88
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
  3. 48
      app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt

@ -321,13 +321,11 @@ class ComposeActivity : BaseActivity(),
} }
} }
private var hasNoAttachmentLimits: Boolean = false
private fun reenableAttachments() { private fun reenableAttachments() {
// in case of we already had disabled attachments // in case of we already had disabled attachments
// but got information about extension later // but got information about extension later
enableButton(composeAddMediaButton, true, true) enableButton(composeAddMediaButton, true, true)
enablePollButton(viewModel.poll == null) enablePollButton(viewModel.poll != null)
hasNoAttachmentLimits = true
} }
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
@ -336,9 +334,12 @@ class ComposeActivity : BaseActivity(),
maximumTootCharacters = instanceData.maxChars maximumTootCharacters = instanceData.maxChars
updateVisibleCharactersLeft() updateVisibleCharactersLeft()
composeScheduleButton.visible(instanceData.supportsScheduled) composeScheduleButton.visible(instanceData.supportsScheduled)
composeMarkdownButton.visible(instanceData.supportsFormatting) }
if(instanceData.hasNoAttachmentLimits) viewModel.instanceMetadata.observe { instanceData ->
composeMarkdownButton.visible(instanceData.supportsMarkdown)
if(instanceData.software.equals("pleroma")) {
reenableAttachments() reenableAttachments()
}
} }
viewModel.emoji.observe { emoji -> setEmojiList(emoji) } viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
@ -366,11 +367,12 @@ class ComposeActivity : BaseActivity(),
updateScheduleButton() updateScheduleButton()
} }
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
val active = (hasNoAttachmentLimits) || (poll == null if(!viewModel.hasNoAttachmentLimits) {
&& media!!.size != 4 val active = (poll == null && media!!.size != 4
&& media.firstOrNull()?.type != QueuedMedia.Type.VIDEO) && media.firstOrNull()?.type != QueuedMedia.Type.VIDEO)
enableButton(composeAddMediaButton, active, active) enableButton(composeAddMediaButton, active, active)
enablePollButton(active && poll == null) enablePollButton(media.isNullOrEmpty())
}
}.subscribe() }.subscribe()
viewModel.uploadError.observe { viewModel.uploadError.observe {
displayTransientError(R.string.error_media_upload_sending) displayTransientError(R.string.error_media_upload_sending)
@ -910,7 +912,7 @@ class ComposeActivity : BaseActivity(),
val intent = Intent(Intent.ACTION_GET_CONTENT) val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE) intent.addCategory(Intent.CATEGORY_OPENABLE)
if(!hasNoAttachmentLimits) { if(!viewModel.hasNoAttachmentLimits) {
val mimeTypes = arrayOf("image/*", "video/*") val mimeTypes = arrayOf("image/*", "video/*")
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
} }

@ -62,18 +62,51 @@ class ComposeViewModel
private var inReplyToId: String? = null private var inReplyToId: String? = null
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData() private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData()
private val nodeinfo: MutableLiveData<NodeInfo?> = MutableLiveData()
public var markdownMode: Boolean = false public var markdownMode: Boolean = false
public var hasNoAttachmentLimits = false
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance -> val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
ComposeInstanceParams( ComposeInstanceParams(
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false, supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
supportsFormatting = instance?.version?.let { VersionUtils(it).isPleroma() } ?: false,
hasNoAttachmentLimits = instance?.version?.let { VersionUtils(it).isPleroma() } ?: false
) )
} }
val instanceMetadata: LiveData<ComposeInstanceMetadata> = nodeinfo.map { nodeinfo ->
val software = nodeinfo?.software?.name ?: "mastodon"
if(software.equals("pleroma")) {
hasNoAttachmentLimits = true
ComposeInstanceMetadata(
software = "pleroma",
supportsMarkdown = nodeinfo?.metadata?.postFormats?.contains("text/markdown") ?: false,
supportsBBcode = nodeinfo?.metadata?.postFormats?.contains("text/bbcode") ?: false,
supportsHTML = nodeinfo?.metadata?.postFormats?.contains("text/html") ?: false,
videoLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_VIDEO_SIZE_LIMIT,
imageLimit = nodeinfo?.metadata?.uploadLimits?.general ?: STATUS_IMAGE_SIZE_LIMIT
)
} else if(software.equals("pixelfed")) {
ComposeInstanceMetadata(
software = "pixelfed",
supportsMarkdown = false,
supportsBBcode = false,
supportsHTML = false,
videoLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize ?: STATUS_VIDEO_SIZE_LIMIT,
imageLimit = nodeinfo?.metadata?.config?.uploader?.maxPhotoSize ?: STATUS_IMAGE_SIZE_LIMIT
)
} else {
ComposeInstanceMetadata(
software = "mastodon",
supportsMarkdown = false,
supportsBBcode = false,
supportsHTML = false,
videoLimit = STATUS_VIDEO_SIZE_LIMIT,
imageLimit = STATUS_IMAGE_SIZE_LIMIT
)
}
}
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData() val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
val markMediaAsSensitive = val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
@ -110,7 +143,7 @@ class ComposeViewModel
db.instanceDao().insertOrReplace(it) db.instanceDao().insertOrReplace(it)
} }
.onErrorResumeNext( .onErrorResumeNext(
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
) )
.subscribe ({ instanceEntity -> .subscribe ({ instanceEntity ->
emoji.postValue(instanceEntity.emojiList) emoji.postValue(instanceEntity.emojiList)
@ -120,16 +153,34 @@ class ComposeViewModel
Log.w(TAG, "error loading instance data", throwable) Log.w(TAG, "error loading instance data", throwable)
}) })
.autoDispose() .autoDispose()
api.getNodeinfoLinks().subscribe({ links ->
if(links.links.size > 0) {
api.getNodeinfo(links.links[0].href).subscribe({ni ->
nodeinfo.postValue(ni)
}, {
err -> Log.d(TAG, "Failed to get nodeinfo", err)
}
)
}
}, {
err -> Log.d(TAG, "Failed to get nodeinfo links", err)
}
)
} }
fun pickMedia(uri: Uri, filename: String?): LiveData<Either<Throwable, QueuedMedia>> { fun pickMedia(uri: Uri, filename: String?): LiveData<Either<Throwable, QueuedMedia>> {
// We are not calling .toLiveData() here because we don't want to stop the process when // We are not calling .toLiveData() here because we don't want to stop the process when
// the Activity goes away temporarily (like on screen rotation). // the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>() val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
mediaUploader.prepareMedia(uri, instanceParams.value!!.hasNoAttachmentLimits) val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
mediaUploader.prepareMedia(uri, videoLimit, imageLimit)
.map { (type, uri, size) -> .map { (type, uri, size) ->
val mediaItems = media.value!! val mediaItems = media.value!!
if (!instanceParams.value!!.hasNoAttachmentLimits if (!hasNoAttachmentLimits
&& type == QueuedMedia.Type.VIDEO && type == QueuedMedia.Type.VIDEO
&& mediaItems.isNotEmpty() && mediaItems.isNotEmpty()
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) { && mediaItems[0].type == QueuedMedia.Type.IMAGE) {
@ -149,10 +200,13 @@ class ComposeViewModel
private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String): QueuedMedia { private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String): QueuedMedia {
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize, filename, val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize, filename,
instanceParams.value!!.hasNoAttachmentLimits) hasNoAttachmentLimits)
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
media.value = media.value!! + mediaItem media.value = media.value!! + mediaItem
mediaToDisposable[mediaItem.localId] = mediaUploader mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem) .uploadMedia(mediaItem, videoLimit, imageLimit )
.subscribe ({ event -> .subscribe ({ event ->
val item = media.value?.find { it.localId == mediaItem.localId } val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe ?: return@subscribe
@ -180,7 +234,7 @@ class ComposeViewModel
private fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) { private fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) {
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown", val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown",
instanceParams.value!!.hasNoAttachmentLimits, -1, id, description) hasNoAttachmentLimits, -1, id, description)
media.value = media.value!! + mediaItem media.value = media.value!! + mediaItem
} }
@ -453,12 +507,22 @@ fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = defau
const val DEFAULT_CHARACTER_LIMIT = 500 const val DEFAULT_CHARACTER_LIMIT = 500
private const val DEFAULT_MAX_OPTION_COUNT = 4 private const val DEFAULT_MAX_OPTION_COUNT = 4
private const val DEFAULT_MAX_OPTION_LENGTH = 25 private const val DEFAULT_MAX_OPTION_LENGTH = 25
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
data class ComposeInstanceParams( data class ComposeInstanceParams(
val maxChars: Int, val maxChars: Int,
val pollMaxOptions: Int, val pollMaxOptions: Int,
val pollMaxLength: Int, val pollMaxLength: Int,
val supportsScheduled: Boolean, val supportsScheduled: Boolean
val supportsFormatting: Boolean, )
val hasNoAttachmentLimits: Boolean
data class ComposeInstanceMetadata(
val software: String,
val supportsMarkdown: Boolean,
val supportsBBcode: Boolean,
val supportsHTML: Boolean,
val videoLimit: Int,
val imageLimit: Int
) )

@ -59,8 +59,8 @@ fun createNewImageFile(context: Context): File {
data class PreparedMedia(val type: Int, val uri: Uri, val size: Long) data class PreparedMedia(val type: Int, val uri: Uri, val size: Long)
interface MediaUploader { interface MediaUploader {
fun prepareMedia(inUri: Uri, hasNoLimits: Boolean): Single<PreparedMedia> fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int): Single<PreparedMedia>
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> fun uploadMedia(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable<UploadEvent>
} }
class VideoSizeException : Exception() class VideoSizeException : Exception()
@ -71,19 +71,19 @@ class MediaUploaderImpl(
private val context: Context, private val context: Context,
private val mastodonApi: MastodonApi private val mastodonApi: MastodonApi
) : MediaUploader { ) : MediaUploader {
override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> { override fun uploadMedia(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable<UploadEvent> {
return Observable return Observable
.fromCallable { .fromCallable {
if (shouldResizeMedia(media)) { if (shouldResizeMedia(media, imageLimit)) {
downsize(media) downsize(media, imageLimit)
} }
media media
} }
.switchMap { upload(it) } .switchMap { upload(it, videoLimit, imageLimit) }
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
} }
override fun prepareMedia(inUri: Uri, hasNoLimits: Boolean): Single<PreparedMedia> { override fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int): Single<PreparedMedia> {
return Single.fromCallable { return Single.fromCallable {
var mediaSize = getMediaSize(contentResolver, inUri) var mediaSize = getMediaSize(contentResolver, inUri)
var uri = inUri var uri = inUri
@ -120,7 +120,7 @@ class MediaUploaderImpl(
val topLevelType = mimeType.substring(0, mimeType.indexOf('/')) val topLevelType = mimeType.substring(0, mimeType.indexOf('/'))
when (topLevelType) { when (topLevelType) {
"video" -> { "video" -> {
if (!hasNoLimits && mediaSize > STATUS_VIDEO_SIZE_LIMIT) { if (mediaSize > videoLimit) {
throw VideoSizeException() throw VideoSizeException()
} }
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
@ -141,7 +141,7 @@ class MediaUploaderImpl(
private val contentResolver = context.contentResolver private val contentResolver = context.contentResolver
private fun upload(media: QueuedMedia): Observable<UploadEvent> { private fun upload(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable<UploadEvent> {
return Observable.create { emitter -> return Observable.create { emitter ->
var mimeType = contentResolver.getType(media.uri) var mimeType = contentResolver.getType(media.uri)
val map = MimeTypeMap.getSingleton() val map = MimeTypeMap.getSingleton()
@ -156,7 +156,6 @@ class MediaUploaderImpl(
if (mimeType == null) mimeType = "multipart/form-data" if (mimeType == null) mimeType = "multipart/form-data"
var lastProgress = -1 var lastProgress = -1
val fileBody = ProgressRequestBody(stream, media.mediaSize, val fileBody = ProgressRequestBody(stream, media.mediaSize,
mimeType.toMediaTypeOrNull()) { percentage -> mimeType.toMediaTypeOrNull()) { percentage ->
@ -181,24 +180,33 @@ class MediaUploaderImpl(
} }
} }
private fun downsize(media: QueuedMedia): QueuedMedia { private fun downsize(media: QueuedMedia, imageLimit: Int): QueuedMedia {
val file = createNewImageFile(context) val file = createNewImageFile(context)
DownsizeImageTask.resize(arrayOf(media.uri), DownsizeImageTask.resize(arrayOf(media.uri), imageLimit, context.contentResolver, file)
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file)
return media.copy(uri = file.toUri(), mediaSize = file.length()) return media.copy(uri = file.toUri(), mediaSize = file.length())
} }
private fun shouldResizeMedia(media: QueuedMedia): Boolean { private fun shouldResizeMedia(media: QueuedMedia, imageLimit: Int): Boolean {
return !media.noChanges && media.type == QueuedMedia.Type.IMAGE // resize only images
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT if(media.type == QueuedMedia.Type.IMAGE) {
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) // resize when exceed image limit
if(media.mediaSize < imageLimit)
return true
// don't resize when instance permits any image resolution(Pleroma)
if(media.noChanges)
return false
// resize when exceed pixel limit
if(getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
return true
}
return false
} }
private companion object { private companion object {
private const val TAG = "MediaUploaderImpl" private const val TAG = "MediaUploaderImpl"
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
} }
} }

Loading…
Cancel
Save