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

@ -62,6 +62,7 @@ public interface BindableConversationItem extends Unbindable {
void onVoiceNoteSeekTo(@NonNull Uri uri, double position);
void onGroupMigrationLearnMoreClicked(@NonNull GroupMigrationMembershipChange membershipChange);
void onJoinGroupCallClicked();
void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId);
/** @return true if handled, false if you want to let the normal url handling continue */
boolean onUrlClicked(@NonNull String url);

View file

@ -536,6 +536,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
.startChain(new RequestGroupV2InfoJob(groupId))
.then(new GroupV2UpdateSelfProfileKeyJob(groupId))
.enqueue();
if (viewModel.getArgs().isFirstTimeInSelfCreatedGroup()) {
groupViewModel.inviteFriendsOneTimeIfJustSelfInGroup(getSupportFragmentManager(), groupId);
}
}
if (groupCallViewModel != null) {

View file

@ -93,6 +93,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInfoBottomSheetDialogFragment;
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
import org.thoughtcrime.securesms.jobs.MultiDeviceViewOnceOpenJob;
@ -1418,6 +1419,11 @@ public class ConversationFragment extends LoggingFragment {
public void onJoinGroupCallClicked() {
CommunicationActions.startVideoCall(requireActivity(), recipient.get());
}
@Override
public void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(requireActivity().getSupportFragmentManager(), groupId);
}
}
@Override

View file

