238 lines
12 KiB
Java
238 lines
12 KiB
Java
package org.thoughtcrime.securesms.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.storageservice.protos.groups.AccessControl;
|
|
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
|
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
|
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
|
|
import org.thoughtcrime.securesms.groups.v2.GroupLinkPassword;
|
|
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
|
|
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
|
import org.thoughtcrime.securesms.recipients.RecipientId;
|
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
|
|
|
import java.util.Collections;
|
|
import java.util.Comparator;
|
|
import java.util.List;
|
|
|
|
public final class LiveGroup {
|
|
|
|
private static final Comparator<GroupMemberEntry.FullMember> LOCAL_FIRST = (m1, m2) -> Boolean.compare(m2.getMember().isLocalNumber(), m1.getMember().isLocalNumber());
|
|
private static final Comparator<GroupMemberEntry.FullMember> ADMIN_FIRST = (m1, m2) -> Boolean.compare(m2.isAdmin(), m1.isAdmin());
|
|
private static final Comparator<GroupMemberEntry.FullMember> HAS_DISPLAY_NAME = (m1, m2) -> Boolean.compare(m2.getMember().hasAUserSetDisplayName(ApplicationDependencies.getApplication()), m1.getMember().hasAUserSetDisplayName(ApplicationDependencies.getApplication()));
|
|
private static final Comparator<GroupMemberEntry.FullMember> ALPHABETICAL = (m1, m2) -> m1.getMember().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(m2.getMember().getDisplayName(ApplicationDependencies.getApplication()));
|
|
private static final Comparator<? super GroupMemberEntry.FullMember> MEMBER_ORDER = ComparatorCompat.chain(LOCAL_FIRST)
|
|
.thenComparing(ADMIN_FIRST)
|
|
.thenComparing(HAS_DISPLAY_NAME)
|
|
.thenComparing(ALPHABETICAL);
|
|
|
|
private final GroupDatabase groupDatabase;
|
|
private final LiveData<Recipient> recipient;
|
|
private final LiveData<GroupDatabase.GroupRecord> groupRecord;
|
|
private final LiveData<List<GroupMemberEntry.FullMember>> fullMembers;
|
|
private final LiveData<List<GroupMemberEntry.RequestingMember>> requestingMembers;
|
|
private final LiveData<GroupLinkUrlAndStatus> groupLink;
|
|
|
|
public LiveGroup(@NonNull GroupId groupId) {
|
|
Context context = ApplicationDependencies.getApplication();
|
|
MutableLiveData<LiveRecipient> liveRecipient = new MutableLiveData<>();
|
|
|
|
this.groupDatabase = DatabaseFactory.getGroupDatabase(context);
|
|
this.recipient = Transformations.switchMap(liveRecipient, LiveRecipient::getLiveData);
|
|
this.groupRecord = LiveDataUtil.filterNotNull(LiveDataUtil.mapAsync(recipient, groupRecipient -> groupDatabase.getGroup(groupRecipient.getId()).orNull()));
|
|
this.fullMembers = mapToFullMembers(this.groupRecord);
|
|
this.requestingMembers = mapToRequestingMembers(this.groupRecord);
|
|
|
|
if (groupId.isV2()) {
|
|
LiveData<GroupDatabase.V2GroupProperties> v2Properties = Transformations.map(this.groupRecord, GroupDatabase.GroupRecord::requireV2GroupProperties);
|
|
this.groupLink = Transformations.map(v2Properties, g -> {
|
|
DecryptedGroup group = g.getDecryptedGroup();
|
|
AccessControl.AccessRequired addFromInviteLink = group.getAccessControl().getAddFromInviteLink();
|
|
|
|
if (group.getInviteLinkPassword().isEmpty()) {
|
|
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.externalGroup(context, groupId).live()));
|
|
}
|
|
|
|
protected static LiveData<List<GroupMemberEntry.FullMember>> mapToFullMembers(@NonNull LiveData<GroupDatabase.GroupRecord> 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<List<GroupMemberEntry.RequestingMember>> mapToRequestingMembers(@NonNull LiveData<GroupDatabase.GroupRecord> groupRecord) {
|
|
return LiveDataUtil.mapAsync(groupRecord,
|
|
g -> {
|
|
if (!g.isV2Group()) {
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
boolean selfAdmin = g.isAdmin(Recipient.self());
|
|
List<DecryptedRequestingMember> requestingMembersList = g.requireV2GroupProperties().getDecryptedGroup().getRequestingMembersList();
|
|
|
|
return Stream.of(requestingMembersList)
|
|
.map(requestingMember -> {
|
|
Recipient recipient = Recipient.externalPush(ApplicationDependencies.getApplication(), UuidUtil.fromByteString(requestingMember.getUuid()), null, false);
|
|
return new GroupMemberEntry.RequestingMember(recipient, selfAdmin);
|
|
})
|
|
.toList();
|
|
});
|
|
}
|
|
|
|
public LiveData<String> 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<Recipient> getGroupRecipient() {
|
|
return recipient;
|
|
}
|
|
|
|
public LiveData<Boolean> isSelfAdmin() {
|
|
return Transformations.map(groupRecord, g -> g.isAdmin(Recipient.self()));
|
|
}
|
|
|
|
public LiveData<Boolean> isActive() {
|
|
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::isActive);
|
|
}
|
|
|
|
public LiveData<Boolean> getRecipientIsAdmin(@NonNull RecipientId recipientId) {
|
|
return LiveDataUtil.mapAsync(groupRecord, g -> g.isAdmin(Recipient.resolved(recipientId)));
|
|
}
|
|
|
|
public LiveData<Integer> getPendingMemberCount() {
|
|
return Transformations.map(groupRecord, g -> g.isV2Group() ? g.requireV2GroupProperties().getDecryptedGroup().getPendingMembersCount() : 0);
|
|
}
|
|
|
|
public LiveData<Integer> getPendingAndRequestingMemberCount() {
|
|
return Transformations.map(groupRecord, g -> {
|
|
if (g.isV2Group()) {
|
|
DecryptedGroup decryptedGroup = g.requireV2GroupProperties().getDecryptedGroup();
|
|
|
|
return decryptedGroup.getPendingMembersCount() + decryptedGroup.getRequestingMembersCount();
|
|
}
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
public LiveData<GroupAccessControl> getMembershipAdditionAccessControl() {
|
|
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getMembershipAdditionAccessControl);
|
|
}
|
|
|
|
public LiveData<GroupAccessControl> getAttributesAccessControl() {
|
|
return Transformations.map(groupRecord, GroupDatabase.GroupRecord::getAttributesAccessControl);
|
|
}
|
|
|
|
public LiveData<List<GroupMemberEntry.FullMember>> getNonAdminFullMembers() {
|
|
return Transformations.map(fullMembers,
|
|
members -> Stream.of(members)
|
|
.filterNot(GroupMemberEntry.FullMember::isAdmin)
|
|
.toList());
|
|
}
|
|
|
|
public LiveData<List<GroupMemberEntry.FullMember>> getFullMembers() {
|
|
return fullMembers;
|
|
}
|
|
|
|
public LiveData<List<GroupMemberEntry.RequestingMember>> getRequestingMembers() {
|
|
return requestingMembers;
|
|
}
|
|
|
|
public LiveData<Integer> getExpireMessages() {
|
|
return Transformations.map(recipient, Recipient::getExpireMessages);
|
|
}
|
|
|
|
public LiveData<Boolean> selfCanEditGroupAttributes() {
|
|
return LiveDataUtil.combineLatest(selfMemberLevel(), getAttributesAccessControl(), LiveGroup::applyAccessControl);
|
|
}
|
|
|
|
public LiveData<Boolean> selfCanAddMembers() {
|
|
return LiveDataUtil.combineLatest(selfMemberLevel(), getMembershipAdditionAccessControl(), LiveGroup::applyAccessControl);
|
|
}
|
|
|
|
/**
|
|
* A string representing the count of full members and pending members if > 0.
|
|
*/
|
|
public LiveData<String> 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<String> getFullMembershipCountDescription(@NonNull Resources resources) {
|
|
return Transformations.map(getFullMembers(), fullMembers -> getMembershipDescription(resources, 0, fullMembers.size()));
|
|
}
|
|
|
|
public LiveData<GroupDatabase.MemberLevel> getMemberLevel(@NonNull Recipient recipient) {
|
|
return Transformations.map(groupRecord, g -> g.memberLevel(recipient));
|
|
}
|
|
|
|
private static String getMembershipDescription(@NonNull Resources resources, int invitedCount, int fullMemberCount) {
|
|
return invitedCount > 0 ? resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, fullMemberCount,
|
|
fullMemberCount, invitedCount)
|
|
: resources.getQuantityString(R.plurals.MessageRequestProfileView_members, fullMemberCount,
|
|
fullMemberCount);
|
|
}
|
|
|
|
private LiveData<GroupDatabase.MemberLevel> selfMemberLevel() {
|
|
return Transformations.map(groupRecord, g -> g.memberLevel(Recipient.self()));
|
|
}
|
|
|
|
private static boolean applyAccessControl(@NonNull GroupDatabase.MemberLevel memberLevel, @NonNull GroupAccessControl rights) {
|
|
switch (rights) {
|
|
case ALL_MEMBERS: return memberLevel.isInGroup();
|
|
case ONLY_ADMINS: return memberLevel == GroupDatabase.MemberLevel.ADMINISTRATOR;
|
|
case NO_ONE : return false;
|
|
default: throw new AssertionError();
|
|
}
|
|
}
|
|
|
|
public LiveData<GroupLinkUrlAndStatus> getGroupLink() {
|
|
return groupLink;
|
|
}
|
|
} |