feat: 新增搜索历史消息功能
- 新增搜索历史消息功能 - 修复部分问题
This commit is contained in:
parent
ec9e3a8af4
commit
b8c80e4b00
@ -2,7 +2,19 @@
|
||||
<project version="4">
|
||||
<component name="GitCommitMessageStorage">
|
||||
<option name="messageStorage">
|
||||
<MessageStorage />
|
||||
<MessageStorage>
|
||||
<option name="commitTemplate">
|
||||
<CommitTemplate>
|
||||
<option name="body" value="" />
|
||||
<option name="changes" value="" />
|
||||
<option name="closes" value="" />
|
||||
<option name="scope" value="" />
|
||||
<option name="skipCi" value="" />
|
||||
<option name="subject" value="" />
|
||||
<option name="type" value="feat" />
|
||||
</CommitTemplate>
|
||||
</option>
|
||||
</MessageStorage>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
@ -52,4 +52,13 @@ object LocalDatabase {
|
||||
val offset = 0
|
||||
return query.find(offset.toLong(), limit)
|
||||
}
|
||||
|
||||
fun getAllHistoryMessages(contactId: String, msgLocalId: Long): List<Messages> {
|
||||
val query = messagesBox
|
||||
.query(Messages_.takerId.equal(contactId))
|
||||
.greaterOrEqual(Messages_.msgLocalId, msgLocalId)
|
||||
.order(Messages_.timestamp, QueryBuilder.DESCENDING)
|
||||
.build()
|
||||
return query.find()
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.kaixed.kchat.data.model.search
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/12/11 16:47
|
||||
*/
|
||||
data class ChatHistoryItem(
|
||||
var msgLocalId: Long,
|
||||
var content: String,
|
||||
var timestamp: Long,
|
||||
var isMine: Boolean
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
package com.kaixed.kchat.manager
|
||||
|
||||
import com.kaixed.kchat.data.LocalDatabase
|
||||
import com.kaixed.kchat.data.LocalDatabase.getAllHistoryMessages
|
||||
import com.kaixed.kchat.data.LocalDatabase.getMessagesWithContact
|
||||
import com.kaixed.kchat.data.LocalDatabase.getMoreMessages
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
@ -13,7 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
*/
|
||||
object MessagesManager {
|
||||
|
||||
private const val LIMIT = 10L
|
||||
private const val LIMIT = 20L
|
||||
|
||||
private val _messages = MutableStateFlow<List<Messages>>(emptyList())
|
||||
|
||||
@ -24,6 +25,13 @@ object MessagesManager {
|
||||
private var loading = false
|
||||
private var tempIndex: Long = 0
|
||||
|
||||
fun queryHistory(msgLocalId: Long): Int {
|
||||
_messages.value = emptyList()
|
||||
val msg = getAllHistoryMessages(contactId, msgLocalId)
|
||||
_messages.value = msg
|
||||
return msg.size
|
||||
}
|
||||
|
||||
fun setContactId(contactId: String) {
|
||||
this.contactId = contactId
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
package com.kaixed.kchat.processor
|
||||
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import io.objectbox.Box
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/12/12 15:14
|
||||
*/
|
||||
object MessageProcessor {
|
||||
|
||||
private const val INTERVAL = 5
|
||||
|
||||
private val messagesBox: Box<Messages> by lazy { getBox(Messages::class.java) }
|
||||
|
||||
fun processorMsg(messages: Messages): Messages {
|
||||
val curTimestamp = System.currentTimeMillis()
|
||||
val localMsg = messagesBox.query(Messages_.takerId.equal(messages.takerId))
|
||||
.lessOrEqual(Messages_.timestamp, curTimestamp)
|
||||
.orderDesc(Messages_.timestamp).build().findFirst()
|
||||
|
||||
var showTimer = true
|
||||
localMsg?.let {
|
||||
showTimer = curTimestamp - it.timestamp >= INTERVAL * 1L * 60 * 1000
|
||||
}
|
||||
|
||||
messages.isShowTimer = showTimer
|
||||
return messages
|
||||
}
|
||||
}
|
@ -19,9 +19,11 @@ import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET
|
||||
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL
|
||||
import com.kaixed.kchat.network.OkhttpHelper
|
||||
import com.kaixed.kchat.processor.MessageProcessor
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_COMMON_DATA
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getCurrentContactId
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||
import com.kaixed.kchat.utils.SingleLiveEvent
|
||||
import com.tencent.mmkv.MMKV
|
||||
import io.objectbox.Box
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -60,9 +62,9 @@ class WebSocketService : Service() {
|
||||
|
||||
private val serviceScope = CoroutineScope(IO + serviceJob)
|
||||
|
||||
private val _messagesMutableLiveData = MutableLiveData<Messages?>()
|
||||
private val _messagesMutableLiveData = SingleLiveEvent<Messages?>()
|
||||
|
||||
val messageLivedata: LiveData<Messages?> get() = _messagesMutableLiveData
|
||||
val messageLivedata: SingleLiveEvent<Messages?> get() = _messagesMutableLiveData
|
||||
|
||||
private val _conversations = MutableLiveData<List<Conversation>?>()
|
||||
|
||||
@ -233,6 +235,7 @@ class WebSocketService : Service() {
|
||||
} else {
|
||||
messages.takerId = messages.senderId
|
||||
_messagesMutableLiveData.postValue(messages)
|
||||
MessageProcessor.processorMsg(messages)
|
||||
messagesBox.put(messages)
|
||||
updateConversationList(messages)
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.model.FunctionItem
|
||||
import com.kaixed.kchat.databinding.ActivityChatBinding
|
||||
import com.kaixed.kchat.manager.MessagesManager
|
||||
import com.kaixed.kchat.processor.MessageProcessor
|
||||
import com.kaixed.kchat.service.WebSocketService
|
||||
import com.kaixed.kchat.service.WebSocketService.LocalBinder
|
||||
import com.kaixed.kchat.ui.adapter.ChatAdapter
|
||||
@ -45,6 +46,7 @@ import com.kaixed.kchat.ui.widget.LoadingDialogFragment
|
||||
import com.kaixed.kchat.utils.Constants.CURRENT_CONTACT_ID
|
||||
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
|
||||
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getAvatarUrl
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getKeyboardHeight
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||
import com.kaixed.kchat.utils.ImageEngines
|
||||
@ -91,6 +93,10 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
|
||||
private val fileViewModel: FileViewModel by viewModels()
|
||||
|
||||
private var isSearchHistory = false
|
||||
|
||||
private var msgLocalId = 0L
|
||||
|
||||
companion object {
|
||||
private const val UNBLOCK_DELAY_TIME = 200L
|
||||
}
|
||||
@ -109,6 +115,17 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
setPanelChange()
|
||||
getKeyBoardVisibility()
|
||||
observeViewModel()
|
||||
if (isSearchHistory) {
|
||||
val size = MessagesManager.queryHistory(msgLocalId)
|
||||
binding.recycleChatList.smoothScrollToPosition(size - 1)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (!isSearchHistory) {
|
||||
MessagesManager.firstLoadMessages()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeViewModel() {
|
||||
@ -376,8 +393,11 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
contactId = intent.getStringExtra("contactId").toString()
|
||||
contactNickname = intent.getStringExtra("contactNickname")
|
||||
contactId = intent?.getStringExtra("contactId").toString()
|
||||
contactNickname = intent?.getStringExtra("contactNickname")
|
||||
isSearchHistory = intent?.getBooleanExtra("isSearchHistory", false) == true
|
||||
msgLocalId = intent.getLongExtra("msgLocalId", 0)
|
||||
|
||||
binding.ctb.setTitleName(contactNickname!!)
|
||||
|
||||
softKeyboardHeight = getKeyboardHeight()
|
||||
@ -414,7 +434,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
|
||||
private fun handleMsg(messages: Messages) {
|
||||
MessagesManager.receiveMessage(messages)
|
||||
// messagesViewModel.receiveMessage(messages)
|
||||
binding.recycleChatList.smoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
@ -424,10 +443,11 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
timestamp = System.currentTimeMillis(),
|
||||
senderId = username,
|
||||
takerId = talkerId,
|
||||
avatarUrl = getAvatarUrl(),
|
||||
type = type,
|
||||
)
|
||||
|
||||
val id: Long = messagesBox.put(messages)
|
||||
val msg = MessageProcessor.processorMsg(messages)
|
||||
val id: Long = messagesBox.put(msg)
|
||||
|
||||
val jsonObject = JSONObject()
|
||||
val jsonObject2 = JSONObject()
|
||||
@ -445,7 +465,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
|
||||
binding.etInput.setText("")
|
||||
}
|
||||
|
||||
|
||||
private fun bindWebSocketService() {
|
||||
bindService(
|
||||
Intent(
|
||||
|
@ -3,7 +3,6 @@ package com.kaixed.kchat.ui.activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.bumptech.glide.Glide
|
||||
import com.kaixed.kchat.data.LocalDatabase.getContactByUsername
|
||||
import com.kaixed.kchat.databinding.ActivityChatDetailBinding
|
||||
@ -54,7 +53,11 @@ class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
|
||||
}
|
||||
|
||||
binding.ciSearchChatHistory.setOnClickListener {
|
||||
val intent = Intent(this, SearchChatHistory::class.java)
|
||||
val intent = Intent(this, SearchChatHistory::class.java).apply {
|
||||
putExtra("contactNickname", contact?.remark ?: contact?.nickname)
|
||||
putExtra("contactAvatarUrl", contact?.avatarUrl)
|
||||
putExtra("contactId", contactId)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,6 @@ class ContactPermissionActivity : BaseActivity<ActivityContactPermissionBinding>
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
|
@ -1,16 +1,49 @@
|
||||
package com.kaixed.kchat.ui.activity
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import com.kaixed.kchat.R
|
||||
import androidx.core.text.buildSpannedString
|
||||
import androidx.core.text.inSpans
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.kaixed.kchat.data.local.box.ObjectBox.getBox
|
||||
import com.kaixed.kchat.data.local.entity.Messages
|
||||
import com.kaixed.kchat.data.local.entity.Messages_
|
||||
import com.kaixed.kchat.data.model.search.ChatHistoryItem
|
||||
import com.kaixed.kchat.databinding.ActivitySearchChatHistoryBinding
|
||||
import com.kaixed.kchat.ui.adapter.ChatHistoryAdapter
|
||||
import com.kaixed.kchat.ui.base.BaseActivity
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||
import io.objectbox.Box
|
||||
|
||||
class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
|
||||
companion object {
|
||||
private const val VIEW_CHAT_HISTORY = 0
|
||||
private const val VIEW_SEARCH_FAILED = 1
|
||||
private const val VIEW_SELECT = 2
|
||||
}
|
||||
|
||||
private val messageBox: Box<Messages> by lazy { getBox(Messages::class.java) }
|
||||
|
||||
private val chatHistoryAdapter by lazy {
|
||||
ChatHistoryAdapter(
|
||||
contactAvatarUrl,
|
||||
contactNickname,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
private var contactAvatarUrl: String = ""
|
||||
|
||||
private var contactNickname: String = ""
|
||||
|
||||
private var contactId: String = ""
|
||||
|
||||
override fun inflateBinding(): ActivitySearchChatHistoryBinding {
|
||||
return ActivitySearchChatHistoryBinding.inflate(layoutInflater)
|
||||
}
|
||||
@ -18,10 +51,93 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
setContentView(R.layout.activity_search_chat_history)
|
||||
setListener()
|
||||
setupRecyclerView()
|
||||
showView(VIEW_SELECT)
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
binding.rvChatHistory.layoutManager = LinearLayoutManager(this)
|
||||
binding.rvChatHistory.adapter = chatHistoryAdapter
|
||||
chatHistoryAdapter.setContactId(contactId)
|
||||
|
||||
chatHistoryAdapter.let { adapter ->
|
||||
val viewHolder = adapter.createViewHolder(binding.rvChatHistory, adapter.getItemViewType(0))
|
||||
val itemView = viewHolder.itemView
|
||||
itemView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(binding.rvChatHistory.width, View.MeasureSpec.EXACTLY),
|
||||
View.MeasureSpec.UNSPECIFIED
|
||||
)
|
||||
val itemHeight = itemView.measuredHeight
|
||||
Log.d("RecyclerView", "单个 Item 高度: $itemHeight")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun setListener() {
|
||||
binding.tvCancel.setOnClickListener { finish() }
|
||||
binding.etSearch.addTextChangedListener(afterTextChanged =
|
||||
{
|
||||
if (it?.length == 0) {
|
||||
chatHistoryAdapter.submitList(null)
|
||||
showView(VIEW_SELECT)
|
||||
} else {
|
||||
getData(it.toString())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun showView(viewToShowIndex: Int) {
|
||||
val viewList = listOf(binding.rvChatHistory, binding.tvSearchFailed, binding.clSelect)
|
||||
viewList.forEachIndexed { index, view ->
|
||||
view.visibility = if (index == viewToShowIndex) View.VISIBLE else View.INVISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
private fun getData(matchedField: String) {
|
||||
val data = loadDataFromDb(matchedField)
|
||||
|
||||
val items = data.map {
|
||||
ChatHistoryItem(it.msgLocalId, it.content, it.timestamp, it.senderId == getUsername())
|
||||
}
|
||||
|
||||
chatHistoryAdapter.setMatchedField(matchedField)
|
||||
chatHistoryAdapter.submitList(null)
|
||||
chatHistoryAdapter.submitList(items)
|
||||
|
||||
when (items.isEmpty()) {
|
||||
true -> {
|
||||
val text = createEmptyMessage(matchedField)
|
||||
binding.tvSearchFailed.text = text
|
||||
showView(VIEW_SEARCH_FAILED)
|
||||
}
|
||||
|
||||
false -> {
|
||||
showView(VIEW_CHAT_HISTORY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEmptyMessage(matchedField: String): CharSequence {
|
||||
return buildSpannedString {
|
||||
append("没有找到与")
|
||||
append("“")
|
||||
inSpans(ForegroundColorSpan(Color.parseColor("#07C160"))) {
|
||||
append(matchedField)
|
||||
}
|
||||
append("”")
|
||||
append("相关的聊天记录")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadDataFromDb(content: String): List<Messages> {
|
||||
return messageBox.query(Messages_.content.contains(content)).orderDesc(Messages_.timestamp)
|
||||
.build().find()
|
||||
}
|
||||
|
||||
override fun initData() {
|
||||
|
||||
contactNickname = intent?.getStringExtra("contactNickname") ?: ""
|
||||
contactAvatarUrl = intent?.getStringExtra("contactAvatarUrl") ?: ""
|
||||
contactId = intent?.getStringExtra("contactId") ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import com.kaixed.kchat.databinding.ChatRecycleItemImageNormalBinding
|
||||
import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding
|
||||
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
|
||||
import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow
|
||||
import com.kaixed.kchat.utils.TextUtil
|
||||
import com.kaixed.kchat.utils.TextUtil.extractDimensionsAndPrefix
|
||||
import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility
|
||||
import com.kaixed.kchat.utils.ViewUtil.setViewVisibility
|
||||
@ -42,25 +43,12 @@ class ChatAdapter(
|
||||
|
||||
companion object {
|
||||
const val CUSTOM = 0
|
||||
|
||||
// 提示消息
|
||||
const val TIP = 2
|
||||
|
||||
// 图片消息
|
||||
const val IMAGE = 4
|
||||
|
||||
// 语音消息
|
||||
const val VOICE = 6
|
||||
|
||||
// 位置消息
|
||||
const val LOCATION = 8
|
||||
|
||||
// 表情消息
|
||||
const val EMOJI = 10
|
||||
|
||||
// 红包消息
|
||||
const val RED_PACKET = 12
|
||||
|
||||
private const val TAG = "ChatAdapter"
|
||||
}
|
||||
|
||||
@ -68,25 +56,22 @@ class ChatAdapter(
|
||||
return when (viewType) {
|
||||
CUSTOM -> {
|
||||
CustomViewHolder(
|
||||
ChatRecycleItemCustomNormalBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
ChatRecycleItemCustomNormalBinding
|
||||
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
IMAGE -> {
|
||||
ImageViewHolder(
|
||||
ChatRecycleItemImageNormalBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
ChatRecycleItemImageNormalBinding
|
||||
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
TipViewHolder(
|
||||
ChatRecycleItemTipBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
ChatRecycleItemTipBinding
|
||||
.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -108,7 +93,7 @@ class ChatAdapter(
|
||||
}
|
||||
}
|
||||
(holder as? HasTimer)?.let {
|
||||
changeTimerVisibility(position, currentList, it.getTimerView(), singleMessage)
|
||||
changeTimerVisibility(it.getTimerView(), singleMessage)
|
||||
}
|
||||
handleLongClick(holder, position, singleMessage)
|
||||
}
|
||||
@ -176,12 +161,15 @@ class ChatAdapter(
|
||||
contentId = binding.tvMsgContent.id,
|
||||
contentMineId = binding.tvMsgContentMine.id
|
||||
)
|
||||
Glide.with(binding.root.context).load(message.avatarUrl)
|
||||
.into(if (sender) binding.ifvAvatarMine else binding.ifvAvatar)
|
||||
|
||||
val contentView = if (sender) binding.tvMsgContentMine else binding.tvMsgContent
|
||||
|
||||
contentView.text = message.content.replaceSpan("[委屈]") {
|
||||
CenterImageSpan(binding.root.context, R.drawable.emoji).setDrawableSize(55)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun getTimerView(): TextView = binding.tvTimer
|
||||
@ -191,6 +179,8 @@ class ChatAdapter(
|
||||
RecyclerView.ViewHolder(binding.root), HasTimer {
|
||||
fun bindData(message: Messages) {
|
||||
val sender = message.senderId == getUsername()
|
||||
Glide.with(binding.root.context).load(message.avatarUrl)
|
||||
.into(if (sender) binding.ifvAvatarMine else binding.ifvAvatar)
|
||||
setViewVisibility(
|
||||
parentView = binding.root,
|
||||
sender = sender,
|
||||
@ -226,7 +216,6 @@ class ChatAdapter(
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return getItem(position).type.toInt()
|
||||
|
||||
}
|
||||
|
||||
private fun updateDb(message: Messages) {
|
||||
|
@ -0,0 +1,87 @@
|
||||
package com.kaixed.kchat.ui.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import com.bumptech.glide.Glide
|
||||
import com.drake.spannable.replaceSpanFirst
|
||||
import com.drake.spannable.span.HighlightSpan
|
||||
import com.kaixed.kchat.data.model.search.ChatHistoryItem
|
||||
import com.kaixed.kchat.databinding.ChatHistoryRecycleItemBinding
|
||||
import com.kaixed.kchat.ui.activity.ChatActivity
|
||||
import com.kaixed.kchat.utils.ConstantsUtil
|
||||
import com.kaixed.kchat.utils.TextUtil
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/12/11 16:42
|
||||
*/
|
||||
class ChatHistoryAdapter(
|
||||
private val contactAvatarUrl: String,
|
||||
private val contactNickname: String,
|
||||
private val context: Context
|
||||
) :
|
||||
ListAdapter<ChatHistoryItem, ChatHistoryAdapter.MyViewHolder>(DiffCallback()) {
|
||||
|
||||
private var matchedField: String = ""
|
||||
private val highlightSpan = HighlightSpan("#07C160")
|
||||
private var contactId = ""
|
||||
|
||||
fun setMatchedField(field: String) {
|
||||
matchedField = field
|
||||
}
|
||||
|
||||
fun setContactId(contactId: String) {
|
||||
this.contactId = contactId
|
||||
}
|
||||
|
||||
class MyViewHolder(val binding: ChatHistoryRecycleItemBinding) : ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
|
||||
return MyViewHolder(
|
||||
ChatHistoryRecycleItemBinding.inflate(
|
||||
LayoutInflater.from(parent.context), parent, false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
|
||||
val bind = holder.binding
|
||||
val chatHistoryItem = getItem(position)
|
||||
bind.tvTime.text = TextUtil.getTimestampString(chatHistoryItem.timestamp)
|
||||
bind.tvContent.text = chatHistoryItem.content.replaceSpanFirst(matchedField) {
|
||||
highlightSpan
|
||||
}
|
||||
bind.tvNickname.text =
|
||||
if (chatHistoryItem.isMine) ConstantsUtil.getNickName() else contactNickname
|
||||
val avatarUrl =
|
||||
if (chatHistoryItem.isMine) ConstantsUtil.getAvatarUrl() else contactAvatarUrl
|
||||
Glide.with(bind.root.context).load(avatarUrl).into(bind.ifvAvatar)
|
||||
bind.root.postDelayed({
|
||||
val intent = Intent(context, ChatActivity::class.java).apply {
|
||||
putExtra("contactId", contactId)
|
||||
putExtra("contactNickname", contactNickname)
|
||||
putExtra("isSearchHistory", true)
|
||||
putExtra("msgLocalId", chatHistoryItem.msgLocalId)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}, 200L)
|
||||
}
|
||||
|
||||
class DiffCallback : DiffUtil.ItemCallback<ChatHistoryItem>() {
|
||||
override fun areItemsTheSame(oldItem: ChatHistoryItem, newItem: ChatHistoryItem): Boolean {
|
||||
return oldItem.msgLocalId == newItem.msgLocalId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: ChatHistoryItem,
|
||||
newItem: ChatHistoryItem
|
||||
): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
@ -77,10 +77,7 @@ class FriendListAdapter(var items: MutableList<Contact>, private val context: Co
|
||||
if (position == 0) {
|
||||
holder.binding.root.setOnClickListener {
|
||||
context.startActivity(
|
||||
Intent(
|
||||
context,
|
||||
ContactRequestListActivity::class.java
|
||||
)
|
||||
Intent(context, ContactRequestListActivity::class.java)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -101,9 +98,9 @@ class FriendListAdapter(var items: MutableList<Contact>, private val context: Co
|
||||
} else {
|
||||
holder.binding.tvLetter.text = item.quanpin?.substring(0, 1)?.uppercase()
|
||||
}
|
||||
holder.binding.tvLetter.visibility = ViewGroup.VISIBLE
|
||||
holder.binding.tvLetter.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.binding.tvLetter.visibility = ViewGroup.GONE
|
||||
holder.binding.tvLetter.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (position <= defaultItems.size - 1) {
|
||||
|
@ -96,8 +96,6 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
|
||||
contactViewModel.getContactRequestList(getUsername())
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun loadData() {
|
||||
loading = true
|
||||
binding.tvLoading.visibility = View.VISIBLE
|
||||
|
@ -0,0 +1,24 @@
|
||||
package com.kaixed.kchat.ui.widget
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.AttributeSet
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.kaixed.kchat.R
|
||||
|
||||
/**
|
||||
* @Author: kaixed
|
||||
* @Date: 2024/12/12 10:09
|
||||
*/
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
class CustomEditText @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = androidx.appcompat.R.attr.editTextStyle
|
||||
) : AppCompatEditText(context, attrs, defStyleAttr) {
|
||||
init {
|
||||
textCursorDrawable = ContextCompat.getDrawable(context, R.drawable.cursor)
|
||||
}
|
||||
}
|
@ -29,8 +29,10 @@ object ConstantsUtil {
|
||||
fun getUsername(): String =
|
||||
userSessionMMKV.getString(USERNAME_KEY, "") ?: ""
|
||||
|
||||
fun getAvatarUrl(): String =
|
||||
userSessionMMKV.getString(AVATAR_URL, "") ?: ""
|
||||
fun getAvatarUrl(): String {
|
||||
val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: ""
|
||||
return TextUtil.extractDimensionsAndPrefix(avatarUrl)?.first ?: ""
|
||||
}
|
||||
|
||||
fun getStatusBarHeight(): Int =
|
||||
commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT)
|
||||
@ -49,5 +51,4 @@ object ConstantsUtil {
|
||||
}
|
||||
return isFirstLaunch
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -101,14 +101,11 @@ object ViewUtil {
|
||||
|
||||
|
||||
fun changeTimerVisibility(
|
||||
position: Int,
|
||||
messages: List<Messages>,
|
||||
tvTimer: TextView,
|
||||
singleMessage: Messages
|
||||
) {
|
||||
val showTimer: Boolean =
|
||||
if (position == messages.size - 1) true
|
||||
else singleMessage.timestamp - messages[position + 1].timestamp >= 5 * 1L * 60 * 1000
|
||||
singleMessage.isShowTimer
|
||||
|
||||
if (showTimer) {
|
||||
tvTimer.visibility = View.VISIBLE
|
||||
|
@ -9,69 +9,245 @@
|
||||
android:fitsSystemWindows="true"
|
||||
tools:context=".ui.activity.SearchChatHistory">
|
||||
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cv_search"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_margin="15dp"
|
||||
app:cardCornerRadius="7dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardMaxElevation="0dp"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_cancel"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<androidx.cardview.widget.CardView
|
||||
android:id="@+id/cv_search"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="30dp"
|
||||
android:layout_margin="10dp"
|
||||
android:layout_weight="1.0"
|
||||
app:cardCornerRadius="7dp"
|
||||
app:cardElevation="0dp"
|
||||
app:cardMaxElevation="0dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_search"
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="10dp"
|
||||
android:src="@drawable/icon_search" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_search"
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_toEndOf="@id/iv_search"
|
||||
android:background="@null"
|
||||
android:hint="搜索"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="15sp" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</RelativeLayout>
|
||||
<ImageView
|
||||
android:id="@+id/iv_search"
|
||||
android:layout_width="17dp"
|
||||
android:layout_height="17dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="10dp"
|
||||
android:src="@drawable/icon_search" />
|
||||
|
||||
<com.kaixed.kchat.ui.widget.CustomEditText
|
||||
android:id="@+id/et_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_toEndOf="@id/iv_search"
|
||||
android:background="@null"
|
||||
android:hint="搜索"
|
||||
android:inputType="text"
|
||||
android:maxLines="1"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="15sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cancel"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:gravity="center"
|
||||
android:text="取消"
|
||||
android:textColor="#576B95"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_cancel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="15dp"
|
||||
android:text="取消"
|
||||
android:textColor="#576B95"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/cv_search"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/cv_search"
|
||||
app:layout_constraintTop_toTopOf="@id/cv_search" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/divider_height"
|
||||
android:layout_marginTop="10dp"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintTop_toBottomOf="@id/cv_search" />
|
||||
app:layout_constraintTop_toBottomOf="@id/ll_search" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rv_chat_history"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/cl_select"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view"
|
||||
app:layout_constraintVertical_bias="0.1">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="搜索指定内容"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:text="日期"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_picture"
|
||||
app:layout_constraintHorizontal_bias="0.7"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_picture"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:text="图片与视频"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="13sp"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_title" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_file"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:text="文件"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.3"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_picture"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_link"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:text="链接"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_songs"
|
||||
app:layout_constraintHorizontal_bias="0.7"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_songs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:text="音乐与音频"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_picture" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_trade"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:text="交易"
|
||||
android:textColor="@color/normal"
|
||||
android:textSize="13sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.3"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_songs"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view1"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_picture"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_date"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||
|
||||
<View
|
||||
android:id="@+id/view2"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_picture"
|
||||
app:layout_constraintEnd_toStartOf="@id/tv_file"
|
||||
app:layout_constraintStart_toEndOf="@id/tv_picture"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_picture" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||
app:layout_constraintEnd_toEndOf="@id/view1"
|
||||
app:layout_constraintStart_toStartOf="@id/view1"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintBottom_toBottomOf="@id/tv_songs"
|
||||
app:layout_constraintEnd_toEndOf="@id/view2"
|
||||
app:layout_constraintStart_toStartOf="@id/view2"
|
||||
app:layout_constraintTop_toTopOf="@id/tv_songs" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_search_failed"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/view"
|
||||
app:layout_constraintVertical_bias="0.3" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
61
app/src/main/res/layout/chat_history_recycle_item.xml
Normal file
61
app/src/main/res/layout/chat_history_recycle_item.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/white">
|
||||
|
||||
<androidx.constraintlayout.utils.widget.ImageFilterView
|
||||
android:id="@+id/ifv_avatar"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginVertical="10dp"
|
||||
android:layout_marginStart="10dp"
|
||||
android:src="@drawable/ic_default_avatar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:roundPercent="0.3" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_nickname"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:text="糕小菜"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/tv_content"
|
||||
app:layout_constraintStart_toEndOf="@id/ifv_avatar"
|
||||
app:layout_constraintTop_toTopOf="@id/ifv_avatar" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_content"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:text="糕小菜"
|
||||
android:textColor="@color/black"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/ifv_avatar"
|
||||
app:layout_constraintStart_toEndOf="@id/ifv_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/tv_nickname" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="11月9日"
|
||||
android:textSize="14sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/ifv_avatar" />
|
||||
|
||||
<View
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/divider_height"
|
||||
android:background="@color/gray"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/tv_nickname"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
Loading…
Reference in New Issue
Block a user