Refactor conversation settings screens into a single fragment with new UI.

This commit is contained in:
Alex Hart
2021-06-24 13:52:54 -03:00
committed by Cody Henthorne
parent f19033a7a2
commit da2ee33dff
121 changed files with 4394 additions and 4076 deletions

View File

@@ -8,6 +8,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.FragmentActivity;
import com.annimon.stream.Stream;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
@@ -76,7 +77,7 @@ public final class LeaveGroupDialog {
}
private void showSelectNewAdminDialog() {
new AlertDialog.Builder(activity)
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.LeaveGroupDialog_choose_new_admin)
.setMessage(R.string.LeaveGroupDialog_before_you_leave_you_must_choose_at_least_one_new_admin_for_this_group)
.setNegativeButton(android.R.string.cancel, null)
@@ -85,7 +86,7 @@ public final class LeaveGroupDialog {
}
private void showLeaveDialog() {
new AlertDialog.Builder(activity)
new MaterialAlertDialogBuilder(activity)
.setTitle(R.string.LeaveGroupDialog_leave_group)
.setCancelable(true)
.setMessage(R.string.LeaveGroupDialog_you_will_no_longer_be_able_to_send_or_receive_messages_in_this_group)

View File

@@ -1,5 +1,7 @@
package org.thoughtcrime.securesms.groups.ui.addmembers;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
@@ -9,14 +11,20 @@ import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.ViewModelProviders;
import org.thoughtcrime.securesms.ContactSelectionActivity;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.ArrayList;
import java.util.List;
public class AddMembersActivity extends PushContactSelectionActivity {
public static final String GROUP_ID = "group_id";
@@ -24,6 +32,22 @@ public class AddMembersActivity extends PushContactSelectionActivity {
private View done;
private AddMembersViewModel viewModel;
public static @NonNull Intent createIntent(@NonNull Context context,
@NonNull GroupId groupId,
int displayModeFlags,
int selectionWarning,
int selectionLimit,
@NonNull List<RecipientId> membersWithoutSelf) {
Intent intent = new Intent(context, AddMembersActivity.class);
intent.putExtra(AddMembersActivity.GROUP_ID, groupId.toString());
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, displayModeFlags);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(selectionWarning, selectionLimit));
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(membersWithoutSelf));
return intent;
}
@Override
protected void onCreate(Bundle icicle, boolean ready) {
getIntent().putExtra(ContactSelectionActivity.EXTRA_LAYOUT_RES_ID, R.layout.add_members_activity);

View File

@@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.managegroup;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityOptionsCompat;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
public class ManageGroupActivity extends PassphraseRequiredActivity {
private static final String GROUP_ID = "GROUP_ID";
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
public static Intent newIntent(@NonNull Context context, @NonNull GroupId groupId) {
Intent intent = new Intent(context, ManageGroupActivity.class);
intent.putExtra(GROUP_ID, groupId.toString());
return intent;
}
public static @Nullable Bundle createTransitionBundle(@NonNull Context activityContext, @NonNull View from) {
if (activityContext instanceof Activity) {
return ActivityOptionsCompat.makeSceneTransitionAnimation((Activity) activityContext, from, "avatar").toBundle();
} else {
return null;
}
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
}
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
super.onCreate(savedInstanceState, ready);
getWindow().getDecorView().setSystemUiVisibility(getWindow().getDecorView().getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
setContentView(R.layout.group_manage_activity);
if (savedInstanceState == null) {
getSupportFragmentManager().beginTransaction()
.replace(R.id.container, ManageGroupFragment.newInstance(getIntent().getStringExtra(GROUP_ID)))
.commitNow();
}
}
@Override
public void onResume() {
super.onResume();
dynamicTheme.onResume(this);
}
}

View File

@@ -1,521 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.managegroup;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CompoundButton;
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.core.widget.TextViewCompat;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProviders;
import com.google.android.material.snackbar.Snackbar;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.AvatarPreviewActivity;
import org.thoughtcrime.securesms.InviteActivity;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MediaPreviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PushContactSelectionActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto80dp;
import org.thoughtcrime.securesms.conversation.colors.AvatarColor;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.GroupMemberListView;
import org.thoughtcrime.securesms.groups.ui.LeaveGroupDialog;
import org.thoughtcrime.securesms.groups.ui.invitesandrequests.ManagePendingAndRequestingMembersActivity;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupDescriptionDialog;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentDialog;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupRightsDialog;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.ui.migration.GroupsV1MigrationInitiationBottomSheetDialogFragment;
import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil;
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.profiles.edit.EditProfileActivity;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.disappearingmessages.RecipientDisappearingMessagesActivity;
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
import org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkDialogFragment;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import org.thoughtcrime.securesms.wallpaper.ChatWallpaperActivity;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
public class ManageGroupFragment extends LoggingFragment {
private static final String GROUP_ID = "GROUP_ID";
private static final String TAG = Log.tag(ManageGroupFragment.class);
private static final int RETURN_FROM_MEDIA = 33114;
private static final int PICK_CONTACT = 61341;
public static final String DIALOG_TAG = "DIALOG";
private ManageGroupViewModel viewModel;
private GroupMemberListView groupMemberList;
private View pendingAndRequestingRow;
private TextView pendingAndRequestingCount;
private Toolbar toolbar;
private TextView groupName;
private EmojiTextView groupDescription;
private LearnMoreTextView groupInfoText;
private TextView memberCountUnderAvatar;
private TextView memberCountAboveList;
private AvatarImageView avatar;
private ThreadPhotoRailView threadPhotoRailView;
private View groupMediaCard;
private View accessControlCard;
private View groupLinkCard;
private ManageGroupViewModel.CursorFactory cursorFactory;
private View sharedMediaRow;
private View editGroupAccessRow;
private TextView editGroupAccessValue;
private View editGroupMembershipRow;
private TextView editGroupMembershipValue;
private View disappearingMessagesCard;
private View disappearingMessagesRow;
private TextView disappearingMessages;
private View blockAndLeaveCard;
private TextView blockGroup;
private TextView unblockGroup;
private TextView leaveGroup;
private TextView addMembers;
private SwitchCompat muteNotificationsSwitch;
private View muteNotificationsRow;
private TextView muteNotificationsUntilLabel;
private TextView customNotificationsButton;
private View customNotificationsRow;
private View mentionsRow;
private TextView mentionsValue;
private View toggleAllMembers;
private View groupLinkRow;
private TextView groupLinkButton;
private TextView wallpaperButton;
static ManageGroupFragment newInstance(@NonNull String groupId) {
ManageGroupFragment fragment = new ManageGroupFragment();
Bundle args = new Bundle();
args.putString(GROUP_ID, groupId);
fragment.setArguments(args);
return fragment;
}
@Override
public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState)
{
View view = inflater.inflate(R.layout.group_manage_fragment, container, false);
avatar = view.findViewById(R.id.group_avatar);
toolbar = view.findViewById(R.id.toolbar);
groupName = view.findViewById(R.id.name);
groupDescription = view.findViewById(R.id.manage_group_description);
groupInfoText = view.findViewById(R.id.manage_group_info_text);
memberCountUnderAvatar = view.findViewById(R.id.member_count);
memberCountAboveList = view.findViewById(R.id.member_count_2);
groupMemberList = view.findViewById(R.id.group_members);
pendingAndRequestingRow = view.findViewById(R.id.pending_and_requesting_members_row);
pendingAndRequestingCount = view.findViewById(R.id.pending_and_requesting_members_count);
threadPhotoRailView = view.findViewById(R.id.recent_photos);
groupMediaCard = view.findViewById(R.id.group_media_card);
accessControlCard = view.findViewById(R.id.group_access_control_card);
groupLinkCard = view.findViewById(R.id.group_link_card);
sharedMediaRow = view.findViewById(R.id.shared_media_row);
editGroupAccessRow = view.findViewById(R.id.edit_group_access_row);
editGroupAccessValue = view.findViewById(R.id.edit_group_access_value);
editGroupMembershipRow = view.findViewById(R.id.edit_group_membership_row);
editGroupMembershipValue = view.findViewById(R.id.edit_group_membership_value);
disappearingMessagesCard = view.findViewById(R.id.group_disappearing_messages_card);
disappearingMessagesRow = view.findViewById(R.id.disappearing_messages_row);
disappearingMessages = view.findViewById(R.id.disappearing_messages);
blockAndLeaveCard = view.findViewById(R.id.group_block_and_leave_card);
blockGroup = view.findViewById(R.id.blockGroup);
unblockGroup = view.findViewById(R.id.unblockGroup);
leaveGroup = view.findViewById(R.id.leaveGroup);
addMembers = view.findViewById(R.id.add_members);
muteNotificationsUntilLabel = view.findViewById(R.id.group_mute_notifications_until);
muteNotificationsSwitch = view.findViewById(R.id.group_mute_notifications_switch);
muteNotificationsRow = view.findViewById(R.id.group_mute_notifications_row);
customNotificationsButton = view.findViewById(R.id.group_custom_notifications_button);
customNotificationsRow = view.findViewById(R.id.group_custom_notifications_row);
mentionsRow = view.findViewById(R.id.group_mentions_row);
mentionsValue = view.findViewById(R.id.group_mentions_value);
toggleAllMembers = view.findViewById(R.id.toggle_all_members);
groupLinkRow = view.findViewById(R.id.group_link_row);
groupLinkButton = view.findViewById(R.id.group_link_button);
wallpaperButton = view.findViewById(R.id.chat_wallpaper);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
Context context = requireContext();
GroupId groupId = getGroupId();
ManageGroupViewModel.Factory factory = new ManageGroupViewModel.Factory(context, groupId);
disappearingMessagesCard.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
blockAndLeaveCard.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
viewModel = ViewModelProviders.of(requireActivity(), factory).get(ManageGroupViewModel.class);
viewModel.getMembers().observe(getViewLifecycleOwner(), members -> groupMemberList.setMembers(members));
viewModel.getCanCollapseMemberList().observe(getViewLifecycleOwner(), canCollapseMemberList -> {
if (canCollapseMemberList) {
toggleAllMembers.setVisibility(View.VISIBLE);
toggleAllMembers.setOnClickListener(v -> viewModel.revealCollapsedMembers());
} else {
toggleAllMembers.setVisibility(View.GONE);
}
});
viewModel.getPendingAndRequestingCount().observe(getViewLifecycleOwner(), pendingAndRequestingCount -> {
pendingAndRequestingRow.setOnClickListener(v -> {
FragmentActivity activity = requireActivity();
activity.startActivity(ManagePendingAndRequestingMembersActivity.newIntent(activity, groupId.requireV2()));
});
if (pendingAndRequestingCount == 0) {
this.pendingAndRequestingCount.setVisibility(View.GONE);
} else {
this.pendingAndRequestingCount.setText(String.format(Locale.getDefault(), "%d", pendingAndRequestingCount));
this.pendingAndRequestingCount.setVisibility(View.VISIBLE);
}
});
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
toolbar.setOnMenuItemClickListener(this::onMenuItemSelected);
toolbar.inflateMenu(R.menu.manage_group_fragment);
viewModel.getCanEditGroupAttributes().observe(getViewLifecycleOwner(), canEdit -> {
toolbar.getMenu().findItem(R.id.action_edit).setVisible(canEdit);
disappearingMessages.setEnabled(canEdit);
disappearingMessagesRow.setEnabled(canEdit);
});
viewModel.getTitle().observe(getViewLifecycleOwner(), groupName::setText);
viewModel.getDescription().observe(getViewLifecycleOwner(), this::updateGroupDescription);
viewModel.getMemberCountSummary().observe(getViewLifecycleOwner(), memberCountUnderAvatar::setText);
viewModel.getFullMemberCountSummary().observe(getViewLifecycleOwner(), memberCountAboveList::setText);
viewModel.getGroupRecipient().observe(getViewLifecycleOwner(), groupRecipient -> {
avatar.setFallbackPhotoProvider(new FallbackPhotoProvider(groupRecipient.getAvatarColor()));
avatar.setRecipient(groupRecipient);
avatar.setOnClickListener(v -> {
FragmentActivity activity = requireActivity();
activity.startActivity(AvatarPreviewActivity.intentFromRecipientId(activity, groupRecipient.getId()),
AvatarPreviewActivity.createTransitionBundle(activity, avatar));
});
customNotificationsRow.setOnClickListener(v -> CustomNotificationsDialogFragment.create(groupRecipient.getId())
.show(requireFragmentManager(), DIALOG_TAG));
wallpaperButton.setOnClickListener(v -> startActivity(ChatWallpaperActivity.createIntent(requireContext(), groupRecipient.getId())));
Drawable colorCircle = groupRecipient.getChatColors().asCircle();
colorCircle.setBounds(0, 0, ViewUtil.dpToPx(16), ViewUtil.dpToPx(16));
TextViewCompat.setCompoundDrawablesRelative(wallpaperButton, null, null, colorCircle, null);
});
if (groupId.isV2()) {
groupLinkRow.setOnClickListener(v -> ShareableGroupLinkDialogFragment.create(groupId.requireV2())
.show(requireFragmentManager(), DIALOG_TAG));
viewModel.getGroupLinkOn().observe(getViewLifecycleOwner(), linkEnabled -> groupLinkButton.setText(booleanToOnOff(linkEnabled)));
}
viewModel.getGroupViewState().observe(getViewLifecycleOwner(), vs -> {
if (vs == null) return;
sharedMediaRow.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(context, vs.getThreadId())));
setMediaCursorFactory(vs.getMediaCursorFactory());
threadPhotoRailView.setListener(mediaRecord ->
startActivityForResult(MediaPreviewActivity.intentFromMediaRecord(context,
mediaRecord,
ViewUtil.isLtr(threadPhotoRailView)),
RETURN_FROM_MEDIA));
groupLinkCard.setVisibility(vs.getGroupRecipient().requireGroupId().isV2() ? View.VISIBLE : View.GONE);
});
leaveGroup.setVisibility(groupId.isPush() ? View.VISIBLE : View.GONE);
leaveGroup.setOnClickListener(v -> LeaveGroupDialog.handleLeavePushGroup(requireActivity(), groupId.requirePush(), () -> startActivity(MainActivity.clearTop(context))));
viewModel.getDisappearingMessageTimer().observe(getViewLifecycleOwner(), string -> disappearingMessages.setText(string));
disappearingMessagesRow.setOnClickListener(v -> {
Recipient recipient = viewModel.getGroupRecipient().getValue();
if (recipient != null) {
startActivity(RecipientDisappearingMessagesActivity.forRecipient(requireContext(), recipient.getId()));
}
});
blockGroup.setOnClickListener(v -> viewModel.blockAndLeave(requireActivity()));
unblockGroup.setOnClickListener(v -> viewModel.unblock(requireActivity()));
addMembers.setOnClickListener(v -> viewModel.onAddMembersClick(this, PICK_CONTACT));
viewModel.getMembershipRights().observe(getViewLifecycleOwner(), r -> {
if (r != null) {
editGroupMembershipValue.setText(r.getString());
editGroupMembershipRow.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.MEMBERSHIP, r, (from, to) -> viewModel.applyMembershipRightsChange(to)).show());
}
}
);
viewModel.getEditGroupAttributesRights().observe(getViewLifecycleOwner(), r -> {
if (r != null) {
editGroupAccessValue.setText(r.getString());
editGroupAccessRow.setOnClickListener(v -> new GroupRightsDialog(context, GroupRightsDialog.Type.ATTRIBUTES, r, (from, to) -> viewModel.applyAttributesRightsChange(to)).show());
}
}
);
viewModel.getIsAdmin().observe(getViewLifecycleOwner(), admin -> {
accessControlCard.setVisibility(admin ? View.VISIBLE : View.GONE);
editGroupMembershipRow.setEnabled(admin);
editGroupMembershipValue.setEnabled(admin);
editGroupAccessRow.setEnabled(admin);
editGroupAccessValue.setEnabled(admin);
});
viewModel.getCanAddMembers().observe(getViewLifecycleOwner(), canEdit -> addMembers.setVisibility(canEdit ? View.VISIBLE : View.GONE));
groupMemberList.setRecipientClickListener(recipient -> RecipientBottomSheetDialogFragment.create(recipient.getId(), groupId).show(requireFragmentManager(), "BOTTOM"));
groupMemberList.setOverScrollMode(View.OVER_SCROLL_NEVER);
final CompoundButton.OnCheckedChangeListener muteSwitchListener = (buttonView, isChecked) -> {
if (isChecked) {
MuteDialog.show(context, viewModel::setMuteUntil, () -> muteNotificationsSwitch.setChecked(false));
} else {
viewModel.clearMuteUntil();
}
};
muteNotificationsRow.setOnClickListener(v -> {
if (muteNotificationsSwitch.isEnabled()) {
muteNotificationsSwitch.toggle();
}
});
viewModel.getMuteState().observe(getViewLifecycleOwner(), muteState -> {
if (muteNotificationsSwitch.isChecked() != muteState.isMuted()) {
muteNotificationsSwitch.setOnCheckedChangeListener(null);
muteNotificationsSwitch.setChecked(muteState.isMuted());
}
muteNotificationsSwitch.setEnabled(true);
muteNotificationsSwitch.setOnCheckedChangeListener(muteSwitchListener);
muteNotificationsUntilLabel.setVisibility(muteState.isMuted() ? View.VISIBLE : View.GONE);
if (muteState.isMuted()) {
if (muteState.getMutedUntil() == Long.MAX_VALUE) {
muteNotificationsUntilLabel.setText(R.string.ManageGroupActivity_always);
} else {
muteNotificationsUntilLabel.setText(getString(R.string.ManageGroupActivity_until_s,
DateUtils.getTimeString(requireContext(),
Locale.getDefault(),
muteState.getMutedUntil())));
}
}
});
customNotificationsRow.setVisibility(View.VISIBLE);
if (NotificationChannels.supported()) {
viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
customNotificationsButton.setText(booleanToOnOff(hasCustomNotifications));
});
}
mentionsRow.setVisibility(groupId.isV2() ? View.VISIBLE : View.GONE);
mentionsRow.setOnClickListener(v -> viewModel.handleMentionNotificationSelection());
viewModel.getMentionSetting().observe(getViewLifecycleOwner(), value -> mentionsValue.setText(value));
viewModel.getCanLeaveGroup().observe(getViewLifecycleOwner(), canLeave -> leaveGroup.setVisibility(canLeave ? View.VISIBLE : View.GONE));
viewModel.getCanBlockGroup().observe(getViewLifecycleOwner(), canBlock -> blockGroup.setVisibility(canBlock ? View.VISIBLE : View.GONE));
viewModel.getCanUnblockGroup().observe(getViewLifecycleOwner(), canUnblock -> unblockGroup.setVisibility(canUnblock ? View.VISIBLE : View.GONE));
viewModel.getGroupInfoMessage().observe(getViewLifecycleOwner(), message -> {
switch (message) {
case LEGACY_GROUP_LEARN_MORE:
groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_learn_more);
groupInfoText.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
groupInfoText.setLearnMoreVisible(true);
groupInfoText.setVisibility(View.VISIBLE);
break;
case LEGACY_GROUP_UPGRADE:
groupInfoText.setText(R.string.ManageGroupActivity_legacy_group_upgrade);
groupInfoText.setOnLinkClickListener(v -> GroupsV1MigrationInitiationBottomSheetDialogFragment.showForInitiation(requireFragmentManager(), Recipient.externalPossiblyMigratedGroup(requireContext(), groupId).getId()));
groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_upgrade_this_group);
groupInfoText.setVisibility(View.VISIBLE);
break;
case LEGACY_GROUP_TOO_LARGE:
groupInfoText.setText(context.getString(R.string.ManageGroupActivity_legacy_group_too_large, FeatureFlags.groupLimits().getHardLimit() - 1));
groupInfoText.setLearnMoreVisible(false);
groupInfoText.setVisibility(View.VISIBLE);
break;
case MMS_WARNING:
groupInfoText.setText(R.string.ManageGroupActivity_this_is_an_insecure_mms_group);
groupInfoText.setOnLinkClickListener(v -> startActivity(new Intent(requireContext(), InviteActivity.class)));
groupInfoText.setLearnMoreVisible(true, R.string.ManageGroupActivity_invite_now);
groupInfoText.setVisibility(View.VISIBLE);
break;
default:
groupInfoText.setVisibility(View.GONE);
break;
}
});
}
private static int booleanToOnOff(boolean isOn) {
return isOn ? R.string.ManageGroupActivity_on
: R.string.ManageGroupActivity_off;
}
public boolean onMenuItemSelected(@NonNull MenuItem item) {
if (item.getItemId() == R.id.action_edit) {
startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId()));
return true;
}
return false;
}
private GroupId getGroupId() {
return GroupId.parseOrThrow(Objects.requireNonNull(requireArguments().getString(GROUP_ID)));
}
private void setMediaCursorFactory(@Nullable ManageGroupViewModel.CursorFactory cursorFactory) {
if (this.cursorFactory != cursorFactory) {
this.cursorFactory = cursorFactory;
applyMediaCursorFactory();
}
}
private void applyMediaCursorFactory() {
Context context = getContext();
if (context == null) return;
if (this.cursorFactory != null) {
Cursor cursor = this.cursorFactory.create();
getViewLifecycleOwner().getLifecycle().addObserver(new LifecycleCursorWrapper(cursor));
threadPhotoRailView.setCursor(GlideApp.with(context), cursor);
groupMediaCard.setVisibility(cursor.getCount() > 0 ? View.VISIBLE : View.GONE);
} else {
threadPhotoRailView.setCursor(GlideApp.with(context), null);
groupMediaCard.setVisibility(View.GONE);
}
}
private void updateGroupDescription(@NonNull ManageGroupViewModel.Description description) {
if (!TextUtils.isEmpty(description.getDescription()) || description.canEditDescription()) {
groupDescription.setVisibility(View.VISIBLE);
groupDescription.setMovementMethod(LongClickMovementMethod.getInstance(requireContext()));
memberCountUnderAvatar.setVisibility(View.GONE);
} else {
groupDescription.setVisibility(View.GONE);
groupDescription.setMovementMethod(null);
memberCountUnderAvatar.setVisibility(View.VISIBLE);
}
if (TextUtils.isEmpty(description.getDescription())) {
if (description.canEditDescription()) {
groupDescription.setOverflowText(null);
groupDescription.setText(R.string.ManageGroupActivity_add_group_description);
groupDescription.setOnClickListener(v -> startActivity(EditProfileActivity.getIntentForGroupProfile(requireActivity(), getGroupId())));
}
} else {
groupDescription.setOnClickListener(null);
GroupDescriptionUtil.setText(requireContext(),
groupDescription,
description.getDescription(),
description.shouldLinkifyWebLinks(),
() -> GroupDescriptionDialog.show(getChildFragmentManager(), getGroupId(), null, description.shouldLinkifyWebLinks()));
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RETURN_FROM_MEDIA) {
applyMediaCursorFactory();
} else if (requestCode == PICK_CONTACT && data != null) {
List<RecipientId> selected = data.getParcelableArrayListExtra(PushContactSelectionActivity.KEY_SELECTED_RECIPIENTS);
SimpleProgressDialog.DismissibleDialog progress = SimpleProgressDialog.showDelayed(requireContext());
viewModel.onAddMembers(selected, new AsynchronousCallback.MainThread<ManageGroupViewModel.AddMembersResult, GroupChangeFailureReason>() {
@Override
public void onComplete(ManageGroupViewModel.AddMembersResult result) {
progress.dismiss();
if (!result.getNewInvitedMembers().isEmpty()) {
GroupInviteSentDialog.showInvitesSent(requireContext(), result.getNewInvitedMembers());
}
if (result.getNumberOfMembersAdded() > 0) {
String string = getResources().getQuantityString(R.plurals.ManageGroupActivity_added,
result.getNumberOfMembersAdded(),
result.getNumberOfMembersAdded());
Snackbar.make(requireView(), string, Snackbar.LENGTH_SHORT).setTextColor(Color.WHITE).show();
}
}
@Override
public void onError(@Nullable GroupChangeFailureReason error) {
progress.dismiss();
Toast.makeText(requireContext(), GroupErrors.getUserDisplayMessage(error), Toast.LENGTH_LONG).show();
}
});
}
}
private final class FallbackPhotoProvider extends Recipient.FallbackPhotoProvider {
private final AvatarColor groupColors;
private FallbackPhotoProvider(@NonNull AvatarColor groupColors) {
this.groupColors = groupColors;
}
@Override
public @NonNull FallbackContactPhoto getPhotoForGroup() {
return new FallbackPhoto80dp(R.drawable.ic_group_80, groupColors.colorInt());
}
};
}

