Signal-Android/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragmen...

876 wiersze
35 KiB
Kotlin

package org.thoughtcrime.securesms.components.settings.conversation
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.Rect
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.core.view.doOnPreDraw
import androidx.fragment.app.viewModels
import androidx.navigation.Navigation
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import app.cash.exhaustive.Exhaustive
import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.AvatarPreviewActivity
import org.thoughtcrime.securesms.BlockUnblockDialog
import org.thoughtcrime.securesms.InviteActivity
import org.thoughtcrime.securesms.MuteDialog
import org.thoughtcrime.securesms.PushContactSelectionActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.badges.BadgeImageView
import org.thoughtcrime.securesms.badges.Badges
import org.thoughtcrime.securesms.badges.Badges.displayBadges
import org.thoughtcrime.securesms.badges.models.Badge
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
import org.thoughtcrime.securesms.components.AvatarImageView
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon
import org.thoughtcrime.securesms.components.settings.DSLSettingsText
import org.thoughtcrime.securesms.components.settings.NO_TINT
import org.thoughtcrime.securesms.components.settings.configure
import org.thoughtcrime.securesms.components.settings.conversation.preferences.AvatarPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.BioTextPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.GroupDescriptionPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.InternalPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LargeIconClickPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.RecipientPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.SharedMediaPreference
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader
import org.thoughtcrime.securesms.conversation.ConversationIntents
import org.thoughtcrime.securesms.groups.ParcelableGroupId
import org.thoughtcrime.securesms.groups.ui.GroupErrors
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity
import org.thoughtcrime.securesms.groups.ui.addtogroup.AddToGroupsActivity
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientExporter
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment
import org.thoughtcrime.securesms.stories.Stories
import org.thoughtcrime.securesms.stories.StoryViewerArgs
import org.thoughtcrime.securesms.stories.dialogs.StoryDialogs
import org.thoughtcrime.securesms.stories.viewer.AddToGroupStoryDelegate
import org.thoughtcrime.securesms.stories.viewer.StoryViewerActivity
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.ContextUtil
import org.thoughtcrime.securesms.util.ExpirationUtil
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity
private const val REQUEST_CODE_VIEW_CONTACT = 1
private const val REQUEST_CODE_ADD_CONTACT = 2
private const val REQUEST_CODE_ADD_MEMBERS_TO_GROUP = 3
private const val REQUEST_CODE_RETURN_FROM_MEDIA = 4
class ConversationSettingsFragment : DSLSettingsFragment(
layoutId = R.layout.conversation_settings_fragment,
menuId = R.menu.conversation_settings
) {
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
private val blockIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24).apply {
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
}
}
private val unblockIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_block_tinted_24)
}
private val leaveIcon by lazy {
ContextUtil.requireDrawable(requireContext(), R.drawable.ic_leave_tinted_24).apply {
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
}
}
private val viewModel by viewModels<ConversationSettingsViewModel>(
factoryProducer = {
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
val groupId = args.groupId as? ParcelableGroupId
ConversationSettingsViewModel.Factory(
recipientId = args.recipientId,
groupId = ParcelableGroupId.get(groupId),
repository = ConversationSettingsRepository(requireContext())
)
}
)
private lateinit var callback: Callback
private lateinit var toolbar: Toolbar
private lateinit var toolbarAvatarContainer: FrameLayout
private lateinit var toolbarAvatar: AvatarImageView
private lateinit var toolbarBadge: BadgeImageView
private lateinit var toolbarTitle: TextView
private lateinit var toolbarBackground: View
private lateinit var addToGroupStoryDelegate: AddToGroupStoryDelegate
private val navController get() = Navigation.findNavController(requireView())
override fun onAttach(context: Context) {
super.onAttach(context)
callback = context as Callback
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
toolbar = view.findViewById(R.id.toolbar)
toolbarAvatarContainer = view.findViewById(R.id.toolbar_avatar_container)
toolbarAvatar = view.findViewById(R.id.toolbar_avatar)
toolbarBadge = view.findViewById(R.id.toolbar_badge)
toolbarTitle = view.findViewById(R.id.toolbar_title)
toolbarBackground = view.findViewById(R.id.toolbar_background)
val args: ConversationSettingsFragmentArgs = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
if (args.recipientId != null) {
layoutManagerProducer = Badges::createLayoutManagerForGridWithBadges
}
super.onViewCreated(view, savedInstanceState)
recyclerView?.addOnScrollListener(ConversationSettingsOnUserScrolledAnimationHelper(toolbarAvatarContainer, toolbarTitle, toolbarBackground))
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
REQUEST_CODE_ADD_MEMBERS_TO_GROUP -> if (data != null) {
val selected: List<RecipientId> = requireNotNull(data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS))
val progress: SimpleProgressDialog.DismissibleDialog = SimpleProgressDialog.showDelayed(requireContext())
viewModel.onAddToGroupComplete(selected) {
progress.dismiss()
}
}
REQUEST_CODE_RETURN_FROM_MEDIA -> viewModel.refreshSharedMedia()
REQUEST_CODE_ADD_CONTACT -> viewModel.refreshRecipient()
REQUEST_CODE_VIEW_CONTACT -> viewModel.refreshRecipient()
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return if (item.itemId == R.id.action_edit) {
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
val groupId = args.groupId as ParcelableGroupId
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), requireNotNull(ParcelableGroupId.get(groupId))))
true
} else {
super.onOptionsItemSelected(item)
}
}
override fun getMaterial3OnScrollHelper(toolbar: Toolbar?): Material3OnScrollHelper {
return object : Material3OnScrollHelper(requireActivity(), toolbar!!) {
override val inactiveColorSet = ColorSet(
toolbarColorRes = R.color.signal_colorBackground_0,
statusBarColorRes = R.color.signal_colorBackground
)
}
}
override fun bindAdapter(adapter: MappingAdapter) {
val args = ConversationSettingsFragmentArgs.fromBundle(requireArguments())
BioTextPreference.register(adapter)
AvatarPreference.register(adapter)
ButtonStripPreference.register(adapter)
LargeIconClickPreference.register(adapter)
SharedMediaPreference.register(adapter)
RecipientPreference.register(adapter)
InternalPreference.register(adapter)
GroupDescriptionPreference.register(adapter)
LegacyGroupPreference.register(adapter)
val recipientId = args.recipientId
if (recipientId != null) {
Badge.register(adapter) { badge, _, _ ->
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, recipientId, badge)
}
}
addToGroupStoryDelegate = AddToGroupStoryDelegate(this)
viewModel.state.observe(viewLifecycleOwner) { state ->
if (state.recipient != Recipient.UNKNOWN) {
toolbarAvatar.buildOptions()
.withQuickContactEnabled(false)
.withUseSelfProfileAvatar(false)
.withFixedSize(ViewUtil.dpToPx(80))
.load(state.recipient)
if (!state.recipient.isSelf) {
toolbarBadge.setBadgeFromRecipient(state.recipient)
}
state.withRecipientSettingsState {
toolbarTitle.text = if (state.recipient.isSelf) getString(R.string.note_to_self) else state.recipient.getDisplayName(requireContext())
}
state.withGroupSettingsState {
toolbarTitle.text = it.groupTitle
toolbar.menu.findItem(R.id.action_edit).isVisible = it.canEditGroupAttributes
}
}
adapter.submitList(getConfiguration(state).toMappingModelList()) {
if (state.isLoaded) {
(view?.parent as? ViewGroup)?.doOnPreDraw {
callback.onContentWillRender()
}
}
}
}
viewModel.events.observe(viewLifecycleOwner) { event ->
@Exhaustive
when (event) {
is ConversationSettingsEvent.AddToAGroup -> handleAddToAGroup(event)
is ConversationSettingsEvent.AddMembersToGroup -> handleAddMembersToGroup(event)
ConversationSettingsEvent.ShowGroupHardLimitDialog -> showGroupHardLimitDialog()
is ConversationSettingsEvent.ShowAddMembersToGroupError -> showAddMembersToGroupError(event)
is ConversationSettingsEvent.ShowGroupInvitesSentDialog -> showGroupInvitesSentDialog(event)
is ConversationSettingsEvent.ShowMembersAdded -> showMembersAdded(event)
is ConversationSettingsEvent.InitiateGroupMigration -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(parentFragmentManager, event.recipientId)
}
}
}
private fun getConfiguration(state: ConversationSettingsState): DSLConfiguration {
return configure {
if (state.recipient == Recipient.UNKNOWN) {
return@configure
}
customPref(
AvatarPreference.Model(
recipient = state.recipient,
storyViewState = state.storyViewState,
onAvatarClick = { avatar ->
val viewAvatarIntent = AvatarPreviewActivity.intentFromRecipientId(requireContext(), state.recipient.id)
val viewAvatarTransitionBundle = AvatarPreviewActivity.createTransitionBundle(requireActivity(), avatar)
if (Stories.isFeatureEnabled() && avatar.hasStory()) {
val viewStoryIntent = StoryViewerActivity.createIntent(
requireContext(),
StoryViewerArgs(
recipientId = state.recipient.id,
isInHiddenStoryMode = state.recipient.shouldHideStory(),
isFromQuote = true
)
)
StoryDialogs.displayStoryOrProfileImage(
context = requireContext(),
onViewStory = { startActivity(viewStoryIntent) },
onViewAvatar = { startActivity(viewAvatarIntent, viewAvatarTransitionBundle) }
)
} else if (!state.recipient.isSelf) {
startActivity(viewAvatarIntent, viewAvatarTransitionBundle)
}
},
onBadgeClick = { badge ->
ViewBadgeBottomSheetDialogFragment.show(parentFragmentManager, state.recipient.id, badge)
}
)
)
state.withRecipientSettingsState {
customPref(BioTextPreference.RecipientModel(recipient = state.recipient))
}
state.withGroupSettingsState { groupState ->
val groupMembershipDescription = if (groupState.groupId.isV1) {
String.format("%s · %s", groupState.membershipCountDescription, getString(R.string.ManageGroupActivity_legacy_group))
} else if (!groupState.canEditGroupAttributes && groupState.groupDescription.isNullOrEmpty()) {
groupState.membershipCountDescription
} else {
null
}
customPref(
BioTextPreference.GroupModel(
groupTitle = groupState.groupTitle,
groupMembershipDescription = groupMembershipDescription
)
)
if (groupState.groupId.isV2) {
customPref(
GroupDescriptionPreference.Model(
groupId = groupState.groupId,
groupDescription = groupState.groupDescription,
descriptionShouldLinkify = groupState.groupDescriptionShouldLinkify,
canEditGroupAttributes = groupState.canEditGroupAttributes,
onEditGroupDescription = {
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), groupState.groupId))
},
onViewGroupDescription = {
GroupDescriptionDialog.show(childFragmentManager, groupState.groupId, null, groupState.groupDescriptionShouldLinkify)
}
)
)
} else if (groupState.legacyGroupState != LegacyGroupPreference.State.NONE) {
customPref(
LegacyGroupPreference.Model(
state = groupState.legacyGroupState,
onLearnMoreClick = { GroupsLearnMoreBottomSheetDialogFragment.show(parentFragmentManager) },
onUpgradeClick = { viewModel.initiateGroupUpgrade() },
onMmsWarningClick = { startActivity(Intent(requireContext(), InviteActivity::class.java)) }
)
)
}
}
if (state.displayInternalRecipientDetails) {
customPref(
InternalPreference.Model(
recipient = state.recipient,
onInternalDetailsClicked = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToInternalDetailsSettingsFragment(state.recipient.id)
navController.safeNavigate(action)
}
)
)
}
customPref(
ButtonStripPreference.Model(
state = state.buttonStripState,
onAddToStoryClick = {
if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ConversationSettingsFragment__cant_add_to_group_story)
.setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story)
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
} else {
addToGroupStoryDelegate.addToStory(state.recipient.id)
}
},
onVideoClick = {
if (state.recipient.isPushV2Group && state.requireGroupSettingsState().isAnnouncementGroup && !state.requireGroupSettingsState().isSelfAdmin) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.ConversationActivity_cant_start_group_call)
.setMessage(R.string.ConversationActivity_only_admins_of_this_group_can_start_a_call)
.setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() }
.show()
} else {
CommunicationActions.startVideoCall(requireActivity(), state.recipient)
}
},
onAudioClick = {
CommunicationActions.startVoiceCall(requireActivity(), state.recipient)
},
onMuteClick = {
if (!state.buttonStripState.isMuted) {
MuteDialog.show(requireContext(), viewModel::setMuteUntil)
} else {
MaterialAlertDialogBuilder(requireContext())
.setMessage(state.recipient.muteUntil.formatMutedUntil(requireContext()))
.setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ ->
viewModel.unmute()
dialog.dismiss()
}
.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
.show()
}
},
onSearchClick = {
val intent = ConversationIntents.createBuilder(requireContext(), state.recipient.id, state.threadId)
.withSearchOpen(true)
.build()
startActivity(intent)
requireActivity().finish()
}
)
)
dividerPref()
val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan))
val icon = if (state.disappearingMessagesLifespan <= 0 || state.recipient.isBlocked) {
R.drawable.ic_update_timer_disabled_16
} else {
R.drawable.ic_update_timer_16
}
var enabled = !state.recipient.isBlocked
state.withGroupSettingsState {
enabled = it.canEditGroupAttributes && !state.recipient.isBlocked
}
if (!state.recipient.isReleaseNotes && !state.recipient.isBlocked) {
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
summary = summary,
icon = DSLSettingsIcon.from(icon),
isEnabled = enabled,
onClick = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
.setInitialValue(state.disappearingMessagesLifespan)
.setRecipientId(state.recipient.id)
.setForResultMode(false)
navController.safeNavigate(action)
}
)
}
if (!state.recipient.isReleaseNotes) {
clickPref(
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
onClick = {
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
}
)
}
if (!state.recipient.isSelf) {
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications),
icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24),
onClick = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
navController.safeNavigate(action)
}
)
}
state.withRecipientSettingsState { recipientState ->
when (recipientState.contactLinkState) {
ContactLinkState.OPEN -> {
@Suppress("DEPRECATION")
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__contact_details),
icon = DSLSettingsIcon.from(R.drawable.ic_profile_circle_24),
onClick = {
startActivityForResult(Intent(Intent.ACTION_VIEW, state.recipient.contactUri), REQUEST_CODE_VIEW_CONTACT)
}
)
}
ContactLinkState.ADD -> {
@Suppress("DEPRECATION")
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_as_a_contact),
icon = DSLSettingsIcon.from(R.drawable.ic_plus_24),
onClick = {
try {
startActivityForResult(RecipientExporter.export(state.recipient).asAddContactIntent(), REQUEST_CODE_ADD_CONTACT)
} catch (e: ActivityNotFoundException) {
Toast.makeText(context, R.string.ConversationSettingsFragment__contacts_app_not_found, Toast.LENGTH_SHORT).show()
}
}
)
}
ContactLinkState.NONE -> {
}
}
if (recipientState.identityRecord != null) {
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__view_safety_number),
icon = DSLSettingsIcon.from(R.drawable.ic_safety_number_24),
onClick = {
startActivity(VerifyIdentityActivity.newIntent(requireActivity(), recipientState.identityRecord))
}
)
}
}
if (state.sharedMedia != null && state.sharedMedia.count > 0) {
dividerPref()
sectionHeaderPref(R.string.recipient_preference_activity__shared_media)
@Suppress("DEPRECATION")
customPref(
SharedMediaPreference.Model(
mediaCursor = state.sharedMedia,
mediaIds = state.sharedMediaIds,
onMediaRecordClick = { mediaRecord, isLtr ->
startActivityForResult(
MediaIntentFactory.intentFromMediaRecord(requireContext(), mediaRecord, isLtr, allMediaInRail = true),
REQUEST_CODE_RETURN_FROM_MEDIA
)
}
)
)
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
onClick = {
startActivity(MediaOverviewActivity.forThread(requireContext(), state.threadId))
}
)
}
state.withRecipientSettingsState { recipientSettingsState ->
if (state.recipient.badges.isNotEmpty()) {
dividerPref()
sectionHeaderPref(R.string.ManageProfileFragment_badges)
displayBadges(requireContext(), state.recipient.badges)
textPref(
summary = DSLSettingsText.from(
R.string.ConversationSettingsFragment__get_badges
)
)
}
if (recipientSettingsState.selfHasGroups && !state.recipient.isReleaseNotes) {
dividerPref()
val groupsInCommonCount = recipientSettingsState.allGroupsInCommon.size
sectionHeaderPref(
DSLSettingsText.from(
if (groupsInCommonCount == 0) {
getString(R.string.ManageRecipientActivity_no_groups_in_common)
} else {
resources.getQuantityString(
R.plurals.ManageRecipientActivity_d_groups_in_common,
groupsInCommonCount,
groupsInCommonCount
)
}
)
)
if (!state.recipient.isBlocked) {
customPref(
LargeIconClickPreference.Model(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_to_a_group),
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
onClick = {
viewModel.onAddToGroup()
}
)
)
}
for (group in recipientSettingsState.groupsInCommon) {
customPref(
RecipientPreference.Model(
recipient = group,
onClick = {
CommunicationActions.startConversation(requireActivity(), group, null)
requireActivity().finish()
}
)
)
}
if (recipientSettingsState.canShowMoreGroupsInCommon) {
customPref(
LargeIconClickPreference.Model(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
onClick = {
viewModel.revealAllMembers()
}
)
)
}
}
}
state.withGroupSettingsState { groupState ->
val memberCount = groupState.allMembers.size
if (groupState.canAddToGroup || memberCount > 0) {
dividerPref()
sectionHeaderPref(DSLSettingsText.from(resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)))
}
if (groupState.canAddToGroup) {
customPref(
LargeIconClickPreference.Model(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_members),
icon = DSLSettingsIcon.from(R.drawable.add_to_a_group, NO_TINT),
onClick = {
viewModel.onAddToGroup()
}
)
)
}
for (member in groupState.members) {
customPref(
RecipientPreference.Model(
recipient = member.member,
isAdmin = member.isAdmin,
onClick = {
RecipientBottomSheetDialogFragment.create(member.member.id, groupState.groupId).show(parentFragmentManager, "BOTTOM")
}
)
)
}
if (groupState.canShowMoreGroupMembers) {
customPref(
LargeIconClickPreference.Model(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__see_all),
icon = DSLSettingsIcon.from(R.drawable.show_more, NO_TINT),
onClick = {
viewModel.revealAllMembers()
}
)
)
}
if (state.recipient.isPushV2Group) {
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__group_link),
summary = DSLSettingsText.from(if (groupState.groupLinkEnabled) R.string.preferences_on else R.string.preferences_off),
icon = DSLSettingsIcon.from(R.drawable.ic_link_16),
onClick = {
navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToShareableGroupLinkFragment(groupState.groupId.requireV2().toString()))
}
)
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__requests_and_invites),
icon = DSLSettingsIcon.from(R.drawable.ic_update_group_add_16),
onClick = {
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), groupState.groupId.requireV2()))
}
)
if (groupState.isSelfAdmin) {
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__permissions),
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
onClick = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToPermissionsSettingsFragment(ParcelableGroupId.from(groupState.groupId))
navController.safeNavigate(action)
}
)
}
}
if (groupState.canLeave) {
dividerPref()
clickPref(
title = DSLSettingsText.from(R.string.conversation__menu_leave_group, alertTint),
icon = DSLSettingsIcon.from(leaveIcon),
onClick = {
LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupState.groupId.requirePush(), null)
}
)
}
}
if (state.canModifyBlockedState) {
state.withRecipientSettingsState {
dividerPref()
}
state.withGroupSettingsState {
if (!it.canLeave) {
dividerPref()
}
}
val isBlocked = state.recipient.isBlocked
val isGroup = state.recipient.isPushGroup
val title = when {
isBlocked && isGroup -> R.string.ConversationSettingsFragment__unblock_group
isBlocked -> R.string.ConversationSettingsFragment__unblock
isGroup -> R.string.ConversationSettingsFragment__block_group
else -> R.string.ConversationSettingsFragment__block
}
val titleTint = if (isBlocked) null else alertTint
val blockUnblockIcon = if (isBlocked) unblockIcon else blockIcon
clickPref(
title = if (titleTint != null) DSLSettingsText.from(title, titleTint) else DSLSettingsText.from(title),
icon = DSLSettingsIcon.from(blockUnblockIcon),
onClick = {
if (state.recipient.isBlocked) {
BlockUnblockDialog.showUnblockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
viewModel.unblock()
}
} else {
BlockUnblockDialog.showBlockFor(requireContext(), viewLifecycleOwner.lifecycle, state.recipient) {
viewModel.block()
}
}
}
)
}
}
}
private fun formatDisappearingMessagesLifespan(disappearingMessagesLifespan: Int): String {
return if (disappearingMessagesLifespan <= 0) {
getString(R.string.preferences_off)
} else {
ExpirationUtil.getExpirationDisplayValue(requireContext(), disappearingMessagesLifespan)
}
}
private fun handleAddToAGroup(addToAGroup: ConversationSettingsEvent.AddToAGroup) {
startActivity(AddToGroupsActivity.newIntent(requireContext(), addToAGroup.recipientId, addToAGroup.groupMembership))
}
@Suppress("DEPRECATION")
private fun handleAddMembersToGroup(addMembersToGroup: ConversationSettingsEvent.AddMembersToGroup) {
startActivityForResult(
AddMembersActivity.createIntent(
requireContext(),
addMembersToGroup.groupId,
ContactsCursorLoader.DisplayMode.FLAG_PUSH,
addMembersToGroup.selectionWarning,
addMembersToGroup.selectionLimit,
addMembersToGroup.isAnnouncementGroup,
addMembersToGroup.groupMembersWithoutSelf
),
REQUEST_CODE_ADD_MEMBERS_TO_GROUP
)
}
private fun showGroupHardLimitDialog() {
GroupLimitDialog.showHardLimitMessage(requireContext())
}
private fun showAddMembersToGroupError(showAddMembersToGroupError: ConversationSettingsEvent.ShowAddMembersToGroupError) {
Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(showAddMembersToGroupError.failureReason), Toast.LENGTH_LONG).show()
}
private fun showGroupInvitesSentDialog(showGroupInvitesSentDialog: ConversationSettingsEvent.ShowGroupInvitesSentDialog) {
GroupInviteSentDialog.showInvitesSent(requireContext(), viewLifecycleOwner, showGroupInvitesSentDialog.invitesSentTo)
}
private fun showMembersAdded(showMembersAdded: ConversationSettingsEvent.ShowMembersAdded) {
val string = resources.getQuantityString(
R.plurals.ManageGroupActivity_added,
showMembersAdded.membersAddedCount,
showMembersAdded.membersAddedCount
)
Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).show()
}
private class ConversationSettingsOnUserScrolledAnimationHelper(
private val toolbarAvatar: View,
private val toolbarTitle: View,
private val toolbarBackground: View
) : OnScrollAnimationHelper() {
override val duration: Long = 200L
private val actionBarSize = DimensionUnit.DP.toPixels(64f)
private val rect = Rect()
override fun getAnimationState(recyclerView: RecyclerView): AnimationState {
val layoutManager = recyclerView.layoutManager!!
val firstVisibleItemPosition = if (layoutManager is FlexboxLayoutManager) {
layoutManager.findFirstVisibleItemPosition()
} else {
(layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
}
return if (firstVisibleItemPosition == 0) {
val firstChild = requireNotNull(layoutManager.getChildAt(0))
firstChild.getLocalVisibleRect(rect)
if (rect.height() <= actionBarSize) {
AnimationState.SHOW
} else {
AnimationState.HIDE
}
} else {
AnimationState.SHOW
}
}
override fun show(duration: Long) {
toolbarAvatar
.animate()
.setDuration(duration)
.translationY(0f)
.alpha(1f)
toolbarTitle
.animate()
.setDuration(duration)
.translationY(0f)
.alpha(1f)
toolbarBackground
.animate()
.setDuration(duration)
.alpha(1f)
}
override fun hide(duration: Long) {
toolbarAvatar
.animate()
.setDuration(duration)
.translationY(ViewUtil.dpToPx(56).toFloat())
.alpha(0f)
toolbarTitle
.animate()
.setDuration(duration)
.translationY(ViewUtil.dpToPx(56).toFloat())
.alpha(0f)
toolbarBackground
.animate()
.setDuration(duration)
.alpha(0f)
}
}
interface Callback {
fun onContentWillRender()
}
}