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
|
2019-12-27 06:46:18 +01:00
|
|
|
import android.net.ConnectivityManager
|
2019-12-19 19:09:40 +01:00
|
|
|
import android.net.Uri
|
|
|
|
import android.os.Build
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.os.Parcelable
|
2022-12-05 19:15:58 +01:00
|
|
|
import android.provider.MediaStore
|
2019-12-19 19:09:40 +01:00
|
|
|
import android.util.Log
|
|
|
|
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
|
2022-11-04 19:22:38 +01:00
|
|
|
import androidx.activity.OnBackPressedCallback
|
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
|
2019-12-27 06:46:18 +01:00
|
|
|
import androidx.core.widget.doAfterTextChanged
|
2022-04-21 18:46:21 +02:00
|
|
|
import androidx.lifecycle.lifecycleScope
|
2019-12-27 06:46:18 +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
|
2022-12-31 13:01:35 +01:00
|
|
|
import com.google.android.material.color.MaterialColors
|
2019-12-19 19:09:40 +01:00
|
|
|
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
|
2019-12-27 06:46:18 +01:00
|
|
|
import com.keylesspalace.tusky.appstore.EventHub
|
|
|
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
2022-09-12 18:21:00 +02:00
|
|
|
import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog
|
Add UI for image-attachment "focus" (#2620)
* Attempt-zero implementation of a "focus" feature for image attachments. Choose "Set focus" in the attachment menu, tap once to select focus point (no visual feedback currently), tap "OK". Works in tests.
* Remove code duplication between 'update description' and 'update focus'
* Fix ktlint/bitrise failures
* Make updateMediaItem private
* When focus is set on a post attachment the preview focuses correctly. ProgressImageView now inherits from MediaPreviewImageView.
* Replace use of PointF for Focus where focus is represented, fix ktlint
* Substitute 'focus' for 'focus point' in strings
* First attempt draw focus point. Only updates on initial load. Modeled on code from RoundedCorners builtin from Glide
* Redraw focus after each tap
* Dark curtain where focus isn't (now looks like mastosoc)
* Correct ktlint for FocusDialog
* draft: switch to overlay for focus indicator
* Draw focus circle, but ImageView and FocusIndicatorView seem to share a single canvas
* Switch focus circle to path approach
* Correctly scale, save and load focuses. Clamp to visible area. Focus editor looks and feels right
* ktlint fixes and comments
* Focus indicator drawing should use device-independent pixels
* Shrink focus window when it gets unattractively tall (no linting, misbehaves on wide aspect ratio screens)
* Correct max-height behavior for screens in landscape mode
* Focus attachment result is are flipped on x axis; fix this
* Correctly thread focus through on scheduled posts, redrafted posts, and drafts (but draft focus is lost on post)
* More focus ktlint fixes
* Fix specific case where a draft is given a focus, then deleted, then posted in that order
* Fix accidental file change in focus PR
* ktLint fix
* Fix property style warnings in focus
* Fix remaining style warnings from focus PR
Co-authored-by: Conny Duck <k.pozniak@gmx.at>
2022-09-21 20:28:06 +02:00
|
|
|
import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog
|
2019-12-27 06:46:18 +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
|
2022-12-31 13:01:35 +01:00
|
|
|
import com.keylesspalace.tusky.util.APP_THEME_DEFAULT
|
2021-06-28 21:13:24 +02:00
|
|
|
import com.keylesspalace.tusky.util.PickMediaFiles
|
|
|
|
import com.keylesspalace.tusky.util.afterTextChanged
|
2022-12-02 19:19:17 +01:00
|
|
|
import com.keylesspalace.tusky.util.getInitialLanguage
|
|
|
|
import com.keylesspalace.tusky.util.getLocaleList
|
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
|
2022-11-24 15:45:19 +01:00
|
|
|
import com.keylesspalace.tusky.util.modernLanguageCode
|
2021-06-28 21:13:24 +02:00
|
|
|
import com.keylesspalace.tusky.util.onTextChanged
|
2022-12-31 13:01:35 +01:00
|
|
|
import com.keylesspalace.tusky.util.setDrawableTint
|
2021-06-28 21:13:24 +02:00
|
|
|
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,
|
2022-09-12 18:21:00 +02:00
|
|
|
ComposeScheduleView.OnTimeSetListener,
|
|
|
|
CaptionDialog.Listener {
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
@Inject
|
|
|
|
lateinit var viewModelFactory: ViewModelFactory
|
2019-12-27 06:46:18 +01:00
|
|
|
@Inject
|
|
|
|
lateinit var eventHub: EventHub
|
2019-12-19 19:09:40 +01:00
|
|
|
|
|
|
|
private lateinit var composeOptionsBehavior: BottomSheetBehavior<*>
|
|
|
|
private lateinit var addMediaBehavior: BottomSheetBehavior<*>
|
|
|
|
private lateinit var emojiBehavior: BottomSheetBehavior<*>
|
|
|
|
private lateinit var scheduleBehavior: BottomSheetBehavior<*>
|
|
|
|
|
|
|
|
private var photoUploadUri: Uri? = null
|
2021-01-21 18:57:09 +01:00
|
|
|
|
2022-12-05 19:15:28 +01:00
|
|
|
private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
|
|
|
|
|
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,
|
Add UI for image-attachment "focus" (#2620)
* Attempt-zero implementation of a "focus" feature for image attachments. Choose "Set focus" in the attachment menu, tap once to select focus point (no visual feedback currently), tap "OK". Works in tests.
* Remove code duplication between 'update description' and 'update focus'
* Fix ktlint/bitrise failures
* Make updateMediaItem private
* When focus is set on a post attachment the preview focuses correctly. ProgressImageView now inherits from MediaPreviewImageView.
* Replace use of PointF for Focus where focus is represented, fix ktlint
* Substitute 'focus' for 'focus point' in strings
* First attempt draw focus point. Only updates on initial load. Modeled on code from RoundedCorners builtin from Glide
* Redraw focus after each tap
* Dark curtain where focus isn't (now looks like mastosoc)
* Correct ktlint for FocusDialog
* draft: switch to overlay for focus indicator
* Draw focus circle, but ImageView and FocusIndicatorView seem to share a single canvas
* Switch focus circle to path approach
* Correctly scale, save and load focuses. Clamp to visible area. Focus editor looks and feels right
* ktlint fixes and comments
* Focus indicator drawing should use device-independent pixels
* Shrink focus window when it gets unattractively tall (no linting, misbehaves on wide aspect ratio screens)
* Correct max-height behavior for screens in landscape mode
* Focus attachment result is are flipped on x axis; fix this
* Correctly thread focus through on scheduled posts, redrafted posts, and drafts (but draft focus is lost on post)
* More focus ktlint fixes
* Fix specific case where a draft is given a focus, then deleted, then posted in that order
* Fix accidental file change in focus PR
* ktLint fix
* Fix property style warnings in focus
* Fix remaining style warnings from focus PR
Co-authored-by: Conny Duck <k.pozniak@gmx.at>
2022-09-21 20:28:06 +02:00
|
|
|
null, // Intentionally reset focus when cropping
|
2022-05-22 21:01:14 +02:00
|
|
|
itemOld
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (result == CropImage.CancelledResult) {
|
|
|
|
Log.w("ComposeActivity", "Edit image cancelled by user")
|
|
|
|
} else {
|
|
|
|
Log.w("ComposeActivity", "Edit image failed: " + result.error)
|
2022-12-06 19:28:44 +01:00
|
|
|
displayTransientMessage(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)
|
|
|
|
}
|
|
|
|
|
2022-12-31 13:01:35 +01:00
|
|
|
val theme = preferences.getString("appTheme", APP_THEME_DEFAULT)
|
2019-12-19 19:09:40 +01:00
|
|
|
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
|
|
|
|
|
2022-12-05 19:15:28 +01:00
|
|
|
setupAvatar(activeAccount)
|
2019-12-19 19:09:40 +01:00
|
|
|
val mediaAdapter = MediaPreviewAdapter(
|
2021-05-22 17:50:08 +02:00
|
|
|
this,
|
|
|
|
onAddCaption = { item ->
|
2022-09-12 18:21:00 +02:00
|
|
|
CaptionDialog.newInstance(item.localId, item.description, item.uri)
|
|
|
|
.show(supportFragmentManager, "caption_dialog")
|
2021-05-22 17:50:08 +02:00
|
|
|
},
|
Add UI for image-attachment "focus" (#2620)
* Attempt-zero implementation of a "focus" feature for image attachments. Choose "Set focus" in the attachment menu, tap once to select focus point (no visual feedback currently), tap "OK". Works in tests.
* Remove code duplication between 'update description' and 'update focus'
* Fix ktlint/bitrise failures
* Make updateMediaItem private
* When focus is set on a post attachment the preview focuses correctly. ProgressImageView now inherits from MediaPreviewImageView.
* Replace use of PointF for Focus where focus is represented, fix ktlint
* Substitute 'focus' for 'focus point' in strings
* First attempt draw focus point. Only updates on initial load. Modeled on code from RoundedCorners builtin from Glide
* Redraw focus after each tap
* Dark curtain where focus isn't (now looks like mastosoc)
* Correct ktlint for FocusDialog
* draft: switch to overlay for focus indicator
* Draw focus circle, but ImageView and FocusIndicatorView seem to share a single canvas
* Switch focus circle to path approach
* Correctly scale, save and load focuses. Clamp to visible area. Focus editor looks and feels right
* ktlint fixes and comments
* Focus indicator drawing should use device-independent pixels
* Shrink focus window when it gets unattractively tall (no linting, misbehaves on wide aspect ratio screens)
* Correct max-height behavior for screens in landscape mode
* Focus attachment result is are flipped on x axis; fix this
* Correctly thread focus through on scheduled posts, redrafted posts, and drafts (but draft focus is lost on post)
* More focus ktlint fixes
* Fix specific case where a draft is given a focus, then deleted, then posted in that order
* Fix accidental file change in focus PR
* ktLint fix
* Fix property style warnings in focus
* Fix remaining style warnings from focus PR
Co-authored-by: Conny Duck <k.pozniak@gmx.at>
2022-09-21 20:28:06 +02:00
|
|
|
onAddFocus = { item ->
|
|
|
|
makeFocusDialog(item.focus, item.uri) { newFocus ->
|
|
|
|
viewModel.updateFocus(item.localId, newFocus)
|
2021-05-22 17:50:08 +02:00
|
|
|
}
|
|
|
|
},
|
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
|
|
|
|
|
|
|
/* 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)
|
2022-11-10 14:20:09 +01:00
|
|
|
viewModel.setup(composeOptions, useCachedData(preferences, composeOptions?.tootRightNow == true))
|
2022-09-17 19:05:56 +02:00
|
|
|
|
2022-12-08 10:18:12 +01:00
|
|
|
setupButtons()
|
|
|
|
subscribeToUpdates(mediaAdapter)
|
|
|
|
|
2022-10-18 19:38:17 +02:00
|
|
|
if (accountManager.shouldDisplaySelfUsername(this)) {
|
2022-09-17 19:05:56 +02:00
|
|
|
binding.composeUsernameView.text = getString(
|
|
|
|
R.string.compose_active_account_description,
|
|
|
|
activeAccount.fullName
|
|
|
|
)
|
|
|
|
binding.composeUsernameView.show()
|
|
|
|
} else {
|
|
|
|
binding.composeUsernameView.hide()
|
|
|
|
}
|
|
|
|
|
2021-01-21 18:57:09 +01:00
|
|
|
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
2021-03-04 07:08:48 +01:00
|
|
|
setupQuoteView(composeOptions?.quoteStatusAuthor, composeOptions?.quoteStatusContent)
|
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-12-02 19:19:17 +01:00
|
|
|
setupLanguageSpinner(getInitialLanguage(composeOptions?.language, accountManager.activeAccount))
|
2021-02-06 08:14:51 +01:00
|
|
|
setupComposeField(preferences, viewModel.startingText)
|
2019-12-27 06:46:18 +01:00
|
|
|
setupDefaultTagViews(preferences)
|
2019-12-19 19:09:40 +01:00
|
|
|
setupContentWarningField(composeOptions?.contentWarning)
|
|
|
|
setupPollView()
|
|
|
|
applyShareIntent(intent, savedInstanceState)
|
2022-08-03 17:23:54 +02:00
|
|
|
|
2022-10-18 19:38:27 +02:00
|
|
|
/* Finally, overwrite state with data from saved instance state. */
|
|
|
|
savedInstanceState?.let {
|
|
|
|
photoUploadUri = it.getParcelable(PHOTO_UPLOAD_URI_KEY)
|
|
|
|
|
|
|
|
(it.getSerializable(VISIBILITY_KEY) as Status.Visibility).apply {
|
|
|
|
setStatusVisibility(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply {
|
|
|
|
viewModel.contentWarningChanged(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
it.getString(SCHEDULED_TIME_KEY)?.let { time ->
|
|
|
|
viewModel.updateScheduledAt(time)
|
|
|
|
}
|
|
|
|
}
|
2019-12-27 06:46:18 +01:00
|
|
|
|
|
|
|
if (composeOptions?.tootRightNow == true && calculateTextLength() > 0) {
|
|
|
|
onSendClicked()
|
|
|
|
}
|
2022-11-10 14:20:09 +01:00
|
|
|
|
2022-08-03 17:23:54 +02:00
|
|
|
binding.composeEditField.post {
|
|
|
|
binding.composeEditField.requestFocus()
|
|
|
|
}
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
|
|
|
|
2022-11-10 14:20:09 +01:00
|
|
|
private fun useCachedData(preferences: SharedPreferences, tootRightNow: Boolean): Boolean {
|
2021-03-04 07:08:48 +01:00
|
|
|
if (tootRightNow) {
|
2022-11-10 14:20:09 +01:00
|
|
|
return true // from Quick Toot
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
|
|
|
if (!preferences.getBoolean("limitedBandwidthActive", false)) {
|
2022-11-10 14:20:09 +01:00
|
|
|
return false // Limited Bandwidth Mode disabled
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
|
|
|
if (!preferences.getBoolean("limitedBandwidthOnlyMobileNetwork", true)) {
|
2022-11-10 14:20:09 +01:00
|
|
|
return true // Limited Bandwidth Mode enabled && Only Mobile Network disabled
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
2022-11-10 14:20:09 +01:00
|
|
|
return (getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
|
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-02-26 20:41:02 +01: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
|
|
|
|
2022-12-31 13:01:35 +01:00
|
|
|
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
|
|
|
|
2022-12-31 13:01:35 +01:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-03-04 07:08:48 +01:00
|
|
|
private fun setupQuoteView(quoteStatusAuthor: String?, quoteStatusContent: String?) {
|
2020-05-16 10:47:53 +02:00
|
|
|
if (quoteStatusAuthor != null) {
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.composeQuoteView.show()
|
|
|
|
binding.composeQuoteView.text = getString(R.string.quote_to, quoteStatusAuthor)
|
2020-05-16 10:47:53 +02:00
|
|
|
val arrowDownIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_down).apply { sizeDp = 12 }
|
|
|
|
|
2023-01-25 00:46:02 +01:00
|
|
|
setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary)
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.composeQuoteView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
2020-05-16 10:47:53 +02:00
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.composeQuoteView.setOnClickListener {
|
|
|
|
TransitionManager.beginDelayedTransition(binding.composeQuoteContentView.parent as ViewGroup)
|
2020-05-16 10:47:53 +02:00
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
if (binding.composeQuoteContentView.isVisible) {
|
|
|
|
binding.composeQuoteContentView.hide()
|
|
|
|
binding.composeQuoteView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowDownIcon, null)
|
2020-05-16 10:47:53 +02:00
|
|
|
} else {
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.composeQuoteContentView.show()
|
2020-05-16 10:47:53 +02:00
|
|
|
val arrowUpIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_arrow_drop_up).apply { sizeDp = 12 }
|
|
|
|
|
2023-01-25 00:46:02 +01:00
|
|
|
setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary)
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, arrowUpIcon, null)
|
2020-05-16 10:47:53 +02:00
|
|
|
}
|
|
|
|
}
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
2021-04-28 04:54:29 +02:00
|
|
|
quoteStatusContent?.let { binding.composeQuoteContentView.text = it }
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
|
|
|
|
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) {
|
2022-12-06 19:28:44 +01:00
|
|
|
displayTransientMessage(throwable.errorMessage)
|
2022-06-30 20:51:05 +02:00
|
|
|
} else {
|
2022-12-06 19:28:44 +01:00
|
|
|
displayTransientMessage(R.string.error_media_upload_sending)
|
2022-06-30 20:51:05 +02:00
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-27 06:46:18 +01:00
|
|
|
private fun setupDefaultTagViews(preferences: SharedPreferences) {
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.checkboxUseDefaultText.isChecked = preferences.getBoolean(PREF_USE_DEFAULT_TAG, false)
|
|
|
|
binding.checkboxUseDefaultText.setOnCheckedChangeListener { _, isChecked ->
|
2019-12-27 06:46:18 +01:00
|
|
|
preferences.edit()
|
2022-11-10 15:16:52 +01:00
|
|
|
.putBoolean(PREF_USE_DEFAULT_TAG, isChecked)
|
|
|
|
.apply()
|
2019-12-27 06:46:18 +01:00
|
|
|
eventHub.dispatch(PreferenceChangedEvent(PREF_USE_DEFAULT_TAG))
|
|
|
|
}
|
|
|
|
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.editTextDefaultText.setText(preferences.getString(PREF_DEFAULT_TAG, ""))
|
|
|
|
binding.editTextDefaultText.doAfterTextChanged {
|
2019-12-27 06:46:18 +01:00
|
|
|
preferences.edit()
|
2022-11-10 15:16:52 +01:00
|
|
|
.putString(PREF_DEFAULT_TAG, it.toString())
|
|
|
|
.apply()
|
2019-12-27 06:46:18 +01:00
|
|
|
eventHub.dispatch(PreferenceChangedEvent(PREF_DEFAULT_TAG))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
private fun setupButtons() {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeOptionsBottomSheet.listener = this
|
2021-04-28 04:54:29 +02:00
|
|
|
binding.composeOptionsBottomSheet.allowUnleakable(viewModel.domain in CAN_USE_UNLEAKABLE)
|
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() }
|
2022-12-06 19:28:44 +01:00
|
|
|
binding.descriptionMissingWarningButton.setOnClickListener {
|
|
|
|
displayTransientMessage(R.string.hint_media_description_missing)
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2022-12-31 13:01:35 +01:00
|
|
|
val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary)
|
2019-12-19 19:09:40 +01:00
|
|
|
|
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
|
|
|
|
2022-12-05 19:15:58 +01:00
|
|
|
binding.actionPhotoTake.visible(Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null)
|
|
|
|
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.actionPhotoTake.setOnClickListener { initiateCameraApp() }
|
|
|
|
binding.actionPhotoPick.setOnClickListener { onMediaPick() }
|
|
|
|
binding.addPollTextActionTextView.setOnClickListener { openPollDialog() }
|
2022-11-04 19:22:38 +01:00
|
|
|
|
|
|
|
onBackPressedDispatcher.addCallback(
|
|
|
|
this,
|
|
|
|
object : OnBackPressedCallback(true) {
|
|
|
|
override fun handleOnBackPressed() {
|
|
|
|
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
|
|
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
|
|
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
|
|
|
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED
|
|
|
|
) {
|
|
|
|
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCloseButton()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2022-11-24 15:45:19 +01:00
|
|
|
private fun setupLanguageSpinner(initialLanguage: String) {
|
2022-08-31 18:53:57 +02:00
|
|
|
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
|
|
|
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
2022-11-24 15:45:19 +01:00
|
|
|
viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
|
2022-08-31 18:53:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onNothingSelected(parent: AdapterView<*>) {
|
2022-11-24 15:45:19 +01:00
|
|
|
parent.setSelection(0)
|
2022-08-31 18:53:57 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
binding.composePostLanguageButton.apply {
|
2022-12-02 19:19:17 +01:00
|
|
|
adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, getLocaleList(initialLanguage))
|
2022-11-24 15:45:19 +01:00
|
|
|
setSelection(0)
|
2022-08-31 18:53:57 +02:00
|
|
|
}
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-05 19:15:28 +01:00
|
|
|
private fun setupAvatar(activeAccount: AccountEntity) {
|
2019-12-19 19:09:40 +01:00
|
|
|
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)
|
2022-10-18 19:38:27 +02:00
|
|
|
outState.putSerializable(VISIBILITY_KEY, viewModel.statusVisibility.value)
|
|
|
|
outState.putBoolean(CONTENT_WARNING_VISIBLE_KEY, viewModel.showContentWarning.value)
|
|
|
|
outState.putString(SCHEDULED_TIME_KEY, viewModel.scheduledAt.value)
|
2019-12-19 19:09:40 +01:00
|
|
|
super.onSaveInstanceState(outState)
|
|
|
|
}
|
|
|
|
|
2022-12-06 19:28:44 +01:00
|
|
|
private fun displayTransientMessage(message: String) {
|
|
|
|
val bar = Snackbar.make(binding.activityCompose, message, 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-12-06 19:28:44 +01:00
|
|
|
private fun displayTransientMessage(@StringRes stringId: Int) {
|
|
|
|
displayTransientMessage(getString(stringId))
|
2022-06-30 20:51:05 +02:00
|
|
|
}
|
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()
|
2022-12-06 19:28:44 +01:00
|
|
|
binding.descriptionMissingWarningButton.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)
|
2022-12-31 13:01:35 +01:00
|
|
|
MaterialColors.getColor(binding.composeHideMediaButton, android.R.attr.textColorTertiary)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeHideMediaButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
2022-12-06 19:28:44 +01:00
|
|
|
|
|
|
|
var oneMediaWithoutDescription = false
|
|
|
|
for (media in viewModel.media.value) {
|
|
|
|
if (media.description == null || media.description.isEmpty()) {
|
|
|
|
oneMediaWithoutDescription = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
binding.descriptionMissingWarningButton.visibility = if (oneMediaWithoutDescription) View.VISIBLE else View.GONE
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun updateScheduleButton() {
|
2022-12-08 10:18:12 +01:00
|
|
|
if (viewModel.editing) {
|
|
|
|
// Can't reschedule a published status
|
|
|
|
enableButton(binding.composeScheduleButton, clickable = false, colorActive = false)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
2022-12-08 10:18:12 +01:00
|
|
|
@ColorInt val color = if (binding.composeScheduleView.time == null) {
|
2022-12-31 13:01:35 +01:00
|
|
|
MaterialColors.getColor(binding.composeScheduleButton, android.R.attr.textColorTertiary)
|
2022-12-08 10:18:12 +01:00
|
|
|
} else {
|
|
|
|
getColor(R.color.tusky_blue)
|
|
|
|
}
|
|
|
|
binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-08 10:18:12 +01:00
|
|
|
private fun enableButtons(enable: Boolean, editing: Boolean) {
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeAddMediaButton.isClickable = enable
|
2022-12-08 10:18:12 +01:00
|
|
|
binding.composeToggleVisibilityButton.isClickable = enable && !editing
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeEmojiButton.isClickable = enable
|
|
|
|
binding.composeHideMediaButton.isClickable = enable
|
2022-12-08 10:18:12 +01:00
|
|
|
binding.composeScheduleButton.isClickable = enable && !editing
|
2021-03-07 19:05:51 +01:00
|
|
|
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
|
2020-02-12 04:16:30 +01:00
|
|
|
Status.Visibility.UNLEAKABLE -> R.drawable.ic_low_vision_24dp
|
2019-12-19 19:09:40 +01:00
|
|
|
else -> R.drawable.ic_lock_open_24dp
|
|
|
|
}
|
2021-03-07 19:05:51 +01:00
|
|
|
binding.composeToggleVisibilityButton.setImageResource(iconRes)
|
2022-12-08 10:18:12 +01:00
|
|
|
if (viewModel.editing) {
|
|
|
|
// Can't update visibility on published status
|
|
|
|
enableButton(binding.composeToggleVisibilityButton, clickable = false, colorActive = false)
|
|
|
|
}
|
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)
|
2022-12-06 19:28:44 +01:00
|
|
|
displayTransientMessage(errorMessage)
|
2019-12-19 19:09:40 +01:00
|
|
|
} 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)
|
2022-11-16 20:43:49 +01:00
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && 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
|
2021-04-28 04:54:29 +02:00
|
|
|
if (binding.checkboxUseDefaultText.isChecked) {
|
|
|
|
length += 1 + binding.editTextDefaultText.length()
|
2019-12-27 06:46:18 +01:00
|
|
|
}
|
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 {
|
2022-12-31 13:01:35 +01:00
|
|
|
MaterialColors.getColor(binding.composeCharactersLeftView, android.R.attr.textColorTertiary)
|
2020-10-13 18:30:06 +02:00
|
|
|
}
|
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))
|
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() {
|
2022-12-08 10:18:12 +01:00
|
|
|
enableButtons(false, viewModel.editing)
|
2021-04-28 04:54:29 +02:00
|
|
|
var 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)
|
2022-12-08 10:18:12 +01:00
|
|
|
enableButtons(true, viewModel.editing)
|
2019-12-19 19:09:40 +01:00
|
|
|
} else if (characterCount <= maximumTootCharacters) {
|
2021-04-28 04:54:29 +02:00
|
|
|
if (binding.checkboxUseDefaultText.isChecked) {
|
|
|
|
contentText += " ${binding.editTextDefaultText.text}"
|
2019-12-27 06:46:18 +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
|
|
|
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)
|
2022-12-08 10:18:12 +01:00
|
|
|
enableButtons(true, viewModel.editing)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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) {
|
2022-12-06 19:28:44 +01:00
|
|
|
displayTransientMessage(R.string.error_media_upload_opening)
|
2021-05-22 17:50:08 +02:00
|
|
|
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
|
2022-12-31 13:01:35 +01:00
|
|
|
setDrawableTint(
|
2021-06-28 21:13:24 +02:00
|
|
|
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
|
2022-12-31 13:01:35 +01:00
|
|
|
val textColor = MaterialColors.getColor(
|
|
|
|
binding.addPollTextActionTextView,
|
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-12-06 19:28:44 +01:00
|
|
|
displayTransientMessage(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()
|
2022-12-31 13:01:35 +01:00
|
|
|
MaterialColors.getColor(binding.composeContentWarningButton, android.R.attr.textColorTertiary)
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
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 onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
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) {
|
2022-11-04 19:22:38 +01:00
|
|
|
onBackPressedDispatcher.onBackPressed()
|
2020-04-18 13:45:19 +02:00
|
|
|
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)) {
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
when (viewModel.composeKind) {
|
|
|
|
ComposeKind.NEW -> getSaveAsDraftOrDiscardDialog(contentText, contentWarning)
|
|
|
|
ComposeKind.EDIT_DRAFT -> getUpdateDraftOrDiscardDialog(contentText, contentWarning)
|
|
|
|
ComposeKind.EDIT_POSTED -> getContinueEditingOrDiscardDialog()
|
|
|
|
ComposeKind.EDIT_SCHEDULED -> getContinueEditingOrDiscardDialog()
|
|
|
|
}.show()
|
|
|
|
} else {
|
|
|
|
viewModel.stopUploads()
|
|
|
|
finishWithoutSlideOutAnimation()
|
|
|
|
}
|
|
|
|
}
|
2022-11-09 19:33:48 +01:00
|
|
|
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
/**
|
|
|
|
* User is editing a new post, and can either save the changes as a draft or discard them.
|
|
|
|
*/
|
|
|
|
private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
|
|
|
val warning = if (viewModel.media.value.isNotEmpty()) {
|
|
|
|
R.string.compose_save_draft_loses_media
|
|
|
|
} else {
|
|
|
|
R.string.compose_save_draft
|
|
|
|
}
|
|
|
|
|
|
|
|
return AlertDialog.Builder(this)
|
|
|
|
.setMessage(warning)
|
|
|
|
.setPositiveButton(R.string.action_save) { _, _ ->
|
|
|
|
viewModel.stopUploads()
|
|
|
|
saveDraftAndFinish(contentText, contentWarning)
|
|
|
|
}
|
|
|
|
.setNegativeButton(R.string.action_delete) { _, _ ->
|
|
|
|
viewModel.stopUploads()
|
|
|
|
deleteDraftAndFinish()
|
2022-11-09 19:33:48 +01:00
|
|
|
}
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
}
|
2022-11-09 19:33:48 +01:00
|
|
|
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
/**
|
|
|
|
* User is editing an existing draft, and can either update the draft with the new changes or
|
|
|
|
* discard them.
|
|
|
|
*/
|
|
|
|
private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder {
|
|
|
|
val warning = if (viewModel.media.value.isNotEmpty()) {
|
|
|
|
R.string.compose_save_draft_loses_media
|
2019-12-19 19:09:40 +01:00
|
|
|
} else {
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
R.string.compose_save_draft
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
|
|
|
|
return AlertDialog.Builder(this)
|
|
|
|
.setMessage(warning)
|
|
|
|
.setPositiveButton(R.string.action_save) { _, _ ->
|
|
|
|
viewModel.stopUploads()
|
|
|
|
saveDraftAndFinish(contentText, contentWarning)
|
|
|
|
}
|
|
|
|
.setNegativeButton(R.string.action_discard) { _, _ ->
|
|
|
|
viewModel.stopUploads()
|
|
|
|
finishWithoutSlideOutAnimation()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* User is editing a post (scheduled, or posted), and can either go back to editing, or
|
|
|
|
* discard the changes.
|
|
|
|
*/
|
|
|
|
private fun getContinueEditingOrDiscardDialog(): AlertDialog.Builder {
|
|
|
|
return AlertDialog.Builder(this)
|
|
|
|
.setMessage(R.string.compose_unsaved_changes)
|
|
|
|
.setPositiveButton(R.string.action_continue_edit) { _, _ ->
|
|
|
|
// Do nothing, dialog will dismiss, user can continue editing
|
|
|
|
}
|
|
|
|
.setNegativeButton(R.string.action_discard) { _, _ ->
|
|
|
|
viewModel.stopUploads()
|
|
|
|
finishWithoutSlideOutAnimation()
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2022-12-05 19:15:28 +01:00
|
|
|
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
|
|
|
binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis)
|
2021-03-07 19:05:51 +01:00
|
|
|
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,
|
Add UI for image-attachment "focus" (#2620)
* Attempt-zero implementation of a "focus" feature for image attachments. Choose "Set focus" in the attachment menu, tap once to select focus point (no visual feedback currently), tap "OK". Works in tests.
* Remove code duplication between 'update description' and 'update focus'
* Fix ktlint/bitrise failures
* Make updateMediaItem private
* When focus is set on a post attachment the preview focuses correctly. ProgressImageView now inherits from MediaPreviewImageView.
* Replace use of PointF for Focus where focus is represented, fix ktlint
* Substitute 'focus' for 'focus point' in strings
* First attempt draw focus point. Only updates on initial load. Modeled on code from RoundedCorners builtin from Glide
* Redraw focus after each tap
* Dark curtain where focus isn't (now looks like mastosoc)
* Correct ktlint for FocusDialog
* draft: switch to overlay for focus indicator
* Draw focus circle, but ImageView and FocusIndicatorView seem to share a single canvas
* Switch focus circle to path approach
* Correctly scale, save and load focuses. Clamp to visible area. Focus editor looks and feels right
* ktlint fixes and comments
* Focus indicator drawing should use device-independent pixels
* Shrink focus window when it gets unattractively tall (no linting, misbehaves on wide aspect ratio screens)
* Correct max-height behavior for screens in landscape mode
* Focus attachment result is are flipped on x axis; fix this
* Correctly thread focus through on scheduled posts, redrafted posts, and drafts (but draft focus is lost on post)
* More focus ktlint fixes
* Fix specific case where a draft is given a focus, then deleted, then posted in that order
* Fix accidental file change in focus PR
* ktLint fix
* Fix property style warnings in focus
* Fix remaining style warnings from focus PR
Co-authored-by: Conny Duck <k.pozniak@gmx.at>
2022-09-21 20:28:06 +02:00
|
|
|
val description: String? = null,
|
2022-12-08 10:18:12 +01:00
|
|
|
val focus: Attachment.Focus? = null,
|
2022-12-29 19:58:23 +01:00
|
|
|
val state: State
|
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
|
|
|
}
|
2022-12-29 19:58:23 +01:00
|
|
|
enum class State {
|
|
|
|
UPLOADING, UNPROCESSED, PROCESSED, PUBLISHED
|
|
|
|
}
|
2019-12-19 19:09:40 +01:00
|
|
|
}
|
|
|
|
|
2023-01-13 19:49:56 +01:00
|
|
|
override fun onTimeSet(time: String?) {
|
2021-02-23 20:29:02 +01:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-09-12 18:21:00 +02:00
|
|
|
override fun onUpdateDescription(localId: Int, description: String) {
|
|
|
|
lifecycleScope.launch {
|
|
|
|
if (!viewModel.updateDescription(localId, description)) {
|
|
|
|
Toast.makeText(this@ComposeActivity, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Fix saving changes to statuses when editing (#3103)
* Fix saving changes to statuses when editing
With the previous code backing out of a status editing operation where changes
had been made (whether it was editing an existing status, a scheduled status,
or a draft) would prompt the user to save the changes as a new draft.
See https://github.com/tuskyapp/Tusky/issues/2704 and
https://github.com/tuskyapp/Tusky/issues/2705 for more detail.
The fix:
- Create an enum to represent the four different kinds of edits that can
happen
- Editing a new status (i.e., composing it for the first time)
- Editing a posted status
- Editing a draft
- Editing a scheduled status
- Store this in ComposeOptions, and set it appropriately everywhere
ComposeOptions is created.
- Check the edit kind when backing out of ComposeActivity, and use this to
show one of three different dialogs as appropriate so the user can:
- Save as new draft or discard changes
- Continue editing or discard changes
- Update existing draft or discard changes
Also fix ComposeViewModel.didChange(), which erroneously reported false if the
old text started with the new text (e.g., if the old text was "hello, world"
and it was edited to "hello", didChange() would not consider that to be a
change).
Fixes https://github.com/tuskyapp/Tusky/issues/2704,
https://github.com/tuskyapp/Tusky/issues/2705
* Use orEmpty extension function
2022-12-31 13:04:49 +01:00
|
|
|
/**
|
|
|
|
* Status' kind. This particularly affects how the status is handled if the user
|
|
|
|
* backs out of the edit.
|
|
|
|
*/
|
|
|
|
enum class ComposeKind {
|
|
|
|
/** Status is new */
|
|
|
|
NEW,
|
|
|
|
|
|
|
|
/** Editing a posted status */
|
|
|
|
EDIT_POSTED,
|
|
|
|
|
|
|
|
/** Editing a status started as an existing draft */
|
|
|
|
EDIT_DRAFT,
|
|
|
|
|
|
|
|
/** Editing an an existing scheduled status */
|
|
|
|
EDIT_SCHEDULED
|
|
|
|
}
|
|
|
|
|
2019-12-19 19:09:40 +01:00
|
|
|
@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,
|
2022-04-12 17:59:06 +02:00
|
|
|
var quoteId: String? = null,
|
|
|
|
var quoteStatusAuthor: String? = null,
|
|
|
|
var quoteStatusContent: String? = null,
|
2021-05-22 17:50:08 +02:00
|
|
|
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-04-12 17:59:06 +02:00
|
|
|
var modifiedInitialState: Boolean? = null,
|
2022-08-31 18:53:57 +02:00
|
|
|
var language: String? = null,
|
2022-12-08 10:18:12 +01:00
|
|
|
var statusId: String? = null,
|
2023-01-25 00:46:02 +01:00
|
|
|
var kind: ComposeKind? = null,
|
2022-04-12 17:59:06 +02:00
|
|
|
var tootRightNow: Boolean? = 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"
|
2022-10-18 19:38:27 +02:00
|
|
|
private const val VISIBILITY_KEY = "VISIBILITY"
|
|
|
|
private const val SCHEDULED_TIME_KEY = "SCHEDULE"
|
|
|
|
private const val CONTENT_WARNING_VISIBLE_KEY = "CONTENT_WARNING_VISIBLE"
|
2019-12-19 19:09:40 +01:00
|
|
|
|
2019-12-27 06:46:18 +01:00
|
|
|
@JvmField
|
2020-05-16 09:02:01 +02:00
|
|
|
val CAN_USE_UNLEAKABLE = arrayOf("itabashi.0j0.jp", "odakyu.app")
|
2019-12-27 06:46:18 +01:00
|
|
|
|
|
|
|
const val PREF_DEFAULT_TAG = "default_tag"
|
|
|
|
const val PREF_USE_DEFAULT_TAG = "use_default_tag"
|
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|