diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index c4ed3707..510848b7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -321,13 +321,11 @@ class ComposeActivity : BaseActivity(), } } - private var hasNoAttachmentLimits: Boolean = false private fun reenableAttachments() { // in case of we already had disabled attachments // but got information about extension later enableButton(composeAddMediaButton, true, true) - enablePollButton(viewModel.poll == null) - hasNoAttachmentLimits = true + enablePollButton(viewModel.poll != null) } private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { @@ -336,9 +334,12 @@ class ComposeActivity : BaseActivity(), maximumTootCharacters = instanceData.maxChars updateVisibleCharactersLeft() 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() + } } viewModel.emoji.observe { emoji -> setEmojiList(emoji) } combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> @@ -366,11 +367,12 @@ class ComposeActivity : BaseActivity(), updateScheduleButton() } combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> - val active = (hasNoAttachmentLimits) || (poll == null - && media!!.size != 4 - && media.firstOrNull()?.type != QueuedMedia.Type.VIDEO) - enableButton(composeAddMediaButton, active, active) - enablePollButton(active && poll == null) + if(!viewModel.hasNoAttachmentLimits) { + val active = (poll == null && media!!.size != 4 + && media.firstOrNull()?.type != QueuedMedia.Type.VIDEO) + enableButton(composeAddMediaButton, active, active) + enablePollButton(media.isNullOrEmpty()) + } }.subscribe() viewModel.uploadError.observe { displayTransientError(R.string.error_media_upload_sending) @@ -910,7 +912,7 @@ class ComposeActivity : BaseActivity(), val intent = Intent(Intent.ACTION_GET_CONTENT) intent.addCategory(Intent.CATEGORY_OPENABLE) - if(!hasNoAttachmentLimits) { + if(!viewModel.hasNoAttachmentLimits) { val mimeTypes = arrayOf("image/*", "video/*") intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 5231ee1d..5acca508 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -62,18 +62,51 @@ class ComposeViewModel private var inReplyToId: String? = null private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private val instance: MutableLiveData = MutableLiveData() + private val nodeinfo: MutableLiveData = MutableLiveData() public var markdownMode: Boolean = false + public var hasNoAttachmentLimits = false val instanceParams: LiveData = instance.map { instance -> ComposeInstanceParams( maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, - supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false, - supportsFormatting = instance?.version?.let { VersionUtils(it).isPleroma() } ?: false, - hasNoAttachmentLimits = instance?.version?.let { VersionUtils(it).isPleroma() } ?: false + supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false ) } + val instanceMetadata: LiveData = 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?> = MutableLiveData() val markMediaAsSensitive = mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) @@ -110,7 +143,7 @@ class ComposeViewModel db.instanceDao().insertOrReplace(it) } .onErrorResumeNext( - db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) + db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) ) .subscribe ({ instanceEntity -> emoji.postValue(instanceEntity.emojiList) @@ -120,16 +153,34 @@ class ComposeViewModel Log.w(TAG, "error loading instance data", throwable) }) .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> { // 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). val liveData = MutableLiveData>() - 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) -> val mediaItems = media.value!! - if (!instanceParams.value!!.hasNoAttachmentLimits + if (!hasNoAttachmentLimits && type == QueuedMedia.Type.VIDEO && mediaItems.isNotEmpty() && mediaItems[0].type == QueuedMedia.Type.IMAGE) { @@ -149,10 +200,13 @@ class ComposeViewModel private fun addMediaToQueue(type: Int, uri: Uri, mediaSize: Long, filename: String): QueuedMedia { 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 mediaToDisposable[mediaItem.localId] = mediaUploader - .uploadMedia(mediaItem) + .uploadMedia(mediaItem, videoLimit, imageLimit ) .subscribe ({ event -> val item = media.value?.find { it.localId == mediaItem.localId } ?: return@subscribe @@ -180,7 +234,7 @@ class ComposeViewModel private fun addUploadedMedia(id: String, type: Int, uri: Uri, description: String?) { val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, "unknown", - instanceParams.value!!.hasNoAttachmentLimits, -1, id, description) + hasNoAttachmentLimits, -1, id, description) media.value = media.value!! + mediaItem } @@ -453,12 +507,22 @@ fun mutableLiveData(default: T) = MutableLiveData().apply { value = defau const val DEFAULT_CHARACTER_LIMIT = 500 private const val DEFAULT_MAX_OPTION_COUNT = 4 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( val maxChars: Int, val pollMaxOptions: Int, val pollMaxLength: Int, - val supportsScheduled: Boolean, - val supportsFormatting: Boolean, - val hasNoAttachmentLimits: Boolean + val supportsScheduled: Boolean +) + +data class ComposeInstanceMetadata( + val software: String, + val supportsMarkdown: Boolean, + val supportsBBcode: Boolean, + val supportsHTML: Boolean, + val videoLimit: Int, + val imageLimit: Int ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 10eb30b5..dd96d380 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -59,8 +59,8 @@ fun createNewImageFile(context: Context): File { data class PreparedMedia(val type: Int, val uri: Uri, val size: Long) interface MediaUploader { - fun prepareMedia(inUri: Uri, hasNoLimits: Boolean): Single - fun uploadMedia(media: QueuedMedia): Observable + fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int): Single + fun uploadMedia(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable } class VideoSizeException : Exception() @@ -71,19 +71,19 @@ class MediaUploaderImpl( private val context: Context, private val mastodonApi: MastodonApi ) : MediaUploader { - override fun uploadMedia(media: QueuedMedia): Observable { + override fun uploadMedia(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable { return Observable .fromCallable { - if (shouldResizeMedia(media)) { - downsize(media) + if (shouldResizeMedia(media, imageLimit)) { + downsize(media, imageLimit) } media } - .switchMap { upload(it) } + .switchMap { upload(it, videoLimit, imageLimit) } .subscribeOn(Schedulers.io()) } - override fun prepareMedia(inUri: Uri, hasNoLimits: Boolean): Single { + override fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int): Single { return Single.fromCallable { var mediaSize = getMediaSize(contentResolver, inUri) var uri = inUri @@ -120,7 +120,7 @@ class MediaUploaderImpl( val topLevelType = mimeType.substring(0, mimeType.indexOf('/')) when (topLevelType) { "video" -> { - if (!hasNoLimits && mediaSize > STATUS_VIDEO_SIZE_LIMIT) { + if (mediaSize > videoLimit) { throw VideoSizeException() } PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) @@ -141,7 +141,7 @@ class MediaUploaderImpl( private val contentResolver = context.contentResolver - private fun upload(media: QueuedMedia): Observable { + private fun upload(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable { return Observable.create { emitter -> var mimeType = contentResolver.getType(media.uri) val map = MimeTypeMap.getSingleton() @@ -156,7 +156,6 @@ class MediaUploaderImpl( if (mimeType == null) mimeType = "multipart/form-data" - var lastProgress = -1 val fileBody = ProgressRequestBody(stream, media.mediaSize, 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) - DownsizeImageTask.resize(arrayOf(media.uri), - STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file) + DownsizeImageTask.resize(arrayOf(media.uri), imageLimit, context.contentResolver, file) return media.copy(uri = file.toUri(), mediaSize = file.length()) } - private fun shouldResizeMedia(media: QueuedMedia): Boolean { - return !media.noChanges && media.type == QueuedMedia.Type.IMAGE - && (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT - || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) + private fun shouldResizeMedia(media: QueuedMedia, imageLimit: Int): Boolean { + // resize only images + if(media.type == QueuedMedia.Type.IMAGE) { + // 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 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 - } }