diff --git a/app/src/main/java/awais/instagrabber/activities/MainActivity.java b/app/src/main/java/awais/instagrabber/activities/MainActivity.java index fa022192..117ded8b 100644 --- a/app/src/main/java/awais/instagrabber/activities/MainActivity.java +++ b/app/src/main/java/awais/instagrabber/activities/MainActivity.java @@ -700,4 +700,8 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage }); EmojiCompat.init(config); } + + public Toolbar getToolbar() { + return binding.toolbar; + } } \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/DirectPendingUsersAdapter.java b/app/src/main/java/awais/instagrabber/adapters/DirectPendingUsersAdapter.java new file mode 100644 index 00000000..cf50c7d8 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/DirectPendingUsersAdapter.java @@ -0,0 +1,117 @@ +package awais.instagrabber.adapters; + +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import awais.instagrabber.adapters.viewholder.directmessages.DirectPendingUserViewHolder; +import awais.instagrabber.databinding.LayoutDmPendingUserItemBinding; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; + +public final class DirectPendingUsersAdapter extends ListAdapter { + + private static final DiffUtil.ItemCallback DIFF_CALLBACK = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final PendingUser oldItem, @NonNull final PendingUser newItem) { + return oldItem.user.getPk() == newItem.user.getPk(); + } + + @Override + public boolean areContentsTheSame(@NonNull final PendingUser oldItem, @NonNull final PendingUser newItem) { + return Objects.equals(oldItem.user.getUsername(), newItem.user.getUsername()) && + Objects.equals(oldItem.user.getFullName(), newItem.user.getFullName()) && + Objects.equals(oldItem.requester, newItem.requester); + } + }; + + private final PendingUserCallback callback; + + public DirectPendingUsersAdapter(final PendingUserCallback callback) { + super(DIFF_CALLBACK); + this.callback = callback; + setHasStableIds(true); + } + + public void submitPendingRequests(final DirectThreadParticipantRequestsResponse requests) { + if (requests == null || requests.getUsers() == null) { + submitList(Collections.emptyList()); + return; + } + submitList(parse(requests)); + } + + private List parse(final DirectThreadParticipantRequestsResponse requests) { + final List users = requests.getUsers(); + final Map requesterUsernames = requests.getRequesterUsernames(); + return users.stream() + .map(user -> new PendingUser(user, requesterUsernames.get(user.getPk()))) + .collect(Collectors.toList()); + } + + @NonNull + @Override + public DirectPendingUserViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final LayoutDmPendingUserItemBinding binding = LayoutDmPendingUserItemBinding.inflate(layoutInflater, parent, false); + return new DirectPendingUserViewHolder(binding, callback); + } + + @Override + public void onBindViewHolder(@NonNull final DirectPendingUserViewHolder holder, final int position) { + final PendingUser pendingUser = getItem(position); + holder.bind(position, pendingUser); + } + + @Override + public long getItemId(final int position) { + final PendingUser item = getItem(position); + return item.user.getPk(); + } + + public static class PendingUser { + private final User user; + private final String requester; + + private boolean inProgress; + + public PendingUser(final User user, final String requester) { + this.user = user; + this.requester = requester; + } + + public User getUser() { + return user; + } + + public String getRequester() { + return requester; + } + + public boolean isInProgress() { + return inProgress; + } + + public PendingUser setInProgress(final boolean inProgress) { + this.inProgress = inProgress; + return this; + } + } + + public interface PendingUserCallback { + void onClick(int position, PendingUser pendingUser); + + void onApprove(int position, PendingUser pendingUser); + + void onDeny(int position, PendingUser pendingUser); + } +} \ No newline at end of file diff --git a/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectPendingUserViewHolder.java b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectPendingUserViewHolder.java new file mode 100644 index 00000000..bc982822 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/viewholder/directmessages/DirectPendingUserViewHolder.java @@ -0,0 +1,89 @@ +package awais.instagrabber.adapters.viewholder.directmessages; + +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.util.Log; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.RecyclerView; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser; +import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback; +import awais.instagrabber.customviews.VerticalImageSpan; +import awais.instagrabber.databinding.LayoutDmPendingUserItemBinding; +import awais.instagrabber.repositories.responses.User; +import awais.instagrabber.utils.Utils; + +public class DirectPendingUserViewHolder extends RecyclerView.ViewHolder { + private static final String TAG = DirectPendingUserViewHolder.class.getSimpleName(); + + private final LayoutDmPendingUserItemBinding binding; + private final PendingUserCallback callback; + private final int drawableSize; + + private VerticalImageSpan verifiedSpan; + + public DirectPendingUserViewHolder(@NonNull final LayoutDmPendingUserItemBinding binding, + final PendingUserCallback callback) { + super(binding.getRoot()); + this.binding = binding; + this.callback = callback; + drawableSize = Utils.convertDpToPx(24); + } + + public void bind(final int position, final PendingUser pendingUser) { + if (pendingUser == null) return; + binding.getRoot().setOnClickListener(v -> { + if (callback == null) return; + callback.onClick(position, pendingUser); + }); + setUsername(pendingUser); + binding.requester.setText(itemView.getResources().getString(R.string.added_by, pendingUser.getRequester())); + binding.profilePic.setImageURI(pendingUser.getUser().getProfilePicUrl()); + if (pendingUser.isInProgress()) { + binding.approve.setVisibility(View.GONE); + binding.deny.setVisibility(View.GONE); + binding.progress.setVisibility(View.VISIBLE); + return; + } + binding.approve.setVisibility(View.VISIBLE); + binding.deny.setVisibility(View.VISIBLE); + binding.progress.setVisibility(View.GONE); + binding.approve.setOnClickListener(v -> { + if (callback == null) return; + callback.onApprove(position, pendingUser); + }); + binding.deny.setOnClickListener(v -> { + if (callback == null) return; + callback.onDeny(position, pendingUser); + }); + } + + private void setUsername(final PendingUser pendingUser) { + final User user = pendingUser.getUser(); + final SpannableStringBuilder sb = new SpannableStringBuilder(user.getUsername()); + if (user.isVerified()) { + if (verifiedSpan == null) { + final Drawable verifiedDrawable = AppCompatResources.getDrawable(itemView.getContext(), R.drawable.verified); + if (verifiedDrawable != null) { + final Drawable drawable = verifiedDrawable.mutate(); + drawable.setBounds(0, 0, drawableSize, drawableSize); + verifiedSpan = new VerticalImageSpan(drawable); + } + } + try { + if (verifiedSpan != null) { + sb.append(" "); + sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } catch (Exception e) { + Log.e(TAG, "bind: ", e); + } + } + binding.username.setText(sb); + } +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java new file mode 100644 index 00000000..caba0b38 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/ConfirmDialogFragment.java @@ -0,0 +1,103 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.fragment.app.DialogFragment; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; + +import awais.instagrabber.R; + +public class ConfirmDialogFragment extends DialogFragment { + private Context context; + private ConfirmDialogFragmentCallback callback; + + @NonNull + public static ConfirmDialogFragment newInstance(final int requestCode, + @StringRes final int title, + @StringRes final int message, + @StringRes final int positiveText, + @StringRes final int negativeText, + @StringRes final int neutralText) { + Bundle args = new Bundle(); + args.putInt("requestCode", requestCode); + args.putInt("title", title); + args.putInt("message", message); + args.putInt("positive", positiveText); + args.putInt("negative", negativeText); + args.putInt("neutral", neutralText); + ConfirmDialogFragment fragment = new ConfirmDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + public ConfirmDialogFragment() {} + + @Override + public void onAttach(@NonNull final Context context) { + super.onAttach(context); + try { + callback = (ConfirmDialogFragmentCallback) getParentFragment(); + } catch (ClassCastException e) { + throw new ClassCastException("Calling fragment must implement ConfirmDialogFragmentCallback interface"); + } + this.context = context; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable final Bundle savedInstanceState) { + final Bundle arguments = getArguments(); + int title = -1; + int message = -1; + int positiveButtonText = R.string.ok; + int negativeButtonText = R.string.cancel; + int neutralButtonText = -1; + final int requestCode; + if (arguments != null) { + title = arguments.getInt("title", -1); + message = arguments.getInt("message", -1); + positiveButtonText = arguments.getInt("positive", R.string.ok); + negativeButtonText = arguments.getInt("negative", R.string.cancel); + neutralButtonText = arguments.getInt("neutral", -1); + requestCode = arguments.getInt("requestCode", 0); + } else { + requestCode = 0; + } + final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(context) + .setPositiveButton(positiveButtonText, (d, w) -> { + if (callback == null) return; + callback.onPositiveButtonClicked(requestCode); + }) + .setNegativeButton(negativeButtonText, (dialog, which) -> { + if (callback == null) return; + callback.onNegativeButtonClicked(requestCode); + }); + if (title > 0) { + builder.setTitle(title); + } + if (message > 0) { + builder.setMessage(message); + } + if (neutralButtonText > 0) { + builder.setNeutralButton(neutralButtonText, (dialog, which) -> { + if (callback == null) return; + callback.onNeutralButtonClicked(requestCode); + }); + } + return builder.create(); + } + + public interface ConfirmDialogFragmentCallback { + void onPositiveButtonClicked(int requestCode); + + void onNegativeButtonClicked(int requestCode); + + void onNeutralButtonClicked(int requestCode); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java index 0bb6c1d8..7de9533e 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java @@ -34,11 +34,17 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import awais.instagrabber.ProfileNavGraphDirections; import awais.instagrabber.R; import awais.instagrabber.UserSearchNavGraphDirections; +import awais.instagrabber.adapters.DirectPendingUsersAdapter; +import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUser; +import awais.instagrabber.adapters.DirectPendingUsersAdapter.PendingUserCallback; import awais.instagrabber.adapters.DirectUsersAdapter; import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; +import awais.instagrabber.dialogs.ConfirmDialogFragment; +import awais.instagrabber.dialogs.ConfirmDialogFragment.ConfirmDialogFragmentCallback; import awais.instagrabber.dialogs.MultiOptionDialogFragment; import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; import awais.instagrabber.fragments.UserSearchFragment; @@ -46,16 +52,21 @@ import awais.instagrabber.fragments.UserSearchFragmentDirections; import awais.instagrabber.models.Resource; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectThread; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.viewmodels.DirectInboxViewModel; import awais.instagrabber.viewmodels.DirectSettingsViewModel; -public class DirectMessageSettingsFragment extends Fragment { +public class DirectMessageSettingsFragment extends Fragment implements ConfirmDialogFragmentCallback { private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName(); + public static final int APPROVAL_REQUIRED_REQUEST_CODE = 200; private FragmentDirectMessagesSettingsBinding binding; private DirectSettingsViewModel viewModel; private DirectUsersAdapter usersAdapter; + private boolean isPendingRequestsSetupDone = false; + private DirectPendingUsersAdapter pendingUsersAdapter; + private Set approvalRequiredUsers; // private List> options; @Override @@ -165,6 +176,7 @@ public class DirectMessageSettingsFragment extends Fragment { public void onDestroyView() { super.onDestroyView(); binding = null; + isPendingRequestsSetupDone = false; } private void setupObservers() { @@ -178,18 +190,21 @@ public class DirectMessageSettingsFragment extends Fragment { usersAdapter.setAdminUserIds(adminUserIds); }); viewModel.getMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted)); + if (viewModel.isViewerAdmin()) { + viewModel.getApprovalRequiredToJoin().observe(getViewLifecycleOwner(), required -> binding.approvalRequired.setChecked(required)); + viewModel.getPendingRequests().observe(getViewLifecycleOwner(), this::setPendingRequests); + } final NavController navController = NavHostFragment.findNavController(this); final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); if (backStackEntry != null) { final MutableLiveData resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); resultLiveData.observe(getViewLifecycleOwner(), result -> { - LiveData> detailsChangeResourceLiveData = null; if ((result instanceof RankedRecipient)) { final RankedRecipient recipient = (RankedRecipient) result; final User user = getUser(recipient); // Log.d(TAG, "result: " + user); if (user != null) { - detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton(recipient.getUser())); + addMembers(Collections.singleton(recipient.getUser())); } } else if ((result instanceof Set)) { try { @@ -201,19 +216,35 @@ public class DirectMessageSettingsFragment extends Fragment { .filter(Objects::nonNull) .collect(Collectors.toSet()); // Log.d(TAG, "result: " + users); - detailsChangeResourceLiveData = viewModel.addMembers(users); + addMembers(users); } catch (Exception e) { Log.e(TAG, "search users result: ", e); Snackbar.make(binding.getRoot(), e.getMessage() != null ? e.getMessage() : "", Snackbar.LENGTH_LONG).show(); } } - if (detailsChangeResourceLiveData != null) { - observeDetailsChange(detailsChangeResourceLiveData); - } }); } } + private void addMembers(final Set users) { + final Boolean approvalRequired = viewModel.getApprovalRequiredToJoin().getValue(); + if (!viewModel.isViewerAdmin() && approvalRequired != null && approvalRequired) { + approvalRequiredUsers = users; + final ConfirmDialogFragment confirmDialogFragment = ConfirmDialogFragment.newInstance( + APPROVAL_REQUIRED_REQUEST_CODE, + R.string.admin_approval_required, + R.string.admin_approval_required_description, + R.string.ok, + R.string.cancel, + -1 + ); + confirmDialogFragment.show(getChildFragmentManager(), "approval_required_dialog"); + return; + } + final LiveData> detailsChangeResourceLiveData = viewModel.addMembers(users); + observeDetailsChange(detailsChangeResourceLiveData); + } + @Nullable private User getUser(@NonNull final RankedRecipient recipient) { User user = null; @@ -279,16 +310,28 @@ public class DirectMessageSettingsFragment extends Fragment { binding.muteMessagesLabel.setOnClickListener(v -> binding.muteMessages.toggle()); binding.muteMessages.setOnCheckedChangeListener((buttonView, isChecked) -> { final LiveData> resourceLiveData = isChecked ? viewModel.mute() : viewModel.unmute(); - handleMuteChangeResource(resourceLiveData, buttonView); + handleSwitchChangeResource(resourceLiveData, buttonView); }); binding.muteMentionsLabel.setOnClickListener(v -> binding.muteMentions.toggle()); binding.muteMentions.setOnCheckedChangeListener((buttonView, isChecked) -> { final LiveData> resourceLiveData = isChecked ? viewModel.muteMentions() : viewModel.unmuteMentions(); - handleMuteChangeResource(resourceLiveData, buttonView); + handleSwitchChangeResource(resourceLiveData, buttonView); + }); + if (!viewModel.isViewerAdmin()) { + binding.pendingMembersGroup.setVisibility(View.GONE); + binding.approvalRequired.setVisibility(View.GONE); + binding.approvalRequiredLabel.setVisibility(View.GONE); + return; + } + binding.approvalRequired.setVisibility(View.VISIBLE); + binding.approvalRequiredLabel.setVisibility(View.VISIBLE); + binding.approvalRequiredLabel.setOnClickListener(v -> binding.approvalRequired.toggle()); + binding.approvalRequired.setOnCheckedChangeListener((buttonView, isChecked) -> { + }); } - private void handleMuteChangeResource(final LiveData> resourceLiveData, final CompoundButton buttonView) { + private void handleSwitchChangeResource(final LiveData> resourceLiveData, final CompoundButton buttonView) { resourceLiveData.observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; switch (resource.status) { @@ -297,6 +340,7 @@ public class DirectMessageSettingsFragment extends Fragment { break; case ERROR: buttonView.setEnabled(true); + buttonView.setChecked(!buttonView.isChecked()); if (resource.message != null) { Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); } @@ -316,7 +360,10 @@ public class DirectMessageSettingsFragment extends Fragment { usersAdapter = new DirectUsersAdapter( inviter != null ? inviter.getPk() : -1, (position, user, selected) -> { - // navigate to profile + final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections + .actionGlobalProfileFragment() + .setUsername("@" + user.getUsername()); + NavHostFragment.findNavController(this).navigate(directions); }, (position, user) -> { final ArrayList> options = viewModel.createUserOptions(user); @@ -340,6 +387,45 @@ public class DirectMessageSettingsFragment extends Fragment { binding.users.setAdapter(usersAdapter); } + private void setPendingRequests(final DirectThreadParticipantRequestsResponse requests) { + if (requests == null || requests.getUsers() == null || requests.getUsers().isEmpty()) { + binding.pendingMembersGroup.setVisibility(View.GONE); + return; + } + if (!isPendingRequestsSetupDone) { + final Context context = getContext(); + if (context == null) return; + binding.pendingMembers.setLayoutManager(new LinearLayoutManager(context)); + pendingUsersAdapter = new DirectPendingUsersAdapter(new PendingUserCallback() { + @Override + public void onClick(final int position, final PendingUser pendingUser) { + final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections + .actionGlobalProfileFragment() + .setUsername("@" + pendingUser.getUser().getUsername()); + NavHostFragment.findNavController(DirectMessageSettingsFragment.this).navigate(directions); + } + + @Override + public void onApprove(final int position, final PendingUser pendingUser) { + final LiveData> resourceLiveData = viewModel.approveUsers(Collections.singletonList(pendingUser.getUser())); + observeApprovalChange(resourceLiveData, position, pendingUser); + } + + @Override + public void onDeny(final int position, final PendingUser pendingUser) { + final LiveData> resourceLiveData = viewModel.denyUsers(Collections.singletonList(pendingUser.getUser())); + observeApprovalChange(resourceLiveData, position, pendingUser); + } + }); + binding.pendingMembers.setAdapter(pendingUsersAdapter); + binding.pendingMembersGroup.setVisibility(View.VISIBLE); + isPendingRequestsSetupDone = true; + } + if (pendingUsersAdapter != null) { + pendingUsersAdapter.submitPendingRequests(requests); + } + } + private void observeDetailsChange(@NonNull final LiveData> detailsChangeResourceLiveData) { detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; @@ -356,6 +442,48 @@ public class DirectMessageSettingsFragment extends Fragment { }); } + private void observeApprovalChange(@NonNull final LiveData> detailsChangeResourceLiveData, + final int position, + @NonNull final PendingUser pendingUser) { + detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + switch (resource.status) { + case SUCCESS: + // pending user will be removed from the list, so no need to set the progress to false + // pendingUser.setInProgress(false); + break; + case LOADING: + pendingUser.setInProgress(true); + break; + case ERROR: + pendingUser.setInProgress(false); + if (resource.message != null) { + Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); + } + break; + } + pendingUsersAdapter.notifyItemChanged(position); + }); + } + + @Override + public void onPositiveButtonClicked(final int requestCode) { + if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE && approvalRequiredUsers != null) { + final LiveData> detailsChangeResourceLiveData = viewModel.addMembers(approvalRequiredUsers); + observeDetailsChange(detailsChangeResourceLiveData); + } + } + + @Override + public void onNegativeButtonClicked(final int requestCode) { + if (requestCode == APPROVAL_REQUIRED_REQUEST_CODE) { + approvalRequiredUsers = null; + } + } + + @Override + public void onNeutralButtonClicked(final int requestCode) {} + // class ChangeSettings extends AsyncTask { // String action, argument; // boolean ok = false; diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index c4671f51..9f15a5db 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -4,6 +4,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -49,6 +50,8 @@ import androidx.transition.TransitionManager; import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; +import com.google.android.material.badge.BadgeDrawable; +import com.google.android.material.badge.BadgeUtils; import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; @@ -58,6 +61,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import awais.instagrabber.ProfileNavGraphDirections; import awais.instagrabber.R; import awais.instagrabber.UserSearchNavGraphDirections; import awais.instagrabber.activities.CameraActivity; @@ -140,6 +144,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private DirectItem itemToForward; private MutableLiveData backStackSavedStateResultLiveData; private int prevLength; + private BadgeDrawable pendingRequestCountBadgeDrawable; + private boolean isPendingRequestCountBadgeAttached = false; private final AppExecutors appExecutors = AppExecutors.getInstance(); private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { @@ -413,6 +419,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } binding.send.stopScale(); setupBackStackResultObserver(); + attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); } @Override @@ -421,6 +428,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact cleanup(); } + @SuppressLint("UnsafeExperimentalUsageError") private void cleanup() { if (prevTitleRunnable != null) { appExecutors.mainThread().cancel(prevTitleRunnable); @@ -439,6 +447,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact ((DirectItemViewHolder) holder).cleanup(); } } + isPendingRequestCountBadgeAttached = false; + if (pendingRequestCountBadgeDrawable != null) { + BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + pendingRequestCountBadgeDrawable = null; + } } private void init() { @@ -705,6 +718,28 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } prevLength = length; }); + viewModel.getPendingRequestsCount().observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge); + } + + @SuppressLint("UnsafeExperimentalUsageError") + private void attachPendingRequestsBadge(@Nullable final Integer count) { + if (pendingRequestCountBadgeDrawable == null) { + final Context context = getContext(); + if (context == null) return; + pendingRequestCountBadgeDrawable = BadgeDrawable.create(context); + } + if (count == null || count == 0) { + BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + isPendingRequestCountBadgeAttached = false; + pendingRequestCountBadgeDrawable.setNumber(0); + return; + } + if (pendingRequestCountBadgeDrawable.getNumber() == count) return; + pendingRequestCountBadgeDrawable.setNumber(count); + if (!isPendingRequestCountBadgeAttached) { + BadgeUtils.attachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); + isPendingRequestCountBadgeAttached = true; + } } private void showExtraInputOption(final boolean show) { @@ -1396,9 +1431,10 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact } private void navigateToUser(@NonNull final String username) { - final Bundle bundle = new Bundle(); - bundle.putString("username", "@" + username); - NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(R.id.action_global_profileFragment, bundle); + final ProfileNavGraphDirections.ActionGlobalProfileFragment direction = ProfileNavGraphDirections + .actionGlobalProfileFragment() + .setUsername("@" + username); + NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(direction); } public static class ItemsAdapterDataMerger extends MediatorLiveData> { diff --git a/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java b/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java index aec63ef7..defe235a 100644 --- a/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java +++ b/app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java @@ -8,6 +8,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; import retrofit2.Call; import retrofit2.http.FieldMap; @@ -15,6 +16,7 @@ import retrofit2.http.FormUrlEncoded; import retrofit2.http.GET; import retrofit2.http.POST; import retrofit2.http.Path; +import retrofit2.http.Query; import retrofit2.http.QueryMap; public interface DirectMessagesRepository { @@ -95,4 +97,19 @@ public interface DirectMessagesRepository { @POST("/api/v1/direct_v2/threads/{threadId}/unmute_mentions/") Call unmuteMentions(@Path("threadId") String threadId, @FieldMap final Map form); + + @GET("/api/v1/direct_v2/threads/{threadId}/participant_requests/") + Call participantRequests(@Path("threadId") String threadId, + @Query("page_size") int pageSize, + @Query("cursor") String cursor); + + @FormUrlEncoded + @POST("/api/v1/direct_v2/threads/{threadId}/approve_participant_requests/") + Call approveParticipantRequests(@Path("threadId") String threadId, + @FieldMap final Map form); + + @FormUrlEncoded + @POST("/api/v1/direct_v2/threads/{threadId}/deny_participant_requests/") + Call declineParticipantRequests(@Path("threadId") String threadId, + @FieldMap final Map form); } diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java index 2eaa9eab..b2099056 100644 --- a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java @@ -41,6 +41,7 @@ public class DirectThread implements Serializable { private final boolean isSpam; private final DirectItem lastPermanentItem; private final DirectThreadDirectStory directStory; + private final boolean approvalRequiredForNewMembers; public DirectThread(final String threadId, final String threadV2Id, @@ -72,7 +73,8 @@ public class DirectThread implements Serializable { final String oldestCursor, final boolean isSpam, final DirectItem lastPermanentItem, - final DirectThreadDirectStory directStory) { + final DirectThreadDirectStory directStory, + final boolean approvalRequiredForNewMembers) { this.threadId = threadId; this.threadV2Id = threadV2Id; this.users = users; @@ -104,6 +106,7 @@ public class DirectThread implements Serializable { this.isSpam = isSpam; this.lastPermanentItem = lastPermanentItem; this.directStory = directStory; + this.approvalRequiredForNewMembers = approvalRequiredForNewMembers; } public String getThreadId() { @@ -238,6 +241,10 @@ public class DirectThread implements Serializable { return directStory; } + public boolean isApprovalRequiredForNewMembers() { + return approvalRequiredForNewMembers; + } + @Nullable public DirectItem getFirstDirectItem() { DirectItem firstItem = null; diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java new file mode 100644 index 00000000..adb6780c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadParticipantRequestsResponse.java @@ -0,0 +1,66 @@ +package awais.instagrabber.repositories.responses.directmessages; + +import androidx.annotation.NonNull; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +import awais.instagrabber.repositories.responses.User; + +public class DirectThreadParticipantRequestsResponse implements Serializable, Cloneable { + private List users; + private final Map requesterUsernames; + private final String cursor; + private final int totalThreadParticipants; + private final int totalParticipantRequests; + private final String status; + + public DirectThreadParticipantRequestsResponse(final List users, + final Map requesterUsernames, + final String cursor, + final int totalThreadParticipants, + final int totalParticipantRequests, + final String status) { + this.users = users; + this.requesterUsernames = requesterUsernames; + this.cursor = cursor; + this.totalThreadParticipants = totalThreadParticipants; + this.totalParticipantRequests = totalParticipantRequests; + this.status = status; + } + + public List getUsers() { + return users; + } + + public void setUsers(final List users) { + this.users = users; + } + + public Map getRequesterUsernames() { + return requesterUsernames; + } + + public String getCursor() { + return cursor; + } + + public int getTotalThreadParticipants() { + return totalThreadParticipants; + } + + public int getTotalParticipantRequests() { + return totalParticipantRequests; + } + + public String getStatus() { + return status; + } + + @NonNull + @Override + public Object clone() throws CloneNotSupportedException { + return super.clone(); + } +} diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java index fbd4eb3a..2decc502 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java @@ -5,6 +5,7 @@ import android.content.res.Resources; import android.util.Log; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.core.util.Pair; import androidx.lifecycle.AndroidViewModel; @@ -20,6 +21,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; @@ -31,6 +33,7 @@ import awais.instagrabber.repositories.responses.FriendshipRestrictResponse; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.TextUtils; @@ -61,6 +64,8 @@ public class DirectSettingsViewModel extends AndroidViewModel { private final MutableLiveData> adminUserIds = new MutableLiveData<>(Collections.emptyList()); private final MutableLiveData muted = new MutableLiveData<>(false); private final MutableLiveData mentionsMuted = new MutableLiveData<>(false); + private final MutableLiveData approvalRequiredToJoin = new MutableLiveData<>(false); + private final MutableLiveData pendingRequests = new MutableLiveData<>(null); private final DirectMessagesService directMessagesService; private final long userId; private final Resources resources; @@ -107,6 +112,10 @@ public class DirectSettingsViewModel extends AndroidViewModel { viewerIsAdmin = adminUserIds.contains(userId); muted.postValue(thread.isMuted()); mentionsMuted.postValue(thread.isMentionsMuted()); + approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers()); + if (thread.isGroup() && viewerIsAdmin) { + fetchPendingRequests(); + } } public boolean isGroup() { @@ -140,6 +149,18 @@ public class DirectSettingsViewModel extends AndroidViewModel { return muted; } + public LiveData getApprovalRequiredToJoin() { + return approvalRequiredToJoin; + } + + public LiveData getPendingRequests() { + return pendingRequests; + } + + public boolean isViewerAdmin() { + return viewerIsAdmin; + } + public LiveData> updateTitle(final String newTitle) { final MutableLiveData> data = new MutableLiveData<>(); final Call addUsersRequest = directMessagesService @@ -454,9 +475,52 @@ public class DirectSettingsViewModel extends AndroidViewModel { return data; } + public LiveData> approveUsers(final List users) { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Call approveUsersRequest = directMessagesService + .approveParticipantRequests(thread.getThreadId(), users.stream().map(User::getPk).collect(Collectors.toList())); + handleDetailsChangeRequest(data, approveUsersRequest); + return data; + } + + public LiveData> denyUsers(final List users) { + final MutableLiveData> data = new MutableLiveData<>(); + data.postValue(Resource.loading(null)); + final Call approveUsersRequest = directMessagesService + .declineParticipantRequests(thread.getThreadId(), users.stream().map(User::getPk).collect(Collectors.toList())); + handleDetailsChangeRequest(data, approveUsersRequest, () -> { + final DirectThreadParticipantRequestsResponse pendingRequestsValue = pendingRequests.getValue(); + if (pendingRequestsValue == null) return; + final List pendingUsers = pendingRequestsValue.getUsers(); + if (pendingUsers == null || pendingUsers.isEmpty()) return; + final List filtered = pendingUsers.stream() + .filter(o -> !users.contains(o)) + .collect(Collectors.toList()); + try { + final DirectThreadParticipantRequestsResponse clone = (DirectThreadParticipantRequestsResponse) pendingRequestsValue.clone(); + clone.setUsers(filtered); + pendingRequests.postValue(clone); + } catch (CloneNotSupportedException e) { + Log.e(TAG, "denyUsers: ", e); + } + }); + return data; + } + + private interface OnSuccessAction { + void onSuccess(); + } + private void handleDetailsChangeRequest(final MutableLiveData> data, - final Call addUsersRequest) { - addUsersRequest.enqueue(new Callback() { + final Call request) { + handleDetailsChangeRequest(data, request, null); + } + + private void handleDetailsChangeRequest(final MutableLiveData> data, + final Call request, + @Nullable final OnSuccessAction action) { + request.enqueue(new Callback() { @Override public void onResponse(@NonNull final Call call, @NonNull final Response response) { @@ -464,16 +528,19 @@ public class DirectSettingsViewModel extends AndroidViewModel { handleErrorResponse(response, data); return; } - final DirectThreadDetailsChangeResponse addUserResponse = response.body(); - if (addUserResponse == null) { + final DirectThreadDetailsChangeResponse changeResponse = response.body(); + if (changeResponse == null) { data.postValue(Resource.error("Response is null", null)); return; } data.postValue(Resource.success(new Object())); - final DirectThread thread = addUserResponse.getThread(); + final DirectThread thread = changeResponse.getThread(); if (thread != null) { setThread(thread); } + if (action != null) { + action.onSuccess(); + } } @Override @@ -578,4 +645,44 @@ public class DirectSettingsViewModel extends AndroidViewModel { public void setViewer(final User viewer) { this.viewer = viewer; } + + private void fetchPendingRequests() { + final Call request = directMessagesService.participantRequests(thread.getThreadId(), 5, null); + request.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + if (response.errorBody() != null) { + try { + final String string = response.errorBody().string(); + final String msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string); + Log.e(TAG, msg); + } catch (IOException e) { + Log.e(TAG, "onResponse: ", e); + } + return; + } + Log.e(TAG, "onResponse: request was not successful and response error body was null"); + return; + } + final DirectThreadParticipantRequestsResponse body = response.body(); + if (body == null) { + Log.e(TAG, "onResponse: response body was null"); + return; + } + pendingRequests.postValue(body); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); + } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index 36641e86..560492f1 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -46,6 +46,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroa import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponsePayload; import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.utils.BitmapUtils; import awais.instagrabber.utils.Constants; @@ -82,6 +83,7 @@ public class DirectThreadViewModel extends AndroidViewModel { private final MutableLiveData> users = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData> leftUsers = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData replyToItem = new MutableLiveData<>(); + private final MutableLiveData pendingRequestsCount = new MutableLiveData<>(null); private final DirectMessagesService service; private final ContentResolver contentResolver; @@ -89,6 +91,7 @@ public class DirectThreadViewModel extends AndroidViewModel { private final String csrfToken; private final File recordingsDir; private final Application application; + private final long viewerId; private String cursor; private String threadId; @@ -97,7 +100,7 @@ public class DirectThreadViewModel extends AndroidViewModel { private User currentUser; private Call chatsRequest; private VoiceRecorder voiceRecorder; - private final long viewerId; + private boolean viewerIsAdmin; public DirectThreadViewModel(@NonNull final Application application) { super(application); @@ -328,6 +331,10 @@ public class DirectThreadViewModel extends AndroidViewModel { return replyToItem; } + public LiveData getPendingRequestsCount() { + return pendingRequestsCount; + } + public void fetchChats() { final Boolean isFetching = fetching.getValue(); if ((isFetching != null && isFetching) || !hasOlder) return; @@ -391,6 +398,11 @@ public class DirectThreadViewModel extends AndroidViewModel { users.postValue(thread.getUsers()); leftUsers.postValue(thread.getLeftUsers()); fetching.postValue(false); + final List adminUserIds = thread.getAdminUserIds(); + viewerIsAdmin = adminUserIds.contains(viewerId); + if (thread.isGroup() && viewerIsAdmin) { + fetchPendingRequests(); + } } public LiveData> sendText(final String text) { @@ -934,7 +946,7 @@ public class DirectThreadViewModel extends AndroidViewModel { @NonNull final MutableLiveData> data, @NonNull final DirectItem directItem) { try { - final String string = response.errorBody().string(); + final String string = response.errorBody() != null ? response.errorBody().string() : ""; final String msg = String.format(Locale.US, "onResponse: url: %s, responseCode: %d, errorBody: %s", call.request().url().toString(), @@ -1047,4 +1059,44 @@ public class DirectThreadViewModel extends AndroidViewModel { // Log.d(TAG, "setReplyToItem: " + item); replyToItem.postValue(item); } + + private void fetchPendingRequests() { + final Call request = service.participantRequests(threadId, 1, null); + request.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (!response.isSuccessful()) { + if (response.errorBody() != null) { + try { + final String string = response.errorBody().string(); + final String msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string); + Log.e(TAG, msg); + } catch (IOException e) { + Log.e(TAG, "onResponse: ", e); + } + return; + } + Log.e(TAG, "onResponse: request was not successful and response error body was null"); + return; + } + final DirectThreadParticipantRequestsResponse body = response.body(); + if (body == null) { + Log.e(TAG, "onResponse: response body was null"); + return; + } + pendingRequestsCount.postValue(body.getTotalParticipantRequests()); + } + + @Override + public void onFailure(@NonNull final Call call, @NonNull final Throwable t) { + Log.e(TAG, "onFailure: ", t); + } + }); + } } diff --git a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java index 774c2520..32f1198a 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java @@ -32,6 +32,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; +import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; @@ -349,4 +350,31 @@ public class DirectMessagesService extends BaseService { ); return repository.unmuteMentions(threadId, form); } + + public Call participantRequests(@NonNull final String threadId, + final int pageSize, + @Nullable final String cursor) { + return repository.participantRequests(threadId, pageSize, cursor); + } + + public Call approveParticipantRequests(@NonNull final String threadId, + @NonNull final List userIds) { + final ImmutableMap form = ImmutableMap.of( + "_csrftoken", csrfToken, + "_uuid", deviceUuid, + "user_ids", new JSONArray(userIds).toString() + // , "share_join_chat_story", String.valueOf(true) + ); + return repository.approveParticipantRequests(threadId, form); + } + + public Call declineParticipantRequests(@NonNull final String threadId, + @NonNull final List userIds) { + final ImmutableMap form = ImmutableMap.of( + "_csrftoken", csrfToken, + "_uuid", deviceUuid, + "user_ids", new JSONArray(userIds).toString() + ); + return repository.declineParticipantRequests(threadId, form); + } } diff --git a/app/src/main/res/layout/fragment_direct_messages_settings.xml b/app/src/main/res/layout/fragment_direct_messages_settings.xml index c1370a1b..e254586d 100644 --- a/app/src/main/res/layout/fragment_direct_messages_settings.xml +++ b/app/src/main/res/layout/fragment_direct_messages_settings.xml @@ -101,10 +101,37 @@ android:layout_marginEnd="8dp" android:paddingStart="0dp" android:paddingEnd="8dp" - app:layout_constraintBottom_toTopOf="@id/leave" + app:layout_constraintBottom_toTopOf="@id/approval_required" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/mute_messages" /> + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@id/pending_members_header" + tools:itemCount="2" + tools:listitem="@layout/layout_dm_user_item" /> + app:layout_constraintTop_toBottomOf="@id/pending_members" /> + + + app:constraint_referenced_ids="title_edit_input_layout, mute_mentions_label, mute_mentions, leave, add_members, approval_required, approval_required_label" /> @@ -155,5 +226,6 @@ android:id="@+id/users" android:layout_width="match_parent" android:layout_height="match_parent" - app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:listitem="@layout/layout_dm_user_item" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_dm_pending_user_item.xml b/app/src/main/res/layout/layout_dm_pending_user_item.xml new file mode 100644 index 00000000..d16d8cd5 --- /dev/null +++ b/app/src/main/res/layout/layout_dm_pending_user_item.xml @@ -0,0 +1,118 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/direct_messages_nav_graph.xml b/app/src/main/res/navigation/direct_messages_nav_graph.xml index 4d68d5f3..819a428c 100644 --- a/app/src/main/res/navigation/direct_messages_nav_graph.xml +++ b/app/src/main/res/navigation/direct_messages_nav_graph.xml @@ -9,12 +9,7 @@ - - + app:destination="@id/profile_nav_graph" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31c0d0d6..8f366d17 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -407,4 +407,10 @@ Video Voice message Post + Approval required to join + Requests + Admins only + Added by %s + Admin approval required + An admin approval will be required to add new members to the group