Stickers: add PleromaFE stickers support, enabled in settings

main
Alibek Omarov 5 years ago
parent 6417f31767
commit d705a85690
  1. 117
      app/src/main/java/com/keylesspalace/tusky/adapter/StickerAdapater.kt
  2. 99
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
  3. 77
      app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
  4. 24
      app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
  5. 27
      app/src/main/java/com/keylesspalace/tusky/entity/Sticker.kt
  6. 11
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
  7. 66
      app/src/main/java/com/keylesspalace/tusky/view/EmojiKeyboard.java
  8. 16
      app/src/main/res/drawable/ic_sticker.xml
  9. 23
      app/src/main/res/layout/activity_compose.xml
  10. 12
      app/src/main/res/layout/item_emoji_keyboard_sticker.xml
  11. 2
      app/src/main/res/layout/item_emoji_picker.xml
  12. 3
      app/src/main/res/values/husky.xml
  13. 6
      app/src/main/res/xml/preferences.xml

@ -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…
Cancel
Save