mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 11:35:34 +00:00 
			
		
		
		
	Approve/Deny new users
This commit is contained in:
		
							parent
							
								
									6aacf1945f
								
							
						
					
					
						commit
						7addb16e5c
					
				| @ -700,4 +700,8 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage | |||||||
|               }); |               }); | ||||||
|         EmojiCompat.init(config); |         EmojiCompat.init(config); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public Toolbar getToolbar() { | ||||||
|  |         return binding.toolbar; | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -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<DirectPendingUsersAdapter.PendingUser, DirectPendingUserViewHolder> { | ||||||
|  | 
 | ||||||
|  |     private static final DiffUtil.ItemCallback<PendingUser> DIFF_CALLBACK = new DiffUtil.ItemCallback<PendingUser>() { | ||||||
|  |         @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<PendingUser> parse(final DirectThreadParticipantRequestsResponse requests) { | ||||||
|  |         final List<User> users = requests.getUsers(); | ||||||
|  |         final Map<Long, String> 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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -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); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -34,11 +34,17 @@ import java.util.Optional; | |||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
|  | import awais.instagrabber.ProfileNavGraphDirections; | ||||||
| import awais.instagrabber.R; | import awais.instagrabber.R; | ||||||
| import awais.instagrabber.UserSearchNavGraphDirections; | 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.adapters.DirectUsersAdapter; | ||||||
| import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | ||||||
| import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; | 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; | ||||||
| import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; | import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; | ||||||
| import awais.instagrabber.fragments.UserSearchFragment; | import awais.instagrabber.fragments.UserSearchFragment; | ||||||
| @ -46,16 +52,21 @@ import awais.instagrabber.fragments.UserSearchFragmentDirections; | |||||||
| import awais.instagrabber.models.Resource; | import awais.instagrabber.models.Resource; | ||||||
| import awais.instagrabber.repositories.responses.User; | import awais.instagrabber.repositories.responses.User; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
| import awais.instagrabber.viewmodels.DirectInboxViewModel; | import awais.instagrabber.viewmodels.DirectInboxViewModel; | ||||||
| import awais.instagrabber.viewmodels.DirectSettingsViewModel; | 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(); |     private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName(); | ||||||
|  |     public static final int APPROVAL_REQUIRED_REQUEST_CODE = 200; | ||||||
| 
 | 
 | ||||||
|     private FragmentDirectMessagesSettingsBinding binding; |     private FragmentDirectMessagesSettingsBinding binding; | ||||||
|     private DirectSettingsViewModel viewModel; |     private DirectSettingsViewModel viewModel; | ||||||
|     private DirectUsersAdapter usersAdapter; |     private DirectUsersAdapter usersAdapter; | ||||||
|  |     private boolean isPendingRequestsSetupDone = false; | ||||||
|  |     private DirectPendingUsersAdapter pendingUsersAdapter; | ||||||
|  |     private Set<User> approvalRequiredUsers; | ||||||
|     // private List<Option<String>> options; |     // private List<Option<String>> options; | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -165,6 +176,7 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|     public void onDestroyView() { |     public void onDestroyView() { | ||||||
|         super.onDestroyView(); |         super.onDestroyView(); | ||||||
|         binding = null; |         binding = null; | ||||||
|  |         isPendingRequestsSetupDone = false; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setupObservers() { |     private void setupObservers() { | ||||||
| @ -178,18 +190,21 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|             usersAdapter.setAdminUserIds(adminUserIds); |             usersAdapter.setAdminUserIds(adminUserIds); | ||||||
|         }); |         }); | ||||||
|         viewModel.getMuted().observe(getViewLifecycleOwner(), muted -> binding.muteMessages.setChecked(muted)); |         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 NavController navController = NavHostFragment.findNavController(this); | ||||||
|         final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); |         final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); | ||||||
|         if (backStackEntry != null) { |         if (backStackEntry != null) { | ||||||
|             final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); |             final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); | ||||||
|             resultLiveData.observe(getViewLifecycleOwner(), result -> { |             resultLiveData.observe(getViewLifecycleOwner(), result -> { | ||||||
|                 LiveData<Resource<Object>> detailsChangeResourceLiveData = null; |  | ||||||
|                 if ((result instanceof RankedRecipient)) { |                 if ((result instanceof RankedRecipient)) { | ||||||
|                     final RankedRecipient recipient = (RankedRecipient) result; |                     final RankedRecipient recipient = (RankedRecipient) result; | ||||||
|                     final User user = getUser(recipient); |                     final User user = getUser(recipient); | ||||||
|                     // Log.d(TAG, "result: " + user); |                     // Log.d(TAG, "result: " + user); | ||||||
|                     if (user != null) { |                     if (user != null) { | ||||||
|                         detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton(recipient.getUser())); |                         addMembers(Collections.singleton(recipient.getUser())); | ||||||
|                     } |                     } | ||||||
|                 } else if ((result instanceof Set)) { |                 } else if ((result instanceof Set)) { | ||||||
|                     try { |                     try { | ||||||
| @ -201,19 +216,35 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|                                                           .filter(Objects::nonNull) |                                                           .filter(Objects::nonNull) | ||||||
|                                                           .collect(Collectors.toSet()); |                                                           .collect(Collectors.toSet()); | ||||||
|                         // Log.d(TAG, "result: " + users); |                         // Log.d(TAG, "result: " + users); | ||||||
|                         detailsChangeResourceLiveData = viewModel.addMembers(users); |                         addMembers(users); | ||||||
|                     } catch (Exception e) { |                     } catch (Exception e) { | ||||||
|                         Log.e(TAG, "search users result: ", e); |                         Log.e(TAG, "search users result: ", e); | ||||||
|                         Snackbar.make(binding.getRoot(), e.getMessage() != null ? e.getMessage() : "", Snackbar.LENGTH_LONG).show(); |                         Snackbar.make(binding.getRoot(), e.getMessage() != null ? e.getMessage() : "", Snackbar.LENGTH_LONG).show(); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if (detailsChangeResourceLiveData != null) { |  | ||||||
|                     observeDetailsChange(detailsChangeResourceLiveData); |  | ||||||
|                 } |  | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void addMembers(final Set<User> 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<Resource<Object>> detailsChangeResourceLiveData = viewModel.addMembers(users); | ||||||
|  |         observeDetailsChange(detailsChangeResourceLiveData); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Nullable |     @Nullable | ||||||
|     private User getUser(@NonNull final RankedRecipient recipient) { |     private User getUser(@NonNull final RankedRecipient recipient) { | ||||||
|         User user = null; |         User user = null; | ||||||
| @ -279,16 +310,28 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|         binding.muteMessagesLabel.setOnClickListener(v -> binding.muteMessages.toggle()); |         binding.muteMessagesLabel.setOnClickListener(v -> binding.muteMessages.toggle()); | ||||||
|         binding.muteMessages.setOnCheckedChangeListener((buttonView, isChecked) -> { |         binding.muteMessages.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||||
|             final LiveData<Resource<Object>> resourceLiveData = isChecked ? viewModel.mute() : viewModel.unmute(); |             final LiveData<Resource<Object>> resourceLiveData = isChecked ? viewModel.mute() : viewModel.unmute(); | ||||||
|             handleMuteChangeResource(resourceLiveData, buttonView); |             handleSwitchChangeResource(resourceLiveData, buttonView); | ||||||
|         }); |         }); | ||||||
|         binding.muteMentionsLabel.setOnClickListener(v -> binding.muteMentions.toggle()); |         binding.muteMentionsLabel.setOnClickListener(v -> binding.muteMentions.toggle()); | ||||||
|         binding.muteMentions.setOnCheckedChangeListener((buttonView, isChecked) -> { |         binding.muteMentions.setOnCheckedChangeListener((buttonView, isChecked) -> { | ||||||
|             final LiveData<Resource<Object>> resourceLiveData = isChecked ? viewModel.muteMentions() : viewModel.unmuteMentions(); |             final LiveData<Resource<Object>> 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<Resource<Object>> resourceLiveData, final CompoundButton buttonView) { |     private void handleSwitchChangeResource(final LiveData<Resource<Object>> resourceLiveData, final CompoundButton buttonView) { | ||||||
|         resourceLiveData.observe(getViewLifecycleOwner(), resource -> { |         resourceLiveData.observe(getViewLifecycleOwner(), resource -> { | ||||||
|             if (resource == null) return; |             if (resource == null) return; | ||||||
|             switch (resource.status) { |             switch (resource.status) { | ||||||
| @ -297,6 +340,7 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|                     break; |                     break; | ||||||
|                 case ERROR: |                 case ERROR: | ||||||
|                     buttonView.setEnabled(true); |                     buttonView.setEnabled(true); | ||||||
|  |                     buttonView.setChecked(!buttonView.isChecked()); | ||||||
|                     if (resource.message != null) { |                     if (resource.message != null) { | ||||||
|                         Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); |                         Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); | ||||||
|                     } |                     } | ||||||
| @ -316,7 +360,10 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|         usersAdapter = new DirectUsersAdapter( |         usersAdapter = new DirectUsersAdapter( | ||||||
|                 inviter != null ? inviter.getPk() : -1, |                 inviter != null ? inviter.getPk() : -1, | ||||||
|                 (position, user, selected) -> { |                 (position, user, selected) -> { | ||||||
|                     // navigate to profile |                     final ProfileNavGraphDirections.ActionGlobalProfileFragment directions = ProfileNavGraphDirections | ||||||
|  |                             .actionGlobalProfileFragment() | ||||||
|  |                             .setUsername("@" + user.getUsername()); | ||||||
|  |                     NavHostFragment.findNavController(this).navigate(directions); | ||||||
|                 }, |                 }, | ||||||
|                 (position, user) -> { |                 (position, user) -> { | ||||||
|                     final ArrayList<Option<String>> options = viewModel.createUserOptions(user); |                     final ArrayList<Option<String>> options = viewModel.createUserOptions(user); | ||||||
| @ -340,6 +387,45 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|         binding.users.setAdapter(usersAdapter); |         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<Resource<Object>> resourceLiveData = viewModel.approveUsers(Collections.singletonList(pendingUser.getUser())); | ||||||
|  |                     observeApprovalChange(resourceLiveData, position, pendingUser); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 @Override | ||||||
|  |                 public void onDeny(final int position, final PendingUser pendingUser) { | ||||||
|  |                     final LiveData<Resource<Object>> 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<Resource<Object>> detailsChangeResourceLiveData) { |     private void observeDetailsChange(@NonNull final LiveData<Resource<Object>> detailsChangeResourceLiveData) { | ||||||
|         detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> { |         detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> { | ||||||
|             if (resource == null) return; |             if (resource == null) return; | ||||||
| @ -356,6 +442,48 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void observeApprovalChange(@NonNull final LiveData<Resource<Object>> 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<Resource<Object>> 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, Void, Void> { |     // class ChangeSettings extends AsyncTask<String, Void, Void> { | ||||||
|     //     String action, argument; |     //     String action, argument; | ||||||
|     //     boolean ok = false; |     //     boolean ok = false; | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import android.animation.Animator; | |||||||
| import android.animation.AnimatorListenerAdapter; | import android.animation.AnimatorListenerAdapter; | ||||||
| import android.animation.AnimatorSet; | import android.animation.AnimatorSet; | ||||||
| import android.animation.ObjectAnimator; | import android.animation.ObjectAnimator; | ||||||
|  | import android.annotation.SuppressLint; | ||||||
| import android.app.Activity; | import android.app.Activity; | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.Intent; | import android.content.Intent; | ||||||
| @ -49,6 +50,8 @@ import androidx.transition.TransitionManager; | |||||||
| import androidx.vectordrawable.graphics.drawable.Animatable2Compat; | import androidx.vectordrawable.graphics.drawable.Animatable2Compat; | ||||||
| import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; | 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.android.material.snackbar.Snackbar; | ||||||
| import com.google.common.collect.ImmutableList; | import com.google.common.collect.ImmutableList; | ||||||
| 
 | 
 | ||||||
| @ -58,6 +61,7 @@ import java.util.List; | |||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| 
 | 
 | ||||||
|  | import awais.instagrabber.ProfileNavGraphDirections; | ||||||
| import awais.instagrabber.R; | import awais.instagrabber.R; | ||||||
| import awais.instagrabber.UserSearchNavGraphDirections; | import awais.instagrabber.UserSearchNavGraphDirections; | ||||||
| import awais.instagrabber.activities.CameraActivity; | import awais.instagrabber.activities.CameraActivity; | ||||||
| @ -140,6 +144,8 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|     private DirectItem itemToForward; |     private DirectItem itemToForward; | ||||||
|     private MutableLiveData<Object> backStackSavedStateResultLiveData; |     private MutableLiveData<Object> backStackSavedStateResultLiveData; | ||||||
|     private int prevLength; |     private int prevLength; | ||||||
|  |     private BadgeDrawable pendingRequestCountBadgeDrawable; | ||||||
|  |     private boolean isPendingRequestCountBadgeAttached = false; | ||||||
| 
 | 
 | ||||||
|     private final AppExecutors appExecutors = AppExecutors.getInstance(); |     private final AppExecutors appExecutors = AppExecutors.getInstance(); | ||||||
|     private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { |     private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { | ||||||
| @ -413,6 +419,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|         } |         } | ||||||
|         binding.send.stopScale(); |         binding.send.stopScale(); | ||||||
|         setupBackStackResultObserver(); |         setupBackStackResultObserver(); | ||||||
|  |         attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -421,6 +428,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|         cleanup(); |         cleanup(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @SuppressLint("UnsafeExperimentalUsageError") | ||||||
|     private void cleanup() { |     private void cleanup() { | ||||||
|         if (prevTitleRunnable != null) { |         if (prevTitleRunnable != null) { | ||||||
|             appExecutors.mainThread().cancel(prevTitleRunnable); |             appExecutors.mainThread().cancel(prevTitleRunnable); | ||||||
| @ -439,6 +447,11 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|                 ((DirectItemViewHolder) holder).cleanup(); |                 ((DirectItemViewHolder) holder).cleanup(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         isPendingRequestCountBadgeAttached = false; | ||||||
|  |         if (pendingRequestCountBadgeDrawable != null) { | ||||||
|  |             BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); | ||||||
|  |             pendingRequestCountBadgeDrawable = null; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void init() { |     private void init() { | ||||||
| @ -705,6 +718,28 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|             } |             } | ||||||
|             prevLength = length; |             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) { |     private void showExtraInputOption(final boolean show) { | ||||||
| @ -1396,9 +1431,10 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void navigateToUser(@NonNull final String username) { |     private void navigateToUser(@NonNull final String username) { | ||||||
|         final Bundle bundle = new Bundle(); |         final ProfileNavGraphDirections.ActionGlobalProfileFragment direction = ProfileNavGraphDirections | ||||||
|         bundle.putString("username", "@" + username); |                 .actionGlobalProfileFragment() | ||||||
|         NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(R.id.action_global_profileFragment, bundle); |                 .setUsername("@" + username); | ||||||
|  |         NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(direction); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static class ItemsAdapterDataMerger extends MediatorLiveData<Pair<User, DirectThread>> { |     public static class ItemsAdapterDataMerger extends MediatorLiveData<Pair<User, DirectThread>> { | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThread; | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| import retrofit2.http.FieldMap; | import retrofit2.http.FieldMap; | ||||||
| @ -15,6 +16,7 @@ import retrofit2.http.FormUrlEncoded; | |||||||
| import retrofit2.http.GET; | import retrofit2.http.GET; | ||||||
| import retrofit2.http.POST; | import retrofit2.http.POST; | ||||||
| import retrofit2.http.Path; | import retrofit2.http.Path; | ||||||
|  | import retrofit2.http.Query; | ||||||
| import retrofit2.http.QueryMap; | import retrofit2.http.QueryMap; | ||||||
| 
 | 
 | ||||||
| public interface DirectMessagesRepository { | public interface DirectMessagesRepository { | ||||||
| @ -95,4 +97,19 @@ public interface DirectMessagesRepository { | |||||||
|     @POST("/api/v1/direct_v2/threads/{threadId}/unmute_mentions/") |     @POST("/api/v1/direct_v2/threads/{threadId}/unmute_mentions/") | ||||||
|     Call<String> unmuteMentions(@Path("threadId") String threadId, |     Call<String> unmuteMentions(@Path("threadId") String threadId, | ||||||
|                                 @FieldMap final Map<String, String> form); |                                 @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @GET("/api/v1/direct_v2/threads/{threadId}/participant_requests/") | ||||||
|  |     Call<DirectThreadParticipantRequestsResponse> 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<DirectThreadDetailsChangeResponse> approveParticipantRequests(@Path("threadId") String threadId, | ||||||
|  |                                                                        @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/{threadId}/deny_participant_requests/") | ||||||
|  |     Call<DirectThreadDetailsChangeResponse> declineParticipantRequests(@Path("threadId") String threadId, | ||||||
|  |                                                                        @FieldMap final Map<String, String> form); | ||||||
| } | } | ||||||
|  | |||||||
| @ -41,6 +41,7 @@ public class DirectThread implements Serializable { | |||||||
|     private final boolean isSpam; |     private final boolean isSpam; | ||||||
|     private final DirectItem lastPermanentItem; |     private final DirectItem lastPermanentItem; | ||||||
|     private final DirectThreadDirectStory directStory; |     private final DirectThreadDirectStory directStory; | ||||||
|  |     private final boolean approvalRequiredForNewMembers; | ||||||
| 
 | 
 | ||||||
|     public DirectThread(final String threadId, |     public DirectThread(final String threadId, | ||||||
|                         final String threadV2Id, |                         final String threadV2Id, | ||||||
| @ -72,7 +73,8 @@ public class DirectThread implements Serializable { | |||||||
|                         final String oldestCursor, |                         final String oldestCursor, | ||||||
|                         final boolean isSpam, |                         final boolean isSpam, | ||||||
|                         final DirectItem lastPermanentItem, |                         final DirectItem lastPermanentItem, | ||||||
|                         final DirectThreadDirectStory directStory) { |                         final DirectThreadDirectStory directStory, | ||||||
|  |                         final boolean approvalRequiredForNewMembers) { | ||||||
|         this.threadId = threadId; |         this.threadId = threadId; | ||||||
|         this.threadV2Id = threadV2Id; |         this.threadV2Id = threadV2Id; | ||||||
|         this.users = users; |         this.users = users; | ||||||
| @ -104,6 +106,7 @@ public class DirectThread implements Serializable { | |||||||
|         this.isSpam = isSpam; |         this.isSpam = isSpam; | ||||||
|         this.lastPermanentItem = lastPermanentItem; |         this.lastPermanentItem = lastPermanentItem; | ||||||
|         this.directStory = directStory; |         this.directStory = directStory; | ||||||
|  |         this.approvalRequiredForNewMembers = approvalRequiredForNewMembers; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public String getThreadId() { |     public String getThreadId() { | ||||||
| @ -238,6 +241,10 @@ public class DirectThread implements Serializable { | |||||||
|         return directStory; |         return directStory; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public boolean isApprovalRequiredForNewMembers() { | ||||||
|  |         return approvalRequiredForNewMembers; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @Nullable |     @Nullable | ||||||
|     public DirectItem getFirstDirectItem() { |     public DirectItem getFirstDirectItem() { | ||||||
|         DirectItem firstItem = null; |         DirectItem firstItem = null; | ||||||
|  | |||||||
| @ -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<User> users; | ||||||
|  |     private final Map<Long, String> requesterUsernames; | ||||||
|  |     private final String cursor; | ||||||
|  |     private final int totalThreadParticipants; | ||||||
|  |     private final int totalParticipantRequests; | ||||||
|  |     private final String status; | ||||||
|  | 
 | ||||||
|  |     public DirectThreadParticipantRequestsResponse(final List<User> users, | ||||||
|  |                                                    final Map<Long, String> 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<User> getUsers() { | ||||||
|  |         return users; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setUsers(final List<User> users) { | ||||||
|  |         this.users = users; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Map<Long, String> 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(); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -5,6 +5,7 @@ import android.content.res.Resources; | |||||||
| import android.util.Log; | import android.util.Log; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
| import androidx.annotation.StringRes; | import androidx.annotation.StringRes; | ||||||
| import androidx.core.util.Pair; | import androidx.core.util.Pair; | ||||||
| import androidx.lifecycle.AndroidViewModel; | import androidx.lifecycle.AndroidViewModel; | ||||||
| @ -20,6 +21,7 @@ import java.io.IOException; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Locale; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.stream.Collectors; | 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.User; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; | ||||||
| import awais.instagrabber.utils.Constants; | import awais.instagrabber.utils.Constants; | ||||||
| import awais.instagrabber.utils.CookieUtils; | import awais.instagrabber.utils.CookieUtils; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| @ -61,6 +64,8 @@ public class DirectSettingsViewModel extends AndroidViewModel { | |||||||
|     private final MutableLiveData<List<Long>> adminUserIds = new MutableLiveData<>(Collections.emptyList()); |     private final MutableLiveData<List<Long>> adminUserIds = new MutableLiveData<>(Collections.emptyList()); | ||||||
|     private final MutableLiveData<Boolean> muted = new MutableLiveData<>(false); |     private final MutableLiveData<Boolean> muted = new MutableLiveData<>(false); | ||||||
|     private final MutableLiveData<Boolean> mentionsMuted = new MutableLiveData<>(false); |     private final MutableLiveData<Boolean> mentionsMuted = new MutableLiveData<>(false); | ||||||
|  |     private final MutableLiveData<Boolean> approvalRequiredToJoin = new MutableLiveData<>(false); | ||||||
|  |     private final MutableLiveData<DirectThreadParticipantRequestsResponse> pendingRequests = new MutableLiveData<>(null); | ||||||
|     private final DirectMessagesService directMessagesService; |     private final DirectMessagesService directMessagesService; | ||||||
|     private final long userId; |     private final long userId; | ||||||
|     private final Resources resources; |     private final Resources resources; | ||||||
| @ -107,6 +112,10 @@ public class DirectSettingsViewModel extends AndroidViewModel { | |||||||
|         viewerIsAdmin = adminUserIds.contains(userId); |         viewerIsAdmin = adminUserIds.contains(userId); | ||||||
|         muted.postValue(thread.isMuted()); |         muted.postValue(thread.isMuted()); | ||||||
|         mentionsMuted.postValue(thread.isMentionsMuted()); |         mentionsMuted.postValue(thread.isMentionsMuted()); | ||||||
|  |         approvalRequiredToJoin.postValue(thread.isApprovalRequiredForNewMembers()); | ||||||
|  |         if (thread.isGroup() && viewerIsAdmin) { | ||||||
|  |             fetchPendingRequests(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean isGroup() { |     public boolean isGroup() { | ||||||
| @ -140,6 +149,18 @@ public class DirectSettingsViewModel extends AndroidViewModel { | |||||||
|         return muted; |         return muted; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public LiveData<Boolean> getApprovalRequiredToJoin() { | ||||||
|  |         return approvalRequiredToJoin; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<DirectThreadParticipantRequestsResponse> getPendingRequests() { | ||||||
|  |         return pendingRequests; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isViewerAdmin() { | ||||||
|  |         return viewerIsAdmin; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public LiveData<Resource<Object>> updateTitle(final String newTitle) { |     public LiveData<Resource<Object>> updateTitle(final String newTitle) { | ||||||
|         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|         final Call<DirectThreadDetailsChangeResponse> addUsersRequest = directMessagesService |         final Call<DirectThreadDetailsChangeResponse> addUsersRequest = directMessagesService | ||||||
| @ -454,9 +475,52 @@ public class DirectSettingsViewModel extends AndroidViewModel { | |||||||
|         return data; |         return data; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public LiveData<Resource<Object>> approveUsers(final List<User> users) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         data.postValue(Resource.loading(null)); | ||||||
|  |         final Call<DirectThreadDetailsChangeResponse> approveUsersRequest = directMessagesService | ||||||
|  |                 .approveParticipantRequests(thread.getThreadId(), users.stream().map(User::getPk).collect(Collectors.toList())); | ||||||
|  |         handleDetailsChangeRequest(data, approveUsersRequest); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Resource<Object>> denyUsers(final List<User> users) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         data.postValue(Resource.loading(null)); | ||||||
|  |         final Call<DirectThreadDetailsChangeResponse> 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<User> pendingUsers = pendingRequestsValue.getUsers(); | ||||||
|  |             if (pendingUsers == null || pendingUsers.isEmpty()) return; | ||||||
|  |             final List<User> 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<Resource<Object>> data, |     private void handleDetailsChangeRequest(final MutableLiveData<Resource<Object>> data, | ||||||
|                                             final Call<DirectThreadDetailsChangeResponse> addUsersRequest) { |                                             final Call<DirectThreadDetailsChangeResponse> request) { | ||||||
|         addUsersRequest.enqueue(new Callback<DirectThreadDetailsChangeResponse>() { |         handleDetailsChangeRequest(data, request, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleDetailsChangeRequest(final MutableLiveData<Resource<Object>> data, | ||||||
|  |                                             final Call<DirectThreadDetailsChangeResponse> request, | ||||||
|  |                                             @Nullable final OnSuccessAction action) { | ||||||
|  |         request.enqueue(new Callback<DirectThreadDetailsChangeResponse>() { | ||||||
|             @Override |             @Override | ||||||
|             public void onResponse(@NonNull final Call<DirectThreadDetailsChangeResponse> call, |             public void onResponse(@NonNull final Call<DirectThreadDetailsChangeResponse> call, | ||||||
|                                    @NonNull final Response<DirectThreadDetailsChangeResponse> response) { |                                    @NonNull final Response<DirectThreadDetailsChangeResponse> response) { | ||||||
| @ -464,16 +528,19 @@ public class DirectSettingsViewModel extends AndroidViewModel { | |||||||
|                     handleErrorResponse(response, data); |                     handleErrorResponse(response, data); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 final DirectThreadDetailsChangeResponse addUserResponse = response.body(); |                 final DirectThreadDetailsChangeResponse changeResponse = response.body(); | ||||||
|                 if (addUserResponse == null) { |                 if (changeResponse == null) { | ||||||
|                     data.postValue(Resource.error("Response is null", null)); |                     data.postValue(Resource.error("Response is null", null)); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 data.postValue(Resource.success(new Object())); |                 data.postValue(Resource.success(new Object())); | ||||||
|                 final DirectThread thread = addUserResponse.getThread(); |                 final DirectThread thread = changeResponse.getThread(); | ||||||
|                 if (thread != null) { |                 if (thread != null) { | ||||||
|                     setThread(thread); |                     setThread(thread); | ||||||
|                 } |                 } | ||||||
|  |                 if (action != null) { | ||||||
|  |                     action.onSuccess(); | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             @Override |             @Override | ||||||
| @ -578,4 +645,44 @@ public class DirectSettingsViewModel extends AndroidViewModel { | |||||||
|     public void setViewer(final User viewer) { |     public void setViewer(final User viewer) { | ||||||
|         this.viewer = viewer; |         this.viewer = viewer; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void fetchPendingRequests() { | ||||||
|  |         final Call<DirectThreadParticipantRequestsResponse> request = directMessagesService.participantRequests(thread.getThreadId(), 5, null); | ||||||
|  |         request.enqueue(new Callback<DirectThreadParticipantRequestsResponse>() { | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<DirectThreadParticipantRequestsResponse> call, | ||||||
|  |                                    @NonNull final Response<DirectThreadParticipantRequestsResponse> 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<DirectThreadParticipantRequestsResponse> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -46,6 +46,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroa | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponseMessageMetadata; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponsePayload; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponsePayload; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
| import awais.instagrabber.utils.BitmapUtils; | import awais.instagrabber.utils.BitmapUtils; | ||||||
| import awais.instagrabber.utils.Constants; | import awais.instagrabber.utils.Constants; | ||||||
| @ -82,6 +83,7 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|     private final MutableLiveData<List<User>> users = new MutableLiveData<>(new ArrayList<>()); |     private final MutableLiveData<List<User>> users = new MutableLiveData<>(new ArrayList<>()); | ||||||
|     private final MutableLiveData<List<User>> leftUsers = new MutableLiveData<>(new ArrayList<>()); |     private final MutableLiveData<List<User>> leftUsers = new MutableLiveData<>(new ArrayList<>()); | ||||||
|     private final MutableLiveData<DirectItem> replyToItem = new MutableLiveData<>(); |     private final MutableLiveData<DirectItem> replyToItem = new MutableLiveData<>(); | ||||||
|  |     private final MutableLiveData<Integer> pendingRequestsCount = new MutableLiveData<>(null); | ||||||
| 
 | 
 | ||||||
|     private final DirectMessagesService service; |     private final DirectMessagesService service; | ||||||
|     private final ContentResolver contentResolver; |     private final ContentResolver contentResolver; | ||||||
| @ -89,6 +91,7 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|     private final String csrfToken; |     private final String csrfToken; | ||||||
|     private final File recordingsDir; |     private final File recordingsDir; | ||||||
|     private final Application application; |     private final Application application; | ||||||
|  |     private final long viewerId; | ||||||
| 
 | 
 | ||||||
|     private String cursor; |     private String cursor; | ||||||
|     private String threadId; |     private String threadId; | ||||||
| @ -97,7 +100,7 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|     private User currentUser; |     private User currentUser; | ||||||
|     private Call<DirectThreadFeedResponse> chatsRequest; |     private Call<DirectThreadFeedResponse> chatsRequest; | ||||||
|     private VoiceRecorder voiceRecorder; |     private VoiceRecorder voiceRecorder; | ||||||
|     private final long viewerId; |     private boolean viewerIsAdmin; | ||||||
| 
 | 
 | ||||||
|     public DirectThreadViewModel(@NonNull final Application application) { |     public DirectThreadViewModel(@NonNull final Application application) { | ||||||
|         super(application); |         super(application); | ||||||
| @ -328,6 +331,10 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|         return replyToItem; |         return replyToItem; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public LiveData<Integer> getPendingRequestsCount() { | ||||||
|  |         return pendingRequestsCount; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public void fetchChats() { |     public void fetchChats() { | ||||||
|         final Boolean isFetching = fetching.getValue(); |         final Boolean isFetching = fetching.getValue(); | ||||||
|         if ((isFetching != null && isFetching) || !hasOlder) return; |         if ((isFetching != null && isFetching) || !hasOlder) return; | ||||||
| @ -391,6 +398,11 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|         users.postValue(thread.getUsers()); |         users.postValue(thread.getUsers()); | ||||||
|         leftUsers.postValue(thread.getLeftUsers()); |         leftUsers.postValue(thread.getLeftUsers()); | ||||||
|         fetching.postValue(false); |         fetching.postValue(false); | ||||||
|  |         final List<Long> adminUserIds = thread.getAdminUserIds(); | ||||||
|  |         viewerIsAdmin = adminUserIds.contains(viewerId); | ||||||
|  |         if (thread.isGroup() && viewerIsAdmin) { | ||||||
|  |             fetchPendingRequests(); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public LiveData<Resource<DirectItem>> sendText(final String text) { |     public LiveData<Resource<DirectItem>> sendText(final String text) { | ||||||
| @ -934,7 +946,7 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|                                  @NonNull final MutableLiveData<Resource<DirectItem>> data, |                                  @NonNull final MutableLiveData<Resource<DirectItem>> data, | ||||||
|                                  @NonNull final DirectItem directItem) { |                                  @NonNull final DirectItem directItem) { | ||||||
|         try { |         try { | ||||||
|             final String string = response.errorBody().string(); |             final String string = response.errorBody() != null ? response.errorBody().string() : ""; | ||||||
|             final String msg = String.format(Locale.US, |             final String msg = String.format(Locale.US, | ||||||
|                                              "onResponse: url: %s, responseCode: %d, errorBody: %s", |                                              "onResponse: url: %s, responseCode: %d, errorBody: %s", | ||||||
|                                              call.request().url().toString(), |                                              call.request().url().toString(), | ||||||
| @ -1047,4 +1059,44 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|         // Log.d(TAG, "setReplyToItem: " + item); |         // Log.d(TAG, "setReplyToItem: " + item); | ||||||
|         replyToItem.postValue(item); |         replyToItem.postValue(item); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void fetchPendingRequests() { | ||||||
|  |         final Call<DirectThreadParticipantRequestsResponse> request = service.participantRequests(threadId, 1, null); | ||||||
|  |         request.enqueue(new Callback<DirectThreadParticipantRequestsResponse>() { | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<DirectThreadParticipantRequestsResponse> call, | ||||||
|  |                                    @NonNull final Response<DirectThreadParticipantRequestsResponse> 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<DirectThreadParticipantRequestsResponse> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThread; | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import awais.instagrabber.utils.Utils; | import awais.instagrabber.utils.Utils; | ||||||
| @ -349,4 +350,31 @@ public class DirectMessagesService extends BaseService { | |||||||
|         ); |         ); | ||||||
|         return repository.unmuteMentions(threadId, form); |         return repository.unmuteMentions(threadId, form); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public Call<DirectThreadParticipantRequestsResponse> participantRequests(@NonNull final String threadId, | ||||||
|  |                                                                              final int pageSize, | ||||||
|  |                                                                              @Nullable final String cursor) { | ||||||
|  |         return repository.participantRequests(threadId, pageSize, cursor); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<DirectThreadDetailsChangeResponse> approveParticipantRequests(@NonNull final String threadId, | ||||||
|  |                                                                               @NonNull final List<Long> userIds) { | ||||||
|  |         final ImmutableMap<String, String> 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<DirectThreadDetailsChangeResponse> declineParticipantRequests(@NonNull final String threadId, | ||||||
|  |                                                                               @NonNull final List<Long> userIds) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "_csrftoken", csrfToken, | ||||||
|  |                 "_uuid", deviceUuid, | ||||||
|  |                 "user_ids", new JSONArray(userIds).toString() | ||||||
|  |         ); | ||||||
|  |         return repository.declineParticipantRequests(threadId, form); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -101,10 +101,37 @@ | |||||||
|                     android:layout_marginEnd="8dp" |                     android:layout_marginEnd="8dp" | ||||||
|                     android:paddingStart="0dp" |                     android:paddingStart="0dp" | ||||||
|                     android:paddingEnd="8dp" |                     android:paddingEnd="8dp" | ||||||
|                     app:layout_constraintBottom_toTopOf="@id/leave" |                     app:layout_constraintBottom_toTopOf="@id/approval_required" | ||||||
|                     app:layout_constraintEnd_toEndOf="parent" |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|                     app:layout_constraintTop_toBottomOf="@id/mute_messages" /> |                     app:layout_constraintTop_toBottomOf="@id/mute_messages" /> | ||||||
| 
 | 
 | ||||||
|  |                 <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |                     android:id="@+id/approval_required_label" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="0dp" | ||||||
|  |                     android:gravity="center_vertical" | ||||||
|  |                     android:paddingStart="16dp" | ||||||
|  |                     android:paddingEnd="16dp" | ||||||
|  |                     android:text="@string/approval_required_for_new_members" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" | ||||||
|  |                     android:textColor="?android:textColorPrimary" | ||||||
|  |                     app:layout_constraintBottom_toBottomOf="@id/approval_required" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                     app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                     app:layout_constraintTop_toTopOf="@id/approval_required" /> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.switchmaterial.SwitchMaterial | ||||||
|  |                     android:id="@+id/approval_required" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:layout_marginTop="4dp" | ||||||
|  |                     android:layout_marginEnd="8dp" | ||||||
|  |                     android:paddingStart="0dp" | ||||||
|  |                     android:paddingEnd="8dp" | ||||||
|  |                     app:layout_constraintBottom_toTopOf="@id/leave" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/mute_mentions" /> | ||||||
|  | 
 | ||||||
|                 <androidx.appcompat.widget.AppCompatTextView |                 <androidx.appcompat.widget.AppCompatTextView | ||||||
|                     android:id="@+id/leave" |                     android:id="@+id/leave" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
| @ -119,8 +146,44 @@ | |||||||
|                     android:text="@string/dms_action_leave" |                     android:text="@string/dms_action_leave" | ||||||
|                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" | ||||||
|                     android:textColor="@color/red_600" |                     android:textColor="@color/red_600" | ||||||
|  |                     app:layout_constraintBottom_toTopOf="@id/pending_members_header" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/approval_required" /> | ||||||
|  | 
 | ||||||
|  |                 <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |                     android:id="@+id/pending_members_header" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:paddingLeft="16dp" | ||||||
|  |                     android:paddingTop="16dp" | ||||||
|  |                     android:paddingRight="16dp" | ||||||
|  |                     android:paddingBottom="8dp" | ||||||
|  |                     android:text="@string/requests" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body2" | ||||||
|  |                     android:textColor="?colorAccent" | ||||||
|  |                     android:textStyle="bold" | ||||||
|  |                     app:layout_constraintBottom_toTopOf="@id/pending_members" | ||||||
|  |                     app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/leave" /> | ||||||
|  | 
 | ||||||
|  |                 <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |                     android:id="@+id/pending_members_admin_only" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:paddingLeft="16dp" | ||||||
|  |                     android:paddingRight="16dp" | ||||||
|  |                     android:text="@string/admins_only" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" | ||||||
|  |                     app:layout_constraintBaseline_toBaselineOf="@id/pending_members_header" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" /> | ||||||
|  | 
 | ||||||
|  |                 <androidx.recyclerview.widget.RecyclerView | ||||||
|  |                     android:id="@+id/pending_members" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|                     app:layout_constraintBottom_toTopOf="@id/add_members" |                     app:layout_constraintBottom_toTopOf="@id/add_members" | ||||||
|                     app:layout_constraintTop_toBottomOf="@id/mute_mentions" /> |                     app:layout_constraintTop_toBottomOf="@id/pending_members_header" | ||||||
|  |                     tools:itemCount="2" | ||||||
|  |                     tools:listitem="@layout/layout_dm_user_item" /> | ||||||
| 
 | 
 | ||||||
|                 <androidx.appcompat.widget.AppCompatTextView |                 <androidx.appcompat.widget.AppCompatTextView | ||||||
|                     android:id="@+id/add_members" |                     android:id="@+id/add_members" | ||||||
| @ -139,13 +202,21 @@ | |||||||
|                     app:drawableStartCompat="@drawable/ic_add" |                     app:drawableStartCompat="@drawable/ic_add" | ||||||
|                     app:drawableTint="?android:textColorPrimary" |                     app:drawableTint="?android:textColorPrimary" | ||||||
|                     app:layout_constraintBottom_toBottomOf="parent" |                     app:layout_constraintBottom_toBottomOf="parent" | ||||||
|                     app:layout_constraintTop_toBottomOf="@id/leave" /> |                     app:layout_constraintTop_toBottomOf="@id/pending_members" /> | ||||||
|  | 
 | ||||||
|  |                 <androidx.constraintlayout.widget.Group | ||||||
|  |                     android:id="@+id/pending_members_group" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="0dp" | ||||||
|  |                     android:visibility="gone" | ||||||
|  |                     app:constraint_referenced_ids="pending_members,pending_members_admin_only,pending_members_header" | ||||||
|  |                     tools:visibility="visible" /> | ||||||
| 
 | 
 | ||||||
|                 <androidx.constraintlayout.widget.Group |                 <androidx.constraintlayout.widget.Group | ||||||
|                     android:id="@+id/group_settings" |                     android:id="@+id/group_settings" | ||||||
|                     android:layout_width="0dp" |                     android:layout_width="0dp" | ||||||
|                     android:layout_height="0dp" |                     android:layout_height="0dp" | ||||||
|                     app:constraint_referenced_ids="title_edit_input_layout, mute_mentions_label, mute_mentions, leave, add_members" /> |                     app:constraint_referenced_ids="title_edit_input_layout, mute_mentions_label, mute_mentions, leave, add_members, approval_required, approval_required_label" /> | ||||||
|             </androidx.constraintlayout.widget.ConstraintLayout> |             </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
| 
 | 
 | ||||||
|         </com.google.android.material.appbar.CollapsingToolbarLayout> |         </com.google.android.material.appbar.CollapsingToolbarLayout> | ||||||
| @ -155,5 +226,6 @@ | |||||||
|         android:id="@+id/users" |         android:id="@+id/users" | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="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" /> | ||||||
| </androidx.coordinatorlayout.widget.CoordinatorLayout> | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||||
							
								
								
									
										118
									
								
								app/src/main/res/layout/layout_dm_pending_user_item.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								app/src/main/res/layout/layout_dm_pending_user_item.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,118 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="72dp" | ||||||
|  |     android:background="?android:selectableItemBackground" | ||||||
|  |     android:clickable="true" | ||||||
|  |     android:focusable="true" | ||||||
|  |     android:orientation="horizontal" | ||||||
|  |     android:paddingStart="16dp" | ||||||
|  |     android:paddingEnd="16dp"> | ||||||
|  | 
 | ||||||
|  |     <com.facebook.drawee.view.SimpleDraweeView | ||||||
|  |         android:id="@+id/profile_pic" | ||||||
|  |         android:layout_width="@dimen/dm_inbox_avatar_size" | ||||||
|  |         android:layout_height="@dimen/dm_inbox_avatar_size" | ||||||
|  |         app:actualImageScaleType="centerCrop" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/username" | ||||||
|  |         app:layout_constraintHorizontal_chainStyle="spread_inside" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:roundAsCircle="true" | ||||||
|  |         tools:background="@mipmap/ic_launcher" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |         android:id="@+id/username" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginStart="16dp" | ||||||
|  |         android:ellipsize="end" | ||||||
|  |         android:gravity="bottom" | ||||||
|  |         android:singleLine="true" | ||||||
|  |         android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" | ||||||
|  |         app:layout_constraintBottom_toTopOf="@id/requester" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/controls_barrier" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/profile_pic" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:layout_constraintVertical_chainStyle="packed" | ||||||
|  |         tools:text="username......................." /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |         android:id="@+id/requester" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:ellipsize="end" | ||||||
|  |         android:maxLines="1" | ||||||
|  |         android:textAppearance="@style/TextAppearance.AppCompat.Caption" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="@id/username" | ||||||
|  |         app:layout_constraintStart_toStartOf="@id/username" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@id/username" | ||||||
|  |         tools:text="Added by someone" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.constraintlayout.widget.Barrier | ||||||
|  |         android:id="@+id/controls_barrier" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         app:barrierAllowsGoneWidgets="true" | ||||||
|  |         app:barrierDirection="start" | ||||||
|  |         app:constraint_referenced_ids="progress,approve,deny" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatImageView | ||||||
|  |         android:id="@+id/approve" | ||||||
|  |         android:layout_width="42dp" | ||||||
|  |         android:layout_height="42dp" | ||||||
|  |         android:layout_marginStart="4dp" | ||||||
|  |         android:background="?selectableItemBackgroundBorderless" | ||||||
|  |         android:scaleType="centerInside" | ||||||
|  |         android:visibility="visible" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/deny" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/controls_barrier" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:srcCompat="@drawable/ic_check_24" | ||||||
|  |         app:tint="@color/green_500" | ||||||
|  |         tools:visibility="visible" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatImageView | ||||||
|  |         android:id="@+id/deny" | ||||||
|  |         android:layout_width="42dp" | ||||||
|  |         android:layout_height="42dp" | ||||||
|  |         android:layout_marginStart="8dp" | ||||||
|  |         android:background="?selectableItemBackgroundBorderless" | ||||||
|  |         android:scaleType="centerInside" | ||||||
|  |         android:visibility="visible" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="@id/profile_pic" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/approve" | ||||||
|  |         app:layout_constraintTop_toTopOf="@id/profile_pic" | ||||||
|  |         app:srcCompat="@drawable/ic_close_24" | ||||||
|  |         app:tint="@color/red_500" | ||||||
|  |         tools:visibility="visible" /> | ||||||
|  | 
 | ||||||
|  |     <com.google.android.material.progressindicator.CircularProgressIndicator | ||||||
|  |         android:id="@+id/progress" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:indeterminate="true" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         app:indicatorColor="?colorSurface" | ||||||
|  |         app:indicatorSize="30dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="@id/profile_pic" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/controls_barrier" | ||||||
|  |         app:layout_constraintTop_toTopOf="@id/profile_pic" | ||||||
|  |         app:trackColor="@color/blue_900" | ||||||
|  |         tools:visibility="gone" /> | ||||||
|  | 
 | ||||||
|  |     <include | ||||||
|  |         layout="@layout/item_pref_divider" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="1dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="@id/thread_title" /> | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
| @ -9,12 +9,7 @@ | |||||||
| 
 | 
 | ||||||
|     <action |     <action | ||||||
|         android:id="@+id/action_global_profileFragment" |         android:id="@+id/action_global_profileFragment" | ||||||
|         app:destination="@id/profile_nav_graph"> |         app:destination="@id/profile_nav_graph" /> | ||||||
|         <argument |  | ||||||
|             android:name="username" |  | ||||||
|             app:argType="string" |  | ||||||
|             app:nullable="true" /> |  | ||||||
|     </action> |  | ||||||
| 
 | 
 | ||||||
|     <include app:graph="@navigation/location_nav_graph" /> |     <include app:graph="@navigation/location_nav_graph" /> | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -407,4 +407,10 @@ | |||||||
|     <string name="video">Video</string> |     <string name="video">Video</string> | ||||||
|     <string name="voice_message">Voice message</string> |     <string name="voice_message">Voice message</string> | ||||||
|     <string name="post">Post</string> |     <string name="post">Post</string> | ||||||
|  |     <string name="approval_required_for_new_members">Approval required to join</string> | ||||||
|  |     <string name="requests">Requests</string> | ||||||
|  |     <string name="admins_only">Admins only</string> | ||||||
|  |     <string name="added_by">Added by %s</string> | ||||||
|  |     <string name="admin_approval_required">Admin approval required</string> | ||||||
|  |     <string name="admin_approval_required_description">An admin approval will be required to add new members to the group</string> | ||||||
| </resources> | </resources> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user