Invite Friends bottom sheet.

This commit is contained in:
Alan Evans
2021-01-08 19:37:03 -04:00
parent 3739eb7731
commit 4d229862b6
17 changed files with 616 additions and 31 deletions

View File

@@ -15,6 +15,7 @@ import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.groups.v2.GroupLinkPassword;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
@@ -300,13 +301,13 @@ public final class GroupManager {
}
@WorkerThread
public static void setGroupLinkEnabledState(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull GroupLinkState state)
public static GroupInviteLinkUrl setGroupLinkEnabledState(@NonNull Context context,
@NonNull GroupId.V2 groupId,
@NonNull GroupLinkState state)
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException, GroupChangeBusyException
{
try (GroupManagerV2.GroupEditor editor = new GroupManagerV2(context).edit(groupId.requireV2())) {
editor.setJoinByGroupLinkState(state);
return editor.setJoinByGroupLinkState(state);
}
}

View File

@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.v2.GroupCandidateHelper;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.groups.v2.GroupLinkPassword;
import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor;
import org.thoughtcrime.securesms.jobs.ProfileUploadJob;
@@ -507,7 +508,7 @@ final class GroupManagerV2 {
}
@WorkerThread
public GroupManager.GroupActionResult setJoinByGroupLinkState(@NonNull GroupManager.GroupLinkState state)
public @Nullable GroupInviteLinkUrl setJoinByGroupLinkState(@NonNull GroupManager.GroupLinkState state)
throws GroupChangeFailedException, GroupNotAMemberException, GroupInsufficientRightsException, IOException
{
AccessControl.AccessRequired access;
@@ -519,7 +520,7 @@ final class GroupManagerV2 {
default: throw new AssertionError();
}
GroupChange.Actions.Builder change = groupOperations.createChangeJoinByLinkRights(access);
GroupChange.Actions.Builder change = groupOperations.createChangeJoinByLinkRights(access);
if (state != GroupManager.GroupLinkState.DISABLED) {
DecryptedGroup group = groupDatabase.requireGroup(groupId).requireV2GroupProperties().getDecryptedGroup();
@@ -530,7 +531,17 @@ final class GroupManagerV2 {
}
}
return commitChangeWithConflictResolution(change);
commitChangeWithConflictResolution(change);
if (state != GroupManager.GroupLinkState.DISABLED) {
GroupDatabase.V2GroupProperties v2GroupProperties = groupDatabase.requireGroup(groupId).requireV2GroupProperties();
GroupMasterKey groupMasterKey = v2GroupProperties.getGroupMasterKey();
DecryptedGroup decryptedGroup = v2GroupProperties.getDecryptedGroup();
return GroupInviteLinkUrl.forGroup(groupMasterKey, decryptedGroup);
} else {
return null;
}
}
private @NonNull GroupManager.GroupActionResult commitChangeWithConflictResolution(@NonNull GroupChange.Actions.Builder change)

View File

