feat: 新增搜索历史消息功能

- 新增搜索历史消息功能
- 修复部分问题
This commit is contained in:
糕小菜 2024-12-12 16:14:10 +08:00
parent ec9e3a8af4
commit b8c80e4b00
19 changed files with 651 additions and 108 deletions

View File

@ -2,7 +2,19 @@
<project version="4"> <project version="4">
<component name="GitCommitMessageStorage"> <component name="GitCommitMessageStorage">
<option name="messageStorage"> <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> </option>
</component> </component>
</project> </project>

View File

@ -52,4 +52,13 @@ object LocalDatabase {
val offset = 0 val offset = 0
return query.find(offset.toLong(), limit) 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()
}
} }

View File

@ -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
)

View File

@ -1,6 +1,7 @@
package com.kaixed.kchat.manager package com.kaixed.kchat.manager
import com.kaixed.kchat.data.LocalDatabase 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.getMessagesWithContact
import com.kaixed.kchat.data.LocalDatabase.getMoreMessages import com.kaixed.kchat.data.LocalDatabase.getMoreMessages
import com.kaixed.kchat.data.local.entity.Messages import com.kaixed.kchat.data.local.entity.Messages
@ -13,7 +14,7 @@ import kotlinx.coroutines.flow.StateFlow
*/ */
object MessagesManager { object MessagesManager {
private const val LIMIT = 10L private const val LIMIT = 20L
private val _messages = MutableStateFlow<List<Messages>>(emptyList()) private val _messages = MutableStateFlow<List<Messages>>(emptyList())
@ -24,6 +25,13 @@ object MessagesManager {
private var loading = false private var loading = false
private var tempIndex: Long = 0 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) { fun setContactId(contactId: String) {
this.contactId = contactId this.contactId = contactId
} }

View File

@ -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
}
}

View File

@ -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
import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL import com.kaixed.kchat.network.NetworkInterface.WEBSOCKET_SERVER_URL
import com.kaixed.kchat.network.OkhttpHelper 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.Constants.MMKV_COMMON_DATA
import com.kaixed.kchat.utils.ConstantsUtil.getCurrentContactId import com.kaixed.kchat.utils.ConstantsUtil.getCurrentContactId
import com.kaixed.kchat.utils.ConstantsUtil.getUsername import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.SingleLiveEvent
import com.tencent.mmkv.MMKV import com.tencent.mmkv.MMKV
import io.objectbox.Box import io.objectbox.Box
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -60,9 +62,9 @@ class WebSocketService : Service() {
private val serviceScope = CoroutineScope(IO + serviceJob) 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>?>() private val _conversations = MutableLiveData<List<Conversation>?>()
@ -233,6 +235,7 @@ class WebSocketService : Service() {
} else { } else {
messages.takerId = messages.senderId messages.takerId = messages.senderId
_messagesMutableLiveData.postValue(messages) _messagesMutableLiveData.postValue(messages)
MessageProcessor.processorMsg(messages)
messagesBox.put(messages) messagesBox.put(messages)
updateConversationList(messages) updateConversationList(messages)
} }

View File

