mirror of
				https://github.com/KokaKiwi/BarInsta
				synced 2025-10-31 11:35:34 +00:00 
			
		
		
		
	Update DM settings page
This commit is contained in:
		
							parent
							
								
									093ccc9f00
								
							
						
					
					
						commit
						e1d8e02630
					
				| @ -73,6 +73,7 @@ import awais.instagrabber.utils.CookieUtils; | |||||||
| import awais.instagrabber.utils.FlavorTown; | import awais.instagrabber.utils.FlavorTown; | ||||||
| import awais.instagrabber.utils.IntentUtils; | import awais.instagrabber.utils.IntentUtils; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
|  | import awais.instagrabber.utils.Utils; | ||||||
| import awais.instagrabber.utils.emoji.EmojiParser; | import awais.instagrabber.utils.emoji.EmojiParser; | ||||||
| 
 | 
 | ||||||
| import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController; | import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController; | ||||||
| @ -477,6 +478,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage | |||||||
|             @SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack(); |             @SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack(); | ||||||
|             setupMenu(backStack.size(), destinationId); |             setupMenu(backStack.size(), destinationId); | ||||||
|             binding.bottomNavView.setVisibility(SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId) ? View.VISIBLE : View.GONE); |             binding.bottomNavView.setVisibility(SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId) ? View.VISIBLE : View.GONE); | ||||||
|  | 
 | ||||||
|  |             // explicitly hide keyboard when we navigate | ||||||
|  |             final View view = getCurrentFocus(); | ||||||
|  |             Utils.hideKeyboard(view); | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,47 +0,0 @@ | |||||||
| package awais.instagrabber.adapters; |  | ||||||
| 
 |  | ||||||
| import android.view.LayoutInflater; |  | ||||||
| import android.view.View; |  | ||||||
| import android.view.ViewGroup; |  | ||||||
| 
 |  | ||||||
| import androidx.annotation.NonNull; |  | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| 
 |  | ||||||
| import java.util.List; |  | ||||||
| 
 |  | ||||||
| import awais.instagrabber.adapters.viewholder.FollowsViewHolder; |  | ||||||
| import awais.instagrabber.databinding.ItemFollowBinding; |  | ||||||
| import awais.instagrabber.models.ProfileModel; |  | ||||||
| 
 |  | ||||||
