chats: disable pagination for chat listing, another finalization of chat bubble design
This commit is contained in:
parent
bcfd202bbb
commit
69f37208a5
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 25,
|
||||
"identityHash": "322074fec4881114e2d85da67166e31f",
|
||||
"identityHash": "8c97dfd1b3d04602e25139ec97d2a282",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "TootEntity",
|
||||
@ -802,7 +802,7 @@
|
||||
},
|
||||
{
|
||||
"tableName": "ChatMessageEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT NOT NULL, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`localId` INTEGER NOT NULL, `messageId` TEXT NOT NULL, `content` TEXT, `chatId` TEXT NOT NULL, `accountId` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `attachment` TEXT, `emojis` TEXT NOT NULL, PRIMARY KEY(`localId`, `messageId`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "localId",
|
||||
@ -820,7 +820,7 @@
|
||||
"fieldPath": "content",
|
||||
"columnName": "content",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "chatId",
|
||||
@ -867,7 +867,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '322074fec4881114e2d85da67166e31f')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '8c97dfd1b3d04602e25139ec97d2a282')"
|
||||
]
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
<resources>
|
||||
<!-- HUSKY SPECIFIC STRINGS -->
|
||||
<string name="chats">Chats</string>
|
||||
<string name="chat_our_last_message">You: %s</string>
|
||||
<string name="action_mark_as_read">Mark as read</string>
|
||||
|
||||
<string name="action_reply_to">Reply to</string>
|
||||
@ -32,6 +33,11 @@
|
||||
<string name="pref_title_enable_big_emojis">Enable bigger custom emojis</string>
|
||||
<string name="pref_title_enable_experimental_stickers">Enable experimental Pleroma-FE stickers(if available)</string>
|
||||
|
||||
<string name="attachment_type_image">Image</string>
|
||||
<string name="attachment_type_video">Video</string>
|
||||
<string name="attachment_type_audio">Audio</string>
|
||||
<string name="attachment_type_unknown">Attachment</string>
|
||||
|
||||
<!-- REPLACEMENT FOR TUSKY STRINGS -->
|
||||
<string name="action_toggle_visibility">Post visibility</string>
|
||||
<string name="action_schedule_toot">Schedule post</string>
|
||||
@ -87,7 +93,6 @@
|
||||
<string name="title_scheduled_toot">Scheduled posts</string>
|
||||
<string name="title_reblogged_by">Repeated by</string>
|
||||
<string name="title_view_thread">Post</string>
|
||||
<string name="chat_message_hint_text">Just landed in L.A.</string>
|
||||
|
||||
<!--
|
||||
<string name="about_tusky_version">Husky %s</string>
|
||||
|
@ -1,27 +1,24 @@
|
||||
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 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.entity.ChatMessage
|
||||
import com.keylesspalace.tusky.interfaces.ChatActionListener
|
||||
import com.keylesspalace.tusky.util.*
|
||||
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 com.keylesspalace.tusky.viewdata.ChatViewData
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
@ -37,21 +34,22 @@ class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
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 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, statusDisplayOptions)
|
||||
setCreatedAt(msg.createdAt)
|
||||
} else {
|
||||
if(payload is List<*>) {
|
||||
for (item in payload) {
|
||||
if (ChatsViewHolder.Key.KEY_CREATED == item) {
|
||||
setCreatedAt(msg.createdAt, statusDisplayOptions)
|
||||
setCreatedAt(msg.createdAt)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -154,26 +152,8 @@ class ChatMessagesViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
private fun setCreatedAt(createdAt: Date) {
|
||||
timestamp.text = sdf.format(createdAt)
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,6 +222,6 @@ class ChatMessagesAdapter(private val dataSource : TimelineAdapter.AdapterDataSo
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return dataSource.getItemAt(position).getViewDataId()
|
||||
return dataSource.getItemAt(position).getViewDataId().toLong()
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.opengl.Visibility
|
||||
import android.text.TextUtils
|
||||
import android.text.format.DateUtils
|
||||
@ -38,16 +39,20 @@ class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
||||
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
|
||||
|
||||
fun setupWithChat(chat: ChatViewData.Concrete, listener: ChatActionListener, statusDisplayOptions: StatusDisplayOptions, payload: Any?) {
|
||||
if(payload == null) {
|
||||
fun setupWithChat(chat: ChatViewData.Concrete,
|
||||
listener: ChatActionListener,
|
||||
statusDisplayOptions: StatusDisplayOptions,
|
||||
localUserId: String,
|
||||
payload: Any?) {
|
||||
if (payload == null) {
|
||||
displayName.text = chat.account.displayName?.emojify(chat.account.emojis, displayName, true)
|
||||
?: ""
|
||||
userName.text = userName.context.getString(R.string.status_username_format, chat.account.username)
|
||||
setUpdatedAt(chat.updatedAt, statusDisplayOptions)
|
||||
setAvatar(chat.account.avatar, chat.account.bot, statusDisplayOptions)
|
||||
if(chat.unread <= 0) {
|
||||
if (chat.unread <= 0) {
|
||||
unread.visibility = View.GONE
|
||||
} else if(chat.unread > 99) {
|
||||
} else if (chat.unread > 99) {
|
||||
unread.text = ":)"
|
||||
} else {
|
||||
unread.text = chat.unread.toString()
|
||||
@ -59,7 +64,7 @@ class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
}
|
||||
val onClickListener = View.OnClickListener {
|
||||
val pos = adapterPosition
|
||||
if(pos != RecyclerView.NO_POSITION)
|
||||
if (pos != RecyclerView.NO_POSITION)
|
||||
listener.openChat(pos)
|
||||
}
|
||||
|
||||
@ -69,7 +74,19 @@ class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
itemView.setOnClickListener(onClickListener)
|
||||
|
||||
chat.lastMessage?.let {
|
||||
content.text = it.content.emojify(it.emojis, content, true)
|
||||
var text = if (it.content != null) {
|
||||
content.setTypeface(null, Typeface.NORMAL)
|
||||
|
||||
it.content.emojify(it.emojis, content, true)
|
||||
} else if (it.attachment != null) {
|
||||
content.setTypeface(null, Typeface.ITALIC)
|
||||
|
||||
content.resources.getString(it.attachment.describeAttachmentType())
|
||||
} else ""
|
||||
|
||||
content.text = if(it.accountId == localUserId) {
|
||||
content.resources.getString(R.string.chat_our_last_message).format(text)
|
||||
} else text
|
||||
}
|
||||
} else {
|
||||
if(payload is List<*>) {
|
||||
@ -127,7 +144,8 @@ class ChatsViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
class ChatsAdapter(private val dataSource: TimelineAdapter.AdapterDataSource<ChatViewData>,
|
||||
val statusDisplayOptions: StatusDisplayOptions,
|
||||
private val chatActionListener: ChatActionListener) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
private val chatActionListener: ChatActionListener,
|
||||
val localUserId: String) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
private val VIEW_TYPE_CHAT = 0
|
||||
private val VIEW_TYPE_PLACEHOLDER = 1
|
||||
@ -149,7 +167,8 @@ class ChatsAdapter(private val dataSource: TimelineAdapter.AdapterDataSource<Cha
|
||||
if(holder is PlaceholderViewHolder) {
|
||||
holder.setup(chatActionListener, (chat as ChatViewData.Placeholder).isLoading)
|
||||
} else if(holder is ChatsViewHolder) {
|
||||
holder.setupWithChat(chat as ChatViewData.Concrete, chatActionListener, statusDisplayOptions,
|
||||
holder.setupWithChat(chat as ChatViewData.Concrete, chatActionListener,
|
||||
statusDisplayOptions, localUserId,
|
||||
if (payloads != null && payloads.isNotEmpty()) payloads[0] else null)
|
||||
}
|
||||
}
|
||||
@ -175,6 +194,6 @@ class ChatsAdapter(private val dataSource: TimelineAdapter.AdapterDataSource<Cha
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return dataSource.getItemAt(position).getViewDataId()
|
||||
return dataSource.getItemAt(position).getViewDataId().toLong()
|
||||
}
|
||||
}
|
@ -11,38 +11,38 @@ 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.ChatMesssageOrPlaceholder
|
||||
import com.keylesspalace.tusky.repository.ChatRepository
|
||||
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.toolbar
|
||||
import androidx.arch.core.util.Function
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.*
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.repository.Placeholder
|
||||
import com.keylesspalace.tusky.repository.TimelineRequestMode
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.activity_chat.progressBar
|
||||
import kotlinx.android.synthetic.main.fragment_timeline.*
|
||||
import java.io.IOException
|
||||
import java.lang.Exception
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
class ChatActivity: BottomSheetActivity(),
|
||||
Injectable, ChatActionListener {
|
||||
private val TAG = "ChatsF" // logging tag
|
||||
private val TAG = "ChatsActivity" // logging tag
|
||||
private val LOAD_AT_ONCE = 30
|
||||
|
||||
@Inject
|
||||
@ -54,11 +54,21 @@ class ChatActivity: BottomSheetActivity(),
|
||||
|
||||
lateinit var adapter: ChatMessagesAdapter
|
||||
|
||||
private val msgs = PairedList<ChatMessageStatus, ChatMessageViewData?>(Function<ChatMessageStatus, ChatMessageViewData?> {input ->
|
||||
private val msgs = PairedList<ChatMesssageOrPlaceholder, ChatMessageViewData?>(Function<ChatMesssageOrPlaceholder, ChatMessageViewData?> { input ->
|
||||
input.asRightOrNull()?.let(ViewDataUtils::chatMessageToViewData) ?:
|
||||
ChatMessageViewData.Placeholder(input.asLeft().id, false)
|
||||
})
|
||||
|
||||
private var bottomLoading = false
|
||||
private var eventRegistered = false
|
||||
private var isNeedRefresh = false
|
||||
private var didLoadEverythingBottom = false
|
||||
private var initialUpdateFailed = false
|
||||
|
||||
private enum class FetchEnd {
|
||||
TOP, BOTTOM, MIDDLE
|
||||
}
|
||||
|
||||
private val listUpdateCallback = object : ListUpdateCallback {
|
||||
override fun onInserted(position: Int, count: Int) {
|
||||
Log.d(TAG, "onInserted")
|
||||
@ -119,7 +129,7 @@ class ChatActivity: BottomSheetActivity(),
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
chatId = intent.getStringExtra(ID)
|
||||
val chatId = intent.getStringExtra(ID)
|
||||
val avatarUrl = intent.getStringExtra(AVATAR_URL)
|
||||
val displayName = intent.getStringExtra(DISPLAY_NAME)
|
||||
val username = intent.getStringExtra(USERNAME)
|
||||
@ -128,6 +138,7 @@ class ChatActivity: BottomSheetActivity(),
|
||||
if(chatId == null || avatarUrl == null || displayName == null || username == null || emojis == null) {
|
||||
throw IllegalArgumentException("Can't open ChatActivity without chat id")
|
||||
}
|
||||
this.chatId = chatId
|
||||
|
||||
if(accountManager.activeAccount == null) {
|
||||
throw Exception("No active account!")
|
||||
@ -164,12 +175,16 @@ class ChatActivity: BottomSheetActivity(),
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
layoutManager.reverseLayout = true
|
||||
recycler.layoutManager = layoutManager
|
||||
recycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||
// recycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||
recycler.adapter = adapter
|
||||
|
||||
tryCache()
|
||||
}
|
||||
|
||||
private fun clearPlaceholdersForResponse(msgs: MutableList<ChatMesssageOrPlaceholder>) {
|
||||
msgs.removeAll { it.isLeft() }
|
||||
}
|
||||
|
||||
private fun tryCache() {
|
||||
// Request timeline from disk to make it quick, then replace it with timeline from
|
||||
// the server to update it
|
||||
@ -178,21 +193,293 @@ class ChatActivity: BottomSheetActivity(),
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { msgs ->
|
||||
if (msgs.size > 1) {
|
||||
val mutableChats = msgs.toMutableList()
|
||||
val mutableMsgs = msgs.toMutableList()
|
||||
clearPlaceholdersForResponse(mutableMsgs)
|
||||
this.msgs.clear()
|
||||
this.msgs.addAll(mutableChats)
|
||||
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()) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
this.msgs.addAll(messages)
|
||||
updateAdapter()
|
||||
}
|
||||
bottomLoading = false
|
||||
}, {
|
||||
initialUpdateFailed = true
|
||||
// Indicate that we are not loading anymore
|
||||
progressBar.visibility = View.GONE
|
||||
})
|
||||
}
|
||||
|
||||
private fun showNothing() {
|
||||
messageView.visibility = View.VISIBLE
|
||||
messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||
}
|
||||
|
||||
private fun loadAbove() {
|
||||
var firstOrNull: String? = null
|
||||
var secondOrNull: String? = null
|
||||
for (i in msgs.indices) {
|
||||
val msg = msgs[i]
|
||||
if (msg.isRight()) {
|
||||
firstOrNull = msg.asRight().id
|
||||
if (i + 1 < msgs.size && msgs[i + 1].isRight()) {
|
||||
secondOrNull = msgs[i + 1].asRight().id
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
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) {
|
||||
// allow getting old statuses/fallbacks for network only for for bottom loading
|
||||
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) })
|
||||
}
|
||||
|
||||
private fun updateAdapter() {
|
||||
Log.d(TAG, "updateAdapter")
|
||||
differ.submitList(msgs.pairedCopy)
|
||||
}
|
||||
|
||||
private fun updateMessages(newMsgs: MutableList<ChatMesssageOrPlaceholder>, fullFetch: Boolean) {
|
||||
if (newMsgs.isEmpty()) {
|
||||
updateAdapter()
|
||||
return
|
||||
}
|
||||
if (msgs.isEmpty()) {
|
||||
msgs.addAll(newMsgs)
|
||||
} else {
|
||||
val lastOfNew = newMsgs[newMsgs.size - 1]
|
||||
val index = msgs.indexOf(lastOfNew)
|
||||
if (index >= 0) {
|
||||
msgs.subList(0, index).clear()
|
||||
}
|
||||
val newIndex = newMsgs.indexOf(msgs[0])
|
||||
if (newIndex == -1) {
|
||||
if (index == -1 && fullFetch) {
|
||||
newMsgs.findLast { it.isRight() }?.let {
|
||||
val placeholderId = it.asRight().id.inc()
|
||||
newMsgs.add(Either.Left(Placeholder(placeholderId)))
|
||||
}
|
||||
}
|
||||
msgs.addAll(0, newMsgs)
|
||||
} else {
|
||||
msgs.addAll(0, newMsgs.subList(0, newIndex))
|
||||
}
|
||||
}
|
||||
// Remove all consecutive placeholders
|
||||
removeConsecutivePlaceholders()
|
||||
updateAdapter()
|
||||
}
|
||||
|
||||
private fun removeConsecutivePlaceholders() {
|
||||
for (i in 0 until msgs.size - 1) {
|
||||
if (msgs[i].isLeft() && msgs[i + 1].isLeft()) {
|
||||
msgs.removeAt(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun replacePlaceholderWithMessages(newMsgs: MutableList<ChatMesssageOrPlaceholder>,
|
||||
fullFetch: Boolean, pos: Int) {
|
||||
val placeholder = msgs[pos]
|
||||
if (placeholder.isLeft()) {
|
||||
msgs.removeAt(pos)
|
||||
}
|
||||
if (newMsgs.isEmpty()) {
|
||||
updateAdapter()
|
||||
return
|
||||
}
|
||||
if (fullFetch) {
|
||||
newMsgs.add(placeholder)
|
||||
}
|
||||
msgs.addAll(pos, newMsgs)
|
||||
removeConsecutivePlaceholders()
|
||||
updateAdapter()
|
||||
}
|
||||
|
||||
private fun addItems(newMsgs: List<ChatMesssageOrPlaceholder>) {
|
||||
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)) {
|
||||
msgs.addAll(newMsgs)
|
||||
removeConsecutivePlaceholders()
|
||||
updateAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFetchTimelineSuccess(msgs: MutableList<ChatMesssageOrPlaceholder>,
|
||||
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) {
|
||||
FetchEnd.TOP -> {
|
||||
updateMessages(msgs, fullFetch)
|
||||
}
|
||||
FetchEnd.MIDDLE -> {
|
||||
replacePlaceholderWithMessages(msgs, fullFetch, pos)
|
||||
}
|
||||
FetchEnd.BOTTOM -> {
|
||||
if (this.msgs.isNotEmpty() && !this.msgs.last().isRight()) {
|
||||
this.msgs.removeAt(this.msgs.size - 1)
|
||||
updateAdapter()
|
||||
}
|
||||
|
||||
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) {
|
||||
addItems(msgs)
|
||||
} else {
|
||||
updateMessages(msgs, fullFetch)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
updateBottomLoadingState(fetchEnd)
|
||||
progressBar.visibility = View.GONE
|
||||
if (this.msgs.size == 0) {
|
||||
showNothing()
|
||||
} else {
|
||||
messageView.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun onRefresh() {
|
||||
messageView.visibility = View.GONE
|
||||
isNeedRefresh = false
|
||||
|
||||
if (this.initialUpdateFailed) {
|
||||
updateCurrent()
|
||||
}
|
||||
loadAbove()
|
||||
}
|
||||
|
||||
private fun onFetchTimelineFailure(exception: Exception, fetchEnd: FetchEnd, position: Int) {
|
||||
topProgressBar.hide()
|
||||
if (fetchEnd == FetchEnd.MIDDLE && !msgs[position].isRight()) {
|
||||
var placeholder = msgs[position].asLeftOrNull()
|
||||
val newViewData: ChatMessageViewData
|
||||
if (placeholder == null) {
|
||||
val msg = msgs[position - 1].asRight()
|
||||
val newId = msg.id.dec()
|
||||
placeholder = Placeholder(newId)
|
||||
}
|
||||
newViewData = ChatMessageViewData.Placeholder(placeholder.id, false)
|
||||
msgs.setPairedItem(position, newViewData)
|
||||
updateAdapter()
|
||||
} else if (msgs.isEmpty()) {
|
||||
messageView.visibility = View.VISIBLE
|
||||
if (exception is IOException) {
|
||||
messageView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
onRefresh()
|
||||
}
|
||||
} else {
|
||||
messageView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
||||
progressBar.visibility = View.VISIBLE
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.e(TAG, "Fetch Failure: " + exception.message)
|
||||
updateBottomLoadingState(fetchEnd)
|
||||
progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun updateBottomLoadingState(fetchEnd: FetchEnd) {
|
||||
if (fetchEnd == FetchEnd.BOTTOM) {
|
||||
bottomLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLoadMore(position: Int) {
|
||||
//check bounds before accessing list,
|
||||
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")
|
||||
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 (id) = msgs[position].asLeft()
|
||||
val newViewData = ChatMessageViewData.Placeholder(id, true)
|
||||
msgs.setPairedItem(position, newViewData)
|
||||
updateAdapter()
|
||||
} else {
|
||||
Log.e(TAG, "error loading more")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
@ -203,6 +490,27 @@ class ChatActivity: BottomSheetActivity(),
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
startUpdateTimestamp()
|
||||
}
|
||||
|
||||
/**
|
||||
* Start to update adapter every minute to refresh timestamp
|
||||
* If setting absoluteTimeView is false
|
||||
* Auto dispose observable on pause
|
||||
*/
|
||||
private fun startUpdateTimestamp() {
|
||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
val useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||
if (!useAbsoluteTime) {
|
||||
Observable.interval(1, TimeUnit.MINUTES)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_PAUSE)
|
||||
.subscribe { updateAdapter() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAccount(id: String) {
|
||||
viewAccount(id)
|
||||
}
|
||||
|
@ -133,7 +133,6 @@ class MediaPreviewAdapter(
|
||||
view.marqueeRepeatLimit = -1
|
||||
view.setSingleLine()
|
||||
view.setSelected(true)
|
||||
view.maxLines = 1
|
||||
view.textSize = 16.0f
|
||||
view.setOnClickListener {
|
||||
onMediaClick(adapterPosition, view)
|
||||
|
@ -363,7 +363,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||
"PRIMARY KEY (`localId`, `chatId`))");
|
||||
database.execSQL("CREATE TABLE `ChatMessageEntity` (`localId` INTEGER NOT NULL," +
|
||||
"`messageId` TEXT NOT NULL," +
|
||||
"`content` TEXT NOT NULL," +
|
||||
"`content` TEXT," +
|
||||
"`chatId` TEXT NOT NULL," +
|
||||
"`accountId` TEXT NOT NULL," +
|
||||
"`createdAt` INTEGER NOT NULL," +
|
||||
|
@ -12,7 +12,7 @@ import androidx.room.Entity
|
||||
data class ChatMessageEntity(
|
||||
val localId: Long,
|
||||
val messageId: String,
|
||||
val content: String,
|
||||
val content: String?,
|
||||
val chatId: String,
|
||||
val accountId: String,
|
||||
val createdAt: Long,
|
||||
|
@ -28,7 +28,7 @@ ELSE 1 END)
|
||||
AND (CASE WHEN :sinceId IS NOT NULL THEN
|
||||
(LENGTH(c.chatId) > LENGTH(:sinceId) OR LENGTH(c.chatId) == LENGTH(:sinceId) AND c.chatId > :sinceId)
|
||||
ELSE 1 END)
|
||||
ORDER BY LENGTH(c.chatId) DESC, c.chatId DESC
|
||||
ORDER BY c.updatedAt DESC
|
||||
LIMIT :limit
|
||||
""")
|
||||
abstract fun getChatsForAccount(localId: Long, maxId: String?, sinceId: String?, limit: Int) : Single<List<ChatEntityWithAccount>>
|
||||
|
@ -60,8 +60,8 @@ class NetworkModule {
|
||||
addInterceptor(InstanceSwitchAuthInterceptor(accountManager))
|
||||
if (BuildConfig.DEBUG) {
|
||||
addInterceptor(HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
//level = HttpLoggingInterceptor.Level.HEADERS
|
||||
//level = HttpLoggingInterceptor.Level.BASIC
|
||||
level = HttpLoggingInterceptor.Level.HEADERS
|
||||
//level = HttpLoggingInterceptor.Level.BODY
|
||||
})
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonParseException
|
||||
import com.google.gson.annotations.JsonAdapter
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.R
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@ -62,6 +63,15 @@ data class Attachment(
|
||||
}
|
||||
}
|
||||
|
||||
fun describeAttachmentType() : Int {
|
||||
return when(type) {
|
||||
Type.IMAGE -> R.string.attachment_type_image
|
||||
Type.VIDEO, Type.GIFV -> R.string.attachment_type_video
|
||||
Type.AUDIO -> R.string.attachment_type_audio
|
||||
Type.UNKNOWN -> R.string.attachment_type_unknown
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The meta data of an [Attachment].
|
||||
*/
|
||||
|
@ -21,12 +21,13 @@ import java.util.*
|
||||
|
||||
data class ChatMessage(
|
||||
val id: String,
|
||||
val content: Spanned,
|
||||
val content: Spanned?,
|
||||
@SerializedName("chat_id") val chatId: String,
|
||||
@SerializedName("account_id") val accountId: String,
|
||||
@SerializedName("created_at") val createdAt: Date,
|
||||
val attachment: Attachment?,
|
||||
val emojis: List<Emoji>
|
||||
val emojis: List<Emoji>,
|
||||
val card: Card?
|
||||
)
|
||||
|
||||
data class Chat(
|
||||
|
@ -24,6 +24,7 @@ import com.keylesspalace.tusky.components.chat.ChatActivity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.Chat
|
||||
import com.keylesspalace.tusky.entity.NewChatMessage
|
||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.ChatActionListener
|
||||
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
||||
@ -51,6 +52,8 @@ import javax.inject.Inject
|
||||
class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, ReselectableFragment, ChatActionListener, OnRefreshListener {
|
||||
private val TAG = "ChatsF" // logging tag
|
||||
private val LOAD_AT_ONCE = 30
|
||||
private val BROKEN_PAGINATION_IN_BACKEND = true // break pagination until it's not fixed in plemora
|
||||
|
||||
|
||||
@Inject
|
||||
lateinit var eventHub: EventHub
|
||||
@ -159,7 +162,8 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
preferences.getBoolean("showBotOverlay", true),
|
||||
false, CardViewMode.NONE,false
|
||||
)
|
||||
adapter = ChatsAdapter(dataSource, statusDisplayOptions, this)
|
||||
|
||||
adapter = ChatsAdapter(dataSource, statusDisplayOptions, this, accountManager.activeAccount!!.accountId)
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
@ -177,7 +181,6 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled
|
||||
swipeRefreshLayout.setOnRefreshListener(this)
|
||||
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
||||
@ -198,32 +201,28 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
if (isNeedRefresh) onRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendInitialRequest() {
|
||||
// debug
|
||||
// sendFetchChatsRequest(null, null, null, FetchEnd.BOTTOM, -1)
|
||||
tryCache()
|
||||
}
|
||||
|
||||
private fun clearPlaceholdersForResponse(chats: MutableList<Either<Placeholder, Chat>>) {
|
||||
chats.removeAll { it.isLeft() }
|
||||
}
|
||||
|
||||
private fun tryCache() {
|
||||
// Request timeline from disk to make it quick, then replace it with timeline from
|
||||
// the server to update it
|
||||
chatRepo.getChats(null, null, null, LOAD_AT_ONCE, TimelineRequestMode.DISK)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe { chats ->
|
||||
if (chats.size > 1) {
|
||||
val mutableChats = chats.toMutableList()
|
||||
clearPlaceholdersForResponse(mutableChats)
|
||||
this.chats.clear()
|
||||
this.chats.addAll(mutableChats)
|
||||
.subscribe { newChats ->
|
||||
if (newChats.size > 1) {
|
||||
val mutableChats = newChats.toMutableList()
|
||||
mutableChats.removeAll { it.isLeft() }
|
||||
|
||||
chats.clear()
|
||||
chats.addAll(mutableChats)
|
||||
|
||||
updateAdapter()
|
||||
progressBar.visibility = View.GONE
|
||||
// Request statuses including current top to refresh all of them
|
||||
}
|
||||
updateCurrent()
|
||||
loadAbove()
|
||||
@ -231,36 +230,40 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
}
|
||||
|
||||
private fun updateCurrent() {
|
||||
if (chats.isEmpty()) {
|
||||
if (!BROKEN_PAGINATION_IN_BACKEND && chats.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
val topId = chats.first { it.isRight() }.asRight().id
|
||||
val topId = chats.firstOrNull { it.isRight() }?.asRight()?.id
|
||||
chatRepo.getChats(topId, null, null, LOAD_AT_ONCE, TimelineRequestMode.NETWORK)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||
.subscribe({ chats ->
|
||||
.subscribe({ newChats ->
|
||||
initialUpdateFailed = false
|
||||
// When cached timeline is too old, we would replace it with nothing
|
||||
if (chats.isNotEmpty()) {
|
||||
if (newChats.isNotEmpty()) {
|
||||
// clear old cached statuses
|
||||
if(this.chats.isNotEmpty()) {
|
||||
this.chats.removeAll {
|
||||
if(it.isRight()) {
|
||||
val chat = it.asRight()
|
||||
chat.id.length < topId.length || chat.id < topId
|
||||
if(BROKEN_PAGINATION_IN_BACKEND) {
|
||||
chats.clear()
|
||||
} else {
|
||||
val placeholder = it.asLeft()
|
||||
placeholder.id.length < topId.length || placeholder.id < topId
|
||||
chats.removeAll {
|
||||
if(it.isLeft()) {
|
||||
val p = it.asLeft()
|
||||
p.id.length < topId!!.length || p.id < topId
|
||||
} else {
|
||||
val c = it.asRight()
|
||||
c.id.length < topId!!.length || c.id < topId
|
||||
}
|
||||
}
|
||||
}
|
||||
this.chats.addAll(chats)
|
||||
chats.addAll(newChats)
|
||||
updateAdapter()
|
||||
}
|
||||
bottomLoading = false
|
||||
},
|
||||
{
|
||||
// Indicate that we are not loading anymore
|
||||
progressBar.visibility = View.GONE
|
||||
swipeRefreshLayout.isRefreshing = false
|
||||
}, {
|
||||
initialUpdateFailed = true
|
||||
// Indicate that we are not loading anymore
|
||||
progressBar.visibility = View.GONE
|
||||
@ -377,6 +380,11 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
}
|
||||
|
||||
private fun loadAbove() {
|
||||
if(BROKEN_PAGINATION_IN_BACKEND) {
|
||||
updateCurrent()
|
||||
return
|
||||
}
|
||||
|
||||
var firstOrNull: String? = null
|
||||
var secondOrNull: String? = null
|
||||
for (i in chats.indices) {
|
||||
@ -397,6 +405,10 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
}
|
||||
|
||||
private fun onLoadMore() {
|
||||
if (BROKEN_PAGINATION_IN_BACKEND)
|
||||
updateCurrent()
|
||||
return
|
||||
|
||||
if (didLoadEverythingBottom || bottomLoading) {
|
||||
return
|
||||
}
|
||||
@ -458,7 +470,7 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
val newIndex = newChats.indexOf(chats[0])
|
||||
if (newIndex == -1) {
|
||||
if (index == -1 && fullFetch) {
|
||||
newChats.findLast { it.isRight() }?.let {
|
||||
newChats.last { it.isRight() }.let {
|
||||
val placeholderId = it.asRight().id.inc()
|
||||
newChats.add(Left(Placeholder(placeholderId)))
|
||||
}
|
||||
@ -723,6 +735,9 @@ class ChatsFragment : BaseFragment(), Injectable, RefreshableFragment, Reselecta
|
||||
}
|
||||
|
||||
override fun openChat(position: Int) {
|
||||
if(position < 0 || position >= chats.size)
|
||||
return
|
||||
|
||||
val chat = chats[position].asRightOrNull()
|
||||
chat?.let {
|
||||
bottomSheetActivity.openChat(it)
|
||||
|
@ -1,6 +1,5 @@
|
||||
package com.keylesspalace.tusky.repository
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.toHtml
|
||||
@ -19,17 +18,15 @@ import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
typealias ChatStatus = Either<Placeholder, Chat>
|
||||
typealias ChatMessageStatus = Either<Placeholder, ChatMessage>
|
||||
typealias ChatMesssageOrPlaceholder = 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>>
|
||||
fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMesssageOrPlaceholder>>
|
||||
}
|
||||
|
||||
class ChatRepositoryImpl(
|
||||
@ -52,10 +49,16 @@ class ChatRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
override fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMessageStatus>> {
|
||||
override fun getChatMessages(chatId: String, maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int, requestMode: TimelineRequestMode) : Single<out List<ChatMesssageOrPlaceholder>> {
|
||||
val acc = accountManager.activeAccount ?: throw IllegalStateException()
|
||||
val accountId = acc.id
|
||||
|
||||
/*return if (requestMode == DISK) {
|
||||
getChatMessagesFromDb(chatId, accountId, maxId, sinceId, limit)
|
||||
} else {
|
||||
getChatMessagesFromNetwork(chatId, maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode)
|
||||
}*/
|
||||
|
||||
return getChatMessagesFromNetwork(chatId, maxId, sinceId, sincedIdMinusOne, limit, accountId, requestMode)
|
||||
}
|
||||
|
||||
@ -82,7 +85,7 @@ class ChatRepositoryImpl(
|
||||
private fun getChatMessagesFromNetwork(chatId: String, maxId: String?, sinceId: String?,
|
||||
sinceIdMinusOne: String?, limit: Int,
|
||||
accountId: Long, requestMode: TimelineRequestMode
|
||||
): Single<out List<ChatMessageStatus>> {
|
||||
): Single<out List<ChatMesssageOrPlaceholder>> {
|
||||
return mastodonApi.getChatMessages(chatId, maxId, null, sinceIdMinusOne, 0, limit + 1).map {
|
||||
it.mapTo(mutableListOf(), ChatMessage::lift)
|
||||
}
|
||||
@ -123,6 +126,7 @@ class ChatRepositoryImpl(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun saveChatsToDb(accountId: Long, chats: List<Chat>,
|
||||
maxId: String?, sinceId: String?
|
||||
): List<ChatStatus> {
|
||||
@ -209,7 +213,7 @@ fun ChatMessage.toEntity(timelineUserId: Long, gson: Gson) : ChatMessageEntity {
|
||||
return ChatMessageEntity(
|
||||
localId = timelineUserId,
|
||||
messageId = this.id,
|
||||
content = this.content.toHtml(),
|
||||
content = this.content?.toHtml(),
|
||||
chatId = this.chatId,
|
||||
accountId = this.accountId,
|
||||
createdAt = this.createdAt.time,
|
||||
@ -225,19 +229,20 @@ fun Chat.toEntity(timelineUserId: Long, gson: Gson): Pair<ChatEntity, ChatMessag
|
||||
accountId = this.account.id,
|
||||
unread = this.unread,
|
||||
updatedAt = this.updatedAt.time,
|
||||
lastMessageId = this.lastMessage?.let { it.id }
|
||||
lastMessageId = this.lastMessage?.id
|
||||
), this.lastMessage?.toEntity(timelineUserId, gson))
|
||||
}
|
||||
|
||||
fun ChatMessageEntity.toChatMessage(gson: Gson) : ChatMessage {
|
||||
return ChatMessage(
|
||||
id = this.messageId,
|
||||
content = this.content.parseAsHtml().trimTrailingWhitespace(),
|
||||
content = this.content?.let { it.parseAsHtml().trimTrailingWhitespace() },
|
||||
chatId = this.chatId,
|
||||
accountId = this.accountId,
|
||||
createdAt = Date(this.createdAt),
|
||||
attachment = this.attachment?.let { gson.fromJson(it, Attachment::class.java) },
|
||||
emojis = gson.fromJson(this.emojis, object : TypeToken<List<Emoji>>() {}.type )
|
||||
emojis = gson.fromJson(this.emojis, object : TypeToken<List<Emoji>>() {}.type ),
|
||||
card = null /* don't care about card */
|
||||
)
|
||||
}
|
||||
|
||||
@ -254,6 +259,6 @@ fun ChatEntityWithAccount.toChat(gson: Gson) : ChatStatus {
|
||||
).lift()
|
||||
}
|
||||
|
||||
fun ChatMessage.lift(): ChatMessageStatus = Either.Right(this)
|
||||
fun ChatMessage.lift(): ChatMesssageOrPlaceholder = Either.Right(this)
|
||||
|
||||
fun Chat.lift(): ChatStatus = Either.Right(this)
|
||||
|
@ -104,7 +104,8 @@ public final class ViewDataUtils {
|
||||
msg.getAccountId(),
|
||||
msg.getCreatedAt(),
|
||||
msg.getAttachment(),
|
||||
msg.getEmojis()
|
||||
msg.getEmojis(),
|
||||
msg.getCard()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,15 @@
|
||||
package com.keylesspalace.tusky.viewdata
|
||||
|
||||
import android.text.Spanned
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.Attachment
|
||||
import com.keylesspalace.tusky.entity.Card
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import java.util.*
|
||||
|
||||
|
||||
abstract class ChatViewData {
|
||||
abstract fun getViewDataId() : Long
|
||||
abstract fun getViewDataId() : Int
|
||||
abstract fun deepEquals(o: ChatViewData) : Boolean
|
||||
|
||||
class Concrete(val account : Account,
|
||||
@ -14,8 +17,8 @@ abstract class ChatViewData {
|
||||
val unread: Long,
|
||||
val lastMessage: ChatMessageViewData.Concrete?,
|
||||
val updatedAt: Date ) : ChatViewData() {
|
||||
override fun getViewDataId(): Long {
|
||||
return id.hashCode().toLong()
|
||||
override fun getViewDataId(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
override fun deepEquals(o: ChatViewData): Boolean {
|
||||
@ -30,34 +33,55 @@ abstract class ChatViewData {
|
||||
override fun hashCode(): Int {
|
||||
return Objects.hash(account, id, unread, lastMessage, updatedAt)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
return deepEquals(other as Concrete)
|
||||
}
|
||||
}
|
||||
|
||||
class Placeholder(val id: String, val isLoading: Boolean) : ChatViewData() {
|
||||
override fun getViewDataId(): Long {
|
||||
return id.hashCode().toLong()
|
||||
override fun getViewDataId(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
override fun deepEquals(o: ChatViewData): Boolean {
|
||||
if( o !is Placeholder ) return false
|
||||
return o.isLoading == isLoading && o.id == id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = if (isLoading) 1 else 0
|
||||
result = 31 * result + id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
return deepEquals(other as Placeholder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class ChatMessageViewData {
|
||||
abstract fun getViewDataId() : Long
|
||||
abstract fun getViewDataId() : Int
|
||||
abstract fun deepEquals(o: ChatMessageViewData) : Boolean
|
||||
|
||||
class Concrete(val id: String,
|
||||
val content: Spanned,
|
||||
val content: Spanned?,
|
||||
val chatId: String,
|
||||
val accountId: String,
|
||||
val createdAt: Date,
|
||||
val attachment: Attachment?,
|
||||
val emojis: List<Emoji>) : ChatMessageViewData()
|
||||
val emojis: List<Emoji>,
|
||||
val card: Card?) : ChatMessageViewData()
|
||||
{
|
||||
override fun getViewDataId(): Long {
|
||||
return id.hashCode().toLong()
|
||||
override fun getViewDataId(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
override fun deepEquals(o: ChatMessageViewData): Boolean {
|
||||
@ -70,21 +94,42 @@ abstract class ChatMessageViewData {
|
||||
&& Objects.equals(o.createdAt, createdAt)
|
||||
&& Objects.equals(o.attachment, attachment)
|
||||
&& Objects.equals(o.emojis, emojis)
|
||||
&& Objects.equals(o.card, card)
|
||||
}
|
||||
|
||||
override fun hashCode() : Int {
|
||||
return Objects.hash(id, content, chatId, accountId, createdAt, attachment)
|
||||
return Objects.hash(id, content, chatId, accountId, createdAt, attachment, card)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
return deepEquals(other as Concrete)
|
||||
}
|
||||
}
|
||||
|
||||
class Placeholder(val id: String, val isLoading: Boolean) : ChatMessageViewData() {
|
||||
override fun getViewDataId(): Long {
|
||||
return id.hashCode().toLong()
|
||||
override fun getViewDataId(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
override fun deepEquals(o: ChatMessageViewData): Boolean {
|
||||
if( o !is Placeholder) return false
|
||||
return o.isLoading == isLoading && o.id == id
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = if (isLoading) 1 else 0
|
||||
result = 31 * result + id.hashCode()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
return deepEquals(other as Placeholder)
|
||||
}
|
||||
}
|
||||
}
|
@ -157,7 +157,7 @@
|
||||
android:textSize="?attr/status_text_large"
|
||||
android:singleLine="false"
|
||||
android:background="@null"
|
||||
android:hint="@string/chat_message_hint_text"
|
||||
android:hint="@string/hint_compose"
|
||||
app:layout_constraintEnd_toStartOf="@+id/emojiButton"
|
||||
app:layout_constraintRight_toLeftOf="@id/emojiButton"
|
||||
app:layout_constraintStart_toEndOf="@+id/attachmentButton"
|
||||
|
@ -5,29 +5,31 @@
|
||||
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">
|
||||
android:layout_marginEnd="@dimen/chat_message_h_padding"
|
||||
android:layout_marginBottom="@dimen/chat_message_v_padding">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentLayout"
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@drawable/message_background"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:backgroundTint="@color/colorBackgroundAccent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
app:layout_constraintWidth_max="wrap"
|
||||
app:layout_constraintWidth_percent="0.8"
|
||||
>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/attachmentLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="@dimen/chat_message_max_width"
|
||||
android:layout_height="@dimen/chat_media_preview_item_height"
|
||||
android:layout_marginTop="@dimen/chat_message_h_padding"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
>
|
||||
|
||||
<com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||
android:id="@+id/attachment"
|
||||
@ -42,32 +44,33 @@
|
||||
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_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:textColor="@color/textColorPrimary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
tools:text="AAAAAAAA" />
|
||||
</LinearLayout>
|
||||
android:paddingStart="@dimen/chat_message_h_padding"
|
||||
android:paddingEnd="50dp"
|
||||
android:paddingBottom="@dimen/chat_message_v_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/attachmentLayout"
|
||||
app:layout_constraintBottom_toBottomOf="@id/datetime"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="MeowMeowMeow" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/datetime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
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"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="12:39"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -5,38 +5,37 @@
|
||||
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">
|
||||
android:layout_marginStart="@dimen/chat_message_h_padding"
|
||||
android:layout_marginBottom="@dimen/chat_message_h_padding">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/contentLayout"
|
||||
android:layout_width="wrap_content"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="0dp"
|
||||
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"
|
||||
app:layout_constraintWidth_max="wrap"
|
||||
app:layout_constraintWidth_percent="0.8"
|
||||
>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/attachmentLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="@dimen/chat_message_max_width"
|
||||
android:layout_height="@dimen/chat_media_preview_item_height"
|
||||
android:layout_marginTop="@dimen/chat_message_h_padding"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
tools:visibility="visible"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
>
|
||||
|
||||
<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"
|
||||
/>
|
||||
tools:src="@drawable/elephant_friend_empty" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediaOverlay"
|
||||
@ -45,32 +44,33 @@
|
||||
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>
|
||||
android:textSize="?attr/status_text_large"
|
||||
android:paddingStart="@dimen/chat_message_h_padding"
|
||||
android:paddingEnd="50dp"
|
||||
android:paddingBottom="@dimen/chat_message_v_padding"
|
||||
app:layout_constraintTop_toBottomOf="@id/attachmentLayout"
|
||||
app:layout_constraintBottom_toBottomOf="@id/datetime"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
tools:text="MeowMeowMeow" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/datetime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
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"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:text="12:39"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -62,4 +62,5 @@
|
||||
<dimen name="chat_small_padding">8dp</dimen>
|
||||
<dimen name="chat_radius_fix">12dp</dimen>
|
||||
<dimen name="chat_media_preview_item_height">160dp</dimen>
|
||||
<dimen name="chat_message_max_width">300dp</dimen>
|
||||
</resources>
|
||||
|
Loading…
Reference in New Issue
Block a user