From 81abedb89b564a71a386f33b51bbc55eaae9512e Mon Sep 17 00:00:00 2001 From: Adolfo Santiago Date: Sat, 21 May 2022 11:29:36 +0200 Subject: [PATCH] Fix animated emojis in composable views --- .../tusky/adapter/EmojiAdapter.kt | 32 +- .../announcements/AnnouncementsActivity.kt | 9 +- .../tusky/components/chat/ChatActivity.kt | 676 +++++++++++------- .../components/compose/ComposeActivity.kt | 10 +- 4 files changed, 451 insertions(+), 276 deletions(-) diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt index 70a6163..d5b7833 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -15,38 +15,48 @@ package com.keylesspalace.tusky.adapter -import androidx.recyclerview.widget.RecyclerView import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageView +import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide +import com.github.penfeizhou.animation.glide.AnimationDecoderOption import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Emoji -import java.util.* -class EmojiAdapter(emojiList: List, private val onEmojiSelectedListener: OnEmojiSelectedListener) : RecyclerView.Adapter() { - private val emojiList : List +class EmojiAdapter( + emojiList: List, + private val onEmojiSelectedListener: OnEmojiSelectedListener, + private val animateEmojis: Boolean +) : RecyclerView.Adapter() { + + private val emojis: List init { - this.emojiList = emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } - .sortedBy { it.shortcode.toLowerCase(Locale.ROOT) } + this.emojis = + emojiList.filter { emoji -> emoji.visibleInPicker == null || emoji.visibleInPicker } + .sortedBy { it.shortcode.lowercase() } } override fun getItemCount(): Int { - return emojiList.size + return emojis.size } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EmojiHolder { - val view = LayoutInflater.from(parent.context).inflate(R.layout.item_emoji_button, parent, false) as ImageView + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_emoji_button, parent, false) as ImageView return EmojiHolder(view) } override fun onBindViewHolder(viewHolder: EmojiHolder, position: Int) { - val emoji = emojiList[position] + val emoji = emojis[position] Glide.with(viewHolder.emojiImageView) - .load(emoji.url) - .into(viewHolder.emojiImageView) + .load(emoji.url) + .set(AnimationDecoderOption.DISABLE_ANIMATION_GIF_DECODER, !animateEmojis) + .set(AnimationDecoderOption.DISABLE_ANIMATION_WEBP_DECODER, !animateEmojis) + .set(AnimationDecoderOption.DISABLE_ANIMATION_APNG_DECODER, !animateEmojis) + .into(viewHolder.emojiImageView) viewHolder.emojiImageView.setOnClickListener { onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/husky/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index d1c269c..0f78dc1 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -55,6 +55,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } + private lateinit var preferences: SharedPreferences private lateinit var adapter: AnnouncementAdapter private val picker by lazy { EmojiPicker(this) } @@ -89,7 +90,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) announcementsList.addItemDecoration(divider) - val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + preferences = PreferenceManager.getDefaultSharedPreferences(this) val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled) @@ -128,7 +129,11 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, viewModel.emojis.observe(this) { it?.let { list -> - picker.adapter = EmojiAdapter(list, this) + picker.adapter = EmojiAdapter( + list, + this, + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) } } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt b/husky/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt index c5ca662..0eb10c7 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt @@ -5,13 +5,13 @@ import android.app.Activity import android.app.ProgressDialog import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.content.pm.PackageManager import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.MediaStore -import android.util.Log import android.view.KeyEvent import android.view.MenuItem import android.view.View @@ -21,15 +21,6 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting -import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.entity.Chat -import com.keylesspalace.tusky.entity.Emoji -import com.keylesspalace.tusky.interfaces.ChatActionListener -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.repository.ChatMesssageOrPlaceholder -import com.keylesspalace.tusky.repository.ChatRepository -import com.keylesspalace.tusky.viewdata.ChatMessageViewData import androidx.arch.core.util.Function import androidx.core.app.ActivityCompat import androidx.core.app.ActivityOptionsCompat @@ -41,28 +32,70 @@ import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputContentInfoCompat import androidx.lifecycle.Lifecycle import androidx.preference.PreferenceManager -import androidx.recyclerview.widget.* +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListUpdateCallback import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.request.target.CustomTarget 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.* -import com.keylesspalace.tusky.adapter.* -import com.keylesspalace.tusky.appstore.* -import com.keylesspalace.tusky.components.common.* +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.ViewTagActivity +import com.keylesspalace.tusky.adapter.ChatMessagesAdapter +import com.keylesspalace.tusky.adapter.ChatMessagesViewHolder +import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.adapter.TimelineAdapter +import com.keylesspalace.tusky.appstore.ChatMessageDeliveredEvent +import com.keylesspalace.tusky.appstore.ChatMessageReceivedEvent +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.common.AudioSizeException +import com.keylesspalace.tusky.components.common.DEFAULT_CHARACTER_LIMIT +import com.keylesspalace.tusky.components.common.MediaSizeException +import com.keylesspalace.tusky.components.common.VideoOrImageException +import com.keylesspalace.tusky.components.common.VideoSizeException +import com.keylesspalace.tusky.components.common.createNewImageFile +import com.keylesspalace.tusky.components.common.toFileName import com.keylesspalace.tusky.components.compose.ComposeActivity -import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter +import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Chat +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.interfaces.ChatActionListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.repository.ChatMesssageOrPlaceholder +import com.keylesspalace.tusky.repository.ChatRepository import com.keylesspalace.tusky.repository.Placeholder import com.keylesspalace.tusky.repository.TimelineRequestMode import com.keylesspalace.tusky.service.MessageToSend import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.* +import com.keylesspalace.tusky.util.ComposeTokenizer +import com.keylesspalace.tusky.util.Either +import com.keylesspalace.tusky.util.PairedList +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.ViewDataUtils +import com.keylesspalace.tusky.util.afterTextChanged +import com.keylesspalace.tusky.util.dec +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.highlightSpans +import com.keylesspalace.tusky.util.inc +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.withLifecycleContext import com.keylesspalace.tusky.view.EmojiKeyboard +import com.keylesspalace.tusky.viewdata.ChatMessageViewData import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt @@ -70,46 +103,71 @@ import com.mikepenz.iconics.utils.sizeDp import com.uber.autodispose.android.lifecycle.autoDispose import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.android.synthetic.main.activity_chat.* -import kotlinx.android.synthetic.main.toolbar_basic.toolbar import java.io.File import java.io.IOException -import java.lang.Exception import java.util.concurrent.TimeUnit import javax.inject.Inject +import kotlinx.android.synthetic.main.activity_chat.actionPhotoPick +import kotlinx.android.synthetic.main.activity_chat.actionPhotoTake +import kotlinx.android.synthetic.main.activity_chat.activityChat +import kotlinx.android.synthetic.main.activity_chat.addMediaBottomSheet +import kotlinx.android.synthetic.main.activity_chat.attachmentButton +import kotlinx.android.synthetic.main.activity_chat.attachmentLayout +import kotlinx.android.synthetic.main.activity_chat.chatAvatar +import kotlinx.android.synthetic.main.activity_chat.chatTitle +import kotlinx.android.synthetic.main.activity_chat.chatUsername +import kotlinx.android.synthetic.main.activity_chat.editText +import kotlinx.android.synthetic.main.activity_chat.emojiButton +import kotlinx.android.synthetic.main.activity_chat.emojiView +import kotlinx.android.synthetic.main.activity_chat.imageAttachment +import kotlinx.android.synthetic.main.activity_chat.messageView +import kotlinx.android.synthetic.main.activity_chat.progressBar +import kotlinx.android.synthetic.main.activity_chat.recycler +import kotlinx.android.synthetic.main.activity_chat.sendButton +import kotlinx.android.synthetic.main.activity_chat.stickerButton +import kotlinx.android.synthetic.main.activity_chat.stickerKeyboard +import kotlinx.android.synthetic.main.activity_chat.textAttachment +import kotlinx.android.synthetic.main.toolbar_basic.toolbar +import timber.log.Timber + +class ChatActivity : BottomSheetActivity(), + Injectable, + ChatActionListener, + ComposeAutoCompleteAdapter.AutocompletionProvider, + EmojiKeyboard.OnEmojiSelectedListener, + OnEmojiSelectedListener, + InputConnectionCompat.OnCommitContentListener { -class ChatActivity: BottomSheetActivity(), - Injectable, - ChatActionListener, - ComposeAutoCompleteAdapter.AutocompletionProvider, - EmojiKeyboard.OnEmojiSelectedListener, - OnEmojiSelectedListener, - InputConnectionCompat.OnCommitContentListener { - private val TAG = "ChatsActivity" // logging tag private val LOAD_AT_ONCE = 30 @Inject lateinit var eventHub: EventHub + @Inject lateinit var api: MastodonApi + @Inject lateinit var chatsRepo: ChatRepository + @Inject lateinit var serviceClient: ServiceClient + @Inject lateinit var viewModelFactory: ViewModelFactory @VisibleForTesting val viewModel: ChatViewModel by viewModels { viewModelFactory } + @VisibleForTesting var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT lateinit var adapter: ChatMessagesAdapter - private val msgs = PairedList(Function { input -> - input.asRightOrNull()?.let(ViewDataUtils::chatMessageToViewData) ?: - ChatMessageViewData.Placeholder(input.asLeft().id, false) - }) + private val msgs = + PairedList(Function { input -> + input.asRightOrNull()?.let(ViewDataUtils::chatMessageToViewData) + ?: ChatMessageViewData.Placeholder(input.asLeft().id, false) + }) private var bottomLoading = false private var isNeedRefresh = false @@ -117,7 +175,7 @@ class ChatActivity: BottomSheetActivity(), private var initialUpdateFailed = false private var haveStickers = false - private lateinit var addMediaBehavior : BottomSheetBehavior<*> + private lateinit var addMediaBehavior: BottomSheetBehavior<*> private lateinit var emojiBehavior: BottomSheetBehavior<*> private lateinit var stickerBehavior: BottomSheetBehavior<*> @@ -130,40 +188,49 @@ class ChatActivity: BottomSheetActivity(), private val listUpdateCallback = object : ListUpdateCallback { override fun onInserted(position: Int, count: Int) { - Log.d(TAG, "onInserted") + Timber.d("onInserted") adapter.notifyItemRangeInserted(position, count) - if (position == 0) { + if(position == 0) { recycler.scrollToPosition(0) } } override fun onRemoved(position: Int, count: Int) { - Log.d(TAG, "onRemoved") + Timber.d("onRemoved") adapter.notifyItemRangeRemoved(position, count) } override fun onMoved(fromPosition: Int, toPosition: Int) { - Log.d(TAG, "onMoved") + Timber.d("onMoved") adapter.notifyItemMoved(fromPosition, toPosition) } override fun onChanged(position: Int, count: Int, payload: Any?) { - Log.d(TAG, "onChanged") + Timber.d("onChanged") adapter.notifyItemRangeChanged(position, count, payload) } } private val diffCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean { + override fun areItemsTheSame( + oldItem: ChatMessageViewData, + newItem: ChatMessageViewData + ): Boolean { return oldItem.getViewDataId() == newItem.getViewDataId() } - override fun areContentsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean { + override fun areContentsTheSame( + oldItem: ChatMessageViewData, + newItem: ChatMessageViewData + ): Boolean { return false // Items are different always. It allows to refresh timestamp on every view holder update } - override fun getChangePayload(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Any? { - return if (oldItem.deepEquals(newItem)) { + override fun getChangePayload( + oldItem: ChatMessageViewData, + newItem: ChatMessageViewData + ): Any? { + return if(oldItem.deepEquals(newItem)) { //If items are equal - update timestamp only listOf(ChatMessagesViewHolder.Key.KEY_CREATED) } else // If items are different - update a whole view holder @@ -171,8 +238,10 @@ class ChatActivity: BottomSheetActivity(), } } - private val differ = AsyncListDiffer(listUpdateCallback, - AsyncDifferConfig.Builder(diffCallback).build()) + private val differ = AsyncListDiffer( + listUpdateCallback, + AsyncDifferConfig.Builder(diffCallback).build() + ) private val dataSource = object : TimelineAdapter.AdapterDataSource { override fun getItemCount(): Int { @@ -184,11 +253,13 @@ class ChatActivity: BottomSheetActivity(), } } - private lateinit var chatId : String - private lateinit var avatarUrl : String - private lateinit var displayName : String - private lateinit var username : String - private lateinit var emojis : ArrayList + private lateinit var chatId: String + private lateinit var avatarUrl: String + private lateinit var displayName: String + private lateinit var username: String + private lateinit var emojis: ArrayList + + private lateinit var preferences: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -197,18 +268,23 @@ class ChatActivity: BottomSheetActivity(), throw Exception("No active account!") } - chatId = intent.getStringExtra(ID) ?: throw IllegalArgumentException("Can't open ChatActivity without chatId") - avatarUrl = intent.getStringExtra(AVATAR_URL) ?: throw IllegalArgumentException("Can't open ChatActivity without avatarUrl") - displayName = intent.getStringExtra(DISPLAY_NAME) ?: throw IllegalArgumentException("Can't open ChatActivity without displayName") - username = intent.getStringExtra(USERNAME) ?: throw IllegalArgumentException("Can't open ChatActivity without username") - emojis = intent.getParcelableArrayListExtra(EMOJIS) ?: throw IllegalArgumentException("Can't open ChatActivity without emojis") + chatId = intent.getStringExtra(ID) + ?: throw IllegalArgumentException("Can't open ChatActivity without chatId") + avatarUrl = intent.getStringExtra(AVATAR_URL) + ?: throw IllegalArgumentException("Can't open ChatActivity without avatarUrl") + displayName = intent.getStringExtra(DISPLAY_NAME) + ?: throw IllegalArgumentException("Can't open ChatActivity without displayName") + username = intent.getStringExtra(USERNAME) + ?: throw IllegalArgumentException("Can't open ChatActivity without username") + emojis = intent.getParcelableArrayListExtra(EMOJIS) + ?: throw IllegalArgumentException("Can't open ChatActivity without emojis") setContentView(R.layout.activity_chat) setSupportActionBar(toolbar) subscribeToUpdates() - val preferences = PreferenceManager.getDefaultSharedPreferences(this) + preferences = PreferenceManager.getDefaultSharedPreferences(this) viewModel.tryFetchStickers = preferences.getBoolean(PrefKeys.STICKERS, false) viewModel.anonymizeNames = preferences.getBoolean(PrefKeys.ANONYMIZE_FILENAMES, false) @@ -223,27 +299,27 @@ class ChatActivity: BottomSheetActivity(), photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY) eventHub.events - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe { event: Event? -> - when(event) { - is ChatMessageDeliveredEvent -> { - if(event.chatMsg.chatId == chatId) { - onRefresh() - enableButton(attachmentButton, true, true) - enableButton(stickerButton, haveStickers, haveStickers) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event: Event? -> + when(event) { + is ChatMessageDeliveredEvent -> { + if(event.chatMsg.chatId == chatId) { + onRefresh() + enableButton(attachmentButton, true, true) + enableButton(stickerButton, haveStickers, haveStickers) - sending = false - enableSendButton() - } + sending = false + enableSendButton() } - is ChatMessageReceivedEvent -> { - if(event.chatMsg.chatId == chatId) { - onRefresh() - } + } + is ChatMessageReceivedEvent -> { + if(event.chatMsg.chatId == chatId) { + onRefresh() } } } + } tryCache() } @@ -254,7 +330,12 @@ class ChatActivity: BottomSheetActivity(), setDisplayHomeAsUpEnabled(true) setDisplayShowHomeEnabled(true) } - loadAvatar(avatarUrl, chatAvatar, resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp),true) + loadAvatar( + avatarUrl, + chatAvatar, + resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp), + true + ) chatTitle.text = displayName.emojify(emojis, chatTitle, true) chatUsername.text = username } @@ -280,7 +361,7 @@ class ChatActivity: BottomSheetActivity(), popup.menu.add(0, removeId, 0, R.string.action_remove) popup.setOnMenuItemClickListener { menuItem -> viewModel.media.value?.get(0)?.let { - when (menuItem.itemId) { + when(menuItem.itemId) { addCaptionId -> { makeCaptionDialog(it.description, it.uri) { newDescription -> viewModel.updateDescription(it.localId, newDescription) @@ -306,7 +387,8 @@ class ChatActivity: BottomSheetActivity(), editText.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } editText.setAdapter( - ComposeAutoCompleteAdapter(this)) + ComposeAutoCompleteAdapter(this) + ) editText.setTokenizer(ComposeTokenizer()) editText.setText(startingText) @@ -320,8 +402,9 @@ class ChatActivity: BottomSheetActivity(), } // work around Android platform bug -> https://issuetracker.google.com/issues/67102093 - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O - || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1) { + if(Build.VERSION.SDK_INT == Build.VERSION_CODES.O + || Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 + ) { editText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } } @@ -332,7 +415,7 @@ class ChatActivity: BottomSheetActivity(), return val haveMedia = viewModel.media.value?.isNotEmpty() ?: false - val haveText = editText.text.isNotEmpty() + val haveText = editText.text.isNotEmpty() enableButton(sendButton, haveMedia || haveText, haveMedia || haveText) } @@ -342,17 +425,22 @@ class ChatActivity: BottomSheetActivity(), } /** This is for the fancy keyboards which can insert images and stuff. */ - override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean { + override fun onCommitContent( + inputContentInfo: InputContentInfoCompat, + flags: Int, + opts: Bundle? + ): Boolean { // Verify the returned content's type is of the correct MIME type val supported = inputContentInfo.description.hasMimeType("image/*") if(supported) { - val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 + val lacksPermission = + (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0 if(lacksPermission) { try { inputContentInfo.requestPermission() - } catch (e: Exception) { - Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message) + } catch(e: Exception) { + Timber.e("InputContentInfoCompat#requestPermission() failed: ${e.message}") return false } } @@ -382,7 +470,11 @@ class ChatActivity: BottomSheetActivity(), enableSendButton() enableButton(attachmentButton, notHaveMedia, notHaveMedia) - enableButton(stickerButton, haveStickers && notHaveMedia, haveStickers && notHaveMedia) + enableButton( + stickerButton, + haveStickers && notHaveMedia, + haveStickers && notHaveMedia + ) if(!notHaveMedia) { val media = it[0] @@ -412,10 +504,10 @@ class ChatActivity: BottomSheetActivity(), imageAttachment.setProgress(media.uploadPercent) Glide.with(imageAttachment.context) - .load(media.uri) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .dontAnimate() - .into(imageAttachment) + .load(media.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(imageAttachment) } } @@ -431,8 +523,12 @@ class ChatActivity: BottomSheetActivity(), } private fun setEmojiList(emojiList: List?) { - if (emojiList != null) { - emojiView.adapter = EmojiAdapter(emojiList, this@ChatActivity) + if(emojiList != null) { + emojiView.adapter = EmojiAdapter( + emojiList, + this@ChatActivity, + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) enableButton(emojiButton, true, emojiList.isNotEmpty()) } } @@ -441,7 +537,7 @@ class ChatActivity: BottomSheetActivity(), // If you select "backward" in an editable, you get SelectionStart > SelectionEnd val start = editText.selectionStart.coerceAtMost(editText.selectionEnd) val end = editText.selectionStart.coerceAtLeast(editText.selectionEnd) - val textToInsert = if (start > 0 && !editText.text[start - 1].isWhitespace()) { + val textToInsert = if(start > 0 && !editText.text[start - 1].isWhitespace()) { " $text" } else { text @@ -457,7 +553,7 @@ class ChatActivity: BottomSheetActivity(), } override fun onEmojiSelected(id: String, shortcode: String) { - Glide.with(this).asFile().load(shortcode).into( object : CustomTarget() { + Glide.with(this).asFile().load(shortcode).into(object : CustomTarget() { override fun onLoadCleared(placeholder: Drawable?) { displayTransientError(R.string.error_sticker_fetch) } @@ -492,10 +588,19 @@ class ChatActivity: BottomSheetActivity(), val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) - val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { colorInt = textColor; sizeDp = 18 } - actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds(cameraIcon, null, null, null) + val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { + colorInt = textColor; sizeDp = 18 + } + actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds( + cameraIcon, + null, + null, + null + ) - val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { colorInt = textColor; sizeDp = 18 } + val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { + colorInt = textColor; sizeDp = 18 + } actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds(imageIcon, null, null, null) actionPhotoTake.setOnClickListener { initiateCameraApp() } @@ -505,14 +610,16 @@ class ChatActivity: BottomSheetActivity(), private fun onSendClicked() { val media = viewModel.getSingleMedia() - serviceClient.sendChatMessage(MessageToSend( + serviceClient.sendChatMessage( + MessageToSend( editText.text.toString(), media?.id, media?.uri?.toString(), accountManager.activeAccount!!.id, this.chatId, 0 - )) + ) + ) sending = true editText.text.clear() @@ -523,7 +630,7 @@ class ChatActivity: BottomSheetActivity(), } private fun openPickDialog() { - if (addMediaBehavior.state == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + if(addMediaBehavior.state == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { addMediaBehavior.state = BottomSheetBehavior.STATE_EXPANDED emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN @@ -534,11 +641,14 @@ class ChatActivity: BottomSheetActivity(), private fun showEmojis() { emojiView.adapter?.let { - if (it.itemCount == 0) { - val errorMessage = getString(R.string.error_no_custom_emojis, accountManager.activeAccount!!.domain) + if(it.itemCount == 0) { + val errorMessage = getString( + R.string.error_no_custom_emojis, + accountManager.activeAccount!!.domain + ) Toast.makeText(this, errorMessage, Toast.LENGTH_SHORT).show() } else { - if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + if(emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN @@ -550,7 +660,7 @@ class ChatActivity: BottomSheetActivity(), } private fun showStickers() { - if (stickerBehavior.state == BottomSheetBehavior.STATE_HIDDEN || stickerBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + if(stickerBehavior.state == BottomSheetBehavior.STATE_HIDDEN || stickerBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { stickerBehavior.state = BottomSheetBehavior.STATE_EXPANDED addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN @@ -566,18 +676,20 @@ class ChatActivity: BottomSheetActivity(), // android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was // way before permission dialogues have been introduced. val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) - if (intent.resolveActivity(packageManager) != null) { + if(intent.resolveActivity(packageManager) != null) { val photoFile: File = try { createNewImageFile(this) - } catch (ex: IOException) { + } catch(ex: IOException) { displayTransientError(R.string.error_media_upload_opening) return } // Continue only if the File was successfully created - photoUploadUri = FileProvider.getUriForFile(this, - BuildConfig.APPLICATION_ID + ".fileprovider", - photoFile) + photoUploadUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + ".fileprovider", + photoFile + ) intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri) startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT) } @@ -587,12 +699,18 @@ class ChatActivity: BottomSheetActivity(), addMediaBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onStateChanged(bottomSheet: View, newState: Int) { //Wait until bottom sheet is not collapsed and show next screen after - if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + if(newState == BottomSheetBehavior.STATE_COLLAPSED) { addMediaBehavior.removeBottomSheetCallback(this) - if (ContextCompat.checkSelfPermission(this@ChatActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { - ActivityCompat.requestPermissions(this@ChatActivity, - arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), - PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) + if(ContextCompat.checkSelfPermission( + this@ChatActivity, + Manifest.permission.READ_EXTERNAL_STORAGE + ) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this@ChatActivity, + arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), + PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE + ) } else { initiateMediaPicking() } @@ -604,19 +722,24 @@ class ChatActivity: BottomSheetActivity(), addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, - grantResults: IntArray) { - if (requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + override fun onRequestPermissionsResult( + requestCode: Int, permissions: Array, + grantResults: IntArray + ) { + if(requestCode == PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { + if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { initiateMediaPicking() } else { - val bar = Snackbar.make(activityChat, R.string.error_media_upload_permission, - Snackbar.LENGTH_SHORT).apply { + val bar = Snackbar.make( + activityChat, R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT + ).apply { } - bar.setAction(R.string.action_retry) { onMediaPick()} + bar.setAction(R.string.action_retry) { onMediaPick() } //necessary so snackbar is shown over everything - bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + bar.view.elevation = + resources.getDimension(R.dimen.compose_activity_snackbar_elevation) bar.show() } } @@ -632,48 +755,55 @@ class ChatActivity: BottomSheetActivity(), override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { super.onActivityResult(requestCode, resultCode, intent) - if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { + if(resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { pickMedia(intent.data!!) - } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { + } else if(resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { pickMedia(photoUploadUri!!) } } private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { button.isEnabled = clickable - ThemeUtils.setDrawableTint(this, button.drawable, - if (colorActive) android.R.attr.textColorTertiary - else R.attr.textColorDisabled) + ThemeUtils.setDrawableTint( + this, button.drawable, + if(colorActive) android.R.attr.textColorTertiary + else R.attr.textColorDisabled + ) } - private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null, filename: String? = null) { + private fun pickMedia( + uri: Uri, + contentInfoCompat: InputContentInfoCompat? = null, + filename: String? = null + ) { withLifecycleContext { - viewModel.pickMedia(uri, filename ?: uri.toFileName(contentResolver)).observe { exceptionOrItem -> + viewModel.pickMedia(uri, filename ?: uri.toFileName(contentResolver)) + .observe { exceptionOrItem -> - contentInfoCompat?.releasePermission() + contentInfoCompat?.releasePermission() - if(exceptionOrItem.isLeft()) { - val errorId = when (val exception = exceptionOrItem.asLeft()) { - is VideoSizeException -> { - R.string.error_video_upload_size - } - is MediaSizeException -> { - R.string.error_media_upload_size - } - is AudioSizeException -> { - R.string.error_audio_upload_size - } - is VideoOrImageException -> { - R.string.error_media_upload_image_or_video - } - else -> { - Log.d(TAG, "That file could not be opened", exception) - R.string.error_media_upload_opening + if(exceptionOrItem.isLeft()) { + val errorId = when(val exception = exceptionOrItem.asLeft()) { + is VideoSizeException -> { + R.string.error_video_upload_size + } + is MediaSizeException -> { + R.string.error_media_upload_size + } + is AudioSizeException -> { + R.string.error_audio_upload_size + } + is VideoOrImageException -> { + R.string.error_media_upload_image_or_video + } + else -> { + Timber.e("That file could not be opened", exception) + R.string.error_media_upload_opening + } } + displayTransientError(errorId) } - displayTransientError(errorId) } - } } } @@ -698,57 +828,64 @@ class ChatActivity: BottomSheetActivity(), // Request timeline from disk to make it quick, then replace it with timeline from // the server to update it chatsRepo.getChatMessages(chatId, null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe { msgs -> - if (msgs.size > 1) { - val mutableMsgs = msgs.toMutableList() - clearPlaceholdersForResponse(mutableMsgs) - this.msgs.clear() - this.msgs.addAll(mutableMsgs) - updateAdapter() - progressBar.visibility = View.GONE - // Request statuses including current top to refresh all of them - } - updateCurrent() - loadAbove() + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { msgs -> + if(msgs.size > 1) { + val mutableMsgs = msgs.toMutableList() + clearPlaceholdersForResponse(mutableMsgs) + this.msgs.clear() + this.msgs.addAll(mutableMsgs) + updateAdapter() + progressBar.visibility = View.GONE + // Request statuses including current top to refresh all of them } + updateCurrent() + loadAbove() + } } private fun updateCurrent() { - if (msgs.isEmpty()) { + if(msgs.isEmpty()) { return } - val topId = msgs.first { it.isRight() }.asRight().id - chatsRepo.getChatMessages(chatId, topId, null, null, LOAD_AT_ONCE, TimelineRequestMode.NETWORK) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe({ messages -> - initialUpdateFailed = false - // When cached timeline is too old, we would replace it with nothing - if (messages.isNotEmpty()) { - // clear old cached statuses - if(this.msgs.isNotEmpty()) { - this.msgs.removeAll { - if(it.isRight()) { - val chat = it.asRight() - chat.id.length < topId.length || chat.id < topId - } else { - val placeholder = it.asLeft() - placeholder.id.length < topId.length || placeholder.id < topId - } + val topId = msgs.first { it.isRight() }.asRight().id + chatsRepo.getChatMessages( + chatId, + topId, + null, + null, + LOAD_AT_ONCE, + TimelineRequestMode.NETWORK + ) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ messages -> + initialUpdateFailed = false + // When cached timeline is too old, we would replace it with nothing + if(messages.isNotEmpty()) { + // clear old cached statuses + if(this.msgs.isNotEmpty()) { + this.msgs.removeAll { + if(it.isRight()) { + val chat = it.asRight() + chat.id.length < topId.length || chat.id < topId + } else { + val placeholder = it.asLeft() + placeholder.id.length < topId.length || placeholder.id < topId } } - this.msgs.addAll(messages) - updateAdapter() } - bottomLoading = false - }, { - initialUpdateFailed = true - // Indicate that we are not loading anymore - progressBar.visibility = View.GONE - }) + this.msgs.addAll(messages) + updateAdapter() + } + bottomLoading = false + }, { + initialUpdateFailed = true + // Indicate that we are not loading anymore + progressBar.visibility = View.GONE + }) } private fun showNothing() { @@ -759,60 +896,65 @@ class ChatActivity: BottomSheetActivity(), private fun loadAbove() { var firstOrNull: String? = null var secondOrNull: String? = null - for (i in msgs.indices) { + for(i in msgs.indices) { val msg = msgs[i] - if (msg.isRight()) { + if(msg.isRight()) { firstOrNull = msg.asRight().id - if (i + 1 < msgs.size && msgs[i + 1].isRight()) { + if(i + 1 < msgs.size && msgs[i + 1].isRight()) { secondOrNull = msgs[i + 1].asRight().id } break } } - if (firstOrNull != null) { + if(firstOrNull != null) { sendFetchMessagesRequest(null, firstOrNull, secondOrNull, FetchEnd.TOP, -1) } else { sendFetchMessagesRequest(null, null, null, FetchEnd.BOTTOM, -1) } } - private fun sendFetchMessagesRequest(maxId: String?, sinceId: String?, - sinceIdMinusOne: String?, - fetchEnd: FetchEnd, pos: Int) { + private fun sendFetchMessagesRequest( + maxId: String?, sinceId: String?, + sinceIdMinusOne: String?, + fetchEnd: FetchEnd, pos: Int + ) { // allow getting old statuses/fallbacks for network only for for bottom loading - val mode = if (fetchEnd == FetchEnd.BOTTOM) { + val mode = if(fetchEnd == FetchEnd.BOTTOM) { TimelineRequestMode.ANY } else { TimelineRequestMode.NETWORK } chatsRepo.getChatMessages(chatId, maxId, sinceId, sinceIdMinusOne, LOAD_AT_ONCE, mode) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( { result -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, - { onFetchTimelineFailure(Exception(it), fetchEnd, pos) }) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ result -> onFetchTimelineSuccess(result.toMutableList(), fetchEnd, pos) }, + { onFetchTimelineFailure(Exception(it), fetchEnd, pos) }) } private fun updateAdapter() { - Log.d(TAG, "updateAdapter") + Timber.d("updateAdapter") differ.submitList(msgs.pairedCopy) } - private fun updateMessages(newMsgs: MutableList, fullFetch: Boolean) { - if (newMsgs.isEmpty()) { + private fun updateMessages( + newMsgs: MutableList, + fullFetch: Boolean + ) { + if(newMsgs.isEmpty()) { updateAdapter() return } - if (msgs.isEmpty()) { + if(msgs.isEmpty()) { msgs.addAll(newMsgs) } else { val lastOfNew = newMsgs[newMsgs.size - 1] val index = msgs.indexOf(lastOfNew) - if (index >= 0) { + if(index >= 0) { msgs.subList(0, index).clear() } val newIndex = newMsgs.indexOf(msgs[0]) - if (newIndex == -1) { - if (index == -1 && fullFetch) { + if(newIndex == -1) { + if(index == -1 && fullFetch) { newMsgs.findLast { it.isRight() }?.let { val placeholderId = it.asRight().id.inc() newMsgs.add(Either.Left(Placeholder(placeholderId))) @@ -829,24 +971,26 @@ class ChatActivity: BottomSheetActivity(), } private fun removeConsecutivePlaceholders() { - for (i in 0 until msgs.size - 1) { - if (msgs[i].isLeft() && msgs[i + 1].isLeft()) { + for(i in 0 until msgs.size - 1) { + if(msgs[i].isLeft() && msgs[i + 1].isLeft()) { msgs.removeAt(i) } } } - private fun replacePlaceholderWithMessages(newMsgs: MutableList, - fullFetch: Boolean, pos: Int) { + private fun replacePlaceholderWithMessages( + newMsgs: MutableList, + fullFetch: Boolean, pos: Int + ) { val placeholder = msgs[pos] - if (placeholder.isLeft()) { + if(placeholder.isLeft()) { msgs.removeAt(pos) } - if (newMsgs.isEmpty()) { + if(newMsgs.isEmpty()) { updateAdapter() return } - if (fullFetch) { + if(fullFetch) { newMsgs.add(placeholder) } msgs.addAll(pos, newMsgs) @@ -855,64 +999,66 @@ class ChatActivity: BottomSheetActivity(), } private fun addItems(newMsgs: List) { - if (newMsgs.isEmpty()) { + if(newMsgs.isEmpty()) { return } val last = msgs.findLast { it.isRight() } // I was about to replace findStatus with indexOf but it is incorrect to compare value // types by ID anyway and we should change equals() for Status, I think, so this makes sense - if (last != null && !newMsgs.contains(last)) { + if(last != null && !newMsgs.contains(last)) { msgs.addAll(newMsgs) removeConsecutivePlaceholders() updateAdapter() } } - private fun onFetchTimelineSuccess(msgs: MutableList, - fetchEnd: FetchEnd, pos: Int) { + private fun onFetchTimelineSuccess( + msgs: MutableList, + fetchEnd: FetchEnd, pos: Int + ) { // We filled the hole (or reached the end) if the server returned less statuses than we // we asked for. val fullFetch = msgs.size >= LOAD_AT_ONCE - when (fetchEnd) { + when(fetchEnd) { FetchEnd.TOP -> { updateMessages(msgs, fullFetch) val last = msgs.indexOfFirst { it.isRight() } mastodonApi.markChatAsRead(chatId, msgs[last].asRight().id) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe({ - Log.d(TAG, "Marked new messages as read up to ${msgs[last].asRight().id}") - }, { - Log.d(TAG, "Failed to mark messages as read", it) - }) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe({ + Timber.d("Marked new messages as read up to ${msgs[last].asRight().id}") + }, { + Timber.e("Failed to mark messages as read", it) + }) } FetchEnd.MIDDLE -> { replacePlaceholderWithMessages(msgs, fullFetch, pos) } FetchEnd.BOTTOM -> { - if (this.msgs.isNotEmpty() && !this.msgs.last().isRight()) { + if(this.msgs.isNotEmpty() && !this.msgs.last().isRight()) { this.msgs.removeAt(this.msgs.size - 1) updateAdapter() } - if (msgs.isNotEmpty() && !msgs.last().isRight()) { + if(msgs.isNotEmpty() && !msgs.last().isRight()) { // Removing placeholder if it's the last one from the cache msgs.removeAt(msgs.size - 1) } val oldSize = this.msgs.size - if (this.msgs.size > 1) { + if(this.msgs.size > 1) { addItems(msgs) } else { updateMessages(msgs, fullFetch) } - if (this.msgs.size == oldSize) { + if(this.msgs.size == oldSize) { // This may be a brittle check but seems like it works // Can we check it using headers somehow? Do all server support them? didLoadEverythingBottom = true @@ -921,7 +1067,7 @@ class ChatActivity: BottomSheetActivity(), } updateBottomLoadingState(fetchEnd) progressBar.visibility = View.GONE - if (this.msgs.size == 0) { + if(this.msgs.size == 0) { showNothing() } else { messageView.visibility = View.GONE @@ -932,17 +1078,17 @@ class ChatActivity: BottomSheetActivity(), messageView.visibility = View.GONE isNeedRefresh = false - if (this.initialUpdateFailed) { + if(this.initialUpdateFailed) { updateCurrent() } loadAbove() } private fun onFetchTimelineFailure(exception: Exception, fetchEnd: FetchEnd, position: Int) { - if (fetchEnd == FetchEnd.MIDDLE && !msgs[position].isRight()) { + if(fetchEnd == FetchEnd.MIDDLE && !msgs[position].isRight()) { var placeholder = msgs[position].asLeftOrNull() val newViewData: ChatMessageViewData - if (placeholder == null) { + if(placeholder == null) { val msg = msgs[position - 1].asRight() val newId = msg.id.dec() placeholder = Placeholder(newId) @@ -950,9 +1096,9 @@ class ChatActivity: BottomSheetActivity(), newViewData = ChatMessageViewData.Placeholder(placeholder.id, false) msgs.setPairedItem(position, newViewData) updateAdapter() - } else if (msgs.isEmpty()) { + } else if(msgs.isEmpty()) { messageView.visibility = View.VISIBLE - if (exception is IOException) { + if(exception is IOException) { messageView.setup(R.drawable.elephant_offline, R.string.error_network) { progressBar.visibility = View.VISIBLE onRefresh() @@ -964,52 +1110,55 @@ class ChatActivity: BottomSheetActivity(), } } } - Log.e(TAG, "Fetch Failure: " + exception.message) + Timber.e("Fetch Failure: " + exception.message) updateBottomLoadingState(fetchEnd) progressBar.visibility = View.GONE } private fun updateBottomLoadingState(fetchEnd: FetchEnd) { - if (fetchEnd == FetchEnd.BOTTOM) { + if(fetchEnd == FetchEnd.BOTTOM) { bottomLoading = false } } override fun onLoadMore(position: Int) { //check bounds before accessing list, - if (msgs.size >= position && position > 0) { + if(msgs.size >= position && position > 0) { val fromChat = msgs[position - 1].asRightOrNull() val toChat = msgs[position + 1].asRightOrNull() - if (fromChat == null || toChat == null) { - Log.e(TAG, "Failed to load more at $position, wrong placeholder position") + if(fromChat == null || toChat == null) { + Timber.e("Failed to load more at $position, wrong placeholder position") return } - val maxMinusOne = if (msgs.size > position + 1 && msgs[position + 2].isRight()) msgs[position + 1].asRight().id else null - sendFetchMessagesRequest(fromChat.id, toChat.id, maxMinusOne, - FetchEnd.MIDDLE, position) + val maxMinusOne = + if(msgs.size > position + 1 && msgs[position + 2].isRight()) msgs[position + 1].asRight().id else null + sendFetchMessagesRequest( + fromChat.id, toChat.id, maxMinusOne, + FetchEnd.MIDDLE, position + ) val (id) = msgs[position].asLeft() val newViewData = ChatMessageViewData.Placeholder(id, true) msgs.setPairedItem(position, newViewData) updateAdapter() } else { - Log.e(TAG, "error loading more") + Timber.e("error loading more") } } override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { - Log.d(TAG, event.toString()) + Timber.d(event.toString()) if(event.action == KeyEvent.ACTION_DOWN) { - if (event.isCtrlPressed) { - if (keyCode == KeyEvent.KEYCODE_ENTER) { + if(event.isCtrlPressed) { + if(keyCode == KeyEvent.KEYCODE_ENTER) { // send message by pressing CTRL + ENTER onSendClicked() return true } } - if (keyCode == KeyEvent.KEYCODE_BACK) { + if(keyCode == KeyEvent.KEYCODE_BACK) { onBackPressed() return true } @@ -1020,12 +1169,13 @@ class ChatActivity: BottomSheetActivity(), override fun onBackPressed() { // Acting like a teen: deliberately ignoring parent. - if (addMediaBehavior.state != BottomSheetBehavior.STATE_HIDDEN || - emojiBehavior.state != BottomSheetBehavior.STATE_HIDDEN || - stickerBehavior.state != BottomSheetBehavior.STATE_HIDDEN) { + if(addMediaBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + emojiBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + stickerBehavior.state != BottomSheetBehavior.STATE_HIDDEN + ) { addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN - emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN - stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + stickerBehavior.state = BottomSheetBehavior.STATE_HIDDEN return } @@ -1033,7 +1183,7 @@ class ChatActivity: BottomSheetActivity(), } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + when(item.itemId) { android.R.id.home -> { finish() return true @@ -1053,13 +1203,13 @@ class ChatActivity: BottomSheetActivity(), * Auto dispose observable on pause */ private fun startUpdateTimestamp() { - val preferences = PreferenceManager.getDefaultSharedPreferences(this) + preferences = PreferenceManager.getDefaultSharedPreferences(this) val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false) - if (!useAbsoluteTime) { + if(!useAbsoluteTime) { Observable.interval(1, TimeUnit.MINUTES) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_PAUSE) - .subscribe { updateAdapter() } + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_PAUSE) + .subscribe { updateAdapter() } } } @@ -1086,7 +1236,8 @@ class ChatActivity: BottomSheetActivity(), if(view != null) { val url = attachment.url ViewCompat.setTransitionName(view, url) - val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, url) + val options = + ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, url) startActivity(intent, options.toBundle()) } else { @@ -1106,12 +1257,15 @@ class ChatActivity: BottomSheetActivity(), private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" private const val MESSAGE_KEY = "MESSAGE" - fun getIntent(context: Context, chat: Chat) : Intent { + fun getIntent(context: Context, chat: Chat): Intent { val intent = Intent(context, ChatActivity::class.java) intent.putExtra(ID, chat.id) intent.putExtra(AVATAR_URL, chat.account.avatar) intent.putExtra(DISPLAY_NAME, chat.account.displayName ?: chat.account.localUsername) - intent.putParcelableArrayListExtra(EMOJIS, ArrayList(chat.account.emojis ?: emptyList())) + intent.putParcelableArrayListExtra( + EMOJIS, + ArrayList(chat.account.emojis ?: emptyList()) + ) intent.putExtra(USERNAME, chat.account.username) return intent } diff --git a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index 0114e93..e45bfaa 100644 --- a/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/husky/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -167,9 +167,11 @@ class ComposeActivity : BaseActivity(), private val maxUploadMediaNumber = 4 private var mediaCount = 0 + private lateinit var preferences: SharedPreferences + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val preferences = PreferenceManager.getDefaultSharedPreferences(this) + preferences = PreferenceManager.getDefaultSharedPreferences(this) val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) if(theme == "black") { setTheme(R.style.TuskyDialogActivityBlackTheme) @@ -1447,7 +1449,11 @@ class ComposeActivity : BaseActivity(), private fun setEmojiList(emojiList: List?) { if(emojiList != null) { - binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity) + binding.emojiView.adapter = EmojiAdapter( + emojiList, + this@ComposeActivity, + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) } }