diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index b6d7a6daa6..d0701824bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 9d05c26886..c766a2fcee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index dcd1c0aebe..15048e1c68 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java index 9b4d4daeb8..35041eda78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationGroupViewModel.java @@ -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> gv1MigrationSuggestions; private final LiveData 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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java index 0bb8dd1556..6c20311045 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationIntents.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index cce190e08b..709d36a7d4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java index e393b46ef8..7282fffd9d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManager.java @@ -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); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 0e174ab12c..4a74f8b944 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java index e5853657cc..638323ce0f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/creategroup/details/AddGroupDetailsActivity.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/EnableInviteLinkError.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/EnableInviteLinkError.java new file mode 100644 index 0000000000..4589efafa8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/EnableInviteLinkError.java @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.groups.ui.invitesandrequests.invite; + +enum EnableInviteLinkError { + BUSY, + FAILED, + NETWORK_ERROR, + INSUFFICIENT_RIGHTS, + NOT_IN_GROUP, +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteFriendsBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteFriendsBottomSheetDialogFragment.java new file mode 100644 index 0000000000..b6a72f770b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteFriendsBottomSheetDialogFragment.java @@ -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); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteFriendsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteFriendsViewModel.java new file mode 100644 index 0000000000..06e23d2e06 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteFriendsViewModel.java @@ -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 enableErrors = new SingleLiveEvent<>(); + private final MutableLiveData busy = new MediatorLiveData<>(); + private final MutableLiveData enableSuccess = new SingleLiveEvent<>(); + private final LiveData groupLink; + private final MutableLiveData 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 getGroupInviteLinkAndStatus() { + return groupLink; + } + + void enable() { + busy.setValue(true); + repository.enableGroupInviteLink(getCurrentMemberApproval(), new AsynchronousCallback.WorkerThread() { + @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 isBusy() { + return busy; + } + + LiveData getEnableSuccess() { + return enableSuccess; + } + + LiveData getEnableErrors() { + return enableErrors; + } + + LiveData 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 create(@NonNull Class modelClass) { + //noinspection unchecked + return (T) new GroupLinkInviteFriendsViewModel(groupId, new GroupLinkInviteRepository(context.getApplicationContext(), groupId)); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteRepository.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteRepository.java new file mode 100644 index 0000000000..f87f61244b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/invite/GroupLinkInviteRepository.java @@ -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 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); + } + }); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java index 0c92db68f1..98dbf39543 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/invitesandrequests/joining/GroupJoinBottomSheetDialogFragment.java @@ -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"; diff --git a/app/src/main/res/drawable/group_link_admin_approval_border.xml b/app/src/main/res/drawable/group_link_admin_approval_border.xml new file mode 100644 index 0000000000..c298463463 --- /dev/null +++ b/app/src/main/res/drawable/group_link_admin_approval_border.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/group_invite_link_enable_and_share_bottom_sheet.xml b/app/src/main/res/layout/group_invite_link_enable_and_share_bottom_sheet.xml new file mode 100644 index 0000000000..c2a227a520 --- /dev/null +++ b/app/src/main/res/layout/group_invite_link_enable_and_share_bottom_sheet.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + +