2019-12-19 19:09:40 +01:00
|
|
|
/* Copyright 2019 Tusky Contributors
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
|
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
|
|
* License, or (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
|
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
|
|
* Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.components.compose
|
|
|
|
|
|
|
|
import android.Manifest
|
2022-03-09 20:50:23 +01:00
|
|
|
import android.app.NotificationManager
|
2019-12-19 19:09:40 +01:00
|
|
|
import android.app.ProgressDialog
|
2022-03-09 20:50:23 +01:00
|
|
|
import android.content.ClipData
|
2019-12-19 19:09:40 +01:00
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
|
|
|
import android.content.SharedPreferences
|
|
|
|
import android.content.pm.PackageManager
|
2022-05-22 21:01:14 +02:00
|
|
|
import android.graphics.Bitmap
|
2019-12-19 19:09:40 +01:00
|
|
|
import android.graphics.PorterDuff
|
|
|
|
import android.graphics.PorterDuffColorFilter
|
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.Parcelable
|
2021-01-31 20:27:02 +01:00
|
|
|
import android.util.Log
|
2019-12-19 19:09:40 +01:00
|
|
|
import android.view.KeyEvent
|
|
|
|
import android.view.MenuItem
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
2022-08-31 18:53:57 +02:00
|
|
|
import android.widget.AdapterView
|
2021-06-28 21:13:24 +02:00
|
|
|
import android.widget.ImageButton
|
|
|
|
import android.widget.LinearLayout
|
|
|
|
import android.widget.PopupMenu
|
|
|
|
import android.widget.Toast
|
2021-05-22 17:50:08 +02:00
|
|
|
import androidx.activity.result.contract.ActivityResultContracts
|
2020-02-25 19:49:41 +01:00
|
|
|
import androidx.activity.viewModels
|
2019-12-19 19:09:40 +01:00
|
|
|
import androidx.annotation.ColorInt
|
|
|
|
import androidx.annotation.StringRes
|
|
|
|
import androidx.annotation.VisibleForTesting
|
|
|
|
import androidx.appcompat.app.AlertDialog
|
|
|
|
import androidx.core.app.ActivityCompat
|
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import androidx.core.content.FileProvider
|
2022-03-09 20:50:23 +01:00
|
|
|
import androidx.core.view.ContentInfoCompat
|
|
|
|
import androidx.core.view.OnReceiveContentListener
|
2019-12-19 19:09:40 +01:00
|
|
|
import androidx.core.view.isGone
|
|
|
|
import androidx.core.view.isVisible
|
2022-04-21 18:46:21 +02:00
|
|
|
import androidx.lifecycle.lifecycleScope
|
2020-01-13 15:21:40 +01:00
|
|
|
import androidx.preference.PreferenceManager
|
2019-12-19 19:09:40 +01:00
|
|
|
import androidx.recyclerview.widget.LinearLayoutManager
|
|
|
|
import androidx.transition.TransitionManager
|
2022-05-22 21:01:14 +02:00
|
|
|
import com.canhub.cropper.CropImage
|
|
|
|
import com.canhub.cropper.CropImageContract
|
|
|
|
import com.canhub.cropper.options
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
|
|
|
import com.google.android.material.snackbar.Snackbar
|
|
|
|
import com.keylesspalace.tusky.BaseActivity
|
|
|
|
import com.keylesspalace.tusky.BuildConfig
|
|
|
|
import com.keylesspalace.tusky.R
|
|
|
|
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
2022-08-31 18:53:57 +02:00
|
|
|
import com.keylesspalace.tusky.adapter.LocaleAdapter
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
|
|
|
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
|
2020-01-13 15:21:40 +01:00
|
|
|
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
2021-02-23 20:29:02 +01:00
|
|
|
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
2022-04-21 18:46:21 +02:00
|
|
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
2021-03-07 19:05:51 +01:00
|
|
|
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.db.AccountEntity
|
2021-01-21 18:57:09 +01:00
|
|
|
import com.keylesspalace.tusky.db.DraftAttachment
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.keylesspalace.tusky.di.Injectable
|
|
|
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
|
|
|
import com.keylesspalace.tusky.entity.Attachment
|
|
|
|
import com.keylesspalace.tusky.entity.Emoji
|
|
|
|
import com.keylesspalace.tusky.entity.NewPoll
|
|
|
|
import com.keylesspalace.tusky.entity.Status
|
2021-02-06 08:14:51 +01:00
|
|
|
import com.keylesspalace.tusky.settings.PrefKeys
|
2021-06-28 21:13:24 +02:00
|
|
|
import com.keylesspalace.tusky.util.PickMediaFiles
|
|
|
|
import com.keylesspalace.tusky.util.ThemeUtils
|
|
|
|
import com.keylesspalace.tusky.util.afterTextChanged
|
2022-05-22 21:01:14 +02:00
|
|
|
import com.keylesspalace.tusky.util.getMediaSize
|
2021-06-28 21:13:24 +02:00
|
|
|
import com.keylesspalace.tusky.util.hide
|
|
|
|
import com.keylesspalace.tusky.util.highlightSpans
|
|
|
|
import com.keylesspalace.tusky.util.loadAvatar
|
|
|
|
import com.keylesspalace.tusky.util.onTextChanged
|
|
|
|
import com.keylesspalace.tusky.util.show
|
|
|
|
import com.keylesspalace.tusky.util.viewBinding
|
|
|
|
import com.keylesspalace.tusky.util.visible
|
2019-12-19 19:09:40 +01:00
|
|
|
import com.mikepenz.iconics.IconicsDrawable
|
2020-04-15 18:57:53 +02:00
|
|
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
|
|
|
import com.mikepenz.iconics.utils.colorInt
|
|
|
|
import com.mikepenz.iconics.utils.sizeDp
|
2022-07-26 20:24:50 +02:00
|
|
|
import kotlinx.coroutines.flow.collect
|
|
|
|
import kotlinx.coroutines.flow.combine
|
|
|
|
import kotlinx.coroutines.flow.first
|
2022-04-21 18:46:21 +02:00
|
|
|
import kotlinx.coroutines.launch
|
2021-03-21 12:42:28 +01:00
|
|
|
import kotlinx.parcelize.Parcelize
|
2019-12-19 19:09:40 +01:00
|
|
|
import java.io.File
|
|
|
|
import java.io.IOException
|
2022-08-05 18:55:13 +02:00
|
|
|
import java.text.DecimalFormat
|
2021-03-21 12:42:28 +01:00
|
|
|
import java.util.Locale
|
2019-12-19 19:09:40 +01:00
|
|
|
import javax.inject.Inject
|
|
|
|
import kotlin.math.max
|
|
|
|
import kotlin.math.min
|
|
|
|
|
2021-06-28 21:13:24 +02:00
|
|
|
class ComposeActivity :
|
|
|
|
BaseActivity(),
|
2021-05-22 17:50:08 +02:00
|
|
|
ComposeOptionsListener,
|
|
|
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
|
|
|
OnEmojiSelectedListener,
|
|
|
|
Injectable,
|
2022-03-09 20:50:23 +01:00
|
|
|
OnReceiveContentListener,
|
2021-05-22 17:50:08 +02:00
|
|
|
ComposeScheduleView.OnTimeSetListener {
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
@Inject
|
|
|
|
lateinit var viewModelFactory: ViewModelFactory
|
|
|
|
|
|
|
|
private lateinit var composeOptionsBehavior: BottomSheetBehavior<*>
|
|
|
|
private lateinit var addMediaBehavior: BottomSheetBehavior<*>
|
|
|
|
private lateinit var emojiBehavior: BottomSheetBehavior<*>
|
|
|
|
private lateinit var scheduleBehavior: BottomSheetBehavior<*>
|
|
|
|
|
|
|
|
// this only exists when a status is trying to be sent, but uploads are still occurring
|
|
|
|
private var finishingUploadDialog: ProgressDialog? = null
|
|
|
|
private var photoUploadUri: Uri? = null
|
2021-01-21 18:57:09 +01:00
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
@VisibleForTesting
|
2022-04-21 18:46:21 +02:00
|
|
|
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
|
|
|
var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2020-02-25 19:49:41 +01:00
|
|
|
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
private val binding by viewBinding(ActivityComposeBinding::inflate)
|
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS
|
2020-01-30 21:17:37 +01:00
|
|
|
|
2021-05-22 17:50:08 +02:00
|
|
|
private val takePicture = registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
|
|
|
if (success) {
|
|
|
|
pickMedia(photoUploadUri!!)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris ->
|
2022-07-26 20:24:50 +02:00
|
|
|
if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) {
|
2021-05-22 17:50:08 +02:00
|
|
|
Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
|
|
|
} else {
|
|
|
|
uris.forEach { uri ->
|
|
|
|
pickMedia(uri)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-22 21:01:14 +02:00
|
|
|
// Contract kicked off by editImageInQueue; expects viewModel.cropImageItemOld set
|
|
|
|
private val cropImage = registerForActivityResult(CropImageContract()) { result ->
|
|
|
|
val uriNew = result.uriContent
|
|
|
|
if (result.isSuccessful && uriNew != null) {
|
|
|
|
viewModel.cropImageItemOld?.let { itemOld ->
|
2022-06-30 20:51:05 +02:00
|
|
|
val size = getMediaSize(contentResolver, uriNew)
|
2022-05-22 21:01:14 +02:00
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.addMediaToQueue(
|
|
|
|
itemOld.type,
|
|
|
|
uriNew,
|
|
|
|
size,
|
|
|
|
itemOld.description,
|
|
|
|
itemOld
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (result == CropImage.CancelledResult) {
|
|
|
|
Log.w("ComposeActivity", "Edit image cancelled by user")
|
|
|
|
} else {
|
|
|
|
Log.w("ComposeActivity", "Edit image failed: " + result.error)
|
2022-06-21 22:14:11 +02:00
|
|
|
displayTransientError(R.string.error_image_edit_failed)
|
2022-05-22 21:01:14 +02:00
|
|
|
}
|
|
|
|
viewModel.cropImageItemOld = null
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
2021-03-07 19:05:51 +01:00
|
|
|
|
2022-03-09 20:50:23 +01:00
|
|
|
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)
|
|
|
|
if (notificationId != -1) {
|
|
|
|
// ComposeActivity was opened from a notification, delete the notification
|
|
|
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
|
|
|
notificationManager.cancel(notificationId)
|
|
|
|
}
|
|
|
|
|
|
|
|
val accountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1)
|
|
|
|
if (accountId != -1L) {
|
|
|
|
accountManager.setActiveAccount(accountId)
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
|
|
|
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
|
|
|
if (theme == "black") {
|
|
|
|
setTheme(R.style.TuskyDialogActivityBlackTheme)
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
setContentView(binding.root)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
setupActionBar()
|
|
|
|
// do not do anything when not logged in, activity will be finished in super.onCreate() anyway
|
|
|
|
val activeAccount = accountManager.activeAccount ?: return
|
|
|
|
|
|
|
|
setupAvatar(preferences, activeAccount)
|
|
|
|
val mediaAdapter = MediaPreviewAdapter(
|
2021-05-22 17:50:08 +02:00
|
|
|
this,
|
|
|
|
onAddCaption = { item ->
|
|
|
|
makeCaptionDialog(item.description, item.uri) { newDescription ->
|
|
|
|
viewModel.updateDescription(item.localId, newDescription)
|
|
|
|
}
|
|
|
|
},
|
2022-05-22 21:01:14 +02:00
|
|
|
onEditImage = this::editImageInQueue,
|
2021-05-22 17:50:08 +02:00
|
|
|
onRemove = this::removeMediaFromQueue
|
2019-12-19 19:09:40 +01:00
|
|
|
)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeMediaPreviewBar.layoutManager =
|
2021-05-22 17:50:08 +02:00
|
|
|
LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeMediaPreviewBar.adapter = mediaAdapter
|
|
|
|
binding.composeMediaPreviewBar.itemAnimator = null
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
setupButtons()
|
2022-07-26 20:24:50 +02:00
|
|
|
subscribeToUpdates(mediaAdapter)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2020-02-24 22:03:00 +01:00
|
|
|
photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY)
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
/* If the composer is started up as a reply to another post, override the "starting" state
|
|
|
|
* based on what the intent from the reply request passes. */
|
2021-01-21 18:57:09 +01:00
|
|
|
|
|
|
|
val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
|
|
|
|
|
|
|
viewModel.setup(composeOptions)
|
|
|
|
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
2022-03-20 20:21:42 +01:00
|
|
|
val statusContent = composeOptions?.content
|
|
|
|
if (!statusContent.isNullOrEmpty()) {
|
|
|
|
binding.composeEditField.setText(statusContent)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-08-31 18:53:57 +02:00
|
|
|
setupLanguageSpinner(getInitialLanguage(composeOptions?.language))
|
2021-02-06 08:14:51 +01:00
|
|
|
setupComposeField(preferences, viewModel.startingText)
|
2019-12-19 19:09:40 +01:00
|
|
|
setupContentWarningField(composeOptions?.contentWarning)
|
|
|
|
setupPollView()
|
|
|
|
applyShareIntent(intent, savedInstanceState)
|
2022-08-03 17:23:54 +02:00
|
|
|
|
|
|
|
binding.composeEditField.post {
|
|
|
|
binding.composeEditField.requestFocus()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
|
|
|
|
if (savedInstanceState == null) {
|
2019-12-19 19:09:40 +01:00
|
|
|
/* Get incoming images being sent through a share action from another app. Only do this
|
|
|
|
* when savedInstanceState is null, otherwise both the images from the intent and the
|
|
|
|
* instance state will be re-queued. */
|
2021-01-21 18:57:09 +01:00
|
|
|
intent.type?.also { type ->
|
2020-01-16 19:05:52 +01:00
|
|
|
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
|
2021-01-21 18:57:09 +01:00
|
|
|
when (intent.action) {
|
|
|
|
Intent.ACTION_SEND -> {
|
|
|
|
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { uri ->
|
|
|
|
pickMedia(uri)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2021-01-21 18:57:09 +01:00
|
|
|
}
|
|
|
|
Intent.ACTION_SEND_MULTIPLE -> {
|
|
|
|
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.forEach { uri ->
|
|
|
|
pickMedia(uri)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-03-28 18:39:05 +02:00
|
|
|
}
|
2020-06-21 18:25:17 +02:00
|
|
|
|
2022-03-28 18:39:05 +02:00
|
|
|
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
|
|
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
|
|
|
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
|
|
|
subject + '\n' + text
|
|
|
|
} else {
|
|
|
|
text
|
|
|
|
}
|
2020-02-26 20:41:02 +01:00
|
|
|
|
2022-03-28 18:39:05 +02:00
|
|
|
if (shareBody.isNotBlank()) {
|
|
|
|
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
|
|
|
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
|
|
|
val left = min(start, end)
|
|
|
|
val right = max(start, end)
|
|
|
|
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
|
|
|
// move edittext cursor to first when shareBody parsed
|
|
|
|
binding.composeEditField.text.insert(0, "\n")
|
|
|
|
binding.composeEditField.setSelection(0)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) {
|
2019-12-19 19:09:40 +01:00
|
|
|
if (replyingStatusAuthor != null) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeReplyView.show()
|
|
|
|
binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
2020-04-15 18:57:53 +02:00
|
|
|
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
ThemeUtils.setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeReplyView.setOnClickListener {
|
|
|
|
TransitionManager.beginDelayedTransition(binding.composeReplyContentView.parent as ViewGroup)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
if (binding.composeReplyContentView.isVisible) {
|
|
|
|
binding.composeReplyContentView.hide()
|
|
|
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeReplyContentView.show()
|
2020-04-15 18:57:53 +02:00
|
|
|
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
ThemeUtils.setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
replyingStatusContent?.let { binding.composeReplyContentView.text = it }
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupContentWarningField(startingContentWarning: String?) {
|
|
|
|
if (startingContentWarning != null) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeContentWarningField.setText(startingContentWarning)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() }
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2021-02-06 08:14:51 +01:00
|
|
|
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
2022-03-09 20:50:23 +01:00
|
|
|
binding.composeEditField.setOnReceiveContentListener(this)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setAdapter(
|
2021-05-22 17:50:08 +02:00
|
|
|
ComposeAutoCompleteAdapter(
|
|
|
|
this,
|
|
|
|
preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
2022-05-17 19:55:37 +02:00
|
|
|
preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false),
|
|
|
|
preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true)
|
2021-05-22 17:50:08 +02:00
|
|
|
)
|
2021-02-06 08:14:51 +01:00
|
|
|
)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setTokenizer(ComposeTokenizer())
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setText(startingText)
|
|
|
|
binding.composeEditField.setSelection(binding.composeEditField.length())
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
val mentionColour = binding.composeEditField.linkTextColors.defaultColor
|
|
|
|
highlightSpans(binding.composeEditField.text, mentionColour)
|
|
|
|
binding.composeEditField.afterTextChanged { editable ->
|
2019-12-19 19:09:40 +01:00
|
|
|
highlightSpans(editable, mentionColour)
|
|
|
|
updateVisibleCharactersLeft()
|
|
|
|
}
|
|
|
|
|
|
|
|
// work around Android platform bug -> https://issuetracker.google.com/issues/67102093
|
2021-06-28 21:13:24 +02:00
|
|
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O ||
|
|
|
|
Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1
|
|
|
|
) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
|
2022-07-26 20:24:50 +02:00
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.instanceInfo.collect { instanceData ->
|
2019-12-19 19:09:40 +01:00
|
|
|
maximumTootCharacters = instanceData.maxChars
|
2022-03-01 19:43:36 +01:00
|
|
|
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
2022-07-26 20:24:50 +02:00
|
|
|
maxUploadMediaNumber = instanceData.maxMediaAttachments
|
2019-12-19 19:09:40 +01:00
|
|
|
updateVisibleCharactersLeft()
|
|
|
|
}
|
2022-07-26 20:24:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.emoji.collect(::setEmojiList)
|
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive ->
|
2019-12-19 19:09:40 +01:00
|
|
|
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
|
|
|
showContentWarning(showContentWarning)
|
2022-07-26 20:24:50 +02:00
|
|
|
}.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.statusVisibility.collect(::setStatusVisibility)
|
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.media.collect { media ->
|
|
|
|
mediaAdapter.submitList(media)
|
|
|
|
|
|
|
|
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
|
|
|
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-07-26 20:24:50 +02:00
|
|
|
}
|
2022-05-03 19:12:35 +02:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.poll.collect { poll ->
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.pollPreview.visible(poll != null)
|
|
|
|
poll?.let(binding.pollPreview::setPoll)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-07-26 20:24:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.scheduledAt.collect { scheduledAt ->
|
2020-11-18 21:12:27 +01:00
|
|
|
if (scheduledAt == null) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeScheduleView.resetSchedule()
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeScheduleView.setDateTime(scheduledAt)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
updateScheduleButton()
|
|
|
|
}
|
2022-07-26 20:24:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.media.combine(viewModel.poll) { media, poll ->
|
2021-06-28 21:13:24 +02:00
|
|
|
val active = poll == null &&
|
2022-07-26 20:24:50 +02:00
|
|
|
media.size < maxUploadMediaNumber &&
|
2021-06-28 21:13:24 +02:00
|
|
|
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
2021-03-07 19:05:51 +01:00
|
|
|
enableButton(binding.composeAddMediaButton, active, active)
|
2022-07-26 20:24:50 +02:00
|
|
|
enablePollButton(media.isEmpty())
|
|
|
|
}.collect()
|
|
|
|
}
|
|
|
|
|
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.uploadError.collect { throwable ->
|
2022-06-30 20:51:05 +02:00
|
|
|
if (throwable is UploadServerError) {
|
|
|
|
displayTransientError(throwable.errorMessage)
|
|
|
|
} else {
|
|
|
|
displayTransientError(R.string.error_media_upload_sending)
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-07-26 20:24:50 +02:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupButtons() {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeOptionsBottomSheet.listener = this
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet)
|
|
|
|
addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet)
|
|
|
|
scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView)
|
|
|
|
emojiBehavior = BottomSheetBehavior.from(binding.emojiView)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
enableButton(binding.composeEmojiButton, clickable = false, colorActive = false)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
// Setup the interface buttons.
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeTootButton.setOnClickListener { onSendClicked() }
|
|
|
|
binding.composeAddMediaButton.setOnClickListener { openPickDialog() }
|
|
|
|
binding.composeToggleVisibilityButton.setOnClickListener { showComposeOptions() }
|
|
|
|
binding.composeContentWarningButton.setOnClickListener { onContentWarningChanged() }
|
|
|
|
binding.composeEmojiButton.setOnClickListener { showEmojis() }
|
|
|
|
binding.composeHideMediaButton.setOnClickListener { toggleHideMedia() }
|
|
|
|
binding.composeScheduleButton.setOnClickListener { onScheduleClick() }
|
|
|
|
binding.composeScheduleView.setResetOnClickListener { resetSchedule() }
|
|
|
|
binding.composeScheduleView.setListener(this)
|
|
|
|
binding.atButton.setOnClickListener { atButtonClicked() }
|
|
|
|
binding.hashButton.setOnClickListener { hashButtonClicked() }
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
|
|
|
|
2020-04-15 18:57:53 +02:00
|
|
|
val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 }
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2020-04-15 18:57:53 +02:00
|
|
|
val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 }
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2020-04-15 18:57:53 +02:00
|
|
|
val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { colorInt = textColor; sizeDp = 18 }
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(pollIcon, null, null, null)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
|
|
|
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
|
|
|
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-08-31 18:53:57 +02:00
|
|
|
private fun setupLanguageSpinner(initialLanguage: String?) {
|
|
|
|
val locales = Locale.getAvailableLocales()
|
|
|
|
.filter { it.country.isNullOrEmpty() && it.script.isNullOrEmpty() && it.variant.isNullOrEmpty() } // Only "base" languages, "en" but not "en_DK"
|
|
|
|
var currentLocaleIndex = locales.indexOfFirst { it.language == initialLanguage }
|
|
|
|
if (currentLocaleIndex < 0) {
|
|
|
|
Log.e(TAG, "Error looking up language tag '$initialLanguage', falling back to english")
|
|
|
|
currentLocaleIndex = locales.indexOfFirst { it.language == "en" }
|
|
|
|
}
|
|
|
|
|
|
|
|
val context = this
|
|
|
|
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
|
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
|
|
|
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).language
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onNothingSelected(parent: AdapterView<*>) {
|
|
|
|
parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() })
|
|
|
|
}
|
|
|
|
}
|
|
|
|
binding.composePostLanguageButton.apply {
|
|
|
|
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales)
|
|
|
|
setSelection(currentLocaleIndex)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun getInitialLanguage(language: String? = null): String {
|
|
|
|
return if (language.isNullOrEmpty()) {
|
|
|
|
// Setting the application ui preference sets the default locale
|
|
|
|
Locale.getDefault().language
|
|
|
|
} else {
|
|
|
|
language
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
private fun setupActionBar() {
|
2021-03-07 19:05:51 +01:00
|
|
|
setSupportActionBar(binding.toolbar)
|
2019-12-19 19:09:40 +01:00
|
|
|
supportActionBar?.run {
|
|
|
|
title = null
|
|
|
|
setDisplayHomeAsUpEnabled(true)
|
|
|
|
setDisplayShowHomeEnabled(true)
|
2020-01-30 21:37:28 +01:00
|
|
|
setHomeAsUpIndicator(R.drawable.ic_close_24dp)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupAvatar(preferences: SharedPreferences, activeAccount: AccountEntity) {
|
|
|
|
val actionBarSizeAttr = intArrayOf(R.attr.actionBarSize)
|
|
|
|
val a = obtainStyledAttributes(null, actionBarSizeAttr)
|
|
|
|
val avatarSize = a.getDimensionPixelSize(0, 1)
|
|
|
|
a.recycle()
|
|
|
|
|
|
|
|
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
|
|
|
|
loadAvatar(
|
2021-05-22 17:50:08 +02:00
|
|
|
activeAccount.profilePictureUrl,
|
|
|
|
binding.composeAvatar,
|
|
|
|
avatarSize / 8,
|
|
|
|
animateAvatars
|
2019-12-19 19:09:40 +01:00
|
|
|
)
|
2021-06-28 21:13:24 +02:00
|
|
|
binding.composeAvatar.contentDescription = getString(
|
|
|
|
R.string.compose_active_account_description,
|
|
|
|
activeAccount.fullName
|
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun replaceTextAtCaret(text: CharSequence) {
|
|
|
|
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
2021-03-07 19:05:51 +01:00
|
|
|
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
|
|
|
|
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
|
|
|
|
val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) {
|
2020-02-21 22:08:41 +01:00
|
|
|
" $text"
|
|
|
|
} else {
|
|
|
|
text
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.text.replace(start, end, textToInsert)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
// Set the cursor after the inserted text
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setSelection(start + text.length)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2020-01-13 15:21:17 +01:00
|
|
|
fun prependSelectedWordsWith(text: CharSequence) {
|
|
|
|
// If you select "backward" in an editable, you get SelectionStart > SelectionEnd
|
2021-03-07 19:05:51 +01:00
|
|
|
val start = binding.composeEditField.selectionStart.coerceAtMost(binding.composeEditField.selectionEnd)
|
|
|
|
val end = binding.composeEditField.selectionStart.coerceAtLeast(binding.composeEditField.selectionEnd)
|
|
|
|
val editorText = binding.composeEditField.text
|
2020-01-13 15:21:17 +01:00
|
|
|
|
|
|
|
if (start == end) {
|
|
|
|
// No selection, just insert text at caret
|
|
|
|
editorText.insert(start, text)
|
|
|
|
// Set the cursor after the inserted text
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setSelection(start + text.length)
|
2020-01-13 15:21:17 +01:00
|
|
|
} else {
|
|
|
|
var wasWord: Boolean
|
|
|
|
var isWord = end < editorText.length && !Character.isWhitespace(editorText[end])
|
|
|
|
var newEnd = end
|
|
|
|
|
|
|
|
// Iterate the selection backward so we don't have to juggle indices on insertion
|
|
|
|
var index = end - 1
|
|
|
|
while (index >= start - 1 && index >= 0) {
|
|
|
|
wasWord = isWord
|
|
|
|
isWord = !Character.isWhitespace(editorText[index])
|
|
|
|
if (wasWord && !isWord) {
|
|
|
|
// We've reached the beginning of a word, perform insert
|
|
|
|
editorText.insert(index + 1, text)
|
|
|
|
newEnd += text.length
|
|
|
|
}
|
|
|
|
--index
|
|
|
|
}
|
|
|
|
|
|
|
|
if (start == 0 && isWord) {
|
|
|
|
// Special case when the selection includes the start of the text
|
|
|
|
editorText.insert(0, text)
|
|
|
|
newEnd += text.length
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keep the same text (including insertions) selected
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.setSelection(start, newEnd)
|
2020-01-13 15:21:17 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
private fun atButtonClicked() {
|
2020-01-13 15:21:17 +01:00
|
|
|
prependSelectedWordsWith("@")
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun hashButtonClicked() {
|
2020-01-13 15:21:17 +01:00
|
|
|
prependSelectedWordsWith("#")
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onSaveInstanceState(outState: Bundle) {
|
2020-02-24 22:03:00 +01:00
|
|
|
outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri)
|
2019-12-19 19:09:40 +01:00
|
|
|
super.onSaveInstanceState(outState)
|
|
|
|
}
|
|
|
|
|
2022-06-30 20:51:05 +02:00
|
|
|
private fun displayTransientError(errorMessage: String) {
|
|
|
|
val bar = Snackbar.make(binding.activityCompose, errorMessage, Snackbar.LENGTH_LONG)
|
2021-06-28 21:13:24 +02:00
|
|
|
// necessary so snackbar is shown over everything
|
2019-12-19 19:09:40 +01:00
|
|
|
bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
2022-06-30 20:51:05 +02:00
|
|
|
bar.setAnchorView(R.id.composeBottomBar)
|
2019-12-19 19:09:40 +01:00
|
|
|
bar.show()
|
|
|
|
}
|
2022-06-30 20:51:05 +02:00
|
|
|
private fun displayTransientError(@StringRes stringId: Int) {
|
|
|
|
displayTransientError(getString(stringId))
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
private fun toggleHideMedia() {
|
|
|
|
this.viewModel.toggleMarkSensitive()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) {
|
2022-06-30 20:51:05 +02:00
|
|
|
if (viewModel.media.value.isEmpty()) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.hide()
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.show()
|
2019-12-19 19:09:40 +01:00
|
|
|
@ColorInt val color = if (contentWarningShown) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
|
|
|
binding.composeHideMediaButton.isClickable = false
|
2022-08-04 16:48:26 +02:00
|
|
|
getColor(R.color.transparent_tusky_blue)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.isClickable = true
|
2019-12-19 19:09:40 +01:00
|
|
|
if (markMediaSensitive) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp)
|
2022-08-04 16:48:26 +02:00
|
|
|
getColor(R.color.tusky_blue)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp)
|
2019-12-19 19:09:40 +01:00
|
|
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
|
|
|
}
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateScheduleButton() {
|
2021-03-07 19:05:51 +01:00
|
|
|
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
2019-12-19 19:09:40 +01:00
|
|
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
|
|
|
} else {
|
2022-08-04 16:48:26 +02:00
|
|
|
getColor(R.color.tusky_blue)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun enableButtons(enable: Boolean) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeAddMediaButton.isClickable = enable
|
|
|
|
binding.composeToggleVisibilityButton.isClickable = enable
|
|
|
|
binding.composeEmojiButton.isClickable = enable
|
|
|
|
binding.composeHideMediaButton.isClickable = enable
|
|
|
|
binding.composeScheduleButton.isClickable = enable
|
|
|
|
binding.composeTootButton.isEnabled = enable
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun setStatusVisibility(visibility: Status.Visibility) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeOptionsBottomSheet.setStatusVisibility(visibility)
|
|
|
|
binding.composeTootButton.setStatusVisibility(visibility)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
val iconRes = when (visibility) {
|
|
|
|
Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp
|
|
|
|
Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp
|
|
|
|
Status.Visibility.DIRECT -> R.drawable.ic_email_24dp
|
|
|
|
Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp
|
|
|
|
else -> R.drawable.ic_lock_open_24dp
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeToggleVisibilityButton.setImageResource(iconRes)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun showComposeOptions() {
|
|
|
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_HIDDEN || composeOptionsBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
} else {
|
|
|
|
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onScheduleClick() {
|
2020-11-18 21:12:27 +01:00
|
|
|
if (viewModel.scheduledAt.value == null) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeScheduleView.openPickDateDialog()
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
|
|
|
showScheduleView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showScheduleView() {
|
|
|
|
if (scheduleBehavior.state == BottomSheetBehavior.STATE_HIDDEN || scheduleBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
|
|
|
scheduleBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
} else {
|
|
|
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showEmojis() {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.emojiView.adapter?.let {
|
2019-12-19 19:09:40 +01:00
|
|
|
if (it.itemCount == 0) {
|
|
|
|
val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain)
|
|
|
|
Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show()
|
|
|
|
} else {
|
|
|
|
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
|
|
|
emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
} else {
|
|
|
|
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun openPickDialog() {
|
|
|
|
if (addMediaBehavior.state == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
} else {
|
|
|
|
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun onMediaPick() {
|
|
|
|
addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
|
|
|
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
2021-06-28 21:13:24 +02:00
|
|
|
// Wait until bottom sheet is not collapsed and show next screen after
|
2019-12-19 19:09:40 +01:00
|
|
|
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
|
|
|
|
addMediaBehavior.removeBottomSheetCallback(this)
|
|
|
|
if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
2021-06-28 21:13:24 +02:00
|
|
|
ActivityCompat.requestPermissions(
|
|
|
|
this@ComposeActivity,
|
2021-05-22 17:50:08 +02:00
|
|
|
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
|
2021-06-28 21:13:24 +02:00
|
|
|
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE
|
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-05-22 17:50:08 +02:00
|
|
|
pickMediaFile.launch(true)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
|
|
}
|
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
private fun openPollDialog() = lifecycleScope.launch {
|
2019-12-19 19:09:40 +01:00
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
2022-07-26 20:24:50 +02:00
|
|
|
val instanceParams = viewModel.instanceInfo.first()
|
2021-06-28 21:13:24 +02:00
|
|
|
showAddPollDialog(
|
2022-07-26 20:24:50 +02:00
|
|
|
context = this@ComposeActivity,
|
|
|
|
poll = viewModel.poll.value,
|
|
|
|
maxOptionCount = instanceParams.pollMaxOptions,
|
|
|
|
maxOptionLength = instanceParams.pollMaxLength,
|
|
|
|
minDuration = instanceParams.pollMinDuration,
|
|
|
|
maxDuration = instanceParams.pollMaxDuration,
|
|
|
|
onUpdatePoll = viewModel::updatePoll
|
2021-06-28 21:13:24 +02:00
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun setupPollView() {
|
|
|
|
val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
|
|
|
val marginBottom = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
|
|
|
|
|
|
|
val layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
|
|
layoutParams.setMargins(margin, margin, margin, marginBottom)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.pollPreview.layoutParams = layoutParams
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.pollPreview.setOnClickListener {
|
|
|
|
val popup = PopupMenu(this, binding.pollPreview)
|
2019-12-19 19:09:40 +01:00
|
|
|
val editId = 1
|
|
|
|
val removeId = 2
|
|
|
|
popup.menu.add(0, editId, 0, R.string.edit_poll)
|
|
|
|
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
|
|
|
popup.setOnMenuItemClickListener { menuItem ->
|
|
|
|
when (menuItem.itemId) {
|
|
|
|
editId -> openPollDialog()
|
|
|
|
removeId -> removePoll()
|
|
|
|
}
|
|
|
|
true
|
|
|
|
}
|
|
|
|
popup.show()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun removePoll() {
|
|
|
|
viewModel.poll.value = null
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.pollPreview.hide()
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onVisibilityChanged(visibility: Status.Visibility) {
|
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
|
|
viewModel.statusVisibility.value = visibility
|
|
|
|
}
|
|
|
|
|
|
|
|
@VisibleForTesting
|
|
|
|
fun calculateTextLength(): Int {
|
|
|
|
var offset = 0
|
2021-03-07 19:05:51 +01:00
|
|
|
val urlSpans = binding.composeEditField.urls
|
2019-12-19 19:09:40 +01:00
|
|
|
if (urlSpans != null) {
|
|
|
|
for (span in urlSpans) {
|
2022-03-24 19:52:18 +01:00
|
|
|
// it's expected that this will be negative
|
|
|
|
// when the url length is less than the reserved character count
|
|
|
|
offset += (span.url.length - charactersReservedPerUrl)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
var length = binding.composeEditField.length() - offset
|
2022-07-26 20:24:50 +02:00
|
|
|
if (viewModel.showContentWarning.value) {
|
2021-03-07 19:05:51 +01:00
|
|
|
length += binding.composeContentWarningField.length()
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
return length
|
|
|
|
}
|
|
|
|
|
2022-08-31 18:53:57 +02:00
|
|
|
@VisibleForTesting
|
|
|
|
val selectedLanguage: String?
|
|
|
|
get() = viewModel.postLanguage
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
private fun updateVisibleCharactersLeft() {
|
2020-10-25 18:36:00 +01:00
|
|
|
val remainingLength = maximumTootCharacters - calculateTextLength()
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength)
|
2020-10-13 18:30:06 +02:00
|
|
|
|
|
|
|
val textColor = if (remainingLength < 0) {
|
2022-08-04 16:48:26 +02:00
|
|
|
getColor(R.color.tusky_red)
|
2020-10-13 18:30:06 +02:00
|
|
|
} else {
|
|
|
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeCharactersLeftView.setTextColor(textColor)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun onContentWarningChanged() {
|
2021-03-07 19:05:51 +01:00
|
|
|
val showWarning = binding.composeContentWarningBar.isGone
|
2020-04-18 15:06:24 +02:00
|
|
|
viewModel.contentWarningChanged(showWarning)
|
2019-12-19 19:09:40 +01:00
|
|
|
updateVisibleCharactersLeft()
|
|
|
|
}
|
|
|
|
|
2020-02-25 18:33:25 +01:00
|
|
|
private fun verifyScheduledTime(): Boolean {
|
2021-03-07 19:05:51 +01:00
|
|
|
return binding.composeScheduleView.verifyScheduledTime(binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value))
|
2020-02-25 18:33:25 +01:00
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
private fun onSendClicked() {
|
2020-02-25 18:33:25 +01:00
|
|
|
if (verifyScheduledTime()) {
|
|
|
|
sendStatus()
|
|
|
|
} else {
|
|
|
|
showScheduleView()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-03-09 20:50:23 +01:00
|
|
|
/** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */
|
|
|
|
override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? {
|
|
|
|
if (contentInfo.clip.description.hasMimeType("image/*")) {
|
|
|
|
val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
|
|
|
|
split.first?.let { content ->
|
|
|
|
for (i in 0 until content.clip.itemCount) {
|
|
|
|
pickMedia(content.clip.getItemAt(i).uri)
|
2020-02-24 22:03:00 +01:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-03-09 20:50:23 +01:00
|
|
|
return split.second
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-03-09 20:50:23 +01:00
|
|
|
return contentInfo
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun sendStatus() {
|
2020-02-25 18:33:25 +01:00
|
|
|
enableButtons(false)
|
2021-03-07 19:05:51 +01:00
|
|
|
val contentText = binding.composeEditField.text.toString()
|
2019-12-19 19:09:40 +01:00
|
|
|
var spoilerText = ""
|
2022-07-26 20:24:50 +02:00
|
|
|
if (viewModel.showContentWarning.value) {
|
2021-03-07 19:05:51 +01:00
|
|
|
spoilerText = binding.composeContentWarningField.text.toString()
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
val characterCount = calculateTextLength()
|
2022-05-03 19:12:35 +02:00
|
|
|
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.error = getString(R.string.error_empty)
|
2019-12-19 19:09:40 +01:00
|
|
|
enableButtons(true)
|
|
|
|
} else if (characterCount <= maximumTootCharacters) {
|
2022-05-03 19:12:35 +02:00
|
|
|
if (viewModel.media.value.isNotEmpty()) {
|
2020-10-25 18:41:11 +01:00
|
|
|
finishingUploadDialog = ProgressDialog.show(
|
2021-05-22 17:50:08 +02:00
|
|
|
this, getString(R.string.dialog_title_finishing_media_upload),
|
2021-06-28 21:13:24 +02:00
|
|
|
getString(R.string.dialog_message_uploading_media), true, true
|
|
|
|
)
|
2020-10-25 18:41:11 +01:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-07-26 20:24:50 +02:00
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.sendStatus(contentText, spoilerText)
|
2022-03-09 20:50:23 +01:00
|
|
|
finishingUploadDialog?.dismiss()
|
|
|
|
deleteDraftAndFinish()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
2019-12-19 19:09:40 +01:00
|
|
|
enableButtons(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-22 17:50:08 +02:00
|
|
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
|
|
|
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) {
|
|
|
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
2021-05-22 17:50:08 +02:00
|
|
|
pickMediaFile.launch(true)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-06-28 21:13:24 +02:00
|
|
|
Snackbar.make(
|
|
|
|
binding.activityCompose, R.string.error_media_upload_permission,
|
|
|
|
Snackbar.LENGTH_SHORT
|
|
|
|
).apply {
|
2021-05-22 17:50:08 +02:00
|
|
|
setAction(R.string.action_retry) { onMediaPick() }
|
2021-06-28 21:13:24 +02:00
|
|
|
// necessary so snackbar is shown over everything
|
2021-05-22 17:50:08 +02:00
|
|
|
view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation)
|
|
|
|
show()
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun initiateCameraApp() {
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
|
|
|
|
2021-05-22 17:50:08 +02:00
|
|
|
val photoFile: File = try {
|
|
|
|
createNewImageFile(this)
|
|
|
|
} catch (ex: IOException) {
|
|
|
|
displayTransientError(R.string.error_media_upload_opening)
|
|
|
|
return
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2021-05-22 17:50:08 +02:00
|
|
|
// Continue only if the File was successfully created
|
2021-06-28 21:13:24 +02:00
|
|
|
photoUploadUri = FileProvider.getUriForFile(
|
|
|
|
this,
|
2021-05-22 17:50:08 +02:00
|
|
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
2021-06-28 21:13:24 +02:00
|
|
|
photoFile
|
|
|
|
)
|
2021-05-22 17:50:08 +02:00
|
|
|
takePicture.launch(photoUploadUri)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) {
|
|
|
|
button.isEnabled = clickable
|
2021-06-28 21:13:24 +02:00
|
|
|
ThemeUtils.setDrawableTint(
|
|
|
|
this, button.drawable,
|
2021-05-22 17:50:08 +02:00
|
|
|
if (colorActive) android.R.attr.textColorTertiary
|
2021-06-28 21:13:24 +02:00
|
|
|
else R.attr.textColorDisabled
|
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun enablePollButton(enable: Boolean) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.addPollTextActionTextView.isEnabled = enable
|
2021-06-28 21:13:24 +02:00
|
|
|
val textColor = ThemeUtils.getColor(
|
|
|
|
this,
|
2021-05-22 17:50:08 +02:00
|
|
|
if (enable) android.R.attr.textColorTertiary
|
2021-06-28 21:13:24 +02:00
|
|
|
else R.attr.textColorDisabled
|
|
|
|
)
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.addPollTextActionTextView.setTextColor(textColor)
|
|
|
|
binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-05-22 21:01:14 +02:00
|
|
|
private fun editImageInQueue(item: QueuedMedia) {
|
|
|
|
// If input image is lossless, output image should be lossless.
|
|
|
|
// Currently the only supported lossless format is png.
|
|
|
|
val mimeType: String? = contentResolver.getType(item.uri)
|
|
|
|
val isPng: Boolean = mimeType != null && mimeType.endsWith("/png")
|
2022-06-30 20:51:05 +02:00
|
|
|
val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg")
|
2022-05-22 21:01:14 +02:00
|
|
|
|
|
|
|
// "Authority" must be the same as the android:authorities string in AndroidManifest.xml
|
2022-06-30 20:51:05 +02:00
|
|
|
val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile)
|
2022-05-22 21:01:14 +02:00
|
|
|
|
|
|
|
viewModel.cropImageItemOld = item
|
|
|
|
|
|
|
|
cropImage.launch(
|
|
|
|
options(uri = item.uri) {
|
|
|
|
setOutputUri(uriNew)
|
|
|
|
setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG)
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
private fun removeMediaFromQueue(item: QueuedMedia) {
|
|
|
|
viewModel.removeMediaFromQueue(item)
|
|
|
|
}
|
|
|
|
|
2022-03-09 20:50:23 +01:00
|
|
|
private fun pickMedia(uri: Uri) {
|
2022-04-21 18:46:21 +02:00
|
|
|
lifecycleScope.launch {
|
|
|
|
viewModel.pickMedia(uri).onFailure { throwable ->
|
2022-08-05 18:55:13 +02:00
|
|
|
val errorString = when (throwable) {
|
|
|
|
is FileSizeException -> {
|
|
|
|
val decimalFormat = DecimalFormat("0.##")
|
|
|
|
val allowedSizeInMb = throwable.allowedSizeInBytes.toDouble() / (1024 * 1024)
|
|
|
|
val formattedSize = decimalFormat.format(allowedSizeInMb)
|
|
|
|
getString(R.string.error_multimedia_size_limit, formattedSize)
|
|
|
|
}
|
|
|
|
is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video)
|
|
|
|
else -> getString(R.string.error_media_upload_opening)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
2022-08-05 18:55:13 +02:00
|
|
|
displayTransientError(errorString)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun showContentWarning(show: Boolean) {
|
2021-03-07 19:05:51 +01:00
|
|
|
TransitionManager.beginDelayedTransition(binding.composeContentWarningBar.parent as ViewGroup)
|
2019-12-19 19:09:40 +01:00
|
|
|
@ColorInt val color = if (show) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeContentWarningBar.show()
|
|
|
|
binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length)
|
|
|
|
binding.composeContentWarningField.requestFocus()
|
2022-08-04 16:48:26 +02:00
|
|
|
getColor(R.color.tusky_blue)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeContentWarningBar.hide()
|
|
|
|
binding.composeEditField.requestFocus()
|
2019-12-19 19:09:40 +01:00
|
|
|
ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeContentWarningButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
|
|
if (item.itemId == android.R.id.home) {
|
|
|
|
handleCloseButton()
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
return super.onOptionsItemSelected(item)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onBackPressed() {
|
|
|
|
// Acting like a teen: deliberately ignoring parent.
|
|
|
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
2021-05-22 17:50:08 +02:00
|
|
|
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
|
|
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
2021-06-28 21:13:24 +02:00
|
|
|
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
|
|
|
) {
|
2019-12-19 19:09:40 +01:00
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCloseButton()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
|
|
Log.d(TAG, event.toString())
|
2020-11-18 21:12:27 +01:00
|
|
|
if (event.action == KeyEvent.ACTION_DOWN) {
|
2020-04-18 13:45:19 +02:00
|
|
|
if (event.isCtrlPressed) {
|
|
|
|
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
|
|
|
// send toot by pressing CTRL + ENTER
|
|
|
|
this.onSendClicked()
|
|
|
|
return true
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2020-04-18 13:45:19 +02:00
|
|
|
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
|
|
onBackPressed()
|
|
|
|
return true
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
return super.onKeyDown(keyCode, event)
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun handleCloseButton() {
|
2021-03-07 19:05:51 +01:00
|
|
|
val contentText = binding.composeEditField.text.toString()
|
|
|
|
val contentWarning = binding.composeContentWarningField.text.toString()
|
2019-12-19 19:09:40 +01:00
|
|
|
if (viewModel.didChange(contentText, contentWarning)) {
|
|
|
|
AlertDialog.Builder(this)
|
2021-05-22 17:50:08 +02:00
|
|
|
.setMessage(R.string.compose_save_draft)
|
|
|
|
.setPositiveButton(R.string.action_save) { _, _ ->
|
|
|
|
saveDraftAndFinish(contentText, contentWarning)
|
|
|
|
}
|
|
|
|
.setNegativeButton(R.string.action_delete) { _, _ -> deleteDraftAndFinish() }
|
|
|
|
.show()
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
|
|
|
finishWithoutSlideOutAnimation()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun deleteDraftAndFinish() {
|
|
|
|
viewModel.deleteDraft()
|
|
|
|
finishWithoutSlideOutAnimation()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
2022-05-09 19:39:43 +02:00
|
|
|
lifecycleScope.launch {
|
|
|
|
val dialog = if (viewModel.shouldShowSaveDraftDialog()) {
|
|
|
|
ProgressDialog.show(
|
|
|
|
this@ComposeActivity, null,
|
|
|
|
getString(R.string.saving_draft), true, false
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
null
|
|
|
|
}
|
|
|
|
viewModel.saveDraft(contentText, contentWarning)
|
|
|
|
dialog?.cancel()
|
|
|
|
finishWithoutSlideOutAnimation()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
|
|
|
return viewModel.searchAutocompleteSuggestions(token)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onEmojiSelected(shortcode: String) {
|
|
|
|
replaceTextAtCaret(":$shortcode: ")
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun setEmojiList(emojiList: List<Emoji>?) {
|
|
|
|
if (emojiList != null) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity)
|
|
|
|
enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty())
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data class QueuedMedia(
|
2022-05-03 19:12:35 +02:00
|
|
|
val localId: Int,
|
2021-05-22 17:50:08 +02:00
|
|
|
val uri: Uri,
|
|
|
|
val type: Type,
|
|
|
|
val mediaSize: Long,
|
|
|
|
val uploadPercent: Int = 0,
|
|
|
|
val id: String? = null,
|
|
|
|
val description: String? = null
|
2019-12-19 19:09:40 +01:00
|
|
|
) {
|
|
|
|
enum class Type {
|
2020-01-16 19:05:52 +01:00
|
|
|
IMAGE, VIDEO, AUDIO;
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-23 20:29:02 +01:00
|
|
|
override fun onTimeSet(time: String) {
|
|
|
|
viewModel.updateScheduledAt(time)
|
2020-02-25 18:33:25 +01:00
|
|
|
if (verifyScheduledTime()) {
|
|
|
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
} else {
|
|
|
|
showScheduleView()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun resetSchedule() {
|
|
|
|
viewModel.updateScheduledAt(null)
|
|
|
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
}
|
|
|
|
|
|
|
|
@Parcelize
|
|
|
|
data class ComposeOptions(
|
2021-05-22 17:50:08 +02:00
|
|
|
// Let's keep fields var until all consumers are Kotlin
|
|
|
|
var scheduledTootId: String? = null,
|
|
|
|
var draftId: Int? = null,
|
2022-03-20 20:21:42 +01:00
|
|
|
var content: String? = null,
|
2021-05-22 17:50:08 +02:00
|
|
|
var mediaUrls: List<String>? = null,
|
|
|
|
var mediaDescriptions: List<String>? = null,
|
|
|
|
var mentionedUsernames: Set<String>? = null,
|
|
|
|
var inReplyToId: String? = null,
|
|
|
|
var replyVisibility: Status.Visibility? = null,
|
|
|
|
var visibility: Status.Visibility? = null,
|
|
|
|
var contentWarning: String? = null,
|
|
|
|
var replyingStatusAuthor: String? = null,
|
|
|
|
var replyingStatusContent: String? = null,
|
|
|
|
var mediaAttachments: List<Attachment>? = null,
|
|
|
|
var draftAttachments: List<DraftAttachment>? = null,
|
|
|
|
var scheduledAt: String? = null,
|
|
|
|
var sensitive: Boolean? = null,
|
|
|
|
var poll: NewPoll? = null,
|
2022-08-31 18:53:57 +02:00
|
|
|
var modifiedInitialState: Boolean? = null,
|
|
|
|
var language: String? = null,
|
2019-12-19 19:09:40 +01:00
|
|
|
) : Parcelable
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
private const val TAG = "ComposeActivity" // logging tag
|
|
|
|
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
|
|
|
|
2020-10-28 18:43:11 +01:00
|
|
|
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
2022-03-09 20:50:23 +01:00
|
|
|
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
|
|
|
|
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
|
2020-02-24 22:03:00 +01:00
|
|
|
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-03-09 20:50:23 +01:00
|
|
|
/**
|
|
|
|
* @param options ComposeOptions to configure the ComposeActivity
|
|
|
|
* @param notificationId the id of the notification that starts the Activity
|
|
|
|
* @param accountId the id of the account to compose with, null for the current account
|
|
|
|
* @return an Intent to start the ComposeActivity
|
|
|
|
*/
|
2019-12-19 19:09:40 +01:00
|
|
|
@JvmStatic
|
2022-03-09 20:50:23 +01:00
|
|
|
@JvmOverloads
|
|
|
|
fun startIntent(
|
|
|
|
context: Context,
|
|
|
|
options: ComposeOptions,
|
|
|
|
notificationId: Int? = null,
|
|
|
|
accountId: Long? = null
|
|
|
|
): Intent {
|
2019-12-19 19:09:40 +01:00
|
|
|
return Intent(context, ComposeActivity::class.java).apply {
|
|
|
|
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
2022-03-09 20:50:23 +01:00
|
|
|
if (notificationId != null) {
|
|
|
|
putExtra(NOTIFICATION_ID_EXTRA, notificationId)
|
|
|
|
}
|
|
|
|
if (accountId != null) {
|
|
|
|
putExtra(ACCOUNT_ID_EXTRA, accountId)
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fun canHandleMimeType(mimeType: String?): Boolean {
|
2020-01-16 19:05:52 +01:00
|
|
|
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|