Stickers: add PleromaFE stickers support, enabled in settings
This commit is contained in:
parent
6417f31767
commit
d705a85690
|
@ -0,0 +1,117 @@
|
|||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator.TabConfigurationStrategy
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.StickerPack
|
||||
import com.keylesspalace.tusky.view.EmojiKeyboard
|
||||
import com.keylesspalace.tusky.view.EmojiKeyboard.EmojiKeyboardAdapter
|
||||
import java.util.*
|
||||
|
||||
class StickerAdapter(
|
||||
private val stickerPacks: Array<StickerPack>,
|
||||
private val listener: EmojiKeyboard.OnEmojiSelectedListener
|
||||
) : RecyclerView.Adapter<SingleViewHolder>(), TabConfigurationStrategy, EmojiKeyboardAdapter {
|
||||
|
||||
private val recentsAdapter = StickerPageAdapter(null, listener, emptyList())
|
||||
// this value doesn't reflect actual button width but how much we want for button to take space
|
||||
// this is bad, only villains do that
|
||||
private val BUTTON_WIDTH_DP = 90.0f
|
||||
|
||||
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
|
||||
if (position == 0) {
|
||||
tab.setIcon(R.drawable.ic_access_time)
|
||||
return
|
||||
}
|
||||
|
||||
val pack = stickerPacks[position - 1]
|
||||
val imageView = ImageView(tab.view.context)
|
||||
imageView.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
Glide.with(imageView)
|
||||
.asDrawable()
|
||||
.load(pack.internal_url + pack.tabIcon)
|
||||
.thumbnail()
|
||||
.centerCrop()
|
||||
.into( object: CustomTarget<Drawable>() {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||
// tab.icon = resource
|
||||
imageView.setImageDrawable(resource)
|
||||
tab.customView = imageView
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_emoji_keyboard_page, parent, false)
|
||||
val holder = SingleViewHolder(view)
|
||||
|
||||
val dm = parent.context.resources.displayMetrics
|
||||
val wdp = dm.widthPixels / dm.density
|
||||
val rows = (wdp / BUTTON_WIDTH_DP + 0.5).toInt()
|
||||
|
||||
(view as RecyclerView).layoutManager = GridLayoutManager(view.getContext(), rows)
|
||||
return holder
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return stickerPacks.size + 1
|
||||
}
|
||||
|
||||
override fun onRecentsUpdate(set: MutableSet<String>) {
|
||||
val list = set.toMutableList()
|
||||
list.reverse()
|
||||
recentsAdapter.stickers = list
|
||||
recentsAdapter.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SingleViewHolder, position: Int) {
|
||||
if( position == 0 ) {
|
||||
(holder.itemView as RecyclerView).adapter = recentsAdapter
|
||||
} else {
|
||||
val pack = stickerPacks[position - 1]
|
||||
(holder.itemView as RecyclerView).adapter = StickerPageAdapter(pack.internal_url, listener, pack.stickers)
|
||||
}
|
||||
}
|
||||
|
||||
private class StickerPageAdapter(
|
||||
private val url: String?,
|
||||
var listener: EmojiKeyboard.OnEmojiSelectedListener,
|
||||
var stickers: List<String>
|
||||
) : RecyclerView.Adapter<SingleViewHolder>() {
|
||||
override fun getItemCount(): Int {
|
||||
return stickers.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SingleViewHolder, position: Int) {
|
||||
(holder.itemView as AppCompatImageButton).setOnClickListener {
|
||||
listener.onEmojiSelected("", ( url ?: "" ) + stickers[position])
|
||||
}
|
||||
Glide.with(holder.itemView)
|
||||
.load(( url ?: "" ) + stickers[position])
|
||||
.thumbnail()
|
||||
.into(holder.itemView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SingleViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_emoji_keyboard_sticker, parent, false)
|
||||
return SingleViewHolder(view)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,7 @@ import android.content.pm.PackageManager
|
|||
import android.content.ContentResolver
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
|
@ -47,6 +48,7 @@ import androidx.appcompat.app.AlertDialog
|
|||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||
import androidx.core.view.isGone
|
||||
|
@ -56,6 +58,10 @@ import androidx.preference.PreferenceManager
|
|||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.TransitionManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.target.CustomTarget
|
||||
import com.bumptech.glide.request.target.SimpleTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
|
@ -75,6 +81,7 @@ import com.keylesspalace.tusky.entity.Emoji
|
|||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.keylesspalace.tusky.view.EmojiKeyboard
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||
import com.mikepenz.iconics.utils.colorInt
|
||||
|
@ -96,7 +103,8 @@ class ComposeActivity : BaseActivity(),
|
|||
OnEmojiSelectedListener,
|
||||
Injectable,
|
||||
InputConnectionCompat.OnCommitContentListener,
|
||||
TimePickerDialog.OnTimeSetListener {
|
||||
TimePickerDialog.OnTimeSetListener,
|
||||
EmojiKeyboard.OnEmojiSelectedListener {
|
||||
|
||||
@Inject
|
||||
lateinit var viewModelFactory: ViewModelFactory
|
||||
|
@ -105,6 +113,7 @@ class ComposeActivity : BaseActivity(),
|
|||
private lateinit var addMediaBehavior: BottomSheetBehavior<*>
|
||||
private lateinit var emojiBehavior: BottomSheetBehavior<*>
|
||||
private lateinit var scheduleBehavior: BottomSheetBehavior<*>
|
||||
private lateinit var stickerBehavior: BottomSheetBehavior<*>
|
||||
|
||||
// this only exists when a status is trying to be sent, but uploads are still occurring
|
||||
private var finishingUploadDialog: ProgressDialog? = null
|
||||
|
@ -131,6 +140,7 @@ class ComposeActivity : BaseActivity(),
|
|||
// do not do anything when not logged in, activity will be finished in super.onCreate() anyway
|
||||
val activeAccount = accountManager.activeAccount ?: return
|
||||
|
||||
viewModel.tryFetchStickers = preferences.getBoolean("stickers", false)
|
||||
setupAvatar(preferences, activeAccount)
|
||||
val mediaAdapter = MediaPreviewAdapter(
|
||||
this,
|
||||
|
@ -179,13 +189,15 @@ class ComposeActivity : BaseActivity(),
|
|||
setupPollView()
|
||||
applyShareIntent(intent, savedInstanceState)
|
||||
viewModel.setupComplete.value = true
|
||||
|
||||
stickerKeyboard.isSticky = true
|
||||
}
|
||||
|
||||
private fun uriToFilename(uri: Uri): String {
|
||||
var result: String = "unknown"
|
||||
if(uri.scheme.equals("content")) {
|
||||
val cursor = contentResolver.query(uri, null, null, null, null)
|
||||
if(cursor != null) {
|
||||
cursor?.let {
|
||||
try {
|
||||
if(cursor.moveToFirst()) {
|
||||
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
|
||||
|
@ -193,12 +205,12 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
finally {
|
||||
cursor.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(result.equals("unknown")) {
|
||||
val path = uri.getPath()
|
||||
if(path != null) {
|
||||
path?.let {
|
||||
result = path
|
||||
val cut = result.lastIndexOf('/')
|
||||
if (cut != -1) {
|
||||
|
@ -332,7 +344,7 @@ class ComposeActivity : BaseActivity(),
|
|||
// in case of we already had disabled attachments
|
||||
// but got information about extension later
|
||||
enableButton(composeAddMediaButton, true, true)
|
||||
enablePollButton(viewModel.poll != null)
|
||||
enablePollButton(viewModel.poll.value != null)
|
||||
}
|
||||
|
||||
private var supportedFormattingSyntax = arrayListOf<String>()
|
||||
|
@ -375,6 +387,21 @@ class ComposeActivity : BaseActivity(),
|
|||
reenableAttachments()
|
||||
}
|
||||
}
|
||||
viewModel.haveStickers.observe { haveStickers ->
|
||||
if (haveStickers) {
|
||||
composeStickerButton.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
viewModel.instanceStickers.observe { stickers ->
|
||||
/*for(sticker in stickers)
|
||||
Log.d(TAG, "Found sticker pack: %s from %s".format(sticker.title, sticker.internal_url))*/
|
||||
|
||||
if(stickers.isNotEmpty()) {
|
||||
composeStickerButton.visibility = View.VISIBLE
|
||||
enableButton(composeStickerButton, true, true)
|
||||
stickerKeyboard.setupStickerKeyboard(this@ComposeActivity, stickers)
|
||||
}
|
||||
}
|
||||
viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
|
||||
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
|
||||
updateSensitiveMediaToggle(markSensitive, showContentWarning)
|
||||
|
@ -428,9 +455,11 @@ class ComposeActivity : BaseActivity(),
|
|||
addMediaBehavior = BottomSheetBehavior.from(addMediaBottomSheet)
|
||||
scheduleBehavior = BottomSheetBehavior.from(composeScheduleView)
|
||||
emojiBehavior = BottomSheetBehavior.from(emojiView)
|
||||
stickerBehavior = BottomSheetBehavior.from(stickerKeyboard)
|
||||
|
||||
emojiView.layoutManager = GridLayoutManager(this, 3, GridLayoutManager.HORIZONTAL, false)
|
||||
enableButton(composeEmojiButton, clickable = false, colorActive = false)
|
||||
enableButton(composeStickerButton, false, false)
|
||||
|
||||
// Setup the interface buttons.
|
||||
composeTootButton.setOnClickListener { onSendClicked() }
|
||||
|
@ -443,6 +472,7 @@ class ComposeActivity : BaseActivity(),
|
|||
composeScheduleView.setResetOnClickListener { resetSchedule() }
|
||||
composeFormattingSyntax.setOnClickListener { toggleFormattingMode() }
|
||||
composeFormattingSyntax.setOnLongClickListener { selectFormattingSyntax() }
|
||||
composeStickerButton.setOnClickListener { showStickers() }
|
||||
atButton.setOnClickListener { atButtonClicked() }
|
||||
hashButton.setOnClickListener { hashButtonClicked() }
|
||||
codeButton.setOnClickListener { codeButtonClicked() }
|
||||
|
@ -742,6 +772,7 @@ class ComposeActivity : BaseActivity(),
|
|||
composeScheduleButton.isClickable = enable
|
||||
composeFormattingSyntax.isClickable = enable
|
||||
composeTootButton.isEnabled = enable
|
||||
composeStickerButton.isEnabled = enable
|
||||
}
|
||||
|
||||
private fun setStatusVisibility(visibility: Status.Visibility) {
|
||||
|
@ -764,9 +795,10 @@ class ComposeActivity : BaseActivity(),
|
|||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
} else {
|
||||
composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -783,9 +815,10 @@ class ComposeActivity : BaseActivity(),
|
|||
scheduleBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
} else {
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -797,11 +830,12 @@ class ComposeActivity : BaseActivity(),
|
|||
} else {
|
||||
if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
} else {
|
||||
emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -812,9 +846,10 @@ class ComposeActivity : BaseActivity(),
|
|||
addMediaBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
} else {
|
||||
addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN)
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1053,9 +1088,9 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null, filename: String? = null) {
|
||||
withLifecycleContext {
|
||||
viewModel.pickMedia(uri, uriToFilename(uri)).observe { exceptionOrItem ->
|
||||
viewModel.pickMedia(uri, filename ?: uriToFilename(uri)).observe { exceptionOrItem ->
|
||||
|
||||
contentInfoCompat?.releasePermission()
|
||||
|
||||
|
@ -1074,6 +1109,7 @@ class ComposeActivity : BaseActivity(),
|
|||
R.string.error_media_upload_image_or_video
|
||||
}
|
||||
else -> {
|
||||
Log.d(TAG, "That file could not be opened", it)
|
||||
R.string.error_media_upload_opening
|
||||
}
|
||||
}
|
||||
|
@ -1114,11 +1150,13 @@ class ComposeActivity : BaseActivity(),
|
|||
if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||
stickerBehavior.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -1185,6 +1223,35 @@ class ComposeActivity : BaseActivity(),
|
|||
}
|
||||
}
|
||||
|
||||
private fun showStickers() {
|
||||
if (stickerBehavior.state == BottomSheetBehavior.STATE_HIDDEN || stickerBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
} else {
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEmojiSelected(id: String, shortcode: String) {
|
||||
// pickMedia(Uri.parse(shortcode))
|
||||
|
||||
Glide.with(this).asFile().load(shortcode).into( object : CustomTarget<File>() {
|
||||
override fun onLoadCleared(placeholder: Drawable?) {
|
||||
displayTransientError(R.string.error_sticker_fetch)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: File, transition: Transition<in File>?) {
|
||||
val cut = shortcode.lastIndexOf('/')
|
||||
val filename = if(cut != -1) shortcode.substring(cut + 1) else "unknown.png"
|
||||
pickMedia(resource.toUri(), null, filename)
|
||||
}
|
||||
})
|
||||
stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
|
||||
data class QueuedMedia(
|
||||
val localId: Long,
|
||||
val uri: Uri,
|
||||
|
|
|
@ -33,8 +33,11 @@ import com.keylesspalace.tusky.network.MastodonApi
|
|||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.TootToSend
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.rxkotlin.Singles
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -64,6 +67,9 @@ class ComposeViewModel
|
|||
private var contentWarningStateChanged: Boolean = false
|
||||
private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null)
|
||||
private val nodeinfo: MutableLiveData<NodeInfo?> = MutableLiveData(null)
|
||||
private val stickers: MutableLiveData<Array<StickerPack>> = MutableLiveData(emptyArray())
|
||||
public val haveStickers: MutableLiveData<Boolean> = MutableLiveData(false)
|
||||
public var tryFetchStickers = false
|
||||
public var formattingSyntax: String = ""
|
||||
public var hasNoAttachmentLimits = false
|
||||
|
||||
|
@ -108,6 +114,8 @@ class ComposeViewModel
|
|||
)
|
||||
}
|
||||
}
|
||||
val instanceStickers: LiveData<Array<StickerPack>> = stickers // .map { stickers -> HashMap<String,String>(stickers) }
|
||||
|
||||
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
|
||||
val markMediaAsSensitive =
|
||||
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||
|
@ -129,7 +137,6 @@ class ComposeViewModel
|
|||
|
||||
|
||||
init {
|
||||
|
||||
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
|
||||
InstanceEntity(
|
||||
instance = accountManager.activeAccount?.domain!!,
|
||||
|
@ -154,21 +161,19 @@ class ComposeViewModel
|
|||
Log.w(TAG, "error loading instance data", throwable)
|
||||
})
|
||||
.autoDispose()
|
||||
|
||||
|
||||
api.getNodeinfoLinks().subscribe({ links ->
|
||||
if(links.links.size > 0) {
|
||||
api.getNodeinfo(links.links[0].href).subscribe({ni ->
|
||||
nodeinfo.postValue(ni)
|
||||
}, {
|
||||
err -> Log.d(TAG, "Failed to get nodeinfo", err)
|
||||
}
|
||||
)
|
||||
}
|
||||
}, {
|
||||
err -> Log.d(TAG, "Failed to get nodeinfo links", err)
|
||||
|
||||
|
||||
api.getNodeinfoLinks().subscribe({
|
||||
links -> if(links.links.isNotEmpty()) {
|
||||
api.getNodeinfo(links.links[0].href).subscribe({
|
||||
ni -> nodeinfo.postValue(ni)
|
||||
}, {
|
||||
err -> Log.d(TAG, "Failed to get nodeinfo", err)
|
||||
}).autoDispose()
|
||||
}
|
||||
)
|
||||
}, { err ->
|
||||
Log.d(TAG, "Failed to get nodeinfo links", err)
|
||||
}).autoDispose()
|
||||
}
|
||||
|
||||
fun pickMedia(uri: Uri, filename: String?): LiveData<Either<Throwable, QueuedMedia>> {
|
||||
|
@ -178,7 +183,7 @@ class ComposeViewModel
|
|||
val imageLimit = instanceMetadata.value?.videoLimit ?: STATUS_VIDEO_SIZE_LIMIT
|
||||
val videoLimit = instanceMetadata.value?.imageLimit ?: STATUS_IMAGE_SIZE_LIMIT
|
||||
|
||||
mediaUploader.prepareMedia(uri, videoLimit, imageLimit)
|
||||
mediaUploader.prepareMedia(uri, videoLimit, imageLimit, filename)
|
||||
.map { (type, uri, size) ->
|
||||
val mediaItems = media.value!!
|
||||
if (!hasNoAttachmentLimits
|
||||
|
@ -187,7 +192,7 @@ class ComposeViewModel
|
|||
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
||||
throw VideoOrImageException()
|
||||
} else {
|
||||
addMediaToQueue(type, uri, size, if(filename != null) filename else "unknown")
|
||||
addMediaToQueue(type, uri, size, filename ?: "unknown")
|
||||
}
|
||||
}
|
||||
.subscribe({ queuedMedia ->
|
||||
|
@ -421,7 +426,41 @@ class ComposeViewModel
|
|||
super.onCleared()
|
||||
}
|
||||
|
||||
fun getStickers() {
|
||||
if(!tryFetchStickers)
|
||||
return
|
||||
|
||||
api.getStickers().subscribe({ stickers ->
|
||||
if (stickers.isNotEmpty()) {
|
||||
haveStickers.postValue(true)
|
||||
|
||||
val singles = mutableListOf<Single<Response<StickerPack>>>()
|
||||
for(entry in stickers) {
|
||||
val url = entry.value.removePrefix("/").removeSuffix("/") + "/pack.json";
|
||||
singles += api.getStickerPack(url)
|
||||
}
|
||||
|
||||
Single.zip(singles) {
|
||||
it.map {
|
||||
it as Response<StickerPack>
|
||||
it.body()!!.internal_url = it.raw().request.url.toString().removeSuffix("pack.json")
|
||||
it.body()!!
|
||||
}
|
||||
}.onErrorReturn {
|
||||
Log.d(TAG, "Failed to get sticker pack.json", it)
|
||||
emptyList()
|
||||
}.subscribe() { pack ->
|
||||
if(pack.isNotEmpty())
|
||||
this.stickers.postValue(pack.toTypedArray())
|
||||
}.autoDispose()
|
||||
}
|
||||
}, {
|
||||
err -> Log.d(TAG, "Failed to get sticker.json", err)
|
||||
}).autoDispose()
|
||||
}
|
||||
|
||||
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
||||
getStickers() // early as possible
|
||||
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
|
||||
|
||||
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||
|
@ -458,7 +497,6 @@ class ComposeViewModel
|
|||
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
|
||||
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
||||
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
|
||||
else -> QueuedMedia.Type.IMAGE
|
||||
}
|
||||
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
||||
}
|
||||
|
@ -496,8 +534,7 @@ class ComposeViewModel
|
|||
replyingStatusContent = composeOptions?.replyingStatusContent
|
||||
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
||||
|
||||
if(composeOptions?.formattingSyntax != null)
|
||||
formattingSyntax = composeOptions?.formattingSyntax ?: accountManager.activeAccount!!.defaultFormattingSyntax
|
||||
formattingSyntax = composeOptions?.formattingSyntax ?: accountManager.activeAccount!!.defaultFormattingSyntax
|
||||
}
|
||||
|
||||
fun updatePoll(newPoll: NewPoll) {
|
||||
|
|
|
@ -59,7 +59,7 @@ fun createNewImageFile(context: Context): File {
|
|||
data class PreparedMedia(val type: Int, val uri: Uri, val size: Long)
|
||||
|
||||
interface MediaUploader {
|
||||
fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int): Single<PreparedMedia>
|
||||
fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int, filename: String?): Single<PreparedMedia>
|
||||
fun uploadMedia(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable<UploadEvent>
|
||||
}
|
||||
|
||||
|
@ -85,13 +85,21 @@ class MediaUploaderImpl(
|
|||
.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
override fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int): Single<PreparedMedia> {
|
||||
private fun getMimeTypeAndSuffixFromFilenameOrUri(uri: Uri, filename: String?) : Pair<String?, String> {
|
||||
val mimeType = contentResolver.getType(uri)
|
||||
return if(mimeType == null && filename != null) {
|
||||
val extension = filename.substringAfterLast('.', "tmp")
|
||||
Pair(MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension), ".$extension")
|
||||
} else {
|
||||
Pair(mimeType, "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp"))
|
||||
}
|
||||
}
|
||||
|
||||
override fun prepareMedia(inUri: Uri, videoLimit: Int, imageLimit: Int, filename: String?): Single<PreparedMedia> {
|
||||
return Single.fromCallable {
|
||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
||||
var uri = inUri
|
||||
val mimeType = contentResolver.getType(uri)
|
||||
|
||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||
val (mimeType, suffix) = getMimeTypeAndSuffixFromFilenameOrUri(uri, filename)
|
||||
|
||||
try {
|
||||
contentResolver.openInputStream(inUri).use { input ->
|
||||
|
@ -154,10 +162,8 @@ class MediaUploaderImpl(
|
|||
|
||||
private fun upload(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable<UploadEvent> {
|
||||
return Observable.create { emitter ->
|
||||
var mimeType = contentResolver.getType(media.uri)
|
||||
val map = MimeTypeMap.getSingleton()
|
||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||
val filename = String.format("%s_%s_%s.%s",
|
||||
var (mimeType, fileExtension) = getMimeTypeAndSuffixFromFilenameOrUri(media.uri, media.originalFileName)
|
||||
val filename = String.format("%s_%s_%s%s",
|
||||
context.getString(R.string.app_name),
|
||||
Date().time.toString(),
|
||||
randomAlphanumericString(10),
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
/* Copyright 2018 Conny Duck
|
||||
*
|
||||
* 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.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class StickerPack(
|
||||
val title: String,
|
||||
val tabIcon: String,
|
||||
val stickers: List<String>,
|
||||
var internal_url: String = ""
|
||||
) : Parcelable
|
|
@ -610,4 +610,15 @@ interface MastodonApi {
|
|||
@Path("id") statusId: String,
|
||||
@Path("emoji") emoji: String
|
||||
): Single<Response<List<EmojiReaction>>>
|
||||
|
||||
// NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS
|
||||
// just for testing and because puniko asked me
|
||||
@GET("static/stickers.json")
|
||||
fun getStickers() : Single<Map<String, String>>
|
||||
|
||||
@GET
|
||||
fun getStickerPack(
|
||||
@Url path: String
|
||||
): Single<Response<StickerPack>>
|
||||
// NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS NOT AN API CALLS
|
||||
}
|
||||
|
|
|
@ -8,11 +8,16 @@ import android.app.*;
|
|||
import android.text.*;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.adapter.StickerAdapter;
|
||||
import com.keylesspalace.tusky.adapter.UnicodeEmojiAdapter;
|
||||
import com.keylesspalace.tusky.entity.StickerPack;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class EmojiKeyboard extends LinearLayout {
|
||||
|
@ -22,10 +27,10 @@ public class EmojiKeyboard extends LinearLayout {
|
|||
private String preferenceKey;
|
||||
private SharedPreferences pref;
|
||||
private Set<String> recents;
|
||||
private boolean isSticky = false; // TODO
|
||||
private String RECENTS_DELIM = "; ";
|
||||
private int MAX_RECENTS_ITEMS = 50;
|
||||
private RecyclerView.Adapter adapter;
|
||||
public boolean isSticky = false; // TODO
|
||||
|
||||
public EmojiKeyboard(Context context) {
|
||||
super(context);
|
||||
|
@ -53,36 +58,50 @@ public class EmojiKeyboard extends LinearLayout {
|
|||
public static final int UNICODE_MODE = 0;
|
||||
public static final int CUSTOM_MODE = 1;
|
||||
public static final int STICKER_MODE = 2;
|
||||
|
||||
void setupKeyboard(String id, int mode, OnEmojiSelectedListener listener) {
|
||||
switch(mode) {
|
||||
case CUSTOM_MODE:
|
||||
preferenceKey = "CUSTOM_RECENTS";
|
||||
break;
|
||||
case STICKER_MODE:
|
||||
preferenceKey = "STICKER_RECENTS";
|
||||
break;
|
||||
default:
|
||||
case UNICODE_MODE:
|
||||
preferenceKey = "UNICODE_RECENTS";
|
||||
adapter = new UnicodeEmojiAdapter(id, listener);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
private void setupKeyboardWithAdapter(RecyclerView.Adapter adapter, String preferenceKey) {
|
||||
this.preferenceKey = preferenceKey;
|
||||
this.adapter = adapter;
|
||||
|
||||
List<String> list = Arrays.asList(pref.getString(preferenceKey, "").split(RECENTS_DELIM));
|
||||
recents = new LinkedHashSet<String>(list);
|
||||
((EmojiKeyboardAdapter)adapter).onRecentsUpdate(recents);
|
||||
|
||||
|
||||
pager.setAdapter(adapter);
|
||||
|
||||
|
||||
if(currentMediator != null)
|
||||
currentMediator.detach();
|
||||
|
||||
|
||||
currentMediator = new TabLayoutMediator(tabs, pager, (TabLayoutMediator.TabConfigurationStrategy)adapter);
|
||||
currentMediator.attach();
|
||||
}
|
||||
|
||||
public void setupStickerKeyboard(OnEmojiSelectedListener listener, StickerPack packs[]) {
|
||||
MAX_RECENTS_ITEMS = 20;
|
||||
setupKeyboardWithAdapter(new StickerAdapter(packs, (_id, _emoji) -> {
|
||||
this.appendToRecents(_emoji);
|
||||
listener.onEmojiSelected(_id, _emoji);
|
||||
}), "STICKER_RECENTS");
|
||||
}
|
||||
|
||||
public void setupKeyboard(String id, int mode, OnEmojiSelectedListener listener) {
|
||||
switch(mode) {
|
||||
// WOOOPS, I forgot that I need to pass data to adapter
|
||||
// For stickers, use SetupStickerKeyboard instead
|
||||
// For custom emoji, use TODO
|
||||
case CUSTOM_MODE:
|
||||
case STICKER_MODE:
|
||||
throw new IllegalArgumentException();
|
||||
default:
|
||||
case UNICODE_MODE:
|
||||
setupKeyboardWithAdapter(new UnicodeEmojiAdapter(id, (_id, _emoji) -> {
|
||||
this.appendToRecents(_emoji);
|
||||
listener.onEmojiSelected(_id, _emoji);
|
||||
}), "UNICODE_RECENTS");
|
||||
}
|
||||
}
|
||||
|
||||
void appendToRecents(String id) {
|
||||
private void appendToRecents(String id) {
|
||||
recents.remove(id);
|
||||
recents.add(id);
|
||||
int size = recents.size();
|
||||
|
@ -109,11 +128,11 @@ public class EmojiKeyboard extends LinearLayout {
|
|||
}
|
||||
|
||||
public interface OnEmojiSelectedListener {
|
||||
void onEmojiSelected(String id, String emoji);
|
||||
void onEmojiSelected(@NonNull String id, @NonNull String emoji);
|
||||
}
|
||||
|
||||
public interface EmojiKeyboardAdapter {
|
||||
void onRecentsUpdate(Set<String> set);
|
||||
void onRecentsUpdate(@NonNull Set<String> set);
|
||||
}
|
||||
|
||||
public static void show(Context ctx, String id, int mode, OnEmojiSelectedListener listener) {
|
||||
|
@ -125,7 +144,6 @@ public class EmojiKeyboard extends LinearLayout {
|
|||
EmojiKeyboard kbd = (EmojiKeyboard)dialog.findViewById(R.id.dialog_emoji_keyboard);
|
||||
kbd.setupKeyboard(id, mode, (_id, _emoji) -> {
|
||||
listener.onEmojiSelected(_id, _emoji);
|
||||
kbd.appendToRecents(_emoji);
|
||||
if(!kbd.isSticky)
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="m17.6395,2.4501c-0.1153,-0.0029 -0.237,0.0011 -0.3594,0.0098L6.5086,2.4599C4.1934,2.3957 2.2618,4.5 2.4696,6.7919l-0.0019,-0.0566c0.0029,3.6483 -0.0044,7.2998 0.0059,10.9512a0.6173,0.6173 0,0 0,0 0.0137c0.0585,2.2386 2.115,4.0322 4.332,3.8359L6.7508,21.538h5.1055,0.25a0.6173,0.6173 0,0 0,0.4375 -0.1816l0.25,-0.252 8.2891,-8.3203L21.3661,12.499a0.6173,0.6173 0,0 0,0.1797 -0.4355v-0.2148c0.0001,-1.8499 -0,-3.7001 -0.0039,-5.5508a0.6173,0.6173 0,0 0,0 -0.0137C21.4874,4.1928 19.6863,2.5008 17.6395,2.4501ZM12.0965,4.6923c1.8296,-0.0016 3.6576,0.0006 5.4844,0.0156 1.0144,0.0827 1.8488,1.1605 1.7344,2.1738a0.6173,0.6173 0,0 0,-0.0039 0.0684v4.2813h-5.582c-0.3422,0 -0.6713,0.0707 -0.9688,0.1973 -0.2971,0.1265 -0.5637,0.3098 -0.7891,0.5352 -0.2254,0.2254 -0.4087,0.4919 -0.5352,0.7891 -0.1266,0.2975 -0.1973,0.6266 -0.1973,0.9688v5.5801C9.7736,19.3008 8.3092,19.2993 6.8446,19.29 5.6148,19.2409 4.5685,17.9653 4.6981,16.7431a0.6173,0.6173 0,0 0,0.0039 -0.0625c0.0084,-3.4464 -0.0156,-6.8869 0.0137,-10.3223 0.0799,-0.8927 0.9478,-1.6796 1.8438,-1.6641a0.6173,0.6173 0,0 0,0.0078 0c1.8438,0.0043 3.6879,-0.0004 5.5293,-0.0019zM14.1629,13.4658h1.1934,1.8906l-3.7734,3.7891v-1.9063,-1.1934c0,-0.094 0.0201,-0.1843 0.0547,-0.2656 0.0341,-0.0801 0.0831,-0.1554 0.1484,-0.2207 0.0654,-0.0654 0.1406,-0.1144 0.2207,-0.1484 0.0813,-0.0346 0.1716,-0.0547 0.2656,-0.0547z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="square"/>
|
||||
</vector>
|
|
@ -295,6 +295,17 @@
|
|||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||
|
||||
<com.keylesspalace.tusky.view.EmojiKeyboard
|
||||
android:id="@+id/stickerKeyboard"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="300dp"
|
||||
android:background="?attr/colorSurface"
|
||||
android:elevation="12dp"
|
||||
android:layout_marginBottom="56dp"
|
||||
app:behavior_hideable="true"
|
||||
app:behavior_peekHeight="0dp"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -397,6 +408,18 @@
|
|||
android:tooltipText="@string/action_markdown"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_markdown" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/composeStickerButton"
|
||||
style="@style/TuskyImageButton"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:contentDescription="@string/action_sticker"
|
||||
android:padding="4dp"
|
||||
android:tooltipText="@string/action_sticker"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_sticker" />
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginRight="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:layout_marginLeft="0dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:background="@null"
|
||||
android:minWidth="0dp"
|
||||
/>
|
|
@ -9,7 +9,7 @@
|
|||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/picker_tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabMinWidth="0dp"
|
||||
app:tabGravity="fill"
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<string name="action_emoji_reacted_by">Who reacted</string>
|
||||
<string name="action_enable_formatting_syntax">Enable %s</string>
|
||||
<string name="action_disable_formatting_syntax">Disable %s</string>
|
||||
<string name="action_sticker">Stickers</string>
|
||||
|
||||
<string name="title_emoji_reacted_by">%s reacted by</string>
|
||||
|
||||
|
@ -18,6 +19,7 @@
|
|||
<string name="moderator">Moderator</string>
|
||||
|
||||
<string name="error_media_upload_size">File size exceeds instance limits</string>
|
||||
<string name="error_sticker_fetch">An error occurred while fetching sticker</string>
|
||||
|
||||
<string name="notification_emoji_format">%s reacted with %s to your post</string>
|
||||
<string name="notification_emoji_name">Emoji Reactions</string>
|
||||
|
@ -27,5 +29,6 @@
|
|||
<string name="pref_title_notification_filter_emoji">my posts are reacted with emojis</string>
|
||||
<string name="pref_title_hide_muted_users">Hide muted users</string>
|
||||
<string name="pref_title_enable_big_emojis">Enable bigger custom emojis</string>
|
||||
<string name="pref_title_enable_experimental_stickers">Enable experimental Pleroma-FE stickers(if available)</string>
|
||||
</resources>
|
||||
|
||||
|
|
|
@ -102,6 +102,12 @@
|
|||
android:title="@string/pref_title_enable_big_emojis"
|
||||
app:singleLineTitle="false" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="stickers"
|
||||
android:title="@string/pref_title_enable_experimental_stickers"
|
||||
app:singleLineTitle="false" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_title_browser_settings">
|
||||
|
|
Loading…
Reference in New Issue