From 04c204ef4f6c6e943cbff2cd962caac8d967c8cf Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 22 Sep 2026 10:24:45 +0800 Subject: [PATCH 1/9] feat(ui): redesign more and new chat navigation --- app/src/main/AndroidManifest.xml | 5 - .../main/java/one/mixin/android/Constants.kt | 1 - .../ui/address/component/SearchTextField.kt | 3 +- .../android/ui/common/NavigationController.kt | 6 - .../ui/contacts/ContactBottomSheetDialog.kt | 61 --- .../ui/contacts/ContactListFragment.kt | 32 ++ .../android/ui/contacts/ContactsActivity.kt | 43 -- .../android/ui/contacts/ContactsAdapter.kt | 292 ---------- .../android/ui/contacts/ContactsFragment.kt | 235 -------- .../mixin/android/ui/contacts/ContactsPage.kt | 140 +++++ .../NewChatBottomSheetDialogFragment.kt | 50 ++ .../ui/home/ConversationListFragment.kt | 17 +- .../mixin/android/ui/home/ExploreFragment.kt | 504 ++++-------------- .../one/mixin/android/ui/home/MorePage.kt | 223 ++++++++ .../mixin/android/ui/home/bot/InternalApp.kt | 14 +- .../home/inscription/CollectiblesFragment.kt | 19 + .../ui/wallet/WalletHomePrivacyFragment.kt | 21 + .../android/ui/wallet/WalletViewModel.kt | 3 + .../ui/wallet/home/WalletHomeBuilder.kt | 2 + .../android/ui/wallet/home/WalletHomeItem.kt | 1 + .../android/ui/wallet/home/WalletHomeState.kt | 5 + .../wallet/home/components/WalletHomeCard.kt | 29 + .../android/widget/MaterialSearchView.kt | 21 +- .../main/res/drawable/bg_circle_contact.xml | 4 - app/src/main/res/drawable/ic_bot_buy.xml | 21 - app/src/main/res/drawable/ic_bot_circle.xml | 10 - app/src/main/res/drawable/ic_bot_desktop.xml | 22 - .../res/drawable/ic_bot_desktop_logged.xml | 29 - app/src/main/res/drawable/ic_bot_member.xml | 33 -- app/src/main/res/drawable/ic_bot_referral.xml | 15 - app/src/main/res/drawable/ic_bot_support.xml | 37 -- app/src/main/res/drawable/ic_bot_swap.xml | 14 - app/src/main/res/drawable/ic_contacts_qr.xml | 3 - .../main/res/drawable/ic_contacts_receive.xml | 4 - .../main/res/drawable/ic_dot_red_border.xml | 7 - app/src/main/res/drawable/ic_more_buy.xml | 5 + .../res/drawable/ic_more_desktop_logged.xml | 7 + app/src/main/res/drawable/ic_more_member.xml | 7 + app/src/main/res/drawable/ic_more_swap.xml | 5 + app/src/main/res/drawable/ic_new_chat.xml | 13 + .../main/res/layout/fragment_collectibles.xml | 8 + .../layout/fragment_contact_bottom_sheet.xml | 58 -- app/src/main/res/layout/fragment_contacts.xml | 20 - app/src/main/res/layout/fragment_explore.xml | 68 +-- .../main/res/layout/item_contact_contact.xml | 30 -- app/src/main/res/layout/item_favorite.xml | 32 +- .../main/res/layout/item_favorite_edit.xml | 56 -- .../main/res/layout/item_favorite_title.xml | 21 - .../main/res/layout/view_contact_header.xml | 212 -------- .../res/layout/view_contact_list_empty.xml | 52 -- .../main/res/layout/view_material_search.xml | 19 +- app/src/main/res/raw/referral.json | 1 - app/src/main/res/values-zh-rCN/strings.xml | 3 + app/src/main/res/values-zh-rTW/strings.xml | 3 + app/src/main/res/values/strings.xml | 3 + .../android/ui/contacts/ContactInitialTest.kt | 21 + .../ui/wallet/home/WalletHomeBuilderTest.kt | 22 + 57 files changed, 754 insertions(+), 1838 deletions(-) delete mode 100644 app/src/main/java/one/mixin/android/ui/contacts/ContactBottomSheetDialog.kt create mode 100644 app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt delete mode 100644 app/src/main/java/one/mixin/android/ui/contacts/ContactsActivity.kt delete mode 100644 app/src/main/java/one/mixin/android/ui/contacts/ContactsAdapter.kt delete mode 100644 app/src/main/java/one/mixin/android/ui/contacts/ContactsFragment.kt create mode 100644 app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt create mode 100644 app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt create mode 100644 app/src/main/java/one/mixin/android/ui/home/MorePage.kt delete mode 100644 app/src/main/res/drawable/bg_circle_contact.xml delete mode 100644 app/src/main/res/drawable/ic_bot_buy.xml delete mode 100644 app/src/main/res/drawable/ic_bot_circle.xml delete mode 100644 app/src/main/res/drawable/ic_bot_desktop.xml delete mode 100644 app/src/main/res/drawable/ic_bot_desktop_logged.xml delete mode 100644 app/src/main/res/drawable/ic_bot_member.xml delete mode 100644 app/src/main/res/drawable/ic_bot_referral.xml delete mode 100644 app/src/main/res/drawable/ic_bot_support.xml delete mode 100644 app/src/main/res/drawable/ic_bot_swap.xml delete mode 100644 app/src/main/res/drawable/ic_contacts_qr.xml delete mode 100644 app/src/main/res/drawable/ic_contacts_receive.xml delete mode 100644 app/src/main/res/drawable/ic_dot_red_border.xml create mode 100644 app/src/main/res/drawable/ic_more_buy.xml create mode 100644 app/src/main/res/drawable/ic_more_desktop_logged.xml create mode 100644 app/src/main/res/drawable/ic_more_member.xml create mode 100644 app/src/main/res/drawable/ic_more_swap.xml create mode 100644 app/src/main/res/drawable/ic_new_chat.xml delete mode 100644 app/src/main/res/layout/fragment_contact_bottom_sheet.xml delete mode 100644 app/src/main/res/layout/fragment_contacts.xml delete mode 100644 app/src/main/res/layout/item_contact_contact.xml delete mode 100644 app/src/main/res/layout/item_favorite_edit.xml delete mode 100644 app/src/main/res/layout/item_favorite_title.xml delete mode 100644 app/src/main/res/layout/view_contact_header.xml delete mode 100644 app/src/main/res/layout/view_contact_list_empty.xml delete mode 100644 app/src/main/res/raw/referral.json create mode 100644 app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt 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/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..ae51527e08 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt @@ -0,0 +1,32 @@ +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.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(users, { requireActivity().onBackPressedDispatcher.onBackPressed() }, { showUserBottom(parentFragmentManager, it) }) + } + } +} 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..1d6e3234b2 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt @@ -0,0 +1,140 @@ +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.layout.Box +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.lazy.LazyColumn +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.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +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.ui.address.component.SearchTextField +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() } + +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 scope = rememberCoroutineScope() + MixinAppTheme { + Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) { + ContactPageHeader(stringResource(if (newChat) R.string.new_chat else R.string.contacts_title), onBack, newChat) + SearchTextField(query, { query = it }, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp), hint = stringResource(R.string.contacts_search_hint)) + Box(Modifier.weight(1f)) { + LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { + if (onNewGroup != null && onAddContact != null) { + item { ContactAction(R.drawable.ic_new_group, R.string.New_Group, onNewGroup) } + item { ContactAction(R.drawable.ic_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") { + Text(initial, color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.padding(20.dp)) + } + items(users, key = { it.userId }) { user -> + Row(Modifier.fillMaxWidth().clickable { onContact(user) }.padding(start = 20.dp, end = 40.dp).height(70.dp), verticalAlignment = Alignment.CenterVertically) { + UserAvatarImage(user, 50.dp) + Spacer(Modifier.width(20.dp)) + AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user) }, modifier = Modifier.weight(1f)) + } + } + } + } + if (groups.isNotEmpty()) { + Column(Modifier.align(Alignment.CenterEnd).padding(end = 8.dp)) { + (('A'..'Z').map { it.toString() } + "#").forEach { initial -> + Text(initial, color = MixinAppTheme.colors.textAssist, fontSize = 10.sp, lineHeight = 14.sp, + modifier = Modifier.clickable { + var offset = if (newChat) 2 else 0 + for ((label, users) in groups) { + if (label == initial) { + val targetIndex = offset + scope.launch { listState.scrollToItem(targetIndex) } + break + } + offset += users.size + 1 + } + }.padding(horizontal = 8.dp)) + } + } + } + } + } + } +} + +@Composable +internal fun ContactPageHeader(title: String, onBack: () -> Unit, close: Boolean = false) { + Row(Modifier.fillMaxWidth().height(56.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 = MixinAppTheme.colors.textPrimary, fontSize = 18.sp, fontWeight = FontWeight.W600, + modifier = Modifier.weight(1f).padding(start = if (close) 16.dp else 0.dp), textAlign = if (close) androidx.compose.ui.text.style.TextAlign.Start else androidx.compose.ui.text.style.TextAlign.Center) + if (close) { + IconButton(onClick = onBack) { Icon(painterResource(R.drawable.ic_circle_close), stringResource(R.string.Close), tint = androidx.compose.ui.graphics.Color.Unspecified, modifier = Modifier.size(26.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(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(painterResource(icon), null, tint = androidx.compose.ui.graphics.Color.Unspecified, modifier = Modifier.size(50.dp)) + Spacer(Modifier.width(20.dp)) + Text(stringResource(title), color = MixinAppTheme.colors.accent, fontSize = 16.sp, modifier = Modifier.weight(1f)) + Icon(painterResource(R.drawable.ic_arrow_gray_right), null, tint = MixinAppTheme.colors.iconGray) + } +} 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..9d8b25c8dd --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt @@ -0,0 +1,50 @@ +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.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" + } + + 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() - 56.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 7adca5eeaa..d9cf92fb70 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,46 @@ 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.openPermissionSetting -import one.mixin.android.extension.putBoolean -import one.mixin.android.extension.putInt import one.mixin.android.extension.toast -import one.mixin.android.job.TipCounterSyncedLiveData +import one.mixin.android.extension.withArgs 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.ProfileBottomSheetDialogFragment 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.conversation.ConversationActivity import one.mixin.android.ui.device.DeviceFragment import one.mixin.android.ui.home.bot.Bot @@ -53,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 @@ -75,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 = { ProfileBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, ProfileBottomSheetDialogFragment.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 -> @@ -323,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 -> { @@ -417,191 +294,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..9366418e98 --- /dev/null +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -0,0 +1,223 @@ +package one.mixin.android.ui.home + +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.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.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.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.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.ui.address.component.SearchTextField +import one.mixin.android.ui.contacts.ContactPageHeader +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.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 + +@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 { + LazyColumn( + modifier = Modifier.fillMaxSize().background(MixinAppTheme.colors.background), + contentPadding = androidx.compose.foundation.layout.PaddingValues(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(8.dp)) + Column(Modifier.weight(1f)) { + AndroidView(factory = { context -> NameTextView(context) }, update = { view -> view.setName(it) }) + Spacer(Modifier.height(4.dp)) + Text(stringResource(R.string.contact_mixin_id, it.identityNumber), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp) + } + IconButton(onClick = onQr) { + Icon(painterResource(R.drawable.ic_qr_code), stringResource(R.string.My_QR_Code), tint = MixinAppTheme.colors.textRemarks, modifier = Modifier.size(20.dp)) + } + MoreArrow() + } + 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) + } + } + if (index < minOf(contacts.size, 3) - 1) Spacer(Modifier.height(20.dp)) + } + Text(stringResource(R.string.view_all), color = MixinAppTheme.colors.accent, fontSize = 14.sp, + modifier = Modifier.align(Alignment.CenterHorizontally).clickable(onClick = onContacts).padding(top = 18.dp, bottom = 20.dp, start = 16.dp, end = 16.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) { onBot(app) } } + Spacer(Modifier.height(12.dp)) + } + } + } + } + } +} + +@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(12.dp)) { + Icon(painterResource(icon), null, tint = if (desktop && loggedIn) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.textPrimary, modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(bot.name), color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.weight(1f)) + if (showDot) Box(Modifier.padding(start = 4.dp).size(5.dp).background(MixinAppTheme.colors.accent, CircleShape)) + } + Spacer(Modifier.height(5.dp)) + Text(stringResource(if (desktop && loggedIn) R.string.Logined else bot.description), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, lineHeight = 16.sp) + } + } +} + +@Composable +private fun MoreSectionHeader(title: String, count: Int, onClick: () -> Unit) { + Row(Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp, vertical = 20.dp), verticalAlignment = Alignment.CenterVertically) { + Text(title, color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.weight(1f)) + Text(count.toString(), color = MixinAppTheme.colors.textAssist, fontSize = 14.sp) + Spacer(Modifier.width(8.dp)) + MoreArrow() + } +} + +@Composable +private fun MoreArrow() { + Icon(painterResource(R.drawable.ic_arrow_gray_right), null, tint = MixinAppTheme.colors.textRemarks, modifier = Modifier.size(16.dp)) +} + +@Composable +private fun BotRow(app: ExploreApp, external: Boolean = false, onClick: () -> Unit) { + Row(Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp).height(70.dp), verticalAlignment = Alignment.CenterVertically) { + CoilImage(app.iconUrl, R.drawable.ic_avatar_place_holder, Modifier.size(42.dp).clip(CircleShape)) + Spacer(Modifier.width(14.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, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + if (external) Icon(painterResource(R.drawable.ic_arrow_top_right_small), null, tint = MixinAppTheme.colors.textRemarks, modifier = Modifier.size(16.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 }) + MixinAppTheme { + Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) { + ContactPageHeader(stringResource(R.string.bots_title), onBack) + SearchTextField(query, { query = it }, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp), hint = stringResource(R.string.setting_auth_search_hint)) + LazyColumn(Modifier.weight(1f)) { + item { Text(stringResource(R.string.Favorite), color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.padding(16.dp)) } + items(favorites.filter(matches), key = { "favorite:${it.appId}" }) { app -> BotRow(app) { onFavoriteClick(app) } } + item { + Row(Modifier.fillMaxWidth().clickable(onClick = onEdit).padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(painterResource(R.drawable.ic_favorite_edit), null, tint = Color.Unspecified, modifier = Modifier.size(42.dp)) + Spacer(Modifier.width(14.dp)) + Column { + Text(stringResource(R.string.My_favorite_bots), color = MixinAppTheme.colors.textPrimary, fontSize = 16.sp) + Text(stringResource(R.string.add_or_remove_favorite_bots), color = MixinAppTheme.colors.textAssist, fontSize = 14.sp) + } + } + } + groups.forEach { (initial, group) -> + item(key = "header:$initial") { Text(initial, color = MixinAppTheme.colors.textPrimary, modifier = Modifier.padding(16.dp)) } + items(group, key = { it.appId }) { app -> BotRow(app) { onBotClick(app) } } + } + } + } + } +} 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..ec04f37a88 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.Swap, 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_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..af24712ef9 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 @@ -7,6 +7,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.ListPopupWindow +import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager @@ -19,6 +20,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 +28,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 +80,17 @@ 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.setOnClickListener { anchor -> + PopupMenu(requireContext(), anchor).apply { + menu.add(R.string.Search).setOnMenuItemClickListener { + navTo(SearchInscriptionFragment(), SearchInscriptionFragment.TAG) + true + } + show() + } + } root.setOnClickListener { // do nothing } @@ -195,6 +209,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/wallet/WalletHomePrivacyFragment.kt b/app/src/main/java/one/mixin/android/ui/wallet/WalletHomePrivacyFragment.kt index d3d53e8f67..43fb7d1679 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 @@ -70,6 +70,8 @@ import one.mixin.android.ui.conversation.ConversationActivity 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 @@ -119,6 +121,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 @@ -151,6 +154,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() @@ -227,6 +231,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() @@ -457,6 +465,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, ) @@ -468,6 +477,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(), @@ -807,6 +818,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/WalletHomeCard.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/WalletHomeCard.kt index 9bbb602d37..6a7edc994c 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 @@ -30,6 +30,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 @@ -167,6 +168,33 @@ internal fun WalletHomeCard( } } } + WalletHomeCardType.COLLECTIBLES -> SectionCard( + title = stringResource(R.string.Collectibles), + showViewAll = WalletHomeSection.hasMore(state.totalCollectibleCount), + onClick = callbacks::onViewMoreCollectiblesClicked, + contentUsesOwnPadding = true, + ) { + state.collectibles.take(PREVIEW_LIMIT).forEach { collectible -> + Row( + modifier = Modifier.fillMaxWidth() + .clickable { callbacks.onCollectibleClicked(collectible.inscriptionHash) } + .padding(horizontal = 20.dp, vertical = 10.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(8.dp)), + ) + Spacer(Modifier.width(14.dp)) + Column(Modifier.weight(1f)) { + Text(collectible.name, color = MixinAppTheme.colors.textPrimary, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis) + Spacer(Modifier.height(4.dp)) + Text("#${collectible.sequence}", color = MixinAppTheme.colors.textAssist, fontSize = 14.sp) + } + } + } + } WalletHomeCardType.TRANSACTIONS -> SectionCard( title = stringResource(R.string.Transactions), showViewAll = WalletHomeSection.hasMore(state.totalTransactionCount), @@ -349,5 +377,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_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_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_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_buy.xml b/app/src/main/res/drawable/ic_more_buy.xml new file mode 100644 index 0000000000..b3ce631684 --- /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..eca08b425a --- /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_member.xml b/app/src/main/res/drawable/ic_more_member.xml new file mode 100644 index 0000000000..b90a4d1b99 --- /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_swap.xml b/app/src/main/res/drawable/ic_more_swap.xml new file mode 100644 index 0000000000..f59870659d --- /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..01c8d39fc2 --- /dev/null +++ b/app/src/main/res/drawable/ic_new_chat.xml @@ -0,0 +1,13 @@ + + + diff --git a/app/src/main/res/layout/fragment_collectibles.xml b/app/src/main/res/layout/fragment_collectibles.xml index cff070a3d8..accd03cec8 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/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_material_search.xml b/app/src/main/res/layout/view_material_search.xml index 4c40cb1c8d..255c542ba0 100644 --- a/app/src/main/res/layout/view_material_search.xml +++ b/app/src/main/res/layout/view_material_search.xml @@ -49,9 +49,9 @@ android:id="@+id/scan_ib" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_toStartOf="@id/avatar" + android:layout_toStartOf="@id/new_chat" android:layout_centerVertical="true" - android:layout_marginEnd="14dp" + android:layout_marginEnd="10dp" android:background="@drawable/mixin_ripple" android:padding="8dp" app:srcCompat="@drawable/ic_bot_category_scan" @@ -106,16 +106,17 @@ app:srcCompat="@drawable/ic_back" tools:ignore="ContentDescription" /> - + android:contentDescription="@string/new_chat" + app:srcCompat="@drawable/ic_new_chat" /> 总收益 昨日收益 收益率 + 发起聊天 + 联系人 + 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 cbf6cab4ff..8aba542b9d 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -2662,4 +2662,7 @@ 總收益 昨日收益 收益率 + 發起聊天 + 聯絡人 + Mixin ID、姓名、手機號碼 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 48bc8846b8..3cab4cf707 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2693,4 +2693,7 @@ 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 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..5576808c43 --- /dev/null +++ b/app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt @@ -0,0 +1,21 @@ +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 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( From 6f56e621ce1353b3aeb3c936841936850af0e5c6 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 22 Sep 2026 11:25:49 +0800 Subject: [PATCH 2/9] fix(ui): match more and profile to figma --- .../ui/common/profile/ProfileFragment.kt | 196 ++++++++++++++++++ .../mixin/android/ui/contacts/ContactsPage.kt | 141 ++++++++++--- .../NewChatBottomSheetDialogFragment.kt | 2 +- .../mixin/android/ui/home/ExploreFragment.kt | 4 +- .../one/mixin/android/ui/home/MorePage.kt | 80 +++---- .../mixin/android/ui/home/bot/InternalApp.kt | 2 +- .../ui/wallet/home/components/SectionCard.kt | 9 +- .../wallet/home/components/WalletHomeCard.kt | 17 +- app/src/main/res/drawable/ic_add_contact.xml | 5 - .../res/drawable/ic_contacts_add_contact.xml | 6 + .../res/drawable/ic_contacts_new_group.xml | 6 + .../main/res/drawable/ic_contacts_search.xml | 6 + app/src/main/res/drawable/ic_more_arrow.xml | 5 + app/src/main/res/drawable/ic_more_buy.xml | 4 +- .../res/drawable/ic_more_desktop_logged.xml | 8 +- app/src/main/res/drawable/ic_more_link.xml | 5 + app/src/main/res/drawable/ic_more_member.xml | 8 +- app/src/main/res/drawable/ic_more_qr.xml | 10 + app/src/main/res/drawable/ic_more_scan.xml | 5 + app/src/main/res/drawable/ic_more_search.xml | 5 + app/src/main/res/drawable/ic_more_support.xml | 9 + app/src/main/res/drawable/ic_more_swap.xml | 4 +- app/src/main/res/drawable/ic_new_chat.xml | 16 +- app/src/main/res/drawable/ic_new_group.xml | 3 - .../res/drawable/ic_profile_add_contact.xml | 5 + .../main/res/drawable/ic_profile_arrow.xml | 5 + app/src/main/res/drawable/ic_profile_chat.xml | 5 + .../main/res/drawable/ic_profile_contacts.xml | 5 + .../main/res/drawable/ic_profile_details.xml | 7 + .../main/res/drawable/ic_profile_group.xml | 5 + app/src/main/res/drawable/ic_profile_qr.xml | 5 + .../main/res/drawable/ic_profile_receive.xml | 5 + .../main/res/drawable/ic_profile_settings.xml | 6 + app/src/main/res/layout/fragment_explore.xml | 2 +- app/src/main/res/layout/view_home_toolbar.xml | 38 ++-- app/src/main/res/values-v31/styles.xml | 1 + app/src/main/res/values-v35/styles.xml | 1 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/styles.xml | 1 + app/src/main/res/values/styles_night.xml | 1 + app/src/main/res/values/theme.xml | 1 + .../android/ui/contacts/ContactInitialTest.kt | 10 + 42 files changed, 524 insertions(+), 136 deletions(-) create mode 100644 app/src/main/java/one/mixin/android/ui/common/profile/ProfileFragment.kt delete mode 100644 app/src/main/res/drawable/ic_add_contact.xml create mode 100644 app/src/main/res/drawable/ic_contacts_add_contact.xml create mode 100644 app/src/main/res/drawable/ic_contacts_new_group.xml create mode 100644 app/src/main/res/drawable/ic_contacts_search.xml create mode 100644 app/src/main/res/drawable/ic_more_arrow.xml create mode 100644 app/src/main/res/drawable/ic_more_link.xml create mode 100644 app/src/main/res/drawable/ic_more_qr.xml create mode 100644 app/src/main/res/drawable/ic_more_scan.xml create mode 100644 app/src/main/res/drawable/ic_more_search.xml create mode 100644 app/src/main/res/drawable/ic_more_support.xml delete mode 100644 app/src/main/res/drawable/ic_new_group.xml create mode 100644 app/src/main/res/drawable/ic_profile_add_contact.xml create mode 100644 app/src/main/res/drawable/ic_profile_arrow.xml create mode 100644 app/src/main/res/drawable/ic_profile_chat.xml create mode 100644 app/src/main/res/drawable/ic_profile_contacts.xml create mode 100644 app/src/main/res/drawable/ic_profile_details.xml create mode 100644 app/src/main/res/drawable/ic_profile_group.xml create mode 100644 app/src/main/res/drawable/ic_profile_qr.xml create mode 100644 app/src/main/res/drawable/ic_profile_receive.xml create mode 100644 app/src/main/res/drawable/ic_profile_settings.xml 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/ContactsPage.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt index 1d6e3234b2..b6d7c8afc1 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt @@ -3,7 +3,11 @@ 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.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -13,6 +17,8 @@ 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.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -28,9 +34,17 @@ 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.sp import androidx.compose.ui.viewinterop.AndroidView @@ -39,11 +53,11 @@ 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.ui.address.component.SearchTextField 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 @@ -68,47 +82,71 @@ internal fun ContactsPage( } val listState = rememberLazyListState() val scope = rememberCoroutineScope() + val sectionOffsets = remember(groups, newChat) { + var offset = if (newChat) 2 else 0 + groups.mapValues { (_, users) -> offset.also { offset += users.size + 1 } } + } + val scrollToLetter: (String) -> Unit = { letter -> + sectionOffsets[letter]?.let { offset -> scope.launch { listState.scrollToItem(offset) } } + } 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) - SearchTextField(query, { query = it }, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp), hint = stringResource(R.string.contacts_search_hint)) - Box(Modifier.weight(1f)) { + ContactPageHeader(stringResource(if (newChat) R.string.new_chat else R.string.My_Contacts), onBack, newChat) + 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_new_group, R.string.New_Group, onNewGroup) } - item { ContactAction(R.drawable.ic_add_contact, R.string.Add_Contact, onAddContact) } + 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") { - Text(initial, color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.padding(20.dp)) + ContactSectionHeader(initial) } items(users, key = { it.userId }) { user -> - Row(Modifier.fillMaxWidth().clickable { onContact(user) }.padding(start = 20.dp, end = 40.dp).height(70.dp), verticalAlignment = Alignment.CenterVertically) { + Row(Modifier.fillMaxWidth().clickable { onContact(user) }.height(70.dp).padding(start = 20.dp, end = 40.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically) { UserAvatarImage(user, 50.dp) - Spacer(Modifier.width(20.dp)) - AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user) }, modifier = Modifier.weight(1f)) + Spacer(Modifier.width(16.dp)) + AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user); it.setTextColor(nameColor) }, modifier = Modifier.weight(1f)) } } } } if (groups.isNotEmpty()) { - Column(Modifier.align(Alignment.CenterEnd).padding(end = 8.dp)) { - (('A'..'Z').map { it.toString() } + "#").forEach { initial -> - Text(initial, color = MixinAppTheme.colors.textAssist, fontSize = 10.sp, lineHeight = 14.sp, - modifier = Modifier.clickable { - var offset = if (newChat) 2 else 0 - for ((label, users) in groups) { - if (label == initial) { - val targetIndex = offset - scope.launch { listState.scrollToItem(targetIndex) } - break + val railTop = minOf(if (newChat) 200.dp else 60.dp, (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) } - offset += users.size + 1 } - }.padding(horizontal = 8.dp)) + 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 } + }) } } } @@ -117,24 +155,65 @@ internal fun ContactsPage( } } +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) { - Row(Modifier.fillMaxWidth().height(56.dp), verticalAlignment = Alignment.CenterVertically) { + 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 = MixinAppTheme.colors.textPrimary, fontSize = 18.sp, fontWeight = FontWeight.W600, - modifier = Modifier.weight(1f).padding(start = if (close) 16.dp else 0.dp), textAlign = if (close) androidx.compose.ui.text.style.TextAlign.Start else androidx.compose.ui.text.style.TextAlign.Center) + 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.W500, + 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 = androidx.compose.ui.graphics.Color.Unspecified, modifier = Modifier.size(26.dp)) } + IconButton(onClick = onBack) { Icon(painterResource(R.drawable.ic_circle_close), stringResource(R.string.Close), tint = Color.Unspecified, modifier = Modifier.size(26.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(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(painterResource(icon), null, tint = androidx.compose.ui.graphics.Color.Unspecified, modifier = Modifier.size(50.dp)) - Spacer(Modifier.width(20.dp)) - Text(stringResource(title), color = MixinAppTheme.colors.accent, fontSize = 16.sp, modifier = Modifier.weight(1f)) - Icon(painterResource(R.drawable.ic_arrow_gray_right), null, tint = MixinAppTheme.colors.iconGray) + 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 index 9d8b25c8dd..1488e8b75d 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt @@ -44,7 +44,7 @@ class NewChatBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragment() ) } - override fun getBottomSheetHeight(view: View) = requireContext().screenHeight() - view.getSafeAreaInsetsTop() - 56.dp + 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/ExploreFragment.kt b/app/src/main/java/one/mixin/android/ui/home/ExploreFragment.kt index d9cf92fb70..000bedd9aa 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 @@ -37,7 +37,7 @@ 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.ProfileBottomSheetDialogFragment +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 @@ -121,7 +121,7 @@ class ExploreFragment : BaseFragment() { botCount = (favoriteApps + apps).distinctBy { it.appId }.size, isDesktopLogin = isDesktopLogin, clickedBotIds = clickedBotIds, - onProfile = { ProfileBottomSheetDialogFragment.newInstance().showNow(parentFragmentManager, ProfileBottomSheetDialogFragment.TAG) }, + onProfile = { navTo(ProfileFragment(), ProfileFragment.TAG) }, onQr = { Session.getAccountId()?.let { QrBottomSheetDialogFragment.newInstance(it, QrBottomSheetDialogFragment.TYPE_MY_QR) 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 index 9366418e98..ffb6ef77f4 100644 --- a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -32,9 +32,11 @@ 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 @@ -42,8 +44,9 @@ 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.ui.address.component.SearchTextField 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 @@ -70,9 +73,10 @@ internal fun MorePage( onBot: (BotInterface) -> Unit, ) { MixinAppTheme { + val profileNameColor = MixinAppTheme.colors.textMinor.toArgb() LazyColumn( modifier = Modifier.fillMaxSize().background(MixinAppTheme.colors.background), - contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), + contentPadding = androidx.compose.foundation.layout.PaddingValues(start = 16.dp, end = 16.dp, top = 10.dp, bottom = 16.dp), ) { user?.let { item { @@ -81,16 +85,18 @@ internal fun MorePage( verticalAlignment = Alignment.CenterVertically, ) { UserAvatarImage(it, 42.dp) - Spacer(Modifier.width(8.dp)) + Spacer(Modifier.width(9.dp)) Column(Modifier.weight(1f)) { - AndroidView(factory = { context -> NameTextView(context) }, update = { view -> view.setName(it) }) + 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) + Text(stringResource(R.string.contact_mixin_id, it.identityNumber), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, lineHeight = 14.sp, letterSpacing = 0.sp) } - IconButton(onClick = onQr) { - Icon(painterResource(R.drawable.ic_qr_code), stringResource(R.string.My_QR_Code), tint = MixinAppTheme.colors.textRemarks, modifier = Modifier.size(20.dp)) + 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)) + } } - MoreArrow() } Spacer(Modifier.height(12.dp)) } @@ -115,13 +121,12 @@ internal fun MorePage( 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) + 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)) } - Text(stringResource(R.string.view_all), color = MixinAppTheme.colors.accent, fontSize = 14.sp, - modifier = Modifier.align(Alignment.CenterHorizontally).clickable(onClick = onContacts).padding(top = 18.dp, bottom = 20.dp, start = 16.dp, end = 16.dp)) + Spacer(Modifier.height(20.dp)) } } } @@ -130,8 +135,7 @@ internal fun MorePage( Spacer(Modifier.height(12.dp)) Column(Modifier.moreCard()) { MoreSectionHeader(stringResource(R.string.bots_title), botCount, onBots) - favorites.forEach { app -> BotRow(app, true) { onBot(app) } } - Spacer(Modifier.height(12.dp)) + favorites.forEach { app -> BotRow(app, true, 42.dp) { onBot(app) } } } } } @@ -147,46 +151,45 @@ private fun Modifier.moreCard() = fillMaxWidth().clip(RoundedCornerShape(8.dp)) 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(12.dp)) { - Icon(painterResource(icon), null, tint = if (desktop && loggedIn) MixinAppTheme.colors.walletGreen else MixinAppTheme.colors.textPrimary, modifier = Modifier.size(20.dp)) - Spacer(Modifier.width(12.dp)) + Row(modifier.moreCard().clickable(onClick = onClick).heightIn(min = 77.dp).padding(horizontal = 10.dp, vertical = 12.dp)) { + 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, modifier = Modifier.weight(1f)) + Text(stringResource(bot.name), color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp, modifier = Modifier.weight(1f)) if (showDot) Box(Modifier.padding(start = 4.dp).size(5.dp).background(MixinAppTheme.colors.accent, CircleShape)) } - Spacer(Modifier.height(5.dp)) - Text(stringResource(if (desktop && loggedIn) R.string.Logined else bot.description), color = MixinAppTheme.colors.textAssist, fontSize = 12.sp, lineHeight = 16.sp) + 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).padding(horizontal = 16.dp, vertical = 20.dp), verticalAlignment = Alignment.CenterVertically) { - Text(title, color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.weight(1f)) - Text(count.toString(), color = MixinAppTheme.colors.textAssist, fontSize = 14.sp) - Spacer(Modifier.width(8.dp)) + 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_arrow_gray_right), null, tint = MixinAppTheme.colors.textRemarks, modifier = Modifier.size(16.dp)) + 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, onClick: () -> Unit) { - Row(Modifier.fillMaxWidth().clickable(onClick = onClick).padding(horizontal = 16.dp).height(70.dp), verticalAlignment = Alignment.CenterVertically) { - CoilImage(app.iconUrl, R.drawable.ic_avatar_place_holder, Modifier.size(42.dp).clip(CircleShape)) - Spacer(Modifier.width(14.dp)) +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 20.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, maxLines = 1, overflow = TextOverflow.Ellipsis) + 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_arrow_top_right_small), null, tint = MixinAppTheme.colors.textRemarks, modifier = Modifier.size(16.dp)) + if (external) Icon(painterResource(R.drawable.ic_more_link), null, tint = Color.Unspecified, modifier = Modifier.size(24.dp)) } } @@ -199,22 +202,23 @@ internal fun BotsPage(favorites: List, apps: List, onBac MixinAppTheme { Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) { ContactPageHeader(stringResource(R.string.bots_title), onBack) - SearchTextField(query, { query = it }, Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp), hint = stringResource(R.string.setting_auth_search_hint)) + ContactsSearchField(query, { query = it }, stringResource(R.string.setting_auth_search_hint)) LazyColumn(Modifier.weight(1f)) { - item { Text(stringResource(R.string.Favorite), color = MixinAppTheme.colors.textPrimary, fontSize = 14.sp, modifier = Modifier.padding(16.dp)) } + item { ContactSectionHeader(stringResource(R.string.Favorite)) } items(favorites.filter(matches), key = { "favorite:${it.appId}" }) { app -> BotRow(app) { onFavoriteClick(app) } } item { - Row(Modifier.fillMaxWidth().clickable(onClick = onEdit).padding(16.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(painterResource(R.drawable.ic_favorite_edit), null, tint = Color.Unspecified, modifier = Modifier.size(42.dp)) - Spacer(Modifier.width(14.dp)) + 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) - Text(stringResource(R.string.add_or_remove_favorite_bots), color = MixinAppTheme.colors.textAssist, fontSize = 14.sp) + 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") { Text(initial, color = MixinAppTheme.colors.textPrimary, modifier = Modifier.padding(16.dp)) } + item(key = "header:$initial") { ContactSectionHeader(initial) } items(group, key = { it.appId }) { app -> BotRow(app) { onBotClick(app) } } } } 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 ec04f37a88..b46ffe2196 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 @@ -29,6 +29,6 @@ val InternalMember = Bot(INTERNAL_MEMBER_ID, R.string.Mixin_One, R.string.mixin_ 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_support) +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/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 6a7edc994c..08ebc953b8 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 @@ -76,7 +77,7 @@ internal fun WalletHomeCard( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 20.dp) + .padding(horizontal = if (card == WalletHomeCardType.COLLECTIBLES) 16.dp else 20.dp) .clip(RoundedCornerShape(8.dp)) .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor) .then( @@ -173,24 +174,30 @@ internal fun WalletHomeCard( showViewAll = WalletHomeSection.hasMore(state.totalCollectibleCount), onClick = callbacks::onViewMoreCollectiblesClicked, contentUsesOwnPadding = true, + contentFlush = true, + showBottomSpacer = false, + headerPadding = PaddingValues(start = 16.dp, end = 8.dp, top = 13.dp, bottom = 13.dp), + headerTrailing = { + Icon(painterResource(R.drawable.ic_more_arrow), null, tint = Color.Unspecified, modifier = Modifier.size(30.dp)) + }, ) { state.collectibles.take(PREVIEW_LIMIT).forEach { collectible -> Row( modifier = Modifier.fillMaxWidth() .clickable { callbacks.onCollectibleClicked(collectible.inscriptionHash) } - .padding(horizontal = 20.dp, vertical = 10.dp), + .height(70.dp).padding(start = 16.dp, end = 16.dp, bottom = 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(8.dp)), + 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, maxLines = 1, overflow = TextOverflow.Ellipsis) + 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) + Text("#${collectible.sequence}", color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp) } } } 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_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_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_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 index b3ce631684..f3c173eafa 100644 --- a/app/src/main/res/drawable/ic_more_buy.xml +++ b/app/src/main/res/drawable/ic_more_buy.xml @@ -1,5 +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 index eca08b425a..758d58191c 100644 --- a/app/src/main/res/drawable/ic_more_desktop_logged.xml +++ b/app/src/main/res/drawable/ic_more_desktop_logged.xml @@ -1,7 +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 index b90a4d1b99..96d8cdf1d0 100644 --- a/app/src/main/res/drawable/ic_more_member.xml +++ b/app/src/main/res/drawable/ic_more_member.xml @@ -1,7 +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_scan.xml b/app/src/main/res/drawable/ic_more_scan.xml new file mode 100644 index 0000000000..d0869eac00 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_scan.xml @@ -0,0 +1,5 @@ + + + + + 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 index f59870659d..056c0c3884 100644 --- a/app/src/main/res/drawable/ic_more_swap.xml +++ b/app/src/main/res/drawable/ic_more_swap.xml @@ -1,5 +1,5 @@ - - + + diff --git a/app/src/main/res/drawable/ic_new_chat.xml b/app/src/main/res/drawable/ic_new_chat.xml index 01c8d39fc2..ca069c3fcd 100644 --- a/app/src/main/res/drawable/ic_new_chat.xml +++ b/app/src/main/res/drawable/ic_new_chat.xml @@ -1,13 +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_explore.xml b/app/src/main/res/layout/fragment_explore.xml index 479612a4cf..0a0dcfcd8a 100644 --- a/app/src/main/res/layout/fragment_explore.xml +++ b/app/src/main/res/layout/fragment_explore.xml @@ -8,7 +8,7 @@ + android:padding="12dp" + app:srcCompat="@drawable/ic_more_search" + android:contentDescription="@string/Search" /> + android:padding="12dp" + app:srcCompat="@drawable/ic_more_scan" + 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/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml index 7d91b4ebe2..64f58d2ade 100644 --- a/app/src/main/res/values-v31/styles.xml +++ b/app/src/main/res/values-v31/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-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/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/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 index 5576808c43..3bef588e9d 100644 --- a/app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt +++ b/app/src/test/java/one/mixin/android/ui/contacts/ContactInitialTest.kt @@ -10,6 +10,16 @@ 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 ")) From 1d2652147f1eb6af53d1c31222fb77fe13710117 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 22 Sep 2026 11:38:15 +0800 Subject: [PATCH 3/9] fix(ui): restore referral icon animation --- .../one/mixin/android/ui/home/MorePage.kt | 29 ++++++++++++++++++- app/src/main/res/raw/referral.json | 1 + 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/raw/referral.json 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 index ffb6ef77f4..065bbffc29 100644 --- a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -44,18 +44,22 @@ 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.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( @@ -152,7 +156,30 @@ private fun MoreActionCard(bot: Bot, loggedIn: Boolean, showDot: Boolean, modifi 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)) { - Icon(painterResource(icon), null, tint = Color.Unspecified, modifier = Modifier.size(24.dp)) + if (bot.id == INTERNAL_REFERRAL_ID) { + AndroidView( + factory = { context -> + RLottieImageView(context).apply { + val size = context.dpToPx(24f) + setAnimation( + RLottieDrawable(R.raw.referral, "referral", size, size).apply { + setAutoRepeat(1) + setAutoRepeatCount(Int.MAX_VALUE) + }, + ) + 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) { diff --git a/app/src/main/res/raw/referral.json b/app/src/main/res/raw/referral.json new file mode 100644 index 0000000000..fbfaee2515 --- /dev/null +++ b/app/src/main/res/raw/referral.json @@ -0,0 +1 @@ +{"v":"5.12.2","fr":30,"ip":0,"op":40,"w":84,"h":108,"nm":"gift","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"gift","sr":0.33333333333333,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":23,"s":[8]},{"t":40,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[42.5,55.973,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":18,"s":[42.5,55.319,0],"to":[0,0,0],"ti":[0,0,0]},{"t":40,"s":[42.5,55.973,0]}],"ix":2,"l":2},"a":{"a":0,"k":[30.5,30.5,0],"ix":1,"l":2},"s":{"a":0,"k":[150,150,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[39.547,8.861],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":2,"ix":4},"nm":"矩形路径 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[30.273,24.049],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"矩形 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[0,0],[-0.987,0.922],[0,1.303],[0.986,0.921],[1.396,0]],"o":[[0,0],[1.396,0],[0.986,-0.921],[0,-1.302],[-0.987,-0.921],[-7.367,0]],"v":[[-7.368,5.849],[1.702,5.849],[5.424,4.41],[6.965,0.937],[5.424,-2.536],[1.702,-3.974]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0],[-0.987,0.922],[0,1.303],[0.986,0.921],[1.396,0]],"o":[[0,0],[1.396,0],[0.986,-0.921],[0,-1.302],[-0.987,-0.921],[-7.367,0]],"v":[[-7.368,4.911],[2.105,4.911],[6.165,1.971],[7.706,-1.502],[6.165,-4.975],[2.443,-6.413]],"c":true}]},{"t":29,"s":[{"i":[[0,0],[0,0],[-0.987,0.922],[0,1.303],[0.986,0.921],[1.396,0]],"o":[[0,0],[1.396,0],[0.986,-0.921],[0,-1.302],[-0.987,-0.921],[-7.367,0]],"v":[[-7.368,5.849],[1.702,5.849],[5.424,4.41],[6.965,0.937],[5.424,-2.536],[1.702,-3.974]],"c":true}]}],"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[37.532,13.702],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 1","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[{"i":[[0,0],[0,0],[0.986,0.922],[0,1.303],[-0.987,0.921],[-1.396,0]],"o":[[0,0],[-1.396,0],[-0.987,-0.921],[0,-1.302],[0.986,-0.921],[7.367,0]],"v":[[7.368,5.849],[-1.714,5.849],[-5.435,4.41],[-6.977,0.937],[-5.435,-2.536],[-1.714,-3.974]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[0,0],[0,0],[0.986,0.922],[0,1.303],[-0.987,0.921],[-1.396,0]],"o":[[0,0],[-1.396,0],[-0.987,-0.921],[0,-1.302],[0.986,-0.921],[7.367,0]],"v":[[7.367,4.911],[0.688,4.791],[-5.673,1.852],[-7.215,-1.621],[-5.673,-5.094],[-1.952,-6.532]],"c":true}]},{"t":29,"s":[{"i":[[0,0],[0,0],[0.986,0.922],[0,1.303],[-0.987,0.921],[-1.396,0]],"o":[[0,0],[-1.396,0],[-0.987,-0.921],[0,-1.302],[0.986,-0.921],[7.367,0]],"v":[[7.368,5.849],[-1.714,5.849],[-5.435,4.41],[-6.977,0.937],[-5.435,-2.536],[-1.714,-3.974]],"c":true}]}],"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[22.797,13.702],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[30.165,23.525],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 3","np":1,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[30.165,29.081],[30.165,20.387]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[99.738,97.102],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 4","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6,"s":[30.165,18.614],"to":[0,0],"ti":[0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[30.165,12.614],"to":[0,0],"ti":[0,0]},{"t":29.6666666666667,"s":[30.165,18.614]}],"ix":2},"a":{"a":0,"k":[30.165,18.614],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":6,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":20,"s":[6]},{"t":29.6666666666667,"s":[0]}],"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 7","np":5,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[30.165,48.263],[30.165,29.503]],"c":false},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 5","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.739,-0.77],[0,0],[0,0],[0.668,0.742],[0,0],[0,0],[0,0],[0,0]],"o":[[-0.709,0.739],[0,0],[0,0],[-0.742,-0.825],[0,0],[0,0],[0,0],[0,0]],"v":[[14.972,8.333],[12.711,9.145],[-12.79,9.145],[-15.159,8.27],[-15.784,5.817],[-15.784,-11.051],[15.784,-11.051],[15.784,5.942]],"c":true},"ix":2},"nm":"路径 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.666666666667,0.443137254902,0.980392156863,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[30.23,39.497],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[93.709,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"组 6","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":41,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file From 3f138c72bdd9751f02b333568968e95804e32395 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Tue, 22 Sep 2026 13:42:16 +0800 Subject: [PATCH 4/9] fix(ui): match referral animation icon size --- .../main/java/one/mixin/android/ui/home/MorePage.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 index 065bbffc29..aaa6dc3d42 100644 --- a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -1,5 +1,7 @@ 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 @@ -160,13 +162,19 @@ private fun MoreActionCard(bot: Bot, loggedIn: Boolean, showDot: Boolean, modifi AndroidView( factory = { context -> RLottieImageView(context).apply { - val size = context.dpToPx(24f) + 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() } }, From 19706f412778846aeb092091eb87c14b5c8f4f6a Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Wed, 30 Sep 2026 16:04:57 +0800 Subject: [PATCH 5/9] fix(contacts): align list rows and add contact action --- .../android/ui/contacts/ContactListFragment.kt | 8 +++++++- .../mixin/android/ui/contacts/ContactsPage.kt | 16 ++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) 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 index ae51527e08..c9a2240c3b 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactListFragment.kt @@ -9,6 +9,7 @@ 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 @@ -26,7 +27,12 @@ class ContactListFragment : BaseFragment() { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { val users by contacts.observeAsState(emptyList()) - ContactsPage(users, { requireActivity().onBackPressedDispatcher.onBackPressed() }, { showUserBottom(parentFragmentManager, it) }) + 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/ContactsPage.kt b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt index b6d7c8afc1..d6f39b3dae 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt @@ -92,7 +92,7 @@ internal fun ContactsPage( 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.My_Contacts), onBack, newChat) + ContactPageHeader(stringResource(if (newChat) R.string.new_chat else R.string.My_Contacts), onBack, newChat, onAddContact) ContactsSearchField(query, { query = it }, stringResource(R.string.contacts_search_hint), newChat) BoxWithConstraints(Modifier.weight(1f)) { LazyColumn(state = listState, modifier = Modifier.fillMaxSize()) { @@ -109,9 +109,15 @@ internal fun ContactsPage( } 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, 50.dp) + UserAvatarImage(user, if (newChat) 50.dp else 42.dp) Spacer(Modifier.width(16.dp)) - AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user); it.setTextColor(nameColor) }, modifier = Modifier.weight(1f)) + Column(Modifier.weight(1f)) { + AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user); it.setTextColor(nameColor) }) + if (!newChat) { + Spacer(Modifier.height(4.dp)) + Text(user.identityNumber, color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp) + } + } } } } @@ -159,13 +165,15 @@ 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) { +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.W500, 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)) } } From f60464c76a793704b8870b63ddd232b3430f6410 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 8 Oct 2026 17:06:04 +0800 Subject: [PATCH 6/9] fix(ui): address more redesign feedback --- .../mixin/android/ui/contacts/ContactsPage.kt | 87 ++++++++++--------- .../one/mixin/android/ui/home/MorePage.kt | 42 +++++---- .../mixin/android/ui/home/bot/InternalApp.kt | 2 +- .../android/ui/setting/LogAndDebugFragment.kt | 9 ++ .../wallet/home/components/WalletHomeCard.kt | 9 +- app/src/main/res/layout/fragment_explore.xml | 2 +- .../main/res/layout/fragment_log_debug.xml | 12 +++ app/src/main/res/layout/fragment_market.xml | 24 ++--- app/src/main/res/layout/fragment_wallet.xml | 24 ++--- app/src/main/res/layout/view_home_toolbar.xml | 4 +- .../main/res/layout/view_material_search.xml | 16 ++-- app/src/main/res/values-zh-rCN/strings.xml | 1 + app/src/main/res/values-zh-rTW/strings.xml | 1 + app/src/main/res/values/strings.xml | 1 + 14 files changed, 135 insertions(+), 99 deletions(-) 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 index d6f39b3dae..eb3441e7d5 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt @@ -8,6 +8,7 @@ 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 @@ -20,6 +21,7 @@ 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 @@ -45,6 +47,7 @@ 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 @@ -81,14 +84,10 @@ internal fun ContactsPage( .toSortedMap(compareBy { if (it == "#") "[" else it }) } val listState = rememberLazyListState() - val scope = rememberCoroutineScope() val sectionOffsets = remember(groups, newChat) { var offset = if (newChat) 2 else 0 groups.mapValues { (_, users) -> offset.also { offset += users.size + 1 } } } - val scrollToLetter: (String) -> Unit = { letter -> - sectionOffsets[letter]?.let { offset -> scope.launch { listState.scrollToItem(offset) } } - } MixinAppTheme { val nameColor = MixinAppTheme.colors.textMinor.toArgb() Column(Modifier.fillMaxSize().background(MixinAppTheme.colors.background)) { @@ -113,50 +112,56 @@ internal fun ContactsPage( Spacer(Modifier.width(16.dp)) Column(Modifier.weight(1f)) { AndroidView(factory = { NameTextView(it).apply { textView.textSize = 16f } }, update = { it.setName(user); it.setTextColor(nameColor) }) - if (!newChat) { - Spacer(Modifier.height(4.dp)) - Text(user.identityNumber, color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp) - } + Spacer(Modifier.height(4.dp)) + Text(user.identityNumber, color = MixinAppTheme.colors.textAssist, fontSize = 14.sp, lineHeight = 17.sp, letterSpacing = 0.sp) } } } } } - if (groups.isNotEmpty()) { - val railTop = minOf(if (newChat) 200.dp else 60.dp, (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 } - }) + ContactAlphabetIndex(sectionOffsets, listState, if (newChat) 200.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 } + }) } } } @@ -168,7 +173,7 @@ internal fun contactLetterAt(y: Float, height: Int): String = 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.W500, + 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)) } 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 index aaa6dc3d42..567ad2f91c 100644 --- a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -6,6 +6,7 @@ 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 @@ -20,6 +21,7 @@ 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 @@ -47,6 +49,7 @@ 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 @@ -216,7 +219,7 @@ private fun MoreArrow() { @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 20.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically) { + 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)) { @@ -234,28 +237,35 @@ internal fun BotsPage(favorites: List, apps: List, onBac 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)) - LazyColumn(Modifier.weight(1f)) { - item { ContactSectionHeader(stringResource(R.string.Favorite)) } - items(favorites.filter(matches), 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) + 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) } } + } } - 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 b46ffe2196..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 @@ -24,7 +24,7 @@ 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_more_buy) -val InternalSwap = Bot(INTERNAL_SWAP_ID, R.string.Swap, R.string.trade_native_tokens, R.drawable.ic_more_swap) +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_referral) 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/home/components/WalletHomeCard.kt b/app/src/main/java/one/mixin/android/ui/wallet/home/components/WalletHomeCard.kt index ecb3261525..ea56db3606 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 @@ -79,7 +79,7 @@ internal fun WalletHomeCard( Column( modifier = Modifier .fillMaxWidth() - .padding(horizontal = if (card == WalletHomeCardType.COLLECTIBLES) 16.dp else 20.dp) + .padding(horizontal = 20.dp) .clip(RoundedCornerShape(8.dp)) .cardBackground(MixinAppTheme.colors.background, MixinAppTheme.colors.borderColor) .then( @@ -178,16 +178,13 @@ internal fun WalletHomeCard( contentUsesOwnPadding = true, contentFlush = true, showBottomSpacer = false, - headerPadding = PaddingValues(start = 16.dp, end = 8.dp, top = 13.dp, bottom = 13.dp), - headerTrailing = { - Icon(painterResource(R.drawable.ic_more_arrow), null, tint = Color.Unspecified, modifier = Modifier.size(30.dp)) - }, + headerPadding = PaddingValues(20.dp), ) { state.collectibles.take(PREVIEW_LIMIT).forEach { collectible -> Row( modifier = Modifier.fillMaxWidth() .clickable { callbacks.onCollectibleClicked(collectible.inscriptionHash) } - .height(70.dp).padding(start = 16.dp, end = 16.dp, bottom = 20.dp), + .height(70.dp).padding(start = 20.dp, end = 20.dp, bottom = 20.dp), verticalAlignment = Alignment.CenterVertically, ) { CoilImage( diff --git a/app/src/main/res/layout/fragment_explore.xml b/app/src/main/res/layout/fragment_explore.xml index 0a0dcfcd8a..479612a4cf 100644 --- a/app/src/main/res/layout/fragment_explore.xml +++ b/app/src/main/res/layout/fragment_explore.xml @@ -8,7 +8,7 @@ + + 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/view_home_toolbar.xml b/app/src/main/res/layout/view_home_toolbar.xml index c56e5b835f..db66fbbd9c 100644 --- a/app/src/main/res/layout/view_home_toolbar.xml +++ b/app/src/main/res/layout/view_home_toolbar.xml @@ -12,7 +12,7 @@ android:layout_marginStart="16dp" android:textColor="?attr/text_primary" android:textSize="18sp" - android:fontFamily="sans-serif-medium" + android:textStyle="bold" tools:ignore="RelativeOverlap" tools:text="@string/More" /> @@ -46,7 +46,7 @@ android:layout_height="48dp" android:layout_alignParentEnd="true" android:layout_centerVertical="true" - android:layout_marginEnd="4dp" + android:layout_marginEnd="8dp" android:background="@drawable/mixin_ripple" android:padding="12dp" app:srcCompat="@drawable/ic_profile_settings" diff --git a/app/src/main/res/layout/view_material_search.xml b/app/src/main/res/layout/view_material_search.xml index 255c542ba0..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 @@ diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fa7a3706aa..396065c675 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 交易数据吗?此操作不可恢复。删除后系统将重新同步数据。 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index bf66cd673e..782f75c50e 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 現金餘額 理財餘額 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f4c332800..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. From 3581a6d959a0f1cc98f1ba31fe62b2d1418c9e52 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 8 Oct 2026 18:19:43 +0800 Subject: [PATCH 7/9] fix(ui): apply dark theme to new chat and trade icon --- .../android/ui/contacts/NewChatBottomSheetDialogFragment.kt | 3 +++ app/src/main/res/drawable/ic_more_swap.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 index 1488e8b75d..3188bab4f4 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/NewChatBottomSheetDialogFragment.kt @@ -6,6 +6,7 @@ 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 @@ -20,6 +21,8 @@ class NewChatBottomSheetDialogFragment : MixinComposeBottomSheetDialogFragment() const val TAG = "NewChatBottomSheetDialogFragment" } + override fun getTheme() = R.style.AppTheme_Dialog + private val viewModel by viewModels() private val contacts by lazy { viewModel.findContacts() } diff --git a/app/src/main/res/drawable/ic_more_swap.xml b/app/src/main/res/drawable/ic_more_swap.xml index 056c0c3884..aad369e902 100644 --- a/app/src/main/res/drawable/ic_more_swap.xml +++ b/app/src/main/res/drawable/ic_more_swap.xml @@ -1,5 +1,5 @@ - + From 8f1c18ce40f1d7aa680f65611b3f97c1c2e22742 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Thu, 8 Oct 2026 18:29:32 +0800 Subject: [PATCH 8/9] fix(ui): align more badges and collectible ripples --- app/src/main/java/one/mixin/android/ui/home/MorePage.kt | 5 +++-- .../android/ui/wallet/home/components/WalletHomeCard.kt | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) 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 index 567ad2f91c..c75d3b5b5f 100644 --- a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -37,6 +37,7 @@ 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.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -194,8 +195,8 @@ private fun MoreActionCard(bot: Bot, loggedIn: Boolean, showDot: Boolean, modifi 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)) - if (showDot) Box(Modifier.padding(start = 4.dp).size(5.dp).background(MixinAppTheme.colors.accent, CircleShape)) + 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 = 4.dp, top = 2.dp).size(5.dp).background(colorResource(R.color.colorRed), 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) 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 ea56db3606..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 @@ -183,8 +183,9 @@ internal fun WalletHomeCard( state.collectibles.take(PREVIEW_LIMIT).forEach { collectible -> Row( modifier = Modifier.fillMaxWidth() + .height(70.dp).padding(bottom = 20.dp) .clickable { callbacks.onCollectibleClicked(collectible.inscriptionHash) } - .height(70.dp).padding(start = 20.dp, end = 20.dp, bottom = 20.dp), + .padding(horizontal = 20.dp), verticalAlignment = Alignment.CenterVertically, ) { CoilImage( From f4d46afbe3654b27c464c97a490b41e1ad79bd71 Mon Sep 17 00:00:00 2001 From: SeniorZhai Date: Fri, 9 Oct 2026 17:04:21 +0800 Subject: [PATCH 9/9] fix(more): align badges and navigation icons --- .../one/mixin/android/ui/contacts/ContactsPage.kt | 4 ++-- .../main/java/one/mixin/android/ui/home/MorePage.kt | 6 ++++-- .../ui/home/inscription/CollectiblesFragment.kt | 12 +++--------- app/src/main/res/drawable/ic_more_scan.xml | 5 ----- app/src/main/res/layout/fragment_collectibles.xml | 2 +- app/src/main/res/layout/view_home_toolbar.xml | 4 ++-- 6 files changed, 12 insertions(+), 21 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_more_scan.xml 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 index eb3441e7d5..9b53559bf0 100644 --- a/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt +++ b/app/src/main/java/one/mixin/android/ui/contacts/ContactsPage.kt @@ -91,7 +91,7 @@ internal fun ContactsPage( 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.My_Contacts), onBack, newChat, onAddContact) + 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()) { @@ -119,7 +119,7 @@ internal fun ContactsPage( } } } - ContactAlphabetIndex(sectionOffsets, listState, if (newChat) 200.dp else 60.dp) + ContactAlphabetIndex(sectionOffsets, listState, if (newChat) 140.dp else 60.dp) } } } 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 index c75d3b5b5f..5f15ee841e 100644 --- a/app/src/main/java/one/mixin/android/ui/home/MorePage.kt +++ b/app/src/main/java/one/mixin/android/ui/home/MorePage.kt @@ -37,7 +37,6 @@ 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.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow @@ -196,7 +195,10 @@ private fun MoreActionCard(bot: Bot, loggedIn: Boolean, showDot: Boolean, modifi 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 = 4.dp, top = 2.dp).size(5.dp).background(colorResource(R.color.colorRed), CircleShape)) + 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) 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 af24712ef9..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 @@ -7,7 +7,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.ListPopupWindow -import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.recyclerview.widget.GridLayoutManager @@ -82,14 +81,9 @@ class CollectiblesFragment : BaseFragment() { 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.setOnClickListener { anchor -> - PopupMenu(requireContext(), anchor).apply { - menu.add(R.string.Search).setOnMenuItemClickListener { - navTo(SearchInscriptionFragment(), SearchInscriptionFragment.TAG) - true - } - show() - } + titleView.rightIb.contentDescription = getString(R.string.Search) + titleView.rightIb.setOnClickListener { + navTo(SearchInscriptionFragment(), SearchInscriptionFragment.TAG) } root.setOnClickListener { // do nothing diff --git a/app/src/main/res/drawable/ic_more_scan.xml b/app/src/main/res/drawable/ic_more_scan.xml deleted file mode 100644 index d0869eac00..0000000000 --- a/app/src/main/res/drawable/ic_more_scan.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/app/src/main/res/layout/fragment_collectibles.xml b/app/src/main/res/layout/fragment_collectibles.xml index accd03cec8..1b4c9706c8 100644 --- a/app/src/main/res/layout/fragment_collectibles.xml +++ b/app/src/main/res/layout/fragment_collectibles.xml @@ -14,7 +14,7 @@ android:layout_height="wrap_content" app:need_divider="false" app:titleText="@string/Collectibles" - app:rightIcon="@drawable/ic_more" /> + app:rightIcon="@drawable/ic_search_home" />