@@ -73,6 +73,7 @@ public class AddGroupDetailsActivity extends PassphraseRequiredActivity implemen
void goToConversation(@NonNull RecipientId recipientId, long threadId) {
Intent intent = ConversationIntents.createBuilder(this, recipientId, threadId)
.firstTimeInSelfCreatedGroup()
.build();
startActivity(intent);

View File

@@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite;
enum EnableInviteLinkError {
BUSY,
FAILED,
NETWORK_ERROR,
INSUFFICIENT_RIGHTS,
NOT_IN_GROUP,
}

View File

@@ -0,0 +1,175 @@
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.BadGroupIdException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.GroupLinkBottomSheetDialogFragment;
import org.thoughtcrime.securesms.util.BottomSheetUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.util.Objects;
public final class GroupLinkInviteFriendsBottomSheetDialogFragment extends BottomSheetDialogFragment {
private static final String TAG = Log.tag(GroupLinkInviteFriendsBottomSheetDialogFragment.class);
private static final String ARG_GROUP_ID = "group_id";
private Button groupLinkEnableAndShareButton;
private Button groupLinkShareButton;
private View memberApprovalRow;
private View memberApprovalRow2;
private SwitchCompat memberApprovalSwitch;
private SimpleProgressDialog.DismissibleDialog busyDialog;
public static void show(@NonNull FragmentManager manager,
@NonNull GroupId.V2 groupId)
{
GroupLinkInviteFriendsBottomSheetDialogFragment fragment = new GroupLinkInviteFriendsBottomSheetDialogFragment();
Bundle args = new Bundle();
args.putString(ARG_GROUP_ID, groupId.toString());
fragment.setArguments(args);
fragment.show(manager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
setStyle(DialogFragment.STYLE_NORMAL,
ThemeUtil.isDarkTheme(requireContext()) ? R.style.Theme_Signal_RoundedBottomSheet
: R.style.Theme_Signal_RoundedBottomSheet_Light);
super.onCreate(savedInstanceState);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.group_invite_link_enable_and_share_bottom_sheet, container, false);
groupLinkEnableAndShareButton = view.findViewById(R.id.group_link_enable_and_share_button);
groupLinkShareButton = view.findViewById(R.id.group_link_share_button);
memberApprovalRow = view.findViewById(R.id.group_link_enable_and_share_approve_new_members_row);
memberApprovalRow2 = view.findViewById(R.id.group_link_enable_and_share_approve_new_members_row2);
memberApprovalSwitch = view.findViewById(R.id.group_link_enable_and_share_approve_new_members_switch);
view.findViewById(R.id.group_link_enable_and_share_cancel_button).setOnClickListener(v -> dismiss());
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
GroupId.V2 groupId = getGroupId();
GroupLinkInviteFriendsViewModel.Factory factory = new GroupLinkInviteFriendsViewModel.Factory(requireContext().getApplicationContext(), groupId);
GroupLinkInviteFriendsViewModel viewModel = ViewModelProviders.of(this, factory).get(GroupLinkInviteFriendsViewModel.class);
viewModel.getGroupInviteLinkAndStatus()
.observe(getViewLifecycleOwner(), groupLinkUrlAndStatus -> {
if (groupLinkUrlAndStatus.isEnabled()) {
groupLinkShareButton.setVisibility(View.VISIBLE);
groupLinkEnableAndShareButton.setVisibility(View.INVISIBLE);
memberApprovalRow.setVisibility(View.GONE);
memberApprovalRow2.setVisibility(View.GONE);
groupLinkShareButton.setOnClickListener(v -> shareGroupLinkAndDismiss(groupId));
} else {
memberApprovalRow.setVisibility(View.VISIBLE);
memberApprovalRow2.setVisibility(View.VISIBLE);
groupLinkEnableAndShareButton.setVisibility(View.VISIBLE);
groupLinkShareButton.setVisibility(View.INVISIBLE);
}
});
memberApprovalRow.setOnClickListener(v -> viewModel.toggleMemberApproval());
viewModel.getMemberApproval()
.observe(getViewLifecycleOwner(), enabled -> memberApprovalSwitch.setChecked(enabled));
viewModel.isBusy()
.observe(getViewLifecycleOwner(), this::setBusy);
viewModel.getEnableErrors()
.observe(getViewLifecycleOwner(), error -> {
Toast.makeText(requireContext(), errorToMessage(error), Toast.LENGTH_SHORT).show();
if (error == EnableInviteLinkError.NOT_IN_GROUP || error == EnableInviteLinkError.INSUFFICIENT_RIGHTS) {
dismiss();
}
});
groupLinkEnableAndShareButton.setOnClickListener(v -> viewModel.enable());
viewModel.getEnableSuccess()
.observe(getViewLifecycleOwner(), joinGroupSuccess -> {
Log.i(TAG, "Group link enabled, sharing");
shareGroupLinkAndDismiss(groupId);
}
);
}
protected void shareGroupLinkAndDismiss(@NonNull GroupId.V2 groupId) {
dismiss();
GroupLinkBottomSheetDialogFragment.show(requireFragmentManager(), groupId);
}
protected GroupId.V2 getGroupId() {
try {
return GroupId.parse(Objects.requireNonNull(requireArguments().getString(ARG_GROUP_ID)))
.requireV2();
} catch (BadGroupIdException e) {
throw new AssertionError(e);
}
}
private void setBusy(boolean isBusy) {
if (isBusy) {
if (busyDialog == null) {
busyDialog = SimpleProgressDialog.showDelayed(requireContext());
}
} else {
if (busyDialog != null) {
busyDialog.dismiss();
busyDialog = null;
}
}
}
private @NonNull String errorToMessage(@NonNull EnableInviteLinkError error) {
switch (error) {
case NETWORK_ERROR : return getString(R.string.GroupInviteLinkEnableAndShareBottomSheetDialogFragment_encountered_a_network_error);
case INSUFFICIENT_RIGHTS : return getString(R.string.GroupInviteLinkEnableAndShareBottomSheetDialogFragment_you_dont_have_the_right_to_enable_group_link);
case NOT_IN_GROUP : return getString(R.string.GroupInviteLinkEnableAndShareBottomSheetDialogFragment_you_are_not_currently_a_member_of_the_group);
default : return getString(R.string.GroupInviteLinkEnableAndShareBottomSheetDialogFragment_unable_to_enable_group_link_please_try_again_later);
}
}
@Override
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
BottomSheetUtil.show(manager, tag, this);
}
}

View File