View File

@@ -1,233 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.managegroup;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.core.util.Consumer;
import com.annimon.stream.Stream;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.groups.GroupAccessControl;
import org.thoughtcrime.securesms.groups.GroupChangeException;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.GroupManager;
import org.thoughtcrime.securesms.groups.GroupProtoUtil;
import org.thoughtcrime.securesms.groups.MembershipNotSuitableForV2Exception;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupChangeErrorCallback;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
final class ManageGroupRepository {
private static final String TAG = Log.tag(ManageGroupRepository.class);
private final Context context;
ManageGroupRepository(@NonNull Context context) {
this.context = context;
}
void getGroupState(@NonNull GroupId groupId, @NonNull Consumer<GroupStateResult> onGroupStateLoaded) {
SignalExecutors.BOUNDED.execute(() -> onGroupStateLoaded.accept(getGroupState(groupId)));
}
void getGroupCapacity(@NonNull GroupId groupId, @NonNull Consumer<GroupCapacityResult> onGroupCapacityLoaded) {
SimpleTask.run(SignalExecutors.BOUNDED, () -> {
GroupDatabase.GroupRecord groupRecord = DatabaseFactory.getGroupDatabase(context).getGroup(groupId).get();
if (groupRecord.isV2Group()) {
DecryptedGroup decryptedGroup = groupRecord.requireV2GroupProperties().getDecryptedGroup();
List<RecipientId> pendingMembers = Stream.of(decryptedGroup.getPendingMembersList())
.map(member -> GroupProtoUtil.uuidByteStringToRecipientId(member.getUuid()))
.toList();
List<RecipientId> members = new LinkedList<>(groupRecord.getMembers());
members.addAll(pendingMembers);
return new GroupCapacityResult(members, FeatureFlags.groupLimits());
} else {
return new GroupCapacityResult(groupRecord.getMembers(), FeatureFlags.groupLimits());
}
}, onGroupCapacityLoaded::accept);
}
@WorkerThread
private GroupStateResult getGroupState(@NonNull GroupId groupId) {
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
Recipient groupRecipient = Recipient.externalGroupExact(context, groupId);
long threadId = threadDatabase.getThreadIdFor(groupRecipient);
return new GroupStateResult(threadId, groupRecipient);
}
void applyMembershipRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.applyMembershipAdditionRightsChange(context, groupId.requireV2(), newRights);
} catch (GroupChangeException | IOException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void applyAttributesRightsChange(@NonNull GroupId groupId, @NonNull GroupAccessControl newRights, @NonNull GroupChangeErrorCallback error) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.applyAttributesRightsChange(context, groupId.requireV2(), newRights);
} catch (GroupChangeException | IOException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.fromException(e));
}
});
}
public void getRecipient(@NonNull GroupId groupId, @NonNull Consumer<Recipient> recipientCallback) {
SimpleTask.run(SignalExecutors.BOUNDED,
() -> Recipient.externalGroupExact(context, groupId),
recipientCallback::accept);
}
void setMuteUntil(@NonNull GroupId groupId, long until) {
SignalExecutors.BOUNDED.execute(() -> {
RecipientId recipientId = Recipient.externalGroupExact(context, groupId).getId();
DatabaseFactory.getRecipientDatabase(context).setMuted(recipientId, until);
});
}
void addMembers(@NonNull GroupId groupId,
@NonNull List<RecipientId> selected,
@NonNull AsynchronousCallback.WorkerThread<ManageGroupViewModel.AddMembersResult, GroupChangeFailureReason> callback)
{
SignalExecutors.UNBOUNDED.execute(() -> {
try {
GroupManager.GroupActionResult groupActionResult = GroupManager.addMembers(context, groupId.requirePush(), selected);
callback.onComplete(new ManageGroupViewModel.AddMembersResult(groupActionResult.getAddedMemberCount(), Recipient.resolvedList(groupActionResult.getInvitedMembers())));
} catch (GroupChangeException | MembershipNotSuitableForV2Exception | IOException e) {
Log.w(TAG, e);
callback.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void blockAndLeaveGroup(@NonNull GroupId groupId, @NonNull GroupChangeErrorCallback error, @NonNull Runnable onSuccess) {
SignalExecutors.UNBOUNDED.execute(() -> {
try {
RecipientUtil.block(context, Recipient.externalGroupExact(context, groupId));
onSuccess.run();
} catch (GroupChangeException | IOException e) {
Log.w(TAG, e);
error.onError(GroupChangeFailureReason.fromException(e));
}
});
}
void setMentionSetting(@NonNull GroupId groupId, RecipientDatabase.MentionSetting mentionSetting) {
SignalExecutors.BOUNDED.execute(() -> {
RecipientId recipientId = Recipient.externalGroupExact(context, groupId).getId();
DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting);
});
}
@WorkerThread
boolean hasCustomNotifications(Recipient recipient) {
if (recipient.getNotificationChannel() != null || !NotificationChannels.supported()) {
return true;
}
return NotificationChannels.updateWithShortcutBasedChannel(context, recipient);
}
static final class GroupStateResult {
private final long threadId;
private final Recipient recipient;
private GroupStateResult(long threadId,
Recipient recipient)
{
this.threadId = threadId;
this.recipient = recipient;
}
long getThreadId() {
return threadId;
}
Recipient getRecipient() {
return recipient;
}
}
static final class GroupCapacityResult {
private final List<RecipientId> members;
private final SelectionLimits selectionLimits;
GroupCapacityResult(@NonNull List<RecipientId> members, @NonNull SelectionLimits selectionLimits) {
this.members = members;
this.selectionLimits = selectionLimits;
}
public @NonNull List<RecipientId> getMembers() {
return members;
}
public int getSelectionLimit() {
if (!selectionLimits.hasHardLimit()) {
return ContactSelectionListFragment.NO_LIMIT;
}
boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
return selectionLimits.getHardLimit() - (containsSelf ? 1 : 0);
}
public int getSelectionWarning() {
if (!selectionLimits.hasRecommendedLimit()) {
return ContactSelectionListFragment.NO_LIMIT;
}
boolean containsSelf = members.indexOf(Recipient.self().getId()) != -1;
return selectionLimits.getRecommendedLimit() - (containsSelf ? 1 : 0);
}
public int getRemainingCapacity() {
return selectionLimits.getHardLimit() - members.size();
}
public @NonNull List<RecipientId> getMembersWithoutSelf() {
ArrayList<RecipientId> recipientIds = new ArrayList<>(members.size());
RecipientId selfId = Recipient.self().getId();
for (RecipientId recipientId : members) {
if (!recipientId.equals(selfId)) {
recipientIds.add(recipientId);
}
}
return recipientIds;
}
}
}

View File

@@ -1,468 +0,0 @@
package org.thoughtcrime.securesms.groups.ui.managegroup;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.text.TextUtils;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.thoughtcrime.securesms.BlockUnblockDialog;
import org.thoughtcrime.securesms.ContactSelectionListFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
import org.thoughtcrime.securesms.database.MediaDatabase;
import org.thoughtcrime.securesms.database.MentionUtil;
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
import org.thoughtcrime.securesms.groups.GroupAccessControl;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.groups.LiveGroup;
import org.thoughtcrime.securesms.groups.SelectionLimits;
import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
import org.thoughtcrime.securesms.groups.ui.GroupLimitDialog;
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupMentionSettingDialog;
import org.thoughtcrime.securesms.groups.v2.GroupLinkUrlAndStatus;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.AsynchronousCallback;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.ExpirationUtil;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
import org.thoughtcrime.securesms.util.livedata.Store;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.util.ArrayList;
import java.util.List;
public class ManageGroupViewModel extends ViewModel {
private static final int MAX_UNCOLLAPSED_MEMBERS = 6;
private static final int SHOW_COLLAPSED_MEMBERS = 5;
private final Context context;
private final ManageGroupRepository manageGroupRepository;
private final LiveData<String> title;
private final Store<Description> descriptionStore;
private final LiveData<Description> description;
private final LiveData<Boolean> isAdmin;
private final LiveData<Boolean> canEditGroupAttributes;
private final LiveData<Boolean> canAddMembers;
private final LiveData<List<GroupMemberEntry.FullMember>> members;
private final LiveData<Integer> pendingMemberCount;
private final LiveData<Integer> pendingAndRequestingCount;
private final LiveData<String> disappearingMessageTimer;
private final LiveData<String> memberCountSummary;
private final LiveData<String> fullMemberCountSummary;
private final LiveData<GroupAccessControl> editMembershipRights;
private final LiveData<GroupAccessControl> editGroupAttributesRights;
private final LiveData<Recipient> groupRecipient;
private final MutableLiveData<GroupViewState> groupViewState = new MutableLiveData<>(null);
private final LiveData<MuteState> muteState;
private final LiveData<Boolean> hasCustomNotifications;
private final LiveData<Boolean> canCollapseMemberList;
private final DefaultValueLiveData<CollapseState> memberListCollapseState = new DefaultValueLiveData<>(CollapseState.COLLAPSED);
private final LiveData<Boolean> canLeaveGroup;
private final LiveData<Boolean> canBlockGroup;
private final LiveData<Boolean> canUnblockGroup;
private final LiveData<Boolean> showLegacyIndicator;
private final LiveData<String> mentionSetting;
private final LiveData<Boolean> groupLinkOn;
private final LiveData<GroupInfoMessage> groupInfoMessage;
private ManageGroupViewModel(@NonNull Context context, @NonNull GroupId groupId, @NonNull ManageGroupRepository manageGroupRepository) {
this.context = context;
this.manageGroupRepository = manageGroupRepository;
manageGroupRepository.getGroupState(groupId, this::groupStateLoaded);
LiveGroup liveGroup = new LiveGroup(groupId);
this.title = Transformations.map(liveGroup.getTitle(),
title -> TextUtils.isEmpty(title) ? context.getString(R.string.Recipient_unknown)
: title);
this.groupRecipient = liveGroup.getGroupRecipient();
this.isAdmin = liveGroup.isSelfAdmin();
this.canCollapseMemberList = LiveDataUtil.combineLatest(memberListCollapseState,
Transformations.map(liveGroup.getFullMembers(), m -> m.size() > MAX_UNCOLLAPSED_MEMBERS),
(state, hasEnoughMembers) -> state != CollapseState.OPEN && hasEnoughMembers);
this.members = LiveDataUtil.combineLatest(liveGroup.getFullMembers(),
memberListCollapseState,
ManageGroupViewModel::filterMemberList);
this.pendingMemberCount = liveGroup.getPendingMemberCount();
this.pendingAndRequestingCount = liveGroup.getPendingAndRequestingMemberCount();
this.showLegacyIndicator = Transformations.map(groupRecipient, recipient -> recipient.requireGroupId().isV1());
this.memberCountSummary = LiveDataUtil.combineLatest(liveGroup.getMembershipCountDescription(context.getResources()),
this.showLegacyIndicator,
(description, legacy) -> legacy ? String.format("%s · %s", description, context.getString(R.string.ManageGroupActivity_legacy_group))
: description);
this.fullMemberCountSummary = liveGroup.getFullMembershipCountDescription(context.getResources());
this.editMembershipRights = liveGroup.getMembershipAdditionAccessControl();
this.editGroupAttributesRights = liveGroup.getAttributesAccessControl();
this.disappearingMessageTimer = Transformations.map(liveGroup.getExpireMessages(), expiration -> ExpirationUtil.getExpirationDisplayValue(context, expiration));
this.canEditGroupAttributes = liveGroup.selfCanEditGroupAttributes();
this.canAddMembers = liveGroup.selfCanAddMembers();
this.muteState = Transformations.map(this.groupRecipient,
recipient -> new MuteState(recipient.getMuteUntil(), recipient.isMuted()));
this.hasCustomNotifications = LiveDataUtil.mapAsync(this.groupRecipient, manageGroupRepository::hasCustomNotifications);
this.canLeaveGroup = liveGroup.isActive();
this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> RecipientUtil.isBlockable(recipient) && !recipient.isBlocked());
this.canUnblockGroup = Transformations.map(this.groupRecipient, Recipient::isBlocked);
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
this.groupLinkOn = Transformations.map(liveGroup.getGroupLink(), GroupLinkUrlAndStatus::isEnabled);
this.groupInfoMessage = Transformations.map(this.groupRecipient,
recipient -> {
boolean showLegacyInfo = recipient.requireGroupId().isV1();
if (showLegacyInfo && recipient.getParticipants().size() > FeatureFlags.groupLimits().getHardLimit()) {
return GroupInfoMessage.LEGACY_GROUP_TOO_LARGE;
} else if (showLegacyInfo) {
return GroupInfoMessage.LEGACY_GROUP_UPGRADE;
} else if (groupId.isMms()) {
return GroupInfoMessage.MMS_WARNING;
} else {
return GroupInfoMessage.NONE;
}
});
this.descriptionStore = new Store<>(Description.NONE);
this.description = groupId.isV2() ? this.descriptionStore.getStateLiveData() : LiveDataUtil.empty();
if (groupId.isV2()) {
this.descriptionStore.update(liveGroup.getDescription(), (description, state) -> new Description(description, state.shouldLinkifyWebLinks, state.canEditDescription));
this.descriptionStore.update(LiveDataUtil.mapAsync(groupRecipient, r -> RecipientUtil.isMessageRequestAccepted(context, r)), (linkify, state) -> new Description(state.description, linkify, state.canEditDescription));
this.descriptionStore.update(this.canEditGroupAttributes, (canEdit, state) -> new Description(state.description, state.shouldLinkifyWebLinks, canEdit));
}
}
@WorkerThread
private void groupStateLoaded(@NonNull ManageGroupRepository.GroupStateResult groupStateResult) {
groupViewState.postValue(new GroupViewState(groupStateResult.getThreadId(),
groupStateResult.getRecipient(),
() -> new ThreadMediaLoader(context, groupStateResult.getThreadId(), MediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest).getCursor()));
}
LiveData<List<GroupMemberEntry.FullMember>> getMembers() {
return members;
}
LiveData<Integer> getPendingMemberCount() {
return pendingMemberCount;
}
LiveData<Integer> getPendingAndRequestingCount() {
return pendingAndRequestingCount;
}
LiveData<String> getMemberCountSummary() {
return memberCountSummary;
}
LiveData<String> getFullMemberCountSummary() {
return fullMemberCountSummary;
}
LiveData<Recipient> getGroupRecipient() {
return groupRecipient;
}
LiveData<GroupViewState> getGroupViewState() {
return groupViewState;
}
LiveData<String> getTitle() {
return title;
}
LiveData<Description> getDescription() {
return description;
}
LiveData<MuteState> getMuteState() {
return muteState;
}
LiveData<GroupAccessControl> getMembershipRights() {
return editMembershipRights;
}
LiveData<GroupAccessControl> getEditGroupAttributesRights() {
return editGroupAttributesRights;
}
LiveData<Boolean> getIsAdmin() {
return isAdmin;
}
LiveData<Boolean> getCanEditGroupAttributes() {
return canEditGroupAttributes;
}
LiveData<Boolean> getCanAddMembers() {
return canAddMembers;
}
LiveData<String> getDisappearingMessageTimer() {
return disappearingMessageTimer;
}
LiveData<Boolean> hasCustomNotifications() {
return hasCustomNotifications;
}
LiveData<Boolean> getCanCollapseMemberList() {
return canCollapseMemberList;
}
LiveData<Boolean> getCanBlockGroup() {
return canBlockGroup;
}
LiveData<Boolean> getCanUnblockGroup() {
return canUnblockGroup;
}
LiveData<Boolean> getCanLeaveGroup() {
return canLeaveGroup;
}
LiveData<String> getMentionSetting() {
return mentionSetting;
}
LiveData<Boolean> getGroupLinkOn() {
return groupLinkOn;
}
LiveData<GroupInfoMessage> getGroupInfoMessage() {
return groupInfoMessage;
}
void applyMembershipRightsChange(@NonNull GroupAccessControl newRights) {
manageGroupRepository.applyMembershipRightsChange(getGroupId(), newRights, this::showErrorToast);
}
void applyAttributesRightsChange(@NonNull GroupAccessControl newRights) {
manageGroupRepository.applyAttributesRightsChange(getGroupId(), newRights, this::showErrorToast);
}
void blockAndLeave(@NonNull FragmentActivity activity) {
manageGroupRepository.getRecipient(getGroupId(),
recipient -> BlockUnblockDialog.showBlockFor(activity,
activity.getLifecycle(),
recipient,
this::onBlockAndLeaveConfirmed));
}
void unblock(@NonNull FragmentActivity activity) {
manageGroupRepository.getRecipient(getGroupId(),
recipient -> BlockUnblockDialog.showUnblockFor(activity, activity.getLifecycle(), recipient,
() -> RecipientUtil.unblock(context, recipient)));
}
void onAddMembers(@NonNull List<RecipientId> selected,
@NonNull AsynchronousCallback.MainThread<AddMembersResult, GroupChangeFailureReason> callback)
{
manageGroupRepository.addMembers(getGroupId(), selected, callback.toWorkerCallback());
}
void setMuteUntil(long muteUntil) {
manageGroupRepository.setMuteUntil(getGroupId(), muteUntil);
}
void clearMuteUntil() {
manageGroupRepository.setMuteUntil(getGroupId(), 0);
}
void revealCollapsedMembers() {
memberListCollapseState.setValue(CollapseState.OPEN);
}
void handleMentionNotificationSelection() {
manageGroupRepository.getRecipient(getGroupId(), r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), setting -> manageGroupRepository.setMentionSetting(getGroupId(), setting)));
}
private void onBlockAndLeaveConfirmed() {
SimpleProgressDialog.DismissibleDialog dismissibleDialog = SimpleProgressDialog.showDelayed(context);
manageGroupRepository.blockAndLeaveGroup(getGroupId(),
e -> {
dismissibleDialog.dismiss();
showErrorToast(e);
},
dismissibleDialog::dismiss);
}
private @NonNull GroupId getGroupId() {
return groupRecipient.getValue().requireGroupId();
}
private static @NonNull List<GroupMemberEntry.FullMember> filterMemberList(@NonNull List<GroupMemberEntry.FullMember> members,
@NonNull CollapseState collapseState)
{
if (collapseState == CollapseState.COLLAPSED && members.size() > MAX_UNCOLLAPSED_MEMBERS) {
return members.subList(0, SHOW_COLLAPSED_MEMBERS);
} else {
return members;
}
}
@WorkerThread
private void showErrorToast(@NonNull GroupChangeFailureReason e) {
ThreadUtil.runOnMain(() -> Toast.makeText(context, GroupErrors.getUserDisplayMessage(e), Toast.LENGTH_LONG).show());
}
public void onAddMembersClick(@NonNull Fragment fragment, int resultCode) {
manageGroupRepository.getGroupCapacity(getGroupId(), capacity -> {
int remainingCapacity = capacity.getRemainingCapacity();
if (remainingCapacity <= 0) {
GroupLimitDialog.showHardLimitMessage(fragment.requireContext());
} else {
Intent intent = new Intent(fragment.requireActivity(), AddMembersActivity.class);
intent.putExtra(AddMembersActivity.GROUP_ID, getGroupId().toString());
intent.putExtra(ContactSelectionListFragment.DISPLAY_MODE, ContactsCursorLoader.DisplayMode.FLAG_PUSH);
intent.putExtra(ContactSelectionListFragment.SELECTION_LIMITS, new SelectionLimits(capacity.getSelectionWarning(), capacity.getSelectionLimit()));
intent.putParcelableArrayListExtra(ContactSelectionListFragment.CURRENT_SELECTION, new ArrayList<>(capacity.getMembersWithoutSelf()));
fragment.startActivityForResult(intent, resultCode);
}
});
}
static final class AddMembersResult {
private final int numberOfMembersAdded;
private final List<Recipient> newInvitedMembers;
AddMembersResult(int numberOfMembersAdded, @NonNull List<Recipient> newInvitedMembers) {
this.numberOfMembersAdded = numberOfMembersAdded;
this.newInvitedMembers = newInvitedMembers;
}
int getNumberOfMembersAdded() {
return numberOfMembersAdded;
}
List<Recipient> getNewInvitedMembers() {
return newInvitedMembers;
}
}
static final class GroupViewState {
private final long threadId;
@NonNull private final Recipient groupRecipient;
@NonNull private final CursorFactory mediaCursorFactory;
private GroupViewState(long threadId,
@NonNull Recipient groupRecipient,
@NonNull CursorFactory mediaCursorFactory)
{
this.threadId = threadId;
this.groupRecipient = groupRecipient;
this.mediaCursorFactory = mediaCursorFactory;
}
long getThreadId() {
return threadId;
}
@NonNull Recipient getGroupRecipient() {
return groupRecipient;
}
@NonNull CursorFactory getMediaCursorFactory() {
return mediaCursorFactory;
}
}
static final class MuteState {
private final long mutedUntil;
private final boolean isMuted;
MuteState(long mutedUntil, boolean isMuted) {
this.mutedUntil = mutedUntil;
this.isMuted = isMuted;
}
public long getMutedUntil() {
return mutedUntil;
}
public boolean isMuted() {
return isMuted;
}
}
enum GroupInfoMessage {
NONE,
LEGACY_GROUP_LEARN_MORE,
LEGACY_GROUP_UPGRADE,
LEGACY_GROUP_TOO_LARGE,
MMS_WARNING
}
private enum CollapseState {
OPEN,
COLLAPSED
}
interface CursorFactory {
Cursor create();
}
public static class Description {
private static final Description NONE = new Description("", false, false);
private final String description;
private final boolean shouldLinkifyWebLinks;
private final boolean canEditDescription;
public Description(String description, boolean shouldLinkifyWebLinks, boolean canEditDescription) {
this.description = description;
this.shouldLinkifyWebLinks = shouldLinkifyWebLinks;
this.canEditDescription = canEditDescription;
}
public @NonNull String getDescription() {
return description;
}
public boolean shouldLinkifyWebLinks() {
return shouldLinkifyWebLinks;
}
public boolean canEditDescription() {
return canEditDescription;
}
}
public static class Factory implements ViewModelProvider.Factory {
private final Context context;
private final GroupId groupId;
public Factory(@NonNull Context context, @NonNull GroupId groupId) {
this.context = context;
this.groupId = groupId;
}
@Override
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection unchecked
return (T) new ManageGroupViewModel(context, groupId, new ManageGroupRepository(context.getApplicationContext()));
}
}
}