Ammar Githam
4 years ago
44 changed files with 2758 additions and 460 deletions
-
5app/src/main/java/awais/instagrabber/activities/MainActivity.java
-
47app/src/main/java/awais/instagrabber/adapters/DirectMessageMembersAdapter.java
-
183app/src/main/java/awais/instagrabber/adapters/DirectUsersAdapter.java
-
85app/src/main/java/awais/instagrabber/adapters/UserSearchResultsAdapter.java
-
102app/src/main/java/awais/instagrabber/adapters/viewholder/DirectUserViewHolder.java
-
76app/src/main/java/awais/instagrabber/customviews/VerticalImageSpan.java
-
236app/src/main/java/awais/instagrabber/dialogs/MultiOptionDialogFragment.java
-
14app/src/main/java/awais/instagrabber/dialogs/ProfilePicDialogFragment.java
-
270app/src/main/java/awais/instagrabber/fragments/UserSearchFragment.java
-
6app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageInboxFragment.java
-
535app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageSettingsFragment.java
-
9app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java
-
6app/src/main/java/awais/instagrabber/fragments/settings/MorePreferencesFragment.java
-
26app/src/main/java/awais/instagrabber/repositories/DirectMessagesRepository.java
-
3app/src/main/java/awais/instagrabber/repositories/ProfileRepository.java
-
17app/src/main/java/awais/instagrabber/repositories/UserRepository.java
-
35app/src/main/java/awais/instagrabber/repositories/responses/UserSearchResponse.java
-
9app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectInboxResponse.java
-
12app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThread.java
-
19app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectThreadDetailsChangeResponse.java
-
23app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectUser.java
-
18app/src/main/java/awais/instagrabber/repositories/responses/directmessages/DirectUserFriendshipStatus.java
-
90app/src/main/java/awais/instagrabber/utils/Debouncer.java
-
14app/src/main/java/awais/instagrabber/utils/Utils.java
-
16app/src/main/java/awais/instagrabber/utils/ViewUtils.java
-
9app/src/main/java/awais/instagrabber/viewmodels/DirectInboxViewModel.java
-
457app/src/main/java/awais/instagrabber/viewmodels/DirectSettingsViewModel.java
-
155app/src/main/java/awais/instagrabber/viewmodels/UserSearchViewModel.java
-
52app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java
-
35app/src/main/java/awais/instagrabber/webservices/ProfileService.java
-
46app/src/main/java/awais/instagrabber/webservices/StoriesService.java
-
79app/src/main/java/awais/instagrabber/webservices/UserService.java
-
5app/src/main/res/color/ic_circle_check_tint.xml
-
21app/src/main/res/drawable/bg_user_search_input.xml
-
5app/src/main/res/drawable/ic_circle_check.xml
-
10app/src/main/res/drawable/ic_radio_button_unchecked_24.xml
-
10app/src/main/res/drawable/ic_round_check_circle_24.xml
-
214app/src/main/res/layout/fragment_direct_messages_settings.xml
-
51app/src/main/res/layout/fragment_user_search.xml
-
1app/src/main/res/layout/item_follow.xml
-
95app/src/main/res/layout/layout_dm_user_item.xml
-
55app/src/main/res/navigation/direct_messages_nav_graph.xml
-
48app/src/main/res/navigation/user_search_nav_graph.xml
-
14app/src/main/res/values/strings.xml
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
@ -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); |
||||
|
} |
||||
|
} |
@ -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> |
@ -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> |
@ -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> |
@ -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> |
@ -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_width="match_parent" |
||||
android:layout_height="wrap_content"> |
|
||||
|
android:layout_height="wrap_content" |
||||
|
android:background="@null" |
||||
|
android:elevation="0dp" |
||||
|
app:elevation="0dp"> |
||||
|
|
||||
<EditText |
|
||||
android:id="@+id/titleText" |
|
||||
android:layout_width="0dp" |
|
||||
|
<com.google.android.material.appbar.CollapsingToolbarLayout |
||||
|
android:layout_width="match_parent" |
||||
android:layout_height="wrap_content" |
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" /> |
|
||||
|
app:layout_scrollFlags="scroll"> |
||||
|
|
||||
<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.constraintlayout.widget.ConstraintLayout |
||||
|
android:id="@+id/settings_parent" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content"> |
||||
|
|
||||
<androidx.appcompat.widget.AppCompatButton |
|
||||
android:id="@+id/btnLeave" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="wrap_content" |
|
||||
android:layout_marginLeft="8dp" |
|
||||
android:layout_marginStart="6dp" |
|
||||
android:layout_marginRight="8dp" |
|
||||
android:text="@string/dms_action_leave" |
|
||||
android:textColor="@color/btn_red_text_color" |
|
||||
android:textSize="18sp" |
|
||||
app:backgroundTint="@color/btn_red_background" /> |
|
||||
|
<com.google.android.material.textfield.TextInputLayout |
||||
|
android:id="@+id/title_edit_input_layout" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:animateLayoutChanges="true" |
||||
|
android:hint="@string/title" |
||||
|
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"> |
||||
|
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
|
||||
android:id="@+id/swipeRefreshLayout" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="0dp" |
|
||||
android:layout_weight="1"> |
|
||||
|
<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" /> |
||||
|
|
||||
<androidx.core.widget.NestedScrollView |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="match_parent"> |
|
||||
<LinearLayout |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="match_parent" |
|
||||
android:orientation="vertical"> |
|
||||
<androidx.recyclerview.widget.RecyclerView |
|
||||
android:id="@+id/userList" |
|
||||
|
</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:paddingStart="16dp" |
||||
|
android:paddingEnd="16dp" |
||||
|
android:text="@string/mute_messages" |
||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1" |
||||
|
android:textColor="?android:textColorPrimary" |
||||
|
app:layout_constraintBottom_toBottomOf="@id/mute_messages" |
||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||
|
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:nestedScrollingEnabled="false"/> |
|
||||
<androidx.appcompat.widget.AppCompatTextView |
|
||||
android:id="@+id/leftTitle" |
|
||||
|
android:layout_height="wrap_content" |
||||
|
android:gravity="center_vertical" |
||||
|
android:paddingStart="16dp" |
||||
|
android:paddingTop="8dp" |
||||
|
android:paddingEnd="16dp" |
||||
|
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_width="match_parent" |
||||
android:layout_height="wrap_content" |
android:layout_height="wrap_content" |
||||
android:visibility="gone" |
|
||||
android:gravity="center_vertical" |
android:gravity="center_vertical" |
||||
android:padding="5dp" |
|
||||
android:text="@string/dms_left_users" |
|
||||
|
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" |
android:textColor="?android:textColorPrimary" |
||||
android:textSize="24sp"/> |
|
||||
<androidx.recyclerview.widget.RecyclerView |
|
||||
android:id="@+id/leftUserList" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="match_parent" |
|
||||
android:nestedScrollingEnabled="false"/> |
|
||||
</LinearLayout> |
|
||||
</androidx.core.widget.NestedScrollView> |
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |
|
||||
</LinearLayout> |
|
||||
|
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> |
@ -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> |
@ -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> |
@ -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> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue