diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 925c94d779..de49ce9181 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -235,11 +235,6 @@
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
-
Unit,
modifier: Modifier = Modifier,
backgroundColor: Color = MixinAppTheme.colors.backgroundWindow,
+ hint: String = stringResource(R.string.Search),
) {
BasicTextField(
value = value,
@@ -67,7 +68,7 @@ fun SearchTextField(
) {
if (value.isEmpty()) {
Text(
- stringResource(R.string.Search),
+ hint,
color = MixinAppTheme.colors.textAssist,
fontSize = 14.sp
)
diff --git a/app/src/main/java/one/mixin/android/ui/common/NavigationController.kt b/app/src/main/java/one/mixin/android/ui/common/NavigationController.kt
index b28857a92c..9d2e3f1b1f 100644
--- a/app/src/main/java/one/mixin/android/ui/common/NavigationController.kt
+++ b/app/src/main/java/one/mixin/android/ui/common/NavigationController.kt
@@ -2,12 +2,10 @@ package one.mixin.android.ui.common
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
-import android.app.Activity
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import one.mixin.android.R
-import one.mixin.android.ui.contacts.ContactsActivity
import one.mixin.android.ui.home.ConversationListFragment
import one.mixin.android.ui.home.ExploreFragment
import one.mixin.android.ui.home.web3.MarketFragment
@@ -61,10 +59,6 @@ class NavigationController() {
}
}
- fun pushContacts(activity: Activity) {
- ContactsActivity.show(activity)
- }
-
fun showSearch(fm: FragmentManager) {
var searchFragment = fm.findFragmentByTag(SearchFragment.TAG)
if (searchFragment == null) {
diff --git a/app/src/main/java/one/mixin/android/ui/common/profile/ProfileFragment.kt b/app/src/main/java/one/mixin/android/ui/common/profile/ProfileFragment.kt
new file mode 100644
index 0000000000..9789dd86fd
--- /dev/null
+++ b/app/src/main/java/one/mixin/android/ui/common/profile/ProfileFragment.kt
@@ -0,0 +1,196 @@
+package one.mixin.android.ui.common.profile
+
+import android.graphics.Typeface
+import android.os.Build
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.PlatformTextStyle
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.fragment.app.viewModels
+import dagger.hilt.android.AndroidEntryPoint
+import one.mixin.android.R
+import one.mixin.android.compose.theme.MixinAppTheme
+import one.mixin.android.extension.navTo
+import one.mixin.android.session.Session
+import one.mixin.android.ui.common.AvatarActivity
+import one.mixin.android.ui.common.BaseFragment
+import one.mixin.android.ui.common.BottomSheetViewModel
+import one.mixin.android.ui.common.QrBottomSheetDialogFragment
+import one.mixin.android.ui.common.ReceiveQrActivity
+import one.mixin.android.ui.contacts.AddPeopleFragment
+import one.mixin.android.ui.contacts.ContactListFragment
+import one.mixin.android.ui.contacts.ContactPageHeader
+import one.mixin.android.ui.contacts.NewChatBottomSheetDialogFragment
+import one.mixin.android.ui.group.GroupActivity
+import one.mixin.android.ui.setting.SettingActivity
+import one.mixin.android.ui.setting.member.MixinMemberUpgradeBottomSheetDialogFragment
+import one.mixin.android.vo.toUser
+import one.mixin.android.widget.AvatarView
+import one.mixin.android.widget.NameTextView
+
+@AndroidEntryPoint
+class ProfileFragment : BaseFragment() {
+ companion object {
+ const val TAG = "ProfileFragment"
+ }
+
+ private val viewModel by viewModels()
+ private val self by lazy { viewModel.observeSelf() }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
+ ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ val user by self.observeAsState(Session.getAccount()?.toUser())
+ MixinAppTheme {
+ Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) {
+ ContactPageHeader(stringResource(R.string.Profile), { requireActivity().onBackPressedDispatcher.onBackPressed() })
+ LazyColumn(
+ contentPadding = PaddingValues(start = 16.dp, end = 16.dp, bottom = 32.dp),
+ verticalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ item {
+ Column(Modifier.fillMaxWidth().padding(top = 30.dp, bottom = 20.dp), horizontalAlignment = Alignment.CenterHorizontally) {
+ user?.let { user ->
+ AndroidView(
+ factory = { AvatarView(it) },
+ modifier = Modifier.size(90.dp),
+ update = { avatar ->
+ avatar.setInfo(user.fullName, user.avatarUrl, user.userId)
+ avatar.contentDescription = getString(R.string.Profile)
+ avatar.setOnClickListener {
+ user.avatarUrl?.takeIf { it.isNotBlank() }?.let { AvatarActivity.show(requireActivity(), it, avatar) }
+ }
+ },
+ )
+ Spacer(Modifier.height(10.dp))
+ AndroidView(
+ factory = {
+ NameTextView(it).apply {
+ textView.textSize = 18f
+ textView.letterSpacing = -0.4f / 18f
+ textView.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ Typeface.create(textView.typeface, 600, false)
+ } else {
+ Typeface.create(textView.typeface, Typeface.BOLD)
+ }
+ }
+ },
+ update = { name ->
+ name.setName(user)
+ name.setOnIconClickListener {
+ Session.getAccount()?.membership?.plan?.let {
+ MixinMemberUpgradeBottomSheetDialogFragment.newInstance(it).showNow(parentFragmentManager, MixinMemberUpgradeBottomSheetDialogFragment.TAG)
+ }
+ }
+ },
+ )
+ Spacer(Modifier.height(4.dp))
+ Text(stringResource(R.string.contact_mixin_id, user.identityNumber), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, lineHeight = 14.sp, letterSpacing = 0.sp)
+ if (!user.biography.isNullOrBlank()) {
+ Spacer(Modifier.height(12.dp))
+ Text(user.biography.orEmpty(), color = MixinAppTheme.colors.textMinor, fontSize = 14.sp, lineHeight = 24.sp, letterSpacing = 0.sp, textAlign = androidx.compose.ui.text.style.TextAlign.Center)
+ }
+ }
+ }
+ }
+ item {
+ ProfileMenuGroup {
+ ProfileMenuItem(R.drawable.ic_profile_details, R.string.Profile) {
+ ProfileBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, ProfileBottomSheetDialogFragment.TAG)
+ }
+ ProfileMenuItem(R.drawable.ic_profile_qr, R.string.My_QR_Code) {
+ Session.getAccountId()?.let {
+ QrBottomSheetDialogFragment.newInstance(it, QrBottomSheetDialogFragment.TYPE_MY_QR).showNow(parentFragmentManager, QrBottomSheetDialogFragment.TAG)
+ }
+ }
+ ProfileMenuItem(R.drawable.ic_profile_receive, R.string.Receive_Money) {
+ Session.getAccountId()?.let { ReceiveQrActivity.show(requireContext(), it) }
+ }
+ }
+ }
+ item {
+ ProfileMenuGroup {
+ ProfileMenuItem(R.drawable.ic_profile_chat, R.string.new_chat) {
+ NewChatBottomSheetDialogFragment().showNow(parentFragmentManager, NewChatBottomSheetDialogFragment.TAG)
+ }
+ ProfileMenuItem(R.drawable.ic_profile_group, R.string.New_Group) { GroupActivity.show(requireContext()) }
+ }
+ }
+ item {
+ ProfileMenuGroup {
+ ProfileMenuItem(R.drawable.ic_profile_add_contact, R.string.Add_Contact) { navTo(AddPeopleFragment.newInstance(), AddPeopleFragment.TAG) }
+ ProfileMenuItem(R.drawable.ic_profile_contacts, R.string.My_Contacts) { navTo(ContactListFragment(), ContactListFragment.TAG) }
+ }
+ }
+ item {
+ ProfileMenuGroup {
+ ProfileMenuItem(R.drawable.ic_profile_settings, R.string.Settings) { SettingActivity.show(requireContext(), compose = false) }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ viewModel.refreshAccount()
+ }
+}
+
+@Composable
+private fun ProfileMenuGroup(content: @Composable ColumnScope.() -> Unit) {
+ Column(Modifier.fillMaxWidth().clip(RoundedCornerShape(13.dp)).background(MixinAppTheme.colors.backgroundWindow), content = content)
+}
+
+@Composable
+private fun ProfileMenuItem(icon: Int, title: Int, onClick: () -> Unit) {
+ Row(
+ Modifier.fillMaxWidth().clickable(onClick = onClick).heightIn(min = 64.dp).padding(start = 20.dp, end = 10.dp, top = 17.dp, bottom = 17.dp),
+ verticalAlignment = Alignment.Top,
+ ) {
+ Icon(painterResource(icon), null, tint = Color.Unspecified, modifier = Modifier.size(24.dp))
+ Spacer(Modifier.width(16.dp))
+ Text(stringResource(title), color = MixinAppTheme.colors.textPrimary, fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.sp,
+ style = TextStyle(platformStyle = PlatformTextStyle(includeFontPadding = false)), modifier = Modifier.weight(1f).padding(vertical = 2.dp))
+ Spacer(Modifier.width(8.dp))
+ Icon(painterResource(R.drawable.ic_profile_arrow), null, tint = Color.Unspecified, modifier = Modifier.size(30.dp))
+ }
+}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/ContactBottomSheetDialog.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactBottomSheetDialog.kt
deleted file mode 100644
index 2988cacd34..0000000000
--- a/app/src/main/java/one/mixin/android/ui/contacts/ContactBottomSheetDialog.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-@file:Suppress("DEPRECATION")
-
-package one.mixin.android.ui.contacts
-
-import android.annotation.SuppressLint
-import android.app.Dialog
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import one.mixin.android.Constants.ARGS_USER
-import one.mixin.android.R
-import one.mixin.android.databinding.FragmentContactBottomSheetBinding
-import one.mixin.android.ui.common.MixinBottomSheetDialogFragment
-import one.mixin.android.util.viewBinding
-import one.mixin.android.vo.User
-import one.mixin.android.widget.BottomSheet
-
-class ContactBottomSheetDialog : MixinBottomSheetDialogFragment() {
- companion object {
- const val TAG = "ContactBottomSheetDialog"
-
- fun newInstance(user: User) =
- ContactBottomSheetDialog().apply {
- arguments =
- Bundle().apply {
- putParcelable(ARGS_USER, user)
- }
- }
- }
-
- private val user: User by lazy { requireArguments().getParcelable(ARGS_USER)!! }
-
- private val binding by viewBinding(FragmentContactBottomSheetBinding::inflate)
-
- @SuppressLint("RestrictedApi")
- override fun setupDialog(
- dialog: Dialog,
- style: Int,
- ) {
- super.setupDialog(dialog, style)
- contentView = binding.root
- (dialog as BottomSheet).setCustomView(contentView)
-
- binding.apply {
- index.text = if (user.fullName != null && user.fullName!!.isNotEmpty()) user.fullName!![0].toString() else ""
- name.text = user.fullName
- mobileTv.text = getString(R.string.contact_mobile, user.phone)
- inviteTv.setOnClickListener { openSms(user.phone) }
- }
- }
-
- private fun openSms(mobile: String?) {
- if (mobile == null) {
- return
- }
- val smsUri = Uri.parse("smsto:$mobile")
- val smsIntent = Intent(Intent.ACTION_SENDTO, smsUri)
- smsIntent.putExtra("sms_body", getString(R.string.contact_invite_content))
- startActivity(smsIntent)
- }
-}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt
new file mode 100644
index 0000000000..c9a2240c3b
--- /dev/null
+++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt
@@ -0,0 +1,38 @@
+package one.mixin.android.ui.contacts
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.ViewCompositionStrategy
+import androidx.fragment.app.viewModels
+import dagger.hilt.android.AndroidEntryPoint
+import one.mixin.android.extension.navTo
+import one.mixin.android.ui.common.BaseFragment
+import one.mixin.android.ui.common.showUserBottom
+
+@AndroidEntryPoint
+class ContactListFragment : BaseFragment() {
+ companion object {
+ const val TAG = "ContactListFragment"
+ }
+
+ private val viewModel by viewModels()
+ private val contacts by lazy { viewModel.findContacts() }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?) =
+ ComposeView(requireContext()).apply {
+ setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ setContent {
+ val users by contacts.observeAsState(emptyList())
+ ContactsPage(
+ contacts = users,
+ onBack = { requireActivity().onBackPressedDispatcher.onBackPressed() },
+ onContact = { showUserBottom(parentFragmentManager, it) },
+ onAddContact = { navTo(AddPeopleFragment.newInstance(), AddPeopleFragment.TAG) },
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/ContactsActivity.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactsActivity.kt
deleted file mode 100644
index 4de90cd829..0000000000
--- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsActivity.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-@file:Suppress("DEPRECATION")
-
-package one.mixin.android.ui.contacts
-
-import android.app.Activity
-import android.content.Intent
-import android.os.Bundle
-import dagger.hilt.android.AndroidEntryPoint
-import one.mixin.android.R
-import one.mixin.android.extension.replaceFragment
-import one.mixin.android.ui.common.BlazeBaseActivity
-
-@AndroidEntryPoint
-class ContactsActivity : BlazeBaseActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- setContentView(R.layout.activity_contact)
- replaceFragment(ContactsFragment.newInstance(), R.id.container, ContactsFragment.TAG)
- }
-
- override fun onActivityResult(
- requestCode: Int,
- resultCode: Int,
- data: Intent?,
- ) {
- super.onActivityResult(requestCode, resultCode, data)
- val contactsFragment = supportFragmentManager.findFragmentByTag(ContactsFragment.TAG)
- contactsFragment?.onActivityResult(requestCode, resultCode, data)
- }
-
- override fun finish() {
- super.finish()
- overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right)
- }
-
- companion object {
- fun show(activity: Activity) {
- val intent = Intent(activity, ContactsActivity::class.java)
- activity.startActivity(intent)
- activity.overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)
- }
- }
-}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/ContactsAdapter.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactsAdapter.kt
deleted file mode 100644
index 0814282bc9..0000000000
--- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsAdapter.kt
+++ /dev/null
@@ -1,292 +0,0 @@
-package one.mixin.android.ui.contacts
-
-import android.content.Context
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewGroup
-import androidx.core.view.isVisible
-import androidx.recyclerview.widget.RecyclerView
-import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersAdapter
-import one.mixin.android.R
-import one.mixin.android.databinding.ItemContactContactBinding
-import one.mixin.android.databinding.ItemContactHeaderBinding
-import one.mixin.android.databinding.ItemContactNormalBinding
-import one.mixin.android.databinding.ViewContactHeaderBinding
-import one.mixin.android.databinding.ViewContactListEmptyBinding
-import one.mixin.android.session.Session
-import one.mixin.android.vo.Account
-import one.mixin.android.vo.User
-import one.mixin.android.vo.toUser
-
-class ContactsAdapter(val context: Context, var users: List, var friendSize: Int) :
- RecyclerView.Adapter(),
- StickyRecyclerHeadersAdapter {
- companion object {
- const val TYPE_HEADER = 0
- const val TYPE_FOOTER = 1
- const val TYPE_FRIEND = 2
- const val TYPE_CONTACT = 3
-
- const val POS_HEADER = 0
- const val POS_FRIEND = 1
- }
-
- private var mHeaderView: View? = null
- private var mHeaderViewBinding: ViewContactHeaderBinding? = null
- private var mFooterView: View? = null
- private var mFooterViewBinding: ViewContactListEmptyBinding? = null
- private var mContactListener: ContactListener? = null
- var me: User? = null
-
- override fun getItemCount(): Int {
- return if (mHeaderView == null && mFooterView == null) {
- users.size
- } else if (mHeaderView == null && mFooterView != null) {
- users.size + 1
- } else if (mHeaderView != null && mFooterView == null) {
- users.size + 1
- } else {
- users.size + 2
- }
- }
-
- override fun getHeaderId(position: Int): Long {
- if (mHeaderView != null && position == POS_HEADER) {
- return -1
- } else if (mFooterView != null && position == itemCount - 1) {
- return -1
- } else if (friendSize > 0 && position >= POS_FRIEND && position < POS_FRIEND + friendSize) {
- return POS_FRIEND.toLong()
- }
- val u = users[getPosition(position)]
- return if (!u.fullName.isNullOrEmpty()) u.fullName[0].code.toLong() else -1L
- }
-
- override fun getItemViewType(position: Int): Int {
- if (mHeaderView == null && mFooterView == null) {
- return if (friendSize == users.size) {
- TYPE_FRIEND
- } else {
- TYPE_CONTACT
- }
- }
- return when {
- position == POS_HEADER -> TYPE_HEADER
- position == itemCount - 1 -> {
- if (mFooterView != null) {
- return TYPE_FOOTER
- }
- return if (friendSize == users.size) {
- TYPE_FRIEND
- } else {
- TYPE_CONTACT
- }
- }
-
- position < POS_FRIEND + friendSize -> TYPE_FRIEND
- else -> TYPE_CONTACT
- }
- }
-
- override fun onCreateHeaderViewHolder(parent: ViewGroup): HeaderViewHolder {
- return HeaderViewHolder(ItemContactHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
-
- override fun onBindHeaderViewHolder(
- holder: HeaderViewHolder,
- position: Int,
- ) {
- if (friendSize > 0 && position < POS_FRIEND + friendSize && position >= POS_HEADER) {
- holder.bind()
- return
- }
- val user =
- try {
- users[getPosition(position)]
- } catch (e: IndexOutOfBoundsException) {
- return
- }
- holder.bind(user)
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): ViewHolder {
- return if (mHeaderView != null && viewType == TYPE_HEADER) {
- HeadViewHolder(mHeaderViewBinding!!)
- } else if (mFooterView != null && viewType == TYPE_FOOTER) {
- FootViewHolder(mFooterViewBinding!!)
- } else if (viewType == TYPE_CONTACT) {
- ContactViewHolder(ItemContactContactBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- } else {
- FriendViewHolder(ItemContactNormalBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
- }
-
- override fun onBindViewHolder(
- holder: ViewHolder,
- position: Int,
- ) {
- when (holder) {
- is HeadViewHolder -> {
- holder.bind(requireNotNull(Session.getAccount()), mContactListener)
- }
-
- is FootViewHolder -> {
- holder.bind(mContactListener, friendSize)
- }
-
- is ContactViewHolder -> {
- val user: User = users[getPosition(position)]
- holder.bind(user, mContactListener)
- }
-
- else -> {
- holder as FriendViewHolder
- val user: User = users[getPosition(position)]
- holder.bind(user, mContactListener)
- }
- }
- }
-
- private fun getPosition(position: Int): Int {
- return if (mHeaderView != null) {
- position - 1
- } else {
- position
- }
- }
-
- fun setHeader(binding: ViewContactHeaderBinding) {
- mHeaderViewBinding = binding
- mHeaderView = binding.root
- }
-
- fun setFooter(binding: ViewContactListEmptyBinding) {
- mFooterViewBinding = binding
- mFooterView = binding.root
- }
-
- fun showEmptyFooter() {
- mFooterViewBinding?.apply {
- emptyRl.isVisible = true
- emptyTipTv.isVisible = true
- }
- }
-
- fun hideEmptyFooter() {
- mFooterViewBinding?.apply {
- emptyRl.isVisible = false
- emptyTipTv.isVisible = false
- }
- }
-
- fun setContactListener(listener: ContactListener) {
- mContactListener = listener
- }
-
- class HeaderViewHolder(val binding: ItemContactHeaderBinding) : RecyclerView.ViewHolder(binding.root) {
- fun bind() {
- binding.header.text = itemView.context.getString(R.string.CONTACTS)
- }
-
- fun bind(user: User) {
- binding.header.text =
- if (!user.fullName.isNullOrEmpty()
- ) {
- user.fullName[0].toString()
- } else {
- ""
- }
- }
- }
-
- open class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
-
- class HeadViewHolder(val binding: ViewContactHeaderBinding) : ViewHolder(binding.root) {
- fun bind(
- account: Account,
- listener: ContactListener?,
- ) {
- val self = account.toUser()
- binding.contactHeaderAvatar.setInfo(account.fullName, account.avatarUrl, account.userId)
- binding.contactHeaderNameTv.setName(account)
- binding.contactHeaderIdTv.text =
- itemView.context.getString(R.string.contact_mixin_id, account.identityNumber)
- binding.contactHeaderMobileTv.text =
- itemView.context.getString(R.string.contact_mobile, account.phone)
- if (listener != null) {
- binding.contactHeaderRl.setOnClickListener { listener.onHeaderRl() }
- binding.newGroupRl.setOnClickListener { listener.onNewGroup() }
- binding.addContactRl.setOnClickListener { listener.onAddContact() }
- binding.myQrFl.setOnClickListener { listener.onMyQr(self) }
- binding.receiveFl.setOnClickListener { listener.onReceiveQr(self) }
- }
- }
- }
-
- class FootViewHolder(val binding: ViewContactListEmptyBinding) : ViewHolder(binding.root) {
- fun bind(
- listener: ContactListener?,
- friendSize: Int,
- ) {
- if (listener != null) {
- binding.emptyRl.setOnClickListener { listener.onEmptyRl() }
- binding.countTv.text = itemView.context.resources.getQuantityString(R.plurals.contact_count, friendSize, friendSize)
- }
- }
- }
-
- class FriendViewHolder(val binding: ItemContactNormalBinding) : ViewHolder(binding.root) {
- fun bind(
- user: User,
- listener: ContactListener?,
- ) {
- binding.apply {
- normal.setName(user)
- mixinIdTv.text = user.identityNumber
- avatar.setInfo(user.fullName, user.avatarUrl, user.userId)
- if (listener != null) {
- itemView.setOnClickListener { listener.onFriendItem(user) }
- }
- }
- }
- }
-
- class ContactViewHolder(val binding: ItemContactContactBinding) : ViewHolder(binding.root) {
- fun bind(
- user: User,
- listener: ContactListener?,
- ) {
- binding.index.text =
- if (!user.fullName.isNullOrEmpty()) {
- user.fullName[0].toString()
- } else {
- ""
- }
- binding.contactFriend.setName(user)
- if (listener != null) {
- itemView.setOnClickListener { listener.onContactItem(user) }
- }
- }
- }
-
- interface ContactListener {
- fun onHeaderRl()
-
- fun onNewGroup()
-
- fun onAddContact()
-
- fun onEmptyRl()
-
- fun onFriendItem(user: User)
-
- fun onContactItem(user: User)
-
- fun onMyQr(self: User?)
-
- fun onReceiveQr(self: User?)
- }
-}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/ContactsFragment.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactsFragment.kt
deleted file mode 100644
index a65d31e375..0000000000
--- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsFragment.kt
+++ /dev/null
@@ -1,235 +0,0 @@
-package one.mixin.android.ui.contacts
-
-import android.Manifest
-import android.annotation.SuppressLint
-import android.content.pm.PackageManager
-import android.os.Bundle
-import android.view.LayoutInflater
-import android.view.View
-import androidx.fragment.app.viewModels
-import androidx.recyclerview.widget.LinearLayoutManager
-import com.timehop.stickyheadersrecyclerview.StickyRecyclerHeadersDecoration
-import com.uber.autodispose.autoDispose
-import dagger.hilt.android.AndroidEntryPoint
-import io.reactivex.android.schedulers.AndroidSchedulers
-import io.reactivex.schedulers.Schedulers
-import one.mixin.android.Constants.Account.PREF_DELETE_MOBILE_CONTACTS
-import one.mixin.android.R
-import one.mixin.android.RxBus
-import one.mixin.android.databinding.FragmentContactsBinding
-import one.mixin.android.databinding.ViewContactHeaderBinding
-import one.mixin.android.databinding.ViewContactListEmptyBinding
-import one.mixin.android.event.MembershipEvent
-import one.mixin.android.extension.addFragment
-import one.mixin.android.extension.defaultSharedPreferences
-import one.mixin.android.extension.openPermissionSetting
-import one.mixin.android.job.MixinJobManager
-import one.mixin.android.job.RefreshContactJob
-import one.mixin.android.job.UploadContactsJob
-import one.mixin.android.ui.common.BaseFragment
-import one.mixin.android.ui.common.QrBottomSheetDialogFragment
-import one.mixin.android.ui.common.QrBottomSheetDialogFragment.Companion.TYPE_MY_QR
-import one.mixin.android.ui.common.ReceiveQrActivity
-import one.mixin.android.ui.common.profile.ProfileBottomSheetDialogFragment
-import one.mixin.android.ui.conversation.ConversationActivity
-import one.mixin.android.ui.group.GroupActivity
-import one.mixin.android.ui.setting.SettingActivity
-import one.mixin.android.util.rxcontact.Contact
-import one.mixin.android.util.rxcontact.RxContacts
-import one.mixin.android.util.rxpermission.RxPermissions
-import one.mixin.android.util.viewBinding
-import one.mixin.android.vo.User
-import one.mixin.android.vo.UserRelationship
-import java.util.Collections
-import javax.inject.Inject
-
-@AndroidEntryPoint
-class ContactsFragment : BaseFragment(R.layout.fragment_contacts) {
- @Inject
- lateinit var jobManager: MixinJobManager
-
- private val contactsViewModel: ContactViewModel by viewModels()
-
- private val contactAdapter: ContactsAdapter by lazy {
- ContactsAdapter(requireContext(), Collections.emptyList(), 0)
- }
-
- companion object {
- const val TAG = "ContactsFragment"
-
- fun newInstance() = ContactsFragment()
- }
-
- private val binding by viewBinding(FragmentContactsBinding::bind)
-
- @SuppressLint("NotifyDataSetChanged")
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
- super.onViewCreated(view, savedInstanceState)
- binding.apply {
- contactRecyclerView.adapter = contactAdapter
- contactRecyclerView.setHasFixedSize(true)
- contactRecyclerView.layoutManager = LinearLayoutManager(context)
- contactRecyclerView.addItemDecoration(StickyRecyclerHeadersDecoration(contactAdapter))
- val header = ViewContactHeaderBinding.inflate(LayoutInflater.from(context), contactRecyclerView, false)
- contactAdapter.setHeader(header)
- val footer = ViewContactListEmptyBinding.inflate(LayoutInflater.from(context), contactRecyclerView, false)
- contactAdapter.setFooter(footer)
- if (!hasContactPermission()) {
- contactAdapter.showEmptyFooter()
- } else {
- contactAdapter.hideEmptyFooter()
- }
- contactAdapter.setContactListener(mContactListener)
- titleView.leftIb.setOnClickListener { activity?.onBackPressedDispatcher?.onBackPressed() }
- titleView.rightAnimator.setOnClickListener {
- SettingActivity.show(requireContext(), compose = false)
- }
- titleView.rightAnimator.setOnLongClickListener {
- SettingActivity.show(requireContext())
- true
- }
- }
-
- if (hasContactPermission() &&
- !defaultSharedPreferences.getBoolean(PREF_DELETE_MOBILE_CONTACTS, false)
- ) {
- fetchContacts()
- }
-
- contactsViewModel.findContacts().observe(
- viewLifecycleOwner,
- ) { users ->
- if (users != null && users.isNotEmpty()) {
- if (!hasContactPermission()) {
- contactAdapter.friendSize = users.size
- contactAdapter.users = users
- } else {
- val newList =
- arrayListOf().apply {
- addAll(users)
- addAll(contactAdapter.users.filter { it.relationship != UserRelationship.FRIEND.name })
- }
- contactAdapter.friendSize = users.size
- contactAdapter.users = newList
- }
- } else {
- if (!hasContactPermission()) {
- contactAdapter.users = Collections.emptyList()
- }
- }
- contactAdapter.notifyDataSetChanged()
- }
- contactsViewModel.findSelf().observe(
- viewLifecycleOwner,
- ) { self ->
- if (self != null) {
- contactAdapter.me = self
- contactAdapter.notifyDataSetChanged()
- }
- }
- RxBus.listen(MembershipEvent::class.java)
- .observeOn(AndroidSchedulers.mainThread())
- .autoDispose(pauseScope)
- .subscribe { _ ->
- if (isAdded) {
- contactAdapter.notifyDataSetChanged()
- }
- }
- }
-
- private fun hasContactPermission() =
- requireContext().checkSelfPermission(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED
-
- @SuppressLint("NotifyDataSetChanged")
- private fun fetchContacts() {
- RxContacts.fetch(requireContext())
- .toSortedList(Contact::compareTo)
- .observeOn(AndroidSchedulers.mainThread())
- .subscribeOn(Schedulers.io())
- .autoDispose(stopScope)
- .subscribe(
- { contacts ->
- val mutableList = mutableListOf()
- for (item in contacts) {
- item.phoneNumbers.mapTo(mutableList) {
- User(
- "",
- "",
- "contact",
- "",
- item.displayName,
- "",
- it,
- false,
- "",
- null,
- )
- }
- }
- mutableList.addAll(0, contactAdapter.users)
- contactAdapter.users = mutableList
- contactAdapter.notifyDataSetChanged()
- },
- { },
- )
- jobManager.addJobInBackground(UploadContactsJob())
- }
-
- private val mContactListener: ContactsAdapter.ContactListener =
- object : ContactsAdapter.ContactListener {
- override fun onHeaderRl() {
- ProfileBottomSheetDialogFragment.newInstance().showNow(
- parentFragmentManager,
- ProfileBottomSheetDialogFragment.TAG,
- )
- }
-
- override fun onNewGroup() {
- GroupActivity.show(requireContext())
- }
-
- override fun onAddContact() {
- activity?.addFragment(this@ContactsFragment, AddPeopleFragment.newInstance(), AddPeopleFragment.TAG)
- }
-
- override fun onEmptyRl() {
- RxPermissions(requireActivity())
- .request(Manifest.permission.READ_CONTACTS)
- .autoDispose(stopScope)
- .subscribe { granted ->
- if (granted) {
- contactAdapter.hideEmptyFooter()
- jobManager.addJobInBackground(UploadContactsJob())
- fetchContacts()
- jobManager.addJobInBackground(RefreshContactJob())
- } else {
- context?.openPermissionSetting()
- }
- }
- }
-
- override fun onFriendItem(user: User) {
- context?.let { ctx -> ConversationActivity.show(ctx, null, user.userId) }
- }
-
- override fun onContactItem(user: User) {
- ContactBottomSheetDialog.newInstance(user).showNow(parentFragmentManager, ContactBottomSheetDialog.TAG)
- }
-
- override fun onMyQr(self: User?) {
- self?.let {
- QrBottomSheetDialogFragment.newInstance(it.userId, TYPE_MY_QR)
- .showNow(parentFragmentManager, QrBottomSheetDialogFragment.TAG)
- }
- }
-
- override fun onReceiveQr(self: User?) {
- self?.let {
- ReceiveQrActivity.show(requireContext(), it.userId)
- }
- }
- }
-}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt
new file mode 100644
index 0000000000..9b53559bf0
--- /dev/null
+++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt
@@ -0,0 +1,232 @@
+package one.mixin.android.ui.contacts
+
+import android.icu.text.AlphabeticIndex
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.awaitEachGesture
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.drag
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.BoxWithConstraintsScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.onClick
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import java.util.Locale
+import kotlinx.coroutines.launch
+import one.mixin.android.R
+import one.mixin.android.compose.UserAvatarImage
+import one.mixin.android.compose.theme.MixinAppTheme
+import one.mixin.android.vo.User
+import one.mixin.android.widget.NameTextView
+
+private val contactIndex by lazy { AlphabeticIndex(Locale.SIMPLIFIED_CHINESE).addLabels(Locale.ENGLISH).buildImmutableIndex() }
+private val contactLetters = ('A'..'Z').map { it.toString() } + "#"
+
+internal fun contactInitial(name: String?): String {
+ val index = contactIndex
+ return index.getBucket(index.getBucketIndex(name.orEmpty().trim()))?.label?.takeIf { it.length == 1 && it[0] in 'A'..'Z' } ?: "#"
+}
+
+@Composable
+internal fun ContactsPage(
+ contacts: List,
+ onBack: () -> Unit,
+ onContact: (User) -> Unit,
+ onNewGroup: (() -> Unit)? = null,
+ onAddContact: (() -> Unit)? = null,
+) {
+ var query by rememberSaveable { mutableStateOf("") }
+ val newChat = onNewGroup != null
+ val groups = remember(contacts, query) {
+ val keyword = query.trim()
+ contacts.filter { it.fullName.orEmpty().contains(keyword, true) || it.identityNumber.contains(keyword, true) || it.phone.orEmpty().contains(keyword, true) }
+ .groupBy { contactInitial(it.fullName) }
+ .toSortedMap(compareBy { if (it == "#") "[" else it })
+ }
+ val listState = rememberLazyListState()
+ val sectionOffsets = remember(groups, newChat) {
+ var offset = if (newChat) 2 else 0
+ groups.mapValues { (_, users) -> offset.also { offset += users.size + 1 } }
+ }
+ MixinAppTheme {
+ val nameColor = MixinAppTheme.colors.textMinor.toArgb()
+ Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) {
+ ContactPageHeader(stringResource(if (newChat) R.string.new_chat else R.string.contacts_title), onBack, newChat, onAddContact)
+ ContactsSearchField(query, { query = it }, stringResource(R.string.contacts_search_hint), newChat)
+ BoxWithConstraints(Modifier.weight(1f)) {
+ LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
+ if (onNewGroup != null && onAddContact != null) {
+ item { ContactAction(R.drawable.ic_contacts_new_group, R.string.New_Group, onNewGroup) }
+ item { ContactAction(R.drawable.ic_contacts_add_contact, R.string.Add_Contact, onAddContact) }
+ }
+ if (groups.isEmpty()) {
+ item { Text(stringResource(R.string.NO_RESULTS), color = MixinAppTheme.colors.textAssist, modifier = Modifier.padding(24.dp)) }
+ }
+ groups.forEach { (initial, users) ->
+ item(key = "header:$initial") {
+ ContactSectionHeader(initial)
+ }
+ items(users, key = { it.userId }) { user ->
+ Row(Modifier.fillMaxWidth().clickable { onContact(user) }.height(70.dp).padding(start = 20.dp, end = 40.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically) {
+ UserAvatarImage(user, if (newChat) 50.dp else 42.dp)
+ Spacer(Modifier.width(16.dp))
+ Column(Modifier.weight(1f)) {
+ AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user); it.setTextColor(nameColor) })
+ Spacer(Modifier.height(4.dp))
+ Text(user.identityNumber, color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp)
+ }
+ }
+ }
+ }
+ }
+ ContactAlphabetIndex(sectionOffsets, listState, if (newChat) 140.dp else 60.dp)
+ }
+ }
+ }
+}
+
+@Composable
+internal fun BoxWithConstraintsScope.ContactAlphabetIndex(sectionOffsets: Map, listState: LazyListState, topOffset: Dp = 60.dp) {
+ if (sectionOffsets.isEmpty()) return
+ val scope = rememberCoroutineScope()
+ val scrollToLetter: (String) -> Unit = { letter ->
+ sectionOffsets[letter]?.let { offset -> scope.launch { listState.scrollToItem(offset) } }
+ }
+ val railTop = minOf(topOffset, (maxHeight - 378.dp).coerceAtLeast(0.dp))
+ Column(
+ Modifier.align(Alignment.TopEnd).padding(top = railTop, end = 8.dp)
+ .height((maxHeight - railTop).coerceAtMost(378.dp))
+ .pointerInput(sectionOffsets) {
+ awaitEachGesture {
+ val down = awaitFirstDown()
+ down.consume()
+ var previousLetter: String? = null
+ fun selectLetter(y: Float) {
+ val letter = contactLetterAt(y, size.height)
+ if (letter != previousLetter) {
+ previousLetter = letter
+ scrollToLetter(letter)
+ }
+ }
+ selectLetter(down.position.y)
+ drag(down.id) { change ->
+ change.consume()
+ selectLetter(change.position.y)
+ }
+ }
+ },
+ ) {
+ contactLetters.forEach { initial ->
+ Text(initial, color = Color(0xFFB8BDC7), fontSize = 11.sp, lineHeight = 14.sp, letterSpacing = 0.sp,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.width(32.dp).weight(1f).semantics {
+ onClick { scrollToLetter(initial); true }
+ })
+ }
+ }
+}
+
+internal fun contactLetterAt(y: Float, height: Int): String =
+ contactLetters[(y / height.coerceAtLeast(1) * contactLetters.size).toInt().coerceIn(contactLetters.indices)]
+
+@Composable
+internal fun ContactPageHeader(title: String, onBack: () -> Unit, close: Boolean = false, onAddContact: (() -> Unit)? = null) {
+ Row(Modifier.fillMaxWidth().height(if (close) 70.dp else 64.dp).padding(end = if (close) 5.dp else 0.dp), verticalAlignment = Alignment.CenterVertically) {
+ if (!close) IconButton(onClick = onBack) { Icon(painterResource(R.drawable.ic_back), stringResource(R.string.Cancel), tint = MixinAppTheme.colors.textPrimary) }
+ Text(title, color = if (close) MixinAppTheme.colors.textMinor else MixinAppTheme.colors.textPrimary, fontSize = 18.sp, lineHeight = 21.sp, letterSpacing = if (close) (-0.4).sp else 0.sp, fontWeight = if (close) FontWeight.W600 else FontWeight.Normal,
+ modifier = Modifier.weight(1f).padding(start = if (close) 16.dp else 0.dp), textAlign = if (close) TextAlign.Start else TextAlign.Center)
+ if (close) {
+ IconButton(onClick = onBack) { Icon(painterResource(R.drawable.ic_circle_close), stringResource(R.string.Close), tint = Color.Unspecified, modifier = Modifier.size(26.dp)) }
+ } else if (onAddContact != null) {
+ IconButton(onClick = onAddContact) { Icon(painterResource(R.drawable.ic_profile_add_contact), stringResource(R.string.Add_Contact), tint = MixinAppTheme.colors.textPrimary, modifier = Modifier.size(24.dp)) }
+ } else Spacer(Modifier.width(48.dp))
+ }
+}
+
+@Composable
+private fun ContactAction(icon: Int, title: Int, onClick: () -> Unit) {
+ Row(Modifier.fillMaxWidth().clickable(onClick = onClick).height(70.dp).padding(start = 20.dp, end = 16.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(painterResource(icon), null, tint = Color.Unspecified, modifier = Modifier.size(50.dp))
+ Spacer(Modifier.width(16.dp))
+ Text(stringResource(title), color = MixinAppTheme.colors.accent, fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.sp, modifier = Modifier.weight(1f))
+ Icon(painterResource(R.drawable.ic_profile_arrow), null, tint = Color.Unspecified, modifier = Modifier.size(30.dp))
+ }
+}
+
+@Composable
+internal fun ContactSectionHeader(title: String) {
+ Text(title, color = MixinAppTheme.colors.textMinor, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp,
+ modifier = Modifier.fillMaxWidth().height(47.dp).padding(start = 20.dp, top = 10.dp))
+}
+
+@Composable
+internal fun ContactsSearchField(value: String, onValueChange: (String) -> Unit, hint: String, newChat: Boolean = false) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, top = if (newChat) 0.dp else 10.dp, bottom = 20.dp)
+ .background(MixinAppTheme.colors.backgroundGrayLight, RoundedCornerShape(20.dp)),
+ textStyle = TextStyle(color = MixinAppTheme.colors.textPrimary, fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.sp),
+ singleLine = true,
+ cursorBrush = SolidColor(MixinAppTheme.colors.accent),
+ decorationBox = { innerTextField ->
+ Row(Modifier.height(40.dp).padding(start = 14.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(painterResource(R.drawable.ic_contacts_search), null, tint = Color.Unspecified, modifier = Modifier.size(20.dp))
+ Spacer(Modifier.width(14.dp))
+ Box(Modifier.weight(1f), contentAlignment = Alignment.CenterStart) {
+ if (value.isEmpty()) {
+ Text(hint, color = Color(0xFFBCBEC3), fontSize = 16.sp, lineHeight = 20.sp, letterSpacing = 0.sp, maxLines = 1)
+ }
+ innerTextField()
+ }
+ if (value.isNotEmpty()) {
+ IconButton(onClick = { onValueChange("") }, modifier = Modifier.size(40.dp)) {
+ Icon(painterResource(R.drawable.ic_float_close), stringResource(R.string.Clear), tint = Color.Unspecified, modifier = Modifier.size(16.dp))
+ }
+ } else {
+ Spacer(Modifier.width(14.dp))
+ }
+ }
+ },
+ )
+}
diff --git a/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt b/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt
new file mode 100644
index 0000000000..3188bab4f4
--- /dev/null
+++ b/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt
@@ -0,0 +1,53 @@
+package one.mixin.android.ui.contacts
+
+import android.view.View
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.fragment.app.viewModels
+import dagger.hilt.android.AndroidEntryPoint
+import one.mixin.android.R
+import one.mixin.android.extension.dp
+import one.mixin.android.extension.getSafeAreaInsetsTop
+import one.mixin.android.extension.navTo
+import one.mixin.android.extension.screenHeight
+import one.mixin.android.ui.common.MixinComposeBottomSheetDialogFragment
+import one.mixin.android.ui.conversation.ConversationActivity
+import one.mixin.android.ui.group.GroupActivity
+
+@AndroidEntryPoint
+class NewChatBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragment() {
+ companion object {
+ const val TAG = "NewChatBottomSheetDialogFragment"
+ }
+
+ override fun getTheme() = R.style.AppTheme_Dialog
+
+ private val viewModel by viewModels()
+ private val contacts by lazy { viewModel.findContacts() }
+
+ @Composable
+ override fun ComposeContent() {
+ val users by contacts.observeAsState(emptyList())
+ ContactsPage(
+ contacts = users,
+ onBack = { dismiss() },
+ onContact = { user ->
+ ConversationActivity.show(requireContext(), recipientId = user.userId)
+ dismiss()
+ },
+ onNewGroup = {
+ GroupActivity.show(requireContext())
+ dismiss()
+ },
+ onAddContact = {
+ navTo(AddPeopleFragment.newInstance(), AddPeopleFragment.TAG)
+ dismiss()
+ },
+ )
+ }
+
+ override fun getBottomSheetHeight(view: View) = requireContext().screenHeight() - view.getSafeAreaInsetsTop() - 64.dp
+
+ override fun showError(error: String) = Unit
+}
diff --git a/app/src/main/java/one/mixin/android/ui/home/ConversationListFragment.kt b/app/src/main/java/one/mixin/android/ui/home/ConversationListFragment.kt
index 75a3d74f6e..7e7537fdf0 100644
--- a/app/src/main/java/one/mixin/android/ui/home/ConversationListFragment.kt
+++ b/app/src/main/java/one/mixin/android/ui/home/ConversationListFragment.kt
@@ -79,6 +79,7 @@ import one.mixin.android.ui.common.VerifyFragment
import one.mixin.android.ui.common.editDialog
import one.mixin.android.ui.common.recyclerview.NormalHolder
import one.mixin.android.ui.common.recyclerview.PagingHeaderAdapter
+import one.mixin.android.ui.contacts.NewChatBottomSheetDialogFragment
import one.mixin.android.ui.conversation.ConversationActivity
import one.mixin.android.ui.home.circle.CirclesFragment
import one.mixin.android.ui.home.reminder.RecoveryReminderBottomSheetDialogFragment
@@ -101,7 +102,6 @@ import one.mixin.android.vo.ConversationStatus
import one.mixin.android.vo.MessageCategory
import one.mixin.android.vo.MessageStatus
import one.mixin.android.vo.PinMessageMinimal
-import one.mixin.android.vo.User
import one.mixin.android.vo.explain
import one.mixin.android.vo.isAudio
import one.mixin.android.vo.isCallMessage
@@ -346,7 +346,7 @@ class ConversationListFragment : LinkFragment() {
if (cid != null) {
openCircleEdit(cid)
} else {
- navigationController.pushContacts(requireActivity())
+ NewChatBottomSheetDialogFragment().showNow(parentFragmentManager, NewChatBottomSheetDialogFragment.TAG)
}
}
@@ -365,15 +365,6 @@ class ConversationListFragment : LinkFragment() {
selectCircle(null,null)
}
}
- RxBus.listen(User::class.java)
- .observeOn(AndroidSchedulers.mainThread())
- .autoDispose(destroyScope)
- .subscribe { u ->
- if (Session.getAccountId() == u.userId) {
- binding.searchBar.avatar.setInfo(u.fullName, u.avatarUrl, u.userId)
- }
- }
-
initSearch()
analytics()
}
@@ -451,8 +442,8 @@ class ConversationListFragment : LinkFragment() {
searchBar.setOnLeftClickListener {
openSearch()
}
- searchBar.setOnGroupClickListener {
- navigationController.pushContacts(requireActivity())
+ searchBar.setOnNewChatClickListener {
+ NewChatBottomSheetDialogFragment().showNow(parentFragmentManager, NewChatBottomSheetDialogFragment.TAG)
}
searchBar.setOnAddClickListener {
addCircle(it.context)
diff --git a/app/src/main/java/one/mixin/android/ui/home/ExploreFragment.kt b/app/src/main/java/one/mixin/android/ui/home/ExploreFragment.kt
index ed3e03bce6..5e2ff04b99 100644
--- a/app/src/main/java/one/mixin/android/ui/home/ExploreFragment.kt
+++ b/app/src/main/java/one/mixin/android/ui/home/ExploreFragment.kt
@@ -1,48 +1,47 @@
package one.mixin.android.ui.home
import android.Manifest
-import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.edit
-import androidx.core.view.isInvisible
import androidx.core.view.isVisible
-import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
-import androidx.recyclerview.widget.RecyclerView
+import com.uber.autodispose.android.lifecycle.scope
import com.uber.autodispose.autoDispose
import dagger.hilt.android.AndroidEntryPoint
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.launch
import one.mixin.android.Constants
-import one.mixin.android.Constants.Account
import one.mixin.android.R
import one.mixin.android.RxBus
import one.mixin.android.databinding.FragmentExploreBinding
-import one.mixin.android.databinding.ItemFavoriteBinding
-import one.mixin.android.databinding.ItemFavoriteEditBinding
-import one.mixin.android.databinding.ItemFavoriteTitleBinding
import one.mixin.android.event.BadgeEvent
import one.mixin.android.event.BotEvent
import one.mixin.android.event.FavoriteEvent
import one.mixin.android.event.SessionEvent
import one.mixin.android.extension.addFragment
import one.mixin.android.extension.defaultSharedPreferences
-import one.mixin.android.extension.dp
import one.mixin.android.extension.navTo
-import one.mixin.android.extension.notEmptyWithElse
import one.mixin.android.extension.openCustomerService
import one.mixin.android.extension.openPermissionSetting
+import one.mixin.android.extension.withArgs
import one.mixin.android.extension.putBoolean
-import one.mixin.android.extension.putInt
-import one.mixin.android.job.TipCounterSyncedLiveData
import one.mixin.android.session.Session
import one.mixin.android.ui.common.BaseFragment
+import one.mixin.android.ui.common.QrBottomSheetDialogFragment
import one.mixin.android.ui.common.profile.MySharedAppsFragment
+import one.mixin.android.ui.common.profile.ProfileFragment
import one.mixin.android.ui.common.showUserBottom
+import one.mixin.android.ui.contacts.ContactListFragment
+import one.mixin.android.ui.contacts.ContactViewModel
import one.mixin.android.ui.device.DeviceFragment
import one.mixin.android.ui.home.bot.Bot
import one.mixin.android.ui.home.bot.BotManagerViewModel
@@ -52,15 +51,9 @@ import one.mixin.android.ui.home.bot.INTERNAL_MEMBER_ID
import one.mixin.android.ui.home.bot.INTERNAL_REFERRAL_ID
import one.mixin.android.ui.home.bot.INTERNAL_SUPPORT_ID
import one.mixin.android.ui.home.bot.INTERNAL_SWAP_ID
-import one.mixin.android.ui.home.bot.InternalBots
-import one.mixin.android.ui.home.bot.InternalLinkDesktop
-import one.mixin.android.ui.home.bot.InternalLinkDesktopLogged
-import one.mixin.android.ui.home.bot.InternalReferral
-import one.mixin.android.ui.home.inscription.CollectiblesFragment
import one.mixin.android.ui.home.reminder.RecoveryReminderBottomSheetDialogFragment
import one.mixin.android.ui.home.web3.trade.SwapActivity
import one.mixin.android.ui.search.SearchExploreFragment
-import one.mixin.android.ui.search.SearchInscriptionFragment
import one.mixin.android.ui.setting.SettingActivity
import one.mixin.android.ui.setting.member.MixinMemberInvoicesFragment
import one.mixin.android.ui.setting.member.MixinMemberUpgradeBottomSheetDialogFragment
@@ -74,241 +67,126 @@ import one.mixin.android.util.rxpermission.RxPermissions
import one.mixin.android.vo.BotInterface
import one.mixin.android.vo.ExploreApp
import one.mixin.android.vo.Plan
-import one.mixin.android.widget.SegmentationItemDecoration
-import one.mixin.android.widget.lottie.RLottieDrawable
-import javax.inject.Inject
+import one.mixin.android.vo.toUser
@AndroidEntryPoint
class ExploreFragment : BaseFragment() {
companion object {
const val TAG = "ExploreFragment"
const val PREF_BOT_CLICKED_IDS = "explore_bot_clicked_ids"
+ private const val ARG_BOTS = "bots"
val SHOW_DOT_BOT_IDS = setOf(INTERNAL_BUY_ID, INTERNAL_SWAP_ID, INTERNAL_MEMBER_ID, INTERNAL_REFERRAL_ID)
- fun newInstance() = ExploreFragment()
+ fun newInstance(bots: Boolean = false) = ExploreFragment().withArgs { putBoolean(ARG_BOTS, bots) }
}
- @Inject
- lateinit var tipCounterSynced: TipCounterSyncedLiveData
-
private var _binding: FragmentExploreBinding? = null
private val binding get() = requireNotNull(_binding)
-
private val botManagerViewModel by viewModels()
-
- override fun onCreateView(
- inflater: LayoutInflater,
- container: ViewGroup?,
- savedInstanceState: Bundle?,
- ): View {
+ private val contactViewModel by viewModels()
+ private val contacts by lazy { contactViewModel.findContacts() }
+ private val self by lazy { contactViewModel.findSelf() }
+ private var favoriteApps by mutableStateOf>(emptyList())
+ private var apps by mutableStateOf>(emptyList())
+ private var isDesktopLogin by mutableStateOf(false)
+ private var clickedBotIds by mutableStateOf>(emptySet())
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentExploreBinding.inflate(inflater, container, false)
+ val bots = arguments?.getBoolean(ARG_BOTS) == true
+ binding.homeToolbar.isVisible = !bots
+ binding.compose.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
+ binding.compose.setContent {
+ if (bots) {
+ BotsPage(
+ favorites = favoriteApps,
+ apps = apps,
+ onBack = { requireActivity().onBackPressedDispatcher.onBackPressed() },
+ onEdit = { navTo(MySharedAppsFragment.newInstance(), MySharedAppsFragment.TAG) },
+ onFavoriteClick = { clickAction(it) },
+ onBotClick = { app ->
+ viewLifecycleOwner.lifecycleScope.launch {
+ botManagerViewModel.findUserByAppId(app.appId)?.let {
+ showUserBottom(parentFragmentManager, it, botEntrySource = AnalyticsTracker.BotSource.MORE_EXPLORE_DIALOG)
+ }
+ }
+ },
+ )
+ } else {
+ val users by contacts.observeAsState(emptyList())
+ val user by self.observeAsState(Session.getAccount()?.toUser())
+ MorePage(
+ user = user,
+ contacts = users,
+ favorites = favoriteApps,
+ botCount = (favoriteApps + apps).distinctBy { it.appId }.size,
+ isDesktopLogin = isDesktopLogin,
+ clickedBotIds = clickedBotIds,
+ onProfile = { navTo(ProfileFragment(), ProfileFragment.TAG) },
+ onQr = {
+ Session.getAccountId()?.let {
+ QrBottomSheetDialogFragment.newInstance(it, QrBottomSheetDialogFragment.TYPE_MY_QR)
+ .showNow(parentFragmentManager, QrBottomSheetDialogFragment.TAG)
+ }
+ },
+ onContacts = { navTo(ContactListFragment(), ContactListFragment.TAG) },
+ onContact = { showUserBottom(parentFragmentManager, it) },
+ onBots = { navTo(newInstance(bots = true), "ExploreBotsFragment") },
+ onBot = { clickAction(it) },
+ )
+ }
+ }
return binding.root
}
- @SuppressLint("RestrictedApi")
- override fun onViewCreated(
- view: View,
- savedInstanceState: Bundle?,
- ) {
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
- binding.apply {
- root.setOnClickListener {
- // do nothing
+ binding.homeToolbar.apply {
+ setOnSearchClickListener {
+ activity?.addFragment(this@ExploreFragment, SearchExploreFragment.newInstance(), SearchExploreFragment.TAG, id = R.id.internal_container)
}
- homeToolbar.setOnSearchClickListener {
- if (radioCollectible.isChecked) {
- activity?.addFragment(
- this@ExploreFragment,
- SearchInscriptionFragment(),
- SearchInscriptionFragment.TAG,
- id = R.id.internal_container,
- )
- } else {
- activity?.addFragment(
- this@ExploreFragment,
- SearchExploreFragment.newInstance(),
- SearchExploreFragment.TAG,
- id = R.id.internal_container,
- )
- }
- }
- homeToolbar.setOnScanClickListener {
+ setOnScanClickListener {
RxPermissions(requireActivity()).request(Manifest.permission.CAMERA).autoDispose(stopScope).subscribe { granted ->
- if (granted) {
- (requireActivity() as? MainActivity)?.showCapture(true)
- } else {
- context?.openPermissionSetting()
- }
+ if (granted) (requireActivity() as? MainActivity)?.showCapture(true) else context?.openPermissionSetting()
}
}
- homeToolbar.setOnSettingsClickListener {
- SettingActivity.show(requireContext(), compose = false)
- }
- favoriteRv.adapter = adapter
- favoriteRv.addItemDecoration(SegmentationItemDecoration())
-
- val exploreSelect = defaultSharedPreferences.getInt(Constants.Account.PREF_EXPLORE_SELECT, 0)
- when (exploreSelect) {
- 0 -> {
- exploreVa.displayedChild = 0
- radioFavorite.isChecked = true
- radioCollectible.isChecked = false
- }
-
- 1 -> {
- exploreVa.displayedChild = 1
- radioFavorite.isChecked = false
- radioCollectible.isChecked = true
- navigate(collectiblesFragment, CollectiblesFragment.TAG)
- }
- }
-
- radioGroupExplore.setOnCheckedChangeListener { _, checkedId ->
- when (checkedId) {
- R.id.radio_favorite -> {
- defaultSharedPreferences.putInt(Constants.Account.PREF_EXPLORE_SELECT, 0)
- exploreVa.displayedChild = 0
- }
-
- R.id.radio_collectible -> {
- defaultSharedPreferences.putInt(Constants.Account.PREF_EXPLORE_SELECT, 1)
- exploreVa.displayedChild = 1
- navigate(collectiblesFragment, CollectiblesFragment.TAG)
- radioCollectible.setBackgroundResource(R.drawable.selector_radio)
- lifecycleScope.launch {
- defaultSharedPreferences.putBoolean(Account.PREF_HAS_USED_MARKET, false)
- }
- RxBus.publish(BadgeEvent(Account.PREF_HAS_USED_MARKET))
- }
-
- }
- }
-
- adapter.isDesktopLogin = Session.getExtensionSessionId() != null
+ setOnSettingsClickListener { SettingActivity.show(requireContext(), compose = false) }
}
+ isDesktopLogin = Session.getExtensionSessionId() != null
+ clickedBotIds = readClickedBotIds()
loadData()
- loadBotData()
- refresh()
- updateFavoriteDot()
-
- RxBus.listen(BotEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(destroyScope).subscribe {
- loadBotData()
- updateFavoriteDot()
- }
- RxBus.listen(FavoriteEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(destroyScope).subscribe {
- loadData()
- updateFavoriteDot()
- }
- RxBus.listen(SessionEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(destroyScope).subscribe {
- adapter.isDesktopLogin = Session.getExtensionSessionId() != null
- updateFavoriteDot()
- }
- RxBus.listen(BadgeEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(destroyScope).subscribe { event ->
- loadData()
- updateFavoriteDot()
- }
- lifecycleScope.launch {
- val market = defaultSharedPreferences.getBoolean(Account.PREF_HAS_USED_MARKET, true)
- if (market) {
- binding.radioCollectible.setBackgroundResource(R.drawable.selector_radio_badge)
- } else {
- binding.radioCollectible.setBackgroundResource(R.drawable.selector_radio)
- }
- }
- }
-
- private val containerFragmentTags = listOf(CollectiblesFragment.TAG)
- private fun navigate(
- destinationFragment: Fragment,
- tag: String,
- ) {
- if (tag !in containerFragmentTags) return
- val tx = parentFragmentManager.beginTransaction()
- val f = parentFragmentManager.findFragmentByTag(tag)
- if (f == null) {
- tx.add(R.id.fragment_container, destinationFragment, tag)
- } else {
- tx.show(f)
- }
- parentFragmentManager.fragments.filter { fragment ->
- fragment.tag != tag && fragment.tag in containerFragmentTags
- }.forEach { fragment ->
- tx.hide(fragment)
- }
- tx.commitAllowingStateLoss()
- }
-
- private val collectiblesFragment by lazy {
- CollectiblesFragment()
- }
-
- private fun loadData() {
- lifecycleScope.launch {
- val favoriteApps = botManagerViewModel.getFavoriteAppsByUserId(Session.getAccountId()!!)
- adapter.setData(favoriteApps)
- }
- }
-
- private val adapter by lazy {
- BotAdapter({
- activity?.addFragment(
- this@ExploreFragment,
- MySharedAppsFragment.newInstance(),
- MySharedAppsFragment.TAG,
- )
- }, { app ->
- clickAction(app)
- }, { bot ->
- lifecycleScope.launch {
- botManagerViewModel.findUserByAppId(bot.getBotId())?.let { user ->
- showUserBottom(parentFragmentManager, user, botEntrySource = AnalyticsTracker.BotSource.MORE_EXPLORE_DIALOG)
- }
- }
- })
- }
-
- private fun refresh() {
- lifecycleScope.launch {
+ viewLifecycleOwner.lifecycleScope.launch {
try {
- botManagerViewModel.refreshFavoriteApps(Session.getAccountId()!!)
+ botManagerViewModel.refreshFavoriteApps(requireNotNull(Session.getAccountId()))
loadData()
} catch (e: Exception) {
ErrorHandler.handleError(e)
}
}
+ RxBus.listen(BotEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(viewLifecycleOwner.scope(androidx.lifecycle.Lifecycle.Event.ON_DESTROY)).subscribe { loadData() }
+ RxBus.listen(FavoriteEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(viewLifecycleOwner.scope(androidx.lifecycle.Lifecycle.Event.ON_DESTROY)).subscribe { loadData() }
+ RxBus.listen(BadgeEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(viewLifecycleOwner.scope(androidx.lifecycle.Lifecycle.Event.ON_DESTROY)).subscribe {
+ clickedBotIds = readClickedBotIds()
+ }
+ RxBus.listen(SessionEvent::class.java).observeOn(AndroidSchedulers.mainThread()).autoDispose(viewLifecycleOwner.scope(androidx.lifecycle.Lifecycle.Event.ON_DESTROY)).subscribe {
+ isDesktopLogin = Session.getExtensionSessionId() != null
+ }
}
- private fun loadBotData() {
- lifecycleScope.launch {
- val apps = botManagerViewModel.getAllExploreApps()
- adapter.list = apps
+ private fun loadData() {
+ viewLifecycleOwner.lifecycleScope.launch {
+ favoriteApps = botManagerViewModel.getFavoriteAppsByUserId(requireNotNull(Session.getAccountId()))
+ apps = botManagerViewModel.getAllExploreApps()
}
}
- private fun getClickedBotIds(): Set {
- return requireContext().defaultSharedPreferences.getString(PREF_BOT_CLICKED_IDS, "")
- ?.split(",")?.filter { it.isNotBlank() }?.toSet() ?: emptySet()
- }
+ private fun readClickedBotIds(): Set =
+ defaultSharedPreferences.getString(PREF_BOT_CLICKED_IDS, "").orEmpty().split(",").filter { it.isNotBlank() }.toSet()
private fun setClickedBotId(id: String) {
- val sp = requireContext().defaultSharedPreferences
- val old = getClickedBotIds().toMutableSet()
- if (old.add(id)) {
- sp.edit { putString(PREF_BOT_CLICKED_IDS, old.joinToString(",")) }
- updateFavoriteDot()
- RxBus.publish(BadgeEvent(PREF_BOT_CLICKED_IDS))
- }
- }
-
- private fun updateFavoriteDot() {
- val clickedIds = getClickedBotIds()
- val needShow = SHOW_DOT_BOT_IDS.any { !clickedIds.contains(it) }
- binding.radioFavorite.setBackgroundResource(
- if (needShow) {
- R.drawable.selector_radio_badge
- } else {
- R.drawable.selector_radio
- }
- )
+ val ids = readClickedBotIds() + id
+ defaultSharedPreferences.edit { putString(PREF_BOT_CLICKED_IDS, ids.joinToString(",")) }
+ clickedBotIds = ids
+ RxBus.publish(BadgeEvent(PREF_BOT_CLICKED_IDS))
}
private val clickAction: (BotInterface) -> Unit = { app ->
@@ -322,7 +200,7 @@ class ExploreFragment : BaseFragment() {
} else if (app is Bot) {
if (SHOW_DOT_BOT_IDS.contains(app.id)) {
setClickedBotId(app.id)
- adapter.notifyDataSetChanged()
+ clickedBotIds = readClickedBotIds()
}
when (app.id) {
INTERNAL_LINK_DESKTOP_ID -> {
@@ -409,191 +287,8 @@ class ExploreFragment : BaseFragment() {
}
}
- class BotAdapter(private val editAction: () -> Unit, private val botAction: (BotInterface) -> Unit, private val appAction: (BotInterface) -> Unit) : RecyclerView.Adapter() {
- private val TYPE_INTERNAL = 0
- private val TYPE_TITLE = 1
- private val TYPE_FAVORITE = 2
- private val TYPE_EDIT = 3
- private val TYPE_BOT = 4
-
- private var favoriteApps: List? = null
- var list: List = listOf()
- @SuppressLint("NotifyDataSetChanged")
- set(value) {
- field = value
- notifyDataSetChanged()
- }
- var isDesktopLogin = false
- set(value) {
- if (value == field) return
-
- field = value
- notifyItemChanged(InternalBots.indexOf(InternalLinkDesktop))
- }
-
- @SuppressLint("NotifyDataSetChanged")
- fun setData(
- favoriteApps: List,
- ) {
- this.favoriteApps = favoriteApps
- notifyDataSetChanged()
- }
-
- override fun onCreateViewHolder(
- parent: ViewGroup,
- viewType: Int,
- ): RecyclerView.ViewHolder {
- return when (viewType) {
- TYPE_EDIT -> {
- FavoriteEditHolder(ItemFavoriteEditBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
-
- TYPE_TITLE -> {
- FavoriteTitleHolder(ItemFavoriteTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
-
- else -> {
- FavoriteHolder(ItemFavoriteBinding.inflate(LayoutInflater.from(parent.context), parent, false))
- }
- }
- }
-
- @SuppressLint("ClickableViewAccessibility")
- override fun onBindViewHolder(
- holder: RecyclerView.ViewHolder,
- position: Int,
- ) {
- val type = getItemViewType(position)
- if (type == TYPE_EDIT) {
- holder.itemView.setOnClickListener {
- editAction.invoke()
- }
- } else if (type == TYPE_INTERNAL || type == TYPE_FAVORITE) {
- getItem(type, position)?.let { app ->
- (holder as FavoriteHolder).bind(app, isDesktopLogin)
- holder.itemView.setOnClickListener {
- botAction.invoke(app)
- }
- }
- } else if (type == TYPE_BOT) {
- getItem(type, position)?.let { bot ->
- (holder as FavoriteHolder).bind(bot, isDesktopLogin)
- holder.itemView.setOnClickListener {
- appAction.invoke(bot)
- }
- }
- } else if (type == TYPE_TITLE) {
- if (position == InternalBots.size) {
- (holder as FavoriteTitleHolder).setText(R.string.Favorite)
- } else {
- (holder as FavoriteTitleHolder).setText(R.string.bots_title)
- }
- }
- }
-
- override fun getItemCount(): Int {
- return InternalBots.size + (favoriteApps?.size ?: 0) + list.notEmptyWithElse({ it.size }, 0) + 3
- }
-
- override fun getItemViewType(position: Int): Int {
- return if (position < InternalBots.size) {
- TYPE_INTERNAL
- } else if (position == InternalBots.size) {
- TYPE_TITLE
- } else if (position < InternalBots.size + (favoriteApps?.size ?: 0) + 1) {
- TYPE_FAVORITE
- } else if (position == InternalBots.size + (favoriteApps?.size ?: 0) + 1) {
- TYPE_EDIT
- } else if (position == InternalBots.size + (favoriteApps?.size ?: 0) + 2) {
- TYPE_TITLE
- } else {
- TYPE_BOT
- }
- }
-
- private fun getItem(
- type: Int,
- position: Int,
- ): BotInterface? {
- return if (type == TYPE_INTERNAL) {
- InternalBots[position]
- } else if (type == TYPE_FAVORITE) {
- favoriteApps?.get(position - InternalBots.size - 1)
- } else {
- list[position - InternalBots.size - (favoriteApps?.size ?: 0) - 3]
- }
- }
- }
-
- class FavoriteHolder(private val itemBinding: ItemFavoriteBinding) : RecyclerView.ViewHolder(itemBinding.root) {
- fun bind(
- app: BotInterface?,
- isDesktopLogin: Boolean,
- ) {
- app ?: return
- if (app is Bot) {
- val a =
- if (app == InternalLinkDesktop && isDesktopLogin) {
- InternalLinkDesktopLogged
- } else {
- app
- }
- itemBinding.apply {
- if (app == InternalReferral) {
- lottie.setImageDrawable(
- RLottieDrawable(
- R.raw.referral,
- "referral",
- 35.dp,
- 35.dp,
- ).apply {
- setAllowDecodeSingleFrame(true)
- setAutoRepeat(1)
- setAutoRepeatCount(Int.MAX_VALUE)
- start()
- },
- )
- avatar.isInvisible = true
- lottieFl.isVisible = true
- } else {
- avatar.renderApp(a)
- avatar.isVisible = true
- lottieFl.isVisible = false
- }
- name.setTextOnly(a.name)
- mixinIdTv.setText(a.description)
- name.setTextOnly(a.name)
- val context = itemView.context
- val clickedIds =
- try {
- context.defaultSharedPreferences.getString(PREF_BOT_CLICKED_IDS, "")
- ?.split(",")?.toSet() ?: emptySet()
- } catch (e: Exception) {
- emptySet()
- }
- if (SHOW_DOT_BOT_IDS.contains(a.id) && !clickedIds.contains(a.id)) {
- dot.visibility = View.VISIBLE
- } else {
- dot.visibility = View.GONE
- }
- }
- } else if (app is ExploreApp) {
- itemBinding.apply {
- avatar.setInfo(app.name, app.iconUrl, app.appId)
- name.setTextOnly(app.name)
- mixinIdTv.text = app.appNumber
- name.setName(app)
- dot.visibility = View.GONE
- }
- }
- }
- }
-
- class FavoriteEditHolder(itemBinding: ItemFavoriteEditBinding) : RecyclerView.ViewHolder(itemBinding.root)
-
- class FavoriteTitleHolder(val itemBinding: ItemFavoriteTitleBinding) : RecyclerView.ViewHolder(itemBinding.root) {
- fun setText(res: Int) {
- itemBinding.title.setText(res)
- }
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
}
}
diff --git a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt
new file mode 100644
index 0000000000..5f15ee841e
--- /dev/null
+++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt
@@ -0,0 +1,275 @@
+package one.mixin.android.ui.home
+
+import android.graphics.Matrix
+import android.widget.ImageView
+import androidx.compose.foundation.background
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import one.mixin.android.R
+import one.mixin.android.compose.CoilImage
+import one.mixin.android.compose.UserAvatarImage
+import one.mixin.android.compose.theme.MixinAppTheme
+import one.mixin.android.extension.dpToPx
+import one.mixin.android.ui.contacts.ContactAlphabetIndex
+import one.mixin.android.ui.contacts.ContactPageHeader
+import one.mixin.android.ui.contacts.ContactSectionHeader
+import one.mixin.android.ui.contacts.ContactsSearchField
+import one.mixin.android.ui.contacts.contactInitial
+import one.mixin.android.ui.home.bot.Bot
+import one.mixin.android.ui.home.bot.INTERNAL_LINK_DESKTOP_ID
+import one.mixin.android.ui.home.bot.INTERNAL_REFERRAL_ID
+import one.mixin.android.ui.home.bot.InternalBots
+import one.mixin.android.ui.wallet.alert.components.cardBackground
+import one.mixin.android.vo.BotInterface
+import one.mixin.android.vo.ExploreApp
+import one.mixin.android.vo.User
+import one.mixin.android.widget.NameTextView
+import one.mixin.android.widget.lottie.RLottieDrawable
+import one.mixin.android.widget.lottie.RLottieImageView
+
+@Composable
+internal fun MorePage(
+ user: User?,
+ contacts: List,
+ favorites: List,
+ botCount: Int,
+ isDesktopLogin: Boolean,
+ clickedBotIds: Set,
+ onProfile: () -> Unit,
+ onQr: () -> Unit,
+ onContacts: () -> Unit,
+ onContact: (User) -> Unit,
+ onBots: () -> Unit,
+ onBot: (BotInterface) -> Unit,
+) {
+ MixinAppTheme {
+ val profileNameColor = MixinAppTheme.colors.textMinor.toArgb()
+ LazyColumn(
+ modifier = Modifier.fillMaxSize().background(MixinAppTheme.colors.background),
+ contentPadding = androidx.compose.foundation.layout.PaddingValues(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 16.dp),
+ ) {
+ user?.let {
+ item {
+ Row(
+ Modifier.moreCard().clickable(onClick = onProfile).padding(start = 16.dp, end = 8.dp).heightIn(min = 68.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ UserAvatarImage(it, 42.dp)
+ Spacer(Modifier.width(9.dp))
+ Column(Modifier.weight(1f)) {
+ AndroidView(factory = { context -> NameTextView(context) }, update = { view -> view.setName(it); view.setTextColor(profileNameColor) })
+ Spacer(Modifier.height(4.dp))
+ Text(stringResource(R.string.contact_mixin_id, it.identityNumber), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, lineHeight = 14.sp, letterSpacing = 0.sp)
+ }
+ Box(Modifier.width(62.dp).height(48.dp)) {
+ Icon(painterResource(R.drawable.ic_more_arrow), null, tint = Color.Unspecified, modifier = Modifier.size(30.dp).align(Alignment.CenterEnd))
+ IconButton(onClick = onQr, modifier = Modifier.size(48.dp).align(Alignment.CenterStart)) {
+ Icon(painterResource(R.drawable.ic_more_qr), stringResource(R.string.My_QR_Code), tint = Color.Unspecified, modifier = Modifier.size(24.dp))
+ }
+ }
+ }
+ Spacer(Modifier.height(12.dp))
+ }
+ }
+ items(InternalBots.chunked(2)) { row ->
+ Row(Modifier.fillMaxWidth().height(IntrinsicSize.Min), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
+ row.forEach { bot ->
+ MoreActionCard(bot, isDesktopLogin, bot.id !in clickedBotIds && bot.id in ExploreFragment.SHOW_DOT_BOT_IDS, Modifier.weight(1f).fillMaxHeight()) { onBot(bot) }
+ }
+ }
+ Spacer(Modifier.height(12.dp))
+ }
+ if (contacts.isNotEmpty()) {
+ item {
+ Spacer(Modifier.height(8.dp))
+ Column(Modifier.moreCard()) {
+ MoreSectionHeader(stringResource(R.string.contacts_title), contacts.size, onContacts)
+ contacts.take(3).forEachIndexed { index, contact ->
+ Row(Modifier.fillMaxWidth().clickable { onContact(contact) }.padding(horizontal = 16.dp).height(50.dp), verticalAlignment = Alignment.CenterVertically) {
+ UserAvatarImage(contact, 42.dp)
+ Spacer(Modifier.width(14.dp))
+ Column(Modifier.weight(1f)) {
+ AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(contact) })
+ Spacer(Modifier.height(4.dp))
+ Text(contact.identityNumber, color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp)
+ }
+ }
+ if (index < minOf(contacts.size, 3) - 1) Spacer(Modifier.height(20.dp))
+ }
+ Spacer(Modifier.height(20.dp))
+ }
+ }
+ }
+ if (botCount > 0) {
+ item {
+ Spacer(Modifier.height(12.dp))
+ Column(Modifier.moreCard()) {
+ MoreSectionHeader(stringResource(R.string.bots_title), botCount, onBots)
+ favorites.forEach { app -> BotRow(app, true, 42.dp) { onBot(app) } }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun Modifier.moreCard() = fillMaxWidth().clip(RoundedCornerShape(8.dp))
+ .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor, borderWidth = 1.dp)
+
+@Composable
+private fun MoreActionCard(bot: Bot, loggedIn: Boolean, showDot: Boolean, modifier: Modifier, onClick: () -> Unit) {
+ val desktop = bot.id == INTERNAL_LINK_DESKTOP_ID
+ val icon = if (desktop && loggedIn) R.drawable.ic_more_desktop_logged else bot.icon
+ Row(modifier.moreCard().clickable(onClick = onClick).heightIn(min = 77.dp).padding(horizontal = 10.dp, vertical = 12.dp)) {
+ if (bot.id == INTERNAL_REFERRAL_ID) {
+ AndroidView(
+ factory = { context ->
+ RLottieImageView(context).apply {
+ val size = context.dpToPx(30f)
+ setAnimation(
+ RLottieDrawable(R.raw.referral, "referral", size, size).apply {
+ setAutoRepeat(1)
+ setAutoRepeatCount(Int.MAX_VALUE)
+ },
+ )
+ val offset = (context.dpToPx(24f) - size) / 2f
+ scaleType = ImageView.ScaleType.MATRIX
+ imageMatrix = Matrix().apply {
+ // Exclude the animation's asymmetric transparent padding when centering the artwork.
+ setTranslate(offset, offset + context.dpToPx(2f))
+ }
+ playAnimation()
+ }
+ },
+ modifier = Modifier.size(24.dp),
+ onRelease = { view ->
+ val drawable = view.animatedDrawable
+ view.clearAnimationDrawable()
+ drawable?.recycle(true)
+ },
+ )
+ } else {
+ Icon(painterResource(icon), null, tint = Color.Unspecified, modifier = Modifier.size(24.dp))
+ }
+ Spacer(Modifier.width(10.dp))
+ Column(Modifier.weight(1f)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(stringResource(bot.name), color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp, modifier = Modifier.weight(1f, fill = false))
+ if (showDot) Box(
+ Modifier.align(Alignment.Top).padding(start = 1.dp).size(9.dp)
+ .background(MixinAppTheme.colors.badgeRed, CircleShape),
+ )
+ }
+ Spacer(Modifier.height(4.dp))
+ Text(stringResource(if (desktop && loggedIn) R.string.Logined else bot.description), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, lineHeight = 16.sp, letterSpacing = 0.sp)
+ }
+ }
+}
+
+@Composable
+private fun MoreSectionHeader(title: String, count: Int, onClick: () -> Unit) {
+ Row(Modifier.fillMaxWidth().clickable(onClick = onClick).height(56.dp).padding(start = 16.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically) {
+ Text(title, color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp, modifier = Modifier.weight(1f))
+ Text(count.toString(), color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp)
+ MoreArrow()
+ }
+}
+
+@Composable
+private fun MoreArrow() {
+ Icon(painterResource(R.drawable.ic_more_arrow), null, tint = Color.Unspecified, modifier = Modifier.size(30.dp))
+}
+
+@Composable
+private fun BotRow(app: ExploreApp, external: Boolean = false, avatarSize: Dp = 50.dp, onClick: () -> Unit) {
+ Row(Modifier.fillMaxWidth().clickable(onClick = onClick).height(70.dp).padding(start = if (external) 16.dp else 20.dp, end = if (external) 8.dp else 40.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically) {
+ CoilImage(app.iconUrl, R.drawable.ic_avatar_place_holder, Modifier.size(avatarSize).clip(CircleShape))
+ Spacer(Modifier.width(if (external) 14.dp else 16.dp))
+ Column(Modifier.weight(1f)) {
+ AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(app) })
+ Spacer(Modifier.height(4.dp))
+ Text(app.appNumber, color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ }
+ if (external) Icon(painterResource(R.drawable.ic_more_link), null, tint = Color.Unspecified, modifier = Modifier.size(24.dp))
+ }
+}
+
+@Composable
+internal fun BotsPage(favorites: List, apps: List, onBack: () -> Unit, onEdit: () -> Unit, onFavoriteClick: (ExploreApp) -> Unit, onBotClick: (ExploreApp) -> Unit) {
+ var query by rememberSaveable { mutableStateOf("") }
+ val matches: (ExploreApp) -> Boolean = { it.name.contains(query.trim(), true) || it.appNumber.contains(query.trim(), true) }
+ val favoriteIds = favorites.map { it.appId }.toSet()
+ val groups = apps.filter { it.appId !in favoriteIds && matches(it) }.groupBy { contactInitial(it.name) }.toSortedMap(compareBy { if (it == "#") "[" else it })
+ val filteredFavorites = favorites.filter(matches)
+ val listState = rememberLazyListState()
+ var offset = filteredFavorites.size + 2
+ val sectionOffsets = groups.mapValues { (_, group) -> offset.also { offset += group.size + 1 } }
+ MixinAppTheme {
+ Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) {
+ ContactPageHeader(stringResource(R.string.bots_title), onBack)
+ ContactsSearchField(query, { query = it }, stringResource(R.string.setting_auth_search_hint))
+ BoxWithConstraints(Modifier.weight(1f)) {
+ LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) {
+ item { ContactSectionHeader(stringResource(R.string.Favorite)) }
+ items(filteredFavorites, key = { "favorite:${it.appId}" }) { app -> BotRow(app) { onFavoriteClick(app) } }
+ item {
+ Row(Modifier.fillMaxWidth().clickable(onClick = onEdit).height(70.dp).padding(start = 20.dp, end = 20.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically) {
+ Icon(painterResource(R.drawable.ic_favorite_edit), null, tint = Color.Unspecified, modifier = Modifier.size(50.dp))
+ Spacer(Modifier.width(16.dp))
+ Column {
+ Text(stringResource(R.string.My_favorite_bots), color = MixinAppTheme.colors.textPrimary, fontSize = 16.sp, lineHeight = 19.sp, letterSpacing = 0.sp)
+ Spacer(Modifier.height(4.dp))
+ Text(stringResource(R.string.add_or_remove_favorite_bots), color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp)
+ }
+ }
+ }
+ groups.forEach { (initial, group) ->
+ item(key = "header:$initial") { ContactSectionHeader(initial) }
+ items(group, key = { it.appId }) { app -> BotRow(app) { onBotClick(app) } }
+ }
+ }
+ ContactAlphabetIndex(sectionOffsets, listState)
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/one/mixin/android/ui/home/bot/InternalApp.kt b/app/src/main/java/one/mixin/android/ui/home/bot/InternalApp.kt
index 7bd6ca9b16..44e8416b05 100644
--- a/app/src/main/java/one/mixin/android/ui/home/bot/InternalApp.kt
+++ b/app/src/main/java/one/mixin/android/ui/home/bot/InternalApp.kt
@@ -23,14 +23,12 @@ const val INTERNAL_LINK_DESKTOP_ID = "7C4346C7-CFEF-40CD-AC70-4041FDC3D941"
const val INTERNAL_SUPPORT_ID = "77443b1f-bbr4-4aad-8b6b-b8f58761e2e9"
-val InternalBuy = Bot(INTERNAL_BUY_ID, R.string.Buy, R.string.buy_crypto_with_cash, R.drawable.ic_bot_buy)
-val InternalSwap = Bot(INTERNAL_SWAP_ID, R.string.Trade, R.string.trade_native_tokens, R.drawable.ic_bot_swap)
-val InternalMember = Bot(INTERNAL_MEMBER_ID, R.string.Mixin_One, R.string.mixin_one_desc, R.drawable.ic_bot_member)
+val InternalBuy = Bot(INTERNAL_BUY_ID, R.string.Buy, R.string.buy_crypto_with_cash, R.drawable.ic_more_buy)
+val InternalSwap = Bot(INTERNAL_SWAP_ID, R.string.Trade, R.string.trade_native_tokens, R.drawable.ic_more_swap)
+val InternalMember = Bot(INTERNAL_MEMBER_ID, R.string.Mixin_One, R.string.mixin_one_desc, R.drawable.ic_more_member)
-val InternalReferral = Bot(INTERNAL_REFERRAL_ID, R.string.Referral, R.string.referral_description, R.drawable.ic_bot_referral)
-val InternalLinkDesktop = Bot(INTERNAL_LINK_DESKTOP_ID, R.string.Link_desktop, R.string.link_desktop_description, R.drawable.ic_bot_desktop)
-val InternalSupport = Bot(INTERNAL_SUPPORT_ID, R.string.Contact_Support, R.string.leave_message_to_team_mixin, R.drawable.ic_bot_support)
-
-val InternalLinkDesktopLogged = Bot(INTERNAL_LINK_DESKTOP_ID, R.string.Link_desktop, R.string.Logined, R.drawable.ic_bot_desktop_logged)
+val InternalReferral = Bot(INTERNAL_REFERRAL_ID, R.string.Referral, R.string.referral_description, R.drawable.ic_referral)
+val InternalLinkDesktop = Bot(INTERNAL_LINK_DESKTOP_ID, R.string.Link_desktop, R.string.link_desktop_description, R.drawable.ic_desktop_login)
+val InternalSupport = Bot(INTERNAL_SUPPORT_ID, R.string.Contact_Support, R.string.leave_message_to_team_mixin, R.drawable.ic_more_support)
val InternalBots = listOf(InternalBuy, InternalSwap, InternalMember, InternalReferral, InternalLinkDesktop, InternalSupport)
diff --git a/app/src/main/java/one/mixin/android/ui/home/inscription/CollectiblesFragment.kt b/app/src/main/java/one/mixin/android/ui/home/inscription/CollectiblesFragment.kt
index 23987334bb..4981418de3 100644
--- a/app/src/main/java/one/mixin/android/ui/home/inscription/CollectiblesFragment.kt
+++ b/app/src/main/java/one/mixin/android/ui/home/inscription/CollectiblesFragment.kt
@@ -19,6 +19,7 @@ import one.mixin.android.extension.colorAttr
import one.mixin.android.extension.defaultSharedPreferences
import one.mixin.android.extension.dp
import one.mixin.android.extension.dpToPx
+import one.mixin.android.extension.navTo
import one.mixin.android.extension.putInt
import one.mixin.android.job.TipCounterSyncedLiveData
import one.mixin.android.tip.wc.SortOrder
@@ -26,6 +27,7 @@ import one.mixin.android.ui.common.BaseFragment
import one.mixin.android.ui.home.inscription.menu.SortMenuAdapter
import one.mixin.android.ui.home.inscription.menu.SortMenuData
import one.mixin.android.ui.home.web3.Web3ViewModel
+import one.mixin.android.ui.search.SearchInscriptionFragment
import javax.inject.Inject
@AndroidEntryPoint
@@ -77,6 +79,12 @@ class CollectiblesFragment : BaseFragment() {
) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
+ titleView.setSubTitle(getString(R.string.Collectibles), getString(R.string.Privacy_Wallet), R.drawable.ic_wallet_privacy)
+ titleView.leftIb.setOnClickListener { requireActivity().onBackPressedDispatcher.onBackPressed() }
+ titleView.rightIb.contentDescription = getString(R.string.Search)
+ titleView.rightIb.setOnClickListener {
+ navTo(SearchInscriptionFragment(), SearchInscriptionFragment.TAG)
+ }
root.setOnClickListener {
// do nothing
}
@@ -195,6 +203,11 @@ class CollectiblesFragment : BaseFragment() {
}
}
+ override fun onDestroyView() {
+ super.onDestroyView()
+ _binding = null
+ }
+
private val menuAdapter: SortMenuAdapter by lazy {
val menuItems = listOf(
SortMenuData(SortOrder.Recent, R.drawable.ic_recent, R.string.Recent),
diff --git a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt
index 32f1b6cb6f..e2a7f0d928 100644
--- a/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt
+++ b/app/src/main/java/one/mixin/android/ui/setting/LogAndDebugFragment.kt
@@ -16,12 +16,14 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import one.mixin.android.Constants
import one.mixin.android.R
+import one.mixin.android.RxBus
import one.mixin.android.databinding.FragmentLogDebugBinding
import one.mixin.android.databinding.ViewCaptchaPreviewBottomBinding
import one.mixin.android.db.DatabaseMonitor
import one.mixin.android.db.property.PropertyHelper.deleteKeyValue
import one.mixin.android.db.property.PropertyHelper.findValueByKey
import one.mixin.android.db.property.PropertyHelper.updateKeyValue
+import one.mixin.android.event.BadgeEvent
import one.mixin.android.extension.alertDialogBuilder
import one.mixin.android.extension.defaultSharedPreferences
import one.mixin.android.extension.indeterminateProgressDialog
@@ -33,6 +35,7 @@ import one.mixin.android.job.MixinJobManager
import one.mixin.android.job.RefreshWeb3TransactionsJob
import one.mixin.android.session.Session
import one.mixin.android.ui.common.BaseFragment
+import one.mixin.android.ui.home.ExploreFragment.Companion.PREF_BOT_CLICKED_IDS
import one.mixin.android.ui.home.reminder.RecoveryReminderBottomSheetDialogFragment
import one.mixin.android.ui.home.reminder.ReminderBottomSheetDialogFragment
import one.mixin.android.ui.home.reminder.VerifyMobileReminderBottomSheetDialogFragment
@@ -152,6 +155,12 @@ class LogAndDebugFragment : BaseFragment(R.layout.fragment_log_debug) {
toast(R.string.Reset_TpSl_Guide)
}
+ resetMoreBadges.setOnClickListener {
+ defaultSharedPreferences.edit().remove(PREF_BOT_CLICKED_IDS).apply()
+ RxBus.publish(BadgeEvent(PREF_BOT_CLICKED_IDS))
+ toast(R.string.Reset_More_Badges)
+ }
+
resetWalletHomeBanners.setOnClickListener {
lifecycleScope.launch {
resetWalletHomeBannerLocalState()
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt
index 6a46f9012d..cd90568f77 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt
@@ -69,6 +69,8 @@ import one.mixin.android.ui.common.UserBottomSheetDialogFragment
import one.mixin.android.ui.common.recyclerview.HeaderAdapter
import one.mixin.android.ui.home.reminder.RecoveryReminderBottomSheetDialogFragment
import one.mixin.android.ui.home.bot.INTERNAL_REFERRAL_ID
+import one.mixin.android.ui.home.inscription.CollectiblesFragment
+import one.mixin.android.ui.home.inscription.InscriptionActivity
import one.mixin.android.ui.home.web3.trade.SwapActivity
import one.mixin.android.ui.home.web3.trade.TradeFragment
import one.mixin.android.ui.home.web3.trade.perps.PerpsActivity
@@ -118,6 +120,7 @@ import one.mixin.android.vo.PendingDisplay
import one.mixin.android.vo.SnapshotItem
import one.mixin.android.vo.WalletHomeTokenSummary
import one.mixin.android.vo.notMessengerUser
+import one.mixin.android.vo.safe.SafeCollectible
import one.mixin.android.vo.safe.TokenItem
import one.mixin.android.vo.safe.toSnapshot
import one.mixin.android.ui.web.WebActivity
@@ -150,6 +153,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet)
private val walletViewModel by viewModels()
private var assets: List = listOf()
+ private var collectibles: List = emptyList()
private var tokenSummary = WalletHomeTokenSummary()
private var recentSnapshots: List = emptyList()
private var positions: List = emptyList()
@@ -226,6 +230,10 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet)
) {
super.onViewCreated(view, savedInstanceState)
Timber.e("onViewCreated called in WalletHomePrivacyFragment")
+ walletViewModel.collectibles().observe(viewLifecycleOwner) {
+ collectibles = it
+ renderHome()
+ }
_walletId.value = Session.getAccountId().orEmpty()
loadCachedCashAccount()
refreshBitcoinPrice()
@@ -456,6 +464,7 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet)
hasEarnAccount = currentEarnAccounts.isNotEmpty(),
hasTopMovers = topMovers.isNotEmpty(),
hasTransactions = recentSnapshots.isNotEmpty(),
+ hasCollectibles = collectibles.isNotEmpty(),
hasPendingIndicator = pendingDisplays.isNotEmpty(),
isLoading = isLoading,
)
@@ -467,6 +476,8 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet)
btcTotal = formatWalletHomeBtcTotal(totalBtc),
fiatSymbol = Fiats.getSymbol(),
privacyTokens = assets.take(WalletHomeSection.PREVIEW_LIMIT),
+ collectibles = collectibles.take(WalletHomeSection.PREVIEW_LIMIT),
+ totalCollectibleCount = collectibles.size,
privacyTransactions = recentSnapshots.take(WalletHomeSection.PREVIEW_LIMIT),
positions = positions.take(WalletHomeSection.PREVIEW_LIMIT),
positionSummary = positions.toWalletHomePositionSummary(),
@@ -799,6 +810,16 @@ class WalletHomePrivacyFragment : BaseFragment(R.layout.fragment_privacy_wallet)
override fun onImportKeyLearnMoreClicked() = Unit
+ override fun onViewMoreCollectiblesClicked() {
+ navTo(CollectiblesFragment.newInstance(), CollectiblesFragment.TAG)
+ defaultSharedPreferences.putBoolean(Constants.Account.PREF_HAS_USED_MARKET, false)
+ RxBus.publish(BadgeEvent(Constants.Account.PREF_HAS_USED_MARKET))
+ }
+
+ override fun onCollectibleClicked(inscriptionHash: String) {
+ InscriptionActivity.show(requireContext(), inscriptionHash)
+ }
+
override fun onViewMoreTokensClicked() {
val state = _homeState.value
val snapshot = WalletHomeBalanceSnapshot(
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt
index c798fb6eed..4698b2274b 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/WalletViewModel.kt
@@ -52,6 +52,7 @@ import one.mixin.android.repository.TokenRepository
import one.mixin.android.repository.UserRepository
import one.mixin.android.repository.Web3Repository
import one.mixin.android.tip.TipBody
+import one.mixin.android.tip.wc.SortOrder
import one.mixin.android.ui.home.web3.widget.MarketSort
import one.mixin.android.ui.oldwallet.AssetRepository
import one.mixin.android.util.SINGLE_DB_THREAD
@@ -112,6 +113,8 @@ internal constructor(
fun walletHomeTokenSummary() = tokenRepository.walletHomeTokenSummary()
+ fun collectibles() = tokenRepository.collectibles(SortOrder.Recent)
+
suspend fun cashAccount(): MixinResponse =
withContext(Dispatchers.IO) {
cashRepository.account()
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeBuilder.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeBuilder.kt
index 6baf15bb3b..a5e05a9314 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeBuilder.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeBuilder.kt
@@ -11,6 +11,7 @@ object WalletHomeBuilder {
hasEarnAccount: Boolean = false,
hasTopMovers: Boolean,
hasTransactions: Boolean,
+ hasCollectibles: Boolean = false,
hasImportKeyAction: Boolean = false,
hasPendingIndicator: Boolean = false,
isWatchWallet: Boolean = false,
@@ -38,6 +39,7 @@ object WalletHomeBuilder {
if (walletType == WalletHomeType.PRIVACY && hasPositions) cards += WalletHomeCardType.POSITIONS
cards += WalletHomeCardType.TOKENS
+ if (walletType == WalletHomeType.PRIVACY && hasCollectibles) cards += WalletHomeCardType.COLLECTIBLES
if (hasTransactions) cards += WalletHomeCardType.TRANSACTIONS
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeItem.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeItem.kt
index 124e8f4cb7..19932efda7 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeItem.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeItem.kt
@@ -20,6 +20,7 @@ enum class WalletHomeCardType {
POSITIONS,
TOP_MOVERS,
TOKENS,
+ COLLECTIBLES,
TRANSACTIONS,
REFERRAL,
SUPPORT,
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt
index 9982479adb..a2ac6acb0c 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/home/WalletHomeState.kt
@@ -11,6 +11,7 @@ import one.mixin.android.extension.numberFormat8
import one.mixin.android.vo.PendingDisplay
import one.mixin.android.vo.SnapshotItem
import one.mixin.android.vo.WalletCategory
+import one.mixin.android.vo.safe.SafeCollectible
import one.mixin.android.vo.safe.TokenItem
import java.math.BigDecimal
@@ -23,6 +24,8 @@ data class WalletHomeState(
val btcTotal: String = "0.00",
val fiatSymbol: String = "",
val privacyTokens: List = emptyList(),
+ val collectibles: List = emptyList(),
+ val totalCollectibleCount: Int = 0,
val web3Tokens: List = emptyList(),
val privacyTransactions: List = emptyList(),
val web3Transactions: List = emptyList(),
@@ -85,6 +88,8 @@ interface WalletHomeCallbacks {
fun onWatchIndicatorClicked()
fun onImportKeyClicked()
fun onImportKeyLearnMoreClicked()
+ fun onViewMoreCollectiblesClicked() = Unit
+ fun onCollectibleClicked(inscriptionHash: String) = Unit
fun onViewMoreTokensClicked()
fun onAllTokensBackClicked()
fun onViewMoreTransactionsClicked()
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/SectionCard.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/SectionCard.kt
index 56d5ca0011..8eaeee08c4 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/SectionCard.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/SectionCard.kt
@@ -3,6 +3,7 @@ package one.mixin.android.ui.wallet.home.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@@ -37,6 +38,7 @@ internal fun SectionCard(
contentUsesOwnPadding: Boolean = false,
contentFlush: Boolean = false,
showBottomSpacer: Boolean = true,
+ headerPadding: PaddingValues = PaddingValues(start = 20.dp, end = 20.dp, top = 20.dp, bottom = if (contentFlush) 0.dp else 20.dp),
headerTrailing: (@Composable RowScope.() -> Unit)? = null,
content: @Composable () -> Unit,
) {
@@ -44,12 +46,7 @@ internal fun SectionCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() }
- .padding(
- start = 20.dp,
- end = 20.dp,
- top = 20.dp,
- bottom = if (contentFlush) 0.dp else 20.dp,
- ),
+ .padding(headerPadding),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
diff --git a/app/src/main/java/one/mixin/android/ui/wallet/home/components/WalletHomeCard.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/WalletHomeCard.kt
index 9ae8ec1776..41573bace6 100644
--- a/app/src/main/java/one/mixin/android/ui/wallet/home/components/WalletHomeCard.kt
+++ b/app/src/main/java/one/mixin/android/ui/wallet/home/components/WalletHomeCard.kt
@@ -3,6 +3,7 @@ package one.mixin.android.ui.wallet.home.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -31,6 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import one.mixin.android.R
+import one.mixin.android.compose.CoilImage
import one.mixin.android.compose.theme.MixinAppTheme
import one.mixin.android.ui.home.web3.trade.perps.TopMoversCard
import one.mixin.android.ui.landing.components.HighlightedTextWithClick
@@ -169,6 +171,37 @@ internal fun WalletHomeCard(
}
}
}
+ WalletHomeCardType.COLLECTIBLES -> SectionCard(
+ title = stringResource(R.string.Collectibles),
+ showViewAll = WalletHomeSection.hasMore(state.totalCollectibleCount),
+ onClick = callbacks::onViewMoreCollectiblesClicked,
+ contentUsesOwnPadding = true,
+ contentFlush = true,
+ showBottomSpacer = false,
+ headerPadding = PaddingValues(20.dp),
+ ) {
+ state.collectibles.take(PREVIEW_LIMIT).forEach { collectible ->
+ Row(
+ modifier = Modifier.fillMaxWidth()
+ .height(70.dp).padding(bottom = 20.dp)
+ .clickable { callbacks.onCollectibleClicked(collectible.inscriptionHash) }
+ .padding(horizontal = 20.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ CoilImage(
+ model = if (collectible.isText) collectible.iconURL else collectible.contentURL,
+ placeholder = if (collectible.isText) R.drawable.ic_text_inscription else R.drawable.ic_default_inscription,
+ modifier = Modifier.size(42.dp).clip(RoundedCornerShape(12.dp)),
+ )
+ Spacer(Modifier.width(14.dp))
+ Column(Modifier.weight(1f)) {
+ Text(collectible.name, color = MixinAppTheme.colors.textPrimary, fontSize = 16.sp, lineHeight = 19.sp, letterSpacing = 0.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
+ Spacer(Modifier.height(4.dp))
+ Text("#${collectible.sequence}", color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp)
+ }
+ }
+ }
+ }
WalletHomeCardType.TRANSACTIONS -> SectionCard(
title = stringResource(R.string.Transactions),
showViewAll = WalletHomeSection.hasMore(state.totalTransactionCount),
@@ -345,5 +378,6 @@ private fun WalletHomeCardType.hasSelfPaddedItems(): Boolean =
this == WalletHomeCardType.POSITIONS ||
this == WalletHomeCardType.TOP_MOVERS ||
this == WalletHomeCardType.TOKENS ||
+ this == WalletHomeCardType.COLLECTIBLES ||
this == WalletHomeCardType.TRANSACTIONS ||
this == WalletHomeCardType.REFERRAL
diff --git a/app/src/main/java/one/mixin/android/widget/MaterialSearchView.kt b/app/src/main/java/one/mixin/android/widget/MaterialSearchView.kt
index 4f8b5d98fd..2b03ab2e73 100644
--- a/app/src/main/java/one/mixin/android/widget/MaterialSearchView.kt
+++ b/app/src/main/java/one/mixin/android/widget/MaterialSearchView.kt
@@ -38,9 +38,7 @@ import one.mixin.android.extension.screenHeight
import one.mixin.android.extension.showKeyboard
import one.mixin.android.extension.translationX
import one.mixin.android.extension.withAlpha
-import one.mixin.android.session.Session
import one.mixin.android.ui.search.SearchFragment.Companion.SEARCH_DEBOUNCE
-import one.mixin.android.vo.toUser
import org.jetbrains.annotations.NotNull
import java.util.concurrent.TimeUnit
import kotlin.math.roundToInt
@@ -58,7 +56,6 @@ class MaterialSearchView : FrameLayout {
private var binding: ViewMaterialSearchBinding =
ViewMaterialSearchBinding.inflate(LayoutInflater.from(context), this, true)
val actionVa get() = binding.actionVa
- val avatar get() = binding.avatar
val logo get() = binding.logo
val dot get() = binding.dot
val scan get() = binding.scanIb
@@ -157,10 +154,6 @@ class MaterialSearchView : FrameLayout {
onSubmitQuery()
true
}
- Session.getAccount()?.toUser()?.let { u ->
- binding.avatar.setInfo(u.fullName, u.avatarUrl, u.userId)
- binding.avatar.setTextSize(14f)
- }
// Don't auto dispose
disposable =
@@ -194,7 +187,7 @@ class MaterialSearchView : FrameLayout {
containerDisplay = false
binding.searchIb.fadeIn()
binding.scanIb.fadeIn()
- binding.avatar.fadeIn()
+ binding.newChat.fadeIn()
binding.actionVa.fadeOut()
ValueAnimator.ofFloat(1f, 0f).apply {
addListener(
@@ -222,7 +215,7 @@ class MaterialSearchView : FrameLayout {
containerDisplay = true
binding.searchIb.fadeOut()
binding.scanIb.fadeOut()
- binding.avatar.fadeOut()
+ binding.newChat.fadeOut()
binding.actionVa.fadeIn()
binding.containerCircle.isVisible = true
showAction?.invoke()
@@ -256,7 +249,7 @@ class MaterialSearchView : FrameLayout {
private val rightTranslationX = 168f
fun dragSearch(progress: Float) {
- binding.avatar.translationX = context.dpToPx(rightTranslationX) * progress
+ binding.newChat.translationX = context.dpToPx(rightTranslationX) * progress
binding.searchIb.translationX = context.dpToPx(rightTranslationX) * progress
binding.scanIb.translationX = context.dpToPx(rightTranslationX) * progress
val fastFadeOut = (1 - 2 * progress).coerceAtLeast(0f)
@@ -317,7 +310,7 @@ class MaterialSearchView : FrameLayout {
binding.searchEt.setText("")
oldLeftX = binding.logoLayout.x
oldSearchWidth = binding.searchEt.measuredWidth
- binding.avatar.translationX(context.dpToPx(rightTranslationX).toFloat())
+ binding.newChat.translationX(context.dpToPx(rightTranslationX).toFloat())
binding.searchIb.translationX(context.dpToPx(rightTranslationX).toFloat())
binding.scanIb.translationX(context.dpToPx(rightTranslationX).toFloat())
mSearchViewListener?.onSearchViewOpened()
@@ -375,7 +368,7 @@ class MaterialSearchView : FrameLayout {
binding.rightClear.visibility = View.GONE
hideLoading()
- binding.avatar.translationX(0f)
+ binding.newChat.translationX(0f)
binding.searchIb.translationX(0f)
binding.scanIb.translationX(0f)
clearFocus()
@@ -470,8 +463,8 @@ class MaterialSearchView : FrameLayout {
binding.searchView.layoutParams.height = height
}
- fun setOnGroupClickListener(onClickListener: OnClickListener) {
- binding.avatar.setOnClickListener(onClickListener)
+ fun setOnNewChatClickListener(onClickListener: OnClickListener) {
+ binding.newChat.setOnClickListener(onClickListener)
}
fun setOnAddClickListener(onClickListener: OnClickListener) {
diff --git a/app/src/main/res/drawable/bg_circle_contact.xml b/app/src/main/res/drawable/bg_circle_contact.xml
deleted file mode 100644
index b40e0544bf..0000000000
--- a/app/src/main/res/drawable/bg_circle_contact.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_add_contact.xml b/app/src/main/res/drawable/ic_add_contact.xml
deleted file mode 100644
index 8961779ebd..0000000000
--- a/app/src/main/res/drawable/ic_add_contact.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_buy.xml b/app/src/main/res/drawable/ic_bot_buy.xml
deleted file mode 100644
index 93e367d6c6..0000000000
--- a/app/src/main/res/drawable/ic_bot_buy.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_circle.xml b/app/src/main/res/drawable/ic_bot_circle.xml
deleted file mode 100644
index e63bd17136..0000000000
--- a/app/src/main/res/drawable/ic_bot_circle.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_desktop.xml b/app/src/main/res/drawable/ic_bot_desktop.xml
deleted file mode 100644
index 5ee0142bc9..0000000000
--- a/app/src/main/res/drawable/ic_bot_desktop.xml
+++ /dev/null
@@ -1,22 +0,0 @@
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_desktop_logged.xml b/app/src/main/res/drawable/ic_bot_desktop_logged.xml
deleted file mode 100644
index bd53794d26..0000000000
--- a/app/src/main/res/drawable/ic_bot_desktop_logged.xml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_member.xml b/app/src/main/res/drawable/ic_bot_member.xml
deleted file mode 100644
index 6d0771804e..0000000000
--- a/app/src/main/res/drawable/ic_bot_member.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_referral.xml b/app/src/main/res/drawable/ic_bot_referral.xml
deleted file mode 100644
index e106bbe4a4..0000000000
--- a/app/src/main/res/drawable/ic_bot_referral.xml
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_support.xml b/app/src/main/res/drawable/ic_bot_support.xml
deleted file mode 100644
index 9f0e27ff07..0000000000
--- a/app/src/main/res/drawable/ic_bot_support.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_bot_swap.xml b/app/src/main/res/drawable/ic_bot_swap.xml
deleted file mode 100644
index 6bbc7fbcfd..0000000000
--- a/app/src/main/res/drawable/ic_bot_swap.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_contacts_add_contact.xml b/app/src/main/res/drawable/ic_contacts_add_contact.xml
new file mode 100644
index 0000000000..dad7be6c87
--- /dev/null
+++ b/app/src/main/res/drawable/ic_contacts_add_contact.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_contacts_new_group.xml b/app/src/main/res/drawable/ic_contacts_new_group.xml
new file mode 100644
index 0000000000..06e59ac6c5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_contacts_new_group.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_contacts_qr.xml b/app/src/main/res/drawable/ic_contacts_qr.xml
deleted file mode 100644
index 62c1ba2783..0000000000
--- a/app/src/main/res/drawable/ic_contacts_qr.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_contacts_receive.xml b/app/src/main/res/drawable/ic_contacts_receive.xml
deleted file mode 100644
index 81edc8e808..0000000000
--- a/app/src/main/res/drawable/ic_contacts_receive.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_contacts_search.xml b/app/src/main/res/drawable/ic_contacts_search.xml
new file mode 100644
index 0000000000..88288c2d0a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_contacts_search.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_dot_red_border.xml b/app/src/main/res/drawable/ic_dot_red_border.xml
deleted file mode 100644
index d6d30bb567..0000000000
--- a/app/src/main/res/drawable/ic_dot_red_border.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/app/src/main/res/drawable/ic_more_arrow.xml b/app/src/main/res/drawable/ic_more_arrow.xml
new file mode 100644
index 0000000000..4712894b68
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_arrow.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_buy.xml b/app/src/main/res/drawable/ic_more_buy.xml
new file mode 100644
index 0000000000..f3c173eafa
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_buy.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_desktop_logged.xml b/app/src/main/res/drawable/ic_more_desktop_logged.xml
new file mode 100644
index 0000000000..758d58191c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_desktop_logged.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_link.xml b/app/src/main/res/drawable/ic_more_link.xml
new file mode 100644
index 0000000000..0295778010
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_link.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_member.xml b/app/src/main/res/drawable/ic_more_member.xml
new file mode 100644
index 0000000000..96d8cdf1d0
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_member.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_qr.xml b/app/src/main/res/drawable/ic_more_qr.xml
new file mode 100644
index 0000000000..52809a8bd5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_qr.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_search.xml b/app/src/main/res/drawable/ic_more_search.xml
new file mode 100644
index 0000000000..14f3606a4b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_search.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_support.xml b/app/src/main/res/drawable/ic_more_support.xml
new file mode 100644
index 0000000000..377effebc7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_support.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_more_swap.xml b/app/src/main/res/drawable/ic_more_swap.xml
new file mode 100644
index 0000000000..aad369e902
--- /dev/null
+++ b/app/src/main/res/drawable/ic_more_swap.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_new_chat.xml b/app/src/main/res/drawable/ic_new_chat.xml
new file mode 100644
index 0000000000..ca069c3fcd
--- /dev/null
+++ b/app/src/main/res/drawable/ic_new_chat.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_new_group.xml b/app/src/main/res/drawable/ic_new_group.xml
deleted file mode 100644
index 6bffad87c7..0000000000
--- a/app/src/main/res/drawable/ic_new_group.xml
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/app/src/main/res/drawable/ic_profile_add_contact.xml b/app/src/main/res/drawable/ic_profile_add_contact.xml
new file mode 100644
index 0000000000..576d41abdb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_add_contact.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_arrow.xml b/app/src/main/res/drawable/ic_profile_arrow.xml
new file mode 100644
index 0000000000..aae5c73f09
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_arrow.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_chat.xml b/app/src/main/res/drawable/ic_profile_chat.xml
new file mode 100644
index 0000000000..5c32d410b6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_chat.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_contacts.xml b/app/src/main/res/drawable/ic_profile_contacts.xml
new file mode 100644
index 0000000000..a7f92b1ebe
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_contacts.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_details.xml b/app/src/main/res/drawable/ic_profile_details.xml
new file mode 100644
index 0000000000..ed42dc1e79
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_details.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_group.xml b/app/src/main/res/drawable/ic_profile_group.xml
new file mode 100644
index 0000000000..49dce17cb2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_group.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_qr.xml b/app/src/main/res/drawable/ic_profile_qr.xml
new file mode 100644
index 0000000000..36bcea1254
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_qr.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_receive.xml b/app/src/main/res/drawable/ic_profile_receive.xml
new file mode 100644
index 0000000000..38982574cc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_receive.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_profile_settings.xml b/app/src/main/res/drawable/ic_profile_settings.xml
new file mode 100644
index 0000000000..6d0b0c1e16
--- /dev/null
+++ b/app/src/main/res/drawable/ic_profile_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_collectibles.xml b/app/src/main/res/layout/fragment_collectibles.xml
index cff070a3d8..1b4c9706c8 100644
--- a/app/src/main/res/layout/fragment_collectibles.xml
+++ b/app/src/main/res/layout/fragment_collectibles.xml
@@ -8,6 +8,14 @@
android:gravity="center_horizontal"
android:orientation="vertical">
+
+
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_contacts.xml b/app/src/main/res/layout/fragment_contacts.xml
deleted file mode 100644
index b0bc77c83d..0000000000
--- a/app/src/main/res/layout/fragment_contacts.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml
index 0a577f7514..479612a4cf 100644
--- a/app/src/main/res/layout/fragment_explore.xml
+++ b/app/src/main/res/layout/fragment_explore.xml
@@ -1,80 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent" />
diff --git a/app/src/main/res/layout/fragment_log_debug.xml b/app/src/main/res/layout/fragment_log_debug.xml
index f29e00dea4..4e45505ecf 100644
--- a/app/src/main/res/layout/fragment_log_debug.xml
+++ b/app/src/main/res/layout/fragment_log_debug.xml
@@ -225,6 +225,18 @@
android:foreground="?android:attr/selectableItemBackground"
android:textSize="16sp" />
+
+
diff --git a/app/src/main/res/layout/fragment_wallet.xml b/app/src/main/res/layout/fragment_wallet.xml
index d8eff002af..44ecccdc9b 100644
--- a/app/src/main/res/layout/fragment_wallet.xml
+++ b/app/src/main/res/layout/fragment_wallet.xml
@@ -71,37 +71,37 @@
diff --git a/app/src/main/res/layout/item_contact_contact.xml b/app/src/main/res/layout/item_contact_contact.xml
deleted file mode 100644
index 03d2b67518..0000000000
--- a/app/src/main/res/layout/item_contact_contact.xml
+++ /dev/null
@@ -1,30 +0,0 @@
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_favorite.xml b/app/src/main/res/layout/item_favorite.xml
index 343fc7756f..aebb903304 100644
--- a/app/src/main/res/layout/item_favorite.xml
+++ b/app/src/main/res/layout/item_favorite.xml
@@ -19,36 +19,6 @@
android:layout_height="50dp"
android:layout_centerInParent="true" />
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/layout/item_favorite_edit.xml b/app/src/main/res/layout/item_favorite_edit.xml
deleted file mode 100644
index f7661ee2b3..0000000000
--- a/app/src/main/res/layout/item_favorite_edit.xml
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/item_favorite_title.xml b/app/src/main/res/layout/item_favorite_title.xml
deleted file mode 100644
index 095b58074e..0000000000
--- a/app/src/main/res/layout/item_favorite_title.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_contact_header.xml b/app/src/main/res/layout/view_contact_header.xml
deleted file mode 100644
index 407ab5fa61..0000000000
--- a/app/src/main/res/layout/view_contact_header.xml
+++ /dev/null
@@ -1,212 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_contact_list_empty.xml b/app/src/main/res/layout/view_contact_list_empty.xml
deleted file mode 100644
index 8caac3a478..0000000000
--- a/app/src/main/res/layout/view_contact_list_empty.xml
+++ /dev/null
@@ -1,52 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/layout/view_home_toolbar.xml b/app/src/main/res/layout/view_home_toolbar.xml
index 53140db432..e66bfd375e 100644
--- a/app/src/main/res/layout/view_home_toolbar.xml
+++ b/app/src/main/res/layout/view_home_toolbar.xml
@@ -18,38 +18,38 @@
+ android:contentDescription="@string/Search" />
+ android:contentDescription="@string/Scan_QR_Code" />
+ android:padding="12dp"
+ app:srcCompat="@drawable/ic_profile_settings"
+ android:contentDescription="@string/Settings" />
diff --git a/app/src/main/res/layout/view_material_search.xml b/app/src/main/res/layout/view_material_search.xml
index 4c40cb1c8d..e8e39a0cfc 100644
--- a/app/src/main/res/layout/view_material_search.xml
+++ b/app/src/main/res/layout/view_material_search.xml
@@ -35,25 +35,25 @@
@@ -106,16 +106,17 @@
app:srcCompat="@drawable/ic_back"
tools:ignore="ContentDescription" />
-
+ android:contentDescription="@string/new_chat"
+ app:srcCompat="@drawable/ic_new_chat" />
@color/textPrimary
- @color/colorIcon
+ - @color/more_icon
- @color/colorIconWhite
- @color/colorIcon
- @color/colorIconGoogleBlack
diff --git a/app/src/main/res/values-v35/styles.xml b/app/src/main/res/values-v35/styles.xml
index 047f5f6665..7289ceb5e7 100644
--- a/app/src/main/res/values-v35/styles.xml
+++ b/app/src/main/res/values-v35/styles.xml
@@ -34,6 +34,7 @@
- @color/textPrimary
- @color/colorIcon
+ - @color/more_icon
- @color/colorIconWhite
- @color/colorIcon
- @color/colorIconGoogleBlack
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 26dd103d0c..0c1bff5f40 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -2033,6 +2033,7 @@
所有资产
隐藏的资产不计入统计
重置隐藏偏好
+ 重置 More 小红点
重置钱包首页 Banner
删除 Web3 交易数据
确定要删除所有 Web3 交易数据吗?此操作不可恢复。删除后系统将重新同步数据。
@@ -2614,6 +2615,9 @@
总收益
昨日收益
收益率
+ 发起聊天
+ 联系人
+ Mixin ID、姓名、手机号
调整
调整保证金或仓位
添加保证金
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 38e106dcee..58eebfb198 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -1167,6 +1167,7 @@
理財賬戶
現金賬戶
輕鬆出售加密貨幣,賬戶餘額自動賺取年化收益。
+ 重置 More 小紅點
重置錢包首頁 Banner
現金餘額
理財餘額
@@ -2666,6 +2667,9 @@
總收益
昨日收益
收益率
+ 發起聊天
+ 聯絡人
+ Mixin ID、姓名、手機號碼
調整
調整保證金或倉位
新增保證金
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index cbaab93ff7..0b27bc4665 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -21,6 +21,7 @@
#828589
#FF000000
+ #2F3032
#3C4043
#FFFFFF
#D2D4DA
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5bf8c9d1a0..772cd37969 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2087,6 +2087,7 @@
Total Balance
Hidden assets are not included in the statistics.
Reset hidden preferences
+ Reset More badges
Reset wallet home banners
Delete Web3 Transaction Data
Are you sure you want to delete all Web3 transaction data? This action cannot be undone. The data will be re-synchronized after deletion.
@@ -2696,6 +2697,9 @@
Avoid transfer errors caused by complex wallet addresses.
Block malicious apps from gaining unsafe permissions.
No Tokens
+ New Chat
+ Contacts
+ Mixin ID, Name, Phone number
Adjust
Adjust Margin or Position
Add Margin
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index e511b177c9..59b1fccf53 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -30,6 +30,7 @@
- @color/textPrimary
- @color/colorIcon
+ - @color/more_icon
- @color/colorIconWhite
- @color/colorIcon
- @color/colorIconGoogleBlack
diff --git a/app/src/main/res/values/styles_night.xml b/app/src/main/res/values/styles_night.xml
index d3171ccb8c..a4e20561bf 100644
--- a/app/src/main/res/values/styles_night.xml
+++ b/app/src/main/res/values/styles_night.xml
@@ -15,6 +15,7 @@
- @color/textPrimaryNight
- @color/colorIconNight
+ - @color/colorIconNight
- @color/colorIconWhiteNight
- @color/colorIconGrayNight
- @color/colorIconBlackNight
diff --git a/app/src/main/res/values/theme.xml b/app/src/main/res/values/theme.xml
index 905178de3e..7c4ce3bb70 100644
--- a/app/src/main/res/values/theme.xml
+++ b/app/src/main/res/values/theme.xml
@@ -15,6 +15,7 @@
+
diff --git a/app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt b/app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt
new file mode 100644
index 0000000000..3bef588e9d
--- /dev/null
+++ b/app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt
@@ -0,0 +1,31 @@
+package one.mixin.android.ui.contacts
+
+import android.app.Application
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class, sdk = [28])
+class ContactInitialTest {
+ @Test
+ fun indexDragMapsPositionsAndClampsOutsideTheRail() {
+ assertEquals("A", contactLetterAt(-20f, 378))
+ assertEquals("A", contactLetterAt(13f, 378))
+ assertEquals("B", contactLetterAt(14f, 378))
+ assertEquals("N", contactLetterAt(189f, 378))
+ assertEquals("#", contactLetterAt(378f, 378))
+ assertEquals("#", contactLetterAt(500f, 378))
+ }
+
+ @Test
+ fun initialsSupportLatinPinyinAndUnnamedContacts() {
+ assertEquals("A", contactInitial(" alice "))
+ assertEquals("E", contactInitial("Élodie"))
+ assertEquals("B", contactInitial("白话区块链"))
+ assertEquals("#", contactInitial(null))
+ assertEquals("#", contactInitial("123"))
+ }
+}
diff --git a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt
index c25f1622d4..3025294c55 100644
--- a/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt
+++ b/app/src/test/java/one/mixin/android/ui/wallet/home/WalletHomeBuilderTest.kt
@@ -7,6 +7,28 @@ import org.junit.Assert.assertEquals
import org.junit.Test
class WalletHomeBuilderTest {
+ @Test
+ fun collectiblesFollowAssetsOnlyInPrivacyWalletsWithCollectibles() {
+ for (type in WalletHomeType.entries) {
+ for (hasCollectibles in listOf(false, true)) {
+ val cards = WalletHomeBuilder.build(
+ walletType = type,
+ hasAssetValue = true,
+ showBanner = false,
+ showReferral = false,
+ hasPositions = false,
+ hasTopMovers = false,
+ hasTransactions = true,
+ hasCollectibles = hasCollectibles,
+ )
+ val expected = mutableListOf(WalletHomeCardType.BALANCE, WalletHomeCardType.TOKENS)
+ if (type == WalletHomeType.PRIVACY && hasCollectibles) expected += WalletHomeCardType.COLLECTIBLES
+ expected += listOf(WalletHomeCardType.TRANSACTIONS, WalletHomeCardType.SUPPORT)
+ assertEquals(expected, cards)
+ }
+ }
+ }
+
@Test
fun privacyWalletWithPositionsDoesNotShowTopMovers() {
val cards = WalletHomeBuilder.build(