chats: chat message adapter, partially implement activity

main
Alibek Omarov 4 years ago
parent a002a76b92
commit bcfd202bbb
  1. 247
      app/src/main/java/com/keylesspalace/tusky/adapter/ChatMessagesAdapter.kt
  2. 171
      app/src/main/java/com/keylesspalace/tusky/components/chat/ChatActivity.kt
  3. 4
      app/src/main/java/com/keylesspalace/tusky/fragment/ChatsFragment.kt
  4. 9
      app/src/main/java/com/keylesspalace/tusky/interfaces/ChatActionListener.kt
  5. 24
      app/src/main/java/com/keylesspalace/tusky/repository/ChatRepository.kt
  6. 2
      app/src/main/java/com/keylesspalace/tusky/viewdata/ChatViewData.kt
  7. 5
      app/src/main/res/drawable/message_background.xml
  8. 9
      app/src/main/res/layout/activity_chat.xml
  9. 73
      app/src/main/res/layout/item_our_message.xml
  10. 76
      app/src/main/res/layout/item_their_message.xml
  11. 5
      app/src/main/res/values/attrs.xml
  12. 11
      app/src/main/res/values/dimens.xml
  13. 19
      app/src/main/res/values/styles.xml

@ -0,0 +1,247 @@
package com.keylesspalace.tusky.adapter
import android.content.Context
import android.graphics.drawable.BitmapDrawable
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.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
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.entity.ChatMessage
import com.keylesspalace.tusky.interfaces.ChatActionListener
import com.keylesspalace.tusky.util.*
import com.keylesspalace.tusky.view.MediaPreviewImageView
import com.keylesspalace.tusky.viewdata.ChatMessageViewData
import com.keylesspalace.tusky.viewdata.ChatViewData
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 shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", 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) {
content.text = msg.content.emojify(msg.emojis, content)
setAttachment(msg.attachment, chatActionListener)
setCreatedAt(msg.createdAt, statusDisplayOptions)
} else {
if(payload is List<*>) {
for (item in payload) {
if (ChatsViewHolder.Key.KEY_CREATED == item) {
setCreatedAt(msg.createdAt, statusDisplayOptions)
}
}
}
}
}
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 getAbsoluteTime(createdAt: Date?): String? {
if (createdAt == null) {
return "??:??:??"
}
return if (DateUtils.isToday(createdAt.time)) {
shortSdf.format(createdAt)
} else {
longSdf.format(createdAt)
}
}
private fun setCreatedAt(createdAt: Date, statusDisplayOptions: StatusDisplayOptions) {
if (statusDisplayOptions.useAbsoluteTime) {
timestamp.text = getAbsoluteTime(createdAt)
} else {
val then = createdAt.time
val now = System.currentTimeMillis()
val readout = TimestampUtils.getRelativeTimeSpanString(timestamp.context, then, now)
timestamp.text = readout
}
}
}
class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSource<ChatMessageViewData>,
private val chatActionListener: ChatActionListener,
private val statusDisplayOptions: StatusDisplayOptions,
private val localUserId: String)
: RecyclerView.Adapter<RecyclerView.ViewHolder>() {
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<Any>) {
bindViewHolder(holder, position, payload)
}
private fun bindViewHolder(holder: RecyclerView.ViewHolder, position: Int, payloads: MutableList<Any>?) {
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()
}
}

