package org.tm.archive.groups; import android.content.Context; import android.content.res.Resources; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Transformations; import com.annimon.stream.ComparatorCompat; import com.annimon.stream.Stream; import org.signal.core.util.concurrent.SignalExecutors; import org.signal.storageservice.protos.groups.AccessControl; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.tm.archive.R; import org.tm.archive.database.GroupTable; import org.tm.archive.database.SignalDatabase; import org.tm.archive.database.model.GroupRecord; import org.tm.archive.dependencies.ApplicationDependencies; import org.tm.archive.groups.ui.GroupMemberEntry; import org.tm.archive.groups.v2.GroupInviteLinkUrl; import org.tm.archive.groups.v2.GroupLinkUrlAndStatus; import org.tm.archive.recipients.LiveRecipient; import org.tm.archive.recipients.Recipient; import org.tm.archive.recipients.RecipientId; import org.tm.archive.util.livedata.LiveDataUtil; import org.whispersystems.signalservice.api.push.ServiceId; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Set; public final class LiveGroup { private static final Comparator LOCAL_FIRST = (m1, m2) -> Boolean.compare(m2.getMember().isSelf(), m1.getMember().isSelf()); private static final Comparator ADMIN_FIRST = (m1, m2) -> Boolean.compare(m2.isAdmin(), m1.isAdmin()); private static final Comparator HAS_DISPLAY_NAME = (m1, m2) -> Boolean.compare(m2.getMember().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), m1.getMember().hasAUserSetDisplayName(ApplicationDependencies.getApplication())); private static final Comparator ALPHABETICAL = (m1, m2) -> m1.getMember().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(m2.getMember().getDisplayName(ApplicationDependencies.getApplication())); private static final Comparator MEMBER_ORDER = ComparatorCompat.chain(LOCAL_FIRST) .thenComparing(ADMIN_FIRST) .thenComparing(HAS_DISPLAY_NAME) .thenComparing(ALPHABETICAL); private final GroupTable groupDatabase; private final LiveData recipient; private final LiveData groupRecord; private final LiveData> fullMembers; private final LiveData> requestingMembers; private final LiveData groupLink; public LiveGroup(@NonNull GroupId groupId) { Context context = ApplicationDependencies.getApplication(); MutableLiveData liveRecipient = new MutableLiveData<>(); this.groupDatabase = SignalDatabase.groups(); this.recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData); this.groupRecord = LiveDataUtil.filterNotNull(LiveDataUtil.mapAsync(recipient, groupRecipient -> groupDatabase.getGroup(groupRecipient.getId()).orElse(null))); this.fullMembers = mapToFullMembers(this.groupRecord); this.requestingMembers = mapToRequestingMembers(this.groupRecord); if (groupId.isV2()) { LiveData v2Properties = Transformations.map(this.groupRecord, GroupRecord::requireV2GroupProperties); this.groupLink = Transformations.map(v2Properties, g -> { DecryptedGroup group = g.getDecryptedGroup(); AccessControl.AccessRequired addFromInviteLink = group.accessControl != null ? group.accessControl.addFromInviteLink : new AccessControl().addFromInviteLink; if (group.inviteLinkPassword.size() == 0) { return GroupLinkUrlAndStatus.NONE; } boolean enabled = addFromInviteLink == AccessControl.AccessRequired.ANY || addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR; boolean adminApproval = addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR; String url = GroupInviteLinkUrl.forGroup(g.getGroupMasterKey(), group) .getUrl(); return new GroupLinkUrlAndStatus(enabled, adminApproval, url); }); } else { this.groupLink = new MutableLiveData<>(GroupLinkUrlAndStatus.NONE); } SignalExecutors.BOUNDED.execute(() -> liveRecipient.postValue(Recipient.externalGroupExact(groupId).live())); } protected static LiveData> mapToFullMembers(@NonNull LiveData groupRecord) { return LiveDataUtil.mapAsync(groupRecord, g -> Stream.of(g.getMembers()) .map(m -> { Recipient recipient = Recipient.resolved(m); return new GroupMemberEntry.FullMember(recipient, g.isAdmin(recipient)); }) .sorted(MEMBER_ORDER) .toList()); } protected static LiveData> mapToRequestingMembers(@NonNull LiveData groupRecord) { return LiveDataUtil.mapAsync(groupRecord, g -> { if (!g.isV2Group()) { return Collections.emptyList(); } boolean selfAdmin = g.isAdmin(Recipient.self()); List requestingMembersList = g.requireV2GroupProperties().getDecryptedGroup().requestingMembers; return Stream.of(requestingMembersList) .map(requestingMember -> { Recipient recipient = Recipient.externalPush(ServiceId.parseOrThrow(requestingMember.aciBytes)); return new GroupMemberEntry.RequestingMember(recipient, selfAdmin); }) .toList(); }); } public LiveData getTitle() { return LiveDataUtil.combineLatest(groupRecord, recipient, (groupRecord, recipient) -> { String title = groupRecord.getTitle(); if (!TextUtils.isEmpty(title)) { return title; } return recipient.getDisplayName(ApplicationDependencies.getApplication()); }); } public LiveData getDescription() { return Transformations.map(groupRecord, GroupRecord::getDescription); } public LiveData isAnnouncementGroup() { return Transformations.map(groupRecord, GroupRecord::isAnnouncementGroup); } public LiveData getGroupRecipient() { return recipient; } public LiveData isSelfAdmin() { return Transformations.map(groupRecord, g -> g.isAdmin(Recipient.self())); } public LiveData> getBannedMembers() { return Transformations.map(groupRecord, g -> g.isV2Group() ? g.requireV2GroupProperties().getBannedMembers() : Collections.emptySet()); } public LiveData isActive() { return Transformations.map(groupRecord, GroupRecord::isActive); } public LiveData getRecipientIsAdmin(@NonNull RecipientId recipientId) { return LiveDataUtil.mapAsync(groupRecord, g -> g.isAdmin(Recipient.resolved(recipientId))); } public LiveData getPendingMemberCount() { return Transformations.map(groupRecord, g -> g.isV2Group() ? g.requireV2GroupProperties().getDecryptedGroup().pendingMembers.size() : 0); } public LiveData getPendingAndRequestingMemberCount() { return Transformations.map(groupRecord, g -> { if (g.isV2Group()) { DecryptedGroup decryptedGroup = g.requireV2GroupProperties().getDecryptedGroup(); return decryptedGroup.pendingMembers.size() + decryptedGroup.requestingMembers.size(); } return 0; }); } public LiveData getMembershipAdditionAccessControl() { return Transformations.map(groupRecord, GroupRecord::getMembershipAdditionAccessControl); } public LiveData getAttributesAccessControl() { return Transformations.map(groupRecord, GroupRecord::getAttributesAccessControl); } public LiveData> getNonAdminFullMembers() { return Transformations.map(fullMembers, members -> Stream.of(members) .filterNot(GroupMemberEntry.FullMember::isAdmin) .toList()); } public LiveData> getFullMembers() { return fullMembers; } public LiveData> getRequestingMembers() { return requestingMembers; } public LiveData getExpireMessages() { return Transformations.map(recipient, Recipient::getExpiresInSeconds); } public LiveData selfCanEditGroupAttributes() { return LiveDataUtil.combineLatest(selfMemberLevel(), getAttributesAccessControl(), LiveGroup::applyAccessControl); } public LiveData selfCanAddMembers() { return LiveDataUtil.combineLatest(selfMemberLevel(), getMembershipAdditionAccessControl(), LiveGroup::applyAccessControl); } /** * A string representing the count of full members and pending members if > 0. */ public LiveData getMembershipCountDescription(@NonNull Resources resources) { return LiveDataUtil.combineLatest(getFullMembers(), getPendingMemberCount(), (fullMembers, invitedCount) -> getMembershipDescription(resources, invitedCount, fullMembers.size())); } /** * A string representing the count of full members. */ public LiveData getFullMembershipCountDescription(@NonNull Resources resources) { return Transformations.map(getFullMembers(), fullMembers -> getMembershipDescription(resources, 0, fullMembers.size())); } public LiveData getMemberLevel(@NonNull Recipient recipient) { return Transformations.map(groupRecord, g -> g.memberLevel(recipient)); } private static String getMembershipDescription(@NonNull Resources resources, int invitedCount, int fullMemberCount) { if (invitedCount > 0) { String invited = resources.getQuantityString(R.plurals.MessageRequestProfileView_invited, invitedCount, invitedCount); return resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, fullMemberCount, fullMemberCount, invited); } else { return resources.getQuantityString(R.plurals.MessageRequestProfileView_members, fullMemberCount, fullMemberCount); } } private LiveData selfMemberLevel() { return Transformations.map(groupRecord, g -> g.memberLevel(Recipient.self())); } private static boolean applyAccessControl(@NonNull GroupTable.MemberLevel memberLevel, @NonNull GroupAccessControl rights) { switch (rights) { case ALL_MEMBERS: return memberLevel.isInGroup(); case ONLY_ADMINS: return memberLevel == GroupTable.MemberLevel.ADMINISTRATOR; case NO_ONE : return false; default: throw new AssertionError(); } } public LiveData getGroupLink() { return groupLink; } }