| public final class DirectMessageMembersAdapter extends RecyclerView.Adapter<FollowsViewHolder> { |  | ||||||
|     private final List<ProfileModel> profileModels; |  | ||||||
|     private final List<Long> admins; |  | ||||||
|     private final View.OnClickListener onClickListener; |  | ||||||
| 
 |  | ||||||
|     public DirectMessageMembersAdapter(final List<ProfileModel> profileModels, |  | ||||||
|                                        final List<Long> admins, |  | ||||||
|                                        final View.OnClickListener onClickListener) { |  | ||||||
|         this.profileModels = profileModels; |  | ||||||
|         this.admins = admins; |  | ||||||
|         this.onClickListener = onClickListener; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @NonNull |  | ||||||
|     @Override |  | ||||||
|     public FollowsViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { |  | ||||||
|         final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); |  | ||||||
|         final ItemFollowBinding binding = ItemFollowBinding.inflate(layoutInflater, parent, false); |  | ||||||
|         return new FollowsViewHolder(binding); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public void onBindViewHolder(@NonNull final FollowsViewHolder holder, final int position) { |  | ||||||
|         final ProfileModel model = profileModels.get(position); |  | ||||||
|         holder.bind(model, admins, onClickListener); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     @Override |  | ||||||
|     public int getItemCount() { |  | ||||||
|         return profileModels.size(); |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @ -0,0 +1,183 @@ | |||||||
|  | package awais.instagrabber.adapters; | ||||||
|  | 
 | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.StringRes; | ||||||
|  | import androidx.recyclerview.widget.DiffUtil; | ||||||
|  | import androidx.recyclerview.widget.ListAdapter; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import com.google.common.collect.ImmutableList; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.R; | ||||||
|  | import awais.instagrabber.adapters.viewholder.DirectUserViewHolder; | ||||||
|  | import awais.instagrabber.databinding.ItemFavSectionHeaderBinding; | ||||||
|  | import awais.instagrabber.databinding.LayoutDmUserItemBinding; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | 
 | ||||||
|  | public final class DirectUsersAdapter extends ListAdapter<DirectUsersAdapter.DirectUserOrHeader, RecyclerView.ViewHolder> { | ||||||
|  | 
 | ||||||
|  |     private static final int VIEW_TYPE_HEADER = 0; | ||||||
|  |     private static final int VIEW_TYPE_USER = 1; | ||||||
|  |     private static final DiffUtil.ItemCallback<DirectUserOrHeader> DIFF_CALLBACK = new DiffUtil.ItemCallback<DirectUserOrHeader>() { | ||||||
|  |         @Override | ||||||
|  |         public boolean areItemsTheSame(@NonNull final DirectUserOrHeader oldItem, @NonNull final DirectUserOrHeader newItem) { | ||||||
|  |             final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader(); | ||||||
|  |             final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader(); | ||||||
|  |             boolean areSameType = bothHeaders || bothItems; | ||||||
|  |             if (!areSameType) return false; | ||||||
|  |             if (bothHeaders) { | ||||||
|  |                 return oldItem.headerTitle == newItem.headerTitle; | ||||||
|  |             } | ||||||
|  |             if (oldItem.user != null && newItem.user != null) { | ||||||
|  |                 return oldItem.user.getPk() == newItem.user.getPk(); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public boolean areContentsTheSame(@NonNull final DirectUserOrHeader oldItem, @NonNull final DirectUserOrHeader newItem) { | ||||||
|  |             final boolean bothHeaders = oldItem.isHeader() && newItem.isHeader(); | ||||||
|  |             final boolean bothItems = !oldItem.isHeader() && !newItem.isHeader(); | ||||||
|  |             boolean areSameType = bothHeaders || bothItems; | ||||||
|  |             if (!areSameType) return false; | ||||||
|  |             if (bothHeaders) { | ||||||
|  |                 return oldItem.headerTitle == newItem.headerTitle; | ||||||
|  |             } | ||||||
|  |             if (oldItem.user != null && newItem.user != null) { | ||||||
|  |                 return oldItem.user.getUsername().equals(newItem.user.getUsername()) && | ||||||
|  |                         oldItem.user.getFullName().equals(newItem.user.getFullName()); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     private final long inviterId; | ||||||
|  |     private final OnDirectUserClickListener onClickListener; | ||||||
|  |     private final OnDirectUserLongClickListener onLongClickListener; | ||||||
|  |     private List<Long> adminUserIds; | ||||||
|  | 
 | ||||||
|  |     public DirectUsersAdapter(final long inviterId, | ||||||
|  |                               final OnDirectUserClickListener onClickListener, | ||||||
|  |                               final OnDirectUserLongClickListener onLongClickListener) { | ||||||
|  |         super(DIFF_CALLBACK); | ||||||
|  |         this.inviterId = inviterId; | ||||||
|  |         this.onClickListener = onClickListener; | ||||||
|  |         this.onLongClickListener = onLongClickListener; | ||||||
|  |         setHasStableIds(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void submitUsers(final List<DirectUser> users, final List<DirectUser> leftUsers) { | ||||||
|  |         if (users == null && leftUsers == null) return; | ||||||
|  |         final List<DirectUserOrHeader> userOrHeaders = combineLists(users, leftUsers); | ||||||
|  |         submitList(userOrHeaders); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private List<DirectUserOrHeader> combineLists(final List<DirectUser> users, final List<DirectUser> leftUsers) { | ||||||
|  |         final ImmutableList.Builder<DirectUserOrHeader> listBuilder = ImmutableList.builder(); | ||||||
|  |         if (users != null && !users.isEmpty()) { | ||||||
|  |             listBuilder.add(new DirectUserOrHeader(R.string.members)); | ||||||
|  |             users.stream() | ||||||
|  |                  .map(DirectUserOrHeader::new) | ||||||
|  |                  .forEach(listBuilder::add); | ||||||
|  |         } | ||||||
|  |         if (leftUsers != null && !leftUsers.isEmpty()) { | ||||||
|  |             listBuilder.add(new DirectUserOrHeader(R.string.dms_left_users)); | ||||||
|  |             leftUsers.stream() | ||||||
|  |                      .map(DirectUserOrHeader::new) | ||||||
|  |                      .forEach(listBuilder::add); | ||||||
|  |         } | ||||||
|  |         return listBuilder.build(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { | ||||||
|  |         final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); | ||||||
|  |         switch (viewType) { | ||||||
|  |             case VIEW_TYPE_USER: | ||||||
|  |                 final LayoutDmUserItemBinding binding = LayoutDmUserItemBinding.inflate(layoutInflater, parent, false); | ||||||
|  |                 return new DirectUserViewHolder(binding, onClickListener, onLongClickListener); | ||||||
|  |             case VIEW_TYPE_HEADER: | ||||||
|  |             default: | ||||||
|  |                 final ItemFavSectionHeaderBinding headerBinding = ItemFavSectionHeaderBinding.inflate(layoutInflater, parent, false); | ||||||
|  |                 return new HeaderViewHolder(headerBinding); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder, final int position) { | ||||||
|  |         if (holder instanceof HeaderViewHolder) { | ||||||
|  |             ((HeaderViewHolder) holder).bind(getItem(position).headerTitle); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (holder instanceof DirectUserViewHolder) { | ||||||
|  |             final DirectUser user = getItem(position).user; | ||||||
|  |             ((DirectUserViewHolder) holder).bind(position, | ||||||
|  |                                                  user, | ||||||
|  |                                                  user != null && adminUserIds != null && adminUserIds.contains(user.getPk()), | ||||||
|  |                                                  user != null && user.getPk() == inviterId, | ||||||
|  |                                                  false, | ||||||
|  |                                                  false); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int getItemViewType(final int position) { | ||||||
|  |         final DirectUserOrHeader item = getItem(position); | ||||||
|  |         return item.isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_USER; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public long getItemId(final int position) { | ||||||
|  |         final DirectUserOrHeader item = getItem(position); | ||||||
|  |         return item.isHeader() ? item.headerTitle : item.user.getPk(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setAdminUserIds(final List<Long> adminUserIds) { | ||||||
|  |         this.adminUserIds = adminUserIds; | ||||||
|  |         notifyDataSetChanged(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class DirectUserOrHeader { | ||||||
|  |         int headerTitle; | ||||||
|  |         DirectUser user; | ||||||
|  | 
 | ||||||
|  |         public DirectUserOrHeader(final int headerTitle) { | ||||||
|  |             this.headerTitle = headerTitle; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public DirectUserOrHeader(final DirectUser user) { | ||||||
|  |             this.user = user; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         boolean isHeader() { | ||||||
|  |             return headerTitle > 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class HeaderViewHolder extends RecyclerView.ViewHolder { | ||||||
|  |         private final ItemFavSectionHeaderBinding binding; | ||||||
|  | 
 | ||||||
|  |         public HeaderViewHolder(@NonNull final ItemFavSectionHeaderBinding binding) { | ||||||
|  |             super(binding.getRoot()); | ||||||
|  |             this.binding = binding; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public void bind(@StringRes final int headerTitle) { | ||||||
|  |             binding.getRoot().setText(headerTitle); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public interface OnDirectUserClickListener { | ||||||
|  |         void onClick(int position, DirectUser user, boolean selected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public interface OnDirectUserLongClickListener { | ||||||
|  |         boolean onLongClick(int position, DirectUser user); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,85 @@ | |||||||
|  | package awais.instagrabber.adapters; | ||||||
|  | 
 | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.recyclerview.widget.DiffUtil; | ||||||
|  | import androidx.recyclerview.widget.ListAdapter; | ||||||
|  | 
 | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener; | ||||||
|  | import awais.instagrabber.adapters.viewholder.DirectUserViewHolder; | ||||||
|  | import awais.instagrabber.databinding.LayoutDmUserItemBinding; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | 
 | ||||||
|  | public final class UserSearchResultsAdapter extends ListAdapter<DirectUser, DirectUserViewHolder> { | ||||||
|  | 
 | ||||||
|  |     private static final DiffUtil.ItemCallback<DirectUser> DIFF_CALLBACK = new DiffUtil.ItemCallback<DirectUser>() { | ||||||
|  |         @Override | ||||||
|  |         public boolean areItemsTheSame(@NonNull final DirectUser oldItem, @NonNull final DirectUser newItem) { | ||||||
|  |             return oldItem.getPk() == newItem.getPk(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         @Override | ||||||
|  |         public boolean areContentsTheSame(@NonNull final DirectUser oldItem, @NonNull final DirectUser newItem) { | ||||||
|  |             return oldItem.getUsername().equals(newItem.getUsername()) && | ||||||
|  |                     oldItem.getFullName().equals(newItem.getFullName()); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     private final boolean showSelection; | ||||||
|  |     private final Set<Long> selectedUserIds; | ||||||
|  |     private final OnDirectUserClickListener onUserClickListener; | ||||||
|  | 
 | ||||||
|  |     public UserSearchResultsAdapter(final boolean showSelection, | ||||||
|  |                                     final OnDirectUserClickListener onUserClickListener) { | ||||||
|  |         super(DIFF_CALLBACK); | ||||||
|  |         this.showSelection = showSelection; | ||||||
|  |         selectedUserIds = showSelection ? new HashSet<>() : null; | ||||||
|  |         this.onUserClickListener = onUserClickListener; | ||||||
|  |         setHasStableIds(true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public DirectUserViewHolder 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); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onBindViewHolder(@NonNull final DirectUserViewHolder holder, final int position) { | ||||||
|  |         final DirectUser user = getItem(position); | ||||||
|  |         boolean isSelected = selectedUserIds != null && selectedUserIds.contains(user.getPk()); | ||||||
|  |         holder.bind(position, user, false, false, showSelection, isSelected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public long getItemId(final int position) { | ||||||
|  |         return getItem(position).getPk(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSelectedUser(final long userId, final boolean selected) { | ||||||
|  |         if (selectedUserIds == null) return; | ||||||
|  |         int position = -1; | ||||||
|  |         final List<DirectUser> currentList = getCurrentList(); | ||||||
|  |         for (int i = 0; i < currentList.size(); i++) { | ||||||
|  |             if (currentList.get(i).getPk() == userId) { | ||||||
|  |                 position = i; | ||||||
|  |                 break; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (position < 0) return; | ||||||
|  |         if (selected) { | ||||||
|  |             selectedUserIds.add(userId); | ||||||
|  |         } else { | ||||||
|  |             selectedUserIds.remove(userId); | ||||||
|  |         } | ||||||
|  |         notifyItemChanged(position); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,102 @@ | |||||||
|  | package awais.instagrabber.adapters.viewholder; | ||||||
|  | 
 | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | import android.text.SpannableStringBuilder; | ||||||
|  | import android.text.Spanned; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.view.View; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.appcompat.content.res.AppCompatResources; | ||||||
|  | import androidx.recyclerview.widget.RecyclerView; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.R; | ||||||
|  | import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserClickListener; | ||||||
|  | import awais.instagrabber.adapters.DirectUsersAdapter.OnDirectUserLongClickListener; | ||||||
|  | import awais.instagrabber.customviews.VerticalImageSpan; | ||||||
|  | import awais.instagrabber.databinding.LayoutDmUserItemBinding; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | import awais.instagrabber.utils.Utils; | ||||||
|  | 
 | ||||||
|  | public class DirectUserViewHolder extends RecyclerView.ViewHolder { | ||||||
|  |     private static final String TAG = DirectUserViewHolder.class.getSimpleName(); | ||||||
|  | 
 | ||||||
|  |     private final LayoutDmUserItemBinding binding; | ||||||
|  |     private final OnDirectUserClickListener onClickListener; | ||||||
|  |     private final OnDirectUserLongClickListener onLongClickListener; | ||||||
|  |     private final int drawableSize; | ||||||
|  | 
 | ||||||
|  |     private VerticalImageSpan verifiedSpan; | ||||||
|  | 
 | ||||||
|  |     public DirectUserViewHolder(@NonNull final LayoutDmUserItemBinding binding, | ||||||
|  |                                 final OnDirectUserClickListener onClickListener, | ||||||
|  |                                 final OnDirectUserLongClickListener onLongClickListener) { | ||||||
|  |         super(binding.getRoot()); | ||||||
|  |         this.binding = binding; | ||||||
|  |         this.onClickListener = onClickListener; | ||||||
|  |         this.onLongClickListener = onLongClickListener; | ||||||
|  |         drawableSize = Utils.convertDpToPx(24); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void bind(final int position, | ||||||
|  |                      final DirectUser user, | ||||||
|  |                      final boolean isAdmin, | ||||||
|  |                      final boolean isInviter, | ||||||
|  |                      final boolean showSelection, | ||||||
|  |                      final boolean isSelected) { | ||||||
|  |         if (user == null) return; | ||||||
|  |         binding.getRoot().setOnClickListener(v -> { | ||||||
|  |             if (onClickListener == null) return; | ||||||
|  |             onClickListener.onClick(position, user, isSelected); | ||||||
|  |         }); | ||||||
|  |         binding.getRoot().setOnLongClickListener(v -> { | ||||||
|  |             if (onLongClickListener == null) return false; | ||||||
|  |             return onLongClickListener.onLongClick(position, user); | ||||||
|  |         }); | ||||||
|  |         setFullName(user); | ||||||
|  |         binding.username.setText(user.getUsername()); | ||||||
|  |         binding.profilePic.setImageURI(user.getProfilePicUrl()); | ||||||
|  |         setInfo(isAdmin, isInviter); | ||||||
|  |         setSelection(showSelection, isSelected); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setFullName(final DirectUser user) { | ||||||
|  |         final SpannableStringBuilder sb = new SpannableStringBuilder(user.getFullName()); | ||||||
|  |         if (user.isVerified()) { | ||||||
|  |             if (verifiedSpan == null) { | ||||||
|  |                 final Drawable verifiedDrawable = AppCompatResources.getDrawable(itemView.getContext(), R.drawable.verified); | ||||||
|  |                 if (verifiedDrawable != null) { | ||||||
|  |                     final Drawable drawable = verifiedDrawable.mutate(); | ||||||
|  |                     drawable.setBounds(0, 0, drawableSize, drawableSize); | ||||||
|  |                     verifiedSpan = new VerticalImageSpan(drawable); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             try { | ||||||
|  |                 if (verifiedSpan != null) { | ||||||
|  |                     sb.append("  "); | ||||||
|  |                     sb.setSpan(verifiedSpan, sb.length() - 1, sb.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||||
|  |                 } | ||||||
|  |             } catch (Exception e) { | ||||||
|  |                 Log.e(TAG, "bind: ", e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         binding.fullName.setText(sb); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setInfo(final boolean isAdmin, final boolean isInviter) { | ||||||
|  |         if (!isAdmin && !isInviter) { | ||||||
|  |             binding.info.setVisibility(View.GONE); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (isAdmin) { | ||||||
|  |             binding.info.setText(R.string.admin); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         binding.info.setText(R.string.inviter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setSelection(final boolean showSelection, final boolean isSelected) { | ||||||
|  |         binding.select.setVisibility(showSelection ? View.VISIBLE : View.GONE); | ||||||
|  |         binding.getRoot().setSelected(isSelected); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,76 @@ | |||||||
|  | package awais.instagrabber.customviews; | ||||||
|  | 
 | ||||||
|  | import android.graphics.Canvas; | ||||||
|  | import android.graphics.Paint; | ||||||
|  | import android.graphics.Rect; | ||||||
|  | import android.graphics.drawable.Drawable; | ||||||
|  | import android.text.style.ImageSpan; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | public class VerticalImageSpan extends ImageSpan { | ||||||
|  | 
 | ||||||
|  |     public VerticalImageSpan(final Drawable drawable) { | ||||||
|  |         super(drawable); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * update the text line height | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public int getSize(@NonNull Paint paint, | ||||||
|  |                        CharSequence text, | ||||||
|  |                        int start, | ||||||
|  |                        int end, | ||||||
|  |                        Paint.FontMetricsInt fontMetricsInt) { | ||||||
|  |         Drawable drawable = getDrawable(); | ||||||
|  |         Rect rect = drawable.getBounds(); | ||||||
|  |         if (fontMetricsInt != null) { | ||||||
|  |             Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt(); | ||||||
|  |             int fontHeight = fmPaint.descent - fmPaint.ascent; | ||||||
|  |             int drHeight = rect.bottom - rect.top; | ||||||
|  |             int centerY = fmPaint.ascent + fontHeight / 2; | ||||||
|  | 
 | ||||||
|  |             fontMetricsInt.ascent = centerY - drHeight / 2; | ||||||
|  |             fontMetricsInt.top = fontMetricsInt.ascent; | ||||||
|  |             fontMetricsInt.bottom = centerY + drHeight / 2; | ||||||
|  |             fontMetricsInt.descent = fontMetricsInt.bottom; | ||||||
|  |         } | ||||||
|  |         return rect.right; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * see detail message in android.text.TextLine | ||||||
|  |      * | ||||||
|  |      * @param canvas the canvas, can be null if not rendering | ||||||
|  |      * @param text   the text to be draw | ||||||
|  |      * @param start  the text start position | ||||||
|  |      * @param end    the text end position | ||||||
|  |      * @param x      the edge of the replacement closest to the leading margin | ||||||
|  |      * @param top    the top of the line | ||||||
|  |      * @param y      the baseline | ||||||
|  |      * @param bottom the bottom of the line | ||||||
|  |      * @param paint  the work paint | ||||||
|  |      */ | ||||||
|  |     @Override | ||||||
|  |     public void draw(Canvas canvas, | ||||||
|  |                      CharSequence text, | ||||||
|  |                      int start, | ||||||
|  |                      int end, | ||||||
|  |                      float x, | ||||||
|  |                      int top, | ||||||
|  |                      int y, | ||||||
|  |                      int bottom, | ||||||
|  |                      Paint paint) { | ||||||
|  |         Drawable drawable = getDrawable(); | ||||||
|  |         canvas.save(); | ||||||
|  |         Paint.FontMetricsInt fmPaint = paint.getFontMetricsInt(); | ||||||
|  |         int fontHeight = fmPaint.descent - fmPaint.ascent; | ||||||
|  |         int centerY = y + fmPaint.descent - fontHeight / 2; | ||||||
|  |         int transY = centerY - (drawable.getBounds().bottom - drawable.getBounds().top) / 2; | ||||||
|  |         canvas.translate(x, transY); | ||||||
|  |         drawable.draw(canvas); | ||||||
|  |         canvas.restore(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -0,0 +1,236 @@ | |||||||
|  | package awais.instagrabber.dialogs; | ||||||
|  | 
 | ||||||
|  | import android.app.Dialog; | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.util.Log; | ||||||
|  | import android.util.SparseBooleanArray; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.StringRes; | ||||||
|  | import androidx.appcompat.app.AlertDialog; | ||||||
|  | import androidx.fragment.app.DialogFragment; | ||||||
|  | 
 | ||||||
|  | import com.google.common.primitives.Booleans; | ||||||
|  | 
 | ||||||
|  | import java.io.Serializable; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | public class MultiOptionDialogFragment<T extends Serializable> extends DialogFragment { | ||||||
|  |     private static final String TAG = MultiOptionDialogFragment.class.getSimpleName(); | ||||||
|  | 
 | ||||||
|  |     public enum Type { | ||||||
|  |         MULTIPLE, | ||||||
|  |         SINGLE_CHECKED, | ||||||
|  |         SINGLE | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private Context context; | ||||||
|  |     private Type type; | ||||||
|  |     private MultiOptionDialogCallback<T> callback; | ||||||
|  |     private MultiOptionDialogSingleCallback<T> singleCallback; | ||||||
|  |     private List<Option<?>> options; | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(@StringRes final int title, | ||||||
|  |                                                                                     @NonNull final ArrayList<Option<E>> options) { | ||||||
|  |         return newInstance(title, 0, 0, options, Type.SINGLE); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public static <E extends Serializable> MultiOptionDialogFragment<E> newInstance(@StringRes final int title, | ||||||
|  |                                                                                     @StringRes final int positiveButtonText, | ||||||
|  |                                                                                     @StringRes final int negativeButtonText, | ||||||
|  |                                                                                     @NonNull final ArrayList<Option<E>> options, | ||||||
|  |                                                                                     @NonNull final Type type) { | ||||||
|  |         Bundle args = new Bundle(); | ||||||
|  |         args.putInt("title", title); | ||||||
|  |         args.putInt("positiveButtonText", positiveButtonText); | ||||||
|  |         args.putInt("negativeButtonText", negativeButtonText); | ||||||
|  |         args.putSerializable("options", options); | ||||||
|  |         args.putSerializable("type", type); | ||||||
|  |         MultiOptionDialogFragment<E> fragment = new MultiOptionDialogFragment<>(); | ||||||
|  |         fragment.setArguments(args); | ||||||
|  |         return fragment; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onAttach(@NonNull final Context context) { | ||||||
|  |         super.onAttach(context); | ||||||
|  |         this.context = context; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     @Override | ||||||
|  |     public Dialog onCreateDialog(Bundle savedInstanceState) { | ||||||
|  |         final Bundle arguments = getArguments(); | ||||||
|  |         int title = 0; | ||||||
|  |         if (arguments != null) { | ||||||
|  |             title = arguments.getInt("title"); | ||||||
|  |             type = (Type) arguments.getSerializable("type"); | ||||||
|  |         } | ||||||
|  |         final AlertDialog.Builder builder = new AlertDialog.Builder(context); | ||||||
|  |         if (title > 0) { | ||||||
|  |             builder.setTitle(title); | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  |             //noinspection unchecked | ||||||
|  |             options = arguments != null ? (List<Option<?>>) arguments.getSerializable("options") | ||||||
|  |                                         : Collections.emptyList(); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.e(TAG, "onCreateDialog: ", e); | ||||||
|  |             options = Collections.emptyList(); | ||||||
|  |         } | ||||||
|  |         final int negativeButtonText = arguments != null ? arguments.getInt("negativeButtonText", -1) : -1; | ||||||
|  |         if (negativeButtonText > 0) { | ||||||
|  |             builder.setNegativeButton(negativeButtonText, (dialog, which) -> { | ||||||
|  |                 if (callback != null) { | ||||||
|  |                     callback.onCancel(); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if (singleCallback != null) { | ||||||
|  |                     singleCallback.onCancel(); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |         if (type == Type.MULTIPLE || type == Type.SINGLE_CHECKED) { | ||||||
|  |             final int positiveButtonText = arguments != null ? arguments.getInt("positiveButtonText", -1) : -1; | ||||||
|  |             if (positiveButtonText > 0) { | ||||||
|  |                 builder.setPositiveButton(positiveButtonText, (dialog, which) -> { | ||||||
|  |                     if (callback == null || options == null || options.isEmpty()) return; | ||||||
|  |                     try { | ||||||
|  |                         final List<T> selected = new ArrayList<>(); | ||||||
|  |                         final SparseBooleanArray checkedItemPositions = ((AlertDialog) dialog).getListView().getCheckedItemPositions(); | ||||||
|  |                         for (int i = 0; i < checkedItemPositions.size(); i++) { | ||||||
|  |                             final int position = checkedItemPositions.keyAt(i); | ||||||
|  |                             final boolean checked = checkedItemPositions.get(position); | ||||||
|  |                             if (!checked) continue; | ||||||
|  |                             //noinspection unchecked | ||||||
|  |                             final Option<T> option = (Option<T>) options.get(position); | ||||||
|  |                             selected.add(option.value); | ||||||
|  |                         } | ||||||
|  |                         callback.onMultipleSelect(selected); | ||||||
|  |                     } catch (Exception e) { | ||||||
|  |                         Log.e(TAG, "onCreateDialog: ", e); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (type == Type.MULTIPLE) { | ||||||
|  |             if (options != null && !options.isEmpty()) { | ||||||
|  |                 final String[] items = options.stream() | ||||||
|  |                                               .map(option -> option.label) | ||||||
|  |                                               .toArray(String[]::new); | ||||||
|  |                 final boolean[] checkedItems = Booleans.toArray(options.stream() | ||||||
|  |                                                                        .map(option -> option.checked) | ||||||
|  |                                                                        .collect(Collectors.toList())); | ||||||
|  |                 builder.setMultiChoiceItems(items, checkedItems, (dialog, which, isChecked) -> { | ||||||
|  |                     if (callback == null) return; | ||||||
|  |                     try { | ||||||
|  |                         final Option<?> option = options.get(which); | ||||||
|  |                         //noinspection unchecked | ||||||
|  |                         callback.onCheckChange((T) option.value, isChecked); | ||||||
|  |                     } catch (Exception e) { | ||||||
|  |                         Log.e(TAG, "onCreateDialog: ", e); | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             if (options != null && !options.isEmpty()) { | ||||||
|  |                 final String[] items = options.stream() | ||||||
|  |                                               .map(option -> option.label) | ||||||
|  |                                               .toArray(String[]::new); | ||||||
|  |                 if (type == Type.SINGLE_CHECKED) { | ||||||
|  |                     int index = -1; | ||||||
|  |                     for (int i = 0; i < options.size(); i++) { | ||||||
|  |                         if (options.get(i).checked) { | ||||||
|  |                             index = i; | ||||||
|  |                             break; | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     builder.setSingleChoiceItems(items, index, (dialog, which) -> { | ||||||
|  |                         if (callback == null) return; | ||||||
|  |                         try { | ||||||
|  |                             final Option<?> option = options.get(which); | ||||||
|  |                             //noinspection unchecked | ||||||
|  |                             callback.onCheckChange((T) option.value, true); | ||||||
|  |                         } catch (Exception e) { | ||||||
|  |                             Log.e(TAG, "onCreateDialog: ", e); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } else if (type == Type.SINGLE) { | ||||||
|  |                     builder.setItems(items, (dialog, which) -> { | ||||||
|  |                         if (singleCallback == null) return; | ||||||
|  |                         try { | ||||||
|  |                             final Option<?> option = options.get(which); | ||||||
|  |                             //noinspection unchecked | ||||||
|  |                             singleCallback.onSelect((T) option.value); | ||||||
|  |                         } catch (Exception e) { | ||||||
|  |                             Log.e(TAG, "onCreateDialog: ", e); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return builder.create(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setCallback(final MultiOptionDialogCallback<T> callback) { | ||||||
|  |         if (callback == null) return; | ||||||
|  |         this.callback = callback; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSingleCallback(final MultiOptionDialogSingleCallback<T> callback) { | ||||||
|  |         if (callback == null) return; | ||||||
|  |         this.singleCallback = callback; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public interface MultiOptionDialogCallback<T> { | ||||||
|  |         void onSelect(T result); | ||||||
|  | 
 | ||||||
|  |         void onMultipleSelect(List<T> result); | ||||||
|  | 
 | ||||||
|  |         void onCheckChange(T item, boolean isChecked); | ||||||
|  | 
 | ||||||
|  |         void onCancel(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public interface MultiOptionDialogSingleCallback<T> { | ||||||
|  |         void onSelect(T result); | ||||||
|  | 
 | ||||||
|  |         void onCancel(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static class Option<T extends Serializable> { | ||||||
|  |         private final String label; | ||||||
|  |         private final T value; | ||||||
|  |         private final boolean checked; | ||||||
|  | 
 | ||||||
|  |         public Option(final String label, final T value) { | ||||||
|  |             this.label = label; | ||||||
|  |             this.value = value; | ||||||
|  |             this.checked = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public Option(final String label, final T value, final boolean checked) { | ||||||
|  |             this.label = label; | ||||||
|  |             this.value = value; | ||||||
|  |             this.checked = checked; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public String getLabel() { | ||||||
|  |             return label; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public T getValue() { | ||||||
|  |             return value; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public boolean isChecked() { | ||||||
|  |             return checked; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -9,7 +9,6 @@ import android.graphics.drawable.ColorDrawable; | |||||||
| import android.os.AsyncTask; | import android.os.AsyncTask; | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.os.Environment; | import android.os.Environment; | ||||||
| import android.util.Log; |  | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| @ -20,7 +19,6 @@ import androidx.annotation.NonNull; | |||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.core.content.ContextCompat; | import androidx.core.content.ContextCompat; | ||||||
| import androidx.fragment.app.DialogFragment; | import androidx.fragment.app.DialogFragment; | ||||||
| import androidx.fragment.app.FragmentActivity; |  | ||||||
| 
 | 
 | ||||||
| import com.facebook.drawee.backends.pipeline.Fresco; | import com.facebook.drawee.backends.pipeline.Fresco; | ||||||
| import com.facebook.drawee.controller.BaseControllerListener; | import com.facebook.drawee.controller.BaseControllerListener; | ||||||
| @ -32,17 +30,14 @@ import java.io.File; | |||||||
| import awais.instagrabber.R; | import awais.instagrabber.R; | ||||||
| import awais.instagrabber.asyncs.ProfilePictureFetcher; | import awais.instagrabber.asyncs.ProfilePictureFetcher; | ||||||
| import awais.instagrabber.databinding.DialogProfilepicBinding; | import awais.instagrabber.databinding.DialogProfilepicBinding; | ||||||
| import awais.instagrabber.db.entities.Account; |  | ||||||
| import awais.instagrabber.db.repositories.RepositoryCallback; |  | ||||||
| import awais.instagrabber.interfaces.FetchListener; | import awais.instagrabber.interfaces.FetchListener; | ||||||
| import awais.instagrabber.models.StoryModel; |  | ||||||
| import awais.instagrabber.repositories.responses.UserInfo; | import awais.instagrabber.repositories.responses.UserInfo; | ||||||
| import awais.instagrabber.utils.Constants; | import awais.instagrabber.utils.Constants; | ||||||
| import awais.instagrabber.utils.CookieUtils; | import awais.instagrabber.utils.CookieUtils; | ||||||
| import awais.instagrabber.utils.DownloadUtils; | import awais.instagrabber.utils.DownloadUtils; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import awais.instagrabber.webservices.ProfileService; |  | ||||||
| import awais.instagrabber.webservices.ServiceCallback; | import awais.instagrabber.webservices.ServiceCallback; | ||||||
|  | import awais.instagrabber.webservices.UserService; | ||||||
| 
 | 
 | ||||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | import static awais.instagrabber.utils.Utils.settingsHelper; | ||||||
| 
 | 
 | ||||||
| @ -128,8 +123,8 @@ public class ProfilePicDialogFragment extends DialogFragment { | |||||||
| 
 | 
 | ||||||
|     private void fetchAvatar() { |     private void fetchAvatar() { | ||||||
|         if (isLoggedIn) { |         if (isLoggedIn) { | ||||||
|             final ProfileService profileService = ProfileService.getInstance(); |             final UserService userService = UserService.getInstance(); | ||||||
|             profileService.getUserInfo(id, new ServiceCallback<UserInfo>() { |             userService.getUserInfo(id, new ServiceCallback<UserInfo>() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public void onSuccess(final UserInfo result) { |                 public void onSuccess(final UserInfo result) { | ||||||
|                     if (result != null) { |                     if (result != null) { | ||||||
| @ -144,8 +139,9 @@ public class ProfilePicDialogFragment extends DialogFragment { | |||||||
|                     getDialog().dismiss(); |                     getDialog().dismiss(); | ||||||
|                 } |                 } | ||||||
|             }); |             }); | ||||||
|  |             return; | ||||||
|         } |         } | ||||||
|         else new ProfilePictureFetcher(name, id, fetchListener, fallbackUrl, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); |         new ProfilePictureFetcher(name, id, fetchListener, fallbackUrl, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void setupPhoto() { |     private void setupPhoto() { | ||||||
|  | |||||||
| @ -0,0 +1,270 @@ | |||||||
|  | package awais.instagrabber.fragments; | ||||||
|  | 
 | ||||||
|  | import android.content.Context; | ||||||
|  | import android.os.Bundle; | ||||||
|  | import android.view.KeyEvent; | ||||||
|  | import android.view.LayoutInflater; | ||||||
|  | import android.view.View; | ||||||
|  | import android.view.ViewGroup; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.Nullable; | ||||||
|  | import androidx.appcompat.app.ActionBar; | ||||||
|  | import androidx.core.util.Pair; | ||||||
|  | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.lifecycle.SavedStateHandle; | ||||||
|  | import androidx.lifecycle.ViewModelProvider; | ||||||
|  | import androidx.navigation.NavBackStackEntry; | ||||||
|  | import androidx.navigation.NavController; | ||||||
|  | import androidx.navigation.fragment.NavHostFragment; | ||||||
|  | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
|  | import androidx.transition.TransitionManager; | ||||||
|  | 
 | ||||||
|  | import com.google.android.material.chip.Chip; | ||||||
|  | import com.google.android.material.snackbar.Snackbar; | ||||||
|  | 
 | ||||||
|  | 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.directmessages.DirectUser; | ||||||
|  | import awais.instagrabber.utils.TextUtils; | ||||||
|  | import awais.instagrabber.utils.Utils; | ||||||
|  | import awais.instagrabber.utils.ViewUtils; | ||||||
|  | import awais.instagrabber.viewmodels.UserSearchViewModel; | ||||||
|  | 
 | ||||||
|  | public class UserSearchFragment extends Fragment { | ||||||
|  |     private static final String TAG = UserSearchFragment.class.getSimpleName(); | ||||||
|  | 
 | ||||||
|  |     private FragmentUserSearchBinding binding; | ||||||
|  |     private UserSearchViewModel viewModel; | ||||||
|  |     private UserSearchResultsAdapter resultsAdapter; | ||||||
|  |     private int paddingOffset; | ||||||
|  | 
 | ||||||
|  |     private final int windowWidth = Utils.displayMetrics.widthPixels; | ||||||
|  |     private final int minInputWidth = Utils.convertDpToPx(50); | ||||||
|  |     private String actionLabel; | ||||||
|  |     private String title; | ||||||
|  |     private boolean multiple; | ||||||
|  |     private long[] hideUserIds; | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
|  |         super.onCreate(savedInstanceState); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Nullable | ||||||
|  |     @Override | ||||||
|  |     public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { | ||||||
|  |         binding = FragmentUserSearchBinding.inflate(inflater, container, false); | ||||||
|  |         viewModel = new ViewModelProvider(this).get(UserSearchViewModel.class); | ||||||
|  |         return binding.getRoot(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { | ||||||
|  |         paddingOffset = binding.search.getPaddingStart() + binding.search.getPaddingEnd() + binding.group | ||||||
|  |                 .getPaddingStart() + binding.group.getPaddingEnd() + binding.group.getChipSpacingHorizontal(); | ||||||
|  |         init(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public void onDestroyView() { | ||||||
|  |         super.onDestroyView(); | ||||||
|  |         viewModel.cleanup(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void init() { | ||||||
|  |         final Bundle arguments = getArguments(); | ||||||
|  |         if (arguments != null) { | ||||||
|  |             final UserSearchFragmentArgs fragmentArgs = UserSearchFragmentArgs.fromBundle(arguments); | ||||||
|  |             actionLabel = fragmentArgs.getActionLabel(); | ||||||
|  |             title = fragmentArgs.getTitle(); | ||||||
|  |             multiple = fragmentArgs.getMultiple(); | ||||||
|  |             viewModel.setHideUserIds(fragmentArgs.getHideUserIds()); | ||||||
|  |         } | ||||||
|  |         setupTitles(); | ||||||
|  |         setupInput(); | ||||||
|  |         setupResults(); | ||||||
|  |         setupObservers(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setupTitles() { | ||||||
|  |         if (!TextUtils.isEmpty(actionLabel)) { | ||||||
|  |             binding.done.setText(actionLabel); | ||||||
|  |         } | ||||||
|  |         if (!TextUtils.isEmpty(title)) { | ||||||
|  |             final MainActivity activity = (MainActivity) getActivity(); | ||||||
|  |             if (activity != null) { | ||||||
|  |                 final ActionBar actionBar = activity.getSupportActionBar(); | ||||||
|  |                 if (actionBar != null) { | ||||||
|  |                     actionBar.setTitle(title); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setupResults() { | ||||||
|  |         final Context context = getContext(); | ||||||
|  |         if (context == null) return; | ||||||
|  |         binding.results.setLayoutManager(new LinearLayoutManager(context)); | ||||||
|  |         resultsAdapter = new UserSearchResultsAdapter(multiple, (position, user, 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); | ||||||
|  |                 navController.navigateUp(); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             viewModel.setSelectedUser(user, !selected); | ||||||
|  |             resultsAdapter.setSelectedUser(user.getPk(), !selected); | ||||||
|  |             if (!selected) { | ||||||
|  |                 createUserChip(user); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |             final View chip = findChip(user.getPk()); | ||||||
|  |             if (chip == null) return; | ||||||
|  |             removeChip(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()); | ||||||
|  |             navController.navigateUp(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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()); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         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); | ||||||
|  |             } | ||||||
|  |             return false; | ||||||
|  |         }); | ||||||
|  |         binding.group.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { | ||||||
|  |             @Override | ||||||
|  |             public void onChildViewAdded(final View parent, final View child) {} | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onChildViewRemoved(final View parent, final View child) { | ||||||
|  |                 binding.group.post(() -> { | ||||||
|  |                     TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||||
|  |                     calculateInputWidth(0); | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setupObservers() { | ||||||
|  |         viewModel.getUsers().observe(getViewLifecycleOwner(), results -> { | ||||||
|  |             if (results == null) return; | ||||||
|  |             switch (results.status) { | ||||||
|  |                 case SUCCESS: | ||||||
|  |                     resultsAdapter.submitList(results.data); | ||||||
|  |                     break; | ||||||
|  |                 case ERROR: | ||||||
|  |                     if (results.message != null) { | ||||||
|  |                         Snackbar.make(binding.getRoot(), results.message, Snackbar.LENGTH_LONG).show(); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |                 case LOADING: | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         viewModel.showAction().observe(getViewLifecycleOwner(), showAction -> binding.done.setVisibility(showAction ? View.VISIBLE : View.GONE)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void createUserChip(final DirectUser user) { | ||||||
|  |         final Context context = getContext(); | ||||||
|  |         if (context == null) return; | ||||||
|  |         final Chip chip = new Chip(context); | ||||||
|  |         chip.setTag(user); | ||||||
|  |         chip.setText(user.getFullName()); | ||||||
|  |         chip.setCloseIconVisible(true); | ||||||
|  |         chip.setOnCloseIconClickListener(v -> removeSelectedUser(chip)); | ||||||
|  |         binding.group.post(() -> { | ||||||
|  |             final Pair<Integer, Integer> measure = ViewUtils.measure(chip, binding.group); | ||||||
|  |             TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||||
|  |             calculateInputWidth(measure.second != null ? measure.second : 0); | ||||||
|  |             binding.group.addView(chip, binding.group.getChildCount() - 1); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void removeSelectedUser(final View chip) { | ||||||
|  |         final DirectUser user = (DirectUser) chip.getTag(); | ||||||
|  |         if (user == null) return; | ||||||
|  |         viewModel.setSelectedUser(user, false); | ||||||
|  |         resultsAdapter.setSelectedUser(user.getPk(), false); | ||||||
|  |         removeChip(chip); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private View findChip(final long userId) { | ||||||
|  |         final int childCount = binding.group.getChildCount(); | ||||||
|  |         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 DirectUser user = (DirectUser) child.getTag(); | ||||||
|  |             if (user != null && user.getPk() == userId) { | ||||||
|  |                 return child; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void removeChip(final View chip) { | ||||||
|  |         binding.group.post(() -> { | ||||||
|  |             TransitionManager.beginDelayedTransition(binding.getRoot()); | ||||||
|  |             binding.group.removeView(chip); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void calculateInputWidth(final int newChipWidth) { | ||||||
|  |         final View lastChip = getLastChip(); | ||||||
|  |         int lastRight = lastChip != null ? lastChip.getRight() : 0; | ||||||
|  |         final int remainingSpaceInRow = windowWidth - lastRight; | ||||||
|  |         if (remainingSpaceInRow < newChipWidth) { | ||||||
|  |             // next chip will go to the next row, so assume no chips present | ||||||
|  |             lastRight = 0; | ||||||
|  |         } | ||||||
|  |         final int newRight = lastRight + newChipWidth; | ||||||
|  |         final int newInputWidth = windowWidth - newRight - paddingOffset; | ||||||
|  |         binding.search.getLayoutParams().width = newInputWidth < minInputWidth ? windowWidth : newInputWidth; | ||||||
|  |         binding.search.requestLayout(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private View getLastChip() { | ||||||
|  |         final int childCount = binding.group.getChildCount(); | ||||||
|  |         if (childCount == 0) { | ||||||
|  |             return null; | ||||||
|  |         } | ||||||
|  |         for (int i = childCount - 1; i >= 0; i--) { | ||||||
|  |             final View child = binding.group.getChildAt(i); | ||||||
|  |             if (child instanceof Chip) { | ||||||
|  |                 return child; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return null; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -15,6 +15,8 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout; | |||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.lifecycle.Observer; | import androidx.lifecycle.Observer; | ||||||
| import androidx.lifecycle.ViewModelProvider; | import androidx.lifecycle.ViewModelProvider; | ||||||
|  | import androidx.lifecycle.ViewModelStoreOwner; | ||||||
|  | import androidx.navigation.NavController; | ||||||
| import androidx.navigation.fragment.NavHostFragment; | import androidx.navigation.fragment.NavHostFragment; | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
| import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; | import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; | ||||||
| @ -54,7 +56,9 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh | |||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         fragmentActivity = (MainActivity) getActivity(); |         fragmentActivity = (MainActivity) getActivity(); | ||||||
|         if (fragmentActivity != null) { |         if (fragmentActivity != null) { | ||||||
|             viewModel = new ViewModelProvider(fragmentActivity).get(DirectInboxViewModel.class); |             final NavController navController = NavHostFragment.findNavController(this); | ||||||
|  |             final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); | ||||||
|  |             viewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,126 +1,117 @@ | |||||||
| package awais.instagrabber.fragments.directmessages; | package awais.instagrabber.fragments.directmessages; | ||||||
| 
 | 
 | ||||||
| import android.content.Context; | import android.content.Context; | ||||||
| import android.content.DialogInterface; |  | ||||||
| import android.content.Intent; |  | ||||||
| import android.os.AsyncTask; |  | ||||||
| import android.os.Bundle; | import android.os.Bundle; | ||||||
| import android.text.Editable; | import android.text.Editable; | ||||||
| import android.text.TextWatcher; |  | ||||||
| import android.util.Log; | import android.util.Log; | ||||||
| import android.view.LayoutInflater; | import android.view.LayoutInflater; | ||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.ViewGroup; | import android.view.ViewGroup; | ||||||
| import android.widget.ArrayAdapter; |  | ||||||
| import android.widget.EditText; |  | ||||||
| import android.widget.LinearLayout; |  | ||||||
| import android.widget.Toast; |  | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.NonNull; | import androidx.annotation.NonNull; | ||||||
| import androidx.annotation.Nullable; | import androidx.annotation.Nullable; | ||||||
| import androidx.appcompat.app.ActionBar; | import androidx.core.util.Pair; | ||||||
| import androidx.appcompat.app.AlertDialog; |  | ||||||
| import androidx.appcompat.app.AppCompatActivity; |  | ||||||
| import androidx.appcompat.widget.AppCompatButton; |  | ||||||
| import androidx.appcompat.widget.AppCompatImageView; |  | ||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
|  | import androidx.fragment.app.FragmentManager; | ||||||
|  | import androidx.lifecycle.LiveData; | ||||||
|  | import androidx.lifecycle.MutableLiveData; | ||||||
|  | import androidx.lifecycle.ViewModelProvider; | ||||||
|  | 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.navigation.fragment.NavHostFragment; | ||||||
| import androidx.recyclerview.widget.LinearLayoutManager; | import androidx.recyclerview.widget.LinearLayoutManager; | ||||||
| import androidx.recyclerview.widget.RecyclerView; |  | ||||||
| import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; |  | ||||||
| 
 | 
 | ||||||
| import java.io.DataOutputStream; | import com.google.android.material.snackbar.Snackbar; | ||||||
| import java.net.HttpURLConnection; | 
 | ||||||
| import java.net.URL; | import java.util.ArrayList; | ||||||
| import java.net.URLEncoder; | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Optional; | ||||||
|  | import java.util.Set; | ||||||
| 
 | 
 | ||||||
| import awais.instagrabber.R; | import awais.instagrabber.R; | ||||||
| import awais.instagrabber.broadcasts.DMRefreshBroadcastReceiver; | import awais.instagrabber.adapters.DirectUsersAdapter; | ||||||
|  | import awais.instagrabber.customviews.helpers.TextWatcherAdapter; | ||||||
| import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; | import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; | ||||||
| import awais.instagrabber.models.ProfileModel; | import awais.instagrabber.dialogs.MultiOptionDialogFragment; | ||||||
| import awais.instagrabber.utils.Constants; | import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; | ||||||
| import awais.instagrabber.utils.Utils; | import awais.instagrabber.models.Resource; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | import awais.instagrabber.viewmodels.DirectInboxViewModel; | ||||||
|  | import awais.instagrabber.viewmodels.DirectSettingsViewModel; | ||||||
| 
 | 
 | ||||||
| public class DirectMessageSettingsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { | public class DirectMessageSettingsFragment extends Fragment { | ||||||
|     private static final String TAG = "DirectMsgsSettingsFrag"; |     private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName(); | ||||||
| 
 | 
 | ||||||
|     private AppCompatActivity fragmentActivity; |     private FragmentDirectMessagesSettingsBinding binding; | ||||||
|     private RecyclerView userList; |     private DirectSettingsViewModel viewModel; | ||||||
|     private RecyclerView leftUserList; |     private DirectUsersAdapter usersAdapter; | ||||||
|     private EditText titleText; |     private List<Option<String>> options; | ||||||
|     private View leftTitle; |  | ||||||
|     private AppCompatImageView titleSend; |  | ||||||
|     private String threadId; |  | ||||||
|     private String threadTitle; |  | ||||||
|     private final String cookie = Utils.settingsHelper.getString(Constants.COOKIE); |  | ||||||
|     // private AsyncTask<Void, Void, InboxThreadModel> currentlyRunning; |  | ||||||
|     private View.OnClickListener clickListener; |  | ||||||
|     private View.OnClickListener basicClickListener; |  | ||||||
| 
 |  | ||||||
|     // private final FetchListener<InboxThreadModel> fetchListener = new FetchListener<InboxThreadModel>() { |  | ||||||
|     //     @Override |  | ||||||
|     //     public void doBefore() {} |  | ||||||
|     // |  | ||||||
|     //     @Override |  | ||||||
|     //     public void onResult(final InboxThreadModel threadModel) { |  | ||||||
|     //         if (threadModel == null) return; |  | ||||||
|     //         final List<Long> adminList = threadModel.getAdmins(); |  | ||||||
|     //         final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie); |  | ||||||
|     //         if (userIdFromCookie == null) return; |  | ||||||
|     //         final boolean amAdmin = adminList.contains(Long.parseLong(userIdFromCookie)); |  | ||||||
|     //         final DirectMessageMembersAdapter memberAdapter = new DirectMessageMembersAdapter(threadModel.getUsers(), |  | ||||||
|     //                                                                                           adminList, |  | ||||||
|     //                                                                                           amAdmin ? clickListener : basicClickListener); |  | ||||||
|     //         userList.setAdapter(memberAdapter); |  | ||||||
|     //         if (threadModel.getLeftUsers() != null && threadModel.getLeftUsers().size() > 0) { |  | ||||||
|     //             leftTitle.setVisibility(View.VISIBLE); |  | ||||||
|     //             final DirectMessageMembersAdapter leftAdapter = new DirectMessageMembersAdapter(threadModel.getLeftUsers(), |  | ||||||
|     //                                                                                             null, |  | ||||||
|     //                                                                                             basicClickListener); |  | ||||||
|     //             leftUserList.setAdapter(leftAdapter); |  | ||||||
|     //         } |  | ||||||
|     //     } |  | ||||||
|     // }; |  | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { |     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||||
|         super.onCreate(savedInstanceState); |         super.onCreate(savedInstanceState); | ||||||
|         fragmentActivity = (AppCompatActivity) requireActivity(); |         final NavController navController = NavHostFragment.findNavController(this); | ||||||
|         basicClickListener = v -> { |         final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); | ||||||
|             final Object tag = v.getTag(); |         final DirectInboxViewModel inboxViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); | ||||||
|             if (tag instanceof ProfileModel) { |         final List<DirectThread> threads = inboxViewModel.getThreads().getValue(); | ||||||
|                 ProfileModel model = (ProfileModel) tag; |         final Bundle arguments = getArguments(); | ||||||
|                 final Bundle bundle = new Bundle(); |         if (arguments == null) { | ||||||
|                 bundle.putString("username", "@" + model.getUsername()); |             navController.navigateUp(); | ||||||
|                 NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle); |             return; | ||||||
|             } |         } | ||||||
|         }; |         final DirectMessageSettingsFragmentArgs fragmentArgs = DirectMessageSettingsFragmentArgs.fromBundle(arguments); | ||||||
| 
 |         final String threadId = fragmentArgs.getThreadId(); | ||||||
|         clickListener = v -> { |         final Optional<DirectThread> first = threads != null ? threads.stream() | ||||||
|             final Object tag = v.getTag(); |                                                                       .filter(thread -> thread.getThreadId().equals(threadId)) | ||||||
|             if (tag instanceof ProfileModel) { |                                                                       .findFirst() | ||||||
|                 ProfileModel model = (ProfileModel) tag; |                                                              : Optional.empty(); | ||||||
|                 final Context context = getContext(); |         if (!first.isPresent()) { | ||||||
|                 if (context == null) return; |             navController.navigateUp(); | ||||||
|                 final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[]{ |             return; | ||||||
|                         getString(R.string.open_profile), |         } | ||||||
|                         getString(R.string.dms_action_kick), |         viewModel = new ViewModelProvider(this).get(DirectSettingsViewModel.class); | ||||||
|                 }); |         viewModel.setViewer(inboxViewModel.getViewer()); | ||||||
|                 final DialogInterface.OnClickListener clickListener = (d, w) -> { |         viewModel.setThread(first.get()); | ||||||
|                     if (w == 0) { |         // basicClickListener = v -> { | ||||||
|                         final Bundle bundle = new Bundle(); |         //     final Object tag = v.getTag(); | ||||||
|                         bundle.putString("username", "@" + model.getUsername()); |         //     if (tag instanceof ProfileModel) { | ||||||
|                         NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle); |         //         ProfileModel model = (ProfileModel) tag; | ||||||
|                     } else if (w == 1) { |         //         final Bundle bundle = new Bundle(); | ||||||
|                         new ChangeSettings(titleText.getText().toString()).execute("remove_users", model.getId()); |         //         bundle.putString("username", "@" + model.getUsername()); | ||||||
|                         onRefresh(); |         //         NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle); | ||||||
|                     } |         //     } | ||||||
|                 }; |         // }; | ||||||
|                 new AlertDialog.Builder(context) |         // | ||||||
|                         .setAdapter(adapter, clickListener) |         // clickListener = v -> { | ||||||
|                         .show(); |         //     final Object tag = v.getTag(); | ||||||
|             } |         //     if (tag instanceof ProfileModel) { | ||||||
|         }; |         //         ProfileModel model = (ProfileModel) tag; | ||||||
|  |         //         final Context context = getContext(); | ||||||
|  |         //         if (context == null) return; | ||||||
|  |         //         final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[]{ | ||||||
|  |         //                 getString(R.string.open_profile), | ||||||
|  |         //                 getString(R.string.dms_action_kick), | ||||||
|  |         //         }); | ||||||
|  |         //         final DialogInterface.OnClickListener clickListener = (d, w) -> { | ||||||
|  |         //             if (w == 0) { | ||||||
|  |         //                 final Bundle bundle = new Bundle(); | ||||||
|  |         //                 bundle.putString("username", "@" + model.getUsername()); | ||||||
|  |         //                 NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle); | ||||||
|  |         //             } else if (w == 1) { | ||||||
|  |         //                 new ChangeSettings(titleText.getText().toString()).execute("remove_users", model.getId()); | ||||||
|  |         //                 onRefresh(); | ||||||
|  |         //             } | ||||||
|  |         //         }; | ||||||
|  |         //         new AlertDialog.Builder(context) | ||||||
|  |         //                 .setAdapter(adapter, clickListener) | ||||||
|  |         //                 .show(); | ||||||
|  |         //     } | ||||||
|  |         // }; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @NonNull |     @NonNull | ||||||
| @ -128,155 +119,239 @@ public class DirectMessageSettingsFragment extends Fragment implements SwipeRefr | |||||||
|     public View onCreateView(@NonNull final LayoutInflater inflater, |     public View onCreateView(@NonNull final LayoutInflater inflater, | ||||||
|                              final ViewGroup container, |                              final ViewGroup container, | ||||||
|                              final Bundle savedInstanceState) { |                              final Bundle savedInstanceState) { | ||||||
|         final FragmentDirectMessagesSettingsBinding binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false); |         binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false); | ||||||
|         final LinearLayout root = binding.getRoot(); |         // final String threadId = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getThreadId(); | ||||||
|         final Context context = getContext(); |         // threadTitle = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getTitle(); | ||||||
|         if (context == null) return root; |         // binding.swipeRefreshLayout.setEnabled(false); | ||||||
|         final LinearLayoutManager layoutManager = new LinearLayoutManager(context) { |  | ||||||
|             @Override |  | ||||||
|             public boolean canScrollVertically() { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         final LinearLayoutManager layoutManagerDos = new LinearLayoutManager(context) { |  | ||||||
|             @Override |  | ||||||
|             public boolean canScrollVertically() { |  | ||||||
|                 return false; |  | ||||||
|             } |  | ||||||
|         }; |  | ||||||
|         if (getArguments() == null) { |  | ||||||
|             return root; |  | ||||||
|         } |  | ||||||
|         threadId = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getThreadId(); |  | ||||||
|         threadTitle = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getTitle(); |  | ||||||
|         binding.swipeRefreshLayout.setEnabled(false); |  | ||||||
| 
 | 
 | ||||||
|         final ActionBar actionBar = fragmentActivity.getSupportActionBar(); |         // final ActionBar actionBar = fragmentActivity.getSupportActionBar(); | ||||||
|         if (actionBar != null) { |         // if (actionBar != null) { | ||||||
|             actionBar.setTitle(threadTitle); |         //     actionBar.setTitle(threadTitle); | ||||||
|         } |         // } | ||||||
| 
 | 
 | ||||||
|         userList = binding.userList; |         // titleSend.setOnClickListener(v -> new ChangeSettings(titleText.getText().toString()).execute("update_title")); | ||||||
|         userList.setHasFixedSize(true); |  | ||||||
|         userList.setLayoutManager(layoutManager); |  | ||||||
| 
 | 
 | ||||||
|         leftUserList = binding.leftUserList; |         // binding.titleText.addTextChangedListener(new TextWatcherAdapter() { | ||||||
|         leftUserList.setHasFixedSize(true); |         //     @Override | ||||||
|         leftUserList.setLayoutManager(layoutManagerDos); |         //     public void onTextChanged(CharSequence s, int start, int before, int count) { | ||||||
|  |         //         binding.titleSend.setVisibility(s.toString().equals(threadTitle) ? View.GONE : View.VISIBLE); | ||||||
|  |         //     } | ||||||
|  |         // }); | ||||||
| 
 | 
 | ||||||
|         leftTitle = binding.leftTitle; |         // final AppCompatButton btnLeave = binding.btnLeave; | ||||||
| 
 |         // btnLeave.setOnClickListener(v -> new AlertDialog.Builder(context) | ||||||
|         titleText = binding.titleText; |         //         .setTitle(R.string.dms_action_leave_question) | ||||||
|         titleText.setText(threadTitle); |         //         .setPositiveButton(R.string.yes, (x, y) -> new ChangeSettings(titleText.getText().toString()).execute("leave")) | ||||||
| 
 |         //         .setNegativeButton(R.string.no, null) | ||||||
|         titleSend = binding.titleSend; |         //         .show()); | ||||||
|         titleSend.setOnClickListener(v -> new ChangeSettings(titleText.getText().toString()).execute("update_title")); |  | ||||||
| 
 |  | ||||||
|         titleText.addTextChangedListener(new TextWatcher() { |  | ||||||
|             @Override |  | ||||||
|             public void afterTextChanged(Editable s) {} |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public void beforeTextChanged(CharSequence s, int start, int count, int after) {} |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public void onTextChanged(CharSequence s, int start, int before, int count) { |  | ||||||
|                 titleSend.setVisibility(s.toString().equals(threadTitle) ? View.GONE : View.VISIBLE); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
| 
 |  | ||||||
|         final AppCompatButton btnLeave = binding.btnLeave; |  | ||||||
|         btnLeave.setOnClickListener(v -> new AlertDialog.Builder(context) |  | ||||||
|                 .setTitle(R.string.dms_action_leave_question) |  | ||||||
|                 .setPositiveButton(R.string.yes, |  | ||||||
|                                    (x, y) -> new ChangeSettings(titleText.getText().toString()).execute("leave")) |  | ||||||
|                 .setNegativeButton(R.string.no, null) |  | ||||||
|                 .show()); |  | ||||||
| 
 | 
 | ||||||
|         // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); |         // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); | ||||||
|         return root; |         return binding.getRoot(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public void onRefresh() { |     public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { | ||||||
|         stopCurrentExecutor(); |         init(); | ||||||
|         // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); |         setupObservers(); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void stopCurrentExecutor() { |     private void setupObservers() { | ||||||
|         // if (currentlyRunning != null) { |         viewModel.getUsers().observe(getViewLifecycleOwner(), users -> { | ||||||
|         //     try { |             if (usersAdapter == null) return; | ||||||
|         //         currentlyRunning.cancel(true); |             usersAdapter.submitUsers(users.first, users.second); | ||||||
|         //     } catch (final Exception e) { |         }); | ||||||
|         //         if (BuildConfig.DEBUG) { |         viewModel.getTitle().observe(getViewLifecycleOwner(), title -> binding.titleEdit.setText(title)); | ||||||
|         //             Log.e(TAG, "", e); |         viewModel.getAdminUserIds().observe(getViewLifecycleOwner(), adminUserIds -> { | ||||||
|         //         } |             if (usersAdapter == null) return; | ||||||
|         //     } |             usersAdapter.setAdminUserIds(adminUserIds); | ||||||
|         // } |         }); | ||||||
|     } |         final NavController navController = NavHostFragment.findNavController(this); | ||||||
| 
 |         final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); | ||||||
|     class ChangeSettings extends AsyncTask<String, Void, Void> { |         if (backStackEntry != null) { | ||||||
|         String action, argument; |             final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); | ||||||
|         boolean ok = false; |             resultLiveData.observe(getViewLifecycleOwner(), result -> { | ||||||
|         private final String text; |                 LiveData<Resource<Object>> detailsChangeResourceLiveData = null; | ||||||
| 
 |                 if ((result instanceof DirectUser)) { | ||||||
|         public ChangeSettings(final String text) { |                     // Log.d(TAG, "result: " + result); | ||||||
|             this.text = text; |                     detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton((DirectUser) result)); | ||||||
|         } |                 } else if ((result instanceof Set)) { | ||||||
| 
 |                     try { | ||||||
|         protected Void doInBackground(String... rawAction) { |                         // Log.d(TAG, "result: " + result); | ||||||
|             action = rawAction[0]; |                         //noinspection unchecked | ||||||
|             if (rawAction.length == 2) argument = rawAction[1]; |                         detailsChangeResourceLiveData = viewModel.addMembers((Set<DirectUser>) result); | ||||||
|             final String url = "https://i.instagram.com/api/v1/direct_v2/threads/" + threadId + "/" + action + "/"; |                     } catch (Exception e) { | ||||||
|             try { |                         Log.e(TAG, "search users result: ", e); | ||||||
|                 String urlParameters = "_csrftoken=" + cookie.split("csrftoken=")[1].split(";")[0] |                     } | ||||||
|                         + "&_uuid=" + Utils.settingsHelper.getString(Constants.DEVICE_UUID); |  | ||||||
|                 if (action.equals("update_title")) { |  | ||||||
|                     urlParameters += "&title=" + URLEncoder.encode(text, "UTF-8") |  | ||||||
|                                                            .replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'") |  | ||||||
|                                                            .replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~"); |  | ||||||
|                 } else if (action.startsWith("remove_users")) |  | ||||||
|                     urlParameters += ("&user_ids=[" + argument + "]"); |  | ||||||
|                 final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); |  | ||||||
|                 urlConnection.setRequestMethod("POST"); |  | ||||||
|                 urlConnection.setUseCaches(false); |  | ||||||
|                 urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); |  | ||||||
|                 urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); |  | ||||||
|                 urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length)); |  | ||||||
|                 urlConnection.setDoOutput(true); |  | ||||||
|                 DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); |  | ||||||
|                 wr.writeBytes(urlParameters); |  | ||||||
|                 wr.flush(); |  | ||||||
|                 wr.close(); |  | ||||||
|                 urlConnection.connect(); |  | ||||||
|                 if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { |  | ||||||
|                     ok = true; |  | ||||||
|                 } |                 } | ||||||
|                 urlConnection.disconnect(); |                 if (detailsChangeResourceLiveData != null) { | ||||||
|             } catch (Throwable ex) { |                     observeDetailsChange(detailsChangeResourceLiveData); | ||||||
|                 Log.e("austin_debug", "unsend: " + ex); |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void init() { | ||||||
|  |         setupSettings(); | ||||||
|  |         setupMembers(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void setupSettings() { | ||||||
|  |         binding.groupSettings.setVisibility(viewModel.isGroup() ? View.VISIBLE : View.GONE); | ||||||
|  |         if (!viewModel.isGroup()) return; | ||||||
|  |         binding.titleEdit.addTextChangedListener(new TextWatcherAdapter() { | ||||||
|  |             @Override | ||||||
|  |             public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { | ||||||
|  |                 if (s.toString().trim().equals(viewModel.getTitle().getValue())) { | ||||||
|  |                     binding.titleEditInputLayout.setSuffixText(null); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 binding.titleEditInputLayout.setSuffixText(getString(R.string.save)); | ||||||
|             } |             } | ||||||
|             return null; |         }); | ||||||
|         } |         binding.titleEditInputLayout.getSuffixTextView().setOnClickListener(v -> { | ||||||
| 
 |             final Editable text = binding.titleEdit.getText(); | ||||||
|         @Override |             if (text == null) return; | ||||||
|         protected void onPostExecute(Void result) { |             final String newTitle = text.toString().trim(); | ||||||
|             final Context context = getContext(); |             if (newTitle.equals(viewModel.getTitle().getValue())) return; | ||||||
|             if (context == null) return; |             observeDetailsChange(viewModel.updateTitle(newTitle)); | ||||||
|             if (ok) { |         }); | ||||||
|                 Toast.makeText(context, R.string.dms_action_success, Toast.LENGTH_SHORT).show(); |         binding.addMembers.setOnClickListener(v -> { | ||||||
|                 if (action.equals("update_title")) { |             if (!isAdded()) return; | ||||||
|                     threadTitle = titleText.getText().toString(); |             final NavController navController = NavHostFragment.findNavController(this); | ||||||
|                     titleSend.setVisibility(View.GONE); |             final NavDestination currentDestination = navController.getCurrentDestination(); | ||||||
|                     titleText.clearFocus(); |             if (currentDestination == null) return; | ||||||
|                     DirectMessageThreadFragment.hasSentSomething = true; |             if (currentDestination.getId() != R.id.directMessagesSettingsFragment) return; | ||||||
|                 } else if (action.equals("leave")) { |             final Pair<List<DirectUser>, List<DirectUser>> users = viewModel.getUsers().getValue(); | ||||||
|                     context.sendBroadcast(new Intent(DMRefreshBroadcastReceiver.ACTION_REFRESH_DM)); |             final long[] currentUserIds; | ||||||
|                     NavHostFragment.findNavController(DirectMessageSettingsFragment.this).popBackStack(R.id.directMessagesInboxFragment, false); |             if (users != null && users.first != null) { | ||||||
|                 } else { |                 final List<DirectUser> currentMembers = users.first; | ||||||
|                     DirectMessageThreadFragment.hasSentSomething = true; |                 currentUserIds = currentMembers.stream() | ||||||
|                 } |                                                .mapToLong(DirectUser::getPk) | ||||||
|             } else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); |                                                .sorted() | ||||||
|         } |                                                .toArray(); | ||||||
|  |             } else { | ||||||
|  |                 currentUserIds = new long[0]; | ||||||
|  |             } | ||||||
|  |             final NavDirections directions = DirectMessageSettingsFragmentDirections.actionGlobalUserSearch( | ||||||
|  |                     true, | ||||||
|  |                     "Add users", | ||||||
|  |                     "Add", | ||||||
|  |                     currentUserIds | ||||||
|  |             ); | ||||||
|  |             navController.navigate(directions); | ||||||
|  |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     private void setupMembers() { | ||||||
|  |         final Context context = getContext(); | ||||||
|  |         if (context == null) return; | ||||||
|  |         binding.users.setLayoutManager(new LinearLayoutManager(context)); | ||||||
|  |         final DirectUser inviter = viewModel.getThread().getInviter(); | ||||||
|  |         usersAdapter = new DirectUsersAdapter( | ||||||
|  |                 inviter != null ? inviter.getPk() : -1, | ||||||
|  |                 (position, user, selected) -> { | ||||||
|  |                     // navigate to profile | ||||||
|  |                 }, | ||||||
|  |                 (position, user) -> { | ||||||
|  |                     final ArrayList<Option<String>> options = viewModel.createUserOptions(user); | ||||||
|  |                     if (options == null || options.isEmpty()) return true; | ||||||
|  |                     final MultiOptionDialogFragment<String> fragment = MultiOptionDialogFragment.newInstance(-1, options); | ||||||
|  |                     fragment.setSingleCallback(new MultiOptionDialogFragment.MultiOptionDialogSingleCallback<String>() { | ||||||
|  |                         @Override | ||||||
|  |                         public void onSelect(final String action) { | ||||||
|  |                             if (action == null) return; | ||||||
|  |                             observeDetailsChange(viewModel.doAction(user, action)); | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         @Override | ||||||
|  |                         public void onCancel() {} | ||||||
|  |                     }); | ||||||
|  |                     final FragmentManager fragmentManager = getChildFragmentManager(); | ||||||
|  |                     fragment.show(fragmentManager, "actions"); | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |         ); | ||||||
|  |         binding.users.setAdapter(usersAdapter); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void observeDetailsChange(@NonNull final LiveData<Resource<Object>> detailsChangeResourceLiveData) { | ||||||
|  |         detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> { | ||||||
|  |             if (resource == null) return; | ||||||
|  |             switch (resource.status) { | ||||||
|  |                 case SUCCESS: | ||||||
|  |                 case LOADING: | ||||||
|  |                     break; | ||||||
|  |                 case ERROR: | ||||||
|  |                     if (resource.message != null) { | ||||||
|  |                         Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); | ||||||
|  |                     } | ||||||
|  |                     break; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // class ChangeSettings extends AsyncTask<String, Void, Void> { | ||||||
|  |     //     String action, argument; | ||||||
|  |     //     boolean ok = false; | ||||||
|  |     //     private final String text; | ||||||
|  |     // | ||||||
|  |     //     public ChangeSettings(final String text) { | ||||||
|  |     //         this.text = text; | ||||||
|  |     //     } | ||||||
|  |     // | ||||||
|  |     //     protected Void doInBackground(String... rawAction) { | ||||||
|  |     //         action = rawAction[0]; | ||||||
|  |     //         if (rawAction.length == 2) argument = rawAction[1]; | ||||||
|  |     //         final String url = "https://i.instagram.com/api/v1/direct_v2/threads/" + threadId + "/" + action + "/"; | ||||||
|  |     //         try { | ||||||
|  |     //             String urlParameters = "_csrftoken=" + cookie.split("csrftoken=")[1].split(";")[0] | ||||||
|  |     //                     + "&_uuid=" + Utils.settingsHelper.getString(Constants.DEVICE_UUID); | ||||||
|  |     //             if (action.equals("update_title")) { | ||||||
|  |     //                 urlParameters += "&title=" + URLEncoder.encode(text, "UTF-8") | ||||||
|  |     //                                                        .replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'") | ||||||
|  |     //                                                        .replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~"); | ||||||
|  |     //             } else if (action.startsWith("remove_users")) | ||||||
|  |     //                 urlParameters += ("&user_ids=[" + argument + "]"); | ||||||
|  |     //             final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection(); | ||||||
|  |     //             urlConnection.setRequestMethod("POST"); | ||||||
|  |     //             urlConnection.setUseCaches(false); | ||||||
|  |     //             urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT); | ||||||
|  |     //             urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); | ||||||
|  |     //             urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length)); | ||||||
|  |     //             urlConnection.setDoOutput(true); | ||||||
|  |     //             DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream()); | ||||||
|  |     //             wr.writeBytes(urlParameters); | ||||||
|  |     //             wr.flush(); | ||||||
|  |     //             wr.close(); | ||||||
|  |     //             urlConnection.connect(); | ||||||
|  |     //             if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) { | ||||||
|  |     //                 ok = true; | ||||||
|  |     //             } | ||||||
|  |     //             urlConnection.disconnect(); | ||||||
|  |     //         } catch (Throwable ex) { | ||||||
|  |     //             Log.e("austin_debug", "unsend: " + ex); | ||||||
|  |     //         } | ||||||
|  |     //         return null; | ||||||
|  |     //     } | ||||||
|  |     // | ||||||
|  |     //     @Override | ||||||
|  |     //     protected void onPostExecute(Void result) { | ||||||
|  |     //         final Context context = getContext(); | ||||||
|  |     //         if (context == null) return; | ||||||
|  |     //         if (ok) { | ||||||
|  |     //             Toast.makeText(context, R.string.dms_action_success, Toast.LENGTH_SHORT).show(); | ||||||
|  |     //             if (action.equals("update_title")) { | ||||||
|  |     //                 threadTitle = titleText.getText().toString(); | ||||||
|  |     //                 titleSend.setVisibility(View.GONE); | ||||||
|  |     //                 titleText.clearFocus(); | ||||||
|  |     //                 DirectMessageThreadFragment.hasSentSomething = true; | ||||||
|  |     //             } else if (action.equals("leave")) { | ||||||
|  |     //                 context.sendBroadcast(new Intent(DMRefreshBroadcastReceiver.ACTION_REFRESH_DM)); | ||||||
|  |     //                 NavHostFragment.findNavController(DirectMessageSettingsFragment.this).popBackStack(R.id.directMessagesInboxFragment, false); | ||||||
|  |     //             } else { | ||||||
|  |     //                 DirectMessageThreadFragment.hasSentSomething = true; | ||||||
|  |     //             } | ||||||
|  |     //         } else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); | ||||||
|  |     //     } | ||||||
|  |     // } | ||||||
| } | } | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ import androidx.core.content.ContextCompat; | |||||||
| import androidx.fragment.app.Fragment; | import androidx.fragment.app.Fragment; | ||||||
| import androidx.lifecycle.MutableLiveData; | import androidx.lifecycle.MutableLiveData; | ||||||
| import androidx.lifecycle.ViewModelProvider; | import androidx.lifecycle.ViewModelProvider; | ||||||
|  | import androidx.lifecycle.ViewModelStoreOwner; | ||||||
| import androidx.navigation.NavBackStackEntry; | import androidx.navigation.NavBackStackEntry; | ||||||
| import androidx.navigation.NavController; | import androidx.navigation.NavController; | ||||||
| import androidx.navigation.NavDirections; | import androidx.navigation.NavDirections; | ||||||
| @ -249,8 +250,8 @@ public class DirectMessageThreadFragment extends Fragment { | |||||||
|     public boolean onOptionsItemSelected(@NonNull final MenuItem item) { |     public boolean onOptionsItemSelected(@NonNull final MenuItem item) { | ||||||
|         final int itemId = item.getItemId(); |         final int itemId = item.getItemId(); | ||||||
|         if (itemId == R.id.info) { |         if (itemId == R.id.info) { | ||||||
|             // final NavDirections action = DirectMessageThreadFragmentDirections.actionDMThreadFragmentToDMSettingsFragment(threadId, threadTitle); |             final NavDirections action = DirectMessageThreadFragmentDirections.actionDMThreadFragmentToDMSettingsFragment(viewModel.getThreadId()); | ||||||
|             // NavHostFragment.findNavController(this).navigate(action); |             NavHostFragment.findNavController(this).navigate(action); | ||||||
|             return true; |             return true; | ||||||
|         } |         } | ||||||
|         if (itemId == R.id.mark_as_seen) { |         if (itemId == R.id.mark_as_seen) { | ||||||
| @ -372,7 +373,9 @@ public class DirectMessageThreadFragment extends Fragment { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private void getInitialData() { |     private void getInitialData() { | ||||||
|         final DirectInboxViewModel threadListViewModel = new ViewModelProvider(fragmentActivity).get(DirectInboxViewModel.class); |         final NavController navController = NavHostFragment.findNavController(this); | ||||||
|  |         final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph); | ||||||
|  |         final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class); | ||||||
|         final List<DirectThread> threads = threadListViewModel.getThreads().getValue(); |         final List<DirectThread> threads = threadListViewModel.getThreads().getValue(); | ||||||
|         final Optional<DirectThread> first = threads != null |         final Optional<DirectThread> first = threads != null | ||||||
|                                              ? threads.stream() |                                              ? threads.stream() | ||||||
|  | |||||||
| @ -37,8 +37,8 @@ import awais.instagrabber.utils.Constants; | |||||||
| import awais.instagrabber.utils.CookieUtils; | import awais.instagrabber.utils.CookieUtils; | ||||||
| import awais.instagrabber.utils.FlavorTown; | import awais.instagrabber.utils.FlavorTown; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import awais.instagrabber.webservices.ProfileService; |  | ||||||
| import awais.instagrabber.webservices.ServiceCallback; | import awais.instagrabber.webservices.ServiceCallback; | ||||||
|  | import awais.instagrabber.webservices.UserService; | ||||||
| 
 | 
 | ||||||
| import static awais.instagrabber.utils.Utils.settingsHelper; | import static awais.instagrabber.utils.Utils.settingsHelper; | ||||||
| 
 | 
 | ||||||
| @ -199,8 +199,8 @@ public class MorePreferencesFragment extends BasePreferencesFragment { | |||||||
| 
 | 
 | ||||||
|             // adds cookies to database for quick access |             // adds cookies to database for quick access | ||||||
|             final String uid = CookieUtils.getUserIdFromCookie(cookie); |             final String uid = CookieUtils.getUserIdFromCookie(cookie); | ||||||
|             final ProfileService profileService = ProfileService.getInstance(); |             final UserService userService = UserService.getInstance(); | ||||||
|             profileService.getUserInfo(uid, new ServiceCallback<UserInfo>() { |             userService.getUserInfo(uid, new ServiceCallback<UserInfo>() { | ||||||
|                 @Override |                 @Override | ||||||
|                 public void onSuccess(final UserInfo result) { |                 public void onSuccess(final UserInfo result) { | ||||||
|                     // Log.d(TAG, "adding userInfo: " + result); |                     // Log.d(TAG, "adding userInfo: " + result); | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ import java.util.Map; | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | 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.DirectThreadFeedResponse; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| import retrofit2.http.FieldMap; | import retrofit2.http.FieldMap; | ||||||
| @ -30,4 +31,29 @@ public interface DirectMessagesRepository { | |||||||
|     @POST("/api/v1/direct_v2/threads/broadcast/{item}/") |     @POST("/api/v1/direct_v2/threads/broadcast/{item}/") | ||||||
|     Call<DirectThreadBroadcastResponse> broadcast(@Path("item") String item, |     Call<DirectThreadBroadcastResponse> broadcast(@Path("item") String item, | ||||||
|                                                   @FieldMap final Map<String, String> signedForm); |                                                   @FieldMap final Map<String, String> signedForm); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/{threadId}/add_user/") | ||||||
|  |     Call<DirectThreadDetailsChangeResponse> addUsers(@Path("threadId") String threadId, | ||||||
|  |                                                      @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/{threadId}/remove_users/") | ||||||
|  |     Call<String> removeUsers(@Path("threadId") String threadId, | ||||||
|  |                              @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/{threadId}/update_title/") | ||||||
|  |     Call<DirectThreadDetailsChangeResponse> updateTitle(@Path("threadId") String threadId, | ||||||
|  |                                                         @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/{threadId}/add_admins/") | ||||||
|  |     Call<String> addAdmins(@Path("threadId") String threadId, | ||||||
|  |                            @FieldMap final Map<String, String> form); | ||||||
|  | 
 | ||||||
|  |     @FormUrlEncoded | ||||||
|  |     @POST("/api/v1/direct_v2/threads/{threadId}/remove_admins/") | ||||||
|  |     Call<String> removeAdmins(@Path("threadId") String threadId, | ||||||
|  |                               @FieldMap final Map<String, String> form); | ||||||
| } | } | ||||||
|  | |||||||
| @ -9,9 +9,6 @@ import retrofit2.http.QueryMap; | |||||||
| 
 | 
 | ||||||
| public interface ProfileRepository { | public interface ProfileRepository { | ||||||
| 
 | 
 | ||||||
|     @GET("/api/v1/users/{uid}/info/") |  | ||||||
|     Call<String> getUserInfo(@Path("uid") final String uid); |  | ||||||
| 
 |  | ||||||
|     @GET("/api/v1/feed/user/{uid}/") |     @GET("/api/v1/feed/user/{uid}/") | ||||||
|     Call<String> fetch(@Path("uid") final String uid, @QueryMap Map<String, String> queryParams); |     Call<String> fetch(@Path("uid") final String uid, @QueryMap Map<String, String> queryParams); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,17 @@ | |||||||
|  | package awais.instagrabber.repositories; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||||
|  | import retrofit2.Call; | ||||||
|  | import retrofit2.http.GET; | ||||||
|  | import retrofit2.http.Path; | ||||||
|  | import retrofit2.http.Query; | ||||||
|  | 
 | ||||||
|  | public interface UserRepository { | ||||||
|  | 
 | ||||||
|  |     @GET("/api/v1/users/{uid}/info/") | ||||||
|  |     Call<String> getUserInfo(@Path("uid") final String uid); | ||||||
|  | 
 | ||||||
|  |     @GET("/api/v1/users/search/") | ||||||
|  |     Call<UserSearchResponse> search(@Query("timezone_offset") float timezoneOffset, | ||||||
|  |                                     @Query("q") String query); | ||||||
|  | } | ||||||
| @ -0,0 +1,35 @@ | |||||||
|  | package awais.instagrabber.repositories.responses; | ||||||
|  | 
 | ||||||
|  | import java.util.List; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | 
 | ||||||
|  | public class UserSearchResponse { | ||||||
|  |     private final int numResults; | ||||||
|  |     private final List<DirectUser> users; | ||||||
|  |     private final boolean hasMore; | ||||||
|  |     private final String status; | ||||||
|  | 
 | ||||||
|  |     public UserSearchResponse(final int numResults, final List<DirectUser> users, final boolean hasMore, final String status) { | ||||||
|  |         this.numResults = numResults; | ||||||
|  |         this.users = users; | ||||||
|  |         this.hasMore = hasMore; | ||||||
|  |         this.status = status; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public int getNumResults() { | ||||||
|  |         return numResults; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public List<DirectUser> getUsers() { | ||||||
|  |         return users; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean hasMore() { | ||||||
|  |         return hasMore; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getStatus() { | ||||||
|  |         return status; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,6 +1,7 @@ | |||||||
| package awais.instagrabber.repositories.responses.directmessages; | package awais.instagrabber.repositories.responses.directmessages; | ||||||
| 
 | 
 | ||||||
| public class DirectInboxResponse { | public class DirectInboxResponse { | ||||||
|  |     private final DirectUser viewer; | ||||||
|     private final DirectInbox inbox; |     private final DirectInbox inbox; | ||||||
|     private final long seqId; |     private final long seqId; | ||||||
|     private final long snapshotAtMs; |     private final long snapshotAtMs; | ||||||
| @ -8,12 +9,14 @@ public class DirectInboxResponse { | |||||||
|     private final DirectUser mostRecentInviter; |     private final DirectUser mostRecentInviter; | ||||||
|     private final String status; |     private final String status; | ||||||
| 
 | 
 | ||||||
|     public DirectInboxResponse(final DirectInbox inbox, |     public DirectInboxResponse(final DirectUser viewer, | ||||||
|  |                                final DirectInbox inbox, | ||||||
|                                final long seqId, |                                final long seqId, | ||||||
|                                final long snapshotAtMs, |                                final long snapshotAtMs, | ||||||
|                                final int pendingRequestsTotal, |                                final int pendingRequestsTotal, | ||||||
|                                final DirectUser mostRecentInviter, |                                final DirectUser mostRecentInviter, | ||||||
|                                final String status) { |                                final String status) { | ||||||
|  |         this.viewer = viewer; | ||||||
|         this.inbox = inbox; |         this.inbox = inbox; | ||||||
|         this.seqId = seqId; |         this.seqId = seqId; | ||||||
|         this.snapshotAtMs = snapshotAtMs; |         this.snapshotAtMs = snapshotAtMs; | ||||||
| @ -22,6 +25,10 @@ public class DirectInboxResponse { | |||||||
|         this.status = status; |         this.status = status; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public DirectUser getViewer() { | ||||||
|  |         return viewer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public DirectInbox getInbox() { |     public DirectInbox getInbox() { | ||||||
|         return inbox; |         return inbox; | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -10,8 +10,8 @@ public class DirectThread { | |||||||
|     private final String threadId; |     private final String threadId; | ||||||
|     private final String threadV2Id; |     private final String threadV2Id; | ||||||
|     private final List<DirectUser> users; |     private final List<DirectUser> users; | ||||||
|     private final List<String> leftUsers; |     private final List<DirectUser> leftUsers; | ||||||
|     private final List<String> adminUserIds; |     private final List<Long> adminUserIds; | ||||||
|     private final List<DirectItem> items; |     private final List<DirectItem> items; | ||||||
|     private final long lastActivityAt; |     private final long lastActivityAt; | ||||||
|     private final boolean muted; |     private final boolean muted; | ||||||
| @ -42,8 +42,8 @@ public class DirectThread { | |||||||
|     public DirectThread(final String threadId, |     public DirectThread(final String threadId, | ||||||
|                         final String threadV2Id, |                         final String threadV2Id, | ||||||
|                         final List<DirectUser> users, |                         final List<DirectUser> users, | ||||||
|                         final List<String> leftUsers, |                         final List<DirectUser> leftUsers, | ||||||
|                         final List<String> adminUserIds, |                         final List<Long> adminUserIds, | ||||||
|                         final List<DirectItem> items, |                         final List<DirectItem> items, | ||||||
|                         final long lastActivityAt, |                         final long lastActivityAt, | ||||||
|                         final boolean muted, |                         final boolean muted, | ||||||
| @ -115,11 +115,11 @@ public class DirectThread { | |||||||
|         return users; |         return users; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public List<String> getLeftUsers() { |     public List<DirectUser> getLeftUsers() { | ||||||
|         return leftUsers; |         return leftUsers; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public List<String> getAdminUserIds() { |     public List<Long> getAdminUserIds() { | ||||||
|         return adminUserIds; |         return adminUserIds; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,19 @@ | |||||||
|  | package awais.instagrabber.repositories.responses.directmessages; | ||||||
|  | 
 | ||||||
|  | public class DirectThreadDetailsChangeResponse { | ||||||
|  |     private final DirectThread thread; | ||||||
|  |     private final String status; | ||||||
|  | 
 | ||||||
|  |     public DirectThreadDetailsChangeResponse(final DirectThread thread, final String status) { | ||||||
|  |         this.thread = thread; | ||||||
|  |         this.status = status; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public DirectThread getThread() { | ||||||
|  |         return thread; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public String getStatus() { | ||||||
|  |         return status; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -1,8 +1,11 @@ | |||||||
| package awais.instagrabber.repositories.responses.directmessages; | package awais.instagrabber.repositories.responses.directmessages; | ||||||
| 
 | 
 | ||||||
|  | import java.io.Serializable; | ||||||
|  | import java.util.Objects; | ||||||
|  | 
 | ||||||
| import awais.instagrabber.models.ProfileModel; | import awais.instagrabber.models.ProfileModel; | ||||||
| 
 | 
 | ||||||
| public class DirectUser { | public class DirectUser implements Serializable { | ||||||
|     private final long pk; |     private final long pk; | ||||||
|     private final String username; |     private final String username; | ||||||
|     private final String fullName; |     private final String fullName; | ||||||
| @ -104,8 +107,8 @@ public class DirectUser { | |||||||
|                         profileModel.isPrivate(), |                         profileModel.isPrivate(), | ||||||
|                         false, |                         false, | ||||||
|                         profileModel.isRequested(), |                         profileModel.isRequested(), | ||||||
|                         false |                         false, | ||||||
|                 ), |                         profileModel.isRestricted()), | ||||||
|                 profileModel.isVerified(), |                 profileModel.isVerified(), | ||||||
|                 false, |                 false, | ||||||
|                 false, |                 false, | ||||||
| @ -113,4 +116,18 @@ public class DirectUser { | |||||||
|                 null |                 null | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public boolean equals(final Object o) { | ||||||
|  |         if (this == o) return true; | ||||||
|  |         if (o == null || getClass() != o.getClass()) return false; | ||||||
|  |         final DirectUser that = (DirectUser) o; | ||||||
|  |         return pk == that.pk && | ||||||
|  |                 Objects.equals(username, that.username); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @Override | ||||||
|  |     public int hashCode() { | ||||||
|  |         return Objects.hash(pk, username); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,25 +1,31 @@ | |||||||
| package awais.instagrabber.repositories.responses.directmessages; | package awais.instagrabber.repositories.responses.directmessages; | ||||||
| 
 | 
 | ||||||
| public class DirectUserFriendshipStatus { | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import java.io.Serializable; | ||||||
|  | 
 | ||||||
|  | public class DirectUserFriendshipStatus implements Serializable { | ||||||
|     private final boolean following; |     private final boolean following; | ||||||
|     private final boolean blocking; |     private final boolean blocking; | ||||||
|     private final boolean isPrivate; |     private final boolean isPrivate; | ||||||
|     private final boolean incomingRequest; |     private final boolean incomingRequest; | ||||||
|     private final boolean outgoingRequest; |     private final boolean outgoingRequest; | ||||||
|     private final boolean isBestie; |     private final boolean isBestie; | ||||||
|  |     private final boolean isRestricted; | ||||||
| 
 | 
 | ||||||
|     public DirectUserFriendshipStatus(final boolean following, |     public DirectUserFriendshipStatus(final boolean following, | ||||||
|                                       final boolean blocking, |                                       final boolean blocking, | ||||||
|                                       final boolean isPrivate, |                                       final boolean isPrivate, | ||||||
|                                       final boolean incomingRequest, |                                       final boolean incomingRequest, | ||||||
|                                       final boolean outgoingRequest, |                                       final boolean outgoingRequest, | ||||||
|                                       final boolean isBestie) { |                                       final boolean isBestie, final boolean isRestricted) { | ||||||
|         this.following = following; |         this.following = following; | ||||||
|         this.blocking = blocking; |         this.blocking = blocking; | ||||||
|         this.isPrivate = isPrivate; |         this.isPrivate = isPrivate; | ||||||
|         this.incomingRequest = incomingRequest; |         this.incomingRequest = incomingRequest; | ||||||
|         this.outgoingRequest = outgoingRequest; |         this.outgoingRequest = outgoingRequest; | ||||||
|         this.isBestie = isBestie; |         this.isBestie = isBestie; | ||||||
|  |         this.isRestricted = isRestricted; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public boolean isFollowing() { |     public boolean isFollowing() { | ||||||
| @ -46,15 +52,21 @@ public class DirectUserFriendshipStatus { | |||||||
|         return isBestie; |         return isBestie; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public boolean isRestricted() { | ||||||
|  |         return isRestricted; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|     @Override |     @Override | ||||||
|     public String toString() { |     public String toString() { | ||||||
|         return "DirectInboxFeedResponseFriendshipStatus{" + |         return "DirectInboxFeedResponseFriendshipStatus{" + | ||||||
|                 "following=" + following + |                 "following=" + following + | ||||||
|                 ", blocking=" + blocking + |                 ", blocking=" + blocking + | ||||||
|                 ", is_private=" + isPrivate + |                 ", isPrivate=" + isPrivate + | ||||||
|                 ", incomingRequest=" + incomingRequest + |                 ", incomingRequest=" + incomingRequest + | ||||||
|                 ", outgoingRequest=" + outgoingRequest + |                 ", outgoingRequest=" + outgoingRequest + | ||||||
|                 ", isBestie=" + isBestie + |                 ", isBestie=" + isBestie + | ||||||
|  |                 ", isRestricted" + isRestricted + | ||||||
|                 '}'; |                 '}'; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										90
									
								
								app/src/main/java/awais/instagrabber/utils/Debouncer.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								app/src/main/java/awais/instagrabber/utils/Debouncer.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | |||||||
|  | package awais.instagrabber.utils; | ||||||
|  | 
 | ||||||
|  | import java.util.concurrent.ConcurrentHashMap; | ||||||
|  | import java.util.concurrent.Executors; | ||||||
|  | import java.util.concurrent.ScheduledExecutorService; | ||||||
|  | import java.util.concurrent.ScheduledFuture; | ||||||
|  | import java.util.concurrent.TimeUnit; | ||||||
|  | 
 | ||||||
|  | public class Debouncer<T> { | ||||||
|  |     private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); | ||||||
|  |     private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<>(); | ||||||
|  |     private final ConcurrentHashMap<T, ScheduledFuture<?>> futureMap = new ConcurrentHashMap<>(); | ||||||
|  |     private final Callback<T> callback; | ||||||
|  |     private final int interval; | ||||||
|  | 
 | ||||||
|  |     public Debouncer(Callback<T> c, int interval) { | ||||||
|  |         this.callback = c; | ||||||
|  |         this.interval = interval; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void call(T key) { | ||||||
|  |         TimerTask task = new TimerTask(key); | ||||||
|  | 
 | ||||||
|  |         TimerTask prev; | ||||||
|  |         do { | ||||||
|  |             prev = delayedMap.putIfAbsent(key, task); | ||||||
|  |             if (prev == null) { | ||||||
|  |                 final ScheduledFuture<?> future = scheduler.schedule(task, interval, TimeUnit.MILLISECONDS); | ||||||
|  |                 futureMap.put(key, future); | ||||||
|  |             } | ||||||
|  |         } while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void terminate() { | ||||||
|  |         scheduler.shutdownNow(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void cancel(final T key) { | ||||||
|  |         delayedMap.remove(key); | ||||||
|  |         final ScheduledFuture<?> future = futureMap.get(key); | ||||||
|  |         if (future != null) { | ||||||
|  |             future.cancel(true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // The task that wakes up when the wait time elapses | ||||||
|  |     private class TimerTask implements Runnable { | ||||||
|  |         private final T key; | ||||||
|  |         private long dueTime; | ||||||
|  |         private final Object lock = new Object(); | ||||||
|  | 
 | ||||||
|  |         public TimerTask(T key) { | ||||||
|  |             this.key = key; | ||||||
|  |             extend(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public boolean extend() { | ||||||
|  |             synchronized (lock) { | ||||||
|  |                 if (dueTime < 0) // Task has been shutdown | ||||||
|  |                     return false; | ||||||
|  |                 dueTime = System.currentTimeMillis() + interval; | ||||||
|  |                 return true; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public void run() { | ||||||
|  |             synchronized (lock) { | ||||||
|  |                 long remaining = dueTime - System.currentTimeMillis(); | ||||||
|  |                 if (remaining > 0) { // Re-schedule task | ||||||
|  |                     scheduler.schedule(this, remaining, TimeUnit.MILLISECONDS); | ||||||
|  |                 } else { // Mark as terminated and invoke callback | ||||||
|  |                     dueTime = -1; | ||||||
|  |                     try { | ||||||
|  |                         callback.call(key); | ||||||
|  |                     } catch (Exception e) { | ||||||
|  |                         callback.onError(e); | ||||||
|  |                     } finally { | ||||||
|  |                         delayedMap.remove(key); | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public interface Callback<T> { | ||||||
|  |         void call(T key); | ||||||
|  | 
 | ||||||
|  |         void onError(Throwable t); | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -24,6 +24,7 @@ import android.view.Gravity; | |||||||
| import android.view.View; | import android.view.View; | ||||||
| import android.view.Window; | import android.view.Window; | ||||||
| import android.view.WindowManager; | import android.view.WindowManager; | ||||||
|  | import android.view.inputmethod.InputMethodManager; | ||||||
| import android.webkit.MimeTypeMap; | import android.webkit.MimeTypeMap; | ||||||
| import android.widget.Toast; | import android.widget.Toast; | ||||||
| 
 | 
 | ||||||
| @ -340,4 +341,17 @@ public final class Utils { | |||||||
|                 callback |                 callback | ||||||
|         ); |         ); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static void hideKeyboard(final View view) { | ||||||
|  |         if (view == null) return; | ||||||
|  |         final Context context = view.getContext(); | ||||||
|  |         if (context == null) return; | ||||||
|  |         try { | ||||||
|  |             final InputMethodManager manager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); | ||||||
|  |             if (manager == null) return; | ||||||
|  |             manager.hideSoftInputFromWindow(view.getWindowToken(), 0); | ||||||
|  |         } catch (Exception e) { | ||||||
|  |             Log.e(TAG, "hideKeyboard: ", e); | ||||||
|  |         } | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -5,10 +5,14 @@ import android.graphics.drawable.Drawable; | |||||||
| import android.graphics.drawable.GradientDrawable; | import android.graphics.drawable.GradientDrawable; | ||||||
| import android.graphics.drawable.ShapeDrawable; | import android.graphics.drawable.ShapeDrawable; | ||||||
| import android.graphics.drawable.shapes.RoundRectShape; | import android.graphics.drawable.shapes.RoundRectShape; | ||||||
|  | import android.view.View; | ||||||
| import android.widget.FrameLayout; | import android.widget.FrameLayout; | ||||||
|  | import android.widget.TextView; | ||||||
| 
 | 
 | ||||||
| import androidx.annotation.ColorInt; | import androidx.annotation.ColorInt; | ||||||
|  | import androidx.annotation.NonNull; | ||||||
| import androidx.core.content.res.ResourcesCompat; | import androidx.core.content.res.ResourcesCompat; | ||||||
|  | import androidx.core.util.Pair; | ||||||
| 
 | 
 | ||||||
| public final class ViewUtils { | public final class ViewUtils { | ||||||
| 
 | 
 | ||||||
| @ -53,4 +57,16 @@ public final class ViewUtils { | |||||||
|     private static int getSize(float size) { |     private static int getSize(float size) { | ||||||
|         return (int) (size < 0 ? size : Utils.convertDpToPx(size)); |         return (int) (size < 0 ? size : Utils.convertDpToPx(size)); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public static Pair<Integer, Integer> measure(@NonNull final View view, @NonNull final View parent) { | ||||||
|  |         view.measure( | ||||||
|  |                 View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.UNSPECIFIED), | ||||||
|  |                 View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED) | ||||||
|  |         ); | ||||||
|  |         return new Pair<>(view.getMeasuredHeight(), view.getMeasuredWidth()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static float getTextViewValueWidth(final TextView textView, final String text) { | ||||||
|  |         return textView.getPaint().measureText(text); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -14,6 +14,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectInbox; | import awais.instagrabber.repositories.responses.directmessages.DirectInbox; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThread; | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
| import awais.instagrabber.utils.Constants; | import awais.instagrabber.utils.Constants; | ||||||
| import awais.instagrabber.utils.CookieUtils; | import awais.instagrabber.utils.CookieUtils; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| @ -38,6 +39,7 @@ public class DirectInboxViewModel extends ViewModel { | |||||||
|     private long seqId; |     private long seqId; | ||||||
|     private String cursor; |     private String cursor; | ||||||
|     private boolean hasOlder = true; |     private boolean hasOlder = true; | ||||||
|  |     private DirectUser viewer; | ||||||
| 
 | 
 | ||||||
|     public DirectInboxViewModel() { |     public DirectInboxViewModel() { | ||||||
|         final String cookie = settingsHelper.getString(Constants.COOKIE); |         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||||
| @ -76,6 +78,10 @@ public class DirectInboxViewModel extends ViewModel { | |||||||
|         return fetchingInbox; |         return fetchingInbox; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public DirectUser getViewer() { | ||||||
|  |         return viewer; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public void fetchInbox() { |     public void fetchInbox() { | ||||||
|         if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return; |         if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return; | ||||||
|         stopCurrentInboxRequest(); |         stopCurrentInboxRequest(); | ||||||
| @ -108,6 +114,9 @@ public class DirectInboxViewModel extends ViewModel { | |||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         seqId = response.getSeqId(); |         seqId = response.getSeqId(); | ||||||
|  |         if (viewer == null) { | ||||||
|  |             viewer = response.getViewer(); | ||||||
|  |         } | ||||||
|         final DirectInbox inbox = response.getInbox(); |         final DirectInbox inbox = response.getInbox(); | ||||||
|         final List<DirectThread> threads = inbox.getThreads(); |         final List<DirectThread> threads = inbox.getThreads(); | ||||||
|         if (!TextUtils.isEmpty(cursor)) { |         if (!TextUtils.isEmpty(cursor)) { | ||||||
|  | |||||||
| @ -0,0 +1,457 @@ | |||||||
|  | package awais.instagrabber.viewmodels; | ||||||
|  | 
 | ||||||
|  | import android.app.Application; | ||||||
|  | import android.content.res.Resources; | ||||||
|  | import android.util.Log; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.annotation.StringRes; | ||||||
|  | import androidx.core.util.Pair; | ||||||
|  | import androidx.lifecycle.AndroidViewModel; | ||||||
|  | import androidx.lifecycle.LiveData; | ||||||
|  | import androidx.lifecycle.MutableLiveData; | ||||||
|  | 
 | ||||||
|  | import com.google.common.collect.ImmutableList; | ||||||
|  | 
 | ||||||
|  | import org.json.JSONException; | ||||||
|  | import org.json.JSONObject; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.ArrayList; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.R; | ||||||
|  | import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option; | ||||||
|  | import awais.instagrabber.models.Resource; | ||||||
|  | import awais.instagrabber.repositories.responses.FriendshipRepoChangeRootResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.FriendshipRepoRestrictRootResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThread; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | import awais.instagrabber.utils.Constants; | ||||||
|  | import awais.instagrabber.utils.CookieUtils; | ||||||
|  | import awais.instagrabber.utils.TextUtils; | ||||||
|  | import awais.instagrabber.webservices.DirectMessagesService; | ||||||
|  | import awais.instagrabber.webservices.FriendshipService; | ||||||
|  | import awais.instagrabber.webservices.ServiceCallback; | ||||||
|  | import okhttp3.ResponseBody; | ||||||
|  | import retrofit2.Call; | ||||||
|  | import retrofit2.Callback; | ||||||
|  | import retrofit2.Response; | ||||||
|  | 
 | ||||||
|  | import static awais.instagrabber.utils.Utils.settingsHelper; | ||||||
|  | 
 | ||||||
|  | public class DirectSettingsViewModel extends AndroidViewModel { | ||||||
|  |     private static final String TAG = DirectSettingsViewModel.class.getSimpleName(); | ||||||
|  |     private static final String ACTION_KICK = "kick"; | ||||||
|  |     private static final String ACTION_MAKE_ADMIN = "make_admin"; | ||||||
|  |     private static final String ACTION_REMOVE_ADMIN = "remove_admin"; | ||||||
|  |     private static final String ACTION_BLOCK = "block"; | ||||||
|  |     private static final String ACTION_UNBLOCK = "unblock"; | ||||||
|  |     // private static final String ACTION_REPORT = "report"; | ||||||
|  |     private static final String ACTION_RESTRICT = "restrict"; | ||||||
|  |     private static final String ACTION_UNRESTRICT = "unrestrict"; | ||||||
|  | 
 | ||||||
|  |     private final MutableLiveData<Pair<List<DirectUser>, List<DirectUser>>> users = new MutableLiveData<>( | ||||||
|  |             new Pair<>(Collections.emptyList(), Collections.emptyList())); | ||||||
|  |     private final MutableLiveData<String> title = new MutableLiveData<>(""); | ||||||
|  |     private final MutableLiveData<List<Long>> adminUserIds = new MutableLiveData<>(Collections.emptyList()); | ||||||
|  |     private final DirectMessagesService directMessagesService; | ||||||
|  | 
 | ||||||
|  |     private DirectThread thread; | ||||||
|  |     private final String userId; | ||||||
|  |     private boolean viewerIsAdmin; | ||||||
|  |     private final Resources resources; | ||||||
|  |     private final FriendshipService friendshipService; | ||||||
|  |     private final String csrfToken; | ||||||
|  |     private DirectUser viewer; | ||||||
|  | 
 | ||||||
|  |     public DirectSettingsViewModel(final Application application) { | ||||||
|  |         super(application); | ||||||
|  |         final String cookie = settingsHelper.getString(Constants.COOKIE); | ||||||
|  |         userId = CookieUtils.getUserIdFromCookie(cookie); | ||||||
|  |         final String deviceUuid = settingsHelper.getString(Constants.DEVICE_UUID); | ||||||
|  |         csrfToken = CookieUtils.getCsrfTokenFromCookie(cookie); | ||||||
|  |         if (TextUtils.isEmpty(csrfToken) || TextUtils.isEmpty(userId) || TextUtils.isEmpty(deviceUuid)) { | ||||||
|  |             throw new IllegalArgumentException("User is not logged in!"); | ||||||
|  |         } | ||||||
|  |         directMessagesService = DirectMessagesService.getInstance(csrfToken, userId, deviceUuid); | ||||||
|  |         friendshipService = FriendshipService.getInstance(); | ||||||
|  |         resources = getApplication().getResources(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     @NonNull | ||||||
|  |     public DirectThread getThread() { | ||||||
|  |         return thread; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setThread(@NonNull final DirectThread thread) { | ||||||
|  |         this.thread = thread; | ||||||
|  |         List<DirectUser> users = thread.getUsers(); | ||||||
|  |         if (viewer != null) { | ||||||
|  |             final ImmutableList.Builder<DirectUser> builder = ImmutableList.<DirectUser>builder().add(viewer); | ||||||
|  |             if (users != null) { | ||||||
|  |                 builder.addAll(users); | ||||||
|  |             } | ||||||
|  |             users = builder.build(); | ||||||
|  |         } | ||||||
|  |         this.users.postValue(new Pair<>(users, thread.getLeftUsers())); | ||||||
|  |         setTitle(thread.getThreadTitle()); | ||||||
|  |         final List<Long> adminUserIds = thread.getAdminUserIds(); | ||||||
|  |         this.adminUserIds.postValue(adminUserIds); | ||||||
|  |         viewerIsAdmin = adminUserIds.contains(Long.parseLong(userId)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public boolean isGroup() { | ||||||
|  |         if (thread != null) { | ||||||
|  |             return thread.isGroup(); | ||||||
|  |         } | ||||||
|  |         return false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Pair<List<DirectUser>, List<DirectUser>>> getUsers() { | ||||||
|  |         return users; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<String> getTitle() { | ||||||
|  |         return title; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setTitle(final String title) { | ||||||
|  |         if (title == null) { | ||||||
|  |             this.title.postValue(""); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         this.title.postValue(title.trim()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<List<Long>> getAdminUserIds() { | ||||||
|  |         return adminUserIds; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Resource<Object>> updateTitle(final String newTitle) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         final Call<DirectThreadDetailsChangeResponse> addUsersRequest = directMessagesService | ||||||
|  |                 .updateTitle(thread.getThreadId(), newTitle.trim()); | ||||||
|  |         handleDetailsChangeRequest(data, addUsersRequest); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Resource<Object>> addMembers(final Set<DirectUser> users) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         final Call<DirectThreadDetailsChangeResponse> addUsersRequest = directMessagesService | ||||||
|  |                 .addUsers(thread.getThreadId(), users.stream().map(DirectUser::getPk).collect(Collectors.toList())); | ||||||
|  |         handleDetailsChangeRequest(data, addUsersRequest); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Resource<Object>> removeMember(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         final Call<String> request = directMessagesService | ||||||
|  |                 .removeUsers(thread.getThreadId(), Collections.singleton(user.getPk())); | ||||||
|  |         request.enqueue(new Callback<String>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleAdminChangeResponseError(response, data); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 Pair<List<DirectUser>, List<DirectUser>> usersValue = users.getValue(); | ||||||
|  |                 if (usersValue == null) { | ||||||
|  |                     usersValue = new Pair<>(Collections.emptyList(), Collections.emptyList()); | ||||||
|  |                 } | ||||||
|  |                 List<DirectUser> activeUsers = usersValue.first; | ||||||
|  |                 if (activeUsers == null) { | ||||||
|  |                     activeUsers = Collections.emptyList(); | ||||||
|  |                 } | ||||||
|  |                 final List<DirectUser> updatedActiveUsers = activeUsers.stream() | ||||||
|  |                                                                        .filter(user1 -> user1.getPk() != user.getPk()) | ||||||
|  |                                                                        .collect(Collectors.toList()); | ||||||
|  |                 List<DirectUser> leftUsers = usersValue.second; | ||||||
|  |                 if (leftUsers == null) { | ||||||
|  |                     leftUsers = Collections.emptyList(); | ||||||
|  |                 } | ||||||
|  |                 final ImmutableList<DirectUser> updateLeftUsers = ImmutableList.<DirectUser>builder() | ||||||
|  |                         .addAll(leftUsers) | ||||||
|  |                         .add(user) | ||||||
|  |                         .build(); | ||||||
|  |                 users.postValue(new Pair<>(updatedActiveUsers, updateLeftUsers)); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LiveData<Resource<Object>> makeAdmin(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         if (isAdmin(user)) return data; | ||||||
|  |         final Call<String> request = directMessagesService.addAdmins(thread.getThreadId(), Collections.singleton(user.getPk())); | ||||||
|  |         request.enqueue(new Callback<String>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleAdminChangeResponseError(response, data); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final List<Long> currentAdmins = adminUserIds.getValue(); | ||||||
|  |                 adminUserIds.postValue(ImmutableList.<Long>builder() | ||||||
|  |                                                .addAll(currentAdmins != null ? currentAdmins : Collections.emptyList()) | ||||||
|  |                                                .add(user.getPk()) | ||||||
|  |                                                .build()); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LiveData<Resource<Object>> removeAdmin(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         if (!isAdmin(user)) return data; | ||||||
|  |         final Call<String> request = directMessagesService.removeAdmins(thread.getThreadId(), Collections.singleton(user.getPk())); | ||||||
|  |         request.enqueue(new Callback<String>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleAdminChangeResponseError(response, data); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final List<Long> currentAdmins = adminUserIds.getValue(); | ||||||
|  |                 if (currentAdmins == null) return; | ||||||
|  |                 adminUserIds.postValue(currentAdmins.stream() | ||||||
|  |                                                     .filter(userId1 -> userId1 != user.getPk()) | ||||||
|  |                                                     .collect(Collectors.toList())); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleAdminChangeResponseError(@NonNull final Response<String> response, | ||||||
|  |                                                 final MutableLiveData<Resource<Object>> data) { | ||||||
|  |         final ResponseBody errorBody = response.errorBody(); | ||||||
|  |         if (errorBody == null) { | ||||||
|  |             handleErrorResponse(response, data); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  |             final JSONObject json = new JSONObject(errorBody.string()); | ||||||
|  |             if (json.has("message")) { | ||||||
|  |                 data.postValue(Resource.error(json.getString("message"), null)); | ||||||
|  |             } | ||||||
|  |         } catch (IOException | JSONException e) { | ||||||
|  |             Log.e(TAG, "onResponse: ", e); | ||||||
|  |             data.postValue(Resource.error(e.getMessage(), null)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LiveData<Resource<Object>> blockUser(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         friendshipService.block(userId, String.valueOf(user.getPk()), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSuccess(final FriendshipRepoChangeRootResponse result) { | ||||||
|  |                 // refresh thread | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LiveData<Resource<Object>> unblockUser(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         friendshipService.unblock(userId, String.valueOf(user.getPk()), csrfToken, new ServiceCallback<FriendshipRepoChangeRootResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSuccess(final FriendshipRepoChangeRootResponse result) { | ||||||
|  |                 // refresh thread | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LiveData<Resource<Object>> restrictUser(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         friendshipService.toggleRestrict(String.valueOf(user.getPk()), true, csrfToken, new ServiceCallback<FriendshipRepoRestrictRootResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSuccess(final FriendshipRepoRestrictRootResponse result) { | ||||||
|  |                 // refresh thread | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LiveData<Resource<Object>> unRestrictUser(final DirectUser user) { | ||||||
|  |         final MutableLiveData<Resource<Object>> data = new MutableLiveData<>(); | ||||||
|  |         friendshipService.toggleRestrict(String.valueOf(user.getPk()), false, csrfToken, new ServiceCallback<FriendshipRepoRestrictRootResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onSuccess(final FriendshipRepoRestrictRootResponse result) { | ||||||
|  |                 // refresh thread | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         return data; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleDetailsChangeRequest(final MutableLiveData<Resource<Object>> data, | ||||||
|  |                                             final Call<DirectThreadDetailsChangeResponse> addUsersRequest) { | ||||||
|  |         addUsersRequest.enqueue(new Callback<DirectThreadDetailsChangeResponse>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<DirectThreadDetailsChangeResponse> call, | ||||||
|  |                                    @NonNull final Response<DirectThreadDetailsChangeResponse> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleErrorResponse(response, data); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final DirectThreadDetailsChangeResponse addUserResponse = response.body(); | ||||||
|  |                 if (addUserResponse == null) { | ||||||
|  |                     data.postValue(Resource.error("Response is null", null)); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 data.postValue(Resource.success(new Object())); | ||||||
|  |                 final DirectThread thread = addUserResponse.getThread(); | ||||||
|  |                 if (thread != null) { | ||||||
|  |                     setThread(thread); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<DirectThreadDetailsChangeResponse> call, @NonNull final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onFailure: ", t); | ||||||
|  |                 data.postValue(Resource.error(t.getMessage(), null)); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private void handleErrorResponse(@NonNull final Response<?> response, | ||||||
|  |                                      final MutableLiveData<Resource<Object>> data) { | ||||||
|  |         final ResponseBody errorBody = response.errorBody(); | ||||||
|  |         if (errorBody == null) { | ||||||
|  |             data.postValue(Resource.error("Request failed!", null)); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  |             data.postValue(Resource.error(errorBody.string(), null)); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.e(TAG, "onResponse: ", e); | ||||||
|  |             data.postValue(Resource.error(e.getMessage(), null)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public ArrayList<Option<String>> createUserOptions(final DirectUser user) { | ||||||
|  |         final ArrayList<Option<String>> options = new ArrayList<>(); | ||||||
|  |         if (user == null || isSelf(user) || hasLeft(user)) { | ||||||
|  |             return options; | ||||||
|  |         } | ||||||
|  |         if (viewerIsAdmin) { | ||||||
|  |             options.add(new Option<>(getString(R.string.dms_action_kick), ACTION_KICK)); | ||||||
|  | 
 | ||||||
|  |             final boolean isAdmin = isAdmin(user); | ||||||
|  |             options.add(new Option<>( | ||||||
|  |                     isAdmin ? getString(R.string.dms_action_remove_admin) : getString(R.string.dms_action_make_admin), | ||||||
|  |                     isAdmin ? ACTION_REMOVE_ADMIN : ACTION_MAKE_ADMIN | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         final boolean blocking = user.getFriendshipStatus().isBlocking(); | ||||||
|  |         options.add(new Option<>( | ||||||
|  |                 blocking ? getString(R.string.unblock) : getString(R.string.block), | ||||||
|  |                 blocking ? ACTION_UNBLOCK : ACTION_BLOCK | ||||||
|  |         )); | ||||||
|  | 
 | ||||||
|  |         // options.add(new Option<>(getString(R.string.report), ACTION_REPORT)); | ||||||
|  | 
 | ||||||
|  |         if (!isGroup()) { | ||||||
|  |             final boolean restricted = user.getFriendshipStatus().isRestricted(); | ||||||
|  |             options.add(new Option<>( | ||||||
|  |                     restricted ? getString(R.string.unrestrict) : getString(R.string.restrict), | ||||||
|  |                     restricted ? ACTION_UNRESTRICT : ACTION_RESTRICT | ||||||
|  |             )); | ||||||
|  |         } | ||||||
|  |         return options; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean hasLeft(final DirectUser user) { | ||||||
|  |         final Pair<List<DirectUser>, List<DirectUser>> users = this.users.getValue(); | ||||||
|  |         if (users == null || users.second == null) return false; | ||||||
|  |         return users.second.contains(user); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isAdmin(final DirectUser user) { | ||||||
|  |         final List<Long> adminUserIdsValue = adminUserIds.getValue(); | ||||||
|  |         return adminUserIdsValue != null && adminUserIdsValue.contains(user.getPk()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private boolean isSelf(final DirectUser user) { | ||||||
|  |         return user.getPk() == Long.parseLong(userId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private String getString(@StringRes final int resId) { | ||||||
|  |         return resources.getString(resId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Resource<Object>> doAction(final DirectUser user, final String action) { | ||||||
|  |         if (user == null || action == null) return null; | ||||||
|  |         switch (action) { | ||||||
|  |             case ACTION_KICK: | ||||||
|  |                 return removeMember(user); | ||||||
|  |             case ACTION_MAKE_ADMIN: | ||||||
|  |                 return makeAdmin(user); | ||||||
|  |             case ACTION_REMOVE_ADMIN: | ||||||
|  |                 return removeAdmin(user); | ||||||
|  |             case ACTION_BLOCK: | ||||||
|  |                 return blockUser(user); | ||||||
|  |             case ACTION_UNBLOCK: | ||||||
|  |                 return unblockUser(user); | ||||||
|  |             // case ACTION_REPORT: | ||||||
|  |             //     break; | ||||||
|  |             case ACTION_RESTRICT: | ||||||
|  |                 return restrictUser(user); | ||||||
|  |             case ACTION_UNRESTRICT: | ||||||
|  |                 return unRestrictUser(user); | ||||||
|  |             default: | ||||||
|  |                 return null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setViewer(final DirectUser viewer) { | ||||||
|  |         this.viewer = viewer; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -0,0 +1,155 @@ | |||||||
|  | package awais.instagrabber.viewmodels; | ||||||
|  | 
 | ||||||
|  | import android.util.Log; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | import androidx.lifecycle.LiveData; | ||||||
|  | import androidx.lifecycle.MutableLiveData; | ||||||
|  | import androidx.lifecycle.ViewModel; | ||||||
|  | 
 | ||||||
|  | import java.io.IOException; | ||||||
|  | import java.util.Arrays; | ||||||
|  | import java.util.Collections; | ||||||
|  | import java.util.HashSet; | ||||||
|  | import java.util.List; | ||||||
|  | import java.util.Set; | ||||||
|  | import java.util.stream.Collectors; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.models.Resource; | ||||||
|  | import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||||
|  | import awais.instagrabber.repositories.responses.directmessages.DirectUser; | ||||||
|  | import awais.instagrabber.utils.Debouncer; | ||||||
|  | import awais.instagrabber.webservices.UserService; | ||||||
|  | import okhttp3.ResponseBody; | ||||||
|  | import retrofit2.Call; | ||||||
|  | import retrofit2.Callback; | ||||||
|  | import retrofit2.Response; | ||||||
|  | 
 | ||||||
|  | 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 final MutableLiveData<Resource<List<DirectUser>>> users = new MutableLiveData<>(); | ||||||
|  |     private final MutableLiveData<Boolean> showAction = new MutableLiveData<>(false); | ||||||
|  |     private final Debouncer<String> searchDebouncer; | ||||||
|  |     private final Set<DirectUser> selectedUsers = new HashSet<>(); | ||||||
|  |     private final UserService userService; | ||||||
|  |     private long[] hideUserIds; | ||||||
|  | 
 | ||||||
|  |     public UserSearchViewModel() { | ||||||
|  |         userService = UserService.getInstance(); | ||||||
|  |         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); | ||||||
|  |                 prevQuery = currentQuery; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onError(final Throwable t) { | ||||||
|  |                 Log.e(TAG, "onError: ", t); | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |         searchDebouncer = new Debouncer<>(searchCallback, 1000); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Resource<List<DirectUser>>> 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>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<UserSearchResponse> call, @NonNull final Response<UserSearchResponse> response) { | ||||||
|  |                 if (!response.isSuccessful()) { | ||||||
|  |                     handleErrorResponse(response); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 final UserSearchResponse userSearchResponse = response.body(); | ||||||
|  |                 if (userSearchResponse == null) return; | ||||||
|  |                 handleResponse(userSearchResponse); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<UserSearchResponse> call, @NonNull final Throwable t) { | ||||||
|  | 
 | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     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 handleErrorResponse(final Response<UserSearchResponse> response) { | ||||||
|  |         final ResponseBody errorBody = response.errorBody(); | ||||||
|  |         if (errorBody == null) { | ||||||
|  |             users.postValue(Resource.error("Request failed!", Collections.emptyList())); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         String errorString; | ||||||
|  |         try { | ||||||
|  |             errorString = errorBody.string(); | ||||||
|  |             Log.e(TAG, "handleErrorResponse: " + errorString); | ||||||
|  |         } catch (IOException e) { | ||||||
|  |             Log.e(TAG, "handleErrorResponse: ", e); | ||||||
|  |             errorString = e.getMessage(); | ||||||
|  |         } | ||||||
|  |         users.postValue(Resource.error(errorString, Collections.emptyList())); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setSelectedUser(final DirectUser user, final boolean selected) { | ||||||
|  |         if (selected) { | ||||||
|  |             selectedUsers.add(user); | ||||||
|  |         } else { | ||||||
|  |             selectedUsers.remove(user); | ||||||
|  |         } | ||||||
|  |         showAction.postValue(!selectedUsers.isEmpty()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Set<DirectUser> getSelectedUsers() { | ||||||
|  |         return selectedUsers; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void clearResults() { | ||||||
|  |         users.postValue(Resource.success(Collections.emptyList())); | ||||||
|  |         prevQuery = ""; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void cancelSearch() { | ||||||
|  |         searchDebouncer.cancel(DEBOUNCE_KEY); | ||||||
|  |         if (searchRequest != null) { | ||||||
|  |             searchRequest.cancel(); | ||||||
|  |             searchRequest = null; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public LiveData<Boolean> showAction() { | ||||||
|  |         return showAction; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void setHideUserIds(final long[] hideUserIds) { | ||||||
|  |         this.hideUserIds = hideUserIds; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @ -7,6 +7,7 @@ import com.google.common.collect.ImmutableMap; | |||||||
| import org.json.JSONArray; | import org.json.JSONArray; | ||||||
| 
 | 
 | ||||||
| import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||||
|  | import java.util.Collection; | ||||||
| import java.util.HashMap; | import java.util.HashMap; | ||||||
| import java.util.List; | import java.util.List; | ||||||
| import java.util.Map; | import java.util.Map; | ||||||
| @ -25,6 +26,7 @@ import awais.instagrabber.repositories.requests.directmessages.VoiceBroadcastOpt | |||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; | ||||||
| import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; | 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.DirectThreadFeedResponse; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import awais.instagrabber.utils.Utils; | import awais.instagrabber.utils.Utils; | ||||||
| @ -175,4 +177,54 @@ public class DirectMessagesService extends BaseService { | |||||||
|         final Map<String, String> signedForm = Utils.sign(form); |         final Map<String, String> signedForm = Utils.sign(form); | ||||||
|         return repository.broadcast(broadcastOptions.getItemType().getValue(), signedForm); |         return repository.broadcast(broadcastOptions.getItemType().getValue(), signedForm); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|  |     public Call<DirectThreadDetailsChangeResponse> addUsers(final String threadId, | ||||||
|  |                                                             final Collection<Long> userIds) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "_csrftoken", csrfToken, | ||||||
|  |                 "_uuid", deviceUuid, | ||||||
|  |                 "user_ids", new JSONArray(userIds).toString() | ||||||
|  |         ); | ||||||
|  |         return repository.addUsers(threadId, form); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<String> removeUsers(final String threadId, | ||||||
|  |                                     final Collection<Long> userIds) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "_csrftoken", csrfToken, | ||||||
|  |                 "_uuid", deviceUuid, | ||||||
|  |                 "user_ids", new JSONArray(userIds).toString() | ||||||
|  |         ); | ||||||
|  |         return repository.removeUsers(threadId, form); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<DirectThreadDetailsChangeResponse> updateTitle(final String threadId, | ||||||
|  |                                                                final String title) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "_csrftoken", csrfToken, | ||||||
|  |                 "_uuid", deviceUuid, | ||||||
|  |                 "title", title | ||||||
|  |         ); | ||||||
|  |         return repository.updateTitle(threadId, form); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<String> addAdmins(final String threadId, | ||||||
|  |                                   final Collection<Long> userIds) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "_csrftoken", csrfToken, | ||||||
|  |                 "_uuid", deviceUuid, | ||||||
|  |                 "user_ids", new JSONArray(userIds).toString() | ||||||
|  |         ); | ||||||
|  |         return repository.addAdmins(threadId, form); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<String> removeAdmins(final String threadId, | ||||||
|  |                                      final Collection<Long> userIds) { | ||||||
|  |         final ImmutableMap<String, String> form = ImmutableMap.of( | ||||||
|  |                 "_csrftoken", csrfToken, | ||||||
|  |                 "_uuid", deviceUuid, | ||||||
|  |                 "user_ids", new JSONArray(userIds).toString() | ||||||
|  |         ); | ||||||
|  |         return repository.removeAdmins(threadId, form); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -17,8 +17,6 @@ import java.util.List; | |||||||
| import awais.instagrabber.models.FeedModel; | import awais.instagrabber.models.FeedModel; | ||||||
| import awais.instagrabber.repositories.ProfileRepository; | import awais.instagrabber.repositories.ProfileRepository; | ||||||
| import awais.instagrabber.repositories.responses.PostsFetchResponse; | import awais.instagrabber.repositories.responses.PostsFetchResponse; | ||||||
| import awais.instagrabber.repositories.responses.UserInfo; |  | ||||||
| import awais.instagrabber.utils.Constants; |  | ||||||
| import awais.instagrabber.utils.ResponseBodyUtils; | import awais.instagrabber.utils.ResponseBodyUtils; | ||||||
| import awais.instagrabber.utils.TextUtils; | import awais.instagrabber.utils.TextUtils; | ||||||
| import retrofit2.Call; | import retrofit2.Call; | ||||||
| @ -47,39 +45,6 @@ public class ProfileService extends BaseService { | |||||||
|         return instance; |         return instance; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void getUserInfo(final String uid, final ServiceCallback<UserInfo> callback) { |  | ||||||
|         final Call<String> request = repository.getUserInfo(uid); |  | ||||||
|         request.enqueue(new Callback<String>() { |  | ||||||
|             @Override |  | ||||||
|             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { |  | ||||||
|                 final String body = response.body(); |  | ||||||
|                 if (body == null) return; |  | ||||||
|                 try { |  | ||||||
|                     final JSONObject jsonObject = new JSONObject(body); |  | ||||||
|                     final JSONObject user = jsonObject.optJSONObject(Constants.EXTRAS_USER); |  | ||||||
|                     if (user == null) return; |  | ||||||
|                     // Log.d(TAG, "user: " + user.toString()); |  | ||||||
|                     final UserInfo userInfo = new UserInfo( |  | ||||||
|                             uid, |  | ||||||
|                             user.getString(Constants.EXTRAS_USERNAME), |  | ||||||
|                             user.optString("full_name"), |  | ||||||
|                             user.optString("profile_pic_url"), |  | ||||||
|                             user.has("hd_profile_pic_url_info") |  | ||||||
|                                     ? user.getJSONObject("hd_profile_pic_url_info").optString("url") : null |  | ||||||
|                     ); |  | ||||||
|                     callback.onSuccess(userInfo); |  | ||||||
|                 } catch (JSONException e) { |  | ||||||
|                     Log.e(TAG, "Error parsing json", e); |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
| 
 |  | ||||||
|             @Override |  | ||||||
|             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { |  | ||||||
|                 callback.onFailure(t); |  | ||||||
|             } |  | ||||||
|         }); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     public void fetchPosts(final String userId, |     public void fetchPosts(final String userId, | ||||||
|                            final String maxId, |                            final String maxId, | ||||||
|                            final ServiceCallback<PostsFetchResponse> callback) { |                            final ServiceCallback<PostsFetchResponse> callback) { | ||||||
|  | |||||||
| @ -160,8 +160,8 @@ public class StoriesService extends BaseService { | |||||||
|                                 highlightNode.getString("title"), |                                 highlightNode.getString("title"), | ||||||
|                                 highlightNode.getString(Constants.EXTRAS_ID), |                                 highlightNode.getString(Constants.EXTRAS_ID), | ||||||
|                                 highlightNode.getJSONObject("cover_media") |                                 highlightNode.getJSONObject("cover_media") | ||||||
|                                         .getJSONObject("cropped_image_version") |                                              .getJSONObject("cropped_image_version") | ||||||
|                                         .getString("url"), |                                              .getString("url"), | ||||||
|                                 highlightNode.getLong("latest_reel_media"), |                                 highlightNode.getLong("latest_reel_media"), | ||||||
|                                 highlightNode.getInt("media_count") |                                 highlightNode.getInt("media_count") | ||||||
|                         )); |                         )); | ||||||
| @ -188,7 +188,7 @@ public class StoriesService extends BaseService { | |||||||
|         form.put("include_suggested_highlights", "false"); |         form.put("include_suggested_highlights", "false"); | ||||||
|         form.put("is_in_archive_home", "true"); |         form.put("is_in_archive_home", "true"); | ||||||
|         form.put("include_cover", "1"); |         form.put("include_cover", "1"); | ||||||
|         form.put("timezone_offset", String.valueOf(TimeZone.getDefault().getRawOffset() / 1000)); |         form.put("timezone_offset", String.valueOf((float) TimeZone.getDefault().getRawOffset() / 1000)); | ||||||
|         if (!TextUtils.isEmpty(maxId)) { |         if (!TextUtils.isEmpty(maxId)) { | ||||||
|             form.put("max_id", maxId); // NOT TESTED |             form.put("max_id", maxId); // NOT TESTED | ||||||
|         } |         } | ||||||
| @ -338,40 +338,40 @@ public class StoriesService extends BaseService { | |||||||
| 
 | 
 | ||||||
|     // RespondAction.java |     // RespondAction.java | ||||||
|     public void respondToQuestion(final String storyId, |     public void respondToQuestion(final String storyId, | ||||||
|                                    final String stickerId, |                                   final String stickerId, | ||||||
|                                    final String answer, |                                   final String answer, | ||||||
|                                    final String userId, |                                   final String userId, | ||||||
|                                    final String csrfToken, |                                   final String csrfToken, | ||||||
|                                    final ServiceCallback<StoryStickerResponse> callback) { |                                   final ServiceCallback<StoryStickerResponse> callback) { | ||||||
|         respondToSticker(storyId, stickerId, "story_question_response", "response", answer, userId, csrfToken, callback); |         respondToSticker(storyId, stickerId, "story_question_response", "response", answer, userId, csrfToken, callback); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // QuizAction.java |     // QuizAction.java | ||||||
|     public void respondToQuiz(final String storyId, |     public void respondToQuiz(final String storyId, | ||||||
|                                final String stickerId, |                               final String stickerId, | ||||||
|                                final int answer, |                               final int answer, | ||||||
|                                final String userId, |                               final String userId, | ||||||
|                                final String csrfToken, |                               final String csrfToken, | ||||||
|                                final ServiceCallback<StoryStickerResponse> callback) { |                               final ServiceCallback<StoryStickerResponse> callback) { | ||||||
|         respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), userId, csrfToken, callback); |         respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), userId, csrfToken, callback); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // VoteAction.java |     // VoteAction.java | ||||||
|     public void respondToPoll(final String storyId, |     public void respondToPoll(final String storyId, | ||||||
|                                final String stickerId, |                               final String stickerId, | ||||||
|                                final int answer, |                               final int answer, | ||||||
|                                final String userId, |                               final String userId, | ||||||
|                                final String csrfToken, |                               final String csrfToken, | ||||||
|                                final ServiceCallback<StoryStickerResponse> callback) { |                               final ServiceCallback<StoryStickerResponse> callback) { | ||||||
|         respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); |         respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public void respondToSlider(final String storyId, |     public void respondToSlider(final String storyId, | ||||||
|                               final String stickerId, |                                 final String stickerId, | ||||||
|                               final double answer, |                                 final double answer, | ||||||
|                               final String userId, |                                 final String userId, | ||||||
|                               final String csrfToken, |                                 final String csrfToken, | ||||||
|                               final ServiceCallback<StoryStickerResponse> callback) { |                                 final ServiceCallback<StoryStickerResponse> callback) { | ||||||
|         respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); |         respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -0,0 +1,79 @@ | |||||||
|  | package awais.instagrabber.webservices; | ||||||
|  | 
 | ||||||
|  | import android.util.Log; | ||||||
|  | 
 | ||||||
|  | import androidx.annotation.NonNull; | ||||||
|  | 
 | ||||||
|  | import org.json.JSONException; | ||||||
|  | import org.json.JSONObject; | ||||||
|  | 
 | ||||||
|  | import java.util.TimeZone; | ||||||
|  | 
 | ||||||
|  | import awais.instagrabber.repositories.UserRepository; | ||||||
|  | import awais.instagrabber.repositories.responses.UserInfo; | ||||||
|  | import awais.instagrabber.repositories.responses.UserSearchResponse; | ||||||
|  | import awais.instagrabber.utils.Constants; | ||||||
|  | import retrofit2.Call; | ||||||
|  | import retrofit2.Callback; | ||||||
|  | import retrofit2.Response; | ||||||
|  | import retrofit2.Retrofit; | ||||||
|  | 
 | ||||||
|  | public class UserService extends BaseService { | ||||||
|  |     private static final String TAG = UserService.class.getSimpleName(); | ||||||
|  | 
 | ||||||
|  |     private final UserRepository repository; | ||||||
|  | 
 | ||||||
|  |     private static UserService instance; | ||||||
|  | 
 | ||||||
|  |     private UserService() { | ||||||
|  |         final Retrofit retrofit = getRetrofitBuilder() | ||||||
|  |                 .baseUrl("https://i.instagram.com") | ||||||
|  |                 .build(); | ||||||
|  |         repository = retrofit.create(UserRepository.class); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static UserService getInstance() { | ||||||
|  |         if (instance == null) { | ||||||
|  |             instance = new UserService(); | ||||||
|  |         } | ||||||
|  |         return instance; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public void getUserInfo(final String uid, final ServiceCallback<UserInfo> callback) { | ||||||
|  |         final Call<String> request = repository.getUserInfo(uid); | ||||||
|  |         request.enqueue(new Callback<String>() { | ||||||
|  |             @Override | ||||||
|  |             public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) { | ||||||
|  |                 final String body = response.body(); | ||||||
|  |                 if (body == null) return; | ||||||
|  |                 try { | ||||||
|  |                     final JSONObject jsonObject = new JSONObject(body); | ||||||
|  |                     final JSONObject user = jsonObject.optJSONObject(Constants.EXTRAS_USER); | ||||||
|  |                     if (user == null) return; | ||||||
|  |                     // Log.d(TAG, "user: " + user.toString()); | ||||||
|  |                     final UserInfo userInfo = new UserInfo( | ||||||
|  |                             uid, | ||||||
|  |                             user.getString(Constants.EXTRAS_USERNAME), | ||||||
|  |                             user.optString("full_name"), | ||||||
|  |                             user.optString("profile_pic_url"), | ||||||
|  |                             user.has("hd_profile_pic_url_info") | ||||||
|  |                             ? user.getJSONObject("hd_profile_pic_url_info").optString("url") : null | ||||||
|  |                     ); | ||||||
|  |                     callback.onSuccess(userInfo); | ||||||
|  |                 } catch (JSONException e) { | ||||||
|  |                     Log.e(TAG, "Error parsing json", e); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             @Override | ||||||
|  |             public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) { | ||||||
|  |                 callback.onFailure(t); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public Call<UserSearchResponse> search(final String query) { | ||||||
|  |         final float timezoneOffset = (float) TimeZone.getDefault().getRawOffset() / 1000; | ||||||
|  |         return repository.search(timezoneOffset, query); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/color/ic_circle_check_tint.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/color/ic_circle_check_tint.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <selector xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <item android:color="@color/blue_400" android:state_selected="true" /> | ||||||
|  |     <item android:color="?colorControlNormal" /> | ||||||
|  | </selector> | ||||||
							
								
								
									
										21
									
								
								app/src/main/res/drawable/bg_user_search_input.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/src/main/res/drawable/bg_user_search_input.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,21 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <item | ||||||
|  |         android:bottom="1dp" | ||||||
|  |         android:left="-2dp" | ||||||
|  |         android:right="-2dp" | ||||||
|  |         android:top="-2dp"> | ||||||
|  |         <shape android:shape="rectangle"> | ||||||
|  |             <stroke | ||||||
|  |                 android:width="1dp" | ||||||
|  |                 android:color="?colorControlNormal" /> | ||||||
|  | 
 | ||||||
|  |             <solid android:color="#00FFFFFF" /> | ||||||
|  | 
 | ||||||
|  |             <!--<padding android:left="0dp"--> | ||||||
|  |             <!--    android:right="0dp"--> | ||||||
|  |             <!--    android:top="0dp"--> | ||||||
|  |             <!--    android:bottom="0dp" />--> | ||||||
|  |         </shape> | ||||||
|  |     </item> | ||||||
|  | </layer-list> | ||||||
							
								
								
									
										5
									
								
								app/src/main/res/drawable/ic_circle_check.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								app/src/main/res/drawable/ic_circle_check.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <selector xmlns:android="http://schemas.android.com/apk/res/android"> | ||||||
|  |     <item android:drawable="@drawable/ic_round_check_circle_24" android:state_selected="true" /> | ||||||
|  |     <item android:drawable="@drawable/ic_radio_button_unchecked_24" /> | ||||||
|  | </selector> | ||||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_radio_button_unchecked_24.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/drawable/ic_radio_button_unchecked_24.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="24" | ||||||
|  |     android:viewportHeight="24" | ||||||
|  |     android:tint="?attr/colorControlNormal"> | ||||||
|  |   <path | ||||||
|  |       android:fillColor="@android:color/white" | ||||||
|  |       android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/> | ||||||
|  | </vector> | ||||||
							
								
								
									
										10
									
								
								app/src/main/res/drawable/ic_round_check_circle_24.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/drawable/ic_round_check_circle_24.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | <vector xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     android:width="24dp" | ||||||
|  |     android:height="24dp" | ||||||
|  |     android:viewportWidth="24" | ||||||
|  |     android:viewportHeight="24" | ||||||
|  |     android:tint="?attr/colorControlNormal"> | ||||||
|  |   <path | ||||||
|  |       android:fillColor="@android:color/white" | ||||||
|  |       android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM9.29,16.29L5.7,12.7c-0.39,-0.39 -0.39,-1.02 0,-1.41 0.39,-0.39 1.02,-0.39 1.41,0L10,14.17l6.88,-6.88c0.39,-0.39 1.02,-0.39 1.41,0 0.39,0.39 0.39,1.02 0,1.41l-7.59,7.59c-0.38,0.39 -1.02,0.39 -1.41,0z"/> | ||||||
|  | </vector> | ||||||
| @ -1,94 +1,154 @@ | |||||||
| <?xml version="1.0" encoding="utf-8"?> | <?xml version="1.0" encoding="utf-8"?> | ||||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|     android:layout_width="match_parent" |     android:layout_width="match_parent" | ||||||
|     android:layout_height="match_parent" |     android:layout_height="match_parent" | ||||||
|     android:orientation="vertical"> |     android:focusableInTouchMode="true"> | ||||||
| 
 | 
 | ||||||
|     <LinearLayout |     <com.google.android.material.appbar.AppBarLayout | ||||||
|         android:layout_width="match_parent" |  | ||||||
|         android:layout_height="wrap_content"> |  | ||||||
| 
 |  | ||||||
|         <EditText |  | ||||||
|             android:id="@+id/titleText" |  | ||||||
|             android:layout_width="0dp" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_weight="1" |  | ||||||
|             android:gravity="bottom" |  | ||||||
|             android:importantForAutofill="no" |  | ||||||
|             android:inputType="textMultiLine" |  | ||||||
|             android:maxLength="2200" |  | ||||||
|             android:maxLines="10" |  | ||||||
|             android:paddingStart="8dp" |  | ||||||
|             android:paddingLeft="8dp" |  | ||||||
|             android:paddingEnd="4dp" |  | ||||||
|             android:paddingRight="4dp" |  | ||||||
|             android:scrollHorizontally="false" /> |  | ||||||
| 
 |  | ||||||
|         <androidx.appcompat.widget.AppCompatImageView |  | ||||||
|             android:id="@+id/titleSend" |  | ||||||
|             android:layout_width="wrap_content" |  | ||||||
|             android:layout_height="wrap_content" |  | ||||||
|             android:layout_gravity="center" |  | ||||||
|             android:background="?selectableItemBackgroundBorderless" |  | ||||||
|             android:clickable="true" |  | ||||||
|             android:focusable="true" |  | ||||||
|             android:paddingStart="4dp" |  | ||||||
|             android:paddingLeft="4dp" |  | ||||||
|             android:paddingTop="4dp" |  | ||||||
|             android:paddingEnd="8dp" |  | ||||||
|             android:paddingRight="8dp" |  | ||||||
|             android:paddingBottom="4dp" |  | ||||||
|             android:visibility="gone" |  | ||||||
|             app:srcCompat="@drawable/ic_submit" /> |  | ||||||
|     </LinearLayout> |  | ||||||
| 
 |  | ||||||
|     <androidx.appcompat.widget.AppCompatButton |  | ||||||
|         android:id="@+id/btnLeave" |  | ||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="wrap_content" |         android:layout_height="wrap_content" | ||||||
|         android:layout_marginLeft="8dp" |         android:background="@null" | ||||||
|         android:layout_marginStart="6dp" |         android:elevation="0dp" | ||||||
|         android:layout_marginRight="8dp" |         app:elevation="0dp"> | ||||||
|         android:text="@string/dms_action_leave" |  | ||||||
|         android:textColor="@color/btn_red_text_color" |  | ||||||
|         android:textSize="18sp" |  | ||||||
|         app:backgroundTint="@color/btn_red_background" /> |  | ||||||
| 
 | 
 | ||||||
|     <androidx.swiperefreshlayout.widget.SwipeRefreshLayout |         <com.google.android.material.appbar.CollapsingToolbarLayout | ||||||
|         android:id="@+id/swipeRefreshLayout" |  | ||||||
|         android:layout_width="match_parent" |  | ||||||
|         android:layout_height="0dp" |  | ||||||
|         android:layout_weight="1"> |  | ||||||
| 
 |  | ||||||
|         <androidx.core.widget.NestedScrollView |  | ||||||
|             android:layout_width="match_parent" |             android:layout_width="match_parent" | ||||||
|             android:layout_height="match_parent"> |             android:layout_height="wrap_content" | ||||||
|             <LinearLayout |             app:layout_scrollFlags="scroll"> | ||||||
|  | 
 | ||||||
|  |             <androidx.constraintlayout.widget.ConstraintLayout | ||||||
|  |                 android:id="@+id/settings_parent" | ||||||
|                 android:layout_width="match_parent" |                 android:layout_width="match_parent" | ||||||
|                 android:layout_height="match_parent" |                 android:layout_height="wrap_content"> | ||||||
|                 android:orientation="vertical"> | 
 | ||||||
|                 <androidx.recyclerview.widget.RecyclerView |                 <com.google.android.material.textfield.TextInputLayout | ||||||
|                     android:id="@+id/userList" |                     android:id="@+id/title_edit_input_layout" | ||||||
|                     android:layout_width="match_parent" |  | ||||||
|                     android:layout_height="match_parent" |  | ||||||
|                     android:nestedScrollingEnabled="false"/> |  | ||||||
|                 <androidx.appcompat.widget.AppCompatTextView |  | ||||||
|                     android:id="@+id/leftTitle" |  | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="wrap_content" |                     android:layout_height="wrap_content" | ||||||
|  |                     android:animateLayoutChanges="true" | ||||||
|  |                     android:hint="@string/title" | ||||||
|                     android:visibility="gone" |                     android:visibility="gone" | ||||||
|  |                     app:boxBackgroundColor="@android:color/transparent" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                     app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                     app:layout_constraintTop_toTopOf="parent" | ||||||
|  |                     app:suffixText="@string/save" | ||||||
|  |                     app:suffixTextColor="@color/blue_600" | ||||||
|  |                     tools:visibility="visible"> | ||||||
|  | 
 | ||||||
|  |                     <com.google.android.material.textfield.TextInputEditText | ||||||
|  |                         android:id="@+id/title_edit" | ||||||
|  |                         android:layout_width="match_parent" | ||||||
|  |                         android:layout_height="wrap_content" | ||||||
|  |                         android:autofillHints="no" | ||||||
|  |                         android:inputType="text" | ||||||
|  |                         android:maxLength="2200" | ||||||
|  |                         android:scrollHorizontally="false" | ||||||
|  |                         tools:text="test" /> | ||||||
|  | 
 | ||||||
|  |                 </com.google.android.material.textfield.TextInputLayout> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.button.MaterialButton | ||||||
|  |                     android:id="@+id/mute_messages_label" | ||||||
|  |                     style="@style/Widget.MaterialComponents.Button.TextButton" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="0dp" | ||||||
|                     android:gravity="center_vertical" |                     android:gravity="center_vertical" | ||||||
|                     android:padding="5dp" |                     android:paddingStart="16dp" | ||||||
|                     android:text="@string/dms_left_users" |                     android:paddingEnd="16dp" | ||||||
|  |                     android:text="@string/mute_messages" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" | ||||||
|                     android:textColor="?android:textColorPrimary" |                     android:textColor="?android:textColorPrimary" | ||||||
|                     android:textSize="24sp"/> |                     app:layout_constraintBottom_toBottomOf="@id/mute_messages" | ||||||
|                 <androidx.recyclerview.widget.RecyclerView |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|                     android:id="@+id/leftUserList" |                     app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                     app:layout_constraintTop_toTopOf="@id/mute_messages" /> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.switchmaterial.SwitchMaterial | ||||||
|  |                     android:id="@+id/mute_messages" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:paddingStart="0dp" | ||||||
|  |                     android:paddingEnd="8dp" | ||||||
|  |                     app:layout_constraintBottom_toTopOf="@id/mute_mentions" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/title_edit_input_layout" /> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.button.MaterialButton | ||||||
|  |                     android:id="@+id/mute_mentions_label" | ||||||
|  |                     style="@style/Widget.MaterialComponents.Button.TextButton" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="0dp" | ||||||
|  |                     android:gravity="center_vertical" | ||||||
|  |                     android:paddingStart="16dp" | ||||||
|  |                     android:paddingEnd="16dp" | ||||||
|  |                     android:text="@string/mute_mentions" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" | ||||||
|  |                     android:textColor="?android:textColorPrimary" | ||||||
|  |                     app:layout_constraintBottom_toBottomOf="@id/mute_mentions" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                     app:layout_constraintStart_toStartOf="parent" | ||||||
|  |                     app:layout_constraintTop_toTopOf="@id/mute_mentions" /> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.switchmaterial.SwitchMaterial | ||||||
|  |                     android:id="@+id/mute_mentions" | ||||||
|  |                     android:layout_width="wrap_content" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:paddingStart="0dp" | ||||||
|  |                     android:paddingEnd="8dp" | ||||||
|  |                     app:layout_constraintBottom_toTopOf="@id/leave" | ||||||
|  |                     app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/mute_messages" /> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.button.MaterialButton | ||||||
|  |                     android:id="@+id/leave" | ||||||
|  |                     style="@style/Widget.MaterialComponents.Button.TextButton" | ||||||
|                     android:layout_width="match_parent" |                     android:layout_width="match_parent" | ||||||
|                     android:layout_height="match_parent" |                     android:layout_height="wrap_content" | ||||||
|                     android:nestedScrollingEnabled="false"/> |                     android:gravity="center_vertical" | ||||||
|             </LinearLayout> |                     android:paddingStart="16dp" | ||||||
|         </androidx.core.widget.NestedScrollView> |                     android:paddingTop="8dp" | ||||||
|     </androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |                     android:paddingEnd="16dp" | ||||||
| </LinearLayout> |                     android:paddingBottom="8dp" | ||||||
|  |                     android:text="@string/dms_action_leave" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" | ||||||
|  |                     android:textColor="@color/red_600" | ||||||
|  |                     app:layout_constraintBottom_toTopOf="@id/add_members" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/mute_mentions" /> | ||||||
|  | 
 | ||||||
|  |                 <com.google.android.material.button.MaterialButton | ||||||
|  |                     android:id="@+id/add_members" | ||||||
|  |                     style="@style/Widget.MaterialComponents.Button.TextButton" | ||||||
|  |                     android:layout_width="match_parent" | ||||||
|  |                     android:layout_height="wrap_content" | ||||||
|  |                     android:gravity="center_vertical" | ||||||
|  |                     android:paddingStart="16dp" | ||||||
|  |                     android:paddingTop="8dp" | ||||||
|  |                     android:paddingEnd="16dp" | ||||||
|  |                     android:paddingBottom="8dp" | ||||||
|  |                     android:text="@string/add_members" | ||||||
|  |                     android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" | ||||||
|  |                     android:textColor="?android:textColorPrimary" | ||||||
|  |                     app:icon="@drawable/ic_add" | ||||||
|  |                     app:iconTint="?android:textColorPrimary" | ||||||
|  |                     app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |                     app:layout_constraintTop_toBottomOf="@id/leave" /> | ||||||
|  | 
 | ||||||
|  |                 <androidx.constraintlayout.widget.Group | ||||||
|  |                     android:id="@+id/group_settings" | ||||||
|  |                     android:layout_width="0dp" | ||||||
|  |                     android:layout_height="0dp" | ||||||
|  |                     app:constraint_referenced_ids="title_edit_input_layout, mute_mentions_label, mute_mentions, leave, add_members" /> | ||||||
|  |             </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
|  | 
 | ||||||
|  |         </com.google.android.material.appbar.CollapsingToolbarLayout> | ||||||
|  |     </com.google.android.material.appbar.AppBarLayout> | ||||||
|  | 
 | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/users" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="match_parent" | ||||||
|  |         app:layout_behavior="@string/appbar_scrolling_view_behavior" /> | ||||||
|  | </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||||
							
								
								
									
										51
									
								
								app/src/main/res/layout/fragment_user_search.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								app/src/main/res/layout/fragment_user_search.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,51 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="match_parent" | ||||||
|  |     android:focusableInTouchMode="true"> | ||||||
|  | 
 | ||||||
|  |     <com.google.android.material.chip.ChipGroup | ||||||
|  |         android:id="@+id/group" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:background="@drawable/bg_user_search_input" | ||||||
|  |         android:padding="10dp" | ||||||
|  |         app:layout_constraintBottom_toTopOf="@id/results" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent"> | ||||||
|  | 
 | ||||||
|  |         <androidx.appcompat.widget.AppCompatEditText | ||||||
|  |             android:id="@+id/search" | ||||||
|  |             android:layout_width="match_parent" | ||||||
|  |             android:layout_height="48dp" | ||||||
|  |             android:background="@android:color/transparent" | ||||||
|  |             android:gravity="center_vertical" | ||||||
|  |             android:hint="@string/search" | ||||||
|  |             android:importantForAutofill="no" | ||||||
|  |             android:inputType="textNoSuggestions" | ||||||
|  |             android:maxLines="1" | ||||||
|  |             tools:text="test" /> | ||||||
|  |     </com.google.android.material.chip.ChipGroup> | ||||||
|  | 
 | ||||||
|  |     <androidx.recyclerview.widget.RecyclerView | ||||||
|  |         android:id="@+id/results" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         app:layout_constraintBottom_toTopOf="@id/done" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@id/group" /> | ||||||
|  | 
 | ||||||
|  |     <com.google.android.material.button.MaterialButton | ||||||
|  |         android:id="@+id/done" | ||||||
|  |         android:layout_width="match_parent" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginStart="8dp" | ||||||
|  |         android:layout_marginEnd="8dp" | ||||||
|  |         android:text="@string/done" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@id/results" | ||||||
|  |         tools:visibility="visible" /> | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
| @ -17,7 +17,6 @@ | |||||||
|         android:layout_width="match_parent" |         android:layout_width="match_parent" | ||||||
|         android:layout_height="60dp" |         android:layout_height="60dp" | ||||||
|         android:layout_marginStart="66dp" |         android:layout_marginStart="66dp" | ||||||
|         android:layout_marginLeft="66dp" |  | ||||||
|         android:layout_marginEnd="26dp" |         android:layout_marginEnd="26dp" | ||||||
|         android:orientation="vertical"> |         android:orientation="vertical"> | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										95
									
								
								app/src/main/res/layout/layout_dm_user_item.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								app/src/main/res/layout/layout_dm_user_item.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | |||||||
|  | <?xml version="1.0" encoding="utf-8"?> | ||||||
|  | <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||||
|  |     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||||
|  |     xmlns:tools="http://schemas.android.com/tools" | ||||||
|  |     android:layout_width="match_parent" | ||||||
|  |     android:layout_height="72dp" | ||||||
|  |     android:background="?android:selectableItemBackground" | ||||||
|  |     android:clickable="true" | ||||||
|  |     android:focusable="true" | ||||||
|  |     android:orientation="horizontal" | ||||||
|  |     android:paddingStart="16dp" | ||||||
|  |     android:paddingEnd="16dp"> | ||||||
|  | 
 | ||||||
|  |     <com.facebook.drawee.view.SimpleDraweeView | ||||||
|  |         android:id="@+id/profile_pic" | ||||||
|  |         android:layout_width="@dimen/dm_inbox_avatar_size" | ||||||
|  |         android:layout_height="@dimen/dm_inbox_avatar_size" | ||||||
|  |         app:actualImageScaleType="centerCrop" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/full_name" | ||||||
|  |         app:layout_constraintHorizontal_chainStyle="spread_inside" | ||||||
|  |         app:layout_constraintStart_toStartOf="parent" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:roundAsCircle="true" | ||||||
|  |         tools:background="@mipmap/ic_launcher" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |         android:id="@+id/full_name" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:layout_marginStart="16dp" | ||||||
|  |         android:ellipsize="end" | ||||||
|  |         android:gravity="bottom" | ||||||
|  |         android:singleLine="true" | ||||||
|  |         android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1" | ||||||
|  |         app:layout_constraintBottom_toTopOf="@id/username" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/info" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/profile_pic" | ||||||
|  |         app:layout_constraintTop_toTopOf="parent" | ||||||
|  |         app:layout_constraintVertical_chainStyle="packed" | ||||||
|  |         tools:text="Long name......................." /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |         android:id="@+id/info" | ||||||
|  |         android:layout_width="wrap_content" | ||||||
|  |         android:layout_height="0dp" | ||||||
|  |         android:gravity="bottom" | ||||||
|  |         android:paddingStart="4dp" | ||||||
|  |         android:paddingEnd="0dp" | ||||||
|  |         android:singleLine="true" | ||||||
|  |         android:textAppearance="@style/TextAppearance.MaterialComponents.Caption" | ||||||
|  |         android:textColor="@color/blue_400" | ||||||
|  |         app:layout_constraintBaseline_toBaselineOf="@id/thread_title" | ||||||
|  |         app:layout_constraintBottom_toTopOf="@id/username" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/select" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/full_name" | ||||||
|  |         app:layout_constraintTop_toTopOf="@id/full_name" | ||||||
|  |         tools:text="Admin" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatTextView | ||||||
|  |         android:id="@+id/username" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="wrap_content" | ||||||
|  |         android:ellipsize="end" | ||||||
|  |         android:maxLines="1" | ||||||
|  |         android:textAppearance="@style/TextAppearance.AppCompat.Caption" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toStartOf="@id/select" | ||||||
|  |         app:layout_constraintStart_toStartOf="@id/full_name" | ||||||
|  |         app:layout_constraintTop_toBottomOf="@id/full_name" | ||||||
|  |         tools:text="username" /> | ||||||
|  | 
 | ||||||
|  |     <androidx.appcompat.widget.AppCompatImageView | ||||||
|  |         android:id="@+id/select" | ||||||
|  |         android:layout_width="30dp" | ||||||
|  |         android:layout_height="30dp" | ||||||
|  |         android:layout_marginStart="4dp" | ||||||
|  |         android:duplicateParentState="true" | ||||||
|  |         android:visibility="gone" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="@id/profile_pic" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toEndOf="@id/info" | ||||||
|  |         app:layout_constraintTop_toTopOf="@id/profile_pic" | ||||||
|  |         app:srcCompat="@drawable/ic_circle_check" | ||||||
|  |         app:tint="@color/ic_circle_check_tint" | ||||||
|  |         tools:visibility="visible" /> | ||||||
|  | 
 | ||||||
|  |     <include | ||||||
|  |         layout="@layout/item_pref_divider" | ||||||
|  |         android:layout_width="0dp" | ||||||
|  |         android:layout_height="1dp" | ||||||
|  |         app:layout_constraintBottom_toBottomOf="parent" | ||||||
|  |         app:layout_constraintEnd_toEndOf="parent" | ||||||
|  |         app:layout_constraintStart_toStartOf="@id/thread_title" /> | ||||||
|  | </androidx.constraintlayout.widget.ConstraintLayout> | ||||||
| @ -73,14 +73,38 @@ | |||||||
|     <action |     <action | ||||||
|         android:id="@+id/action_global_likesViewerFragment" |         android:id="@+id/action_global_likesViewerFragment" | ||||||
|         app:destination="@id/likes_nav_graph"> |         app:destination="@id/likes_nav_graph"> | ||||||
|     <argument |         <argument | ||||||
|         android:name="postId" |             android:name="postId" | ||||||
|         app:argType="string" |             app:argType="string" | ||||||
|         app:nullable="false" /> |             app:nullable="false" /> | ||||||
|     <argument |         <argument | ||||||
|         android:name="isComment" |             android:name="isComment" | ||||||
|         app:argType="boolean" |             app:argType="boolean" | ||||||
|         app:nullable="false" /> |             app:nullable="false" /> | ||||||
|  |     </action> | ||||||
|  | 
 | ||||||
|  |     <include app:graph="@navigation/user_search_nav_graph" /> | ||||||
|  | 
 | ||||||
|  |     <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="title" | ||||||
|  |             app:argType="string" | ||||||
|  |             app:nullable="true" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="action_label" | ||||||
|  |             app:argType="string" | ||||||
|  |             app:nullable="true" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="hideUserIds" | ||||||
|  |             app:argType="long[]" /> | ||||||
|     </action> |     </action> | ||||||
| 
 | 
 | ||||||
|     <fragment |     <fragment | ||||||
| @ -112,20 +136,23 @@ | |||||||
|     <fragment |     <fragment | ||||||
|         android:id="@+id/directMessagesSettingsFragment" |         android:id="@+id/directMessagesSettingsFragment" | ||||||
|         android:name="awais.instagrabber.fragments.directmessages.DirectMessageSettingsFragment" |         android:name="awais.instagrabber.fragments.directmessages.DirectMessageSettingsFragment" | ||||||
|         android:label="@string/action_settings" |         android:label="@string/details" | ||||||
|         tools:layout="@layout/fragment_direct_messages_settings"> |         tools:layout="@layout/fragment_direct_messages_settings"> | ||||||
|         <argument |         <argument | ||||||
|             android:name="threadId" |             android:name="threadId" | ||||||
|             app:argType="string" /> |             app:argType="string" /> | ||||||
|         <argument |         <action | ||||||
|             android:name="title" |             android:id="@+id/action_settings_to_inbox" | ||||||
|             app:argType="string" /> |             app:destination="@id/directMessagesInboxFragment" | ||||||
|  |             app:popUpTo="@+id/directMessagesInboxFragment" | ||||||
|  |             app:popUpToInclusive="true" /> | ||||||
|  | 
 | ||||||
|     </fragment> |     </fragment> | ||||||
|     <fragment |     <fragment | ||||||
|         android:id="@+id/imageEditFragment" |         android:id="@+id/imageEditFragment" | ||||||
|         android:name="awais.instagrabber.fragments.imageedit.ImageEditFragment" |         android:name="awais.instagrabber.fragments.imageedit.ImageEditFragment" | ||||||
|         tools:layout="@layout/fragment_image_edit" |         android:label="Edit Photo" | ||||||
|         android:label="Edit Photo"> |         tools:layout="@layout/fragment_image_edit"> | ||||||
|         <argument |         <argument | ||||||
|             android:name="uri" |             android:name="uri" | ||||||
|             app:argType="android.net.Uri" |             app:argType="android.net.Uri" | ||||||
|  | |||||||
							
								
								
									
										48
									
								
								app/src/main/res/navigation/user_search_nav_graph.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								app/src/main/res/navigation/user_search_nav_graph.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | |||||||
|  | <?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" | ||||||
|  |     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"> | ||||||
|  |         <argument | ||||||
|  |             android:name="multiple" | ||||||
|  |             app:argType="boolean" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="title" | ||||||
|  |             app:argType="string" | ||||||
|  |             app:nullable="true" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="action_label" | ||||||
|  |             app:argType="string" | ||||||
|  |             app:nullable="true" /> | ||||||
|  |         <argument | ||||||
|  |             android:name="hideUserIds" | ||||||
|  |             app:argType="long[]" /> | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     </fragment> | ||||||
|  | 
 | ||||||
|  |     <!--<action | ||||||
|  |         android:id="@+id/action_global_user_search" | ||||||
|  |         app:destination="@id/user_search"> | ||||||
|  |         <argument | ||||||
|  |             android:name="multiple" | ||||||
|  |             app:argType="boolean" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="title" | ||||||
|  |             app:argType="string" | ||||||
|  |             app:nullable="true" /> | ||||||
|  | 
 | ||||||
|  |         <argument | ||||||
|  |             android:name="action_label" | ||||||
|  |             app:argType="string" | ||||||
|  |             app:nullable="true" /> | ||||||
|  |     </action>--> | ||||||
|  | </navigation> | ||||||
| @ -176,7 +176,7 @@ | |||||||
|     <string name="dms_inbox_raven_media_screenshot">Screenshotted</string> |     <string name="dms_inbox_raven_media_screenshot">Screenshotted</string> | ||||||
|     <string name="dms_inbox_raven_media_cant_deliver">Cannot deliver</string> |     <string name="dms_inbox_raven_media_cant_deliver">Cannot deliver</string> | ||||||
|     <string name="dms_action_success">Great success!</string> |     <string name="dms_action_success">Great success!</string> | ||||||
|     <string name="dms_action_leave">Leave</string> |     <string name="dms_action_leave">Leave chat</string> | ||||||
|     <string name="dms_action_leave_question">Leave this chat?</string> |     <string name="dms_action_leave_question">Leave this chat?</string> | ||||||
|     <string name="dms_action_kick">Kick</string> |     <string name="dms_action_kick">Kick</string> | ||||||
|     <string name="dms_left_users">Left users</string> |     <string name="dms_left_users">Left users</string> | ||||||
| @ -378,4 +378,16 @@ | |||||||
|         <item quantity="other">%s stories</item> |         <item quantity="other">%s stories</item> | ||||||
|     </plurals> |     </plurals> | ||||||
|     <string name="download_permission">Storage permission not granted!</string> |     <string name="download_permission">Storage permission not granted!</string> | ||||||
|  |     <string name="details">Details</string> | ||||||
|  |     <string name="title">Title</string> | ||||||
|  |     <string name="members">Members</string> | ||||||
|  |     <string name="admin">Admin</string> | ||||||
|  |     <string name="inviter">Inviter</string> | ||||||
|  |     <string name="mute_messages">Mute messages</string> | ||||||
|  |     <string name="mute_mentions">Mute mentions</string> | ||||||
|  |     <string name="add_members">Add members</string> | ||||||
|  |     <string name="search">Search</string> | ||||||
|  |     <string name="done">Done</string> | ||||||
|  |     <string name="dms_action_make_admin">Make Admin</string> | ||||||
|  |     <string name="dms_action_remove_admin">Remove as Admin</string> | ||||||
| </resources> | </resources> | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user