Add universal disappearing messages.

This commit is contained in:
Cody Henthorne
2021-05-18 15:19:33 -04:00
committed by Greyson Parrelli
parent 8c6a88374b
commit defd5e8047
70 changed files with 1513 additions and 251 deletions

View File

@@ -36,11 +36,12 @@ public final class GroupManager {
private static final String TAG = Log.tag(GroupManager.class);
@WorkerThread
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
@NonNull Set<Recipient> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms)
public static @NonNull GroupActionResult createGroup(@NonNull Context context,
@NonNull Set<Recipient> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
int disappearingMessagesTimer)
throws GroupChangeBusyException, GroupChangeFailedException, IOException
{
boolean shouldAttemptToCreateV2 = !mms && !SignalStore.internalValues().gv2DoNotCreateGv2Groups();
@@ -49,7 +50,7 @@ public final class GroupManager {
if (shouldAttemptToCreateV2) {
try {
try (GroupManagerV2.GroupCreator groupCreator = new GroupManagerV2(context).create()) {
return groupCreator.createGroup(memberIds, name, avatar);
return groupCreator.createGroup(memberIds, name, avatar, disappearingMessagesTimer);
}
} catch (MembershipNotSuitableForV2Exception e) {
Log.w(TAG, "Attempted to make a GV2, but membership was not suitable, falling back to GV1", e);

View File

@@ -244,23 +244,15 @@ final class GroupManagerV2 {
@WorkerThread
@NonNull GroupManager.GroupActionResult createGroup(@NonNull Collection<RecipientId> members,
@Nullable String name,
@Nullable byte[] avatar)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{
return createGroup(name, avatar, members);
}
@WorkerThread
private @NonNull GroupManager.GroupActionResult createGroup(@Nullable String name,
@Nullable byte[] avatar,
@NonNull Collection<RecipientId> members)
@Nullable byte[] avatar,
int disappearingMessagesTimer)
throws GroupChangeFailedException, IOException, MembershipNotSuitableForV2Exception
{
GroupSecretParams groupSecretParams = GroupSecretParams.generate();
DecryptedGroup decryptedGroup;
try {
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, 0);
decryptedGroup = createGroupOnServer(groupSecretParams, name, avatar, members, Member.Role.DEFAULT, disappearingMessagesTimer);
} catch (GroupAlreadyExistsException e) {
throw new GroupChangeFailedException(e);
}

View File

@@ -17,7 +17,7 @@ public enum GroupChangeFailureReason {
NETWORK,
OTHER;
public static @NonNull GroupChangeFailureReason fromException(@NonNull Exception e) {
public static @NonNull GroupChangeFailureReason fromException(@NonNull Throwable e) {
if (e instanceof MembershipNotSuitableForV2Exception) return GroupChangeFailureReason.NOT_CAPABLE;
if (e instanceof IOException) return GroupChangeFailureReason.NETWORK;
if (e instanceof GroupNotAMemberException) return GroupChangeFailureReason.NOT_A_MEMBER;

View File

@@ -12,6 +12,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -30,8 +31,10 @@ import com.dd.CircularProgressButton;
import org.signal.core.util.EditTextUtil;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.settings.app.privacy.expire.ExpireTimerSettingsFragment;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.groups.ui.creategroup.dialogs.NonGv2MemberDialog;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity;
import org.thoughtcrime.securesms.mediasend.AvatarSelectionBottomSheetDialogFragment;
import org.thoughtcrime.securesms.mediasend.Media;
@@ -40,7 +43,9 @@ import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.util.BitmapUtil;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.text.AfterTextChanged;
@@ -54,6 +59,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
private static final int AVATAR_PLACEHOLDER_INSET_DP = 18;
private static final short REQUEST_CODE_AVATAR = 27621;
private static final short REQUEST_DISAPPEARING_TIMER = 28621;
private CircularProgressButton create;
private Callback callback;
@@ -61,6 +67,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
private Drawable avatarPlaceholder;
private EditText name;
private Toolbar toolbar;
private View disappearingMessagesRow;
@Override
public void onAttach(@NonNull Context context) {
@@ -83,17 +90,19 @@ public class AddGroupDetailsFragment extends LoggingFragment {
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
create = view.findViewById(R.id.create);
name = view.findViewById(R.id.name);
toolbar = view.findViewById(R.id.toolbar);
create = view.findViewById(R.id.create);
name = view.findViewById(R.id.name);
toolbar = view.findViewById(R.id.toolbar);
disappearingMessagesRow = view.findViewById(R.id.group_disappearing_messages_row);
setCreateEnabled(false, false);
GroupMemberListView members = view.findViewById(R.id.member_list);
ImageView avatar = view.findViewById(R.id.group_avatar);
View mmsWarning = view.findViewById(R.id.mms_warning);
LearnMoreTextView gv2Warning = view.findViewById(R.id.gv2_warning);
View addLater = view.findViewById(R.id.add_later);
GroupMemberListView members = view.findViewById(R.id.member_list);
ImageView avatar = view.findViewById(R.id.group_avatar);
View mmsWarning = view.findViewById(R.id.mms_warning);
LearnMoreTextView gv2Warning = view.findViewById(R.id.gv2_warning);
View addLater = view.findViewById(R.id.add_later);
TextView disappearingMessageValue = view.findViewById(R.id.group_disappearing_messages_value);
avatarPlaceholder = VectorDrawableCompat.create(getResources(), R.drawable.ic_camera_outline_32_ultramarine, requireActivity().getTheme());
@@ -115,6 +124,7 @@ public class AddGroupDetailsFragment extends LoggingFragment {
});
viewModel.getCanSubmitForm().observe(getViewLifecycleOwner(), isFormValid -> setCreateEnabled(isFormValid, true));
viewModel.getIsMms().observe(getViewLifecycleOwner(), isMms -> {
disappearingMessagesRow.setVisibility(isMms ? View.GONE : View.VISIBLE);
mmsWarning.setVisibility(isMms ? View.VISIBLE : View.GONE);
name.setHint(isMms ? R.string.AddGroupDetailsFragment__group_name_optional : R.string.AddGroupDetailsFragment__group_name_required);
toolbar.setTitle(isMms ? R.string.AddGroupDetailsFragment__create_group : R.string.AddGroupDetailsFragment__name_this_group);
@@ -143,6 +153,11 @@ public class AddGroupDetailsFragment extends LoggingFragment {
}
});
viewModel.getDisappearingMessagesTimer().observe(getViewLifecycleOwner(), timer -> disappearingMessageValue.setText(ExpirationUtil.getExpirationDisplayValue(requireContext(), timer)));
disappearingMessagesRow.setOnClickListener(v -> {
startActivityForResult(RecipientDisappearingMessagesActivity.forCreateGroup(requireContext(), viewModel.getDisappearingMessagesTimer().getValue()), REQUEST_DISAPPEARING_TIMER);
});
name.requestFocus();
}
@@ -175,6 +190,8 @@ public class AddGroupDetailsFragment extends LoggingFragment {
public void onLoadCleared(@Nullable Drawable placeholder) {
}
});
} else if (requestCode == REQUEST_DISAPPEARING_TIMER && resultCode == Activity.RESULT_OK && data != null) {
viewModel.setDisappearingMessageTimer(data.getIntExtra(ExpireTimerSettingsFragment.FOR_RESULT_VALUE, SignalStore.settings().getUniversalExpireTimer()));
} else {
super.onActivityResult(requestCode, resultCode, data);
}

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV2CapabilityChecker;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
@@ -48,17 +49,24 @@ final class AddGroupDetailsRepository {
});
}
void createGroup(@NonNull Set<RecipientId> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
void createGroup(@NonNull Set<RecipientId> members,
@Nullable byte[] avatar,
@Nullable String name,
boolean mms,
@Nullable Integer disappearingMessagesTimer,
Consumer<GroupCreateResult> resultConsumer)
{
SignalExecutors.BOUNDED.execute(() -> {
Set<Recipient> recipients = new HashSet<>(Stream.of(members).map(Recipient::resolved).toList());
try {
GroupManager.GroupActionResult result = GroupManager.createGroup(context, recipients, avatar, name, mms);
GroupManager.GroupActionResult result = GroupManager.createGroup(context,
recipients,
avatar,
name,
mms,
disappearingMessagesTimer != null ? disappearingMessagesTimer
: SignalStore.settings().getUniversalExpireTimer());
resultConsumer.accept(GroupCreateResult.success(result));
} catch (GroupChangeBusyException e) {

View File

@@ -32,10 +32,11 @@ import java.util.Set;
public final class AddGroupDetailsViewModel extends ViewModel {
private final LiveData<List<GroupMemberEntry.NewGroupCandidate>> members;
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
private final MutableLiveData<String> name = new MutableLiveData<>("");
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
private final DefaultValueLiveData<Set<RecipientId>> deleted = new DefaultValueLiveData<>(new HashSet<>());
private final MutableLiveData<String> name = new MutableLiveData<>("");
private final MutableLiveData<byte[]> avatar = new MutableLiveData<>();
private final SingleLiveEvent<GroupCreateResult> groupCreateResult = new SingleLiveEvent<>();
private final MutableLiveData<Integer> disappearingMessagesTimer = new MutableLiveData<>(SignalStore.settings().getUniversalExpireTimer());
private final LiveData<Boolean> isMms;
private final LiveData<Boolean> canSubmitForm;
private final AddGroupDetailsRepository repository;
@@ -47,12 +48,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
this.repository = repository;
MutableLiveData<List<GroupMemberEntry.NewGroupCandidate>> initialMembers = new MutableLiveData<>();
LiveData<Boolean> isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
LiveData<Boolean> isValidName = Transformations.map(name, name -> !TextUtils.isEmpty(name));
members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms);
members = LiveDataUtil.combineLatest(initialMembers, deleted, AddGroupDetailsViewModel::filterDeletedMembers);
isMms = Transformations.map(members, AddGroupDetailsViewModel::isAnyForcedSms);
LiveData<List<GroupMemberEntry.NewGroupCandidate>> membersToCheckGv2CapabilityOf = LiveDataUtil.combineLatest(isMms, members, (forcedMms, memberList) -> {
if (SignalStore.internalValues().gv2DoNotCreateGv2Groups() || forcedMms) {
@@ -94,6 +93,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
return nonGv2CapableMembers;
}
@NonNull LiveData<Integer> getDisappearingMessagesTimer() {
return disappearingMessagesTimer;
}
void setAvatar(@Nullable byte[] avatar) {
this.avatar.setValue(avatar);
}
@@ -107,18 +110,19 @@ public final class AddGroupDetailsViewModel extends ViewModel {
}
void delete(@NonNull RecipientId recipientId) {
Set<RecipientId> deleted = this.deleted.getValue();
Set<RecipientId> deleted = this.deleted.getValue();
deleted.add(recipientId);
this.deleted.setValue(deleted);
}
void create() {
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
byte[] avatarBytes = avatar.getValue();
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
String groupName = name.getValue();
List<GroupMemberEntry.NewGroupCandidate> members = Objects.requireNonNull(this.members.getValue());
Set<RecipientId> memberIds = Stream.of(members).map(member -> member.getMember().getId()).collect(Collectors.toSet());
byte[] avatarBytes = avatar.getValue();
boolean isGroupMms = isMms.getValue() == Boolean.TRUE;
String groupName = name.getValue();
Integer disappearingTimer = disappearingMessagesTimer.getValue();
if (!isGroupMms && TextUtils.isEmpty(groupName)) {
groupCreateResult.postValue(GroupCreateResult.error(GroupCreateResult.Error.Type.ERROR_INVALID_NAME));
@@ -129,6 +133,7 @@ public final class AddGroupDetailsViewModel extends ViewModel {
avatarBytes,
groupName,
isGroupMms,
disappearingTimer,
groupCreateResult::postValue);
}
@@ -143,6 +148,10 @@ public final class AddGroupDetailsViewModel extends ViewModel {
.anyMatch(member -> !member.getMember().isRegistered());
}
public void setDisappearingMessageTimer(int timer) {
disappearingMessagesTimer.setValue(timer);
}
static final class Factory implements ViewModelProvider.Factory {
private final Collection<RecipientId> recipientIds;

View File

@@ -58,6 +58,7 @@ import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
@@ -280,7 +281,12 @@ public class ManageGroupFragment extends LoggingFragment {
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
disappearingMessagesRow.setOnClickListener(v -> viewModel.handleExpirationSelection());
disappearingMessagesRow.setOnClickListener(v -> {
Recipient recipient = viewModel.getGroupRecipient().getValue();
if (recipient != null) {
startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipient.getId()));
}
});
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
unblockGroup.setOnClickListener(v -> viewModel.unblock(requireActivity()));

View File

@@ -79,17 +79,6 @@ final class ManageGroupRepository {
return new GroupStateResult(threadId, groupRecipient);
}
void setExpiration(@NonNull GroupId groupId, int newExpirationTime, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.updateGroupTimer(context, groupId.requirePush(), newExpirationTime);
} catch (GroupChangeException | IOException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void applyMembershipRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {

View File

@@ -257,14 +257,6 @@ public class ManageGroupViewModel extends ViewModel {
return groupInfoMessage;
}
void handleExpirationSelection() {
manageGroupRepository.getRecipient(getGroupId(),
groupRecipient ->
ExpirationDialog.show(context,
groupRecipient.getExpireMessages(),
expirationTime -> manageGroupRepository.setExpiration(getGroupId(), expirationTime, this::showErrorToast)));
}
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
}