@ -2,44 +2,124 @@ package com.keylesspalace.tusky.components.chat
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.view.View
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.ViewTagActivity
import com.keylesspalace.tusky.adapter.ChatMessagesAdapter
import com.keylesspalace.tusky.adapter.ChatMessagesViewHolder
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder
import com.keylesspalace.tusky.adapter.TimelineAdapter
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.Chat
import com.keylesspalace.tusky.entity.ChatMessage
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.interfaces.ChatActionListener
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.repository.ChatMessageStatus
import com.keylesspalace.tusky.repository.ChatRepository
import com.keylesspalace.tusky.util.StatusDisplayOptions
import com.keylesspalace.tusky.util.emojify
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.repository.ChatStatus
import com.keylesspalace.tusky.viewdata.ChatMessageViewData
import com.keylesspalace.tusky.viewdata.ChatViewData
import kotlinx.android.synthetic.main.activity_chat.*
import kotlinx.android.synthetic.main.toolbar_basic.*
import kotlinx.android.synthetic.main.toolbar_basic.toolbar
import androidx.arch.core.util.Function
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.*
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.repository.TimelineRequestMode
import com.keylesspalace.tusky.util.*
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.android.synthetic.main.activity_chat.progressBar
import kotlinx.android.synthetic.main.fragment_timeline.*
import java.lang.Exception
import javax.inject.Inject
class ChatActivity: BaseActivity(),
Injectable {
class ChatActivity: BottomSheetActivity(),
Injectable, ChatActionListener {
private val TAG = "ChatsF" // logging tag
private val LOAD_AT_ONCE = 30
@Inject
lateinit var eventHub: EventHub
@Inject
lateinit var api: MastodonApi
@Inject
lateinit var chatsRepo: ChatRepository
lateinit var adapter: ChatMessagesAdapter
private val msgs = PairedList<ChatMessageStatus, ChatMessageViewData?>(Function<ChatMessageStatus, ChatMessageViewData?> {input ->
input.asRightOrNull()?.let(ViewDataUtils::chatMessageToViewData) ?:
ChatMessageViewData.Placeholder(input.asLeft().id, false)
})
private val listUpdateCallback = object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int) {
Log.d(TAG, "onInserted")
adapter.notifyItemRangeInserted(position, count)
if (position == 0) {
recycler.scrollToPosition(0)
}
}
override fun onRemoved(position: Int, count: Int) {
Log.d(TAG, "onRemoved")
adapter.notifyItemRangeRemoved(position, count)
}
override fun onMoved(fromPosition: Int, toPosition: Int) {
Log.d(TAG, "onMoved")
adapter.notifyItemMoved(fromPosition, toPosition)
}
override fun onChanged(position: Int, count: Int, payload: Any?) {
Log.d(TAG, "onChanged")
adapter.notifyItemRangeChanged(position, count, payload)
}
}
private val diffCallback = object : DiffUtil.ItemCallback<ChatMessageViewData>() {
override fun areItemsTheSame(oldItem: ChatMessageViewData, newItem: ChatMessageViewData): Boolean {
return oldItem.getViewDataId() == newItem.getViewDataId()
}
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)) {
//If items are equal - update timestamp only
listOf(ChatMessagesViewHolder.Key.KEY_CREATED)
} else // If items are different - update a whole view holder
null
}
}
private val differ = AsyncListDiffer(listUpdateCallback,
AsyncDifferConfig.Builder(diffCallback).build())
private val dataSource = object : TimelineAdapter.AdapterDataSource<ChatMessageViewData> {
override fun getItemCount(): Int {
return differ.currentList.size
}
override fun getItemAt(pos: Int): ChatMessageViewData {
return differ.currentList[pos]
}
}
private lateinit var chatId : String
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val chatId = intent.getStringExtra(ID)
chatId = intent.getStringExtra(ID)
val avatarUrl = intent.getStringExtra(AVATAR_URL)
val displayName = intent.getStringExtra(DISPLAY_NAME)
val username = intent.getStringExtra(USERNAME)
@ -49,6 +129,10 @@ class ChatActivity: BaseActivity(),
throw IllegalArgumentException("Can't open ChatActivity without chat id")
}
if(accountManager.activeAccount == null) {
throw Exception("No active account!")
}
setContentView(R.layout.activity_chat)
setSupportActionBar(toolbar)
@ -63,6 +147,50 @@ class ChatActivity: BaseActivity(),
chatTitle.text = displayName.emojify(emojis, chatTitle, true)
chatUsername.text = username
val statusDisplayOptions = StatusDisplayOptions(
false,
false,
true,
false,
false,
CardViewMode.NONE,
false)
adapter = ChatMessagesAdapter(dataSource, this, statusDisplayOptions, accountManager.activeAccount!!.accountId)
// TODO: a11y
recycler.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(this)
layoutManager.reverseLayout = true
recycler.layoutManager = layoutManager
recycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
recycler.adapter = adapter
tryCache()
}
private fun tryCache() {
// 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 mutableChats = msgs.toMutableList()
this.msgs.clear()
this.msgs.addAll(mutableChats)
updateAdapter()
progressBar.visibility = View.GONE
// Request statuses including current top to refresh all of them
}
}
}
private fun updateAdapter() {
Log.d(TAG, "updateAdapter")
differ.submitList(msgs.pairedCopy)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@ -75,6 +203,20 @@ class ChatActivity: BaseActivity(),
return super.onOptionsItemSelected(item)
}
override fun onViewAccount(id: String) {
viewAccount(id)
}
override fun onViewUrl(url: String) {
viewUrl(url)
}
override fun onViewTag(tag: String) {
val intent = Intent(this, ViewTagActivity::class.java)
intent.putExtra("hashtag", tag)
startActivity(intent)
}
companion object {
fun getIntent(context: Context, chat: Chat) : Intent {
val intent = Intent(context, ChatActivity::class.java)
@ -92,5 +234,4 @@ class ChatActivity: BaseActivity(),
const val USERNAME = "username"
const val EMOJIS = "emojis"
}
}