@ -6,6 +6,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
@ -25,12 +26,14 @@ import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupsV1MigrationUtil;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite.GroupLinkInviteFriendsBottomSheetDialogFragment;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient;
import org.thoughtcrime.securesms.profiles.spoofing.ReviewUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import java.io.IOException;
@ -50,6 +53,8 @@ final class ConversationGroupViewModel extends ViewModel {
private final LiveData<List<RecipientId>> gv1MigrationSuggestions;
private final LiveData<Boolean> gv1MigrationReminder;
private boolean firstTimeInviteFriendsTriggered;
private ConversationGroupViewModel() {
this.liveRecipient = new MutableLiveData<>();
@ -225,6 +230,28 @@ final class ConversationGroupViewModel extends ViewModel {
});
}
void inviteFriendsOneTimeIfJustSelfInGroup(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
if (firstTimeInviteFriendsTriggered) {
return;
}
firstTimeInviteFriendsTriggered = true;
SimpleTask.run(() -> DatabaseFactory.getGroupDatabase(ApplicationDependencies.getApplication())
.requireGroup(groupId)
.getMembers().equals(Collections.singletonList(Recipient.self().getId())),
justSelf -> {
if (justSelf) {
inviteFriends(supportFragmentManager, groupId);
}
}
);
}
void inviteFriends(@NonNull FragmentManager supportFragmentManager, @NonNull GroupId.V2 groupId) {
GroupLinkInviteFriendsBottomSheetDialogFragment.show(supportFragmentManager, groupId);
}
static final class ReviewState {
private static final ReviewState EMPTY = new ReviewState(null, Recipient.UNKNOWN, 0);

View file

@ -18,15 +18,16 @@ import java.util.Objects;
public class ConversationIntents {
private static final String BUBBLE_AUTHORITY = "bubble";
private static final String EXTRA_RECIPIENT = "recipient_id";
private static final String EXTRA_THREAD_ID = "thread_id";
private static final String EXTRA_TEXT = "draft_text";
private static final String EXTRA_MEDIA = "media_list";
private static final String EXTRA_STICKER = "sticker_extra";
private static final String EXTRA_BORDERLESS = "borderless_extra";
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String BUBBLE_AUTHORITY = "bubble";
private static final String EXTRA_RECIPIENT = "recipient_id";
private static final String EXTRA_THREAD_ID = "thread_id";
private static final String EXTRA_TEXT = "draft_text";
private static final String EXTRA_MEDIA = "media_list";
private static final String EXTRA_STICKER = "sticker_extra";
private static final String EXTRA_BORDERLESS = "borderless_extra";
private static final String EXTRA_DISTRIBUTION_TYPE = "distribution_type";
private static final String EXTRA_STARTING_POSITION = "starting_position";
private static final String EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP = "first_time_in_group";
private ConversationIntents() {
}
@ -63,7 +64,8 @@ public class ConversationIntents {
private final StickerLocator stickerLocator;
private final boolean isBorderless;
private final int distributionType;
private final int startingPosition;
private final int startingPosition;
private final boolean firstTimeInSelfCreatedGroup;
static Args from(@NonNull Intent intent) {
if (isBubbleIntent(intent)) {
@ -74,7 +76,8 @@ public class ConversationIntents {
null,
false,
ThreadDatabase.DistributionTypes.DEFAULT,
-1);
-1,
false);
}
return new Args(RecipientId.from(Objects.requireNonNull(intent.getStringExtra(EXTRA_RECIPIENT))),
@ -84,7 +87,8 @@ public class ConversationIntents {
intent.getParcelableExtra(EXTRA_STICKER),
intent.getBooleanExtra(EXTRA_BORDERLESS, false),
intent.getIntExtra(EXTRA_DISTRIBUTION_TYPE, ThreadDatabase.DistributionTypes.DEFAULT),
intent.getIntExtra(EXTRA_STARTING_POSITION, -1));
intent.getIntExtra(EXTRA_STARTING_POSITION, -1),
intent.getBooleanExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, false));
}
private Args(@NonNull RecipientId recipientId,
@ -94,16 +98,18 @@ public class ConversationIntents {
@Nullable StickerLocator stickerLocator,
boolean isBorderless,
int distributionType,
int startingPosition)
int startingPosition,
boolean firstTimeInSelfCreatedGroup)
{
this.recipientId = recipientId;
this.threadId = threadId;
this.draftText = draftText;
this.media = media;
this.stickerLocator = stickerLocator;
this.isBorderless = isBorderless;
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.recipientId = recipientId;
this.threadId = threadId;
this.draftText = draftText;
this.media = media;
this.stickerLocator = stickerLocator;
this.isBorderless = isBorderless;
this.distributionType = distributionType;
this.startingPosition = startingPosition;
this.firstTimeInSelfCreatedGroup = firstTimeInSelfCreatedGroup;
}
public @NonNull RecipientId getRecipientId() {
@ -137,6 +143,10 @@ public class ConversationIntents {
public boolean isBorderless() {
return isBorderless;
}
public boolean isFirstTimeInSelfCreatedGroup() {
return firstTimeInSelfCreatedGroup;
}
}
public final static class Builder {
@ -153,6 +163,7 @@ public class ConversationIntents {
private int startingPosition = -1;
private Uri dataUri;
private String dataType;
private boolean firstTimeInSelfCreatedGroup;
private Builder(@NonNull Context context,
@NonNull RecipientId recipientId,
@ -212,6 +223,11 @@ public class ConversationIntents {
return this;
}
public Builder firstTimeInSelfCreatedGroup() {
this.firstTimeInSelfCreatedGroup = true;
return this;
}
public @NonNull Intent build() {
if (stickerLocator != null && media != null) {
throw new IllegalStateException("Cannot have both sticker and media array");
@ -235,6 +251,7 @@ public class ConversationIntents {
intent.putExtra(EXTRA_DISTRIBUTION_TYPE, distributionType);
intent.putExtra(EXTRA_STARTING_POSITION, startingPosition);
intent.putExtra(EXTRA_BORDERLESS, isBorderless);
intent.putExtra(EXTRA_FIRST_TIME_IN_SELF_CREATED_GROUP, firstTimeInSelfCreatedGroup);
if (draftText != null) {
intent.putExtra(EXTRA_TEXT, draftText);

View file

@ -238,8 +238,7 @@ public final class ConversationUpdateItem extends LinearLayout
actionButton.setVisibility(VISIBLE);
actionButton.setOnClickListener(v -> {
if (batchSelected.isEmpty() && eventListener != null) {
// TODO [alan]
Log.i(TAG, "TODO");
eventListener.onInviteFriendsToGroupClicked(conversationRecipient.requireGroupId().requireV2());
}
});
} else {

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";

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="8dp" />
<stroke
android:width="1dp"
android:color="@color/core_grey_05" />
</shape>

View file

@ -0,0 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:theme="@style/Theme.Signal.RoundedBottomSheet.Light">
<TextView
android:id="@+id/group_link_enable_and_share_title"
style="@style/TextAppearance.Signal.Title2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/GroupInviteLinkEnableAndShareBottomSheetDialogFragment_invite_friends"
android:textColor="@color/signal_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/group_link_enable_and_share_explain"
style="@style/TextAppearance.Signal.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/GroupInviteLinkEnableAndShareBottomSheetDialogFragment_share_a_link_with_friends_to_let_them_quickly_join_this_group"
android:textAlignment="center"
android:textColor="@color/signal_text_primary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_enable_and_share_title" />
<LinearLayout
android:id="@+id/group_link_enable_and_share_approve_new_members_row"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="24dp"
android:background="@drawable/group_link_admin_approval_border"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_enable_and_share_explain">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:text="@string/ShareableGroupLinkDialogFragment__approve_new_members"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/text_color_primary_enabled_selector" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/group_link_enable_and_share_approve_new_members_switch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:clickable="false"
app:layout_constraintBottom_toBottomOf="@id/shareable_group_link_enable_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/shareable_group_link_enable_label"
app:layout_constraintTop_toTopOf="@id/shareable_group_link_enable_label" />
</LinearLayout>
<LinearLayout
android:id="@+id/group_link_enable_and_share_approve_new_members_row2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:background="?selectableItemBackground"
android:orientation="horizontal"
android:paddingStart="32dp"
android:paddingEnd="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_enable_and_share_approve_new_members_row">
<TextView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center_horizontal"
android:layout_weight="1"
android:gravity="center_vertical|start"
android:text="@string/ShareableGroupLinkDialogFragment__require_an_admin_to_approve_new_members_joining_via_the_group_link"
android:textAlignment="viewStart"
android:textAppearance="@style/TextAppearance.Signal.Body2"
android:textColor="@color/text_color_secondary_enabled_selector" />
</LinearLayout>
<Button
android:id="@+id/group_link_enable_and_share_button"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:text="@string/GroupInviteLinkEnableAndShareBottomSheetDialogFragment_enable_and_share_link"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_enable_and_share_approve_new_members_row2"
tools:visibility="visible" />
<Button
android:id="@+id/group_link_share_button"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/GroupInviteLinkEnableAndShareBottomSheetDialogFragment_share_link"
android:visibility="invisible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_enable_and_share_approve_new_members_row2" />
<Button
android:id="@+id/group_link_enable_and_share_cancel_button"
style="@style/Button.Primary"
android:layout_width="0dp"
android:layout_height="64dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@android:string/cancel"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/group_link_enable_and_share_button"
app:layout_constraintVertical_bias="0" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -889,6 +889,18 @@
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_update_linked_device_message">One or more of your linked devices are running a version of Signal that doesn\'t support group links. Update Signal on your linked device(s) to join this group.</string>
<string name="GroupJoinUpdateRequiredBottomSheetDialogFragment_group_link_is_not_valid">Group link is not valid</string>
<!-- GroupInviteLinkEnableAndShareBottomSheetDialogFragment -->
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_invite_friends">Invite friends</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_share_a_link_with_friends_to_let_them_quickly_join_this_group">Share a link with friends to let them quickly join this group.</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_enable_and_share_link">Enable and share link</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_share_link">Share link</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_unable_to_enable_group_link_please_try_again_later">Unable to enable group link. Please try again later</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_encountered_a_network_error">Encountered a network error.</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_you_dont_have_the_right_to_enable_group_link">You don\'t have the right to enable the group link. Please ask an admin.</string>
<string name="GroupInviteLinkEnableAndShareBottomSheetDialogFragment_you_are_not_currently_a_member_of_the_group">You are not currently a member of the group.</string>
<!-- GV2 Request confirmation dialog -->
<string name="RequestConfirmationDialog_add_s_to_the_group">Add “%1$s” to the group?</string>
<string name="RequestConfirmationDialog_deny_request_from_s">Deny request from “%1$s”?</string>