mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-26 11:51:10 +01:00
Manage group links behind feature flag.
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class GroupLinkBottomSheetDialogFragment extends BottomSheetDialogFragment {
|
||||
|
||||
public static final String ARG_GROUP_ID = "group_id";
|
||||
|
||||
public static void show(@NonNull FragmentManager manager, @NonNull GroupId.V2 groupId) {
|
||||
GroupLinkBottomSheetDialogFragment fragment = new GroupLinkBottomSheetDialogFragment();
|
||||
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_link_share_bottom_sheet, container, false);
|
||||
|
||||
View shareViaSignalButton = view.findViewById(R.id.group_link_bottom_sheet_share_via_signal_button);
|
||||
View copyButton = view.findViewById(R.id.group_link_bottom_sheet_copy_button);
|
||||
View viewQrButton = view.findViewById(R.id.group_link_bottom_sheet_qr_code_button);
|
||||
View shareBySystemButton = view.findViewById(R.id.group_link_bottom_sheet_share_via_system_button);
|
||||
|
||||
GroupId.V2 groupId = GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(ARG_GROUP_ID))).requireV2();
|
||||
|
||||
LiveGroup liveGroup = new LiveGroup(groupId);
|
||||
|
||||
liveGroup.getGroupLink().observe(getViewLifecycleOwner(), groupLink -> {
|
||||
if (!groupLink.isEnabled()) {
|
||||
Toast.makeText(requireContext(), R.string.GroupLinkBottomSheet_the_link_is_not_currently_active, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
shareViaSignalButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share within signal
|
||||
shareViaSignalButton.setVisibility(View.GONE);
|
||||
|
||||
copyButton.setOnClickListener(v -> {
|
||||
Context context = requireContext();
|
||||
Util.copyToClipboard(context, groupLink.getUrl());
|
||||
Toast.makeText(context, R.string.GroupLinkBottomSheet_copied_to_clipboard, Toast.LENGTH_SHORT).show();
|
||||
dismiss();
|
||||
});
|
||||
|
||||
viewQrButton.setOnClickListener(v -> dismiss()); // Todo [Alan] GV2 Add share QR within signal
|
||||
viewQrButton.setVisibility(View.GONE);
|
||||
|
||||
shareBySystemButton.setOnClickListener(v -> {
|
||||
ShareCompat.IntentBuilder.from(requireActivity())
|
||||
.setType("text/plain")
|
||||
.setText(groupLink.getUrl())
|
||||
.startChooser();
|
||||
|
||||
dismiss();
|
||||
});
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show(@NonNull FragmentManager manager, @Nullable String tag) {
|
||||
BottomSheetUtil.show(manager, tag, this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
|
||||
public final class ShareableGroupLinkDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String ARG_GROUP_ID = "group_id";
|
||||
|
||||
private ShareableGroupLinkViewModel viewModel;
|
||||
private GroupId.V2 groupId;
|
||||
private SimpleProgressDialog.DismissibleDialog dialog;
|
||||
|
||||
public static DialogFragment create(@NonNull GroupId.V2 groupId) {
|
||||
DialogFragment fragment = new ShareableGroupLinkDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
|
||||
args.putString(ARG_GROUP_ID, groupId.toString());
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setStyle(STYLE_NO_FRAME, ThemeUtil.isDarkTheme(requireActivity()) ? R.style.TextSecure_DarkTheme
|
||||
: R.style.TextSecure_LightTheme);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
return inflater.inflate(R.layout.shareable_group_link_dialog_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
initializeViewModel();
|
||||
initializeViews(view);
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
//noinspection ConstantConditions
|
||||
groupId = GroupId.parseOrThrow(requireArguments().getString(ARG_GROUP_ID)).requireV2();
|
||||
|
||||
ShareableGroupLinkRepository repository = new ShareableGroupLinkRepository(requireContext(), groupId);
|
||||
ShareableGroupLinkViewModel.Factory factory = new ShareableGroupLinkViewModel.Factory(groupId, repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ShareableGroupLinkViewModel.class);
|
||||
}
|
||||
|
||||
private void initializeViews(@NonNull View view) {
|
||||
SwitchCompat shareableGroupLinkSwitch = view.findViewById(R.id.shareable_group_link_enable_switch);
|
||||
TextView shareableGroupLinkDisplay = view.findViewById(R.id.shareable_group_link_display);
|
||||
SwitchCompat approveNewMembersSwitch = view.findViewById(R.id.shareable_group_link_approve_new_members_switch);
|
||||
View shareableGroupLinkRow = view.findViewById(R.id.shareable_group_link_row);
|
||||
View shareRow = view.findViewById(R.id.shareable_group_link_share_row);
|
||||
View resetLinkRow = view.findViewById(R.id.shareable_group_link_reset_link_row);
|
||||
View approveNewMembersRow = view.findViewById(R.id.shareable_group_link_approve_new_members_row);
|
||||
|
||||
Toolbar toolbar = view.findViewById(R.id.shareable_group_link_toolbar);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> dismissAllowingStateLoss());
|
||||
|
||||
viewModel.getGroupLink().observe(getViewLifecycleOwner(), groupLink -> {
|
||||
shareableGroupLinkSwitch.setChecked(groupLink.isEnabled());
|
||||
approveNewMembersSwitch.setChecked(groupLink.isRequiresApproval());
|
||||
shareableGroupLinkDisplay.setText(groupLink.getUrl());
|
||||
});
|
||||
|
||||
shareRow.setOnClickListener(v -> GroupLinkBottomSheetDialogFragment.show(requireFragmentManager(), groupId));
|
||||
|
||||
shareableGroupLinkRow.setOnClickListener(v -> viewModel.onToggleGroupLink(requireContext()));
|
||||
approveNewMembersRow.setOnClickListener(v -> viewModel.onToggleApproveMembers(requireContext()));
|
||||
resetLinkRow.setOnClickListener(v -> viewModel.onResetLink(requireContext()));
|
||||
|
||||
viewModel.getToasts().observe(getViewLifecycleOwner(), t -> Toast.makeText(requireContext(), t, Toast.LENGTH_SHORT).show());
|
||||
|
||||
viewModel.getBusy().observe(getViewLifecycleOwner(), busy -> {
|
||||
if (busy) {
|
||||
if (dialog == null) {
|
||||
dialog = SimpleProgressDialog.showDelayed(requireContext());
|
||||
}
|
||||
} else {
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
dialog = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.storageservice.protos.groups.AccessControl;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
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.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
final class ShareableGroupLinkRepository {
|
||||
|
||||
private final Context context;
|
||||
private final GroupId.V2 groupId;
|
||||
|
||||
ShareableGroupLinkRepository(@NonNull Context context, @NonNull GroupId.V2 groupId) {
|
||||
this.context = context;
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
void cycleGroupLinkPassword(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.cycleGroupLinkPassword(context, groupId);
|
||||
callback.onComplete(null);
|
||||
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void toggleGroupLinkEnabled(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
|
||||
setGroupLinkEnabledState(toggleGroupLinkState(true, false), callback);
|
||||
}
|
||||
|
||||
void toggleGroupLinkApprovalRequired(@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback) {
|
||||
setGroupLinkEnabledState(toggleGroupLinkState(false, true), callback);
|
||||
}
|
||||
|
||||
private void setGroupLinkEnabledState(@NonNull GroupManager.GroupLinkState state,
|
||||
@NonNull AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason> callback)
|
||||
{
|
||||
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||
try {
|
||||
GroupManager.setGroupLinkEnabledState(context, groupId, state);
|
||||
callback.onComplete(null);
|
||||
} catch (GroupNotAMemberException | GroupChangeFailedException | GroupInsufficientRightsException | IOException | GroupChangeBusyException e) {
|
||||
callback.onError(GroupChangeFailureReason.fromException(e));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private GroupManager.GroupLinkState toggleGroupLinkState(boolean toggleEnabled, boolean toggleApprovalNeeded) {
|
||||
AccessControl.AccessRequired currentState = DatabaseFactory.getGroupDatabase(context)
|
||||
.getGroup(groupId)
|
||||
.get()
|
||||
.requireV2GroupProperties()
|
||||
.getDecryptedGroup()
|
||||
.getAccessControl()
|
||||
.getAddFromInviteLink();
|
||||
|
||||
boolean enabled;
|
||||
boolean approvalNeeded;
|
||||
|
||||
switch (currentState) {
|
||||
case UNKNOWN:
|
||||
case UNSATISFIABLE:
|
||||
case UNRECOGNIZED:
|
||||
case MEMBER:
|
||||
enabled = false;
|
||||
approvalNeeded = false;
|
||||
break;
|
||||
case ANY:
|
||||
enabled = true;
|
||||
approvalNeeded = false;
|
||||
break;
|
||||
case ADMINISTRATOR:
|
||||
enabled = true;
|
||||
approvalNeeded = true;
|
||||
break;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
|
||||
if (toggleApprovalNeeded) {
|
||||
approvalNeeded = !approvalNeeded;
|
||||
}
|
||||
|
||||
if (toggleEnabled) {
|
||||
enabled = !enabled;
|
||||
if (enabled) approvalNeeded = true;
|
||||
}
|
||||
|
||||
if (approvalNeeded && enabled) {
|
||||
return GroupManager.GroupLinkState.ENABLED_WITH_APPROVAL;
|
||||
} else {
|
||||
if (enabled) {
|
||||
return GroupManager.GroupLinkState.ENABLED;
|
||||
}
|
||||
}
|
||||
|
||||
return GroupManager.GroupLinkState.DISABLED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package org.thoughtcrime.securesms.recipients.ui.sharablegrouplink;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
|
||||
import org.thoughtcrime.securesms.util.AsynchronousCallback;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
|
||||
final class ShareableGroupLinkViewModel extends ViewModel {
|
||||
|
||||
private final ShareableGroupLinkRepository repository;
|
||||
private final LiveData<GroupLinkUrlAndStatus> groupLink;
|
||||
private final SingleLiveEvent<String> toasts;
|
||||
private final SingleLiveEvent<Boolean> busy;
|
||||
|
||||
private ShareableGroupLinkViewModel(@NonNull GroupId.V2 groupId, @NonNull ShareableGroupLinkRepository repository) {
|
||||
this.repository = repository;
|
||||
this.groupLink = new LiveGroup(groupId).getGroupLink();
|
||||
this.toasts = new SingleLiveEvent<>();
|
||||
this.busy = new SingleLiveEvent<>();
|
||||
}
|
||||
|
||||
LiveData<GroupLinkUrlAndStatus> getGroupLink() {
|
||||
return groupLink;
|
||||
}
|
||||
|
||||
LiveData<String> getToasts() {
|
||||
return toasts;
|
||||
}
|
||||
|
||||
LiveData<Boolean> getBusy() {
|
||||
return busy;
|
||||
}
|
||||
|
||||
void onToggleGroupLink(@NonNull Context context) {
|
||||
busy.setValue(true);
|
||||
repository.toggleGroupLinkEnabled(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
busy.postValue(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onToggleApproveMembers(@NonNull Context context) {
|
||||
busy.setValue(true);
|
||||
repository.toggleGroupLinkApprovalRequired(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
busy.postValue(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void onResetLink(@NonNull Context context) {
|
||||
busy.setValue(true);
|
||||
repository.cycleGroupLinkPassword(new AsynchronousCallback.WorkerThread<Void, GroupChangeFailureReason>() {
|
||||
@Override
|
||||
public void onComplete(@Nullable Void result) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(R.string.ShareableGroupLinkDialogFragment__group_link_reset));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(@Nullable GroupChangeFailureReason error) {
|
||||
busy.postValue(false);
|
||||
toasts.postValue(context.getString(GroupErrors.getUserDisplayMessage(error)));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static final class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final GroupId.V2 groupId;
|
||||
private final ShareableGroupLinkRepository repository;
|
||||
|
||||
public Factory(@NonNull GroupId.V2 groupId, @NonNull ShareableGroupLinkRepository repository) {
|
||||
this.groupId = groupId;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ShareableGroupLinkViewModel(groupId, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user