mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Invite Friends bottom sheet.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite;
|
||||
|
||||
enum EnableInviteLinkError {
|
||||
BUSY,
|
||||
FAILED,
|
||||
NETWORK_ERROR,
|
||||
INSUFFICIENT_RIGHTS,
|
||||
NOT_IN_GROUP,
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user