mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 11:35:34 +00:00 
			
		
		
		
	Allow forwarding messages (need to check types which cannot be forwarded)
This commit is contained in:
		
							parent
							
								
									8e3d0af9d3
								
							
						
					
					
						commit
						8a659c9f1f
					
				| @ -6,80 +6,146 @@ import android.view.ViewGroup; | |||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.recyclerview.widget.DiffUtil; | import androidx.recyclerview.widget.DiffUtil; | ||||||
| import androidx.recyclerview.widget.ListAdapter; | import androidx.recyclerview.widget.ListAdapter; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
| 
 | 
 | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Objects; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener; | import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener; | ||||||
| import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder; | import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder; | ||||||
|  | import awais.instagrabber.adapters.viewholder.directmessages.RecipientThreadViewHolder; | ||||||
| import awais.instagrabber.databinding.LayoutDmUserItemBinding; | import awais.instagrabber.databinding.LayoutDmUserItemBinding; | ||||||
| import awais.instagrabber.repositories.responses.User; | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
| 
 | 
 | ||||||
| public final class UserSearchResultsAdapter extends ListAdapter<User, DirectUserViewHolder> { | public final class UserSearchResultsAdapter extends ListAdapter<RankedRecipient, RecyclerView.ViewHolder> { | ||||||
| 
 |     private static final int VIEW_TYPE_USER = 0; | ||||||
|     private static final DiffUtil.ItemCallback<User> DIFF_CALLBACK = new DiffUtil.ItemCallback<User>() { |     private static final int VIEW_TYPE_THREAD = 1; | ||||||
|  |     private static final DiffUtil.ItemCallback<RankedRecipient> DIFF_CALLBACK = new DiffUtil.ItemCallback<RankedRecipient>() { | ||||||
|         @Override |         @Override | ||||||
|         public boolean areItemsTheSame(@NonNull final User oldItem, @NonNull final User newItem) { |         public boolean areItemsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) { | ||||||
|             return oldItem.getPk() == newItem.getPk(); |             final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null; | ||||||
|  |             if (!bothUsers) return false; | ||||||
|  |             final boolean bothThreads = oldItem.getThread() != null && newItem.getThread() != null; | ||||||
|  |             if (!bothThreads) return false; | ||||||
|  |             if (bothUsers) { | ||||||
|  |                 return oldItem.getUser().getPk() == newItem.getUser().getPk(); | ||||||
|  |             } | ||||||
|  |             return Objects.equals(oldItem.getThread().getThreadId(), newItem.getThread().getThreadId()); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         @Override |         @Override | ||||||
|         public boolean areContentsTheSame(@NonNull final User oldItem, @NonNull final User newItem) { |         public boolean areContentsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) { | ||||||
|             return oldItem.getUsername().equals(newItem.getUsername()) && |             final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null; | ||||||
|                     oldItem.getFullName().equals(newItem.getFullName()); |             if (bothUsers) { | ||||||
|  |                 return Objects.equals(oldItem.getUser().getUsername(), newItem.getUser().getUsername()) && | ||||||
|  |                         Objects.equals(oldItem.getUser().getFullName(), newItem.getUser().getFullName()); | ||||||
|  |             } | ||||||
|  |             return Objects.equals(oldItem.getThread().getThreadTitle(), newItem.getThread().getThreadTitle()); | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
|  | 
 | ||||||
|     private final boolean showSelection; |     private final boolean showSelection; | ||||||
|     private final Set<Long> selectedUserIds; |     private final Set<RankedRecipient> selectedRecipients; | ||||||
|     private final OnDirectUserClickListener onUserClickListener; |     private final OnDirectUserClickListener onUserClickListener; | ||||||
|  |     private final OnRecipientClickListener onRecipientClickListener; | ||||||
| 
 | 
 | ||||||
|     public UserSearchResultsAdapter(final boolean showSelection, |     public UserSearchResultsAdapter(final boolean showSelection, | ||||||
|                                     final OnDirectUserClickListener onUserClickListener) { |                                     final OnRecipientClickListener onRecipientClickListener) { | ||||||
|         super(DIFF_CALLBACK); |         super(DIFF_CALLBACK); | ||||||
|         this.showSelection = showSelection; |         this.showSelection = showSelection; | ||||||
|         selectedUserIds = showSelection ? new HashSet<>() : null; |         selectedRecipients = showSelection ? new HashSet<>() : null; | ||||||
|         this.onUserClickListener = onUserClickListener; |         this.onRecipientClickListener = onRecipientClickListener; | ||||||
|  |         this.onUserClickListener = (position, user, selected) -> { | ||||||
|  |             if (onRecipientClickListener != null) { | ||||||
|  |                 onRecipientClickListener.onClick(position, RankedRecipient.of(user), selected); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|         setHasStableIds(true); |         setHasStableIds(true); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public DirectUserViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { |     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { | ||||||
|         final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); |         final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); | ||||||
|         final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false); |         final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false); | ||||||
|         return new DirectUserViewHolder(binding, onUserClickListener, null); |         if (viewType == VIEW_TYPE_USER) { | ||||||
| 
 |             return new DirectUserViewHolder(binding, onUserClickListener, null); | ||||||
|  |         } | ||||||
|  |         return new RecipientThreadViewHolder(binding, onRecipientClickListener); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onBindViewHolder(@NonNull final DirectUserViewHolder holder, final int position) { |     public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) { | ||||||
|         final User user = getItem(position); |         final RankedRecipient recipient = getItem(position); | ||||||
|         boolean isSelected = selectedUserIds != null && selectedUserIds.contains(user.getPk()); |         final int itemViewType = getItemViewType(position); | ||||||
|         holder.bind(position, user, false, false, showSelection, isSelected); |         if (itemViewType == VIEW_TYPE_USER) { | ||||||
|  |             boolean isSelected = false; | ||||||
|  |             if (selectedRecipients != null) { | ||||||
|  |                 isSelected = selectedRecipients.stream() | ||||||
|  |                                                .anyMatch(rankedRecipient -> rankedRecipient.getUser() != null | ||||||
|  |                                                        && rankedRecipient.getUser().getPk() == recipient.getUser().getPk()); | ||||||
|  |             } | ||||||
|  |             ((DirectUserViewHolder) holder).bind(position, recipient.getUser(), false, false, showSelection, isSelected); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         boolean isSelected = false; | ||||||
|  |         if (selectedRecipients != null) { | ||||||
|  |             isSelected = selectedRecipients.stream() | ||||||
|  |                                            .anyMatch(rankedRecipient -> rankedRecipient.getThread() != null | ||||||
|  |                                                    && Objects.equals(rankedRecipient.getThread().getThreadId(), recipient.getThread().getThreadId())); | ||||||
|  |         } | ||||||
|  |         ((RecipientThreadViewHolder) holder).bind(position, recipient.getThread(), showSelection, isSelected); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public long getItemId(final int position) { |     public long getItemId(final int position) { | ||||||
|         return getItem(position).getPk(); |         final RankedRecipient recipient = getItem(position); | ||||||
|  |         if (recipient.getUser() != null) { | ||||||
|  |             return recipient.getUser().getPk(); | ||||||
|  |         } | ||||||
|  |         if (recipient.getThread() != null) { | ||||||
|  |             return recipient.getThread().getThreadTitle().hashCode(); | ||||||
|  |         } | ||||||
|  |         return 0; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void setSelectedUser(final long userId, final boolean selected) { |     @Override | ||||||
|         if (selectedUserIds == null) return; |     public int getItemViewType(final int position) { | ||||||
|  |         final RankedRecipient recipient = getItem(position); | ||||||
|  |         return recipient.getUser() != null ? VIEW_TYPE_USER : VIEW_TYPE_THREAD; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSelectedRecipient(final RankedRecipient recipient, final boolean selected) { | ||||||
|  |         if (selectedRecipients == null || recipient == null || (recipient.getUser() == null && recipient.getThread() == null)) return; | ||||||
|  |         final boolean isUser = recipient.getUser() != null; | ||||||
|         int position = -1; |         int position = -1; | ||||||
|         final List<User> currentList = getCurrentList(); |         final List<RankedRecipient> currentList = getCurrentList(); | ||||||
|         for (int i = 0; i < currentList.size(); i++) { |         for (int i = 0; i < currentList.size(); i++) { | ||||||
|             if (currentList.get(i).getPk() == userId) { |             final RankedRecipient temp = currentList.get(i); | ||||||
|  |             if (isUser) { | ||||||
|  |                 if (temp.getUser() != null && temp.getUser().getPk() == recipient.getUser().getPk()) { | ||||||
|  |                     position = i; | ||||||
|  |                     break; | ||||||
|  |                 } | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  |             if (temp.getThread() != null && Objects.equals(temp.getThread().getThreadId(), recipient.getThread().getThreadId())) { | ||||||
|                 position = i; |                 position = i; | ||||||
|                 break; |                 break; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         if (position < 0) return; |         if (position < 0) return; | ||||||
|         if (selected) { |         if (selected) { | ||||||
|             selectedUserIds.add(userId); |             selectedRecipients.add(recipient); | ||||||
|         } else { |         } else { | ||||||
|             selectedUserIds.remove(userId); |             selectedRecipients.remove(recipient); | ||||||
|         } |         } | ||||||
|         notifyItemChanged(position); |         notifyItemChanged(position); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public interface OnRecipientClickListener { | ||||||
|  |         void onClick(int position, RankedRecipient recipient, final boolean isSelected); | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -22,4 +22,9 @@ public class DirectItemLikeViewHolder extends DirectItemViewHolder { | |||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {} |     public void bindItem(final DirectItem directItemModel, final MessageDirection messageDirection) {} | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected boolean canForward() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -184,4 +184,9 @@ public class DirectItemRavenMediaViewHolder extends DirectItemViewHolder { | |||||||
|         final String thumbUrl = ResponseBodyUtils.getThumbUrl(imageVersions2); |         final String thumbUrl = ResponseBodyUtils.getThumbUrl(imageVersions2); | ||||||
|         binding.preview.setImageURI(thumbUrl); |         binding.preview.setImageURI(thumbUrl); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected boolean allowLongClick() { | ||||||
|  |         return false; // disabling until confirmed | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -167,4 +167,9 @@ public class DirectItemReelShareViewHolder extends DirectItemViewHolder { | |||||||
|         final String thumbUrl = ResponseBodyUtils.getThumbUrl(imageVersions2); |         final String thumbUrl = ResponseBodyUtils.getThumbUrl(imageVersions2); | ||||||
|         binding.preview.setImageURI(thumbUrl); |         binding.preview.setImageURI(thumbUrl); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected boolean canForward() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -105,4 +105,9 @@ public class DirectItemStoryShareViewHolder extends DirectItemViewHolder { | |||||||
|         binding.ivMediaPreview.setVisibility(View.GONE); |         binding.ivMediaPreview.setVisibility(View.GONE); | ||||||
|         binding.typeIcon.setVisibility(View.GONE); |         binding.typeIcon.setVisibility(View.GONE); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     protected boolean canForward() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -135,6 +135,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | |||||||
|         } |         } | ||||||
|         setupReply(item, messageDirection); |         setupReply(item, messageDirection); | ||||||
|         setReactions(item, position); |         setReactions(item, position); | ||||||
|  |         if (item.getRepliedToMessage() == null && item.showForwardAttribution()) { | ||||||
|  |             setForwardInfo(messageDirection); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setBackground(final MessageDirection messageDirection) { |     private void setBackground(final MessageDirection messageDirection) { | ||||||
| @ -316,6 +319,11 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | |||||||
|         return String.format("Replied to %s", repliedToUsername); |         return String.format("Replied to %s", repliedToUsername); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private void setForwardInfo(final MessageDirection direction) { | ||||||
|  |         binding.replyInfo.setVisibility(View.VISIBLE); | ||||||
|  |         binding.replyInfo.setText(direction == MessageDirection.OUTGOING ? "You forwarded a message" : "Forwarded a message"); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void setReplyGravity(final MessageDirection messageDirection) { |     private void setReplyGravity(final MessageDirection messageDirection) { | ||||||
|         final boolean isIncoming = messageDirection == MessageDirection.INCOMING; |         final boolean isIncoming = messageDirection == MessageDirection.INCOMING; | ||||||
|         final ConstraintLayout.LayoutParams quoteLineLayoutParams = (ConstraintLayout.LayoutParams) binding.quoteLine.getLayoutParams(); |         final ConstraintLayout.LayoutParams quoteLineLayoutParams = (ConstraintLayout.LayoutParams) binding.quoteLine.getLayoutParams(); | ||||||
| @ -426,6 +434,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | |||||||
|         return true; |         return true; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     protected boolean canForward() { | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     protected List<DirectItemContextMenu.MenuItem> getLongClickOptions() { |     protected List<DirectItemContextMenu.MenuItem> getLongClickOptions() { | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| @ -510,6 +522,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | |||||||
|         if (longClickOptions != null) { |         if (longClickOptions != null) { | ||||||
|             builder.addAll(longClickOptions); |             builder.addAll(longClickOptions); | ||||||
|         } |         } | ||||||
|  |         if (canForward()) { | ||||||
|  |             builder.add(new DirectItemContextMenu.MenuItem(R.id.forward, R.string.forward)); | ||||||
|  |         } | ||||||
|         if (messageDirection == MessageDirection.OUTGOING) { |         if (messageDirection == MessageDirection.OUTGOING) { | ||||||
|             builder.add(new DirectItemContextMenu.MenuItem(R.id.unsend, R.string.dms_inbox_unsend)); |             builder.add(new DirectItemContextMenu.MenuItem(R.id.unsend, R.string.dms_inbox_unsend)); | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -169,6 +169,11 @@ public class DirectItemVoiceMediaViewHolder extends DirectItemViewHolder { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     protected boolean canForward() { | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private static class AudioItemState { |     private static class AudioItemState { | ||||||
|         private boolean prepared; |         private boolean prepared; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,89 @@ | |||||||
|  | package awais.instagrabber.adapters.viewholder.directmessages; | ||||||
|  | 
 | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.view.View; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.R; | ||||||
|  | import awais.instagrabber.adapters.UserSearchResultsAdapter.OnRecipientClickListener; | ||||||
|  | import awais.instagrabber.databinding.LayoutDmUserItemBinding; | ||||||
|  | import awais.instagrabber.repositories.responses.User; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
|  | 
 | ||||||
|  | public class RecipientThreadViewHolder extends RecyclerView.ViewHolder { | ||||||
|  |     private static final String TAG = RecipientThreadViewHolder.class.getSimpleName(); | ||||||
|  | 
 | ||||||
|  |     private final LayoutDmUserItemBinding binding; | ||||||
|  |     private final OnRecipientClickListener onThreadClickListener; | ||||||
|  |     private final float translateAmount; | ||||||
|  | 
 | ||||||
|  |     public RecipientThreadViewHolder(@NonNull final LayoutDmUserItemBinding binding, | ||||||
|  |                                      final OnRecipientClickListener onThreadClickListener) { | ||||||
|  |         super(binding.getRoot()); | ||||||
|  |         this.binding = binding; | ||||||
|  |         this.onThreadClickListener = onThreadClickListener; | ||||||
|  |         binding.info.setVisibility(View.GONE); | ||||||
|  |         final Resources resources = itemView.getResources(); | ||||||
|  |         final int avatarSize = resources.getDimensionPixelSize(R.dimen.dm_inbox_avatar_size); | ||||||
|  |         translateAmount = ((float) avatarSize) / 7; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void bind(final int position, | ||||||
|  |                      final DirectThread thread, | ||||||
|  |                      final boolean showSelection, | ||||||
|  |                      final boolean isSelected) { | ||||||
|  |         if (thread == null) return; | ||||||
|  |         binding.getRoot().setOnClickListener(v -> { | ||||||
|  |             if (onThreadClickListener == null) return; | ||||||
|  |             onThreadClickListener.onClick(position, RankedRecipient.of(thread), isSelected); | ||||||
|  |         }); | ||||||
|  |         binding.fullName.setText(thread.getThreadTitle()); | ||||||
|  |         setUsername(thread); | ||||||
|  |         setProfilePic(thread); | ||||||
|  |         setSelection(showSelection, isSelected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setProfilePic(final DirectThread thread) { | ||||||
|  |         final List<User> users = thread.getUsers(); | ||||||
|  |         binding.profilePic.setImageURI(users.get(0).getProfilePicUrl()); | ||||||
|  |         binding.profilePic.setScaleX(1); | ||||||
|  |         binding.profilePic.setScaleY(1); | ||||||
|  |         binding.profilePic.setTranslationX(0); | ||||||
|  |         binding.profilePic.setTranslationY(0); | ||||||
|  |         if (users.size() > 1) { | ||||||
|  |             binding.profilePic2.setVisibility(View.VISIBLE); | ||||||
|  |             binding.profilePic2.setImageURI(users.get(1).getProfilePicUrl()); | ||||||
|  |             binding.profilePic2.setTranslationX(translateAmount); | ||||||
|  |             binding.profilePic2.setTranslationY(translateAmount); | ||||||
|  |             final float scaleAmount = 0.75f; | ||||||
|  |             binding.profilePic2.setScaleX(scaleAmount); | ||||||
|  |             binding.profilePic2.setScaleY(scaleAmount); | ||||||
|  |             binding.profilePic.setScaleX(scaleAmount); | ||||||
|  |             binding.profilePic.setScaleY(scaleAmount); | ||||||
|  |             binding.profilePic.setTranslationX(-translateAmount); | ||||||
|  |             binding.profilePic.setTranslationY(-translateAmount); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         binding.profilePic2.setVisibility(View.GONE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setUsername(final DirectThread thread) { | ||||||
|  |         if (thread.isGroup()) { | ||||||
|  |             binding.username.setVisibility(View.GONE); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         binding.username.setVisibility(View.VISIBLE); | ||||||
|  |         // for a non-group thread, the thread title is the username so set the full name in the username text view | ||||||
|  |         binding.username.setText(thread.getUsers().get(0).getFullName()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setSelection(final boolean showSelection, final boolean isSelected) { | ||||||
|  |         binding.select.setVisibility(showSelection ? View.VISIBLE : View.GONE); | ||||||
|  |         binding.getRoot().setSelected(isSelected); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -23,11 +23,14 @@ import androidx.transition.TransitionManager; | |||||||
| import com.google.android.material.chip.Chip; | import com.google.android.material.chip.Chip; | ||||||
| import com.google.android.material.snackbar.Snackbar; | import com.google.android.material.snackbar.Snackbar; | ||||||
| 
 | 
 | ||||||
|  | import java.util.Objects; | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
| import awais.instagrabber.activities.MainActivity; | import awais.instagrabber.activities.MainActivity; | ||||||
| import awais.instagrabber.adapters.UserSearchResultsAdapter; | import awais.instagrabber.adapters.UserSearchResultsAdapter; | ||||||
| import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | ||||||
| import awais.instagrabber.databinding.FragmentUserSearchBinding; | import awais.instagrabber.databinding.FragmentUserSearchBinding; | ||||||
| import awais.instagrabber.repositories.responses.User; | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import awais.instagrabber.utils.Utils; | import awais.instagrabber.utils.Utils; | ||||||
| import awais.instagrabber.utils.ViewUtils; | import awais.instagrabber.utils.ViewUtils; | ||||||
| @ -46,7 +49,6 @@ public class UserSearchFragment extends Fragment { | |||||||
|     private String actionLabel; |     private String actionLabel; | ||||||
|     private String title; |     private String title; | ||||||
|     private boolean multiple; |     private boolean multiple; | ||||||
|     private long[] hideUserIds; |  | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { |     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
| @ -81,12 +83,17 @@ public class UserSearchFragment extends Fragment { | |||||||
|             actionLabel = fragmentArgs.getActionLabel(); |             actionLabel = fragmentArgs.getActionLabel(); | ||||||
|             title = fragmentArgs.getTitle(); |             title = fragmentArgs.getTitle(); | ||||||
|             multiple = fragmentArgs.getMultiple(); |             multiple = fragmentArgs.getMultiple(); | ||||||
|  |             viewModel.setHideThreadIds(fragmentArgs.getHideThreadIds()); | ||||||
|             viewModel.setHideUserIds(fragmentArgs.getHideUserIds()); |             viewModel.setHideUserIds(fragmentArgs.getHideUserIds()); | ||||||
|  |             viewModel.setSearchMode(fragmentArgs.getSearchMode()); | ||||||
|  |             viewModel.setShowGroups(fragmentArgs.getShowGroups()); | ||||||
|         } |         } | ||||||
|         setupTitles(); |         setupTitles(); | ||||||
|         setupInput(); |         setupInput(); | ||||||
|         setupResults(); |         setupResults(); | ||||||
|         setupObservers(); |         setupObservers(); | ||||||
|  |         // show cached results | ||||||
|  |         viewModel.showCachedResults(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setupTitles() { |     private void setupTitles() { | ||||||
| @ -108,54 +115,64 @@ public class UserSearchFragment extends Fragment { | |||||||
|         final Context context = getContext(); |         final Context context = getContext(); | ||||||
|         if (context == null) return; |         if (context == null) return; | ||||||
|         binding.results.setLayoutManager(new LinearLayoutManager(context)); |         binding.results.setLayoutManager(new LinearLayoutManager(context)); | ||||||
|         resultsAdapter = new UserSearchResultsAdapter(multiple, (position, user, selected) -> { |         resultsAdapter = new UserSearchResultsAdapter(multiple, (position, recipient, selected) -> { | ||||||
|             if (!multiple) { |             if (!multiple) { | ||||||
|                 final NavController navController = NavHostFragment.findNavController(this); |                 final NavController navController = NavHostFragment.findNavController(this); | ||||||
|                 final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); |                 if (!setResult(navController, recipient)) return; | ||||||
|                 if (navBackStackEntry == null) return; |  | ||||||
|                 final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); |  | ||||||
|                 savedStateHandle.set("result", user); |  | ||||||
|                 navController.navigateUp(); |                 navController.navigateUp(); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             viewModel.setSelectedUser(user, !selected); |             viewModel.setSelectedRecipient(recipient, !selected); | ||||||
|             resultsAdapter.setSelectedUser(user.getPk(), !selected); |             resultsAdapter.setSelectedRecipient(recipient, !selected); | ||||||
|             if (!selected) { |             if (!selected) { | ||||||
|                 createUserChip(user); |                 createChip(recipient); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             final View chip = findChip(user.getPk()); |             final View chip = findChip(recipient); | ||||||
|             if (chip == null) return; |             if (chip == null) return; | ||||||
|             removeChip(chip); |             removeChipFromGroup(chip); | ||||||
|         }); |         }); | ||||||
|         binding.results.setAdapter(resultsAdapter); |         binding.results.setAdapter(resultsAdapter); | ||||||
|         binding.done.setOnClickListener(v -> { |         binding.done.setOnClickListener(v -> { | ||||||
|             final NavController navController = NavHostFragment.findNavController(this); |             final NavController navController = NavHostFragment.findNavController(this); | ||||||
|             final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); |             if (!setResult(navController, viewModel.getSelectedRecipients())) return; | ||||||
|             if (navBackStackEntry == null) return; |  | ||||||
|             final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); |  | ||||||
|             savedStateHandle.set("result", viewModel.getSelectedUsers()); |  | ||||||
|             navController.navigateUp(); |             navController.navigateUp(); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private boolean setResult(@NonNull final NavController navController, final RankedRecipient rankedRecipient) { | ||||||
|  |         final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); | ||||||
|  |         if (navBackStackEntry == null) return false; | ||||||
|  |         final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); | ||||||
|  |         savedStateHandle.set("result", rankedRecipient); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean setResult(@NonNull final NavController navController, final Set<RankedRecipient> rankedRecipients) { | ||||||
|  |         final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); | ||||||
|  |         if (navBackStackEntry == null) return false; | ||||||
|  |         final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); | ||||||
|  |         savedStateHandle.set("result", rankedRecipients); | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void setupInput() { |     private void setupInput() { | ||||||
|         binding.search.addTextChangedListener(new TextWatcherAdapter() { |         binding.search.addTextChangedListener(new TextWatcherAdapter() { | ||||||
|             @Override |             @Override | ||||||
|             public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { |             public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { | ||||||
|                 if (TextUtils.isEmpty(s)) { |                 // if (TextUtils.isEmpty(s)) { | ||||||
|                     viewModel.cancelSearch(); |                 //     viewModel.cancelSearch(); | ||||||
|                     viewModel.clearResults(); |                 //     viewModel.clearResults(); | ||||||
|                     return; |                 //     return; | ||||||
|                 } |                 // } | ||||||
|                 viewModel.search(s.toString().trim()); |                 viewModel.search(s == null ? null : s.toString().trim()); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|         binding.search.setOnKeyListener((v, keyCode, event) -> { |         binding.search.setOnKeyListener((v, keyCode, event) -> { | ||||||
|             if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { |             if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { | ||||||
|                 final View chip = getLastChip(); |                 final View chip = getLastChip(); | ||||||
|                 if (chip == null) return false; |                 if (chip == null) return false; | ||||||
|                 removeSelectedUser(chip); |                 removeChip(chip); | ||||||
|             } |             } | ||||||
|             return false; |             return false; | ||||||
|         }); |         }); | ||||||
| @ -175,32 +192,41 @@ public class UserSearchFragment extends Fragment { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setupObservers() { |     private void setupObservers() { | ||||||
|         viewModel.getUsers().observe(getViewLifecycleOwner(), results -> { |         viewModel.getRecipients().observe(getViewLifecycleOwner(), results -> { | ||||||
|             if (results == null) return; |             if (results == null) return; | ||||||
|             switch (results.status) { |             switch (results.status) { | ||||||
|                 case SUCCESS: |                 case SUCCESS: | ||||||
|                     resultsAdapter.submitList(results.data); |                     if (results.data != null) { | ||||||
|  |                         resultsAdapter.submitList(results.data); | ||||||
|  |                     } | ||||||
|                     break; |                     break; | ||||||
|                 case ERROR: |                 case ERROR: | ||||||
|                     if (results.message != null) { |                     if (results.message != null) { | ||||||
|                         Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show(); |                         Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show(); | ||||||
|                     } |                     } | ||||||
|  |                     if (results.data != null) { | ||||||
|  |                         resultsAdapter.submitList(results.data); | ||||||
|  |                     } | ||||||
|                     break; |                     break; | ||||||
|                 case LOADING: |                 case LOADING: | ||||||
|  |                     //noinspection DuplicateBranchesInSwitch | ||||||
|  |                     if (results.data != null) { | ||||||
|  |                         resultsAdapter.submitList(results.data); | ||||||
|  |                     } | ||||||
|                     break; |                     break; | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|         viewModel.showAction().observe(getViewLifecycleOwner(), showAction -> binding.done.setVisibility(showAction ? View.VISIBLE : View.GONE)); |         viewModel.showAction().observe(getViewLifecycleOwner(), showAction -> binding.done.setVisibility(showAction ? View.VISIBLE : View.GONE)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void createUserChip(final User user) { |     private void createChip(final RankedRecipient recipient) { | ||||||
|         final Context context = getContext(); |         final Context context = getContext(); | ||||||
|         if (context == null) return; |         if (context == null) return; | ||||||
|         final Chip chip = new Chip(context); |         final Chip chip = new Chip(context); | ||||||
|         chip.setTag(user); |         chip.setTag(recipient); | ||||||
|         chip.setText(user.getFullName()); |         chip.setText(getRecipientText(recipient)); | ||||||
|         chip.setCloseIconVisible(true); |         chip.setCloseIconVisible(true); | ||||||
|         chip.setOnCloseIconClickListener(v -> removeSelectedUser(chip)); |         chip.setOnCloseIconClickListener(v -> removeChip(chip)); | ||||||
|         binding.group.post(() -> { |         binding.group.post(() -> { | ||||||
|             final Pair<Integer, Integer> measure = ViewUtils.measure(chip, binding.group); |             final Pair<Integer, Integer> measure = ViewUtils.measure(chip, binding.group); | ||||||
|             TransitionManager.beginDelayedTransition(binding.getRoot()); |             TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||||
| @ -209,31 +235,44 @@ public class UserSearchFragment extends Fragment { | |||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void removeSelectedUser(final View chip) { |     private String getRecipientText(final RankedRecipient recipient) { | ||||||
|         final User user = (User) chip.getTag(); |         if (recipient == null) return null; | ||||||
|         if (user == null) return; |         if (recipient.getUser() != null) { | ||||||
|         viewModel.setSelectedUser(user, false); |             return recipient.getUser().getFullName(); | ||||||
|         resultsAdapter.setSelectedUser(user.getPk(), false); |         } | ||||||
|         removeChip(chip); |         if (recipient.getThread() != null) { | ||||||
|  |             return recipient.getThread().getThreadTitle(); | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private View findChip(final long userId) { |     private void removeChip(@NonNull final View chip) { | ||||||
|  |         final RankedRecipient recipient = (RankedRecipient) chip.getTag(); | ||||||
|  |         if (recipient == null) return; | ||||||
|  |         viewModel.setSelectedRecipient(recipient, false); | ||||||
|  |         resultsAdapter.setSelectedRecipient(recipient, false); | ||||||
|  |         removeChipFromGroup(chip); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private View findChip(final RankedRecipient recipient) { | ||||||
|  |         if (recipient == null || recipient.getUser() == null && recipient.getThread() == null) return null; | ||||||
|  |         boolean isUser = recipient.getUser() != null; | ||||||
|         final int childCount = binding.group.getChildCount(); |         final int childCount = binding.group.getChildCount(); | ||||||
|         if (childCount == 0) { |         if (childCount == 0) return null; | ||||||
|             return null; |  | ||||||
|         } |  | ||||||
|         for (int i = childCount - 1; i >= 0; i--) { |         for (int i = childCount - 1; i >= 0; i--) { | ||||||
|             final View child = binding.group.getChildAt(i); |             final View child = binding.group.getChildAt(i); | ||||||
|             if (child == null) continue; |             if (child == null) continue; | ||||||
|             final User user = (User) child.getTag(); |             final RankedRecipient tag = (RankedRecipient) child.getTag(); | ||||||
|             if (user != null && user.getPk() == userId) { |             if (tag == null || isUser && tag.getUser() == null || !isUser && tag.getThread() == null) continue; | ||||||
|  |             if ((isUser && tag.getUser().getPk() == recipient.getUser().getPk()) | ||||||
|  |                     || (!isUser && Objects.equals(tag.getThread().getThreadId(), recipient.getThread().getThreadId()))) { | ||||||
|                 return child; |                 return child; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void removeChip(final View chip) { |     private void removeChipFromGroup(final View chip) { | ||||||
|         binding.group.post(() -> { |         binding.group.post(() -> { | ||||||
|             TransitionManager.beginDelayedTransition(binding.getRoot()); |             TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||||
|             binding.group.removeView(chip); |             binding.group.removeView(chip); | ||||||
| @ -267,4 +306,20 @@ public class UserSearchFragment extends Fragment { | |||||||
|         } |         } | ||||||
|         return null; |         return null; | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public enum SearchMode { | ||||||
|  |         USER_SEARCH("user_name"), | ||||||
|  |         RAVEN("raven"), | ||||||
|  |         RESHARE("reshare"); | ||||||
|  | 
 | ||||||
|  |         private final String name; | ||||||
|  | 
 | ||||||
|  |         SearchMode(final String name) { | ||||||
|  |             this.name = name; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public String getName() { | ||||||
|  |             return name; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -20,7 +20,6 @@ import androidx.lifecycle.ViewModelStoreOwner; | |||||||
| import androidx.navigation.NavBackStackEntry; | import androidx.navigation.NavBackStackEntry; | ||||||
| import androidx.navigation.NavController; | import androidx.navigation.NavController; | ||||||
| import androidx.navigation.NavDestination; | import androidx.navigation.NavDestination; | ||||||
| import androidx.navigation.NavDirections; |  | ||||||
| import androidx.navigation.fragment.NavHostFragment; | import androidx.navigation.fragment.NavHostFragment; | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
| 
 | 
 | ||||||
| @ -29,18 +28,24 @@ import com.google.android.material.snackbar.Snackbar; | |||||||
| 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.Objects; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
|  | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.R; | import awais.instagrabber.R; | ||||||
|  | import awais.instagrabber.UserSearchNavGraphDirections; | ||||||
| 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.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.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.RankedRecipient; | ||||||
| import awais.instagrabber.viewmodels.DirectInboxViewModel; | import awais.instagrabber.viewmodels.DirectInboxViewModel; | ||||||
| import awais.instagrabber.viewmodels.DirectSettingsViewModel; | import awais.instagrabber.viewmodels.DirectSettingsViewModel; | ||||||
| 
 | 
 | ||||||
| @ -155,6 +160,12 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|         setupObservers(); |         setupObservers(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDestroyView() { | ||||||
|  |         super.onDestroyView(); | ||||||
|  |         binding = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void setupObservers() { |     private void setupObservers() { | ||||||
|         viewModel.getUsers().observe(getViewLifecycleOwner(), users -> { |         viewModel.getUsers().observe(getViewLifecycleOwner(), users -> { | ||||||
|             if (usersAdapter == null) return; |             if (usersAdapter == null) return; | ||||||
| @ -171,16 +182,27 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|             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; |                 LiveData<Resource<Object>> detailsChangeResourceLiveData = null; | ||||||
|                 if ((result instanceof User)) { |                 if ((result instanceof RankedRecipient)) { | ||||||
|                     // Log.d(TAG, "result: " + result); |                     final RankedRecipient recipient = (RankedRecipient) result; | ||||||
|                     detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton((User) result)); |                     final User user = getUser(recipient); | ||||||
|  |                     // Log.d(TAG, "result: " + user); | ||||||
|  |                     if (user != null) { | ||||||
|  |                         detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton(recipient.getUser())); | ||||||
|  |                     } | ||||||
|                 } else if ((result instanceof Set)) { |                 } else if ((result instanceof Set)) { | ||||||
|                     try { |                     try { | ||||||
|                         // Log.d(TAG, "result: " + result); |  | ||||||
|                         //noinspection unchecked |                         //noinspection unchecked | ||||||
|                         detailsChangeResourceLiveData = viewModel.addMembers((Set<User>) result); |                         final Set<RankedRecipient> recipients = (Set<RankedRecipient>) result; | ||||||
|  |                         final Set<User> users = recipients.stream() | ||||||
|  |                                                           .filter(Objects::nonNull) | ||||||
|  |                                                           .map(this::getUser) | ||||||
|  |                                                           .filter(Objects::nonNull) | ||||||
|  |                                                           .collect(Collectors.toSet()); | ||||||
|  |                         // Log.d(TAG, "result: " + users); | ||||||
|  |                         detailsChangeResourceLiveData = viewModel.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(), Snackbar.LENGTH_LONG).show(); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 if (detailsChangeResourceLiveData != null) { |                 if (detailsChangeResourceLiveData != null) { | ||||||
| @ -190,6 +212,17 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     @Nullable | ||||||
|  |     private User getUser(@NonNull final RankedRecipient recipient) { | ||||||
|  |         User user = null; | ||||||
|  |         if (recipient.getUser() != null) { | ||||||
|  |             user = recipient.getUser(); | ||||||
|  |         } else if (recipient.getThread() != null && !recipient.getThread().isGroup()) { | ||||||
|  |             user = recipient.getThread().getUsers().get(0); | ||||||
|  |         } | ||||||
|  |         return user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private void init() { |     private void init() { | ||||||
|         setupSettings(); |         setupSettings(); | ||||||
|         setupMembers(); |         setupMembers(); | ||||||
| @ -232,13 +265,14 @@ public class DirectMessageSettingsFragment extends Fragment { | |||||||
|             } else { |             } else { | ||||||
|                 currentUserIds = new long[0]; |                 currentUserIds = new long[0]; | ||||||
|             } |             } | ||||||
|             final NavDirections directions = DirectMessageSettingsFragmentDirections.actionGlobalUserSearch( |             final UserSearchNavGraphDirections.ActionGlobalUserSearch actionGlobalUserSearch = UserSearchFragmentDirections | ||||||
|                     true, |                     .actionGlobalUserSearch() | ||||||
|                     "Add users", |                     .setTitle(getString(R.string.add_members)) | ||||||
|                     "Add", |                     .setActionLabel(getString(R.string.add)) | ||||||
|                     currentUserIds |                     .setHideUserIds(currentUserIds) | ||||||
|             ); |                     .setSearchMode(UserSearchFragment.SearchMode.RAVEN) | ||||||
|             navController.navigate(directions); |                     .setMultiple(true); | ||||||
|  |             navController.navigate(actionGlobalUserSearch); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -34,6 +34,7 @@ import androidx.fragment.app.Fragment; | |||||||
| import androidx.lifecycle.LiveData; | import androidx.lifecycle.LiveData; | ||||||
| import androidx.lifecycle.MediatorLiveData; | import androidx.lifecycle.MediatorLiveData; | ||||||
| import androidx.lifecycle.MutableLiveData; | import androidx.lifecycle.MutableLiveData; | ||||||
|  | import androidx.lifecycle.Observer; | ||||||
| import androidx.lifecycle.ViewModelProvider; | import androidx.lifecycle.ViewModelProvider; | ||||||
| import androidx.lifecycle.ViewModelStoreOwner; | import androidx.lifecycle.ViewModelStoreOwner; | ||||||
| import androidx.navigation.NavBackStackEntry; | import androidx.navigation.NavBackStackEntry; | ||||||
| @ -53,8 +54,10 @@ import java.io.File; | |||||||
| import java.util.ArrayList; | import java.util.ArrayList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Optional; | import java.util.Optional; | ||||||
|  | import java.util.Set; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.R; | import awais.instagrabber.R; | ||||||
|  | import awais.instagrabber.UserSearchNavGraphDirections; | ||||||
| import awais.instagrabber.activities.CameraActivity; | import awais.instagrabber.activities.CameraActivity; | ||||||
| import awais.instagrabber.activities.MainActivity; | import awais.instagrabber.activities.MainActivity; | ||||||
| import awais.instagrabber.adapters.DirectItemsAdapter; | import awais.instagrabber.adapters.DirectItemsAdapter; | ||||||
| @ -75,6 +78,8 @@ import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; | |||||||
| import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; | import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; | ||||||
| import awais.instagrabber.dialogs.MediaPickerBottomDialogFragment; | import awais.instagrabber.dialogs.MediaPickerBottomDialogFragment; | ||||||
| import awais.instagrabber.fragments.PostViewV2Fragment; | import awais.instagrabber.fragments.PostViewV2Fragment; | ||||||
|  | import awais.instagrabber.fragments.UserSearchFragment; | ||||||
|  | import awais.instagrabber.fragments.UserSearchFragmentDirections; | ||||||
| import awais.instagrabber.models.Resource; | import awais.instagrabber.models.Resource; | ||||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||||
| import awais.instagrabber.repositories.responses.Media; | import awais.instagrabber.repositories.responses.Media; | ||||||
| @ -83,6 +88,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectItem; | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; | import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare; | import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
| import awais.instagrabber.utils.AppExecutors; | import awais.instagrabber.utils.AppExecutors; | ||||||
| import awais.instagrabber.utils.PermissionUtils; | import awais.instagrabber.utils.PermissionUtils; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| @ -124,6 +130,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|     private boolean isRecording; |     private boolean isRecording; | ||||||
|     private boolean wasKbShowing; |     private boolean wasKbShowing; | ||||||
|     private int keyboardHeight = Utils.convertDpToPx(250); |     private int keyboardHeight = Utils.convertDpToPx(250); | ||||||
|  |     private DirectItemReactionDialogFragment reactionDialogFragment; | ||||||
|  |     private DirectItem itemToForward; | ||||||
|  |     private MutableLiveData<Object> backStackSavedStateResultLiveData; | ||||||
| 
 | 
 | ||||||
|     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() { | ||||||
| @ -229,13 +238,48 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|                 handleSentMessage(viewModel.unsend(item)); |                 handleSentMessage(viewModel.unsend(item)); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|  |             if (itemId == R.id.forward) { | ||||||
|  |                 itemToForward = item; | ||||||
|  |                 final UserSearchNavGraphDirections.ActionGlobalUserSearch actionGlobalUserSearch = UserSearchFragmentDirections | ||||||
|  |                         .actionGlobalUserSearch() | ||||||
|  |                         .setTitle(getString(R.string.forward)) | ||||||
|  |                         .setActionLabel(getString(R.string.send)) | ||||||
|  |                         .setShowGroups(true) | ||||||
|  |                         .setMultiple(true) | ||||||
|  |                         .setSearchMode(UserSearchFragment.SearchMode.RAVEN); | ||||||
|  |                 final NavController navController = NavHostFragment.findNavController(DirectMessageThreadFragment.this); | ||||||
|  |                 navController.navigate(actionGlobalUserSearch); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     private final DirectItemLongClickListener directItemLongClickListener = position -> { |     private final DirectItemLongClickListener directItemLongClickListener = position -> { | ||||||
|         // viewModel.setSelectedPosition(position); |         // viewModel.setSelectedPosition(position); | ||||||
|     }; |     }; | ||||||
|     private DirectItemReactionDialogFragment reactionDialogFragment; |     private final Observer<Object> backStackSavedStateObserver = result -> { | ||||||
|  |         if (result == null) return; | ||||||
|  |         if (result instanceof Uri) { | ||||||
|  |             final Uri uri = (Uri) result; | ||||||
|  |             handleSentMessage(viewModel.sendUri(uri)); | ||||||
|  |         } else if ((result instanceof RankedRecipient)) { | ||||||
|  |             // Log.d(TAG, "result: " + result); | ||||||
|  |             if (itemToForward != null) { | ||||||
|  |                 viewModel.forward((RankedRecipient) result, itemToForward); | ||||||
|  |             } | ||||||
|  |         } else if ((result instanceof Set)) { | ||||||
|  |             try { | ||||||
|  |                 // Log.d(TAG, "result: " + result); | ||||||
|  |                 if (itemToForward != null) { | ||||||
|  |                     //noinspection unchecked | ||||||
|  |                     viewModel.forward((Set<RankedRecipient>) result, itemToForward); | ||||||
|  |                 } | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 Log.e(TAG, "forward result: ", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         // clear result | ||||||
|  |         backStackSavedStateResultLiveData.postValue(null); | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { |     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
| @ -361,6 +405,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|             binding.send.setX(initialSendX); |             binding.send.setX(initialSendX); | ||||||
|         } |         } | ||||||
|         binding.send.stopScale(); |         binding.send.stopScale(); | ||||||
|  |         setupBackStackResultObserver(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
| @ -587,17 +632,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | |||||||
|             setupItemsAdapter(userThreadPair.first, userThreadPair.second); |             setupItemsAdapter(userThreadPair.first, userThreadPair.second); | ||||||
|         }); |         }); | ||||||
|         viewModel.getItems().observe(getViewLifecycleOwner(), this::submitItemsToAdapter); |         viewModel.getItems().observe(getViewLifecycleOwner(), this::submitItemsToAdapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setupBackStackResultObserver() { | ||||||
|         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"); |             backStackSavedStateResultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); | ||||||
|             resultLiveData.observe(getViewLifecycleOwner(), result -> { |             backStackSavedStateResultLiveData.observe(getViewLifecycleOwner(), backStackSavedStateObserver); | ||||||
|                 if (!(result instanceof Uri)) return; |  | ||||||
|                 final Uri uri = (Uri) result; |  | ||||||
|                 viewModel.sendUri(uri); |  | ||||||
|                 // clear result |  | ||||||
|                 resultLiveData.postValue(null); |  | ||||||
|             }); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,7 @@ | |||||||
| package awais.instagrabber.models.enums; | package awais.instagrabber.models.enums; | ||||||
| 
 | 
 | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | 
 | ||||||
| import com.google.gson.annotations.SerializedName; | import com.google.gson.annotations.SerializedName; | ||||||
| 
 | 
 | ||||||
| import java.io.Serializable; | import java.io.Serializable; | ||||||
| @ -62,4 +64,46 @@ public enum DirectItemType implements Serializable { | |||||||
|     public static DirectItemType valueOf(final int id) { |     public static DirectItemType valueOf(final int id) { | ||||||
|         return map.get(id); |         return map.get(id); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     public String getName() { | ||||||
|  |         switch (this) { | ||||||
|  |             case TEXT: | ||||||
|  |                 return "text"; | ||||||
|  |             case LIKE: | ||||||
|  |                 return "like"; | ||||||
|  |             case LINK: | ||||||
|  |                 return "link"; | ||||||
|  |             case MEDIA: | ||||||
|  |                 return "media"; | ||||||
|  |             case RAVEN_MEDIA: | ||||||
|  |                 return "raven_media"; | ||||||
|  |             case PROFILE: | ||||||
|  |                 return "profile"; | ||||||
|  |             case VIDEO_CALL_EVENT: | ||||||
|  |                 return "video_call_event"; | ||||||
|  |             case ANIMATED_MEDIA: | ||||||
|  |                 return "animated_media"; | ||||||
|  |             case VOICE_MEDIA: | ||||||
|  |                 return "voice_media"; | ||||||
|  |             case MEDIA_SHARE: | ||||||
|  |                 return "media_share"; | ||||||
|  |             case REEL_SHARE: | ||||||
|  |                 return "reel_share"; | ||||||
|  |             case ACTION_LOG: | ||||||
|  |                 return "action_log"; | ||||||
|  |             case PLACEHOLDER: | ||||||
|  |                 return "placeholder"; | ||||||
|  |             case STORY_SHARE: | ||||||
|  |                 return "story_share"; | ||||||
|  |             case CLIP: | ||||||
|  |                 return "clip"; | ||||||
|  |             case FELIX_SHARE: | ||||||
|  |                 return "felix_share"; | ||||||
|  |             case LOCATION: | ||||||
|  |                 return "location"; | ||||||
|  |             default: | ||||||
|  |                 return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
| @ -4,9 +4,11 @@ import java.util.Map; | |||||||
| 
 | 
 | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | ||||||
|  | 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.RankedRecipientsResponse; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| import retrofit2.http.FieldMap; | import retrofit2.http.FieldMap; | ||||||
| import retrofit2.http.FormUrlEncoded; | import retrofit2.http.FormUrlEncoded; | ||||||
| @ -62,4 +64,15 @@ public interface DirectMessagesRepository { | |||||||
|     Call<String> deleteItem(@Path("threadId") String threadId, |     Call<String> deleteItem(@Path("threadId") String threadId, | ||||||
|                             @Path("itemId") String itemId, |                             @Path("itemId") String itemId, | ||||||
|                             @FieldMap final Map<String, String> form); |                             @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @GET("/api/v1/direct_v2/ranked_recipients/") | ||||||
|  |     Call<RankedRecipientsResponse> rankedRecipients(@QueryMap Map<String, String> queryMap); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/broadcast/forward/") | ||||||
|  |     Call<DirectThreadBroadcastResponse> forward(@FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/create_group_thread/") | ||||||
|  |     Call<DirectThread> createThread(@FieldMap final Map<String, String> signedForm); | ||||||
| } | } | ||||||
|  | |||||||
| @ -39,6 +39,7 @@ public class DirectItem implements Cloneable { | |||||||
|     private final int hideInThread; |     private final int hideInThread; | ||||||
|     private Date date; |     private Date date; | ||||||
|     private boolean isPending; |     private boolean isPending; | ||||||
|  |     private boolean showForwardAttribution; | ||||||
| 
 | 
 | ||||||
|     public DirectItem(final String itemId, |     public DirectItem(final String itemId, | ||||||
|                       final long userId, |                       final long userId, | ||||||
| @ -65,7 +66,8 @@ public class DirectItem implements Cloneable { | |||||||
|                       final DirectItem repliedToMessage, |                       final DirectItem repliedToMessage, | ||||||
|                       final DirectItemVoiceMedia voiceMedia, |                       final DirectItemVoiceMedia voiceMedia, | ||||||
|                       final Location location, |                       final Location location, | ||||||
|                       final int hideInThread) { |                       final int hideInThread, | ||||||
|  |                       final boolean showForwardAttribution) { | ||||||
|         this.itemId = itemId; |         this.itemId = itemId; | ||||||
|         this.userId = userId; |         this.userId = userId; | ||||||
|         this.timestamp = timestamp; |         this.timestamp = timestamp; | ||||||
| @ -92,6 +94,7 @@ public class DirectItem implements Cloneable { | |||||||
|         this.voiceMedia = voiceMedia; |         this.voiceMedia = voiceMedia; | ||||||
|         this.location = location; |         this.location = location; | ||||||
|         this.hideInThread = hideInThread; |         this.hideInThread = hideInThread; | ||||||
|  |         this.showForwardAttribution = showForwardAttribution; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public String getItemId() { |     public String getItemId() { | ||||||
| @ -222,6 +225,10 @@ public class DirectItem implements Cloneable { | |||||||
|         this.reactions = reactions; |         this.reactions = reactions; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public boolean showForwardAttribution() { | ||||||
|  |         return showForwardAttribution; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public Object clone() throws CloneNotSupportedException { |     public Object clone() throws CloneNotSupportedException { | ||||||
|  | |||||||
| @ -2,13 +2,14 @@ package awais.instagrabber.repositories.responses.directmessages; | |||||||
| 
 | 
 | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
|  | import java.io.Serializable; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.repositories.responses.User; | import awais.instagrabber.repositories.responses.User; | ||||||
| 
 | 
 | ||||||
| public class DirectThread { | public class DirectThread implements Serializable { | ||||||
|     private final String threadId; |     private final String threadId; | ||||||
|     private final String threadV2Id; |     private final String threadV2Id; | ||||||
|     private final List<User> users; |     private final List<User> users; | ||||||
|  | |||||||
| @ -0,0 +1,46 @@ | |||||||
|  | package awais.instagrabber.repositories.responses.directmessages; | ||||||
|  | 
 | ||||||
|  | import java.io.Serializable; | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.repositories.responses.User; | ||||||
|  | 
 | ||||||
|  | public class RankedRecipient implements Serializable { | ||||||
|  |     private final User user; | ||||||
|  |     private final DirectThread thread; | ||||||
|  | 
 | ||||||
|  |     public RankedRecipient(final User user, final DirectThread thread) { | ||||||
|  |         this.user = user; | ||||||
|  |         this.thread = thread; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public User getUser() { | ||||||
|  |         return user; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public DirectThread getThread() { | ||||||
|  |         return thread; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static RankedRecipient of(final User user) { | ||||||
|  |         return new RankedRecipient(user, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static RankedRecipient of(final DirectThread thread) { | ||||||
|  |         return new RankedRecipient(null, thread); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean equals(final Object o) { | ||||||
|  |         if (this == o) return true; | ||||||
|  |         if (o == null || getClass() != o.getClass()) return false; | ||||||
|  |         final RankedRecipient that = (RankedRecipient) o; | ||||||
|  |         return Objects.equals(user, that.user) && | ||||||
|  |                 Objects.equals(thread, that.thread); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int hashCode() { | ||||||
|  |         return Objects.hash(user, thread); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,50 @@ | |||||||
|  | package awais.instagrabber.repositories.responses.directmessages; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | public class RankedRecipientsResponse { | ||||||
|  |     private final List<RankedRecipient> rankedRecipients; | ||||||
|  |     private final long expires; | ||||||
|  |     private final boolean filtered; | ||||||
|  |     private final String requestId; | ||||||
|  |     private final String rankToken; | ||||||
|  |     private final String status; | ||||||
|  | 
 | ||||||
|  |     public RankedRecipientsResponse(final List<RankedRecipient> rankedRecipients, | ||||||
|  |                                     final long expires, | ||||||
|  |                                     final boolean filtered, | ||||||
|  |                                     final String requestId, | ||||||
|  |                                     final String rankToken, | ||||||
|  |                                     final String status) { | ||||||
|  |         this.rankedRecipients = rankedRecipients; | ||||||
|  |         this.expires = expires; | ||||||
|  |         this.filtered = filtered; | ||||||
|  |         this.requestId = requestId; | ||||||
|  |         this.rankToken = rankToken; | ||||||
|  |         this.status = status; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public List<RankedRecipient> getRankedRecipients() { | ||||||
|  |         return rankedRecipients; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public long getExpires() { | ||||||
|  |         return expires; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isFiltered() { | ||||||
|  |         return filtered; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getRequestId() { | ||||||
|  |         return requestId; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getRankToken() { | ||||||
|  |         return rankToken; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getStatus() { | ||||||
|  |         return status; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -47,8 +47,8 @@ public class DirectItemFactory { | |||||||
|                 null, |                 null, | ||||||
|                 null, |                 null, | ||||||
|                 null, |                 null, | ||||||
|                 0 |                 0, | ||||||
|         ); |                 false); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static DirectItem createImageOrVideo(final long userId, |     public static DirectItem createImageOrVideo(final long userId, | ||||||
| @ -128,8 +128,8 @@ public class DirectItemFactory { | |||||||
|                 null, |                 null, | ||||||
|                 null, |                 null, | ||||||
|                 null, |                 null, | ||||||
|                 0 |                 0, | ||||||
|         ); |                 false); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public static DirectItem createVoice(final long userId, |     public static DirectItem createVoice(final long userId, | ||||||
| @ -209,7 +209,7 @@ public class DirectItemFactory { | |||||||
|                 null, |                 null, | ||||||
|                 voiceMedia, |                 voiceMedia, | ||||||
|                 null, |                 null, | ||||||
|                 0 |                 0, | ||||||
|         ); |                 false); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -0,0 +1,69 @@ | |||||||
|  | package awais.instagrabber.utils; | ||||||
|  | 
 | ||||||
|  | import java.time.LocalDateTime; | ||||||
|  | import java.time.temporal.ChronoUnit; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | ||||||
|  | 
 | ||||||
|  | public class RankedRecipientsCache { | ||||||
|  |     private static final Object LOCK = new Object(); | ||||||
|  | 
 | ||||||
|  |     private static RankedRecipientsCache instance; | ||||||
|  | 
 | ||||||
|  |     public static RankedRecipientsCache getInstance() { | ||||||
|  |         if (instance == null) { | ||||||
|  |             synchronized (LOCK) { | ||||||
|  |                 if (instance == null) { | ||||||
|  |                     instance = new RankedRecipientsCache(); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LocalDateTime lastUpdatedOn; | ||||||
|  |     private RankedRecipientsResponse response; | ||||||
|  |     private boolean updateInitiated = false; | ||||||
|  |     private boolean failed = false; | ||||||
|  | 
 | ||||||
|  |     private RankedRecipientsCache() {} | ||||||
|  | 
 | ||||||
|  |     public List<RankedRecipient> getRankedRecipients() { | ||||||
|  |         if (response != null) { | ||||||
|  |             return response.getRankedRecipients(); | ||||||
|  |         } | ||||||
|  |         return Collections.emptyList(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setRankedRecipientsResponse(final RankedRecipientsResponse response) { | ||||||
|  |         this.response = response; | ||||||
|  |         lastUpdatedOn = LocalDateTime.now(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isExpired() { | ||||||
|  |         if (lastUpdatedOn == null || response == null) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         final long expiresInSecs = response.getExpires(); | ||||||
|  |         return LocalDateTime.now().isAfter(lastUpdatedOn.plus(expiresInSecs, ChronoUnit.SECONDS)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isUpdateInitiated() { | ||||||
|  |         return updateInitiated; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setUpdateInitiated(final boolean updateInitiated) { | ||||||
|  |         this.updateInitiated = updateInitiated; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isFailed() { | ||||||
|  |         return failed; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setFailed(final boolean failed) { | ||||||
|  |         this.failed = failed; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -27,12 +27,14 @@ import java.util.Collections; | |||||||
| import java.util.LinkedList; | import java.util.LinkedList; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Locale; | import java.util.Locale; | ||||||
|  | import java.util.Set; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.customviews.emoji.Emoji; | import awais.instagrabber.customviews.emoji.Emoji; | ||||||
| import awais.instagrabber.models.Resource; | import awais.instagrabber.models.Resource; | ||||||
| import awais.instagrabber.models.UploadVideoOptions; | import awais.instagrabber.models.UploadVideoOptions; | ||||||
|  | import awais.instagrabber.models.enums.DirectItemType; | ||||||
| import awais.instagrabber.repositories.requests.UploadFinishOptions; | import awais.instagrabber.repositories.requests.UploadFinishOptions; | ||||||
| import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; | import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; | ||||||
| import awais.instagrabber.repositories.responses.User; | import awais.instagrabber.repositories.responses.User; | ||||||
| @ -44,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.RankedRecipient; | ||||||
| import awais.instagrabber.utils.BitmapUtils; | import awais.instagrabber.utils.BitmapUtils; | ||||||
| import awais.instagrabber.utils.Constants; | import awais.instagrabber.utils.Constants; | ||||||
| import awais.instagrabber.utils.CookieUtils; | import awais.instagrabber.utils.CookieUtils; | ||||||
| @ -926,4 +929,94 @@ public class DirectThreadViewModel extends AndroidViewModel { | |||||||
|             Log.e(TAG, "onResponse: ", e); |             Log.e(TAG, "onResponse: ", e); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public void forward(final Set<RankedRecipient> recipients, final DirectItem itemToForward) { | ||||||
|  |         if (recipients == null || itemToForward == null) return; | ||||||
|  |         for (final RankedRecipient recipient : recipients) { | ||||||
|  |             forward(recipient, itemToForward); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void forward(final RankedRecipient recipient, final DirectItem itemToForward) { | ||||||
|  |         if (recipient == null || itemToForward == null) return; | ||||||
|  |         if (recipient.getThread() == null && recipient.getUser() != null) { | ||||||
|  |             // create thread and forward | ||||||
|  |             final Call<DirectThread> createThreadRequest = service.createThread(Collections.singletonList(recipient.getUser().getPk()), null); | ||||||
|  |             createThreadRequest.enqueue(new Callback<DirectThread>() { | ||||||
|  |                 @Override | ||||||
|  |                 public void onResponse(@NonNull final Call<DirectThread> call, @NonNull final Response<DirectThread> 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 DirectThread thread = response.body(); | ||||||
|  |                     if (thread == null) { | ||||||
|  |                         Log.e(TAG, "onResponse: thread is null"); | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  |                     forward(thread, itemToForward); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 @Override | ||||||
|  |                 public void onFailure(@NonNull final Call<DirectThread> call, @NonNull final Throwable t) { | ||||||
|  | 
 | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (recipient.getThread() != null) { | ||||||
|  |             // just forward | ||||||
|  |             final DirectThread thread = recipient.getThread(); | ||||||
|  |             forward(thread, itemToForward); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void forward(@NonNull final DirectThread thread, @NonNull final DirectItem itemToForward) { | ||||||
|  |         final DirectItemType itemType = itemToForward.getItemType(); | ||||||
|  |         final Call<DirectThreadBroadcastResponse> request = service.forward(thread.getThreadId(), | ||||||
|  |                                                                             itemType.getName(), | ||||||
|  |                                                                             threadId, | ||||||
|  |                                                                             itemToForward.getItemId()); | ||||||
|  |         request.enqueue(new Callback<DirectThreadBroadcastResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<DirectThreadBroadcastResponse> call, | ||||||
|  |                                    @NonNull final Response<DirectThreadBroadcastResponse> response) { | ||||||
|  |                 if (response.isSuccessful()) return; | ||||||
|  |                 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"); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<DirectThreadBroadcastResponse> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,51 +3,84 @@ package awais.instagrabber.viewmodels; | |||||||
| import android.util.Log; | import android.util.Log; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
| import androidx.lifecycle.LiveData; | import androidx.lifecycle.LiveData; | ||||||
| import androidx.lifecycle.MutableLiveData; | import androidx.lifecycle.MutableLiveData; | ||||||
| import androidx.lifecycle.ViewModel; | import androidx.lifecycle.ViewModel; | ||||||
| 
 | 
 | ||||||
|  | import com.google.common.collect.ImmutableList; | ||||||
|  | 
 | ||||||
| import java.io.IOException; | import java.io.IOException; | ||||||
| import java.util.Arrays; | import java.util.Arrays; | ||||||
| import java.util.Collections; | import java.util.Collections; | ||||||
| import java.util.HashSet; | import java.util.HashSet; | ||||||
|  | import java.util.Iterator; | ||||||
| import java.util.List; | import java.util.List; | ||||||
|  | import java.util.Objects; | ||||||
| import java.util.Set; | import java.util.Set; | ||||||
| import java.util.stream.Collectors; | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
|  | import awais.instagrabber.fragments.UserSearchFragment; | ||||||
| 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.UserSearchResponse; | import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | ||||||
|  | import awais.instagrabber.utils.Constants; | ||||||
|  | import awais.instagrabber.utils.CookieUtils; | ||||||
| import awais.instagrabber.utils.Debouncer; | import awais.instagrabber.utils.Debouncer; | ||||||
|  | import awais.instagrabber.utils.RankedRecipientsCache; | ||||||
|  | import awais.instagrabber.utils.TextUtils; | ||||||
|  | import awais.instagrabber.webservices.DirectMessagesService; | ||||||
| import awais.instagrabber.webservices.UserService; | import awais.instagrabber.webservices.UserService; | ||||||
| import okhttp3.ResponseBody; | import okhttp3.ResponseBody; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| import retrofit2.Callback; | import retrofit2.Callback; | ||||||
| import retrofit2.Response; | import retrofit2.Response; | ||||||
| 
 | 
 | ||||||
|  | import static awais.instagrabber.utils.Utils.settingsHelper; | ||||||
|  | 
 | ||||||
| public class UserSearchViewModel extends ViewModel { | public class UserSearchViewModel extends ViewModel { | ||||||
|     private static final String TAG = UserSearchViewModel.class.getSimpleName(); |     private static final String TAG = UserSearchViewModel.class.getSimpleName(); | ||||||
|     public static final String DEBOUNCE_KEY = "search"; |     public static final String DEBOUNCE_KEY = "search"; | ||||||
| 
 | 
 | ||||||
|     private String prevQuery; |     private String prevQuery; | ||||||
|     private String currentQuery; |     private String currentQuery; | ||||||
|     private Call<UserSearchResponse> searchRequest; |     private Call<?> searchRequest; | ||||||
|  |     private long[] hideUserIds; | ||||||
|  |     private String[] hideThreadIds; | ||||||
|  |     private UserSearchFragment.SearchMode searchMode; | ||||||
|  |     private boolean showGroups; | ||||||
|  |     private boolean waitingForCache; | ||||||
|  |     private boolean showCachedResults; | ||||||
| 
 | 
 | ||||||
|     private final MutableLiveData<Resource<List<User>>> users = new MutableLiveData<>(); |     private final MutableLiveData<Resource<List<RankedRecipient>>> recipients = new MutableLiveData<>(); | ||||||
|     private final MutableLiveData<Boolean> showAction = new MutableLiveData<>(false); |     private final MutableLiveData<Boolean> showAction = new MutableLiveData<>(false); | ||||||
|     private final Debouncer<String> searchDebouncer; |     private final Debouncer<String> searchDebouncer; | ||||||
|     private final Set<User> selectedUsers = new HashSet<>(); |     private final Set<RankedRecipient> selectedRecipients = new HashSet<>(); | ||||||
|     private final UserService userService; |     private final UserService userService; | ||||||
|     private long[] hideUserIds; |     private final DirectMessagesService directMessagesService; | ||||||
|  |     private final RankedRecipientsCache rankedRecipientsCache; | ||||||
| 
 | 
 | ||||||
|     public UserSearchViewModel() { |     public UserSearchViewModel() { | ||||||
|  |         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||||
|  |         final String csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||||
|  |         final long viewerId = CookieUtils.getUserIdFromCookie(cookie); | ||||||
|  |         final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); | ||||||
|  |         if (TextUtils.isEmpty(csrfToken) || viewerId <= 0 || TextUtils.isEmpty(deviceUuid)) { | ||||||
|  |             throw new IllegalArgumentException("User is not logged in!"); | ||||||
|  |         } | ||||||
|         userService = UserService.getInstance(); |         userService = UserService.getInstance(); | ||||||
|  |         directMessagesService = DirectMessagesService.getInstance(csrfToken, viewerId, deviceUuid); | ||||||
|  |         rankedRecipientsCache = RankedRecipientsCache.getInstance(); | ||||||
|  |         if ((rankedRecipientsCache.isFailed() || rankedRecipientsCache.isExpired()) && !rankedRecipientsCache.isUpdateInitiated()) { | ||||||
|  |             updateRankedRecipientCache(); | ||||||
|  |         } | ||||||
|         final Debouncer.Callback<String> searchCallback = new Debouncer.Callback<String>() { |         final Debouncer.Callback<String> searchCallback = new Debouncer.Callback<String>() { | ||||||
|             @Override |             @Override | ||||||
|             public void call(final String key) { |             public void call(final String key) { | ||||||
|                 if (userService == null || (currentQuery != null && currentQuery.equalsIgnoreCase(prevQuery))) return; |                 if (currentQuery != null && currentQuery.equalsIgnoreCase(prevQuery)) return; | ||||||
|                 searchRequest = userService.search(currentQuery); |                 sendSearchRequest(); | ||||||
|                 handleRequest(searchRequest); |  | ||||||
|                 prevQuery = currentQuery; |                 prevQuery = currentQuery; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| @ -59,53 +92,202 @@ public class UserSearchViewModel extends ViewModel { | |||||||
|         searchDebouncer = new Debouncer<>(searchCallback, 1000); |         searchDebouncer = new Debouncer<>(searchCallback, 1000); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public LiveData<Resource<List<User>>> getUsers() { |     private void updateRankedRecipientCache() { | ||||||
|         return users; |         rankedRecipientsCache.setUpdateInitiated(true); | ||||||
|     } |         final Call<RankedRecipientsResponse> request = directMessagesService.rankedRecipients(null, null, null); | ||||||
| 
 |         request.enqueue(new Callback<RankedRecipientsResponse>() { | ||||||
|     public void search(final String query) { |  | ||||||
|         currentQuery = query; |  | ||||||
|         users.postValue(Resource.loading(null)); |  | ||||||
|         searchDebouncer.call(DEBOUNCE_KEY); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void cleanup() { |  | ||||||
|         searchDebouncer.terminate(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     private void handleRequest(final Call<UserSearchResponse> request) { |  | ||||||
|         request.enqueue(new Callback<UserSearchResponse>() { |  | ||||||
|             @Override |             @Override | ||||||
|             public void onResponse(@NonNull final Call<UserSearchResponse> call, @NonNull final Response<UserSearchResponse> response) { |             public void onResponse(@NonNull final Call<RankedRecipientsResponse> call, @NonNull final Response<RankedRecipientsResponse> response) { | ||||||
|                 if (!response.isSuccessful()) { |                 if (!response.isSuccessful()) { | ||||||
|                     handleErrorResponse(response); |                     handleErrorResponse(response, false); | ||||||
|  |                     rankedRecipientsCache.setFailed(true); | ||||||
|  |                     rankedRecipientsCache.setUpdateInitiated(false); | ||||||
|  |                     continueSearchIfRequired(); | ||||||
|                     return; |                     return; | ||||||
|                 } |                 } | ||||||
|                 final UserSearchResponse userSearchResponse = response.body(); |                 if (response.body() == null) { | ||||||
|                 if (userSearchResponse == null) return; |                     Log.e(TAG, "onResponse: response body is null"); | ||||||
|                 handleResponse(userSearchResponse); |                     rankedRecipientsCache.setUpdateInitiated(false); | ||||||
|  |                     rankedRecipientsCache.setFailed(true); | ||||||
|  |                     continueSearchIfRequired(); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 rankedRecipientsCache.setRankedRecipientsResponse(response.body()); | ||||||
|  |                 rankedRecipientsCache.setUpdateInitiated(false); | ||||||
|  |                 continueSearchIfRequired(); | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             @Override |             @Override | ||||||
|             public void onFailure(@NonNull final Call<UserSearchResponse> call, @NonNull final Throwable t) { |             public void onFailure(@NonNull final Call<RankedRecipientsResponse> call, @NonNull final Throwable t) { | ||||||
| 
 |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 rankedRecipientsCache.setUpdateInitiated(false); | ||||||
|  |                 rankedRecipientsCache.setFailed(true); | ||||||
|  |                 continueSearchIfRequired(); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void handleResponse(final UserSearchResponse userSearchResponse) { |     private void continueSearchIfRequired() { | ||||||
|         users.postValue(Resource.success(userSearchResponse |         if (!waitingForCache) { | ||||||
|                                                  .getUsers() |             if (showCachedResults) { | ||||||
|                                                  .stream() |                 recipients.postValue(Resource.success(getCachedRecipients())); | ||||||
|                                                  .filter(directUser -> Arrays.binarySearch(hideUserIds, directUser.getPk()) < 0) |             } | ||||||
|                                                  .collect(Collectors.toList()) |             return; | ||||||
|         )); |         } | ||||||
|  |         waitingForCache = false; | ||||||
|  |         sendSearchRequest(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void handleErrorResponse(final Response<UserSearchResponse> response) { |     public LiveData<Resource<List<RankedRecipient>>> getRecipients() { | ||||||
|  |         return recipients; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void search(@Nullable final String query) { | ||||||
|  |         currentQuery = query; | ||||||
|  |         if (TextUtils.isEmpty(query)) { | ||||||
|  |             cancelSearch(); | ||||||
|  |             if (showCachedResults) { | ||||||
|  |                 recipients.postValue(Resource.success(getCachedRecipients())); | ||||||
|  |             } | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         recipients.postValue(Resource.loading(getCachedRecipients())); | ||||||
|  |         searchDebouncer.call(DEBOUNCE_KEY); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void sendSearchRequest() { | ||||||
|  |         if (!rankedRecipientsCache.isFailed()) { // to avoid infinite loop in case of any network issues | ||||||
|  |             if (rankedRecipientsCache.isUpdateInitiated()) { | ||||||
|  |                 // wait for cache first | ||||||
|  |                 waitingForCache = true; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             if (rankedRecipientsCache.isExpired()) { | ||||||
|  |                 // update cache first | ||||||
|  |                 updateRankedRecipientCache(); | ||||||
|  |                 waitingForCache = true; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         switch (searchMode) { | ||||||
|  |             case RAVEN: | ||||||
|  |             case RESHARE: | ||||||
|  |                 rankedRecipientSearch(); | ||||||
|  |                 break; | ||||||
|  |             case USER_SEARCH: | ||||||
|  |             default: | ||||||
|  |                 defaultUserSearch(); | ||||||
|  |                 break; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void defaultUserSearch() { | ||||||
|  |         searchRequest = userService.search(currentQuery); | ||||||
|  |         //noinspection unchecked | ||||||
|  |         handleRequest((Call<UserSearchResponse>) searchRequest); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void rankedRecipientSearch() { | ||||||
|  |         searchRequest = directMessagesService.rankedRecipients(searchMode.getName(), showGroups, currentQuery); | ||||||
|  |         //noinspection unchecked | ||||||
|  |         handleRankedRecipientRequest((Call<RankedRecipientsResponse>) searchRequest); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     private void handleRankedRecipientRequest(@NonNull final Call<RankedRecipientsResponse> request) { | ||||||
|  |         request.enqueue(new Callback<RankedRecipientsResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<RankedRecipientsResponse> call, @NonNull final Response<RankedRecipientsResponse> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleErrorResponse(response, true); | ||||||
|  |                     searchRequest = null; | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final RankedRecipientsResponse rankedRecipientsResponse = response.body(); | ||||||
|  |                 if (rankedRecipientsResponse == null) { | ||||||
|  |                     recipients.postValue(Resource.error("Response is null!", getCachedRecipients())); | ||||||
|  |                     searchRequest = null; | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final List<RankedRecipient> list = rankedRecipientsResponse.getRankedRecipients(); | ||||||
|  |                 recipients.postValue(Resource.success(mergeResponseWithCache(list))); | ||||||
|  |                 searchRequest = null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<RankedRecipientsResponse> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 recipients.postValue(Resource.error(t.getMessage(), getCachedRecipients())); | ||||||
|  |                 searchRequest = null; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleRequest(@NonNull final Call<UserSearchResponse> request) { | ||||||
|  |         request.enqueue(new Callback<UserSearchResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<UserSearchResponse> call, @NonNull final Response<UserSearchResponse> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleErrorResponse(response, true); | ||||||
|  |                     searchRequest = null; | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final UserSearchResponse userSearchResponse = response.body(); | ||||||
|  |                 if (userSearchResponse == null) { | ||||||
|  |                     recipients.postValue(Resource.error("Response is null!", getCachedRecipients())); | ||||||
|  |                     searchRequest = null; | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final List<RankedRecipient> list = userSearchResponse | ||||||
|  |                         .getUsers() | ||||||
|  |                         .stream() | ||||||
|  |                         .map(RankedRecipient::of) | ||||||
|  |                         .collect(Collectors.toList()); | ||||||
|  |                 recipients.postValue(Resource.success(mergeResponseWithCache(list))); | ||||||
|  |                 searchRequest = null; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<UserSearchResponse> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 recipients.postValue(Resource.error(t.getMessage(), getCachedRecipients())); | ||||||
|  |                 searchRequest = null; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private List<RankedRecipient> mergeResponseWithCache(@NonNull final List<RankedRecipient> list) { | ||||||
|  |         final Iterator<RankedRecipient> iterator = list.stream() | ||||||
|  |                                                        .filter(Objects::nonNull) | ||||||
|  |                                                        .filter(this::filterValidRecipients) | ||||||
|  |                                                        .filter(this::filterOutGroups) | ||||||
|  |                                                        .filter(this::filterIdsToHide) | ||||||
|  |                                                        .iterator(); | ||||||
|  |         return ImmutableList.<RankedRecipient>builder() | ||||||
|  |                 .addAll(getCachedRecipients()) // add cached results first | ||||||
|  |                 .addAll(iterator) | ||||||
|  |                 .build(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     private List<RankedRecipient> getCachedRecipients() { | ||||||
|  |         final List<RankedRecipient> rankedRecipients = rankedRecipientsCache.getRankedRecipients(); | ||||||
|  |         final List<RankedRecipient> list = rankedRecipients != null ? rankedRecipients : Collections.emptyList(); | ||||||
|  |         return list.stream() | ||||||
|  |                    .filter(Objects::nonNull) | ||||||
|  |                    .filter(this::filterValidRecipients) | ||||||
|  |                    .filter(this::filterOutGroups) | ||||||
|  |                    .filter(this::filterQuery) | ||||||
|  |                    .filter(this::filterIdsToHide) | ||||||
|  |                    .collect(Collectors.toList()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleErrorResponse(final Response<?> response, boolean updateResource) { | ||||||
|         final ResponseBody errorBody = response.errorBody(); |         final ResponseBody errorBody = response.errorBody(); | ||||||
|         if (errorBody == null) { |         if (errorBody == null) { | ||||||
|             users.postValue(Resource.error("Request failed!", Collections.emptyList())); |             if (updateResource) { | ||||||
|  |                 recipients.postValue(Resource.error("Request failed!", getCachedRecipients())); | ||||||
|  |             } | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         String errorString; |         String errorString; | ||||||
| @ -116,24 +298,30 @@ public class UserSearchViewModel extends ViewModel { | |||||||
|             Log.e(TAG, "handleErrorResponse: ", e); |             Log.e(TAG, "handleErrorResponse: ", e); | ||||||
|             errorString = e.getMessage(); |             errorString = e.getMessage(); | ||||||
|         } |         } | ||||||
|         users.postValue(Resource.error(errorString, Collections.emptyList())); |         if (updateResource) { | ||||||
|     } |             recipients.postValue(Resource.error(errorString, getCachedRecipients())); | ||||||
| 
 |  | ||||||
|     public void setSelectedUser(final User user, final boolean selected) { |  | ||||||
|         if (selected) { |  | ||||||
|             selectedUsers.add(user); |  | ||||||
|         } else { |  | ||||||
|             selectedUsers.remove(user); |  | ||||||
|         } |         } | ||||||
|         showAction.postValue(!selectedUsers.isEmpty()); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public Set<User> getSelectedUsers() { |     public void cleanup() { | ||||||
|         return selectedUsers; |         searchDebouncer.terminate(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSelectedRecipient(final RankedRecipient recipient, final boolean selected) { | ||||||
|  |         if (selected) { | ||||||
|  |             selectedRecipients.add(recipient); | ||||||
|  |         } else { | ||||||
|  |             selectedRecipients.remove(recipient); | ||||||
|  |         } | ||||||
|  |         showAction.postValue(!selectedRecipients.isEmpty()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Set<RankedRecipient> getSelectedRecipients() { | ||||||
|  |         return selectedRecipients; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void clearResults() { |     public void clearResults() { | ||||||
|         users.postValue(Resource.success(Collections.emptyList())); |         recipients.postValue(Resource.success(Collections.emptyList())); | ||||||
|         prevQuery = ""; |         prevQuery = ""; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -149,7 +337,78 @@ public class UserSearchViewModel extends ViewModel { | |||||||
|         return showAction; |         return showAction; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public void setSearchMode(final UserSearchFragment.SearchMode searchMode) { | ||||||
|  |         this.searchMode = searchMode; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setShowGroups(final boolean showGroups) { | ||||||
|  |         this.showGroups = showGroups; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public void setHideUserIds(final long[] hideUserIds) { |     public void setHideUserIds(final long[] hideUserIds) { | ||||||
|         this.hideUserIds = hideUserIds; |         if (hideUserIds != null) { | ||||||
|  |             final long[] copy = Arrays.copyOf(hideUserIds, hideUserIds.length); | ||||||
|  |             Arrays.sort(copy); | ||||||
|  |             this.hideUserIds = copy; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         this.hideUserIds = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setHideThreadIds(final String[] hideThreadIds) { | ||||||
|  |         if (hideThreadIds != null) { | ||||||
|  |             final String[] copy = Arrays.copyOf(hideThreadIds, hideThreadIds.length); | ||||||
|  |             Arrays.sort(copy); | ||||||
|  |             this.hideThreadIds = copy; | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         this.hideThreadIds = null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean filterOutGroups(@NonNull RankedRecipient recipient) { | ||||||
|  |         // if showGroups is false, remove groups from the list | ||||||
|  |         if (showGroups || recipient.getThread() == null) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         return !recipient.getThread().isGroup(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean filterValidRecipients(@NonNull RankedRecipient recipient) { | ||||||
|  |         // check if both user and thread are null | ||||||
|  |         return recipient.getUser() != null || recipient.getThread() != null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean filterIdsToHide(@NonNull RankedRecipient recipient) { | ||||||
|  |         if (hideThreadIds != null && recipient.getThread() != null) { | ||||||
|  |             return Arrays.binarySearch(hideThreadIds, recipient.getThread().getThreadId()) < 0; | ||||||
|  |         } | ||||||
|  |         if (hideUserIds != null) { | ||||||
|  |             long pk = -1; | ||||||
|  |             if (recipient.getUser() != null) { | ||||||
|  |                 pk = recipient.getUser().getPk(); | ||||||
|  |             } else if (recipient.getThread() != null && !recipient.getThread().isGroup()) { | ||||||
|  |                 final User user = recipient.getThread().getUsers().get(0); | ||||||
|  |                 pk = user.getPk(); | ||||||
|  |             } | ||||||
|  |             return Arrays.binarySearch(hideUserIds, pk) < 0; | ||||||
|  |         } | ||||||
|  |         return true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean filterQuery(@NonNull RankedRecipient recipient) { | ||||||
|  |         if (TextUtils.isEmpty(currentQuery)) { | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|  |         if (recipient.getThread() != null) { | ||||||
|  |             return recipient.getThread().getThreadTitle().toLowerCase().contains(currentQuery.toLowerCase()); | ||||||
|  |         } | ||||||
|  |         return recipient.getUser().getUsername().toLowerCase().contains(currentQuery.toLowerCase()) | ||||||
|  |                 || recipient.getUser().getFullName().toLowerCase().contains(currentQuery.toLowerCase()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void showCachedResults() { | ||||||
|  |         this.showCachedResults = true; | ||||||
|  |         if (rankedRecipientsCache.isUpdateInitiated()) return; | ||||||
|  |         recipients.postValue(Resource.success(getCachedRecipients())); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,6 +1,7 @@ | |||||||
| package awais.instagrabber.webservices; | package awais.instagrabber.webservices; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
| 
 | 
 | ||||||
| import com.google.common.collect.ImmutableMap; | import com.google.common.collect.ImmutableMap; | ||||||
| 
 | 
 | ||||||
| @ -13,6 +14,7 @@ import java.util.List; | |||||||
| import java.util.Map; | import java.util.Map; | ||||||
| import java.util.Objects; | import java.util.Objects; | ||||||
| import java.util.UUID; | import java.util.UUID; | ||||||
|  | import java.util.stream.Collectors; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.repositories.DirectMessagesRepository; | import awais.instagrabber.repositories.DirectMessagesRepository; | ||||||
| import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; | import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; | ||||||
| @ -26,9 +28,11 @@ import awais.instagrabber.repositories.requests.directmessages.VideoBroadcastOpt | |||||||
| import awais.instagrabber.repositories.requests.directmessages.VoiceBroadcastOptions; | import awais.instagrabber.repositories.requests.directmessages.VoiceBroadcastOptions; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | ||||||
|  | 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.RankedRecipientsResponse; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import awais.instagrabber.utils.Utils; | import awais.instagrabber.utils.Utils; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| @ -247,4 +251,56 @@ public class DirectMessagesService extends BaseService { | |||||||
|         ); |         ); | ||||||
|         return repository.deleteItem(threadId, itemId, form); |         return repository.deleteItem(threadId, itemId, form); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public Call<RankedRecipientsResponse> rankedRecipients(@Nullable final String mode, | ||||||
|  |                                                            @Nullable final Boolean showThreads, | ||||||
|  |                                                            @Nullable final String query) { | ||||||
|  |         // String correctedMode = mode; | ||||||
|  |         // if (TextUtils.isEmpty(mode) || (!mode.equals("raven") && !mode.equals("reshare"))) { | ||||||
|  |         //     correctedMode = "raven"; | ||||||
|  |         // } | ||||||
|  |         final ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); | ||||||
|  |         if (mode != null) { | ||||||
|  |             builder.put("mode", mode); | ||||||
|  |         } | ||||||
|  |         if (query != null) { | ||||||
|  |             builder.put("query", query); | ||||||
|  |         } | ||||||
|  |         if (showThreads != null) { | ||||||
|  |             builder.put("showThreads", String.valueOf(showThreads)); | ||||||
|  |         } | ||||||
|  |         return repository.rankedRecipients(builder.build()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<DirectThreadBroadcastResponse> forward(@NonNull final String toThreadId, | ||||||
|  |                                                        @NonNull final String itemType, | ||||||
|  |                                                        @NonNull final String fromThreadId, | ||||||
|  |                                                        @NonNull final String itemId) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "action", "forward_item", | ||||||
|  |                 "thread_id", toThreadId, | ||||||
|  |                 "item_type", itemType, | ||||||
|  |                 "forwarded_from_thread_id", fromThreadId, | ||||||
|  |                 "forwarded_from_thread_item_id", itemId | ||||||
|  |         ); | ||||||
|  |         return repository.forward(form); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<DirectThread> createThread(@NonNull final List<Long> userIds, | ||||||
|  |                                            @Nullable final String threadTitle) { | ||||||
|  |         final List<String> userIdStringList = userIds.stream() | ||||||
|  |                                                      .filter(Objects::nonNull) | ||||||
|  |                                                      .map(String::valueOf) | ||||||
|  |                                                      .collect(Collectors.toList()); | ||||||
|  |         final ImmutableMap.Builder<String, Object> formBuilder = ImmutableMap.<String, Object>builder() | ||||||
|  |                 .put("_csrftoken", csrfToken) | ||||||
|  |                 .put("_uuid", deviceUuid) | ||||||
|  |                 .put("_uid", userId) | ||||||
|  |                 .put("recipient_users", new JSONArray(userIdStringList).toString()); | ||||||
|  |         if (threadTitle != null) { | ||||||
|  |             formBuilder.put("thread_title", threadTitle); | ||||||
|  |         } | ||||||
|  |         final Map<String, String> signedForm = Utils.sign(formBuilder.build()); | ||||||
|  |         return repository.createThread(signedForm); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -29,7 +29,7 @@ | |||||||
|             app:layout_constraintHorizontal_chainStyle="packed" |             app:layout_constraintHorizontal_chainStyle="packed" | ||||||
|             app:layout_constraintStart_toEndOf="@id/ivProfilePic" |             app:layout_constraintStart_toEndOf="@id/ivProfilePic" | ||||||
|             app:layout_constraintTop_toTopOf="parent" |             app:layout_constraintTop_toTopOf="parent" | ||||||
|             tools:visibility="visible" /> |             tools:visibility="gone" /> | ||||||
| 
 | 
 | ||||||
|         <androidx.appcompat.widget.AppCompatTextView |         <androidx.appcompat.widget.AppCompatTextView | ||||||
|             android:id="@+id/reply_info" |             android:id="@+id/reply_info" | ||||||
| @ -42,6 +42,7 @@ | |||||||
|             app:layout_constraintBottom_toTopOf="@id/reply_container" |             app:layout_constraintBottom_toTopOf="@id/reply_container" | ||||||
|             app:layout_constraintStart_toEndOf="@id/quote_line" |             app:layout_constraintStart_toEndOf="@id/quote_line" | ||||||
|             app:layout_constraintTop_toTopOf="parent" |             app:layout_constraintTop_toTopOf="parent" | ||||||
|  |             app:layout_goneMarginStart="0dp" | ||||||
|             tools:text="Replied to you" /> |             tools:text="Replied to you" /> | ||||||
| 
 | 
 | ||||||
|         <FrameLayout |         <FrameLayout | ||||||
| @ -52,6 +53,7 @@ | |||||||
|             android:layout_marginEnd="4dp" |             android:layout_marginEnd="4dp" | ||||||
|             android:layout_marginBottom="4dp" |             android:layout_marginBottom="4dp" | ||||||
|             app:layout_constraintBottom_toTopOf="@id/tvUsername" |             app:layout_constraintBottom_toTopOf="@id/tvUsername" | ||||||
|  | 
 | ||||||
|             app:layout_constraintEnd_toEndOf="parent" |             app:layout_constraintEnd_toEndOf="parent" | ||||||
|             app:layout_constraintHorizontal_bias="1" |             app:layout_constraintHorizontal_bias="1" | ||||||
|             app:layout_constraintHorizontal_chainStyle="packed" |             app:layout_constraintHorizontal_chainStyle="packed" | ||||||
| @ -77,7 +79,7 @@ | |||||||
|                 android:textSize="14sp" |                 android:textSize="14sp" | ||||||
|                 android:visibility="gone" |                 android:visibility="gone" | ||||||
|                 tools:text="Some message" |                 tools:text="Some message" | ||||||
|                 tools:visibility="visible" /> |                 tools:visibility="gone" /> | ||||||
|         </FrameLayout> |         </FrameLayout> | ||||||
| 
 | 
 | ||||||
|         <awais.instagrabber.customviews.CircularImageView |         <awais.instagrabber.customviews.CircularImageView | ||||||
|  | |||||||
| @ -24,6 +24,17 @@ | |||||||
|         app:roundAsCircle="true" |         app:roundAsCircle="true" | ||||||
|         tools:background="@mipmap/ic_launcher" /> |         tools:background="@mipmap/ic_launcher" /> | ||||||
| 
 | 
 | ||||||
|  |     <com.facebook.drawee.view.SimpleDraweeView | ||||||
|  |         android:id="@+id/profile_pic2" | ||||||
|  |         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_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:roundAsCircle="true" | ||||||
|  |         tools:background="@mipmap/ic_launcher" /> | ||||||
|  | 
 | ||||||
|     <androidx.appcompat.widget.AppCompatTextView |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|         android:id="@+id/full_name" |         android:id="@+id/full_name" | ||||||
|         android:layout_width="0dp" |         android:layout_width="0dp" | ||||||
| @ -55,7 +66,8 @@ | |||||||
|         app:layout_constraintEnd_toStartOf="@id/select" |         app:layout_constraintEnd_toStartOf="@id/select" | ||||||
|         app:layout_constraintStart_toEndOf="@id/full_name" |         app:layout_constraintStart_toEndOf="@id/full_name" | ||||||
|         app:layout_constraintTop_toTopOf="@id/full_name" |         app:layout_constraintTop_toTopOf="@id/full_name" | ||||||
|         tools:text="Admin" /> |         tools:text="Admin" | ||||||
|  |         tools:visibility="gone" /> | ||||||
| 
 | 
 | ||||||
|     <androidx.appcompat.widget.AppCompatTextView |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|         android:id="@+id/username" |         android:id="@+id/username" | ||||||
| @ -68,7 +80,8 @@ | |||||||
|         app:layout_constraintEnd_toStartOf="@id/select" |         app:layout_constraintEnd_toStartOf="@id/select" | ||||||
|         app:layout_constraintStart_toStartOf="@id/full_name" |         app:layout_constraintStart_toStartOf="@id/full_name" | ||||||
|         app:layout_constraintTop_toBottomOf="@id/full_name" |         app:layout_constraintTop_toBottomOf="@id/full_name" | ||||||
|         tools:text="username" /> |         tools:text="username" | ||||||
|  |         tools:visibility="gone" /> | ||||||
| 
 | 
 | ||||||
|     <androidx.appcompat.widget.AppCompatImageView |     <androidx.appcompat.widget.AppCompatImageView | ||||||
|         android:id="@+id/select" |         android:id="@+id/select" | ||||||
| @ -83,7 +96,7 @@ | |||||||
|         app:layout_constraintTop_toTopOf="@id/profile_pic" |         app:layout_constraintTop_toTopOf="@id/profile_pic" | ||||||
|         app:srcCompat="@drawable/ic_circle_check" |         app:srcCompat="@drawable/ic_circle_check" | ||||||
|         app:tint="@color/ic_circle_check_tint" |         app:tint="@color/ic_circle_check_tint" | ||||||
|         tools:visibility="gone" /> |         tools:visibility="visible" /> | ||||||
| 
 | 
 | ||||||
|     <androidx.appcompat.widget.AppCompatImageView |     <androidx.appcompat.widget.AppCompatImageView | ||||||
|         android:id="@+id/secondary_image" |         android:id="@+id/secondary_image" | ||||||
|  | |||||||
| @ -86,23 +86,23 @@ | |||||||
|     <action |     <action | ||||||
|         android:id="@+id/action_global_user_search" |         android:id="@+id/action_global_user_search" | ||||||
|         app:destination="@id/user_search_nav_graph"> |         app:destination="@id/user_search_nav_graph"> | ||||||
|         <argument |         <!--<argument--> | ||||||
|             android:name="multiple" |         <!--    android:name="multiple"--> | ||||||
|             app:argType="boolean" /> |         <!--    app:argType="boolean" />--> | ||||||
| 
 | 
 | ||||||
|         <argument |         <!--<argument--> | ||||||
|             android:name="title" |         <!--    android:name="title"--> | ||||||
|             app:argType="string" |         <!--    app:argType="string"--> | ||||||
|             app:nullable="true" /> |         <!--    app:nullable="true" />--> | ||||||
| 
 | 
 | ||||||
|         <argument |         <!--<argument--> | ||||||
|             android:name="action_label" |         <!--    android:name="action_label"--> | ||||||
|             app:argType="string" |         <!--    app:argType="string"--> | ||||||
|             app:nullable="true" /> |         <!--    app:nullable="true" />--> | ||||||
| 
 | 
 | ||||||
|         <argument |         <!--<argument--> | ||||||
|             android:name="hideUserIds" |         <!--    android:name="hideUserIds"--> | ||||||
|             app:argType="long[]" /> |         <!--    app:argType="long[]" />--> | ||||||
|     </action> |     </action> | ||||||
| 
 | 
 | ||||||
|     <fragment |     <fragment | ||||||
|  | |||||||
| @ -1,33 +1,61 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <navigation xmlns:android="http://schemas.android.com/apk/res/android" | <navigation xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:id="@+id/user_search_nav_graph" |     android:id="@+id/user_search_nav_graph" | ||||||
|     app:startDestination="@id/user_search"> |     app:startDestination="@id/user_search"> | ||||||
| 
 | 
 | ||||||
|     <fragment |     <fragment | ||||||
|         android:id="@+id/user_search" |         android:id="@+id/user_search" | ||||||
|         android:name="awais.instagrabber.fragments.UserSearchFragment" |         android:name="awais.instagrabber.fragments.UserSearchFragment" | ||||||
|         android:label="@string/search"> |         android:label="@string/search" | ||||||
|  |         tools:layout="@layout/fragment_user_search"> | ||||||
|         <argument |         <argument | ||||||
|             android:name="multiple" |             android:name="multiple" | ||||||
|  |             android:defaultValue="false" | ||||||
|             app:argType="boolean" /> |             app:argType="boolean" /> | ||||||
| 
 | 
 | ||||||
|         <argument |         <argument | ||||||
|             android:name="title" |             android:name="title" | ||||||
|  |             android:defaultValue="@null" | ||||||
|             app:argType="string" |             app:argType="string" | ||||||
|             app:nullable="true" /> |             app:nullable="true" /> | ||||||
| 
 | 
 | ||||||
|         <argument |         <argument | ||||||
|             android:name="action_label" |             android:name="action_label" | ||||||
|  |             android:defaultValue="@null" | ||||||
|             app:argType="string" |             app:argType="string" | ||||||
|             app:nullable="true" /> |             app:nullable="true" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="show_groups" | ||||||
|  |             android:defaultValue="false" | ||||||
|  |             app:argType="boolean" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="search_mode" | ||||||
|  |             android:defaultValue="USER_SEARCH" | ||||||
|  |             app:argType="awais.instagrabber.fragments.UserSearchFragment$SearchMode" /> | ||||||
|  | 
 | ||||||
|         <argument |         <argument | ||||||
|             android:name="hideUserIds" |             android:name="hideUserIds" | ||||||
|             app:argType="long[]" /> |             android:defaultValue="@null" | ||||||
|  |             app:argType="long[]" | ||||||
|  |             app:nullable="true" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="hideThreadIds" | ||||||
|  |             android:defaultValue="@null" | ||||||
|  |             app:argType="string[]" | ||||||
|  |             app:nullable="true" /> | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     </fragment> |     </fragment> | ||||||
| 
 | 
 | ||||||
|  |     <action | ||||||
|  |         android:id="@+id/action_global_user_search" | ||||||
|  |         app:destination="@id/user_search" /> | ||||||
|  | 
 | ||||||
|     <!--<action--> |     <!--<action--> | ||||||
|     <!--    android:id="@+id/action_global_user_search"--> |     <!--    android:id="@+id/action_global_user_search"--> | ||||||
|     <!--    app:destination="@id/user_search_nav_graph">--> |     <!--    app:destination="@id/user_search_nav_graph">--> | ||||||
|  | |||||||
| @ -2,4 +2,5 @@ | |||||||
| <resources> | <resources> | ||||||
|     <item name="reply" type="id" /> |     <item name="reply" type="id" /> | ||||||
|     <item name="unsend" type="id" /> |     <item name="unsend" type="id" /> | ||||||
|  |     <item name="forward" type="id" /> | ||||||
| </resources> | </resources> | ||||||
| @ -398,4 +398,7 @@ | |||||||
|     <string name="message">Message</string> |     <string name="message">Message</string> | ||||||
|     <string name="reply">Reply</string> |     <string name="reply">Reply</string> | ||||||
|     <string name="tap_to_remove">Tap to remove</string> |     <string name="tap_to_remove">Tap to remove</string> | ||||||
|  |     <string name="forward">Forward</string> | ||||||
|  |     <string name="add">Add</string> | ||||||
|  |     <string name="send">Send</string> | ||||||
| </resources> | </resources> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user