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.recyclerview.widget.DiffUtil; | ||||
| import androidx.recyclerview.widget.ListAdapter; | ||||
| import androidx.recyclerview.widget.RecyclerView; | ||||
| 
 | ||||
| import java.util.HashSet; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener; | ||||
| import awais.instagrabber.adapters.viewholder.directmessages.DirectUserViewHolder; | ||||
| import awais.instagrabber.adapters.viewholder.directmessages.RecipientThreadViewHolder; | ||||
| 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> { | ||||
| 
 | ||||
|     private static final DiffUtil.ItemCallback<User> DIFF_CALLBACK = new DiffUtil.ItemCallback<User>() { | ||||
| public final class UserSearchResultsAdapter extends ListAdapter<RankedRecipient, RecyclerView.ViewHolder> { | ||||
|     private static final int VIEW_TYPE_USER = 0; | ||||
|     private static final int VIEW_TYPE_THREAD = 1; | ||||
|     private static final DiffUtil.ItemCallback<RankedRecipient> DIFF_CALLBACK = new DiffUtil.ItemCallback<RankedRecipient>() { | ||||
|         @Override | ||||
|         public boolean areItemsTheSame(@NonNull final User oldItem, @NonNull final User newItem) { | ||||
|             return oldItem.getPk() == newItem.getPk(); | ||||
|         public boolean areItemsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) { | ||||
|             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 | ||||
|         public boolean areContentsTheSame(@NonNull final User oldItem, @NonNull final User newItem) { | ||||
|             return oldItem.getUsername().equals(newItem.getUsername()) && | ||||
|                     oldItem.getFullName().equals(newItem.getFullName()); | ||||
|         public boolean areContentsTheSame(@NonNull final RankedRecipient oldItem, @NonNull final RankedRecipient newItem) { | ||||
|             final boolean bothUsers = oldItem.getUser() != null && newItem.getUser() != null; | ||||
|             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 Set<Long> selectedUserIds; | ||||
|     private final Set<RankedRecipient> selectedRecipients; | ||||
|     private final OnDirectUserClickListener onUserClickListener; | ||||
|     private final OnRecipientClickListener onRecipientClickListener; | ||||
| 
 | ||||
|     public UserSearchResultsAdapter(final boolean showSelection, | ||||
|                                     final OnDirectUserClickListener onUserClickListener) { | ||||
|                                     final OnRecipientClickListener onRecipientClickListener) { | ||||
|         super(DIFF_CALLBACK); | ||||
|         this.showSelection = showSelection; | ||||
|         selectedUserIds = showSelection ? new HashSet<>() : null; | ||||
|         this.onUserClickListener = onUserClickListener; | ||||
|         selectedRecipients = showSelection ? new HashSet<>() : null; | ||||
|         this.onRecipientClickListener = onRecipientClickListener; | ||||
|         this.onUserClickListener = (position, user, selected) -> { | ||||
|             if (onRecipientClickListener != null) { | ||||
|                 onRecipientClickListener.onClick(position, RankedRecipient.of(user), selected); | ||||
|             } | ||||
|         }; | ||||
|         setHasStableIds(true); | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @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 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 | ||||
|     public void onBindViewHolder(@NonNull final DirectUserViewHolder holder, final int position) { | ||||
|         final User user = getItem(position); | ||||
|         boolean isSelected = selectedUserIds != null && selectedUserIds.contains(user.getPk()); | ||||
|         holder.bind(position, user, false, false, showSelection, isSelected); | ||||
|     public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) { | ||||
|         final RankedRecipient recipient = getItem(position); | ||||
|         final int itemViewType = getItemViewType(position); | ||||
|         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 | ||||
|     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) { | ||||
|         if (selectedUserIds == null) return; | ||||
|     @Override | ||||
|     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; | ||||
|         final List<User> currentList = getCurrentList(); | ||||
|         final List<RankedRecipient> currentList = getCurrentList(); | ||||
|         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; | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (position < 0) return; | ||||
|         if (selected) { | ||||
|             selectedUserIds.add(userId); | ||||
|             selectedRecipients.add(recipient); | ||||
|         } else { | ||||
|             selectedUserIds.remove(userId); | ||||
|             selectedRecipients.remove(recipient); | ||||
|         } | ||||
|         notifyItemChanged(position); | ||||
|     } | ||||
| 
 | ||||
|     public interface OnRecipientClickListener { | ||||
|         void onClick(int position, RankedRecipient recipient, final boolean isSelected); | ||||
|     } | ||||
| } | ||||
| @ -22,4 +22,9 @@ public class DirectItemLikeViewHolder extends DirectItemViewHolder { | ||||
| 
 | ||||
|     @Override | ||||
|     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); | ||||
|         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); | ||||
|         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.typeIcon.setVisibility(View.GONE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected boolean canForward() { | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -135,6 +135,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | ||||
|         } | ||||
|         setupReply(item, messageDirection); | ||||
|         setReactions(item, position); | ||||
|         if (item.getRepliedToMessage() == null && item.showForwardAttribution()) { | ||||
|             setForwardInfo(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); | ||||
|     } | ||||
| 
 | ||||
|     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) { | ||||
|         final boolean isIncoming = messageDirection == MessageDirection.INCOMING; | ||||
|         final ConstraintLayout.LayoutParams quoteLineLayoutParams = (ConstraintLayout.LayoutParams) binding.quoteLine.getLayoutParams(); | ||||
| @ -426,6 +434,10 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     protected boolean canForward() { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     protected List<DirectItemContextMenu.MenuItem> getLongClickOptions() { | ||||
|         return null; | ||||
|     } | ||||
| @ -510,6 +522,9 @@ public abstract class DirectItemViewHolder extends RecyclerView.ViewHolder { | ||||
|         if (longClickOptions != null) { | ||||
|             builder.addAll(longClickOptions); | ||||
|         } | ||||
|         if (canForward()) { | ||||
|             builder.add(new DirectItemContextMenu.MenuItem(R.id.forward, R.string.forward)); | ||||
|         } | ||||
|         if (messageDirection == MessageDirection.OUTGOING) { | ||||
|             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 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.snackbar.Snackbar; | ||||
| 
 | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import awais.instagrabber.activities.MainActivity; | ||||
| import awais.instagrabber.adapters.UserSearchResultsAdapter; | ||||
| import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | ||||
| 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.Utils; | ||||
| import awais.instagrabber.utils.ViewUtils; | ||||
| @ -46,7 +49,6 @@ public class UserSearchFragment extends Fragment { | ||||
|     private String actionLabel; | ||||
|     private String title; | ||||
|     private boolean multiple; | ||||
|     private long[] hideUserIds; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
| @ -81,12 +83,17 @@ public class UserSearchFragment extends Fragment { | ||||
|             actionLabel = fragmentArgs.getActionLabel(); | ||||
|             title = fragmentArgs.getTitle(); | ||||
|             multiple = fragmentArgs.getMultiple(); | ||||
|             viewModel.setHideThreadIds(fragmentArgs.getHideThreadIds()); | ||||
|             viewModel.setHideUserIds(fragmentArgs.getHideUserIds()); | ||||
|             viewModel.setSearchMode(fragmentArgs.getSearchMode()); | ||||
|             viewModel.setShowGroups(fragmentArgs.getShowGroups()); | ||||
|         } | ||||
|         setupTitles(); | ||||
|         setupInput(); | ||||
|         setupResults(); | ||||
|         setupObservers(); | ||||
|         // show cached results | ||||
|         viewModel.showCachedResults(); | ||||
|     } | ||||
| 
 | ||||
|     private void setupTitles() { | ||||
| @ -108,54 +115,64 @@ public class UserSearchFragment extends Fragment { | ||||
|         final Context context = getContext(); | ||||
|         if (context == null) return; | ||||
|         binding.results.setLayoutManager(new LinearLayoutManager(context)); | ||||
|         resultsAdapter = new UserSearchResultsAdapter(multiple, (position, user, selected) -> { | ||||
|         resultsAdapter = new UserSearchResultsAdapter(multiple, (position, recipient, selected) -> { | ||||
|             if (!multiple) { | ||||
|                 final NavController navController = NavHostFragment.findNavController(this); | ||||
|                 final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); | ||||
|                 if (navBackStackEntry == null) return; | ||||
|                 final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); | ||||
|                 savedStateHandle.set("result", user); | ||||
|                 if (!setResult(navController, recipient)) return; | ||||
|                 navController.navigateUp(); | ||||
|                 return; | ||||
|             } | ||||
|             viewModel.setSelectedUser(user, !selected); | ||||
|             resultsAdapter.setSelectedUser(user.getPk(), !selected); | ||||
|             viewModel.setSelectedRecipient(recipient, !selected); | ||||
|             resultsAdapter.setSelectedRecipient(recipient, !selected); | ||||
|             if (!selected) { | ||||
|                 createUserChip(user); | ||||
|                 createChip(recipient); | ||||
|                 return; | ||||
|             } | ||||
|             final View chip = findChip(user.getPk()); | ||||
|             final View chip = findChip(recipient); | ||||
|             if (chip == null) return; | ||||
|             removeChip(chip); | ||||
|             removeChipFromGroup(chip); | ||||
|         }); | ||||
|         binding.results.setAdapter(resultsAdapter); | ||||
|         binding.done.setOnClickListener(v -> { | ||||
|             final NavController navController = NavHostFragment.findNavController(this); | ||||
|             final NavBackStackEntry navBackStackEntry = navController.getPreviousBackStackEntry(); | ||||
|             if (navBackStackEntry == null) return; | ||||
|             final SavedStateHandle savedStateHandle = navBackStackEntry.getSavedStateHandle(); | ||||
|             savedStateHandle.set("result", viewModel.getSelectedUsers()); | ||||
|             if (!setResult(navController, viewModel.getSelectedRecipients())) return; | ||||
|             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() { | ||||
|         binding.search.addTextChangedListener(new TextWatcherAdapter() { | ||||
|             @Override | ||||
|             public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { | ||||
|                 if (TextUtils.isEmpty(s)) { | ||||
|                     viewModel.cancelSearch(); | ||||
|                     viewModel.clearResults(); | ||||
|                     return; | ||||
|                 } | ||||
|                 viewModel.search(s.toString().trim()); | ||||
|                 // if (TextUtils.isEmpty(s)) { | ||||
|                 //     viewModel.cancelSearch(); | ||||
|                 //     viewModel.clearResults(); | ||||
|                 //     return; | ||||
|                 // } | ||||
|                 viewModel.search(s == null ? null : s.toString().trim()); | ||||
|             } | ||||
|         }); | ||||
|         binding.search.setOnKeyListener((v, keyCode, event) -> { | ||||
|             if (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_DEL) { | ||||
|                 final View chip = getLastChip(); | ||||
|                 if (chip == null) return false; | ||||
|                 removeSelectedUser(chip); | ||||
|                 removeChip(chip); | ||||
|             } | ||||
|             return false; | ||||
|         }); | ||||
| @ -175,32 +192,41 @@ public class UserSearchFragment extends Fragment { | ||||
|     } | ||||
| 
 | ||||
|     private void setupObservers() { | ||||
|         viewModel.getUsers().observe(getViewLifecycleOwner(), results -> { | ||||
|         viewModel.getRecipients().observe(getViewLifecycleOwner(), results -> { | ||||
|             if (results == null) return; | ||||
|             switch (results.status) { | ||||
|                 case SUCCESS: | ||||
|                     resultsAdapter.submitList(results.data); | ||||
|                     if (results.data != null) { | ||||
|                         resultsAdapter.submitList(results.data); | ||||
|                     } | ||||
|                     break; | ||||
|                 case ERROR: | ||||
|                     if (results.message != null) { | ||||
|                         Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show(); | ||||
|                     } | ||||
|                     if (results.data != null) { | ||||
|                         resultsAdapter.submitList(results.data); | ||||
|                     } | ||||
|                     break; | ||||
|                 case LOADING: | ||||
|                     //noinspection DuplicateBranchesInSwitch | ||||
|                     if (results.data != null) { | ||||
|                         resultsAdapter.submitList(results.data); | ||||
|                     } | ||||
|                     break; | ||||
|             } | ||||
|         }); | ||||
|         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(); | ||||
|         if (context == null) return; | ||||
|         final Chip chip = new Chip(context); | ||||
|         chip.setTag(user); | ||||
|         chip.setText(user.getFullName()); | ||||
|         chip.setTag(recipient); | ||||
|         chip.setText(getRecipientText(recipient)); | ||||
|         chip.setCloseIconVisible(true); | ||||
|         chip.setOnCloseIconClickListener(v -> removeSelectedUser(chip)); | ||||
|         chip.setOnCloseIconClickListener(v -> removeChip(chip)); | ||||
|         binding.group.post(() -> { | ||||
|             final Pair<Integer, Integer> measure = ViewUtils.measure(chip, binding.group); | ||||
|             TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||
| @ -209,31 +235,44 @@ public class UserSearchFragment extends Fragment { | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private void removeSelectedUser(final View chip) { | ||||
|         final User user = (User) chip.getTag(); | ||||
|         if (user == null) return; | ||||
|         viewModel.setSelectedUser(user, false); | ||||
|         resultsAdapter.setSelectedUser(user.getPk(), false); | ||||
|         removeChip(chip); | ||||
|     private String getRecipientText(final RankedRecipient recipient) { | ||||
|         if (recipient == null) return null; | ||||
|         if (recipient.getUser() != null) { | ||||
|             return recipient.getUser().getFullName(); | ||||
|         } | ||||
|         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(); | ||||
|         if (childCount == 0) { | ||||
|             return null; | ||||
|         } | ||||
|         if (childCount == 0) return null; | ||||
|         for (int i = childCount - 1; i >= 0; i--) { | ||||
|             final View child = binding.group.getChildAt(i); | ||||
|             if (child == null) continue; | ||||
|             final User user = (User) child.getTag(); | ||||
|             if (user != null && user.getPk() == userId) { | ||||
|             final RankedRecipient tag = (RankedRecipient) child.getTag(); | ||||
|             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 null; | ||||
|     } | ||||
| 
 | ||||
|     private void removeChip(final View chip) { | ||||
|     private void removeChipFromGroup(final View chip) { | ||||
|         binding.group.post(() -> { | ||||
|             TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||
|             binding.group.removeView(chip); | ||||
| @ -267,4 +306,20 @@ public class UserSearchFragment extends Fragment { | ||||
|         } | ||||
|         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.NavController; | ||||
| import androidx.navigation.NavDestination; | ||||
| import androidx.navigation.NavDirections; | ||||
| import androidx.navigation.fragment.NavHostFragment; | ||||
| import androidx.recyclerview.widget.LinearLayoutManager; | ||||
| 
 | ||||
| @ -29,18 +28,24 @@ import com.google.android.material.snackbar.Snackbar; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Optional; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| import awais.instagrabber.R; | ||||
| import awais.instagrabber.UserSearchNavGraphDirections; | ||||
| import awais.instagrabber.adapters.DirectUsersAdapter; | ||||
| import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | ||||
| import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; | ||||
| import awais.instagrabber.dialogs.MultiOptionDialogFragment; | ||||
| import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; | ||||
| import awais.instagrabber.fragments.UserSearchFragment; | ||||
| import awais.instagrabber.fragments.UserSearchFragmentDirections; | ||||
| import awais.instagrabber.models.Resource; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||
| import awais.instagrabber.viewmodels.DirectInboxViewModel; | ||||
| import awais.instagrabber.viewmodels.DirectSettingsViewModel; | ||||
| 
 | ||||
| @ -155,6 +160,12 @@ public class DirectMessageSettingsFragment extends Fragment { | ||||
|         setupObservers(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         super.onDestroyView(); | ||||
|         binding = null; | ||||
|     } | ||||
| 
 | ||||
|     private void setupObservers() { | ||||
|         viewModel.getUsers().observe(getViewLifecycleOwner(), users -> { | ||||
|             if (usersAdapter == null) return; | ||||
| @ -171,16 +182,27 @@ public class DirectMessageSettingsFragment extends Fragment { | ||||
|             final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); | ||||
|             resultLiveData.observe(getViewLifecycleOwner(), result -> { | ||||
|                 LiveData<Resource<Object>> detailsChangeResourceLiveData = null; | ||||
|                 if ((result instanceof User)) { | ||||
|                     // Log.d(TAG, "result: " + result); | ||||
|                     detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton((User) result)); | ||||
|                 if ((result instanceof RankedRecipient)) { | ||||
|                     final RankedRecipient recipient = (RankedRecipient) result; | ||||
|                     final User user = getUser(recipient); | ||||
|                     // Log.d(TAG, "result: " + user); | ||||
|                     if (user != null) { | ||||
|                         detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton(recipient.getUser())); | ||||
|                     } | ||||
|                 } else if ((result instanceof Set)) { | ||||
|                     try { | ||||
|                         // Log.d(TAG, "result: " + result); | ||||
|                         //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) { | ||||
|                         Log.e(TAG, "search users result: ", e); | ||||
|                         Snackbar.make(binding.getRoot(), e.getMessage(), Snackbar.LENGTH_LONG).show(); | ||||
|                     } | ||||
|                 } | ||||
|                 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() { | ||||
|         setupSettings(); | ||||
|         setupMembers(); | ||||
| @ -232,13 +265,14 @@ public class DirectMessageSettingsFragment extends Fragment { | ||||
|             } else { | ||||
|                 currentUserIds = new long[0]; | ||||
|             } | ||||
|             final NavDirections directions = DirectMessageSettingsFragmentDirections.actionGlobalUserSearch( | ||||
|                     true, | ||||
|                     "Add users", | ||||
|                     "Add", | ||||
|                     currentUserIds | ||||
|             ); | ||||
|             navController.navigate(directions); | ||||
|             final UserSearchNavGraphDirections.ActionGlobalUserSearch actionGlobalUserSearch = UserSearchFragmentDirections | ||||
|                     .actionGlobalUserSearch() | ||||
|                     .setTitle(getString(R.string.add_members)) | ||||
|                     .setActionLabel(getString(R.string.add)) | ||||
|                     .setHideUserIds(currentUserIds) | ||||
|                     .setSearchMode(UserSearchFragment.SearchMode.RAVEN) | ||||
|                     .setMultiple(true); | ||||
|             navController.navigate(actionGlobalUserSearch); | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -34,6 +34,7 @@ import androidx.fragment.app.Fragment; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import androidx.lifecycle.MediatorLiveData; | ||||
| import androidx.lifecycle.MutableLiveData; | ||||
| import androidx.lifecycle.Observer; | ||||
| import androidx.lifecycle.ViewModelProvider; | ||||
| import androidx.lifecycle.ViewModelStoreOwner; | ||||
| import androidx.navigation.NavBackStackEntry; | ||||
| @ -53,8 +54,10 @@ import java.io.File; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.Optional; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import awais.instagrabber.R; | ||||
| import awais.instagrabber.UserSearchNavGraphDirections; | ||||
| import awais.instagrabber.activities.CameraActivity; | ||||
| import awais.instagrabber.activities.MainActivity; | ||||
| import awais.instagrabber.adapters.DirectItemsAdapter; | ||||
| @ -75,6 +78,8 @@ import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; | ||||
| import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; | ||||
| import awais.instagrabber.dialogs.MediaPickerBottomDialogFragment; | ||||
| import awais.instagrabber.fragments.PostViewV2Fragment; | ||||
| import awais.instagrabber.fragments.UserSearchFragment; | ||||
| import awais.instagrabber.fragments.UserSearchFragmentDirections; | ||||
| import awais.instagrabber.models.Resource; | ||||
| import awais.instagrabber.repositories.requests.StoryViewerOptions; | ||||
| 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.DirectItemStoryShare; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||
| import awais.instagrabber.utils.AppExecutors; | ||||
| import awais.instagrabber.utils.PermissionUtils; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| @ -124,6 +130,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | ||||
|     private boolean isRecording; | ||||
|     private boolean wasKbShowing; | ||||
|     private int keyboardHeight = Utils.convertDpToPx(250); | ||||
|     private DirectItemReactionDialogFragment reactionDialogFragment; | ||||
|     private DirectItem itemToForward; | ||||
|     private MutableLiveData<Object> backStackSavedStateResultLiveData; | ||||
| 
 | ||||
|     private final AppExecutors appExecutors = AppExecutors.getInstance(); | ||||
|     private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { | ||||
| @ -229,13 +238,48 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | ||||
|                 handleSentMessage(viewModel.unsend(item)); | ||||
|                 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 -> { | ||||
|         // 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 | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
| @ -361,6 +405,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | ||||
|             binding.send.setX(initialSendX); | ||||
|         } | ||||
|         binding.send.stopScale(); | ||||
|         setupBackStackResultObserver(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -587,17 +632,14 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact | ||||
|             setupItemsAdapter(userThreadPair.first, userThreadPair.second); | ||||
|         }); | ||||
|         viewModel.getItems().observe(getViewLifecycleOwner(), this::submitItemsToAdapter); | ||||
|     } | ||||
| 
 | ||||
|     private void setupBackStackResultObserver() { | ||||
|         final NavController navController = NavHostFragment.findNavController(this); | ||||
|         final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); | ||||
|         if (backStackEntry != null) { | ||||
|             final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); | ||||
|             resultLiveData.observe(getViewLifecycleOwner(), result -> { | ||||
|                 if (!(result instanceof Uri)) return; | ||||
|                 final Uri uri = (Uri) result; | ||||
|                 viewModel.sendUri(uri); | ||||
|                 // clear result | ||||
|                 resultLiveData.postValue(null); | ||||
|             }); | ||||
|             backStackSavedStateResultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); | ||||
|             backStackSavedStateResultLiveData.observe(getViewLifecycleOwner(), backStackSavedStateObserver); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| package awais.instagrabber.models.enums; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import com.google.gson.annotations.SerializedName; | ||||
| 
 | ||||
| import java.io.Serializable; | ||||
| @ -62,4 +64,46 @@ public enum DirectItemType implements Serializable { | ||||
|     public static DirectItemType valueOf(final int 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.DirectInboxResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.http.FieldMap; | ||||
| import retrofit2.http.FormUrlEncoded; | ||||
| @ -62,4 +64,15 @@ public interface DirectMessagesRepository { | ||||
|     Call<String> deleteItem(@Path("threadId") String threadId, | ||||
|                             @Path("itemId") String itemId, | ||||
|                             @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 Date date; | ||||
|     private boolean isPending; | ||||
|     private boolean showForwardAttribution; | ||||
| 
 | ||||
|     public DirectItem(final String itemId, | ||||
|                       final long userId, | ||||
| @ -65,7 +66,8 @@ public class DirectItem implements Cloneable { | ||||
|                       final DirectItem repliedToMessage, | ||||
|                       final DirectItemVoiceMedia voiceMedia, | ||||
|                       final Location location, | ||||
|                       final int hideInThread) { | ||||
|                       final int hideInThread, | ||||
|                       final boolean showForwardAttribution) { | ||||
|         this.itemId = itemId; | ||||
|         this.userId = userId; | ||||
|         this.timestamp = timestamp; | ||||
| @ -92,6 +94,7 @@ public class DirectItem implements Cloneable { | ||||
|         this.voiceMedia = voiceMedia; | ||||
|         this.location = location; | ||||
|         this.hideInThread = hideInThread; | ||||
|         this.showForwardAttribution = showForwardAttribution; | ||||
|     } | ||||
| 
 | ||||
|     public String getItemId() { | ||||
| @ -222,6 +225,10 @@ public class DirectItem implements Cloneable { | ||||
|         this.reactions = reactions; | ||||
|     } | ||||
| 
 | ||||
|     public boolean showForwardAttribution() { | ||||
|         return showForwardAttribution; | ||||
|     } | ||||
| 
 | ||||
|     @NonNull | ||||
|     @Override | ||||
|     public Object clone() throws CloneNotSupportedException { | ||||
|  | ||||
| @ -2,13 +2,14 @@ package awais.instagrabber.repositories.responses.directmessages; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import java.io.Serializable; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| 
 | ||||
| public class DirectThread { | ||||
| public class DirectThread implements Serializable { | ||||
|     private final String threadId; | ||||
|     private final String threadV2Id; | ||||
|     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, | ||||
|                 0 | ||||
|         ); | ||||
|                 0, | ||||
|                 false); | ||||
|     } | ||||
| 
 | ||||
|     public static DirectItem createImageOrVideo(final long userId, | ||||
| @ -128,8 +128,8 @@ public class DirectItemFactory { | ||||
|                 null, | ||||
|                 null, | ||||
|                 null, | ||||
|                 0 | ||||
|         ); | ||||
|                 0, | ||||
|                 false); | ||||
|     } | ||||
| 
 | ||||
|     public static DirectItem createVoice(final long userId, | ||||
| @ -209,7 +209,7 @@ public class DirectItemFactory { | ||||
|                 null, | ||||
|                 voiceMedia, | ||||
|                 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.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Set; | ||||
| import java.util.UUID; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| import awais.instagrabber.customviews.emoji.Emoji; | ||||
| import awais.instagrabber.models.Resource; | ||||
| import awais.instagrabber.models.UploadVideoOptions; | ||||
| import awais.instagrabber.models.enums.DirectItemType; | ||||
| import awais.instagrabber.repositories.requests.UploadFinishOptions; | ||||
| import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; | ||||
| 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.DirectThreadBroadcastResponsePayload; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; | ||||
| import awais.instagrabber.utils.BitmapUtils; | ||||
| import awais.instagrabber.utils.Constants; | ||||
| import awais.instagrabber.utils.CookieUtils; | ||||
| @ -926,4 +929,94 @@ public class DirectThreadViewModel extends AndroidViewModel { | ||||
|             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 androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.lifecycle.LiveData; | ||||
| import androidx.lifecycle.MutableLiveData; | ||||
| import androidx.lifecycle.ViewModel; | ||||
| 
 | ||||
| import com.google.common.collect.ImmutableList; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.HashSet; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Objects; | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| import awais.instagrabber.fragments.UserSearchFragment; | ||||
| import awais.instagrabber.models.Resource; | ||||
| import awais.instagrabber.repositories.responses.User; | ||||
| 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.RankedRecipientsCache; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.webservices.DirectMessagesService; | ||||
| import awais.instagrabber.webservices.UserService; | ||||
| import okhttp3.ResponseBody; | ||||
| import retrofit2.Call; | ||||
| import retrofit2.Callback; | ||||
| import retrofit2.Response; | ||||
| 
 | ||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | ||||
| 
 | ||||
| public class UserSearchViewModel extends ViewModel { | ||||
|     private static final String TAG = UserSearchViewModel.class.getSimpleName(); | ||||
|     public static final String DEBOUNCE_KEY = "search"; | ||||
| 
 | ||||
|     private String prevQuery; | ||||
|     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 Debouncer<String> searchDebouncer; | ||||
|     private final Set<User> selectedUsers = new HashSet<>(); | ||||
|     private final Set<RankedRecipient> selectedRecipients = new HashSet<>(); | ||||
|     private final UserService userService; | ||||
|     private long[] hideUserIds; | ||||
|     private final DirectMessagesService directMessagesService; | ||||
|     private final RankedRecipientsCache rankedRecipientsCache; | ||||
| 
 | ||||
|     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(); | ||||
|         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>() { | ||||
|             @Override | ||||
|             public void call(final String key) { | ||||
|                 if (userService == null || (currentQuery != null && currentQuery.equalsIgnoreCase(prevQuery))) return; | ||||
|                 searchRequest = userService.search(currentQuery); | ||||
|                 handleRequest(searchRequest); | ||||
|                 if (currentQuery != null && currentQuery.equalsIgnoreCase(prevQuery)) return; | ||||
|                 sendSearchRequest(); | ||||
|                 prevQuery = currentQuery; | ||||
|             } | ||||
| 
 | ||||
| @ -59,53 +92,202 @@ public class UserSearchViewModel extends ViewModel { | ||||
|         searchDebouncer = new Debouncer<>(searchCallback, 1000); | ||||
|     } | ||||
| 
 | ||||
|     public LiveData<Resource<List<User>>> getUsers() { | ||||
|         return users; | ||||
|     } | ||||
| 
 | ||||
|     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>() { | ||||
|     private void updateRankedRecipientCache() { | ||||
|         rankedRecipientsCache.setUpdateInitiated(true); | ||||
|         final Call<RankedRecipientsResponse> request = directMessagesService.rankedRecipients(null, null, null); | ||||
|         request.enqueue(new Callback<RankedRecipientsResponse>() { | ||||
|             @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()) { | ||||
|                     handleErrorResponse(response); | ||||
|                     handleErrorResponse(response, false); | ||||
|                     rankedRecipientsCache.setFailed(true); | ||||
|                     rankedRecipientsCache.setUpdateInitiated(false); | ||||
|                     continueSearchIfRequired(); | ||||
|                     return; | ||||
|                 } | ||||
|                 final UserSearchResponse userSearchResponse = response.body(); | ||||
|                 if (userSearchResponse == null) return; | ||||
|                 handleResponse(userSearchResponse); | ||||
|                 if (response.body() == null) { | ||||
|                     Log.e(TAG, "onResponse: response body is null"); | ||||
|                     rankedRecipientsCache.setUpdateInitiated(false); | ||||
|                     rankedRecipientsCache.setFailed(true); | ||||
|                     continueSearchIfRequired(); | ||||
|                     return; | ||||
|                 } | ||||
|                 rankedRecipientsCache.setRankedRecipientsResponse(response.body()); | ||||
|                 rankedRecipientsCache.setUpdateInitiated(false); | ||||
|                 continueSearchIfRequired(); | ||||
|             } | ||||
| 
 | ||||
|             @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) { | ||||
|         users.postValue(Resource.success(userSearchResponse | ||||
|                                                  .getUsers() | ||||
|                                                  .stream() | ||||
|                                                  .filter(directUser -> Arrays.binarySearch(hideUserIds, directUser.getPk()) < 0) | ||||
|                                                  .collect(Collectors.toList()) | ||||
|         )); | ||||
|     private void continueSearchIfRequired() { | ||||
|         if (!waitingForCache) { | ||||
|             if (showCachedResults) { | ||||
|                 recipients.postValue(Resource.success(getCachedRecipients())); | ||||
|             } | ||||
|             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(); | ||||
|         if (errorBody == null) { | ||||
|             users.postValue(Resource.error("Request failed!", Collections.emptyList())); | ||||
|             if (updateResource) { | ||||
|                 recipients.postValue(Resource.error("Request failed!", getCachedRecipients())); | ||||
|             } | ||||
|             return; | ||||
|         } | ||||
|         String errorString; | ||||
| @ -116,24 +298,30 @@ public class UserSearchViewModel extends ViewModel { | ||||
|             Log.e(TAG, "handleErrorResponse: ", e); | ||||
|             errorString = e.getMessage(); | ||||
|         } | ||||
|         users.postValue(Resource.error(errorString, Collections.emptyList())); | ||||
|     } | ||||
| 
 | ||||
|     public void setSelectedUser(final User user, final boolean selected) { | ||||
|         if (selected) { | ||||
|             selectedUsers.add(user); | ||||
|         } else { | ||||
|             selectedUsers.remove(user); | ||||
|         if (updateResource) { | ||||
|             recipients.postValue(Resource.error(errorString, getCachedRecipients())); | ||||
|         } | ||||
|         showAction.postValue(!selectedUsers.isEmpty()); | ||||
|     } | ||||
| 
 | ||||
|     public Set<User> getSelectedUsers() { | ||||
|         return selectedUsers; | ||||
|     public void cleanup() { | ||||
|         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() { | ||||
|         users.postValue(Resource.success(Collections.emptyList())); | ||||
|         recipients.postValue(Resource.success(Collections.emptyList())); | ||||
|         prevQuery = ""; | ||||
|     } | ||||
| 
 | ||||
| @ -149,7 +337,78 @@ public class UserSearchViewModel extends ViewModel { | ||||
|         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) { | ||||
|         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; | ||||
| 
 | ||||
| import androidx.annotation.NonNull; | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| import com.google.common.collect.ImmutableMap; | ||||
| 
 | ||||
| @ -13,6 +14,7 @@ import java.util.List; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| import java.util.UUID; | ||||
| import java.util.stream.Collectors; | ||||
| 
 | ||||
| import awais.instagrabber.repositories.DirectMessagesRepository; | ||||
| 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.responses.directmessages.DirectBadgeCount; | ||||
| 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.DirectThreadDetailsChangeResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; | ||||
| import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; | ||||
| import awais.instagrabber.utils.TextUtils; | ||||
| import awais.instagrabber.utils.Utils; | ||||
| import retrofit2.Call; | ||||
| @ -247,4 +251,56 @@ public class DirectMessagesService extends BaseService { | ||||
|         ); | ||||
|         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_constraintStart_toEndOf="@id/ivProfilePic" | ||||
|             app:layout_constraintTop_toTopOf="parent" | ||||
|             tools:visibility="visible" /> | ||||
|             tools:visibility="gone" /> | ||||
| 
 | ||||
|         <androidx.appcompat.widget.AppCompatTextView | ||||
|             android:id="@+id/reply_info" | ||||
| @ -42,6 +42,7 @@ | ||||
|             app:layout_constraintBottom_toTopOf="@id/reply_container" | ||||
|             app:layout_constraintStart_toEndOf="@id/quote_line" | ||||
|             app:layout_constraintTop_toTopOf="parent" | ||||
|             app:layout_goneMarginStart="0dp" | ||||
|             tools:text="Replied to you" /> | ||||
| 
 | ||||
|         <FrameLayout | ||||
| @ -52,6 +53,7 @@ | ||||
|             android:layout_marginEnd="4dp" | ||||
|             android:layout_marginBottom="4dp" | ||||
|             app:layout_constraintBottom_toTopOf="@id/tvUsername" | ||||
| 
 | ||||
|             app:layout_constraintEnd_toEndOf="parent" | ||||
|             app:layout_constraintHorizontal_bias="1" | ||||
|             app:layout_constraintHorizontal_chainStyle="packed" | ||||
| @ -77,7 +79,7 @@ | ||||
|                 android:textSize="14sp" | ||||
|                 android:visibility="gone" | ||||
|                 tools:text="Some message" | ||||
|                 tools:visibility="visible" /> | ||||
|                 tools:visibility="gone" /> | ||||
|         </FrameLayout> | ||||
| 
 | ||||
|         <awais.instagrabber.customviews.CircularImageView | ||||
|  | ||||
| @ -24,6 +24,17 @@ | ||||
|         app:roundAsCircle="true" | ||||
|         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 | ||||
|         android:id="@+id/full_name" | ||||
|         android:layout_width="0dp" | ||||
| @ -55,7 +66,8 @@ | ||||
|         app:layout_constraintEnd_toStartOf="@id/select" | ||||
|         app:layout_constraintStart_toEndOf="@id/full_name" | ||||
|         app:layout_constraintTop_toTopOf="@id/full_name" | ||||
|         tools:text="Admin" /> | ||||
|         tools:text="Admin" | ||||
|         tools:visibility="gone" /> | ||||
| 
 | ||||
|     <androidx.appcompat.widget.AppCompatTextView | ||||
|         android:id="@+id/username" | ||||
| @ -68,7 +80,8 @@ | ||||
|         app:layout_constraintEnd_toStartOf="@id/select" | ||||
|         app:layout_constraintStart_toStartOf="@id/full_name" | ||||
|         app:layout_constraintTop_toBottomOf="@id/full_name" | ||||
|         tools:text="username" /> | ||||
|         tools:text="username" | ||||
|         tools:visibility="gone" /> | ||||
| 
 | ||||
|     <androidx.appcompat.widget.AppCompatImageView | ||||
|         android:id="@+id/select" | ||||
| @ -83,7 +96,7 @@ | ||||
|         app:layout_constraintTop_toTopOf="@id/profile_pic" | ||||
|         app:srcCompat="@drawable/ic_circle_check" | ||||
|         app:tint="@color/ic_circle_check_tint" | ||||
|         tools:visibility="gone" /> | ||||
|         tools:visibility="visible" /> | ||||
| 
 | ||||
|     <androidx.appcompat.widget.AppCompatImageView | ||||
|         android:id="@+id/secondary_image" | ||||
|  | ||||
| @ -86,23 +86,23 @@ | ||||
|     <action | ||||
|         android:id="@+id/action_global_user_search" | ||||
|         app:destination="@id/user_search_nav_graph"> | ||||
|         <argument | ||||
|             android:name="multiple" | ||||
|             app:argType="boolean" /> | ||||
|         <!--<argument--> | ||||
|         <!--    android:name="multiple"--> | ||||
|         <!--    app:argType="boolean" />--> | ||||
| 
 | ||||
|         <argument | ||||
|             android:name="title" | ||||
|             app:argType="string" | ||||
|             app:nullable="true" /> | ||||
|         <!--<argument--> | ||||
|         <!--    android:name="title"--> | ||||
|         <!--    app:argType="string"--> | ||||
|         <!--    app:nullable="true" />--> | ||||
| 
 | ||||
|         <argument | ||||
|             android:name="action_label" | ||||
|             app:argType="string" | ||||
|             app:nullable="true" /> | ||||
|         <!--<argument--> | ||||
|         <!--    android:name="action_label"--> | ||||
|         <!--    app:argType="string"--> | ||||
|         <!--    app:nullable="true" />--> | ||||
| 
 | ||||
|         <argument | ||||
|             android:name="hideUserIds" | ||||
|             app:argType="long[]" /> | ||||
|         <!--<argument--> | ||||
|         <!--    android:name="hideUserIds"--> | ||||
|         <!--    app:argType="long[]" />--> | ||||
|     </action> | ||||
| 
 | ||||
|     <fragment | ||||
|  | ||||
| @ -1,33 +1,61 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <navigation xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:id="@+id/user_search_nav_graph" | ||||
|     app:startDestination="@id/user_search"> | ||||
| 
 | ||||
|     <fragment | ||||
|         android:id="@+id/user_search" | ||||
|         android:name="awais.instagrabber.fragments.UserSearchFragment" | ||||
|         android:label="@string/search"> | ||||
|         android:label="@string/search" | ||||
|         tools:layout="@layout/fragment_user_search"> | ||||
|         <argument | ||||
|             android:name="multiple" | ||||
|             android:defaultValue="false" | ||||
|             app:argType="boolean" /> | ||||
| 
 | ||||
|         <argument | ||||
|             android:name="title" | ||||
|             android:defaultValue="@null" | ||||
|             app:argType="string" | ||||
|             app:nullable="true" /> | ||||
| 
 | ||||
|         <argument | ||||
|             android:name="action_label" | ||||
|             android:defaultValue="@null" | ||||
|             app:argType="string" | ||||
|             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 | ||||
|             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> | ||||
| 
 | ||||
|     <action | ||||
|         android:id="@+id/action_global_user_search" | ||||
|         app:destination="@id/user_search" /> | ||||
| 
 | ||||
|     <!--<action--> | ||||
|     <!--    android:id="@+id/action_global_user_search"--> | ||||
|     <!--    app:destination="@id/user_search_nav_graph">--> | ||||
|  | ||||
| @ -2,4 +2,5 @@ | ||||
| <resources> | ||||
|     <item name="reply" type="id" /> | ||||
|     <item name="unsend" type="id" /> | ||||
|     <item name="forward" type="id" /> | ||||
| </resources> | ||||
| @ -398,4 +398,7 @@ | ||||
|     <string name="message">Message</string> | ||||
|     <string name="reply">Reply</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> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user