Procházet zdrojové kódy

feat: IM列表在线状态

DoggyZhang před 2 měsíci
rodič
revize
4935fe4365
20 změnil soubory, kde provedl 368 přidání a 100 odebrání
  1. 6 0
      app/src/main/java/com/adealink/weparty/module/profile/IProfileService.kt
  2. 24 0
      app/src/main/java/com/adealink/weparty/module/profile/ProfileModule.kt
  3. 2 0
      app/src/main/java/com/adealink/weparty/module/profile/listener/IProfileListener.kt
  4. 3 0
      app/src/main/java/com/adealink/weparty/module/profile/viewmodel/IProfileViewModel.kt
  5. 8 0
      module/im/src/main/java/com/adealink/weparty/im/IMServiceImpl.kt
  6. 1 5
      module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt
  7. 126 83
      module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/SessionListItemViewBinder.kt
  8. 3 1
      module/im/src/main/java/com/adealink/weparty/im/list/viewmodel/SessionListViewModel.kt
  9. 7 0
      module/im/src/main/java/com/adealink/weparty/im/manager/session/ISessionManager.kt
  10. 61 1
      module/im/src/main/java/com/adealink/weparty/im/manager/session/SessionManager.kt
  11. 15 5
      module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionTopComp.kt
  12. 1 0
      module/im/src/main/java/com/adealink/weparty/im/session/viewmodel/SessionViewModel.kt
  13. 13 0
      module/profile/src/main/java/com/adealink/weparty/profile/ProfileServiceImpl.kt
  14. 7 0
      module/profile/src/main/java/com/adealink/weparty/profile/manager/IProfileManager.kt
  15. 49 5
      module/profile/src/main/java/com/adealink/weparty/profile/manager/ProfileManager.kt
  16. 16 0
      module/profile/src/main/java/com/adealink/weparty/profile/relation/adapter/VisitorItemViewBinder.kt
  17. 2 0
      module/profile/src/main/java/com/adealink/weparty/profile/relation/viewmodel/VisitorViewModel.kt
  18. 5 0
      module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchViewModel.kt
  19. 6 0
      module/profile/src/main/java/com/adealink/weparty/profile/viewmodel/ProfileViewModel.kt
  20. 13 0
      module/profile/src/main/res/layout/item_relationship_visitor.xml

+ 6 - 0
app/src/main/java/com/adealink/weparty/module/profile/IProfileService.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.module.profile
 
+import androidx.lifecycle.LiveData
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.base.Rlt
 import com.adealink.weparty.aab.IService
