chats: chat message adapter, partially implement activity
This commit is contained in:
parent
26f365ebc8
commit
8dc15880b0
|
@ -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…
Reference in New Issue