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