@@ -0,0 +1,104 @@
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
public class GroupLinkInviteFriendsViewModel extends ViewModel {
private static final boolean INITIAL_MEMBER_APPROVAL_STATE = false;
private final GroupLinkInviteRepository repository;
private final MutableLiveData<EnableInviteLinkError> enableErrors = new SingleLiveEvent<>();
private final MutableLiveData<Boolean> busy = new MediatorLiveData<>();
private final MutableLiveData<GroupInviteLinkUrl> enableSuccess = new SingleLiveEvent<>();
private final LiveData<GroupLinkUrlAndStatus> groupLink;
private final MutableLiveData<Boolean> memberApproval = new MutableLiveData<>(INITIAL_MEMBER_APPROVAL_STATE);
private GroupLinkInviteFriendsViewModel(GroupId.V2 groupId, @NonNull GroupLinkInviteRepository repository) {
this.repository = repository;
LiveGroup liveGroup = new LiveGroup(groupId);
this.groupLink = liveGroup.getGroupLink();
}
LiveData<GroupLinkUrlAndStatus> getGroupInviteLinkAndStatus() {
return groupLink;
}
void enable() {
busy.setValue(true);
repository.enableGroupInviteLink(getCurrentMemberApproval(), new AsynchronousCallback.WorkerThread<GroupInviteLinkUrl, EnableInviteLinkError>() {
@Override
public void onComplete(@Nullable GroupInviteLinkUrl groupInviteLinkUrl) {
busy.postValue(false);
enableSuccess.postValue(groupInviteLinkUrl);
}
@Override
public void onError(@Nullable EnableInviteLinkError error) {
busy.postValue(false);
enableErrors.postValue(error);
}
});
}
LiveData<Boolean> isBusy() {
return busy;
}
LiveData<GroupInviteLinkUrl> getEnableSuccess() {
return enableSuccess;
}
LiveData<EnableInviteLinkError> getEnableErrors() {
return enableErrors;
}
LiveData<Boolean> getMemberApproval() {
return memberApproval;
}
private boolean getCurrentMemberApproval() {
Boolean value = memberApproval.getValue();
if (value == null) {
return INITIAL_MEMBER_APPROVAL_STATE;
}
return value;
}
void toggleMemberApproval() {
memberApproval.postValue(!getCurrentMemberApproval());
}
public static class Factory implements ViewModelProvider.Factory {
private final Context context;
private final GroupId.V2 groupId;
public Factory(@NonNull Context context, @NonNull GroupId.V2 groupId) {
this.context = context;
this.groupId = groupId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new GroupLinkInviteFriendsViewModel(groupId, new GroupLinkInviteRepository(context.getApplicationContext(), groupId));
}
}
}

View File

@@ -0,0 +1,55 @@
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite;
import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.groups.GroupChangeBusyException;
import org.thoughtcrime.securesms.groups.GroupChangeFailedException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupInsufficientRightsException;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupNotAMemberException;
import org.thoughtcrime.securesms.groups.v2.GroupInviteLinkUrl;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import java.io.IOException;
final class GroupLinkInviteRepository {
private final Context context;
private final GroupId.V2 groupId;
GroupLinkInviteRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
this.context = context;
this.groupId = groupId;
}
void enableGroupInviteLink(boolean requireMemberApproval, @NonNull AsynchronousCallback.WorkerThread<GroupInviteLinkUrl, EnableInviteLinkError> callback) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupInviteLinkUrl groupInviteLinkUrl = GroupManager.setGroupLinkEnabledState(context,
groupId,
requireMemberApproval ? GroupManager.GroupLinkState.ENABLED_WITH_APPROVAL
: GroupManager.GroupLinkState.ENABLED);
if (groupInviteLinkUrl == null) {
throw new AssertionError();
}
callback.onComplete(groupInviteLinkUrl);
} catch (IOException e) {
callback.onError(EnableInviteLinkError.NETWORK_ERROR);
} catch (GroupChangeBusyException e) {
callback.onError(EnableInviteLinkError.BUSY);
} catch (GroupChangeFailedException e) {
callback.onError(EnableInviteLinkError.FAILED);
} catch (GroupInsufficientRightsException e) {
callback.onError(EnableInviteLinkError.INSUFFICIENT_RIGHTS);
} catch (GroupNotAMemberException e) {
callback.onError(EnableInviteLinkError.NOT_IN_GROUP);
}
});
}
}

View File

@@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
public final class GroupJoinBottomSheetDialogFragment extends BottomSheetDialogFragment {
private static final String TAG = Log.tag(GroupJoinUpdateRequiredBottomSheetDialogFragment.class);
private static final String TAG = Log.tag(GroupJoinBottomSheetDialogFragment.class);
private static final String ARG_GROUP_INVITE_LINK_URL = "group_invite_url";