@ -33,6 +33,7 @@ import com.kaixed.kchat.data.local.entity.Messages
import com.kaixed.kchat.data.model.FunctionItem import com.kaixed.kchat.data.model.FunctionItem
import com.kaixed.kchat.databinding.ActivityChatBinding import com.kaixed.kchat.databinding.ActivityChatBinding
import com.kaixed.kchat.manager.MessagesManager import com.kaixed.kchat.manager.MessagesManager
import com.kaixed.kchat.processor.MessageProcessor
import com.kaixed.kchat.service.WebSocketService import com.kaixed.kchat.service.WebSocketService
import com.kaixed.kchat.service.WebSocketService.LocalBinder import com.kaixed.kchat.service.WebSocketService.LocalBinder
import com.kaixed.kchat.ui.adapter.ChatAdapter 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.CURRENT_CONTACT_ID
import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO import com.kaixed.kchat.utils.Constants.KEYBOARD_HEIGHT_RATIO
import com.kaixed.kchat.utils.Constants.MMKV_USER_SESSION 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.getKeyboardHeight
import com.kaixed.kchat.utils.ConstantsUtil.getUsername import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.ImageEngines import com.kaixed.kchat.utils.ImageEngines
@ -91,6 +93,10 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
private val fileViewModel: FileViewModel by viewModels() private val fileViewModel: FileViewModel by viewModels()
private var isSearchHistory = false
private var msgLocalId = 0L
companion object { companion object {
private const val UNBLOCK_DELAY_TIME = 200L private const val UNBLOCK_DELAY_TIME = 200L
} }
@ -109,6 +115,17 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
setPanelChange() setPanelChange()
getKeyBoardVisibility() getKeyBoardVisibility()
observeViewModel() 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() { private fun observeViewModel() {
@ -376,8 +393,11 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
} }
override fun initData() { override fun initData() {
contactId = intent.getStringExtra("contactId").toString() contactId = intent?.getStringExtra("contactId").toString()
contactNickname = intent.getStringExtra("contactNickname") contactNickname = intent?.getStringExtra("contactNickname")
isSearchHistory = intent?.getBooleanExtra("isSearchHistory", false) == true
msgLocalId = intent.getLongExtra("msgLocalId", 0)
binding.ctb.setTitleName(contactNickname!!) binding.ctb.setTitleName(contactNickname!!)
softKeyboardHeight = getKeyboardHeight() softKeyboardHeight = getKeyboardHeight()
@ -414,7 +434,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
private fun handleMsg(messages: Messages) { private fun handleMsg(messages: Messages) {
MessagesManager.receiveMessage(messages) MessagesManager.receiveMessage(messages)
// messagesViewModel.receiveMessage(messages)
binding.recycleChatList.smoothScrollToPosition(0) binding.recycleChatList.smoothScrollToPosition(0)
} }
@ -424,10 +443,11 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
timestamp = System.currentTimeMillis(), timestamp = System.currentTimeMillis(),
senderId = username, senderId = username,
takerId = talkerId, takerId = talkerId,
avatarUrl = getAvatarUrl(),
type = type, type = type,
) )
val msg = MessageProcessor.processorMsg(messages)
val id: Long = messagesBox.put(messages) val id: Long = messagesBox.put(msg)
val jsonObject = JSONObject() val jsonObject = JSONObject()
val jsonObject2 = JSONObject() val jsonObject2 = JSONObject()
@ -445,7 +465,6 @@ class ChatActivity : BaseActivity<ActivityChatBinding>(), OnItemClickListener,
binding.etInput.setText("") binding.etInput.setText("")
} }
private fun bindWebSocketService() { private fun bindWebSocketService() {
bindService( bindService(
Intent( Intent(

View File

@ -3,7 +3,6 @@ package com.kaixed.kchat.ui.activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.ViewModelProvider
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.kaixed.kchat.data.LocalDatabase.getContactByUsername import com.kaixed.kchat.data.LocalDatabase.getContactByUsername
import com.kaixed.kchat.databinding.ActivityChatDetailBinding import com.kaixed.kchat.databinding.ActivityChatDetailBinding
@ -54,7 +53,11 @@ class ChatDetailActivity : BaseActivity<ActivityChatDetailBinding>() {
} }
binding.ciSearchChatHistory.setOnClickListener { 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) startActivity(intent)
} }
} }

View File

@ -19,7 +19,6 @@ class ContactPermissionActivity : BaseActivity<ActivityContactPermissionBinding>
} }
override fun initData() { override fun initData() {
TODO("Not yet implemented")
} }
private fun initView() { private fun initView() {

View File

@ -1,16 +1,49 @@
package com.kaixed.kchat.ui.activity package com.kaixed.kchat.ui.activity
import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.text.style.ForegroundColorSpan
import android.util.Log
import android.view.View
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity import androidx.core.text.buildSpannedString
import androidx.core.view.ViewCompat import androidx.core.text.inSpans
import androidx.core.view.WindowInsetsCompat import androidx.core.widget.addTextChangedListener
import com.kaixed.kchat.R 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.databinding.ActivitySearchChatHistoryBinding
import com.kaixed.kchat.ui.adapter.ChatHistoryAdapter
import com.kaixed.kchat.ui.base.BaseActivity import com.kaixed.kchat.ui.base.BaseActivity
import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import io.objectbox.Box
class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() { 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 { override fun inflateBinding(): ActivitySearchChatHistoryBinding {
return ActivitySearchChatHistoryBinding.inflate(layoutInflater) return ActivitySearchChatHistoryBinding.inflate(layoutInflater)
} }
@ -18,10 +51,93 @@ class SearchChatHistory : BaseActivity<ActivitySearchChatHistoryBinding>() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() 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() { override fun initData() {
contactNickname = intent?.getStringExtra("contactNickname") ?: ""
contactAvatarUrl = intent?.getStringExtra("contactAvatarUrl") ?: ""
contactId = intent?.getStringExtra("contactId") ?: ""
} }
} }

View File

@ -21,6 +21,7 @@ import com.kaixed.kchat.databinding.ChatRecycleItemImageNormalBinding
import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding import com.kaixed.kchat.databinding.ChatRecycleItemTipBinding
import com.kaixed.kchat.utils.ConstantsUtil.getUsername import com.kaixed.kchat.utils.ConstantsUtil.getUsername
import com.kaixed.kchat.utils.PopWindowUtil.showPopupWindow 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.TextUtil.extractDimensionsAndPrefix
import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility import com.kaixed.kchat.utils.ViewUtil.changeTimerVisibility
import com.kaixed.kchat.utils.ViewUtil.setViewVisibility import com.kaixed.kchat.utils.ViewUtil.setViewVisibility
@ -42,25 +43,12 @@ class ChatAdapter(
companion object { companion object {
const val CUSTOM = 0 const val CUSTOM = 0
// 提示消息
const val TIP = 2 const val TIP = 2
// 图片消息
const val IMAGE = 4 const val IMAGE = 4
// 语音消息
const val VOICE = 6 const val VOICE = 6
// 位置消息
const val LOCATION = 8 const val LOCATION = 8
// 表情消息
const val EMOJI = 10 const val EMOJI = 10
// 红包消息
const val RED_PACKET = 12 const val RED_PACKET = 12
private const val TAG = "ChatAdapter" private const val TAG = "ChatAdapter"
} }
@ -68,25 +56,22 @@ class ChatAdapter(
return when (viewType) { return when (viewType) {
CUSTOM -> { CUSTOM -> {
CustomViewHolder( CustomViewHolder(
ChatRecycleItemCustomNormalBinding.inflate( ChatRecycleItemCustomNormalBinding
LayoutInflater.from(parent.context), parent, false .inflate(LayoutInflater.from(parent.context), parent, false)
)
) )
} }
IMAGE -> { IMAGE -> {
ImageViewHolder( ImageViewHolder(
ChatRecycleItemImageNormalBinding.inflate( ChatRecycleItemImageNormalBinding
LayoutInflater.from(parent.context), parent, false .inflate(LayoutInflater.from(parent.context), parent, false)
)
) )
} }
else -> { else -> {
TipViewHolder( TipViewHolder(
ChatRecycleItemTipBinding.inflate( ChatRecycleItemTipBinding
LayoutInflater.from(parent.context), parent, false .inflate(LayoutInflater.from(parent.context), parent, false)
)
) )
} }
} }
@ -108,7 +93,7 @@ class ChatAdapter(
} }
} }
(holder as? HasTimer)?.let { (holder as? HasTimer)?.let {
changeTimerVisibility(position, currentList, it.getTimerView(), singleMessage) changeTimerVisibility(it.getTimerView(), singleMessage)
} }
handleLongClick(holder, position, singleMessage) handleLongClick(holder, position, singleMessage)
} }
@ -176,12 +161,15 @@ class ChatAdapter(
contentId = binding.tvMsgContent.id, contentId = binding.tvMsgContent.id,
contentMineId = binding.tvMsgContentMine.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 val contentView = if (sender) binding.tvMsgContentMine else binding.tvMsgContent
contentView.text = message.content.replaceSpan("[委屈]") { contentView.text = message.content.replaceSpan("[委屈]") {
CenterImageSpan(binding.root.context, R.drawable.emoji).setDrawableSize(55) CenterImageSpan(binding.root.context, R.drawable.emoji).setDrawableSize(55)
} }
} }
override fun getTimerView(): TextView = binding.tvTimer override fun getTimerView(): TextView = binding.tvTimer
@ -191,6 +179,8 @@ class ChatAdapter(
RecyclerView.ViewHolder(binding.root), HasTimer { RecyclerView.ViewHolder(binding.root), HasTimer {
fun bindData(message: Messages) { fun bindData(message: Messages) {
val sender = message.senderId == getUsername() val sender = message.senderId == getUsername()
Glide.with(binding.root.context).load(message.avatarUrl)
.into(if (sender) binding.ifvAvatarMine else binding.ifvAvatar)
setViewVisibility( setViewVisibility(
parentView = binding.root, parentView = binding.root,
sender = sender, sender = sender,
@ -226,7 +216,6 @@ class ChatAdapter(
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
return getItem(position).type.toInt() return getItem(position).type.toInt()
} }
private fun updateDb(message: Messages) { private fun updateDb(message: Messages) {

View File

@ -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
}
}
}

View File

@ -77,10 +77,7 @@ class FriendListAdapter(var items: MutableList<Contact>, private val context: Co
if (position == 0) { if (position == 0) {
holder.binding.root.setOnClickListener { holder.binding.root.setOnClickListener {
context.startActivity( context.startActivity(
Intent( Intent(context, ContactRequestListActivity::class.java)
context,
ContactRequestListActivity::class.java
)
) )
} }
} }
@ -101,9 +98,9 @@ class FriendListAdapter(var items: MutableList<Contact>, private val context: Co
} else { } else {
holder.binding.tvLetter.text = item.quanpin?.substring(0, 1)?.uppercase() holder.binding.tvLetter.text = item.quanpin?.substring(0, 1)?.uppercase()
} }
holder.binding.tvLetter.visibility = ViewGroup.VISIBLE holder.binding.tvLetter.visibility = View.VISIBLE
} else { } else {
holder.binding.tvLetter.visibility = ViewGroup.GONE holder.binding.tvLetter.visibility = View.GONE
} }
if (position <= defaultItems.size - 1) { if (position <= defaultItems.size - 1) {

View File

@ -96,8 +96,6 @@ class ContactFragment : BaseFragment<FragmentContactBinding>() {
contactViewModel.getContactRequestList(getUsername()) contactViewModel.getContactRequestList(getUsername())
} }
private fun loadData() { private fun loadData() {
loading = true loading = true
binding.tvLoading.visibility = View.VISIBLE binding.tvLoading.visibility = View.VISIBLE

View File

@ -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)
}
}

View File

@ -29,8 +29,10 @@ object ConstantsUtil {
fun getUsername(): String = fun getUsername(): String =
userSessionMMKV.getString(USERNAME_KEY, "") ?: "" userSessionMMKV.getString(USERNAME_KEY, "") ?: ""
fun getAvatarUrl(): String = fun getAvatarUrl(): String {
userSessionMMKV.getString(AVATAR_URL, "") ?: "" val avatarUrl = userSessionMMKV.getString(AVATAR_URL, "") ?: ""
return TextUtil.extractDimensionsAndPrefix(avatarUrl)?.first ?: ""
}
fun getStatusBarHeight(): Int = fun getStatusBarHeight(): Int =
commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT) commonDataMMKV.getInt(STATUS_BAR_HEIGHT, STATUS_BAR_DEFAULT_HEIGHT)
@ -49,5 +51,4 @@ object ConstantsUtil {
} }
return isFirstLaunch return isFirstLaunch
} }
} }

