mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Add group terminate support.
This commit is contained in:
@@ -20,6 +20,7 @@ import androidx.appcompat.widget.Toolbar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.Navigation
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -28,6 +29,7 @@ import com.google.android.flexbox.FlexboxLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import kotlinx.coroutines.launch
|
||||
import org.signal.core.ui.permissions.Permissions
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.signal.core.util.Result
|
||||
@@ -48,6 +50,7 @@ import org.thoughtcrime.securesms.badges.models.Badge
|
||||
import org.thoughtcrime.securesms.badges.view.ViewBadgeBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView
|
||||
import org.thoughtcrime.securesms.components.ProgressCardDialogFragment
|
||||
import org.thoughtcrime.securesms.components.recyclerview.OnScrollAnimationHelper
|
||||
import org.thoughtcrime.securesms.components.settings.DSLConfiguration
|
||||
import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment
|
||||
@@ -67,6 +70,7 @@ import org.thoughtcrime.securesms.components.settings.conversation.preferences.L
|
||||
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.TerminatedBannerPreference
|
||||
import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerV2
|
||||
@@ -74,6 +78,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.MemberLabelEducationSheet
|
||||
import org.thoughtcrime.securesms.groups.memberlabel.StyledMemberLabel
|
||||
import org.thoughtcrime.securesms.groups.ui.EndGroupDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
|
||||
@@ -147,6 +152,12 @@ class ConversationSettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
private val endGroupIcon by lazy {
|
||||
ContextUtil.requireDrawable(requireContext(), R.drawable.symbol_x_circle_24).apply {
|
||||
colorFilter = PorterDuffColorFilter(alertTint, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
}
|
||||
|
||||
private val viewModel by viewModels<ConversationSettingsViewModel>(
|
||||
factoryProducer = {
|
||||
val groupId = args.groupId as? GroupId
|
||||
@@ -268,6 +279,7 @@ class ConversationSettingsFragment :
|
||||
InternalPreference.register(adapter)
|
||||
GroupDescriptionPreference.register(adapter)
|
||||
LegacyGroupPreference.register(adapter)
|
||||
TerminatedBannerPreference.register(adapter)
|
||||
CallPreference.register(adapter)
|
||||
|
||||
val recipientId = args.recipientId
|
||||
@@ -330,10 +342,17 @@ class ConversationSettingsFragment :
|
||||
return@configure
|
||||
}
|
||||
|
||||
state.withGroupSettingsState {
|
||||
if (it.isTerminated) {
|
||||
customPref(TerminatedBannerPreference.Model())
|
||||
}
|
||||
}
|
||||
|
||||
customPref(
|
||||
AvatarPreference.Model(
|
||||
recipient = state.recipient,
|
||||
storyViewState = state.storyViewState,
|
||||
reduceTopMargin = state.isTerminatedGroup,
|
||||
onAvatarClick = { avatar ->
|
||||
val viewAvatarIntent = AvatarPreviewActivity.intentFromRecipientId(requireContext(), state.recipient.id)
|
||||
val viewAvatarTransitionBundle = AvatarPreviewActivity.createTransitionBundle(requireActivity(), avatar)
|
||||
@@ -392,7 +411,7 @@ class ConversationSettingsFragment :
|
||||
)
|
||||
)
|
||||
|
||||
if (groupState.groupId.isV2) {
|
||||
if (groupState.groupId.isV2 && !groupState.isTerminated) {
|
||||
customPref(
|
||||
GroupDescriptionPreference.Model(
|
||||
groupId = groupState.groupId,
|
||||
@@ -793,10 +812,15 @@ class ConversationSettingsFragment :
|
||||
if (groupState.canAddToGroup || memberCount > 0) {
|
||||
dividerPref()
|
||||
|
||||
sectionHeaderPref(DSLSettingsText.from(resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)))
|
||||
val memberHeaderText = if (groupState.isTerminated) {
|
||||
resources.getQuantityString(R.plurals.ConversationSettingsFragment__d_former_members, memberCount, memberCount)
|
||||
} else {
|
||||
resources.getQuantityString(R.plurals.ContactSelectionListFragment_d_members, memberCount, memberCount)
|
||||
}
|
||||
sectionHeaderPref(DSLSettingsText.from(memberHeaderText))
|
||||
}
|
||||
|
||||
if (groupState.canAddToGroup && !state.isDeprecatedOrUnregistered) {
|
||||
if (groupState.canAddToGroup && !groupState.isTerminated && !state.isDeprecatedOrUnregistered) {
|
||||
customPref(
|
||||
LargeIconClickPreference.Model(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__add_members),
|
||||
@@ -851,14 +875,14 @@ class ConversationSettingsFragment :
|
||||
)
|
||||
}
|
||||
|
||||
if (state.recipient.isPushV2Group) {
|
||||
if (state.recipient.isPushV2Group && !groupState.isTerminated) {
|
||||
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),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
isEnabled = state.recipient.isActiveGroup && !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
navController.safeNavigate(ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToShareableGroupLinkFragment(groupState.groupId.requireV2().toString()))
|
||||
}
|
||||
@@ -880,7 +904,7 @@ class ConversationSettingsFragment :
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__requests_and_invites),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_update_group_add_16),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
isEnabled = state.recipient.isActiveGroup && !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
startActivity(ManagePendingAndRequestingMembersActivity.newIntent(requireContext(), groupState.groupId.requireV2()))
|
||||
}
|
||||
@@ -890,7 +914,7 @@ class ConversationSettingsFragment :
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__permissions),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_lock_24),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
isEnabled = state.recipient.isActiveGroup && !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToPermissionsSettingsFragment(groupState.groupId)
|
||||
navController.safeNavigate(action)
|
||||
@@ -913,7 +937,46 @@ class ConversationSettingsFragment :
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canModifyBlockedState) {
|
||||
state.withGroupSettingsState { groupState ->
|
||||
if (groupState.isTerminated) {
|
||||
dividerPref()
|
||||
|
||||
if (state.isArchived) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationListFragment_unarchive),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_archive_up_24),
|
||||
onClick = {
|
||||
viewModel.toggleArchive()
|
||||
}
|
||||
)
|
||||
} else {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__archive_chat),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_archive_24),
|
||||
onClick = {
|
||||
viewModel.toggleArchive()
|
||||
onToolbarNavigationClicked()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__delete_chat, alertTint),
|
||||
icon = DSLSettingsIcon.from(CoreUiR.drawable.symbol_trash_24, R.color.signal_alert_primary),
|
||||
onClick = {
|
||||
val progressDialog = ProgressCardDialogFragment.create(getString(R.string.ConversationFragment_deleting_messages))
|
||||
progressDialog.show(parentFragmentManager, null)
|
||||
lifecycleScope.launch {
|
||||
viewModel.deleteChat()
|
||||
progressDialog.dismissAllowingStateLoss()
|
||||
onToolbarNavigationClicked()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.canModifyBlockedState && !state.isTerminatedGroup) {
|
||||
state.withRecipientSettingsState {
|
||||
dividerPref()
|
||||
}
|
||||
@@ -1005,6 +1068,50 @@ class ConversationSettingsFragment :
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
if (groupState.isTerminated) {
|
||||
dividerPref()
|
||||
|
||||
val reportSpamTint = R.color.signal_alert_primary
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationFragment_report_spam, ContextCompat.getColor(requireContext(), reportSpamTint)),
|
||||
icon = DSLSettingsIcon.from(R.drawable.symbol_spam_24, reportSpamTint),
|
||||
onClick = {
|
||||
BlockUnblockDialog.showReportSpamFor(
|
||||
requireContext(),
|
||||
viewLifecycleOwner.lifecycle,
|
||||
state.recipient,
|
||||
{
|
||||
viewModel
|
||||
.onReportSpam()
|
||||
.subscribeBy {
|
||||
Toast.makeText(requireContext(), R.string.ConversationFragment_reported_as_spam, Toast.LENGTH_SHORT).show()
|
||||
onToolbarNavigationClicked()
|
||||
}
|
||||
.addTo(lifecycleDisposable)
|
||||
},
|
||||
null
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
state.withGroupSettingsState { groupState ->
|
||||
if (groupState.canEndGroup && RemoteConfig.groupTerminateSend) {
|
||||
dividerPref()
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__end_group, if (state.isDeprecatedOrUnregistered) alertDisabledTint else alertTint),
|
||||
icon = DSLSettingsIcon.from(endGroupIcon),
|
||||
isEnabled = !state.isDeprecatedOrUnregistered,
|
||||
onClick = {
|
||||
EndGroupDialog.show(requireActivity(), groupState.groupId.requireV2(), groupState.groupTitle)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,9 +222,18 @@ class ConversationSettingsRepository(
|
||||
return liveGroup.getMembershipCountDescription(context.resources)
|
||||
}
|
||||
|
||||
fun getExternalPossiblyMigratedGroupRecipientId(groupId: GroupId, consumer: (RecipientId) -> Unit) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
consumer(Recipient.externalPossiblyMigratedGroup(groupId).id)
|
||||
}
|
||||
@WorkerThread
|
||||
fun isArchived(recipientId: RecipientId): Boolean {
|
||||
return SignalDatabase.threads.isArchived(recipientId)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun setArchived(threadId: Long, archived: Boolean) {
|
||||
SignalDatabase.threads.setArchived(setOf(threadId), archived)
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
fun deleteChat(threadId: Long) {
|
||||
SignalDatabase.threads.deleteConversation(threadId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ data class ConversationSettingsState(
|
||||
val buttonStripState: ButtonStripPreference.State = ButtonStripPreference.State(),
|
||||
val disappearingMessagesLifespan: Int = 0,
|
||||
val canModifyBlockedState: Boolean = false,
|
||||
val isArchived: Boolean = false,
|
||||
val sharedMedia: List<MediaTable.MediaRecord> = emptyList(),
|
||||
val sharedMediaIds: List<Long> = listOf(),
|
||||
val displayInternalRecipientDetails: Boolean = false,
|
||||
@@ -29,6 +30,7 @@ data class ConversationSettingsState(
|
||||
) {
|
||||
|
||||
val isLoaded: Boolean = recipient != Recipient.UNKNOWN && sharedMediaLoaded && specificSettingsState.isLoaded
|
||||
val isTerminatedGroup: Boolean = (specificSettingsState as? SpecificSettingsState.GroupSettingsState)?.isTerminated == true
|
||||
|
||||
fun withRecipientSettingsState(consumer: (SpecificSettingsState.RecipientSettingsState) -> Unit) {
|
||||
if (specificSettingsState is SpecificSettingsState.RecipientSettingsState) {
|
||||
@@ -72,6 +74,8 @@ sealed class SpecificSettingsState {
|
||||
val isSelfAdmin: Boolean = false,
|
||||
val canAddToGroup: Boolean = false,
|
||||
val canEditGroupAttributes: Boolean = false,
|
||||
val isActive: Boolean = false,
|
||||
val isTerminated: Boolean = false,
|
||||
val canLeave: Boolean = false,
|
||||
val canShowMoreGroupMembers: Boolean = false,
|
||||
val groupMembersExpanded: Boolean = false,
|
||||
@@ -88,6 +92,8 @@ sealed class SpecificSettingsState {
|
||||
val canSetOwnMemberLabel: Boolean = false
|
||||
) : SpecificSettingsState() {
|
||||
|
||||
val canEndGroup: Boolean get() = isActive && groupId.isV2 && isSelfAdmin
|
||||
|
||||
override val isLoaded: Boolean = groupTitleLoaded && groupDescriptionLoaded
|
||||
|
||||
override fun requireGroupSettingsState(): GroupSettingsState = this
|
||||
|
||||
@@ -144,6 +144,26 @@ sealed class ConversationSettingsViewModel(
|
||||
disposable.clear()
|
||||
}
|
||||
|
||||
fun toggleArchive() {
|
||||
val state = store.state
|
||||
if (state.threadId > 0) {
|
||||
val newArchived = !state.isArchived
|
||||
store.update { it.copy(isArchived = newArchived) }
|
||||
viewModelScope.launch(SignalDispatchers.IO) {
|
||||
repository.setArchived(state.threadId, newArchived)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChat() {
|
||||
withContext(SignalDispatchers.IO) {
|
||||
val threadId = store.state.threadId
|
||||
if (threadId > 0) {
|
||||
repository.deleteChat(threadId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RecipientSettingsViewModel(
|
||||
private val recipientId: RecipientId,
|
||||
private val callMessageIds: LongArray,
|
||||
@@ -299,21 +319,21 @@ sealed class ConversationSettingsViewModel(
|
||||
store.update { it.copy(storyViewState = storyViewState) }
|
||||
}
|
||||
|
||||
val recipientAndIsActive = LiveDataUtil.combineLatest(liveGroup.groupRecipient, liveGroup.isActive) { r, a -> r to a }
|
||||
store.update(recipientAndIsActive) { (recipient, isActive), state ->
|
||||
store.update(liveGroup.groupRecipient) { recipient, state ->
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
buttonStripState = ButtonStripPreference.State(
|
||||
isMessageAvailable = callMessageIds.isNotEmpty(),
|
||||
isVideoAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive,
|
||||
isVideoAvailable = recipient.isPushV2Group && !recipient.isBlocked && recipient.isActiveGroup,
|
||||
isAudioAvailable = false,
|
||||
isAudioSecure = recipient.isPushV2Group,
|
||||
isMuted = recipient.isMuted,
|
||||
isMuteAvailable = true,
|
||||
isSearchAvailable = callMessageIds.isEmpty(),
|
||||
isAddToStoryAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive && !SignalStore.story.isFeatureDisabled
|
||||
isAddToStoryAvailable = recipient.isPushV2Group && !recipient.isBlocked && recipient.isActiveGroup && !SignalStore.story.isFeatureDisabled
|
||||
),
|
||||
canModifyBlockedState = RecipientUtil.isBlockable(recipient),
|
||||
isArchived = repository.isArchived(recipient.id),
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
legacyGroupState = getLegacyGroupState()
|
||||
)
|
||||
@@ -398,11 +418,20 @@ sealed class ConversationSettingsViewModel(
|
||||
store.update(liveGroup.isActive) { isActive, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
isActive = isActive,
|
||||
canLeave = isActive && groupId.isPush
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.isTerminated) { isTerminated, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
isTerminated = isTerminated
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
store.update(liveGroup.title) { title, state ->
|
||||
state.copy(
|
||||
specificSettingsState = state.requireGroupSettingsState().copy(
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.groups.GroupAccessControl
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
||||
class PermissionsSettingsViewModel(
|
||||
@@ -22,8 +23,8 @@ class PermissionsSettingsViewModel(
|
||||
val events: LiveData<PermissionsSettingsEvents> = internalEvents
|
||||
|
||||
init {
|
||||
store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
|
||||
state.copy(selfCanEditSettings = isSelfAdmin)
|
||||
store.update(LiveDataUtil.combineLatest(liveGroup.isSelfAdmin, liveGroup.isActive) { admin, active -> admin && active }) { canEdit, state ->
|
||||
state.copy(selfCanEditSettings = canEdit)
|
||||
}
|
||||
|
||||
store.update(liveGroup.membershipAdditionAccessControl) { membershipAdditionAccessControl, state ->
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.ViewCompat
|
||||
import org.signal.core.util.dp
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.avatar.view.AvatarView
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView
|
||||
@@ -25,6 +27,7 @@ object AvatarPreference {
|
||||
class Model(
|
||||
val recipient: Recipient,
|
||||
val storyViewState: StoryViewState,
|
||||
val reduceTopMargin: Boolean = false,
|
||||
val onAvatarClick: (AvatarView) -> Unit,
|
||||
val onBadgeClick: (Badge) -> Unit
|
||||
) : PreferenceModel<Model>() {
|
||||
@@ -35,7 +38,8 @@ object AvatarPreference {
|
||||
override fun areContentsTheSame(newItem: Model): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
recipient.hasSameContent(newItem.recipient) &&
|
||||
storyViewState == newItem.storyViewState
|
||||
storyViewState == newItem.storyViewState &&
|
||||
reduceTopMargin == newItem.reduceTopMargin
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +53,10 @@ object AvatarPreference {
|
||||
}
|
||||
|
||||
override fun bind(model: Model) {
|
||||
(itemView.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||
it.topMargin = if (model.reduceTopMargin) 0.dp else 40.dp
|
||||
}
|
||||
|
||||
if (model.recipient.isSelf) {
|
||||
badge.setBadge(null)
|
||||
badge.setOnClickListener(null)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.components.settings.conversation.preferences
|
||||
|
||||
import android.view.View
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.settings.PreferenceModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
|
||||
object TerminatedBannerPreference {
|
||||
|
||||
fun register(adapter: MappingAdapter) {
|
||||
adapter.registerFactory(Model::class.java, LayoutFactory(::ViewHolder, R.layout.conversation_settings_terminated_banner))
|
||||
}
|
||||
|
||||
class Model : PreferenceModel<Model>() {
|
||||
override fun areItemsTheSame(newItem: Model): Boolean = true
|
||||
override fun areContentsTheSame(newItem: Model): Boolean = true
|
||||
}
|
||||
|
||||
private class ViewHolder(itemView: View) : MappingViewHolder<Model>(itemView) {
|
||||
override fun bind(model: Model) = Unit
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user