@@ -38,6 +39,8 @@ interface IProfileService : IService<IProfileService> {
 
     fun isUserOnline(uid: String): Boolean
 
+    fun observeUserOnline(uid: String): LiveData<Boolean>
+
     suspend fun isUserOnline(
         uid: String,
         cache: Boolean = true,
@@ -48,6 +51,9 @@ interface IProfileService : IService<IProfileService> {
         cache: Boolean = true,
     ): Rlt<Map<String, Boolean>>
 
+    fun updateUserOnline(uid: String, online: Boolean)
+
+    fun updateUserOnline(onlineStatus: Map<String, Boolean>)
 
     fun getProfileViewModel(owner: ViewModelStoreOwner): IProfileViewModel?
 

+ 24 - 0
app/src/main/java/com/adealink/weparty/module/profile/ProfileModule.kt

@@ -1,5 +1,7 @@
 package com.adealink.weparty.module.profile
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.aab.BaseDynamicModule
 import com.adealink.frame.aab.constant.AABModuleNotInitError
@@ -68,6 +70,10 @@ object ProfileModule : BaseDynamicModule<IProfileService>(IProfileService::class
                 return false
             }
 
+            override fun observeUserOnline(uid: String): LiveData<Boolean> {
+                return MutableLiveData()
+            }
+
             override suspend fun isUserOnline(
                 uid: String,
                 cache: Boolean
@@ -82,6 +88,12 @@ object ProfileModule : BaseDynamicModule<IProfileService>(IProfileService::class
                 return Rlt.Failed(AABModuleNotInitError())
             }
 
+            override fun updateUserOnline(uid: String, online: Boolean) {
+            }
+
+            override fun updateUserOnline(onlineStatus: Map<String, Boolean>) {
+            }
+
             override fun getProfileViewModel(owner: ViewModelStoreOwner): IProfileViewModel? {
                 return null
             }
@@ -144,6 +156,10 @@ object ProfileModule : BaseDynamicModule<IProfileService>(IProfileService::class
         return getService().isUserOnline(uid)
     }
 
+    override fun observeUserOnline(uid: String): LiveData<Boolean> {
+        return getService().observeUserOnline(uid)
+    }
+
     override suspend fun isUserOnline(
         uid: String,
         cache: Boolean
@@ -158,6 +174,14 @@ object ProfileModule : BaseDynamicModule<IProfileService>(IProfileService::class
         return getService().isUserOnline(uidSet, cache)
     }
 
+    override fun updateUserOnline(uid: String, online: Boolean) {
+        getService().updateUserOnline(uid, online)
+    }
+
+    override fun updateUserOnline(onlineStatus: Map<String, Boolean>) {
+        getService().updateUserOnline(onlineStatus)
+    }
+
     override fun getProfileViewModel(owner: ViewModelStoreOwner): IProfileViewModel? {
         return getService().getProfileViewModel(owner)
     }

+ 2 - 0
app/src/main/java/com/adealink/weparty/module/profile/listener/IProfileListener.kt

@@ -4,4 +4,6 @@ import com.adealink.frame.frame.IListener
 
 
 interface IProfileListener : IListener {
+
+    fun onUserOnlineStatusChanged(online: Map<String, Boolean>)
 }

+ 3 - 0
app/src/main/java/com/adealink/weparty/module/profile/viewmodel/IProfileViewModel.kt

@@ -11,6 +11,9 @@ interface IProfileViewModel {
     val userInfoLD: LiveData<UserInfo?>
 
     val myUserInfoLD: LiveData<MyUserInfoData?>
+
+    val userOnlineLD : LiveData<Map<String, Boolean>>
+
     fun pullMyUserInfo(): LiveData<Rlt<MyUserInfoData?>>
 
     fun pullUserInfoBy(uid: String, cache: Boolean): LiveData<Rlt<UserInfo>>

+ 8 - 0
module/im/src/main/java/com/adealink/weparty/im/IMServiceImpl.kt

@@ -5,12 +5,15 @@ import com.adealink.frame.spi.RegisterService
 import com.adealink.weparty.im.constant.OFFICIAL_IMAGE_TEXT_BUSINESS_ID
 import com.adealink.weparty.im.constant.PLAYMATE_ORDER_BUSINESS_ID
 import com.adealink.weparty.im.manager.login.imLoginManager
+import com.adealink.weparty.im.manager.session.sessionManager
 import com.adealink.weparty.im.service.TIMAppService
 import com.adealink.weparty.im.session.mesasge.CustomMessageViewHolder
 import com.adealink.weparty.im.session.mesasge.OfficialImageTextMessageBean
 import com.adealink.weparty.im.session.mesasge.PlaymateOrderMessageBean
 import com.adealink.weparty.module.im.IIMService
 import com.tencent.qcloud.tuikit.tuichat.config.TUIChatConfigs
+import com.tencent.qcloud.tuikit.tuicontact.config.TUIContactConfig
+import com.tencent.qcloud.tuikit.tuiconversation.config.TUIConversationConfig
 
 @RegisterService(value = IIMService::class)
 class IMServiceImpl : IIMService {
@@ -34,10 +37,15 @@ class IMServiceImpl : IIMService {
             CustomMessageViewHolder::class.java
         )
 
+        //开启用户验收
+        TUIConversationConfig.getInstance().isShowUserStatus = true
+        TUIContactConfig.getInstance().isShowUserStatus = true
+
         //开启已读回执
         TUIChatConfigs.getGeneralConfig().isMsgNeedReadReceipt = true
 
         imLoginManager.init(application)
+        sessionManager.init(application)
         TIMAppService().init(application)
     }
 

+ 1 - 5
module/im/src/main/java/com/adealink/weparty/im/list/SessionListFragment.kt

@@ -70,6 +70,7 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
         sessionAdapter.register(OfficialListItemViewBinder(this))
         sessionAdapter.register(
             SessionListItemViewBinder(
+                this,
                 this,
                 { uid ->
                     onAvatarClick(uid)
@@ -99,11 +100,6 @@ class SessionListFragment : BaseFragment(R.layout.fragment_session_list),
         presenter.destroy()
     }
 
-    override fun observeViewModel() {
-        super.observeViewModel()
-        sessionListViewModel
-    }
-
     override fun onItemClick(
         view: View?,
         viewType: Int,

+ 126 - 83
module/im/src/main/java/com/adealink/weparty/im/list/adapter/viewbinder/SessionListItemViewBinder.kt

@@ -2,11 +2,14 @@ package com.adealink.weparty.im.list.adapter.viewbinder
 
 import android.view.LayoutInflater
 import android.view.ViewGroup
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.Observer
 import com.adealink.frame.base.AppBase
 import com.adealink.frame.data.json.froJsonErrorNull
 import com.adealink.frame.dot.NumDot
 import com.adealink.frame.util.onClick
 import com.adealink.weparty.commonui.ext.gone
+import com.adealink.weparty.commonui.ext.isViewValid
 import com.adealink.weparty.commonui.ext.show
 import com.adealink.weparty.commonui.recycleview.adapter.BindingViewHolder
 import com.adealink.weparty.commonui.recycleview.adapter.multitype.ItemViewBinder
@@ -22,109 +25,149 @@ import com.tencent.qcloud.tuikit.tuiconversation.presenter.ConversationPresenter
 
 
 class SessionListItemViewBinder(
-    val listener: OnConversationAdapterListener,
-    val onAvatarClick: (uid: String) -> Unit,
-    val userInfoListener: IUserInfoListener,
+    private val lifecycleOwner: LifecycleOwner,
+    private val listener: OnConversationAdapterListener,
+    private val onAvatarClick: (uid: String) -> Unit,
+    private val userInfoListener: IUserInfoListener,
 ) :
-    ItemViewBinder<CommonSessionListItemData, BindingViewHolder<LayoutSessionListItemBinding>>() {
+    ItemViewBinder<CommonSessionListItemData, SessionListItemViewBinder.ViewHolder>() {
 
     override fun onBindViewHolder(
-        holder: BindingViewHolder<LayoutSessionListItemBinding>,
-        item: CommonSessionListItemData,
+        holder: ViewHolder,
+        item: CommonSessionListItemData
     ) {
-        holder.binding.root.onClick {
-            listener.onItemClick(it, item.data.type, item.data)
-        }
+        holder.update(holder, item)
+    }
+
+    override fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): ViewHolder {
+        return ViewHolder(
+            LayoutSessionListItemBinding.inflate(inflater, parent, false)
+        )
+    }
 
-        val uid = item.data.id
-        holder.binding.ivAvatar.onClick {
-            onAvatarClick.invoke(item.data.id)
-        }
-        userInfoListener.getUserInfo(uid) { userInfo ->
-            updateUserInfo(holder, item, userInfo)
-        }
 
-        val online = ProfileModule.isUserOnline(uid)
-        if (online) {
-            holder.binding.vOnline.show()
-            holder.binding.vOnline.onStart()
-        } else {
-            holder.binding.vOnline.gone()
-            holder.binding.vOnline.onStop()
+    override fun onViewDetachedFromWindow(holder: ViewHolder) {
+        super.onViewDetachedFromWindow(holder)
+        holder.binding.vOnline.onStop()
+    }
+
+    override fun onViewRecycled(holder: ViewHolder) {
+        holder.binding.vOnline.onStop()
+        super.onViewRecycled(holder)
+    }
+
+
+    inner class ViewHolder(
+        binding: LayoutSessionListItemBinding,
+    ) : BindingViewHolder<LayoutSessionListItemBinding>(binding) {
+
+        private var item: CommonSessionListItemData? = null
+
+        fun initView() {
+
         }
 
-        setLastMessageAndStatus(holder, item)
+        private val onlineObserver = object : Observer<Boolean> {
 
-        // TODO: 长按测试用
-        if (!AppBase.isRelease) {
-            holder.binding.root.setOnLongClickListener {
-                listener.onItemLongClick(it, item.data)
-                true
+            var holder: ViewHolder? = null
+
+            override fun onChanged(value: Boolean) {
+                val holder = holder
+                if (holder == null || !holder.isViewValid()) {
+                    return
+                }
+                updateUserOnline(value)
             }
         }
-    }
 
-    private fun updateUserInfo(
-        holder: BindingViewHolder<LayoutSessionListItemBinding>,
-        item: CommonSessionListItemData,
-        userInfo: UserInfo?
-    ) {
-        holder.binding.ivAvatar.setImageUrl(userInfo?.avatar)
-        holder.binding.tvTitle.text = userInfo?.nickName ?: item.data.title
-
-    }
+        fun update(holder: ViewHolder, item: CommonSessionListItemData) {
+            resetItem(this.item)
+            this.item = item
+            onlineObserver.holder = holder
+            binding.root.onClick {
+                listener.onItemClick(it, item.data.type, item.data)
+            }
 
-    private fun setLastMessageAndStatus(
-        holder: BindingViewHolder<LayoutSessionListItemBinding>,
-        item: CommonSessionListItemData,
-    ) {
-        val conversation = item.data
-
-        val draftInfo: DraftInfo? = conversation.draft
-        if (draftInfo != null) {
-            //草稿
-            val draftText =
-                (froJsonErrorNull<HashMap<*, *>>(draftInfo.draftText)?.get("content") as? String)
-                    ?: draftInfo.draftText
-
-            holder.binding.tvDesc.text = draftText
-            setLastMessageTime(holder, draftInfo.draftTime)
-        } else {
-            //上一条消息
-            val lasTUIMessageBean = conversation.lastTUIMessageBean
-            if (lasTUIMessageBean != null) {
-                val displayString = ConversationPresenter.getMessageDisplayString(lasTUIMessageBean)
-                holder.binding.tvDesc.text = displayString
+            val uid = item.data.id
+            binding.ivAvatar.onClick {
+                onAvatarClick.invoke(item.data.id)
+            }
+            userInfoListener.getUserInfo(uid) { userInfo ->
+                updateUserInfo(item, userInfo)
             }
-            if (conversation.lastMessage != null) {
-                setLastMessageTime(holder, conversation.lastMessageTime * 1000)
+
+            val online = ProfileModule.isUserOnline(uid)
+            updateUserOnline(online)
+            ProfileModule.observeUserOnline(uid).observe(lifecycleOwner, onlineObserver)
+            setLastMessageAndStatus(binding, item)
+
+            // TODO: 长按测试用
+            if (!AppBase.isRelease) {
+                binding.root.setOnLongClickListener {
+                    listener.onItemLongClick(it, item.data)
+                    true
+                }
             }
         }
 
-        holder.binding.vDot.show(NumDot(conversation.unRead))
-    }
+        private fun resetItem(item: CommonSessionListItemData?) {
+            item ?: return
+            val uid = item.data.id
+            ProfileModule.observeUserOnline(uid).removeObserver(onlineObserver)
+        }
 
-    private fun setLastMessageTime(
-        holder: BindingViewHolder<LayoutSessionListItemBinding>,
-        timeTs: Long
-    ) {
-        holder.binding.tvTime.text = timeTs.formatTimeStr()
-    }
+        private fun setLastMessageAndStatus(
+            binding: LayoutSessionListItemBinding,
+            item: CommonSessionListItemData,
+        ) {
+            val conversation = item.data
+
+            val draftInfo: DraftInfo? = conversation.draft
+            if (draftInfo != null) {
+                //草稿
+                val draftText =
+                    (froJsonErrorNull<HashMap<*, *>>(draftInfo.draftText)?.get("content") as? String)
+                        ?: draftInfo.draftText
+
+                binding.tvDesc.text = draftText
+                setLastMessageTime(draftInfo.draftTime)
+            } else {
+                //上一条消息
+                val lasTUIMessageBean = conversation.lastTUIMessageBean
+                if (lasTUIMessageBean != null) {
+                    val displayString =
+                        ConversationPresenter.getMessageDisplayString(lasTUIMessageBean)
+                    binding.tvDesc.text = displayString
+                }
+                if (conversation.lastMessage != null) {
+                    setLastMessageTime(conversation.lastMessageTime * 1000)
+                }
+            }
 
-    override fun onCreateViewHolder(
-        inflater: LayoutInflater,
-        parent: ViewGroup,
-    ): BindingViewHolder<LayoutSessionListItemBinding> {
-        return BindingViewHolder(LayoutSessionListItemBinding.inflate(inflater, parent, false))
-    }
+            binding.vDot.show(NumDot(conversation.unRead))
+        }
 
-    override fun onViewDetachedFromWindow(holder: BindingViewHolder<LayoutSessionListItemBinding>) {
-        super.onViewDetachedFromWindow(holder)
-        holder.binding.vOnline.onStop()
-    }
+        private fun setLastMessageTime(timeTs: Long) {
+            binding.tvTime.text = timeTs.formatTimeStr()
+        }
 
-    override fun onViewRecycled(holder: BindingViewHolder<LayoutSessionListItemBinding>) {
-        holder.binding.vOnline.onStop()
-        super.onViewRecycled(holder)
+        private fun updateUserInfo(
+            item: CommonSessionListItemData,
+            userInfo: UserInfo?
+        ) {
+            binding.ivAvatar.setImageUrl(userInfo?.avatar)
+            binding.tvTitle.text = userInfo?.nickName ?: item.data.title
+
+        }
+
+        private fun updateUserOnline(online: Boolean) {
+            if (online) {
+                binding.vOnline.show()
+                binding.vOnline.onStart()
+            } else {
+                binding.vOnline.gone()
+                binding.vOnline.onStop()
+            }
+        }
     }
 }

+ 3 - 1
module/im/src/main/java/com/adealink/weparty/im/list/viewmodel/SessionListViewModel.kt

@@ -1,6 +1,7 @@
 package com.adealink.weparty.im.list.viewmodel
 
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
+import com.adealink.weparty.im.constant.OFFICIAL_UID
 import com.adealink.weparty.im.list.adapter.SessionListAdapter
 import com.adealink.weparty.module.profile.ProfileModule
 import com.tencent.qcloud.tuikit.tuiconversation.bean.ConversationInfo
@@ -34,7 +35,8 @@ class SessionListViewModel : BaseViewModel() {
             for (info in dataSource) {
                 info ?: continue
             }
-            val userIds = dataSource.mapNotNull { it?.id }.distinct().toSet()
+            val userIds =
+                dataSource.mapNotNull { it?.id }.filter { it != OFFICIAL_UID }.distinct().toSet()
             val userInfoDef = async { ProfileModule.getUsersInfoByUid(userIds) }
             val userOnlineDef = async { ProfileModule.isUserOnline(userIds) }
             val userInfoMap = userInfoDef.await()

+ 7 - 0
module/im/src/main/java/com/adealink/weparty/im/manager/session/ISessionManager.kt

@@ -1,10 +1,13 @@
 package com.adealink.weparty.im.manager.session
 
+import android.app.Application
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.frame.IBaseFrame
 
 interface ISessionManager : IBaseFrame<ISessionListener> {
 
+    fun init(application: Application)
+
     suspend fun isSessionBlack(
         uid: String,
     ): Rlt<Boolean>
@@ -26,4 +29,8 @@ interface ISessionManager : IBaseFrame<ISessionListener> {
         mute: Boolean
     ): Rlt<Any>
 
+
+    fun registerUserOnlineStatus(uid: String)
+
+    fun unregisterUserOnlineStatus(uid: String)
 }

+ 61 - 1
module/im/src/main/java/com/adealink/weparty/im/manager/session/SessionManager.kt

@@ -1,18 +1,24 @@
 package com.adealink.weparty.im.manager.session
 
+import android.app.Application
 import com.adealink.frame.base.CommonParamError
 import com.adealink.frame.base.IError
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.base.fastLazy
 import com.adealink.frame.frame.BaseFrame
+import com.adealink.frame.log.Log
 import com.adealink.frame.storage.cache.TimeoutLruCache
 import com.adealink.weparty.App
 import com.adealink.weparty.im.data.IMIsBlackReq
 import com.adealink.weparty.im.datasource.remote.IMHttpService
+import com.adealink.weparty.module.im.data.TAG_IM_SESSION
+import com.adealink.weparty.module.profile.ProfileModule
 import com.tencent.imsdk.v2.V2TIMCallback
 import com.tencent.imsdk.v2.V2TIMManager
 import com.tencent.imsdk.v2.V2TIMMessage
 import com.tencent.imsdk.v2.V2TIMReceiveMessageOptInfo
+import com.tencent.imsdk.v2.V2TIMSDKListener
+import com.tencent.imsdk.v2.V2TIMUserStatus
 import com.tencent.imsdk.v2.V2TIMValueCallback
 import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
 import kotlinx.coroutines.suspendCancellableCoroutine
@@ -45,6 +51,10 @@ class SessionManager : BaseFrame<ISessionListener>(),
         App.instance.networkService.getHttpService(IMHttpService::class.java)
     }
 
+    override fun init(application: Application) {
+        registerUserStatusChanged()
+    }
+
     override suspend fun isSessionBlack(
         uid: String
     ): Rlt<Boolean> {
@@ -153,7 +163,6 @@ class SessionManager : BaseFrame<ISessionListener>(),
                                     )
                                 }
                             }
-
                         }
                     )
                 }
@@ -166,4 +175,55 @@ class SessionManager : BaseFrame<ISessionListener>(),
     }
 
 
+    private val userOnlineRegisterList = mutableListOf<String>()
+    override fun registerUserOnlineStatus(uid: String) {
+        userOnlineRegisterList.add(uid)
+        V2TIMManager.getInstance()
+            .subscribeUserStatus(userOnlineRegisterList, object : V2TIMCallback {
+                override fun onSuccess() {
+                    Log.d(TAG_IM_SESSION, "registerUserOnlineStatus success")
+                }
+
+                override fun onError(code: Int, desc: String?) {
+                    Log.e(TAG_IM_SESSION, "registerUserOnlineStatus fail, code$code, desc:$desc")
+                }
+            })
+    }
+
+    override fun unregisterUserOnlineStatus(uid: String) {
+        userOnlineRegisterList.remove(uid)
+        V2TIMManager.getInstance()
+            .unsubscribeUserStatus(listOf(uid), object : V2TIMCallback {
+                override fun onSuccess() {
+                    Log.d(TAG_IM_SESSION, "unregisterUserOnlineStatus success")
+                }
+
+                override fun onError(code: Int, desc: String?) {
+                    Log.e(TAG_IM_SESSION, "unregisterUserOnlineStatus fail, code$code, desc:$desc")
+                }
+            })
+    }
+
+    private fun registerUserStatusChanged() {
+        Log.d(TAG_IM_SESSION, "registerUserStatusChanged")
+        val v2TIMSDKListener: V2TIMSDKListener = object : V2TIMSDKListener() {
+            override fun onUserStatusChanged(userStatusList: MutableList<V2TIMUserStatus?>?) {
+                Log.d(
+                    TAG_IM_SESSION,
+                    "onUserStatusChanged, ${userStatusList?.joinToString(separator = ",") { "${it?.userID}_${it?.statusType}" }}"
+                )
+                userStatusList?.forEach { status ->
+                    status ?: return@forEach
+                    status.userID ?: return@forEach
+                    ProfileModule.updateUserOnline(
+                        status.userID,
+                        status.statusType == V2TIMUserStatus.V2TIM_USER_STATUS_ONLINE
+                    )
+                }
+            }
+        }
+        V2TIMManager.getInstance().addIMSDKListener(v2TIMSDKListener)
+    }
+
+
 }

+ 15 - 5
module/im/src/main/java/com/adealink/weparty/im/session/comp/SessionTopComp.kt

@@ -68,6 +68,12 @@ class SessionTopComp(
             updateUserInfo(it)
         }
 
+        chatInfo?.let { chatInfo ->
+            ProfileModule.observeUserOnline(chatInfo.id).observe(viewLifecycleOwner) { online ->
+                updateUserOnline(online)
+            }
+        }
+
         followViewModel?.isFollowLD?.observe(viewLifecycleOwner) {
             val uid = chatInfo?.id ?: return@observe
             updateFollow(it[uid]?.isFollowed() ?: false)
@@ -84,11 +90,7 @@ class SessionTopComp(
         chatInfo?.id?.let { uid ->
             profileViewModel?.pullUserInfoBy(uid, false)
             profileViewModel?.isUserOnline(uid)?.observe(viewLifecycleOwner) { online ->
-                if (online) {
-                    topBar.tvUserOnline.text = getCompatString(APP_R.string.common_online)
-                } else {
-                    topBar.tvUserOnline.text = getCompatString(APP_R.string.common_offline)
-                }
+                updateUserOnline(online)
             }
             followViewModel?.isFollow(uid)
         }
@@ -99,6 +101,14 @@ class SessionTopComp(
         topBar.tvUserName.text = userInfo?.nickName
     }
 
+    private fun updateUserOnline(online: Boolean) {
+        if (online) {
+            topBar.tvUserOnline.text = getCompatString(APP_R.string.common_online)
+        } else {
+            topBar.tvUserOnline.text = getCompatString(APP_R.string.common_offline)
+        }
+    }
+
     private fun updateFollow(isFollowed: Boolean) {
         if (isFollowed) {
             topBar.btnFollow.gone()

+ 1 - 0
module/im/src/main/java/com/adealink/weparty/im/session/viewmodel/SessionViewModel.kt

@@ -8,6 +8,7 @@ import com.tencent.qcloud.tuikit.tuichat.TUIChatService
 import com.tencent.qcloud.tuikit.tuichat.bean.ChatInfo
 import com.tencent.qcloud.tuikit.tuichat.interfaces.C2CChatEventListener
 
+
 class SessionViewModel : BaseViewModel() {
 
     private var chatInfo: ChatInfo? = null

+ 13 - 0
module/profile/src/main/java/com/adealink/weparty/profile/ProfileServiceImpl.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.profile
 
+import androidx.lifecycle.LiveData
 import androidx.lifecycle.ViewModelProvider
 import androidx.lifecycle.ViewModelStoreOwner
 import com.adealink.frame.base.Rlt
@@ -69,6 +70,10 @@ class ProfileServiceImpl : IProfileService {
         return profileManager.isUserOnline(uid)
     }
 
+    override fun observeUserOnline(uid: String): LiveData<Boolean> {
+        return profileManager.observeUserOnline(uid)
+    }
+
     override suspend fun isUserOnline(
         uid: String,
         cache: Boolean
@@ -83,6 +88,14 @@ class ProfileServiceImpl : IProfileService {
         return profileManager.isUserOnline(uidSet, cache)
     }
 
+    override fun updateUserOnline(uid: String, online: Boolean) {
+        profileManager.updateUserOnline(uid, online)
+    }
+
+    override fun updateUserOnline(onlineStatus: Map<String, Boolean>) {
+        profileManager.updateUserOnline(onlineStatus)
+    }
+
     override fun getProfileViewModel(owner: ViewModelStoreOwner): IProfileViewModel {
         return ViewModelProvider(owner, ProfileViewModelFactory())[ProfileViewModel::class.java]
     }

+ 7 - 0
module/profile/src/main/java/com/adealink/weparty/profile/manager/IProfileManager.kt

@@ -1,5 +1,6 @@
 package com.adealink.weparty.profile.manager
 
+import androidx.lifecycle.LiveData
 import com.adealink.frame.base.Rlt
 import com.adealink.frame.frame.IBaseFrame
 import com.adealink.weparty.module.profile.data.MyUserInfoData
@@ -36,6 +37,7 @@ interface IProfileManager : IBaseFrame<IProfileListener> {
 
 
     fun isUserOnline(uid: String): Boolean
+    fun observeUserOnline(uid: String): LiveData<Boolean>
 
     suspend fun isUserOnline(
         uid: String,
@@ -47,4 +49,9 @@ interface IProfileManager : IBaseFrame<IProfileListener> {
         cache: Boolean = true,
     ): Rlt<Map<String, Boolean>>
 
+
+    fun updateUserOnline(uid: String, online: Boolean)
+
+    fun updateUserOnline(onlineStatus: Map<String, Boolean>)
+
 }

+ 49 - 5
module/profile/src/main/java/com/adealink/weparty/profile/manager/ProfileManager.kt

@@ -1,5 +1,7 @@
 package com.adealink.weparty.profile.manager
 
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
 import com.adealink.frame.base.CommonDataNullError
 import com.adealink.frame.base.CommonParamError
 import com.adealink.frame.base.Rlt
@@ -21,6 +23,7 @@ import com.adealink.weparty.profile.data.GetUserinfoReq
 import com.adealink.weparty.profile.data.UserOnlineReq
 import com.adealink.weparty.profile.datasource.remote.ProfileHttpService
 import com.adealink.weparty.storage.AppPref
+import kotlinx.coroutines.launch
 
 val profileManager: IProfileManager by lazy { ProfileManager() }
 
@@ -38,7 +41,7 @@ class ProfileManager : BaseFrame<IProfileListener>(), IProfileManager, ILanguage
     }
 
     private val userOnlineCache by lazy {
-        TimeoutLruCache<String, Boolean>(
+        TimeoutLruCache<String, MutableLiveData<Boolean>>(
             1000,
             10 * 60 * 1000
         ) //最多缓存1000个,缓存时长10分钟
@@ -206,7 +209,12 @@ class ProfileManager : BaseFrame<IProfileListener>(), IProfileManager, ILanguage
     }
 
     override fun isUserOnline(uid: String): Boolean {
-        return userOnlineCache[uid] ?: false
+        val liveData = getUserOnlineLiveData(uid)
+        return liveData.value ?: false
+    }
+
+    override fun observeUserOnline(uid: String): LiveData<Boolean> {
+        return getUserOnlineLiveData(uid)
     }
 
     override suspend fun isUserOnline(
@@ -233,8 +241,8 @@ class ProfileManager : BaseFrame<IProfileListener>(), IProfileManager, ILanguage
         val reqUidList = uidSet.toMutableList()
         if (cache) {
             uidSet.forEach { uid ->
-                userOnlineCache[uid]?.let {
-                    onlineMap[uid] = it
+                getUserOnlineLiveData(uid).value?.let { online ->
+                    onlineMap[uid] = online
                     reqUidList.remove(uid)
                 }
             }
@@ -246,13 +254,49 @@ class ProfileManager : BaseFrame<IProfileListener>(), IProfileManager, ILanguage
         val rlt = profileHttpService.isUserOnline(UserOnlineReq(reqUidList))
         if (rlt is Rlt.Success) {
             val onlineResList = rlt.data.data?.list
+            val onlineStatusChanged = mutableMapOf<String, Boolean>()
             onlineResList?.forEach { onlineInfo ->
-                userOnlineCache.put(onlineInfo.userNo, onlineInfo.online)
+                onlineStatusChanged[onlineInfo.userNo] = onlineInfo.online
             }
+            notifyUserOnlineChanged(onlineStatusChanged)
         }
         return Rlt.Success(onlineMap)
     }
 
+    override fun updateUserOnline(uid: String, online: Boolean) {
+        launch {
+            notifyUserOnlineChanged(mapOf(uid to online))
+        }
+    }
+
+    override fun updateUserOnline(onlineStatus: Map<String, Boolean>) {
+        launch {
+            notifyUserOnlineChanged(onlineStatus)
+        }
+    }
+
+    private fun getUserOnlineLiveData(uid: String): MutableLiveData<Boolean> {
+        var livedata = userOnlineCache[uid]
+        if (livedata == null) {
+            livedata = MutableLiveData<Boolean>()
+            userOnlineCache.put(uid, livedata)
+        }
+        return livedata
+    }
+
+    private fun notifyUserOnlineChanged(onlineStatus: Map<String, Boolean>) {
+        launch {
+            for (entry in onlineStatus) {
+                val livedata = getUserOnlineLiveData(entry.key)
+                livedata.postValue(entry.value)
+            }
+            dispatch {
+                it.onUserOnlineStatusChanged(onlineStatus)
+            }
+        }
+    }
+
+
     private fun updateUserInfoCache(
         uid: String,
         userInfo: UserInfo,

+ 16 - 0
module/profile/src/main/java/com/adealink/weparty/profile/relation/adapter/VisitorItemViewBinder.kt

@@ -23,6 +23,22 @@ class VisitorItemViewBinder(val listener: OnVisitorItemClick) :
         holder.binding.tvUserName.text = item.data.nickName
         holder.binding.vGender.setGender(item.data.gender)
         holder.binding.vGender.setAge(item.data.age)
+
+        if (item.data.online) {
+            holder.binding.vOnline.onStart()
+        } else {
+            holder.binding.vOnline.onStop()
+        }
+    }
+
+    override fun onViewDetachedFromWindow(holder: BindingViewHolder<ItemRelationshipVisitorBinding>) {
+        super.onViewDetachedFromWindow(holder)
+        holder.binding.vOnline.onStop()
+    }
+
+    override fun onViewRecycled(holder: BindingViewHolder<ItemRelationshipVisitorBinding>) {
+        holder.binding.vOnline.onStop()
+        super.onViewRecycled(holder)
     }
 
     override fun onCreateViewHolder(

+ 2 - 0
module/profile/src/main/java/com/adealink/weparty/profile/relation/viewmodel/VisitorViewModel.kt

@@ -6,6 +6,7 @@ import com.adealink.frame.mvvm.livedata.ExtLiveData
 import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
 import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.profile.datasource.remote.FollowHttpService
 import com.adealink.weparty.profile.relation.data.VisitorItemData
 import com.adealink.weparty.profile.relation.data.VisitorListReq
@@ -65,6 +66,7 @@ class VisitorViewModel : BaseViewModel() {
                 is Rlt.Success -> {
                     visitorPageHandler.nextPage(rlt.data.data?.next)
                     rlt.data.data?.list?.forEach {
+                        ProfileModule.updateUserOnline(it.uid, it.online)
                         if (!visitorUids.contains(it.uid)) {
                             visitorList.add(VisitorItemData(it))
                             visitorUids.add(it.uid)

+ 5 - 0
module/profile/src/main/java/com/adealink/weparty/profile/search/viewmodel/SearchViewModel.kt

@@ -5,6 +5,7 @@ import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
 import com.adealink.frame.network.data.PageReq
 import com.adealink.weparty.App
+import com.adealink.weparty.module.profile.ProfileModule
 import com.adealink.weparty.network.data.NoMoreDataError
 import com.adealink.weparty.profile.datasource.remote.SearchHttpService
 import com.adealink.weparty.profile.search.data.SearchReq
@@ -52,6 +53,10 @@ class SearchViewModel : BaseViewModel() {
                 is Rlt.Success -> {
                     pageHandler.nextPage(rlt.data.data?.next)
                     searchResultList.addAll(rlt.data.data?.list?.map {
+                        it.online?.let { online ->
+                            ProfileModule.updateUserOnline(it.uid, online)
+                        }
+
                         SearchResultItemData(it)
                     } ?: emptyList())
                     searchRltLD.send(rlt)

+ 6 - 0
module/profile/src/main/java/com/adealink/weparty/profile/viewmodel/ProfileViewModel.kt

@@ -3,6 +3,7 @@ package com.adealink.weparty.profile.viewmodel
 import androidx.lifecycle.LiveData
 import androidx.lifecycle.MutableLiveData
 import com.adealink.frame.base.Rlt
+import com.adealink.frame.mvvm.livedata.ExtMutableLiveData
 import com.adealink.frame.mvvm.livedata.OnceMutableLiveData
 import com.adealink.frame.mvvm.viewmodel.BaseViewModel
 import com.adealink.frame.oss.data.UploadFile
@@ -33,6 +34,7 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener {
 
     override val userInfoLD: LiveData<UserInfo?> = MutableLiveData<UserInfo?>()
     override val myUserInfoLD: LiveData<MyUserInfoData?> = MutableLiveData<MyUserInfoData?>()
+    override val userOnlineLD: LiveData<Map<String, Boolean>> = ExtMutableLiveData()
 
     override fun pullMyUserInfo(): LiveData<Rlt<MyUserInfoData?>> {
         val liveData = OnceMutableLiveData<Rlt<MyUserInfoData?>>()
@@ -199,4 +201,8 @@ class ProfileViewModel : BaseViewModel(), IProfileViewModel, IProfileListener {
         }
         return liveData
     }
+
+    override fun onUserOnlineStatusChanged(online: Map<String, Boolean>) {
+        userOnlineLD.send(online)
+    }
 }

+ 13 - 0
module/profile/src/main/res/layout/item_relationship_visitor.xml

@@ -13,6 +13,19 @@
         app:layout_constraintStart_toStartOf="parent"
         app:layout_constraintTop_toTopOf="parent" />
 
+    <com.adealink.weparty.commonui.ripple.RippleView
+        android:id="@+id/v_online"
+        style="@style/CommonOnlineRipple"
+        android:layout_width="50dp"
+        android:layout_height="50dp"
+        android:visibility="gone"
+        app:layout_constraintBottom_toBottomOf="@id/iv_avatar"
+        app:layout_constraintEnd_toEndOf="@id/iv_avatar"
+        app:layout_constraintStart_toStartOf="@id/iv_avatar"
+        app:layout_constraintTop_toTopOf="@id/iv_avatar"
+        app:ripple_circle_min_radius="23dp"
+        tools:visibility="visible" />
+
     <androidx.appcompat.widget.AppCompatTextView
         android:id="@+id/tv_user_name"
         android:layout_width="wrap_content"