package com.keylesspalace.tusky.adapter import android.content.Context import android.graphics.drawable.ColorDrawable import android.text.TextUtils import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.* import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.interfaces.ChatActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.TimestampUtils import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.view.MediaPreviewImageView import com.keylesspalace.tusky.viewdata.ChatMessageViewData import java.text.SimpleDateFormat import java.util.* import kotlin.math.roundToInt class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) { object Key { const val KEY_CREATED = "created" } private val content: TextView = view.findViewById(R.id.content) private val timestamp: TextView = view.findViewById(R.id.datetime) private val attachmentView: MediaPreviewImageView = view.findViewById(R.id.attachment) private val mediaOverlay: ImageView = view.findViewById(R.id.mediaOverlay) private val attachmentLayout: FrameLayout = view.findViewById(R.id.attachmentLayout) private val sdf = SimpleDateFormat("HH:mm", Locale.getDefault()) private val mediaPreviewUnloaded = ColorDrawable(ThemeUtils.getColor(itemView.context, R.attr.colorBackgroundAccent)) fun setupWithChatMessage(msg: ChatMessageViewData.Concrete, chatActionListener: ChatActionListener, statusDisplayOptions: StatusDisplayOptions, payload: Any?) { if(payload == null) { if(msg.content != null) content.text = msg.content.emojify(msg.emojis, content) setAttachment(msg.attachment, chatActionListener) setCreatedAt(msg.createdAt) } else { if(payload is List<*>) { for (item in payload) { if (ChatsViewHolder.Key.KEY_CREATED == item) { setCreatedAt(msg.createdAt) } } } } } private fun loadImage(imageView: MediaPreviewImageView, previewUrl: String?, meta: Attachment.MetaData?) { if (TextUtils.isEmpty(previewUrl)) { imageView.removeFocalPoint() Glide.with(imageView) .load(mediaPreviewUnloaded) .centerInside() .into(imageView) } else { val focus = meta?.focus if (focus != null) { // If there is a focal point for this attachment: imageView.setFocalPoint(focus) Glide.with(imageView) .load(previewUrl) .placeholder(mediaPreviewUnloaded) .centerInside() .addListener(imageView) .into(imageView) } else { imageView.removeFocalPoint() Glide.with(imageView) .load(previewUrl) .placeholder(mediaPreviewUnloaded) .centerInside() .into(imageView) } } } private fun formatDuration(durationInSeconds: Double): String? { val seconds = durationInSeconds.roundToInt().toInt() % 60 val minutes = durationInSeconds.toInt() % 3600 / 60 val hours = durationInSeconds.toInt() / 3600 return String.format("%d:%02d:%02d", hours, minutes, seconds) } private fun getAttachmentDescription(context: Context, attachment: Attachment): CharSequence { var duration = "" if (attachment.meta?.duration != null && attachment.meta.duration > 0) { duration = formatDuration(attachment.meta.duration.toDouble()) + " " } return if (TextUtils.isEmpty(attachment.description)) { duration + context.getString(R.string.description_status_media_no_description_placeholder) } else { duration + attachment.description } } private fun setAttachmentClickListener(view: View, listener: ChatActionListener, attachment: Attachment, animateTransition: Boolean) { view.setOnClickListener { v: View? -> val position = adapterPosition if (position != RecyclerView.NO_POSITION) { listener.onViewMedia(position, if (animateTransition) v else null) } } view.setOnLongClickListener { v: View? -> val description = getAttachmentDescription(view.context, attachment) Toast.makeText(view.context, description, Toast.LENGTH_LONG).show() true } } private fun setAttachment(attachment: Attachment?, listener: ChatActionListener) { if(attachment == null) { attachmentLayout.visibility = View.GONE } else { attachmentLayout.visibility = View.VISIBLE val previewUrl: String = attachment.previewUrl val description: String? = attachment.description if(description != null && TextUtils.isEmpty(description) ) { attachmentView.contentDescription = description } else { attachmentView.contentDescription = attachmentView.context .getString(R.string.action_view_media) } loadImage(attachmentView, previewUrl, attachment.meta) when(attachment.type) { Attachment.Type.VIDEO, Attachment.Type.GIFV -> { mediaOverlay.visibility = View.VISIBLE } else -> { mediaOverlay.visibility = View.GONE } } setAttachmentClickListener(attachmentView, listener, attachment, true) } } private fun setCreatedAt(createdAt: Date) { timestamp.text = sdf.format(createdAt) } } class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSource, private val chatActionListener: ChatActionListener, private val statusDisplayOptions: StatusDisplayOptions, private val localUserId: String) : RecyclerView.Adapter() { private val VIEW_TYPE_OUR_MESSAGE = 0 private val VIEW_TYPE_THEIR_MESSAGE = 1 private val VIEW_TYPE_PLACEHOLDER = 2 override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { when(viewType) { VIEW_TYPE_OUR_MESSAGE -> { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_our_message, parent, false) return ChatMessagesViewHolder(view) } VIEW_TYPE_THEIR_MESSAGE -> { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_their_message, parent, false) return ChatMessagesViewHolder(view) } else -> { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_status_placeholder, parent, false) return PlaceholderViewHolder(view) } } } override fun getItemCount(): Int { return dataSource.itemCount } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { bindViewHolder(holder, position, null) } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payload: MutableList) { bindViewHolder(holder, position, payload) } private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList?) { val chat: ChatMessageViewData = dataSource.getItemAt(position) if(holder is PlaceholderViewHolder) { holder.setup(chatActionListener, (chat as ChatMessageViewData.Placeholder).isLoading) } else if(holder is ChatMessagesViewHolder) { holder.setupWithChatMessage(chat as ChatMessageViewData.Concrete, chatActionListener, statusDisplayOptions, if (payloads != null && payloads.isNotEmpty()) payloads[0] else null) } } override fun getItemViewType(position: Int): Int { if(dataSource.getItemAt(position) is ChatMessageViewData.Concrete) { val msg = dataSource.getItemAt(position) as ChatMessageViewData.Concrete if(msg.accountId == localUserId) { return VIEW_TYPE_OUR_MESSAGE } return VIEW_TYPE_THEIR_MESSAGE } return VIEW_TYPE_PLACEHOLDER } override fun getItemId(position: Int): Long { return dataSource.getItemAt(position).getViewDataId().toLong() } }