1
0
mirror of https://github.com/KokaKiwi/BarInsta synced 2024-11-23 07:07:30 +00:00

Update DM settings page

This commit is contained in:
Ammar Githam 2021-01-03 18:55:19 +09:00
parent 093ccc9f00
commit e1d8e02630
44 changed files with 2764 additions and 466 deletions

View File

@ -73,6 +73,7 @@ import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.FlavorTown; import awais.instagrabber.utils.FlavorTown;
import awais.instagrabber.utils.IntentUtils; import awais.instagrabber.utils.IntentUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils;
import awais.instagrabber.utils.emoji.EmojiParser; import awais.instagrabber.utils.emoji.EmojiParser;
import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController; import static awais.instagrabber.utils.NavigationExtensions.setupWithNavController;
@ -477,6 +478,10 @@ public class MainActivity extends BaseLanguageActivity implements FragmentManage
@SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack(); @SuppressLint("RestrictedApi") final Deque<NavBackStackEntry> backStack = navController.getBackStack();
setupMenu(backStack.size(), destinationId); setupMenu(backStack.size(), destinationId);
binding.bottomNavView.setVisibility(SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId) ? View.VISIBLE : View.GONE); binding.bottomNavView.setVisibility(SHOW_BOTTOM_VIEW_DESTINATIONS.contains(destinationId) ? View.VISIBLE : View.GONE);
// explicitly hide keyboard when we navigate
final View view = getCurrentFocus();
Utils.hideKeyboard(view);
}); });
} }

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -9,7 +9,6 @@ import android.graphics.drawable.ColorDrawable;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -20,7 +19,6 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
import com.facebook.drawee.backends.pipeline.Fresco; import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener; import com.facebook.drawee.controller.BaseControllerListener;
@ -32,17 +30,14 @@ import java.io.File;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.asyncs.ProfilePictureFetcher; import awais.instagrabber.asyncs.ProfilePictureFetcher;
import awais.instagrabber.databinding.DialogProfilepicBinding; import awais.instagrabber.databinding.DialogProfilepicBinding;
import awais.instagrabber.db.entities.Account;
import awais.instagrabber.db.repositories.RepositoryCallback;
import awais.instagrabber.interfaces.FetchListener; import awais.instagrabber.interfaces.FetchListener;
import awais.instagrabber.models.StoryModel;
import awais.instagrabber.repositories.responses.UserInfo; import awais.instagrabber.repositories.responses.UserInfo;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.DownloadUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.UserService;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -128,8 +123,8 @@ public class ProfilePicDialogFragment extends DialogFragment {
private void fetchAvatar() { private void fetchAvatar() {
if (isLoggedIn) { if (isLoggedIn) {
final ProfileService profileService = ProfileService.getInstance(); final UserService userService = UserService.getInstance();
profileService.getUserInfo(id, new ServiceCallback<UserInfo>() { userService.getUserInfo(id, new ServiceCallback<UserInfo>() {
@Override @Override
public void onSuccess(final UserInfo result) { public void onSuccess(final UserInfo result) {
if (result != null) { if (result != null) {
@ -144,8 +139,9 @@ public class ProfilePicDialogFragment extends DialogFragment {
getDialog().dismiss(); getDialog().dismiss();
} }
}); });
return;
} }
else new ProfilePictureFetcher(name, id, fetchListener, fallbackUrl, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); new ProfilePictureFetcher(name, id, fetchListener, fallbackUrl, false).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
} }
private void setupPhoto() { private void setupPhoto() {

View File

@ -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;
}
}

View File

@ -15,6 +15,8 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer; import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@ -54,7 +56,9 @@ public class DirectMessageInboxFragment extends Fragment implements SwipeRefresh
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
fragmentActivity = (MainActivity) getActivity(); fragmentActivity = (MainActivity) getActivity();
if (fragmentActivity != null) { if (fragmentActivity != null) {
viewModel = new ViewModelProvider(fragmentActivity).get(DirectInboxViewModel.class); final NavController navController = NavHostFragment.findNavController(this);
final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
viewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
} }
} }

View File

@ -1,126 +1,117 @@
package awais.instagrabber.fragments.directmessages; package awais.instagrabber.fragments.directmessages;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar; import androidx.core.util.Pair;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController;
import androidx.navigation.NavDestination;
import androidx.navigation.NavDirections;
import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import java.io.DataOutputStream; import com.google.android.material.snackbar.Snackbar;
import java.net.HttpURLConnection;
import java.net.URL; import java.util.ArrayList;
import java.net.URLEncoder; import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import awais.instagrabber.R; import awais.instagrabber.R;
import awais.instagrabber.broadcasts.DMRefreshBroadcastReceiver; import awais.instagrabber.adapters.DirectUsersAdapter;
import awais.instagrabber.customviews.helpers.TextWatcherAdapter;
import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding; import awais.instagrabber.databinding.FragmentDirectMessagesSettingsBinding;
import awais.instagrabber.models.ProfileModel; import awais.instagrabber.dialogs.MultiOptionDialogFragment;
import awais.instagrabber.utils.Constants; import awais.instagrabber.dialogs.MultiOptionDialogFragment.Option;
import awais.instagrabber.utils.Utils; import awais.instagrabber.models.Resource;
import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.DirectUser;
import awais.instagrabber.viewmodels.DirectInboxViewModel;
import awais.instagrabber.viewmodels.DirectSettingsViewModel;
public class DirectMessageSettingsFragment extends Fragment implements SwipeRefreshLayout.OnRefreshListener { public class DirectMessageSettingsFragment extends Fragment {
private static final String TAG = "DirectMsgsSettingsFrag"; private static final String TAG = DirectMessageSettingsFragment.class.getSimpleName();
private AppCompatActivity fragmentActivity; private FragmentDirectMessagesSettingsBinding binding;
private RecyclerView userList; private DirectSettingsViewModel viewModel;
private RecyclerView leftUserList; private DirectUsersAdapter usersAdapter;
private EditText titleText; private List<Option<String>> options;
private View leftTitle;
private AppCompatImageView titleSend;
private String threadId;
private String threadTitle;
private final String cookie = Utils.settingsHelper.getString(Constants.COOKIE);
// private AsyncTask<Void, Void, InboxThreadModel> currentlyRunning;
private View.OnClickListener clickListener;
private View.OnClickListener basicClickListener;
// private final FetchListener<InboxThreadModel> fetchListener = new FetchListener<InboxThreadModel>() {
// @Override
// public void doBefore() {}
//
// @Override
// public void onResult(final InboxThreadModel threadModel) {
// if (threadModel == null) return;
// final List<Long> adminList = threadModel.getAdmins();
// final String userIdFromCookie = CookieUtils.getUserIdFromCookie(cookie);
// if (userIdFromCookie == null) return;
// final boolean amAdmin = adminList.contains(Long.parseLong(userIdFromCookie));
// final DirectMessageMembersAdapter memberAdapter = new DirectMessageMembersAdapter(threadModel.getUsers(),
// adminList,
// amAdmin ? clickListener : basicClickListener);
// userList.setAdapter(memberAdapter);
// if (threadModel.getLeftUsers() != null && threadModel.getLeftUsers().size() > 0) {
// leftTitle.setVisibility(View.VISIBLE);
// final DirectMessageMembersAdapter leftAdapter = new DirectMessageMembersAdapter(threadModel.getLeftUsers(),
// null,
// basicClickListener);
// leftUserList.setAdapter(leftAdapter);
// }
// }
// };
@Override @Override
public void onCreate(@Nullable final Bundle savedInstanceState) { public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
fragmentActivity = (AppCompatActivity) requireActivity(); final NavController navController = NavHostFragment.findNavController(this);
basicClickListener = v -> { final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
final Object tag = v.getTag(); final DirectInboxViewModel inboxViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
if (tag instanceof ProfileModel) { final List<DirectThread> threads = inboxViewModel.getThreads().getValue();
ProfileModel model = (ProfileModel) tag; final Bundle arguments = getArguments();
final Bundle bundle = new Bundle(); if (arguments == null) {
bundle.putString("username", "@" + model.getUsername()); navController.navigateUp();
NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle); return;
} }
}; final DirectMessageSettingsFragmentArgs fragmentArgs = DirectMessageSettingsFragmentArgs.fromBundle(arguments);
final String threadId = fragmentArgs.getThreadId();
clickListener = v -> { final Optional<DirectThread> first = threads != null ? threads.stream()
final Object tag = v.getTag(); .filter(thread -> thread.getThreadId().equals(threadId))
if (tag instanceof ProfileModel) { .findFirst()
ProfileModel model = (ProfileModel) tag; : Optional.empty();
final Context context = getContext(); if (!first.isPresent()) {
if (context == null) return; navController.navigateUp();
final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[]{ return;
getString(R.string.open_profile), }
getString(R.string.dms_action_kick), viewModel = new ViewModelProvider(this).get(DirectSettingsViewModel.class);
}); viewModel.setViewer(inboxViewModel.getViewer());
final DialogInterface.OnClickListener clickListener = (d, w) -> { viewModel.setThread(first.get());
if (w == 0) { // basicClickListener = v -> {
final Bundle bundle = new Bundle(); // final Object tag = v.getTag();
bundle.putString("username", "@" + model.getUsername()); // if (tag instanceof ProfileModel) {
NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle); // ProfileModel model = (ProfileModel) tag;
} else if (w == 1) { // final Bundle bundle = new Bundle();
new ChangeSettings(titleText.getText().toString()).execute("remove_users", model.getId()); // bundle.putString("username", "@" + model.getUsername());
onRefresh(); // NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle);
} // }
}; // };
new AlertDialog.Builder(context) //
.setAdapter(adapter, clickListener) // clickListener = v -> {
.show(); // final Object tag = v.getTag();
} // if (tag instanceof ProfileModel) {
}; // ProfileModel model = (ProfileModel) tag;
// final Context context = getContext();
// if (context == null) return;
// final ArrayAdapter<String> adapter = new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, new String[]{
// getString(R.string.open_profile),
// getString(R.string.dms_action_kick),
// });
// final DialogInterface.OnClickListener clickListener = (d, w) -> {
// if (w == 0) {
// final Bundle bundle = new Bundle();
// bundle.putString("username", "@" + model.getUsername());
// NavHostFragment.findNavController(this).navigate(R.id.action_global_profileFragment, bundle);
// } else if (w == 1) {
// new ChangeSettings(titleText.getText().toString()).execute("remove_users", model.getId());
// onRefresh();
// }
// };
// new AlertDialog.Builder(context)
// .setAdapter(adapter, clickListener)
// .show();
// }
// };
} }
@NonNull @NonNull
@ -128,155 +119,239 @@ public class DirectMessageSettingsFragment extends Fragment implements SwipeRefr
public View onCreateView(@NonNull final LayoutInflater inflater, public View onCreateView(@NonNull final LayoutInflater inflater,
final ViewGroup container, final ViewGroup container,
final Bundle savedInstanceState) { final Bundle savedInstanceState) {
final FragmentDirectMessagesSettingsBinding binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false); binding = FragmentDirectMessagesSettingsBinding.inflate(inflater, container, false);
final LinearLayout root = binding.getRoot(); // final String threadId = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getThreadId();
final Context context = getContext(); // threadTitle = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getTitle();
if (context == null) return root; // binding.swipeRefreshLayout.setEnabled(false);
final LinearLayoutManager layoutManager = new LinearLayoutManager(context) {
@Override
public boolean canScrollVertically() {
return false;
}
};
final LinearLayoutManager layoutManagerDos = new LinearLayoutManager(context) {
@Override
public boolean canScrollVertically() {
return false;
}
};
if (getArguments() == null) {
return root;
}
threadId = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getThreadId();
threadTitle = DirectMessageSettingsFragmentArgs.fromBundle(getArguments()).getTitle();
binding.swipeRefreshLayout.setEnabled(false);
final ActionBar actionBar = fragmentActivity.getSupportActionBar(); // final ActionBar actionBar = fragmentActivity.getSupportActionBar();
if (actionBar != null) { // if (actionBar != null) {
actionBar.setTitle(threadTitle); // actionBar.setTitle(threadTitle);
} // }
userList = binding.userList; // titleSend.setOnClickListener(v -> new ChangeSettings(titleText.getText().toString()).execute("update_title"));
userList.setHasFixedSize(true);
userList.setLayoutManager(layoutManager);
leftUserList = binding.leftUserList; // binding.titleText.addTextChangedListener(new TextWatcherAdapter() {
leftUserList.setHasFixedSize(true); // @Override
leftUserList.setLayoutManager(layoutManagerDos); // public void onTextChanged(CharSequence s, int start, int before, int count) {
// binding.titleSend.setVisibility(s.toString().equals(threadTitle) ? View.GONE : View.VISIBLE);
// }
// });
leftTitle = binding.leftTitle; // final AppCompatButton btnLeave = binding.btnLeave;
// btnLeave.setOnClickListener(v -> new AlertDialog.Builder(context)
titleText = binding.titleText; // .setTitle(R.string.dms_action_leave_question)
titleText.setText(threadTitle); // .setPositiveButton(R.string.yes, (x, y) -> new ChangeSettings(titleText.getText().toString()).execute("leave"))
// .setNegativeButton(R.string.no, null)
titleSend = binding.titleSend; // .show());
titleSend.setOnClickListener(v -> new ChangeSettings(titleText.getText().toString()).execute("update_title"));
titleText.addTextChangedListener(new TextWatcher() {
@Override
public void afterTextChanged(Editable s) {}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
titleSend.setVisibility(s.toString().equals(threadTitle) ? View.GONE : View.VISIBLE);
}
});
final AppCompatButton btnLeave = binding.btnLeave;
btnLeave.setOnClickListener(v -> new AlertDialog.Builder(context)
.setTitle(R.string.dms_action_leave_question)
.setPositiveButton(R.string.yes,
(x, y) -> new ChangeSettings(titleText.getText().toString()).execute("leave"))
.setNegativeButton(R.string.no, null)
.show());
// currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); // currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute();
return root; return binding.getRoot();
} }
@Override @Override
public void onRefresh() { public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
stopCurrentExecutor(); init();
// currentlyRunning = new DirectMessageInboxThreadFetcher(threadId, null, null, fetchListener).execute(); setupObservers();
} }
private void stopCurrentExecutor() { private void setupObservers() {
// if (currentlyRunning != null) { viewModel.getUsers().observe(getViewLifecycleOwner(), users -> {
// try { if (usersAdapter == null) return;
// currentlyRunning.cancel(true); usersAdapter.submitUsers(users.first, users.second);
// } catch (final Exception e) { });
// if (BuildConfig.DEBUG) { viewModel.getTitle().observe(getViewLifecycleOwner(), title -> binding.titleEdit.setText(title));
// Log.e(TAG, "", e); viewModel.getAdminUserIds().observe(getViewLifecycleOwner(), adminUserIds -> {
// } if (usersAdapter == null) return;
// } usersAdapter.setAdminUserIds(adminUserIds);
// } });
} final NavController navController = NavHostFragment.findNavController(this);
final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry();
class ChangeSettings extends AsyncTask<String, Void, Void> { if (backStackEntry != null) {
String action, argument; final MutableLiveData<Object> resultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result");
boolean ok = false; resultLiveData.observe(getViewLifecycleOwner(), result -> {
private final String text; LiveData<Resource<Object>> detailsChangeResourceLiveData = null;
if ((result instanceof DirectUser)) {
public ChangeSettings(final String text) { // Log.d(TAG, "result: " + result);
this.text = text; detailsChangeResourceLiveData = viewModel.addMembers(Collections.singleton((DirectUser) result));
} } else if ((result instanceof Set)) {
try {
protected Void doInBackground(String... rawAction) { // Log.d(TAG, "result: " + result);
action = rawAction[0]; //noinspection unchecked
if (rawAction.length == 2) argument = rawAction[1]; detailsChangeResourceLiveData = viewModel.addMembers((Set<DirectUser>) result);
final String url = "https://i.instagram.com/api/v1/direct_v2/threads/" + threadId + "/" + action + "/"; } catch (Exception e) {
try { Log.e(TAG, "search users result: ", e);
String urlParameters = "_csrftoken=" + cookie.split("csrftoken=")[1].split(";")[0] }
+ "&_uuid=" + Utils.settingsHelper.getString(Constants.DEVICE_UUID);
if (action.equals("update_title")) {
urlParameters += "&title=" + URLEncoder.encode(text, "UTF-8")
.replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'")
.replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~");
} else if (action.startsWith("remove_users"))
urlParameters += ("&user_ids=[" + argument + "]");
final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
urlConnection.setRequestMethod("POST");
urlConnection.setUseCaches(false);
urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT);
urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length));
urlConnection.setDoOutput(true);
DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream());
wr.writeBytes(urlParameters);
wr.flush();
wr.close();
urlConnection.connect();
if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
ok = true;
} }
urlConnection.disconnect(); if (detailsChangeResourceLiveData != null) {
} catch (Throwable ex) { observeDetailsChange(detailsChangeResourceLiveData);
Log.e("austin_debug", "unsend: " + ex); }
});
}
}
private void init() {
setupSettings();
setupMembers();
}
private void setupSettings() {
binding.groupSettings.setVisibility(viewModel.isGroup() ? View.VISIBLE : View.GONE);
if (!viewModel.isGroup()) return;
binding.titleEdit.addTextChangedListener(new TextWatcherAdapter() {
@Override
public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
if (s.toString().trim().equals(viewModel.getTitle().getValue())) {
binding.titleEditInputLayout.setSuffixText(null);
return;
}
binding.titleEditInputLayout.setSuffixText(getString(R.string.save));
} }
return null; });
} binding.titleEditInputLayout.getSuffixTextView().setOnClickListener(v -> {
final Editable text = binding.titleEdit.getText();
@Override if (text == null) return;
protected void onPostExecute(Void result) { final String newTitle = text.toString().trim();
final Context context = getContext(); if (newTitle.equals(viewModel.getTitle().getValue())) return;
if (context == null) return; observeDetailsChange(viewModel.updateTitle(newTitle));
if (ok) { });
Toast.makeText(context, R.string.dms_action_success, Toast.LENGTH_SHORT).show(); binding.addMembers.setOnClickListener(v -> {
if (action.equals("update_title")) { if (!isAdded()) return;
threadTitle = titleText.getText().toString(); final NavController navController = NavHostFragment.findNavController(this);
titleSend.setVisibility(View.GONE); final NavDestination currentDestination = navController.getCurrentDestination();
titleText.clearFocus(); if (currentDestination == null) return;
DirectMessageThreadFragment.hasSentSomething = true; if (currentDestination.getId() != R.id.directMessagesSettingsFragment) return;
} else if (action.equals("leave")) { final Pair<List<DirectUser>, List<DirectUser>> users = viewModel.getUsers().getValue();
context.sendBroadcast(new Intent(DMRefreshBroadcastReceiver.ACTION_REFRESH_DM)); final long[] currentUserIds;
NavHostFragment.findNavController(DirectMessageSettingsFragment.this).popBackStack(R.id.directMessagesInboxFragment, false); if (users != null && users.first != null) {
} else { final List<DirectUser> currentMembers = users.first;
DirectMessageThreadFragment.hasSentSomething = true; currentUserIds = currentMembers.stream()
} .mapToLong(DirectUser::getPk)
} else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); .sorted()
} .toArray();
} else {
currentUserIds = new long[0];
}
final NavDirections directions = DirectMessageSettingsFragmentDirections.actionGlobalUserSearch(
true,
"Add users",
"Add",
currentUserIds
);
navController.navigate(directions);
});
} }
private void setupMembers() {
final Context context = getContext();
if (context == null) return;
binding.users.setLayoutManager(new LinearLayoutManager(context));
final DirectUser inviter = viewModel.getThread().getInviter();
usersAdapter = new DirectUsersAdapter(
inviter != null ? inviter.getPk() : -1,
(position, user, selected) -> {
// navigate to profile
},
(position, user) -> {
final ArrayList<Option<String>> options = viewModel.createUserOptions(user);
if (options == null || options.isEmpty()) return true;
final MultiOptionDialogFragment<String> fragment = MultiOptionDialogFragment.newInstance(-1, options);
fragment.setSingleCallback(new MultiOptionDialogFragment.MultiOptionDialogSingleCallback<String>() {
@Override
public void onSelect(final String action) {
if (action == null) return;
observeDetailsChange(viewModel.doAction(user, action));
}
@Override
public void onCancel() {}
});
final FragmentManager fragmentManager = getChildFragmentManager();
fragment.show(fragmentManager, "actions");
return true;
}
);
binding.users.setAdapter(usersAdapter);
}
private void observeDetailsChange(@NonNull final LiveData<Resource<Object>> detailsChangeResourceLiveData) {
detailsChangeResourceLiveData.observe(getViewLifecycleOwner(), resource -> {
if (resource == null) return;
switch (resource.status) {
case SUCCESS:
case LOADING:
break;
case ERROR:
if (resource.message != null) {
Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show();
}
break;
}
});
}
// class ChangeSettings extends AsyncTask<String, Void, Void> {
// String action, argument;
// boolean ok = false;
// private final String text;
//
// public ChangeSettings(final String text) {
// this.text = text;
// }
//
// protected Void doInBackground(String... rawAction) {
// action = rawAction[0];
// if (rawAction.length == 2) argument = rawAction[1];
// final String url = "https://i.instagram.com/api/v1/direct_v2/threads/" + threadId + "/" + action + "/";
// try {
// String urlParameters = "_csrftoken=" + cookie.split("csrftoken=")[1].split(";")[0]
// + "&_uuid=" + Utils.settingsHelper.getString(Constants.DEVICE_UUID);
// if (action.equals("update_title")) {
// urlParameters += "&title=" + URLEncoder.encode(text, "UTF-8")
// .replaceAll("\\+", "%20").replaceAll("%21", "!").replaceAll("%27", "'")
// .replaceAll("%28", "(").replaceAll("%29", ")").replaceAll("%7E", "~");
// } else if (action.startsWith("remove_users"))
// urlParameters += ("&user_ids=[" + argument + "]");
// final HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
// urlConnection.setRequestMethod("POST");
// urlConnection.setUseCaches(false);
// urlConnection.setRequestProperty("User-Agent", Constants.I_USER_AGENT);
// urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
// urlConnection.setRequestProperty("Content-Length", Integer.toString(urlParameters.getBytes().length));
// urlConnection.setDoOutput(true);
// DataOutputStream wr = new DataOutputStream(urlConnection.getOutputStream());
// wr.writeBytes(urlParameters);
// wr.flush();
// wr.close();
// urlConnection.connect();
// if (urlConnection.getResponseCode() == HttpURLConnection.HTTP_OK) {
// ok = true;
// }
// urlConnection.disconnect();
// } catch (Throwable ex) {
// Log.e("austin_debug", "unsend: " + ex);
// }
// return null;
// }
//
// @Override
// protected void onPostExecute(Void result) {
// final Context context = getContext();
// if (context == null) return;
// if (ok) {
// Toast.makeText(context, R.string.dms_action_success, Toast.LENGTH_SHORT).show();
// if (action.equals("update_title")) {
// threadTitle = titleText.getText().toString();
// titleSend.setVisibility(View.GONE);
// titleText.clearFocus();
// DirectMessageThreadFragment.hasSentSomething = true;
// } else if (action.equals("leave")) {
// context.sendBroadcast(new Intent(DMRefreshBroadcastReceiver.ACTION_REFRESH_DM));
// NavHostFragment.findNavController(DirectMessageSettingsFragment.this).popBackStack(R.id.directMessagesInboxFragment, false);
// } else {
// DirectMessageThreadFragment.hasSentSomething = true;
// }
// } else Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show();
// }
// }
} }