@ -115,7 +115,7 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
}
}
private val diffCallback: DiffUtil.ItemCallback<ChatViewData> = object : DiffUtil.ItemCallback<ChatViewData>() {
private val diffCallback = object : DiffUtil.ItemCallback<ChatViewData>() {
override fun areItemsTheSame(oldItem: ChatViewData, newItem: ChatViewData): Boolean {
return oldItem.getViewDataId() == newItem.getViewDataId()
}
@ -136,7 +136,7 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
private val differ = AsyncListDiffer(listUpdateCallback,
AsyncDifferConfig.Builder(diffCallback).build())
private val dataSource: TimelineAdapter.AdapterDataSource<ChatViewData> = object : TimelineAdapter.AdapterDataSource<ChatViewData> {
private val dataSource = object : TimelineAdapter.AdapterDataSource<ChatViewData> {
override fun getItemCount(): Int {
return differ.currentList.size
}

@ -4,9 +4,8 @@ import android.view.View
import com.keylesspalace.tusky.entity.Chat
interface ChatActionListener: LinkListener {
fun onLoadMore(position: Int)
fun onMore(chatId: String, v: View)
fun openChat(position: Int)
fun onLoadMore(position: Int) {}
fun onMore(chatId: String, v: View) {}
fun openChat(position: Int) {}
fun onViewMedia(position: Int, view: View?) {}
}

@ -23,10 +23,13 @@ import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList
typealias ChatStatus = Either<Placeholder, Chat>
typealias ChatMessageStatus = Either<Placeholder, ChatMessage>
interface ChatRepository {
fun getChats(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
requestMode: TimelineRequestMode): Single<out List<ChatStatus>>
fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMessageStatus>>
}
class ChatRepositoryImpl(
@ -49,6 +52,13 @@ class ChatRepositoryImpl(
}
}
override fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMessageStatus>> {
val acc = accountManager.activeAccount ?: throw IllegalStateException()
val accountId = acc.id
return getChatMessagesFromNetwork(chatId, maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode)
}
private fun getChatsFromNetwork(maxId: String?, sinceId: String?,
sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode
@ -69,6 +79,16 @@ class ChatRepositoryImpl(
}
}
private fun getChatMessagesFromNetwork(chatId: String, maxId: String?, sinceId: String?,
sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode
): Single<out List<ChatMessageStatus>> {
return mastodonApi.getChatMessages(chatId, maxId, null, sinceIdMinusOne, 0, limit + 1).map {
it.mapTo(mutableListOf(), ChatMessage::lift)
}
}
private fun addFromDbIfNeeded(accountId: Long, chats: List<ChatStatus>,
maxId: String?, sinceId: String?, limit: Int,
requestMode: TimelineRequestMode
@ -234,4 +254,6 @@ fun ChatEntityWithAccount.toChat(gson: Gson) : ChatStatus {
).lift()
}
fun Chat.lift(): Either<Placeholder, Chat> = Either.Right(this)
fun ChatMessage.lift(): ChatMessageStatus = Either.Right(this)
fun Chat.lift(): ChatStatus = Either.Right(this)

@ -77,7 +77,7 @@ abstract class ChatMessageViewData {
}
}
class Placeholder(val id: String, private val isLoading: Boolean) : ChatMessageViewData() {
class Placeholder(val id: String, val isLoading: Boolean) : ChatMessageViewData() {
override fun getViewDataId(): Long {
return id.hashCode().toLong()
}

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="?attr/chat_me_color" />
<corners android:radius="@dimen/chat_radius" />
</shape>

@ -190,4 +190,13 @@
app:layout_constraintRight_toRightOf="parent"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent">
<include layout="@layout/item_status_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/chat_between_messages_padding"
android:paddingBottom="@dimen/chat_between_messages_padding"
android:paddingStart="@dimen/chat_message_h_padding"
android:paddingEnd="@dimen/chat_message_h_padding"
android:layout_gravity="end">
<LinearLayout
android:id="@+id/contentLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/message_background"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent">
<FrameLayout
android:id="@+id/attachmentLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/chat_media_preview_item_height"
android:layout_marginTop="@dimen/chat_message_h_padding"
android:visibility="gone"
tools:visibility="visible">
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/attachment"
android:layout_width="match_parent"
android:layout_height="@dimen/chat_media_preview_item_height"
tools:src="@drawable/elephant_friend_empty" />
<ImageView
android:id="@+id/mediaOverlay"
android:layout_width="match_parent"
android:layout_height="@dimen/chat_media_preview_item_height"
android:scaleType="center"
app:srcCompat="@drawable/ic_play_indicator"
tools:ignore="ContentDescription" />
</FrameLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/chat_message_h_padding"
android:paddingTop="@dimen/chat_message_v_padding"
android:paddingEnd="@dimen/chat_message_h_padding"
android:paddingBottom="@dimen/chat_message_v_padding"
android:textColor="@color/textColorPrimary"
android:textSize="?attr/status_text_large"
tools:text="AAAAAAAA" />
</LinearLayout>
<TextView
android:id="@+id/datetime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/chat_message_h_padding"
android:paddingEnd="@dimen/chat_message_h_padding"
app:layout_constraintEnd_toStartOf="@id/contentLayout"
app:layout_constraintBottom_toBottomOf="@id/contentLayout"
tools:text="12:39"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/chat_between_messages_padding"
android:paddingBottom="@dimen/chat_between_messages_padding"
android:paddingStart="@dimen/chat_message_h_padding"
android:paddingEnd="@dimen/chat_message_h_padding"
android:layout_gravity="start">
<LinearLayout
android:id="@+id/contentLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/message_background"
android:backgroundTint="?attr/colorPrimaryDark"
android:orientation="vertical"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/datetime"
>
<FrameLayout
android:id="@+id/attachmentLayout"
android:layout_width="match_parent"
android:layout_height="@dimen/chat_media_preview_item_height"
android:layout_marginTop="@dimen/chat_message_h_padding"
android:visibility="gone"
tools:visibility="visible">
<com.keylesspalace.tusky.view.MediaPreviewImageView
android:id="@+id/attachment"
android:layout_width="match_parent"
android:layout_height="@dimen/chat_media_preview_item_height"
tools:src="@drawable/elephant_friend_empty"
/>
<ImageView
android:id="@+id/mediaOverlay"
android:layout_width="match_parent"
android:layout_height="@dimen/chat_media_preview_item_height"
android:scaleType="center"
app:srcCompat="@drawable/ic_play_indicator"
tools:ignore="ContentDescription" />
</FrameLayout>
<androidx.emoji.widget.EmojiTextView
android:id="@+id/content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/chat_message_h_padding"
android:paddingTop="@dimen/chat_message_v_padding"
android:paddingEnd="@dimen/chat_message_h_padding"
android:paddingBottom="@dimen/chat_message_v_padding"
android:textSize="?attr/status_text_large"
android:textColor="@color/textColorPrimary"
app:layout_constrainedWidth="true"
tools:text="What you guys are referring to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX. Many computer users run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of events, the version of GNU which is widely used today is often called Linux, and many of its users are not aware that it is basically the GNU system, developed by the GNU Project. There really is a Linux, and these people are using it, but it is just a part of the system they use. Linux is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called Linux distributions are really distributions of GNU/Linux. Thank you for taking your time to cooperate with with me, your friendly GNU+Linux neighbor, Richard Stallman." />
</LinearLayout>
<TextView
android:id="@+id/datetime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/chat_message_h_padding"
android:paddingEnd="@dimen/chat_message_h_padding"
app:layout_constraintStart_toEndOf="@id/contentLayout"
app:layout_constraintBottom_toBottomOf="@id/contentLayout"
tools:text="12:39"
/>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -19,4 +19,9 @@
<attr name="status_text_medium" format="dimension" />
<attr name="status_text_large" format="dimension" />
<attr name="chat_me_color" format="reference|color" />
<attr name="chat_other_color" format="reference|color" />
<attr name="chat_me_text_color" format="reference|color" />
<attr name="chat_other_text_color" format="reference|color" />
<attr name="chat_date_text_color" format="reference|color" />
</resources>

@ -51,4 +51,15 @@
<dimen name="adaptive_bitmap_outer_size">108dp</dimen>
<dimen name="fabMargin">16dp</dimen>
<dimen name="chat_base_padding">16dp</dimen>
<dimen name="chat_large_padding">64dp</dimen>
<dimen name="chat_between_messages_padding">4dp</dimen>
<dimen name="chat_message_v_padding">8dp</dimen>
<dimen name="chat_radius">10dp</dimen>
<dimen name="chat_message_h_padding">10dp</dimen>
<dimen name="chat_avatar_size">36dp</dimen>
<dimen name="chat_small_padding">8dp</dimen>
<dimen name="chat_radius_fix">12dp</dimen>
<dimen name="chat_media_preview_item_height">160dp</dimen>
</resources>

@ -79,6 +79,11 @@
<item name="swipeRefreshLayoutProgressSpinnerBackgroundColor">?attr/colorSurface</item>
<item name="chat_me_color">@color/tusky_blue</item>
<item name="chat_me_text_color">@color/textColorPrimary</item>
<item name="chat_other_color">@color/colorPrimaryDark</item>
<item name="chat_other_text_color">@color/textColorPrimary</item>
<item name="chat_date_text_color">@color/textColorSecondary</item>
</style>
<style name="ViewMediaActivity.AppBarLayout" parent="ThemeOverlay.AppCompat">
@ -160,4 +165,18 @@
<item name="materialDrawerDrawCircularShadow">false</item>
</style>
<style name="TextAppearance.Chat" parent="TextAppearance.AppCompat" />
<style name="TextAppearance.Chat.Date">
<item name="android:textSize">?attr/status_text_small</item>
<item name="android:textColor">?attr/chat_date_text_color</item>
</style>
<style name="TextAppearance.Chat.Content">
<item name="android:textSize">?attr/status_text_medium</item>
</style>
<style name="TextAppearance.Chat.Content.Me">
<item name="android:textColor">@color/textColorPrimary</item>
</style>
<style name="TextAppearance.Chat.Content.Other">
<item name="android:textColor">@color/textColorPrimary</item>
</style>
</resources>

Loading…
Cancel
Save