View File

@ -101,14 +101,11 @@ object ViewUtil {
fun changeTimerVisibility( fun changeTimerVisibility(
position: Int,
messages: List<Messages>,
tvTimer: TextView, tvTimer: TextView,
singleMessage: Messages singleMessage: Messages
) { ) {
val showTimer: Boolean = val showTimer: Boolean =
if (position == messages.size - 1) true singleMessage.isShowTimer
else singleMessage.timestamp - messages[position + 1].timestamp >= 5 * 1L * 60 * 1000
if (showTimer) { if (showTimer) {
tvTimer.visibility = View.VISIBLE tvTimer.visibility = View.VISIBLE

View File

@ -9,69 +9,245 @@
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
tools:context=".ui.activity.SearchChatHistory"> tools:context=".ui.activity.SearchChatHistory">
<androidx.cardview.widget.CardView <LinearLayout
android:id="@+id/cv_search" android:id="@+id/ll_search"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="30dp" android:layout_height="wrap_content"
android:layout_margin="15dp" android:orientation="horizontal"
app:cardCornerRadius="7dp"
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:layout_constraintEnd_toStartOf="@id/tv_cancel"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<RelativeLayout <androidx.cardview.widget.CardView
android:layout_width="match_parent" android:id="@+id/cv_search"
android:layout_height="match_parent"> 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 <RelativeLayout
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"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent">
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> <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 <View
android:id="@+id/view" android:id="@+id/view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/divider_height" android:layout_height="@dimen/divider_height"
android:layout_marginTop="10dp"
android:background="@color/gray" 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> </androidx.constraintlayout.widget.ConstraintLayout>

View 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>