View File

@ -32,6 +32,7 @@ import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModelProvider; import androidx.lifecycle.ViewModelProvider;
import androidx.lifecycle.ViewModelStoreOwner;
import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavBackStackEntry;
import androidx.navigation.NavController; import androidx.navigation.NavController;
import androidx.navigation.NavDirections; import androidx.navigation.NavDirections;
@ -249,8 +250,8 @@ public class DirectMessageThreadFragment extends Fragment {
public boolean onOptionsItemSelected(@NonNull final MenuItem item) { public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
final int itemId = item.getItemId(); final int itemId = item.getItemId();
if (itemId == R.id.info) { if (itemId == R.id.info) {
// final NavDirections action = DirectMessageThreadFragmentDirections.actionDMThreadFragmentToDMSettingsFragment(threadId, threadTitle); final NavDirections action = DirectMessageThreadFragmentDirections.actionDMThreadFragmentToDMSettingsFragment(viewModel.getThreadId());
// NavHostFragment.findNavController(this).navigate(action); NavHostFragment.findNavController(this).navigate(action);
return true; return true;
} }
if (itemId == R.id.mark_as_seen) { if (itemId == R.id.mark_as_seen) {
@ -372,7 +373,9 @@ public class DirectMessageThreadFragment extends Fragment {
} }
private void getInitialData() { private void getInitialData() {
final DirectInboxViewModel threadListViewModel = new ViewModelProvider(fragmentActivity).get(DirectInboxViewModel.class); final NavController navController = NavHostFragment.findNavController(this);
final ViewModelStoreOwner viewModelStoreOwner = navController.getViewModelStoreOwner(R.id.direct_messages_nav_graph);
final DirectInboxViewModel threadListViewModel = new ViewModelProvider(viewModelStoreOwner).get(DirectInboxViewModel.class);
final List<DirectThread> threads = threadListViewModel.getThreads().getValue(); final List<DirectThread> threads = threadListViewModel.getThreads().getValue();
final Optional<DirectThread> first = threads != null final Optional<DirectThread> first = threads != null
? threads.stream() ? threads.stream()

View File

@ -37,8 +37,8 @@ import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.FlavorTown; import awais.instagrabber.utils.FlavorTown;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.webservices.ProfileService;
import awais.instagrabber.webservices.ServiceCallback; import awais.instagrabber.webservices.ServiceCallback;
import awais.instagrabber.webservices.UserService;
import static awais.instagrabber.utils.Utils.settingsHelper; import static awais.instagrabber.utils.Utils.settingsHelper;
@ -199,8 +199,8 @@ public class MorePreferencesFragment extends BasePreferencesFragment {
// adds cookies to database for quick access // adds cookies to database for quick access
final String uid = CookieUtils.getUserIdFromCookie(cookie); final String uid = CookieUtils.getUserIdFromCookie(cookie);
final ProfileService profileService = ProfileService.getInstance(); final UserService userService = UserService.getInstance();
profileService.getUserInfo(uid, new ServiceCallback<UserInfo>() { userService.getUserInfo(uid, new ServiceCallback<UserInfo>() {
@Override @Override
public void onSuccess(final UserInfo result) { public void onSuccess(final UserInfo result) {
// Log.d(TAG, "adding userInfo: " + result); // Log.d(TAG, "adding userInfo: " + result);

View File

@ -5,6 +5,7 @@ import java.util.Map;
import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount;
import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse;
import retrofit2.Call; import retrofit2.Call;
import retrofit2.http.FieldMap; import retrofit2.http.FieldMap;
@ -30,4 +31,29 @@ public interface DirectMessagesRepository {
@POST("/api/v1/direct_v2/threads/broadcast/{item}/") @POST("/api/v1/direct_v2/threads/broadcast/{item}/")
Call<DirectThreadBroadcastResponse> broadcast(@Path("item") String item, Call<DirectThreadBroadcastResponse> broadcast(@Path("item") String item,
@FieldMap final Map<String, String> signedForm); @FieldMap final Map<String, String> signedForm);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/add_user/")
Call<DirectThreadDetailsChangeResponse> addUsers(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/remove_users/")
Call<String> removeUsers(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/update_title/")
Call<DirectThreadDetailsChangeResponse> updateTitle(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/add_admins/")
Call<String> addAdmins(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
@FormUrlEncoded
@POST("/api/v1/direct_v2/threads/{threadId}/remove_admins/")
Call<String> removeAdmins(@Path("threadId") String threadId,
@FieldMap final Map<String, String> form);
} }

View File

@ -9,9 +9,6 @@ import retrofit2.http.QueryMap;
public interface ProfileRepository { public interface ProfileRepository {
@GET("/api/v1/users/{uid}/info/")
Call<String> getUserInfo(@Path("uid") final String uid);
@GET("/api/v1/feed/user/{uid}/") @GET("/api/v1/feed/user/{uid}/")
Call<String> fetch(@Path("uid") final String uid, @QueryMap Map<String, String> queryParams); Call<String> fetch(@Path("uid") final String uid, @QueryMap Map<String, String> queryParams);

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -1,6 +1,7 @@
package awais.instagrabber.repositories.responses.directmessages; package awais.instagrabber.repositories.responses.directmessages;
public class DirectInboxResponse { public class DirectInboxResponse {
private final DirectUser viewer;
private final DirectInbox inbox; private final DirectInbox inbox;
private final long seqId; private final long seqId;
private final long snapshotAtMs; private final long snapshotAtMs;
@ -8,12 +9,14 @@ public class DirectInboxResponse {
private final DirectUser mostRecentInviter; private final DirectUser mostRecentInviter;
private final String status; private final String status;
public DirectInboxResponse(final DirectInbox inbox, public DirectInboxResponse(final DirectUser viewer,
final DirectInbox inbox,
final long seqId, final long seqId,
final long snapshotAtMs, final long snapshotAtMs,
final int pendingRequestsTotal, final int pendingRequestsTotal,
final DirectUser mostRecentInviter, final DirectUser mostRecentInviter,
final String status) { final String status) {
this.viewer = viewer;
this.inbox = inbox; this.inbox = inbox;
this.seqId = seqId; this.seqId = seqId;
this.snapshotAtMs = snapshotAtMs; this.snapshotAtMs = snapshotAtMs;
@ -22,6 +25,10 @@ public class DirectInboxResponse {
this.status = status; this.status = status;
} }
public DirectUser getViewer() {
return viewer;
}
public DirectInbox getInbox() { public DirectInbox getInbox() {
return inbox; return inbox;
} }

View File

@ -10,8 +10,8 @@ public class DirectThread {
private final String threadId; private final String threadId;
private final String threadV2Id; private final String threadV2Id;
private final List<DirectUser> users; private final List<DirectUser> users;
private final List<String> leftUsers; private final List<DirectUser> leftUsers;
private final List<String> adminUserIds; private final List<Long> adminUserIds;
private final List<DirectItem> items; private final List<DirectItem> items;
private final long lastActivityAt; private final long lastActivityAt;
private final boolean muted; private final boolean muted;
@ -42,8 +42,8 @@ public class DirectThread {
public DirectThread(final String threadId, public DirectThread(final String threadId,
final String threadV2Id, final String threadV2Id,
final List<DirectUser> users, final List<DirectUser> users,
final List<String> leftUsers, final List<DirectUser> leftUsers,
final List<String> adminUserIds, final List<Long> adminUserIds,
final List<DirectItem> items, final List<DirectItem> items,
final long lastActivityAt, final long lastActivityAt,
final boolean muted, final boolean muted,
@ -115,11 +115,11 @@ public class DirectThread {
return users; return users;
} }
public List<String> getLeftUsers() { public List<DirectUser> getLeftUsers() {
return leftUsers; return leftUsers;
} }
public List<String> getAdminUserIds() { public List<Long> getAdminUserIds() {
return adminUserIds; return adminUserIds;
} }

View File

@ -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;
}
}

View File

@ -1,8 +1,11 @@
package awais.instagrabber.repositories.responses.directmessages; package awais.instagrabber.repositories.responses.directmessages;
import java.io.Serializable;
import java.util.Objects;
import awais.instagrabber.models.ProfileModel; import awais.instagrabber.models.ProfileModel;
public class DirectUser { public class DirectUser implements Serializable {
private final long pk; private final long pk;
private final String username; private final String username;
private final String fullName; private final String fullName;
@ -104,8 +107,8 @@ public class DirectUser {
profileModel.isPrivate(), profileModel.isPrivate(),
false, false,
profileModel.isRequested(), profileModel.isRequested(),
false false,
), profileModel.isRestricted()),
profileModel.isVerified(), profileModel.isVerified(),
false, false,
false, false,
@ -113,4 +116,18 @@ public class DirectUser {
null null
); );
} }
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final DirectUser that = (DirectUser) o;
return pk == that.pk &&
Objects.equals(username, that.username);
}
@Override
public int hashCode() {
return Objects.hash(pk, username);
}
} }

View File

@ -1,25 +1,31 @@
package awais.instagrabber.repositories.responses.directmessages; package awais.instagrabber.repositories.responses.directmessages;
public class DirectUserFriendshipStatus { import androidx.annotation.NonNull;
import java.io.Serializable;
public class DirectUserFriendshipStatus implements Serializable {
private final boolean following; private final boolean following;
private final boolean blocking; private final boolean blocking;
private final boolean isPrivate; private final boolean isPrivate;
private final boolean incomingRequest; private final boolean incomingRequest;
private final boolean outgoingRequest; private final boolean outgoingRequest;
private final boolean isBestie; private final boolean isBestie;
private final boolean isRestricted;
public DirectUserFriendshipStatus(final boolean following, public DirectUserFriendshipStatus(final boolean following,
final boolean blocking, final boolean blocking,
final boolean isPrivate, final boolean isPrivate,
final boolean incomingRequest, final boolean incomingRequest,
final boolean outgoingRequest, final boolean outgoingRequest,
final boolean isBestie) { final boolean isBestie, final boolean isRestricted) {
this.following = following; this.following = following;
this.blocking = blocking; this.blocking = blocking;
this.isPrivate = isPrivate; this.isPrivate = isPrivate;
this.incomingRequest = incomingRequest; this.incomingRequest = incomingRequest;
this.outgoingRequest = outgoingRequest; this.outgoingRequest = outgoingRequest;
this.isBestie = isBestie; this.isBestie = isBestie;
this.isRestricted = isRestricted;
} }
public boolean isFollowing() { public boolean isFollowing() {
@ -46,15 +52,21 @@ public class DirectUserFriendshipStatus {
return isBestie; return isBestie;
} }
public boolean isRestricted() {
return isRestricted;
}
@NonNull
@Override @Override
public String toString() { public String toString() {
return "DirectInboxFeedResponseFriendshipStatus{" + return "DirectInboxFeedResponseFriendshipStatus{" +
"following=" + following + "following=" + following +
", blocking=" + blocking + ", blocking=" + blocking +
", is_private=" + isPrivate + ", isPrivate=" + isPrivate +
", incomingRequest=" + incomingRequest + ", incomingRequest=" + incomingRequest +
", outgoingRequest=" + outgoingRequest + ", outgoingRequest=" + outgoingRequest +
", isBestie=" + isBestie + ", isBestie=" + isBestie +
", isRestricted" + isRestricted +
'}'; '}';
} }
} }

View File

@ -0,0 +1,90 @@
package awais.instagrabber.utils;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class Debouncer<T> {
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private final ConcurrentHashMap<T, TimerTask> delayedMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<T, ScheduledFuture<?>> futureMap = new ConcurrentHashMap<>();
private final Callback<T> callback;
private final int interval;
public Debouncer(Callback<T> c, int interval) {
this.callback = c;
this.interval = interval;
}
public void call(T key) {
TimerTask task = new TimerTask(key);
TimerTask prev;
do {
prev = delayedMap.putIfAbsent(key, task);
if (prev == null) {
final ScheduledFuture<?> future = scheduler.schedule(task, interval, TimeUnit.MILLISECONDS);
futureMap.put(key, future);
}
} while (prev != null && !prev.extend()); // Exit only if new task was added to map, or existing task was extended successfully
}
public void terminate() {
scheduler.shutdownNow();
}
public void cancel(final T key) {
delayedMap.remove(key);
final ScheduledFuture<?> future = futureMap.get(key);
if (future != null) {
future.cancel(true);
}
}
// The task that wakes up when the wait time elapses
private class TimerTask implements Runnable {
private final T key;
private long dueTime;
private final Object lock = new Object();
public TimerTask(T key) {
this.key = key;
extend();
}
public boolean extend() {
synchronized (lock) {
if (dueTime < 0) // Task has been shutdown
return false;
dueTime = System.currentTimeMillis() + interval;
return true;
}
}
public void run() {
synchronized (lock) {
long remaining = dueTime - System.currentTimeMillis();
if (remaining > 0) { // Re-schedule task
scheduler.schedule(this, remaining, TimeUnit.MILLISECONDS);
} else { // Mark as terminated and invoke callback
dueTime = -1;
try {
callback.call(key);
} catch (Exception e) {
callback.onError(e);
} finally {
delayedMap.remove(key);
}
}
}
}
}
public interface Callback<T> {
void call(T key);
void onError(Throwable t);
}
}

View File

@ -24,6 +24,7 @@ import android.view.Gravity;
import android.view.View; import android.view.View;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.webkit.MimeTypeMap; import android.webkit.MimeTypeMap;
import android.widget.Toast; import android.widget.Toast;
@ -340,4 +341,17 @@ public final class Utils {
callback callback
); );
} }
public static void hideKeyboard(final View view) {
if (view == null) return;
final Context context = view.getContext();
if (context == null) return;
try {
final InputMethodManager manager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE);
if (manager == null) return;
manager.hideSoftInputFromWindow(view.getWindowToken(), 0);
} catch (Exception e) {
Log.e(TAG, "hideKeyboard: ", e);
}
}
} }

View File

@ -5,10 +5,14 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable; import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape; import android.graphics.drawable.shapes.RoundRectShape;
import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt; import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat; import androidx.core.content.res.ResourcesCompat;
import androidx.core.util.Pair;
public final class ViewUtils { public final class ViewUtils {
@ -53,4 +57,16 @@ public final class ViewUtils {
private static int getSize(float size) { private static int getSize(float size) {
return (int) (size < 0 ? size : Utils.convertDpToPx(size)); return (int) (size < 0 ? size : Utils.convertDpToPx(size));
} }
public static Pair<Integer, Integer> measure(@NonNull final View view, @NonNull final View parent) {
view.measure(
View.MeasureSpec.makeMeasureSpec(parent.getWidth(), View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(parent.getHeight(), View.MeasureSpec.UNSPECIFIED)
);
return new Pair<>(view.getMeasuredHeight(), view.getMeasuredWidth());
}
public static float getTextViewValueWidth(final TextView textView, final String text) {
return textView.getPaint().measureText(text);
}
} }

View File

@ -14,6 +14,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount
import awais.instagrabber.repositories.responses.directmessages.DirectInbox; import awais.instagrabber.repositories.responses.directmessages.DirectInbox;
import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.DirectThread;
import awais.instagrabber.repositories.responses.directmessages.DirectUser;
import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.CookieUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
@ -38,6 +39,7 @@ public class DirectInboxViewModel extends ViewModel {
private long seqId; private long seqId;
private String cursor; private String cursor;
private boolean hasOlder = true; private boolean hasOlder = true;
private DirectUser viewer;
public DirectInboxViewModel() { public DirectInboxViewModel() {
final String cookie = settingsHelper.getString(Constants.COOKIE); final String cookie = settingsHelper.getString(Constants.COOKIE);
@ -76,6 +78,10 @@ public class DirectInboxViewModel extends ViewModel {
return fetchingInbox; return fetchingInbox;
} }
public DirectUser getViewer() {
return viewer;
}
public void fetchInbox() { public void fetchInbox() {
if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return; if ((fetchingInbox.getValue() != null && fetchingInbox.getValue()) || !hasOlder) return;
stopCurrentInboxRequest(); stopCurrentInboxRequest();
@ -108,6 +114,9 @@ public class DirectInboxViewModel extends ViewModel {
return; return;
} }
seqId = response.getSeqId(); seqId = response.getSeqId();
if (viewer == null) {
viewer = response.getViewer();
}
final DirectInbox inbox = response.getInbox(); final DirectInbox inbox = response.getInbox();
final List<DirectThread> threads = inbox.getThreads(); final List<DirectThread> threads = inbox.getThreads();
if (!TextUtils.isEmpty(cursor)) { if (!TextUtils.isEmpty(cursor)) {

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -7,6 +7,7 @@ import com.google.common.collect.ImmutableMap;
import org.json.JSONArray; import org.json.JSONArray;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -25,6 +26,7 @@ import awais.instagrabber.repositories.requests.directmessages.VoiceBroadcastOpt
import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount; import awais.instagrabber.repositories.responses.directmessages.DirectBadgeCount;
import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse; import awais.instagrabber.repositories.responses.directmessages.DirectInboxResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadBroadcastResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadDetailsChangeResponse;
import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import awais.instagrabber.utils.Utils; import awais.instagrabber.utils.Utils;
@ -175,4 +177,54 @@ public class DirectMessagesService extends BaseService {
final Map<String, String> signedForm = Utils.sign(form); final Map<String, String> signedForm = Utils.sign(form);
return repository.broadcast(broadcastOptions.getItemType().getValue(), signedForm); return repository.broadcast(broadcastOptions.getItemType().getValue(), signedForm);
} }
public Call<DirectThreadDetailsChangeResponse> addUsers(final String threadId,
final Collection<Long> userIds) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid,
"user_ids", new JSONArray(userIds).toString()
);
return repository.addUsers(threadId, form);
}
public Call<String> removeUsers(final String threadId,
final Collection<Long> userIds) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid,
"user_ids", new JSONArray(userIds).toString()
);
return repository.removeUsers(threadId, form);
}
public Call<DirectThreadDetailsChangeResponse> updateTitle(final String threadId,
final String title) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid,
"title", title
);
return repository.updateTitle(threadId, form);
}
public Call<String> addAdmins(final String threadId,
final Collection<Long> userIds) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid,
"user_ids", new JSONArray(userIds).toString()
);
return repository.addAdmins(threadId, form);
}
public Call<String> removeAdmins(final String threadId,
final Collection<Long> userIds) {
final ImmutableMap<String, String> form = ImmutableMap.of(
"_csrftoken", csrfToken,
"_uuid", deviceUuid,
"user_ids", new JSONArray(userIds).toString()
);
return repository.removeAdmins(threadId, form);
}
} }

View File

@ -17,8 +17,6 @@ import java.util.List;
import awais.instagrabber.models.FeedModel; import awais.instagrabber.models.FeedModel;
import awais.instagrabber.repositories.ProfileRepository; import awais.instagrabber.repositories.ProfileRepository;
import awais.instagrabber.repositories.responses.PostsFetchResponse; import awais.instagrabber.repositories.responses.PostsFetchResponse;
import awais.instagrabber.repositories.responses.UserInfo;
import awais.instagrabber.utils.Constants;
import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.ResponseBodyUtils;
import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.TextUtils;
import retrofit2.Call; import retrofit2.Call;
@ -47,39 +45,6 @@ public class ProfileService extends BaseService {
return instance; return instance;
} }
public void getUserInfo(final String uid, final ServiceCallback<UserInfo> callback) {
final Call<String> request = repository.getUserInfo(uid);
request.enqueue(new Callback<String>() {
@Override
public void onResponse(@NonNull final Call<String> call, @NonNull final Response<String> response) {
final String body = response.body();
if (body == null) return;
try {
final JSONObject jsonObject = new JSONObject(body);
final JSONObject user = jsonObject.optJSONObject(Constants.EXTRAS_USER);
if (user == null) return;
// Log.d(TAG, "user: " + user.toString());
final UserInfo userInfo = new UserInfo(
uid,
user.getString(Constants.EXTRAS_USERNAME),
user.optString("full_name"),
user.optString("profile_pic_url"),
user.has("hd_profile_pic_url_info")
? user.getJSONObject("hd_profile_pic_url_info").optString("url") : null
);
callback.onSuccess(userInfo);
} catch (JSONException e) {
Log.e(TAG, "Error parsing json", e);
}
}
@Override
public void onFailure(@NonNull final Call<String> call, @NonNull final Throwable t) {
callback.onFailure(t);
}
});
}
public void fetchPosts(final String userId, public void fetchPosts(final String userId,
final String maxId, final String maxId,
final ServiceCallback<PostsFetchResponse> callback) { final ServiceCallback<PostsFetchResponse> callback) {

View File

@ -160,8 +160,8 @@ public class StoriesService extends BaseService {
highlightNode.getString("title"), highlightNode.getString("title"),
highlightNode.getString(Constants.EXTRAS_ID), highlightNode.getString(Constants.EXTRAS_ID),
highlightNode.getJSONObject("cover_media") highlightNode.getJSONObject("cover_media")
.getJSONObject("cropped_image_version") .getJSONObject("cropped_image_version")
.getString("url"), .getString("url"),
highlightNode.getLong("latest_reel_media"), highlightNode.getLong("latest_reel_media"),
highlightNode.getInt("media_count") highlightNode.getInt("media_count")
)); ));
@ -188,7 +188,7 @@ public class StoriesService extends BaseService {
form.put("include_suggested_highlights", "false"); form.put("include_suggested_highlights", "false");
form.put("is_in_archive_home", "true"); form.put("is_in_archive_home", "true");
form.put("include_cover", "1"); form.put("include_cover", "1");
form.put("timezone_offset", String.valueOf(TimeZone.getDefault().getRawOffset() / 1000)); form.put("timezone_offset", String.valueOf((float) TimeZone.getDefault().getRawOffset() / 1000));
if (!TextUtils.isEmpty(maxId)) { if (!TextUtils.isEmpty(maxId)) {
form.put("max_id", maxId); // NOT TESTED form.put("max_id", maxId); // NOT TESTED
} }
@ -338,40 +338,40 @@ public class StoriesService extends BaseService {
// RespondAction.java // RespondAction.java
public void respondToQuestion(final String storyId, public void respondToQuestion(final String storyId,
final String stickerId, final String stickerId,
final String answer, final String answer,
final String userId, final String userId,
final String csrfToken, final String csrfToken,
final ServiceCallback<StoryStickerResponse> callback) { final ServiceCallback<StoryStickerResponse> callback) {
respondToSticker(storyId, stickerId, "story_question_response", "response", answer, userId, csrfToken, callback); respondToSticker(storyId, stickerId, "story_question_response", "response", answer, userId, csrfToken, callback);
} }
// QuizAction.java // QuizAction.java
public void respondToQuiz(final String storyId, public void respondToQuiz(final String storyId,
final String stickerId, final String stickerId,
final int answer, final int answer,
final String userId, final String userId,
final String csrfToken, final String csrfToken,
final ServiceCallback<StoryStickerResponse> callback) { final ServiceCallback<StoryStickerResponse> callback) {
respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), userId, csrfToken, callback); respondToSticker(storyId, stickerId, "story_quiz_answer", "answer", String.valueOf(answer), userId, csrfToken, callback);
} }
// VoteAction.java // VoteAction.java
public void respondToPoll(final String storyId, public void respondToPoll(final String storyId,
final String stickerId, final String stickerId,
final int answer, final int answer,
final String userId, final String userId,
final String csrfToken, final String csrfToken,
final ServiceCallback<StoryStickerResponse> callback) { final ServiceCallback<StoryStickerResponse> callback) {
respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); respondToSticker(storyId, stickerId, "story_poll_vote", "vote", String.valueOf(answer), userId, csrfToken, callback);
} }
public void respondToSlider(final String storyId, public void respondToSlider(final String storyId,
final String stickerId, final String stickerId,
final double answer, final double answer,
final String userId, final String userId,
final String csrfToken, final String csrfToken,
final ServiceCallback<StoryStickerResponse> callback) { final ServiceCallback<StoryStickerResponse> callback) {
respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), userId, csrfToken, callback); respondToSticker(storyId, stickerId, "story_slider_vote", "vote", String.valueOf(answer), userId, csrfToken, callback);
} }

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/blue_400" android:state_selected="true" />
<item android:color="?colorControlNormal" />
</selector>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="1dp"
android:left="-2dp"
android:right="-2dp"
android:top="-2dp">
<shape android:shape="rectangle">
<stroke
android:width="1dp"
android:color="?colorControlNormal" />
<solid android:color="#00FFFFFF" />
<!--<padding android:left="0dp"-->
<!-- android:right="0dp"-->
<!-- android:top="0dp"-->
<!-- android:bottom="0dp" />-->
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_round_check_circle_24" android:state_selected="true" />
<item android:drawable="@drawable/ic_radio_button_unchecked_24" />
</selector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/>
</vector>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM9.29,16.29L5.7,12.7c-0.39,-0.39 -0.39,-1.02 0,-1.41 0.39,-0.39 1.02,-0.39 1.41,0L10,14.17l6.88,-6.88c0.39,-0.39 1.02,-0.39 1.41,0 0.39,0.39 0.39,1.02 0,1.41l-7.59,7.59c-0.38,0.39 -1.02,0.39 -1.41,0z"/>
</vector>

View File

@ -1,94 +1,154 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:focusableInTouchMode="true">
<LinearLayout <com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/titleText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="bottom"
android:importantForAutofill="no"
android:inputType="textMultiLine"
android:maxLength="2200"
android:maxLines="10"
android:paddingStart="8dp"
android:paddingLeft="8dp"
android:paddingEnd="4dp"
android:paddingRight="4dp"
android:scrollHorizontally="false" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/titleSend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:paddingStart="4dp"
android:paddingLeft="4dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingRight="8dp"
android:paddingBottom="4dp"
android:visibility="gone"
app:srcCompat="@drawable/ic_submit" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnLeave"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginLeft="8dp" android:background="@null"
android:layout_marginStart="6dp" android:elevation="0dp"
android:layout_marginRight="8dp" app:elevation="0dp">
android:text="@string/dms_action_leave"
android:textColor="@color/btn_red_text_color"
android:textSize="18sp"
app:backgroundTint="@color/btn_red_background" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="wrap_content"
<LinearLayout app:layout_scrollFlags="scroll">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/settings_parent"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content">
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.textfield.TextInputLayout
android:id="@+id/userList" android:id="@+id/title_edit_input_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="false"/>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/leftTitle"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:hint="@string/title"
android:visibility="gone" android:visibility="gone"
app:boxBackgroundColor="@android:color/transparent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:suffixText="@string/save"
app:suffixTextColor="@color/blue_600"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/title_edit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:autofillHints="no"
android:inputType="text"
android:maxLength="2200"
android:scrollHorizontally="false"
tools:text="test" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/mute_messages_label"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:padding="5dp" android:paddingStart="16dp"
android:text="@string/dms_left_users" android:paddingEnd="16dp"
android:text="@string/mute_messages"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary" android:textColor="?android:textColorPrimary"
android:textSize="24sp"/> app:layout_constraintBottom_toBottomOf="@id/mute_messages"
<androidx.recyclerview.widget.RecyclerView app:layout_constraintEnd_toEndOf="parent"
android:id="@+id/leftUserList" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/mute_messages" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/mute_messages"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="8dp"
app:layout_constraintBottom_toTopOf="@id/mute_mentions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/title_edit_input_layout" />
<com.google.android.material.button.MaterialButton
android:id="@+id/mute_mentions_label"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:text="@string/mute_mentions"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="@id/mute_mentions"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/mute_mentions" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/mute_mentions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="0dp"
android:paddingEnd="8dp"
app:layout_constraintBottom_toTopOf="@id/leave"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/mute_messages" />
<com.google.android.material.button.MaterialButton
android:id="@+id/leave"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:nestedScrollingEnabled="false"/> android:gravity="center_vertical"
</LinearLayout> android:paddingStart="16dp"
</androidx.core.widget.NestedScrollView> android:paddingTop="8dp"
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> android:paddingEnd="16dp"
</LinearLayout> android:paddingBottom="8dp"
android:text="@string/dms_action_leave"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="@color/red_600"
app:layout_constraintBottom_toTopOf="@id/add_members"
app:layout_constraintTop_toBottomOf="@id/mute_mentions" />
<com.google.android.material.button.MaterialButton
android:id="@+id/add_members"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp"
android:text="@string/add_members"
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
android:textColor="?android:textColorPrimary"
app:icon="@drawable/ic_add"
app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/leave" />
<androidx.constraintlayout.widget.Group
android:id="@+id/group_settings"
android:layout_width="0dp"
android:layout_height="0dp"
app:constraint_referenced_ids="title_edit_input_layout, mute_mentions_label, mute_mentions, leave, add_members" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/users"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusableInTouchMode="true">
<com.google.android.material.chip.ChipGroup
android:id="@+id/group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_user_search_input"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="@id/results"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/search"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:hint="@string/search"
android:importantForAutofill="no"
android:inputType="textNoSuggestions"
android:maxLines="1"
tools:text="test" />
</com.google.android.material.chip.ChipGroup>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/results"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/done"
app:layout_constraintTop_toBottomOf="@id/group" />
<com.google.android.material.button.MaterialButton
android:id="@+id/done"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/done"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/results"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -17,7 +17,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="60dp" android:layout_height="60dp"
android:layout_marginStart="66dp" android:layout_marginStart="66dp"
android:layout_marginLeft="66dp"
android:layout_marginEnd="26dp" android:layout_marginEnd="26dp"
android:orientation="vertical"> android:orientation="vertical">

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="72dp"
android:background="?android:selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/profile_pic"
android:layout_width="@dimen/dm_inbox_avatar_size"
android:layout_height="@dimen/dm_inbox_avatar_size"
app:actualImageScaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/full_name"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:roundAsCircle="true"
tools:background="@mipmap/ic_launcher" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/full_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:ellipsize="end"
android:gravity="bottom"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintEnd_toStartOf="@id/info"
app:layout_constraintStart_toEndOf="@id/profile_pic"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Long name......................." />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:gravity="bottom"
android:paddingStart="4dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
android:textColor="@color/blue_400"
app:layout_constraintBaseline_toBaselineOf="@id/thread_title"
app:layout_constraintBottom_toTopOf="@id/username"
app:layout_constraintEnd_toStartOf="@id/select"
app:layout_constraintStart_toEndOf="@id/full_name"
app:layout_constraintTop_toTopOf="@id/full_name"
tools:text="Admin" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/username"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/select"
app:layout_constraintStart_toStartOf="@id/full_name"
app:layout_constraintTop_toBottomOf="@id/full_name"
tools:text="username" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/select"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginStart="4dp"
android:duplicateParentState="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@id/profile_pic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/info"
app:layout_constraintTop_toTopOf="@id/profile_pic"
app:srcCompat="@drawable/ic_circle_check"
app:tint="@color/ic_circle_check_tint"
tools:visibility="visible" />
<include
layout="@layout/item_pref_divider"
android:layout_width="0dp"
android:layout_height="1dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/thread_title" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -73,14 +73,38 @@
<action <action
android:id="@+id/action_global_likesViewerFragment" android:id="@+id/action_global_likesViewerFragment"
app:destination="@id/likes_nav_graph"> app:destination="@id/likes_nav_graph">
<argument <argument
android:name="postId" android:name="postId"
app:argType="string" app:argType="string"
app:nullable="false" /> app:nullable="false" />
<argument <argument
android:name="isComment" android:name="isComment"
app:argType="boolean" app:argType="boolean"
app:nullable="false" /> app:nullable="false" />
</action>
<include app:graph="@navigation/user_search_nav_graph" />
<action
android:id="@+id/action_global_user_search"
app:destination="@id/user_search_nav_graph">
<argument
android:name="multiple"
app:argType="boolean" />
<argument
android:name="title"
app:argType="string"
app:nullable="true" />
<argument
android:name="action_label"
app:argType="string"
app:nullable="true" />
<argument
android:name="hideUserIds"
app:argType="long[]" />
</action> </action>
<fragment <fragment
@ -112,20 +136,23 @@
<fragment <fragment
android:id="@+id/directMessagesSettingsFragment" android:id="@+id/directMessagesSettingsFragment"
android:name="awais.instagrabber.fragments.directmessages.DirectMessageSettingsFragment" android:name="awais.instagrabber.fragments.directmessages.DirectMessageSettingsFragment"
android:label="@string/action_settings" android:label="@string/details"
tools:layout="@layout/fragment_direct_messages_settings"> tools:layout="@layout/fragment_direct_messages_settings">
<argument <argument
android:name="threadId" android:name="threadId"
app:argType="string" /> app:argType="string" />
<argument <action
android:name="title" android:id="@+id/action_settings_to_inbox"
app:argType="string" /> app:destination="@id/directMessagesInboxFragment"
app:popUpTo="@+id/directMessagesInboxFragment"
app:popUpToInclusive="true" />
</fragment> </fragment>
<fragment <fragment
android:id="@+id/imageEditFragment" android:id="@+id/imageEditFragment"
android:name="awais.instagrabber.fragments.imageedit.ImageEditFragment" android:name="awais.instagrabber.fragments.imageedit.ImageEditFragment"
tools:layout="@layout/fragment_image_edit" android:label="Edit Photo"
android:label="Edit Photo"> tools:layout="@layout/fragment_image_edit">
<argument <argument
android:name="uri" android:name="uri"
app:argType="android.net.Uri" app:argType="android.net.Uri"

View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/user_search_nav_graph"
app:startDestination="@id/user_search">
<fragment
android:id="@+id/user_search"
android:name="awais.instagrabber.fragments.UserSearchFragment"
android:label="@string/search">
<argument
android:name="multiple"
app:argType="boolean" />
<argument
android:name="title"
app:argType="string"
app:nullable="true" />
<argument
android:name="action_label"
app:argType="string"
app:nullable="true" />
<argument
android:name="hideUserIds"
app:argType="long[]" />
</fragment>
<!--<action
android:id="@+id/action_global_user_search"
app:destination="@id/user_search">
<argument
android:name="multiple"
app:argType="boolean" />
<argument
android:name="title"
app:argType="string"
app:nullable="true" />
<argument
android:name="action_label"
app:argType="string"
app:nullable="true" />
</action>-->
</navigation>

View File

@ -176,7 +176,7 @@
<string name="dms_inbox_raven_media_screenshot">Screenshotted</string> <string name="dms_inbox_raven_media_screenshot">Screenshotted</string>
<string name="dms_inbox_raven_media_cant_deliver">Cannot deliver</string> <string name="dms_inbox_raven_media_cant_deliver">Cannot deliver</string>
<string name="dms_action_success">Great success!</string> <string name="dms_action_success">Great success!</string>
<string name="dms_action_leave">Leave</string> <string name="dms_action_leave">Leave chat</string>
<string name="dms_action_leave_question">Leave this chat?</string> <string name="dms_action_leave_question">Leave this chat?</string>
<string name="dms_action_kick">Kick</string> <string name="dms_action_kick">Kick</string>
<string name="dms_left_users">Left users</string> <string name="dms_left_users">Left users</string>
@ -378,4 +378,16 @@
<item quantity="other">%s stories</item> <item quantity="other">%s stories</item>
</plurals> </plurals>
<string name="download_permission">Storage permission not granted!</string> <string name="download_permission">Storage permission not granted!</string>
<string name="details">Details</string>
<string name="title">Title</string>
<string name="members">Members</string>
<string name="admin">Admin</string>
<string name="inviter">Inviter</string>
<string name="mute_messages">Mute messages</string>
<string name="mute_mentions">Mute mentions</string>
<string name="add_members">Add members</string>
<string name="search">Search</string>
<string name="done">Done</string>
<string name="dms_action_make_admin">Make Admin</string>
<string name="dms_action_remove_admin">Remove as Admin</string>
</resources> </resources>