package org.thoughtcrime.securesms.conversation; import android.app.Application; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; import androidx.fragment.app.FragmentManager; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import com.annimon.stream.Stream; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.database.GroupTable; import org.thoughtcrime.securesms.database.model.GroupRecord; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.groups.GroupChangeBusyException; import org.thoughtcrime.securesms.groups.GroupChangeFailedException; import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.groups.GroupManager; import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil; import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason; import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment; import org.thoughtcrime.securesms.groups.v2.GroupBlockJoinRequestResult; import org.thoughtcrime.securesms.groups.v2.GroupManagementRepository; import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient; import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.AsynchronousCallback; import org.signal.core.util.concurrent.SimpleTask; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.io.IOException; import java.util.Collections; import java.util.List; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; final class ConversationGroupViewModel extends ViewModel { private final MutableLiveData liveRecipient; private final LiveData groupActiveState; private final LiveData selfMembershipLevel; private final LiveData actionableRequestingMembers; private final LiveData reviewState; private final LiveData> gv1MigrationSuggestions; private final GroupManagementRepository groupManagementRepository; private boolean firstTimeInviteFriendsTriggered; private ConversationGroupViewModel() { this.liveRecipient = new MutableLiveData<>(); this.groupManagementRepository = new GroupManagementRepository(); LiveData groupRecord = LiveDataUtil.mapAsync(liveRecipient, ConversationGroupViewModel::getGroupRecordForRecipient); LiveData> duplicates = LiveDataUtil.mapAsync(groupRecord, record -> { if (record != null && record.isV2Group()) { return Stream.of(ReviewUtil.getDuplicatedRecipients(record.getId().requireV2())) .map(ReviewRecipient::getRecipient) .toList(); } else { return Collections.emptyList(); } }); this.groupActiveState = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToGroupActiveState)); this.selfMembershipLevel = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToSelfMembershipLevel)); this.actionableRequestingMembers = Transformations.distinctUntilChanged(Transformations.map(groupRecord, ConversationGroupViewModel::mapToActionableRequestingMemberCount)); this.gv1MigrationSuggestions = Transformations.distinctUntilChanged(LiveDataUtil.mapAsync(groupRecord, ConversationGroupViewModel::mapToGroupV1MigrationSuggestions)); this.reviewState = LiveDataUtil.combineLatest(groupRecord, duplicates, (record, dups) -> dups.isEmpty() ? ReviewState.EMPTY : new ReviewState(record.getId().requireV2(), dups.get(0), dups.size())); } void onRecipientChange(Recipient recipient) { liveRecipient.setValue(recipient); } void onSuggestedMembersBannerDismissed(@NonNull GroupId groupId) { SignalExecutors.BOUNDED.execute(() -> { if (groupId.isV2()) { SignalDatabase.groups().removeUnmigratedV1Members(groupId.requireV2()); liveRecipient.postValue(liveRecipient.getValue()); } }); } /** * The number of pending group join requests that can be actioned by this client. */ LiveData getActionableRequestingMembers() { return actionableRequestingMembers; } LiveData getGroupActiveState() { return groupActiveState; } LiveData getSelfMemberLevel() { return selfMembershipLevel; } public LiveData getReviewState() { return reviewState; } @NonNull LiveData> getGroupV1MigrationSuggestions() { return gv1MigrationSuggestions; } boolean isNonAdminInAnnouncementGroup() { ConversationMemberLevel level = selfMembershipLevel.getValue(); return level != null && level.getMemberLevel() != GroupTable.MemberLevel.ADMINISTRATOR && level.isAnnouncementGroup(); } private static @Nullable GroupRecord getGroupRecordForRecipient(@Nullable Recipient recipient) { if (recipient != null && recipient.isGroup()) { Application context = ApplicationDependencies.getApplication(); GroupTable groupDatabase = SignalDatabase.groups(); return groupDatabase.getGroup(recipient.getId()).orElse(null); } else { return null; } } private static int mapToActionableRequestingMemberCount(@Nullable GroupRecord record) { if (record != null && record.isV2Group() && record.memberLevel(Recipient.self()) == GroupTable.MemberLevel.ADMINISTRATOR) { return record.requireV2GroupProperties() .getDecryptedGroup() .getRequestingMembersCount(); } else { return 0; } } private static GroupActiveState mapToGroupActiveState(@Nullable GroupRecord record) { if (record == null) { return null; } return new GroupActiveState(record.isActive(), record.isV2Group()); } private static ConversationMemberLevel mapToSelfMembershipLevel(@Nullable GroupRecord record) { if (record == null) { return null; } return new ConversationMemberLevel(record.memberLevel(Recipient.self()), record.isAnnouncementGroup()); } @WorkerThread private static List mapToGroupV1MigrationSuggestions(@Nullable GroupRecord record) { if (record == null || !record.isV2Group() || !record.isActive() || record.isPendingMember(Recipient.self())) { return Collections.emptyList(); } return Stream.of(record.getUnmigratedV1Members()) .filterNot(m -> record.getMembers().contains(m)) .map(Recipient::resolved) .filter(GroupsV1MigrationUtil::isAutoMigratable) .map(Recipient::getId) .toList(); } public static void onCancelJoinRequest(@NonNull Recipient recipient, @NonNull AsynchronousCallback.WorkerThread callback) { SignalExecutors.UNBOUNDED.execute(() -> { if (!recipient.isPushV2Group()) { throw new AssertionError(); } try { GroupManager.cancelJoinRequest(ApplicationDependencies.getApplication(), recipient.getGroupId().get().requireV2()); callback.onComplete(null); } catch (GroupChangeFailedException | GroupChangeBusyException | IOException e) { callback.onError(GroupChangeFailureReason.fromException(e)); } }); } void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) { if (firstTimeInviteFriendsTriggered) { return; } firstTimeInviteFriendsTriggered = true; SimpleTask.run(() -> SignalDatabase.groups() .requireGroup(groupId) .getMembers().equals(Collections.singletonList(Recipient.self().getId())), justSelf -> { if (justSelf) { inviteFriends(supportFragmentManager, groupId); } } ); } void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) { GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId); } public Single blockJoinRequests(@NonNull Recipient groupRecipient, @NonNull Recipient recipient) { return groupManagementRepository.blockJoinRequests(groupRecipient.requireGroupId().requireV2(), recipient) .observeOn(AndroidSchedulers.mainThread()); } static final class ReviewState { private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0); private final GroupId.V2 groupId; private final Recipient recipient; private final int count; ReviewState(@Nullable GroupId.V2 groupId, @NonNull Recipient recipient, int count) { this.groupId = groupId; this.recipient = recipient; this.count = count; } public @Nullable GroupId.V2 getGroupId() { return groupId; } public @NonNull Recipient getRecipient() { return recipient; } public int getCount() { return count; } } static final class GroupActiveState { private final boolean isActive; private final boolean isActiveV2; public GroupActiveState(boolean isActive, boolean isV2) { this.isActive = isActive; this.isActiveV2 = isActive && isV2; } public boolean isActiveGroup() { return isActive; } public boolean isActiveV2Group() { return isActiveV2; } } static final class ConversationMemberLevel { private final GroupTable.MemberLevel memberLevel; private final boolean isAnnouncementGroup; private ConversationMemberLevel(GroupTable.MemberLevel memberLevel, boolean isAnnouncementGroup) { this.memberLevel = memberLevel; this.isAnnouncementGroup = isAnnouncementGroup; } public @NonNull GroupTable.MemberLevel getMemberLevel() { return memberLevel; } public boolean isAnnouncementGroup() { return isAnnouncementGroup; } } static class Factory extends ViewModelProvider.NewInstanceFactory { @Override public @NonNull T create(@NonNull Class modelClass) { //noinspection ConstantConditions return modelClass.cast(new ConversationGroupViewModel()); } } }