kopia lustrzana https://github.com/ryukoposting/Signal-Android
500 wiersze
17 KiB
Kotlin
500 wiersze
17 KiB
Kotlin
package org.thoughtcrime.securesms.components.settings.conversation
|
|
|
|
import android.database.Cursor
|
|
import androidx.lifecycle.LiveData
|
|
import androidx.lifecycle.MutableLiveData
|
|
import androidx.lifecycle.Transformations
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.ViewModelProvider
|
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
|
import org.signal.core.util.CursorUtil
|
|
import org.signal.core.util.ThreadUtil
|
|
import org.signal.core.util.concurrent.SignalExecutors
|
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.ButtonStripPreference
|
|
import org.thoughtcrime.securesms.components.settings.conversation.preferences.LegacyGroupPreference
|
|
import org.thoughtcrime.securesms.database.AttachmentTable
|
|
import org.thoughtcrime.securesms.database.RecipientTable
|
|
import org.thoughtcrime.securesms.database.model.StoryViewState
|
|
import org.thoughtcrime.securesms.groups.GroupId
|
|
import org.thoughtcrime.securesms.groups.LiveGroup
|
|
import org.thoughtcrime.securesms.groups.v2.GroupAddMembersResult
|
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
|
import org.thoughtcrime.securesms.recipients.Recipient
|
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
|
import org.thoughtcrime.securesms.recipients.RecipientUtil
|
|
import org.thoughtcrime.securesms.util.FeatureFlags
|
|
import org.thoughtcrime.securesms.util.SingleLiveEvent
|
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
|
|
import org.thoughtcrime.securesms.util.livedata.Store
|
|
import java.util.Optional
|
|
|
|
sealed class ConversationSettingsViewModel(
|
|
private val repository: ConversationSettingsRepository,
|
|
specificSettingsState: SpecificSettingsState,
|
|
) : ViewModel() {
|
|
|
|
private val openedMediaCursors = HashSet<Cursor>()
|
|
|
|
@Volatile
|
|
private var cleared = false
|
|
|
|
protected val store = Store(
|
|
ConversationSettingsState(
|
|
specificSettingsState = specificSettingsState
|
|
)
|
|
)
|
|
protected val internalEvents = SingleLiveEvent<ConversationSettingsEvent>()
|
|
|
|
private val sharedMediaUpdateTrigger = MutableLiveData(Unit)
|
|
|
|
val state: LiveData<ConversationSettingsState> = store.stateLiveData
|
|
val events: LiveData<ConversationSettingsEvent> = internalEvents
|
|
|
|
protected val disposable = CompositeDisposable()
|
|
|
|
init {
|
|
val threadId: LiveData<Long> = Transformations.distinctUntilChanged(Transformations.map(state) { it.threadId })
|
|
val updater: LiveData<Long> = LiveDataUtil.combineLatest(threadId, sharedMediaUpdateTrigger) { tId, _ -> tId }
|
|
|
|
val sharedMedia: LiveData<Optional<Cursor>> = LiveDataUtil.mapAsync(SignalExecutors.BOUNDED, updater) { tId ->
|
|
repository.getThreadMedia(tId)
|
|
}
|
|
|
|
store.update(sharedMedia) { cursor, state ->
|
|
if (!cleared) {
|
|
if (cursor.isPresent) {
|
|
openedMediaCursors.add(cursor.get())
|
|
}
|
|
|
|
val ids: List<Long> = cursor.map<List<Long>> {
|
|
val result = mutableListOf<Long>()
|
|
while (it.moveToNext()) {
|
|
result.add(CursorUtil.requireLong(it, AttachmentTable.ROW_ID))
|
|
}
|
|
result
|
|
}.orElse(listOf())
|
|
|
|
state.copy(
|
|
sharedMedia = cursor.orElse(null),
|
|
sharedMediaIds = ids,
|
|
sharedMediaLoaded = true,
|
|
displayInternalRecipientDetails = repository.isInternalRecipientDetailsEnabled()
|
|
)
|
|
} else {
|
|
cursor.orElse(null).ensureClosed()
|
|
state.copy(sharedMedia = null)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun refreshSharedMedia() {
|
|
sharedMediaUpdateTrigger.postValue(Unit)
|
|
}
|
|
|
|
open fun refreshRecipient(): Unit = error("This ViewModel does not support this interaction")
|
|
|
|
abstract fun setMuteUntil(muteUntil: Long)
|
|
|
|
abstract fun unmute()
|
|
|
|
abstract fun block()
|
|
|
|
abstract fun unblock()
|
|
|
|
abstract fun onAddToGroup()
|
|
|
|
abstract fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit)
|
|
|
|
abstract fun revealAllMembers()
|
|
|
|
override fun onCleared() {
|
|
cleared = true
|
|
openedMediaCursors.forEach { it.ensureClosed() }
|
|
store.clear()
|
|
disposable.clear()
|
|
}
|
|
|
|
private fun Cursor?.ensureClosed() {
|
|
if (this != null && !this.isClosed) {
|
|
this.close()
|
|
}
|
|
}
|
|
|
|
open fun initiateGroupUpgrade(): Unit = error("This ViewModel does not support this interaction")
|
|
|
|
private class RecipientSettingsViewModel(
|
|
private val recipientId: RecipientId,
|
|
private val repository: ConversationSettingsRepository
|
|
) : ConversationSettingsViewModel(
|
|
repository,
|
|
SpecificSettingsState.RecipientSettingsState()
|
|
) {
|
|
|
|
private val liveRecipient = Recipient.live(recipientId)
|
|
|
|
init {
|
|
disposable += StoryViewState.getForRecipientId(recipientId).subscribe { storyViewState ->
|
|
store.update { it.copy(storyViewState = storyViewState) }
|
|
}
|
|
|
|
store.update(liveRecipient.liveData) { recipient, state ->
|
|
val isAudioAvailable = (recipient.isRegistered || SignalStore.misc().smsExportPhase.allowSmsFeatures()) &&
|
|
!recipient.isGroup &&
|
|
!recipient.isBlocked &&
|
|
!recipient.isSelf &&
|
|
!recipient.isReleaseNotes
|
|
|
|
state.copy(
|
|
recipient = recipient,
|
|
buttonStripState = ButtonStripPreference.State(
|
|
isVideoAvailable = recipient.registered == RecipientTable.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
|
|
isAudioAvailable = isAudioAvailable,
|
|
isAudioSecure = recipient.registered == RecipientTable.RegisteredState.REGISTERED,
|
|
isMuted = recipient.isMuted,
|
|
isMuteAvailable = !recipient.isSelf,
|
|
isSearchAvailable = true
|
|
),
|
|
disappearingMessagesLifespan = recipient.expiresInSeconds,
|
|
canModifyBlockedState = !recipient.isSelf && RecipientUtil.isBlockable(recipient),
|
|
specificSettingsState = state.requireRecipientSettingsState().copy(
|
|
contactLinkState = when {
|
|
recipient.isSelf || recipient.isReleaseNotes || recipient.isBlocked -> ContactLinkState.NONE
|
|
recipient.isSystemContact -> ContactLinkState.OPEN
|
|
else -> ContactLinkState.ADD
|
|
}
|
|
)
|
|
)
|
|
}
|
|
|
|
repository.getThreadId(recipientId) { threadId ->
|
|
store.update { state ->
|
|
state.copy(threadId = threadId)
|
|
}
|
|
}
|
|
|
|
if (recipientId != Recipient.self().id) {
|
|
repository.getGroupsInCommon(recipientId) { groupsInCommon ->
|
|
store.update { state ->
|
|
val recipientSettings = state.requireRecipientSettingsState()
|
|
val canShowMore = !recipientSettings.groupsInCommonExpanded && groupsInCommon.size > 6
|
|
|
|
state.copy(
|
|
specificSettingsState = recipientSettings.copy(
|
|
allGroupsInCommon = groupsInCommon,
|
|
groupsInCommon = if (!canShowMore) groupsInCommon else groupsInCommon.take(5),
|
|
canShowMoreGroupsInCommon = canShowMore
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
repository.hasGroups { hasGroups ->
|
|
store.update { state ->
|
|
val recipientSettings = state.requireRecipientSettingsState()
|
|
state.copy(
|
|
specificSettingsState = recipientSettings.copy(
|
|
selfHasGroups = hasGroups
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
repository.getIdentity(recipientId) { identityRecord ->
|
|
store.update { state ->
|
|
state.copy(specificSettingsState = state.requireRecipientSettingsState().copy(identityRecord = identityRecord))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onAddToGroup() {
|
|
repository.getGroupMembership(recipientId) {
|
|
internalEvents.postValue(ConversationSettingsEvent.AddToAGroup(recipientId, it))
|
|
}
|
|
}
|
|
|
|
override fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit) {
|
|
}
|
|
|
|
override fun revealAllMembers() {
|
|
store.update { state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireRecipientSettingsState().copy(
|
|
groupsInCommon = state.requireRecipientSettingsState().allGroupsInCommon,
|
|
groupsInCommonExpanded = true,
|
|
canShowMoreGroupsInCommon = false
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun refreshRecipient() {
|
|
repository.refreshRecipient(recipientId)
|
|
}
|
|
|
|
override fun setMuteUntil(muteUntil: Long) {
|
|
repository.setMuteUntil(recipientId, muteUntil)
|
|
}
|
|
|
|
override fun unmute() {
|
|
repository.setMuteUntil(recipientId, 0)
|
|
}
|
|
|
|
override fun block() {
|
|
repository.block(recipientId)
|
|
}
|
|
|
|
override fun unblock() {
|
|
repository.unblock(recipientId)
|
|
}
|
|
}
|
|
|
|
private class GroupSettingsViewModel(
|
|
private val groupId: GroupId,
|
|
private val repository: ConversationSettingsRepository
|
|
) : ConversationSettingsViewModel(repository, SpecificSettingsState.GroupSettingsState(groupId)) {
|
|
|
|
private val liveGroup = LiveGroup(groupId)
|
|
|
|
init {
|
|
disposable += repository.getStoryViewState(groupId).subscribe { storyViewState ->
|
|
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 ->
|
|
state.copy(
|
|
recipient = recipient,
|
|
buttonStripState = ButtonStripPreference.State(
|
|
isVideoAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive,
|
|
isAudioAvailable = false,
|
|
isAudioSecure = recipient.isPushV2Group,
|
|
isMuted = recipient.isMuted,
|
|
isMuteAvailable = true,
|
|
isSearchAvailable = true,
|
|
isAddToStoryAvailable = recipient.isPushV2Group && !recipient.isBlocked && isActive
|
|
),
|
|
canModifyBlockedState = RecipientUtil.isBlockable(recipient),
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
legacyGroupState = getLegacyGroupState(recipient)
|
|
)
|
|
)
|
|
}
|
|
|
|
repository.getThreadId(groupId) { threadId ->
|
|
store.update { state ->
|
|
state.copy(threadId = threadId)
|
|
}
|
|
}
|
|
|
|
store.update(liveGroup.selfCanEditGroupAttributes()) { selfCanEditGroupAttributes, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
canEditGroupAttributes = selfCanEditGroupAttributes
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.isSelfAdmin) { isSelfAdmin, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
isSelfAdmin = isSelfAdmin
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.expireMessages) { expireMessages, state ->
|
|
state.copy(
|
|
disappearingMessagesLifespan = expireMessages
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.selfCanAddMembers()) { canAddMembers, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
canAddToGroup = canAddMembers
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.fullMembers) { fullMembers, state ->
|
|
val groupState = state.requireGroupSettingsState()
|
|
val canShowMore = !groupState.groupMembersExpanded && fullMembers.size > 6
|
|
|
|
state.copy(
|
|
specificSettingsState = groupState.copy(
|
|
allMembers = fullMembers,
|
|
members = if (!canShowMore) fullMembers else fullMembers.take(5),
|
|
canShowMoreGroupMembers = canShowMore
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.isAnnouncementGroup) { announcementGroup, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
isAnnouncementGroup = announcementGroup
|
|
)
|
|
)
|
|
}
|
|
|
|
val isMessageRequestAccepted: LiveData<Boolean> = LiveDataUtil.mapAsync(liveGroup.groupRecipient) { r -> repository.isMessageRequestAccepted(r) }
|
|
val descriptionState: LiveData<DescriptionState> = LiveDataUtil.combineLatest(liveGroup.description, isMessageRequestAccepted, ::DescriptionState)
|
|
|
|
store.update(descriptionState) { d, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
groupDescription = d.description,
|
|
groupDescriptionShouldLinkify = d.canLinkify,
|
|
groupDescriptionLoaded = true
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.isActive) { isActive, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
canLeave = isActive && groupId.isPush
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.title) { title, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
groupTitle = title,
|
|
groupTitleLoaded = true
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(liveGroup.groupLink) { groupLink, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
groupLinkEnabled = groupLink.isEnabled
|
|
)
|
|
)
|
|
}
|
|
|
|
store.update(repository.getMembershipCountDescription(liveGroup)) { description, state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
membershipCountDescription = description
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private fun getLegacyGroupState(recipient: Recipient): LegacyGroupPreference.State {
|
|
val showLegacyInfo = recipient.requireGroupId().isV1
|
|
|
|
return if (showLegacyInfo && recipient.participantIds.size > FeatureFlags.groupLimits().hardLimit) {
|
|
LegacyGroupPreference.State.TOO_LARGE
|
|
} else if (showLegacyInfo) {
|
|
LegacyGroupPreference.State.UPGRADE
|
|
} else if (groupId.isMms) {
|
|
LegacyGroupPreference.State.MMS_WARNING
|
|
} else {
|
|
LegacyGroupPreference.State.NONE
|
|
}
|
|
}
|
|
|
|
override fun onAddToGroup() {
|
|
repository.getGroupCapacity(groupId) { capacityResult ->
|
|
if (capacityResult.getRemainingCapacity() > 0) {
|
|
|
|
internalEvents.postValue(
|
|
ConversationSettingsEvent.AddMembersToGroup(
|
|
groupId,
|
|
capacityResult.getSelectionWarning(),
|
|
capacityResult.getSelectionLimit(),
|
|
capacityResult.isAnnouncementGroup,
|
|
capacityResult.getMembersWithoutSelf()
|
|
)
|
|
)
|
|
} else {
|
|
internalEvents.postValue(ConversationSettingsEvent.ShowGroupHardLimitDialog)
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun onAddToGroupComplete(selected: List<RecipientId>, onComplete: () -> Unit) {
|
|
repository.addMembers(groupId, selected) {
|
|
ThreadUtil.runOnMain { onComplete() }
|
|
|
|
when (it) {
|
|
is GroupAddMembersResult.Success -> {
|
|
if (it.newMembersInvited.isNotEmpty()) {
|
|
internalEvents.postValue(ConversationSettingsEvent.ShowGroupInvitesSentDialog(it.newMembersInvited))
|
|
}
|
|
|
|
if (it.numberOfMembersAdded > 0) {
|
|
internalEvents.postValue(ConversationSettingsEvent.ShowMembersAdded(it.numberOfMembersAdded))
|
|
}
|
|
}
|
|
is GroupAddMembersResult.Failure -> internalEvents.postValue(ConversationSettingsEvent.ShowAddMembersToGroupError(it.reason))
|
|
}
|
|
}
|
|
}
|
|
|
|
override fun revealAllMembers() {
|
|
store.update { state ->
|
|
state.copy(
|
|
specificSettingsState = state.requireGroupSettingsState().copy(
|
|
members = state.requireGroupSettingsState().allMembers,
|
|
groupMembersExpanded = true,
|
|
canShowMoreGroupMembers = false
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
override fun setMuteUntil(muteUntil: Long) {
|
|
repository.setMuteUntil(groupId, muteUntil)
|
|
}
|
|
|
|
override fun unmute() {
|
|
repository.setMuteUntil(groupId, 0)
|
|
}
|
|
|
|
override fun block() {
|
|
repository.block(groupId)
|
|
}
|
|
|
|
override fun unblock() {
|
|
repository.unblock(groupId)
|
|
}
|
|
|
|
override fun initiateGroupUpgrade() {
|
|
repository.getExternalPossiblyMigratedGroupRecipientId(groupId) {
|
|
internalEvents.postValue(ConversationSettingsEvent.InitiateGroupMigration(it))
|
|
}
|
|
}
|
|
}
|
|
|
|
class Factory(
|
|
private val recipientId: RecipientId? = null,
|
|
private val groupId: GroupId? = null,
|
|
private val repository: ConversationSettingsRepository,
|
|
) : ViewModelProvider.Factory {
|
|
|
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
|
return requireNotNull(
|
|
modelClass.cast(
|
|
when {
|
|
recipientId != null -> RecipientSettingsViewModel(recipientId, repository)
|
|
groupId != null -> GroupSettingsViewModel(groupId, repository)
|
|
else -> error("One of RecipientId or GroupId required.")
|
|
}
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
private class DescriptionState(
|
|
val description: String?,
|
|
val canLinkify: Boolean
|
|
)
|
|
}
|