diff --git a/app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java b/app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java new file mode 100644 index 00000000..83a6f62c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/adapters/GifItemsAdapter.java @@ -0,0 +1,107 @@ +package awais.instagrabber.adapters; + +import android.net.Uri; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.drawee.backends.pipeline.PipelineDraweeControllerBuilder; +import com.facebook.drawee.controller.BaseControllerListener; +import com.facebook.drawee.drawable.ScalingUtils; +import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; +import com.facebook.imagepipeline.common.ResizeOptions; +import com.facebook.imagepipeline.image.ImageInfo; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.util.Objects; + +import awais.instagrabber.databinding.ItemMediaBinding; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; +import awais.instagrabber.utils.Utils; + +public class GifItemsAdapter extends ListAdapter { + + private static final DiffUtil.ItemCallback diffCallback = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) { + return Objects.equals(oldItem.getId(), newItem.getId()); + } + + @Override + public boolean areContentsTheSame(@NonNull final GiphyGif oldItem, @NonNull final GiphyGif newItem) { + return Objects.equals(oldItem.getId(), newItem.getId()); + } + }; + + private final OnItemClickListener onItemClickListener; + + public GifItemsAdapter(final OnItemClickListener onItemClickListener) { + super(diffCallback); + this.onItemClickListener = onItemClickListener; + } + + @NonNull + @Override + public GifViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) { + final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext()); + final ItemMediaBinding binding = ItemMediaBinding.inflate(layoutInflater, parent, false); + return new GifViewHolder(binding, onItemClickListener); + } + + @Override + public void onBindViewHolder(@NonNull final GifViewHolder holder, final int position) { + holder.bind(getItem(position)); + } + + public static class GifViewHolder extends RecyclerView.ViewHolder { + private static final String TAG = GifViewHolder.class.getSimpleName(); + private static final int size = Utils.displayMetrics.widthPixels / 3; + + private final ItemMediaBinding binding; + private final OnItemClickListener onItemClickListener; + + public GifViewHolder(@NonNull final ItemMediaBinding binding, + final OnItemClickListener onItemClickListener) { + super(binding.getRoot()); + this.binding = binding; + this.onItemClickListener = onItemClickListener; + binding.duration.setVisibility(View.GONE); + final GenericDraweeHierarchyBuilder builder = new GenericDraweeHierarchyBuilder(itemView.getResources()); + builder.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER); + binding.item.setHierarchy(builder.build()); + } + + public void bind(final GiphyGif item) { + if (onItemClickListener != null) { + itemView.setOnClickListener(v -> onItemClickListener.onItemClick(item)); + } + final BaseControllerListener controllerListener = new BaseControllerListener() { + @Override + public void onFailure(final String id, final Throwable throwable) { + Log.e(TAG, "onFailure: ", throwable); + } + }; + final ImageRequest request = ImageRequestBuilder + .newBuilderWithSource(Uri.parse(item.getImages().getFixedHeight().getWebp())) + .setResizeOptions(ResizeOptions.forDimensions(size, size)) + .build(); + final PipelineDraweeControllerBuilder builder = Fresco.newDraweeControllerBuilder() + .setImageRequest(request) + .setAutoPlayAnimations(true) + .setControllerListener(controllerListener); + binding.item.setController(builder.build()); + } + } + + public interface OnItemClickListener { + void onItemClick(GiphyGif giphyGif); + } +} diff --git a/app/src/main/java/awais/instagrabber/dialogs/GifPickerBottomDialogFragment.java b/app/src/main/java/awais/instagrabber/dialogs/GifPickerBottomDialogFragment.java new file mode 100644 index 00000000..a7ab61e4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/dialogs/GifPickerBottomDialogFragment.java @@ -0,0 +1,150 @@ +package awais.instagrabber.dialogs; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.GridLayoutManager; + +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.snackbar.Snackbar; + +import awais.instagrabber.R; +import awais.instagrabber.adapters.GifItemsAdapter; +import awais.instagrabber.customviews.helpers.TextWatcherAdapter; +import awais.instagrabber.databinding.LayoutGifPickerBinding; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; +import awais.instagrabber.utils.Debouncer; +import awais.instagrabber.utils.TextUtils; +import awais.instagrabber.viewmodels.GifPickerViewModel; + +public class GifPickerBottomDialogFragment extends BottomSheetDialogFragment { + private static final String TAG = GifPickerBottomDialogFragment.class.getSimpleName(); + private static final int INPUT_DEBOUNCE_INTERVAL = 500; + private static final String INPUT_KEY = "gif_search_input"; + + private LayoutGifPickerBinding binding; + private GifPickerViewModel viewModel; + private GifItemsAdapter gifItemsAdapter; + private OnSelectListener onSelectListener; + private Debouncer inputDebouncer; + + public static GifPickerBottomDialogFragment newInstance() { + final Bundle args = new Bundle(); + final GifPickerBottomDialogFragment fragment = new GifPickerBottomDialogFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.ThemeOverlay_Rounded_BottomSheetDialog); + final Debouncer.Callback callback = new Debouncer.Callback() { + @Override + public void call(final String key) { + final Editable text = binding.input.getText(); + if (TextUtils.isEmpty(text)) { + viewModel.search(null); + return; + } + viewModel.search(text.toString().trim()); + } + + @Override + public void onError(final Throwable t) { + Log.e(TAG, "onError: ", t); + } + }; + inputDebouncer = new Debouncer<>(callback, INPUT_DEBOUNCE_INTERVAL); + } + + @Nullable + @Override + public View onCreateView(@NonNull final LayoutInflater inflater, @Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) { + binding = LayoutGifPickerBinding.inflate(inflater, container, false); + viewModel = new ViewModelProvider(this).get(GifPickerViewModel.class); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { + init(); + } + + @Override + public void onStart() { + super.onStart(); + final Dialog dialog = getDialog(); + if (dialog == null) return; + final BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialog; + final View bottomSheetInternal = bottomSheetDialog.findViewById(com.google.android.material.R.id.design_bottom_sheet); + if (bottomSheetInternal == null) return; + bottomSheetInternal.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + bottomSheetInternal.requestLayout(); + } + + private void init() { + setupList(); + setupInput(); + setupObservers(); + } + + private void setupList() { + final Context context = getContext(); + if (context == null) return; + binding.gifList.setLayoutManager(new GridLayoutManager(context, 3)); + binding.gifList.setHasFixedSize(true); + gifItemsAdapter = new GifItemsAdapter(entry -> { + if (onSelectListener == null) return; + onSelectListener.onSelect(entry); + }); + binding.gifList.setAdapter(gifItemsAdapter); + } + + private void setupInput() { + binding.input.addTextChangedListener(new TextWatcherAdapter() { + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { + inputDebouncer.call(INPUT_KEY); + } + }); + } + + private void setupObservers() { + viewModel.getImages().observe(getViewLifecycleOwner(), imagesResource -> { + if (imagesResource == null) return; + switch (imagesResource.status) { + case SUCCESS: + gifItemsAdapter.submitList(imagesResource.data); + break; + case ERROR: + final Context context = getContext(); + if (context != null && imagesResource.message != null) { + Snackbar.make(context, binding.getRoot(), imagesResource.message, Snackbar.LENGTH_LONG); + } + break; + case LOADING: + break; + } + }); + } + + public void setOnSelectListener(final OnSelectListener onSelectListener) { + this.onSelectListener = onSelectListener; + } + + public interface OnSelectListener { + void onSelect(GiphyGif giphyGif); + } +} diff --git a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java index ed231e53..a4e88f19 100644 --- a/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java +++ b/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment.java @@ -81,6 +81,7 @@ import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCall import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; +import awais.instagrabber.dialogs.GifPickerBottomDialogFragment; import awais.instagrabber.dialogs.MediaPickerBottomDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.fragments.UserSearchFragment; @@ -737,6 +738,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void hideInput() { binding.emojiToggle.setVisibility(View.GONE); + binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); binding.gallery.setVisibility(View.GONE); binding.input.setVisibility(View.GONE); @@ -750,6 +752,7 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact private void showInput() { binding.emojiToggle.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); binding.gallery.setVisibility(View.VISIBLE); binding.input.setVisibility(View.VISIBLE); @@ -788,16 +791,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact binding.send.setListenForRecord(true); startIconAnimation(); } - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); return; } if (binding.send.isListenForRecord()) { binding.send.setListenForRecord(false); startIconAnimation(); } - binding.gallery.setVisibility(View.GONE); + binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); + binding.gallery.setVisibility(View.GONE); } private String getDirectItemPreviewText(final DirectItem item) { @@ -937,8 +942,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact public void onStart() { isRecording = true; binding.input.setHint(null); - binding.gallery.setVisibility(View.GONE); + binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); + binding.gallery.setVisibility(View.GONE); if (PermissionUtils.hasAudioRecordPerms(context)) { viewModel.startRecording(); return; @@ -958,8 +964,9 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact public void onFinish(final long recordTime) { Log.d(TAG, "onFinish"); binding.input.setHint("Message"); - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); viewModel.stopRecording(false); isRecording = false; } @@ -971,16 +978,18 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact if (PermissionUtils.hasAudioRecordPerms(context)) { tooltip.show(binding.send); } - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); viewModel.stopRecording(true); isRecording = false; } }); binding.recordView.setOnBasketAnimationEndListener(() -> { binding.input.setHint(R.string.dms_thread_message_hint); - binding.gallery.setVisibility(View.VISIBLE); + binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); + binding.gallery.setVisibility(View.VISIBLE); }); binding.input.addTextChangedListener(new TextWatcherAdapter() { // int prevLength = 0; @@ -1057,6 +1066,16 @@ public class DirectMessageThreadFragment extends Fragment implements DirectReact mediaPicker.show(getChildFragmentManager(), "MediaPicker"); hideKeyboard(true); }); + binding.gif.setOnClickListener(v -> { + final GifPickerBottomDialogFragment gifPicker = GifPickerBottomDialogFragment.newInstance(); + gifPicker.setOnSelectListener(giphyGif -> { + gifPicker.dismiss(); + if (giphyGif == null) return; + handleSentMessage(viewModel.sendAnimatedMedia(giphyGif)); + }); + gifPicker.show(getChildFragmentManager(), "GifPicker"); + hideKeyboard(true); + }); binding.camera.setOnClickListener(v -> { final Intent intent = new Intent(context, CameraActivity.class); startActivityForResult(intent, CAMERA_REQUEST_CODE); diff --git a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java index 34eff7dc..ab31122a 100644 --- a/app/src/main/java/awais/instagrabber/managers/ThreadManager.java +++ b/app/src/main/java/awais/instagrabber/managers/ThreadManager.java @@ -53,6 +53,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThreadDeta import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; import awais.instagrabber.utils.BitmapUtils; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; @@ -641,6 +642,24 @@ public final class ThreadManager { return data; } + public LiveData> sendAnimatedMedia(@NonNull final GiphyGif giphyGif) { + final MutableLiveData> data = new MutableLiveData<>(); + final Long userId = getCurrentUserId(data); + if (userId == null) return data; + final String clientContext = UUID.randomUUID().toString(); + final DirectItem directItem = DirectItemFactory.createAnimatedMedia(userId, clientContext, giphyGif); + directItem.setPending(true); + addItems(0, Collections.singletonList(directItem)); + data.postValue(Resource.loading(directItem)); + final Call request = service.broadcastAnimatedMedia( + clientContext, + threadIdOrUserIds, + giphyGif + ); + enqueueRequest(request, data, directItem); + return data; + } + public void sendVoice(@NonNull final MutableLiveData> data, @NonNull final Uri uri, @NonNull final List waveform, diff --git a/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java b/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java index 7c399d0b..7fd57da0 100644 --- a/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java +++ b/app/src/main/java/awais/instagrabber/models/enums/BroadcastItemType.java @@ -7,7 +7,8 @@ public enum BroadcastItemType { IMAGE("configure_photo"), LINK("link"), VIDEO("configure_video"), - VOICE("share_voice"); + VOICE("share_voice"), + ANIMATED_MEDIA("animated_media"); private final String value; diff --git a/app/src/main/java/awais/instagrabber/repositories/GifRepository.java b/app/src/main/java/awais/instagrabber/repositories/GifRepository.java new file mode 100644 index 00000000..cf99e59f --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/GifRepository.java @@ -0,0 +1,14 @@ +package awais.instagrabber.repositories; + +import awais.instagrabber.repositories.responses.giphy.GiphyGifResponse; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Query; + +public interface GifRepository { + + @GET("/api/v1/creatives/story_media_search_keyed_format/") + Call searchGiphyGifs(@Query("request_surface") final String requestSurface, + @Query("q") final String query, + @Query("media_types") final String mediaTypes); +} diff --git a/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/AnimatedMediaBroadcastOptions.java b/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/AnimatedMediaBroadcastOptions.java new file mode 100644 index 00000000..8b71f194 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/requests/directmessages/AnimatedMediaBroadcastOptions.java @@ -0,0 +1,27 @@ +package awais.instagrabber.repositories.requests.directmessages; + +import java.util.HashMap; +import java.util.Map; + +import awais.instagrabber.models.enums.BroadcastItemType; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; + +public class AnimatedMediaBroadcastOptions extends BroadcastOptions { + + private final GiphyGif giphyGif; + + public AnimatedMediaBroadcastOptions(final String clientContext, + final ThreadIdOrUserIds threadIdOrUserIds, + final GiphyGif giphyGif) { + super(clientContext, threadIdOrUserIds, BroadcastItemType.ANIMATED_MEDIA); + this.giphyGif = giphyGif; + } + + @Override + public Map getFormMap() { + final Map form = new HashMap<>(); + form.put("is_sticker", String.valueOf(giphyGif.isSticker())); + form.put("id", giphyGif.getId()); + return form; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGif.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGif.java new file mode 100644 index 00000000..53332f3e --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGif.java @@ -0,0 +1,70 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public class GiphyGif { + private final String type; + private final String id; + private final String title; + private final int isSticker; + private final GiphyGifImages images; + + public GiphyGif(final String type, final String id, final String title, final int isSticker, final GiphyGifImages images) { + this.type = type; + this.id = id; + this.title = title; + this.isSticker = isSticker; + this.images = images; + } + + public String getType() { + return type; + } + + public String getId() { + return id; + } + + public String getTitle() { + return title; + } + + public boolean isSticker() { + return isSticker == 1; + } + + public GiphyGifImages getImages() { + return images; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGif giphyGif = (GiphyGif) o; + return isSticker == giphyGif.isSticker && + Objects.equals(type, giphyGif.type) && + Objects.equals(id, giphyGif.id) && + Objects.equals(title, giphyGif.title) && + Objects.equals(images, giphyGif.images); + } + + @Override + public int hashCode() { + return Objects.hash(type, id, title, isSticker, images); + } + + @NonNull + @Override + public String toString() { + return "GiphyGif{" + + "type='" + type + '\'' + + ", id='" + id + '\'' + + ", title='" + title + '\'' + + ", isSticker=" + isSticker() + + ", images=" + images + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImage.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImage.java new file mode 100644 index 00000000..d3659fe4 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImage.java @@ -0,0 +1,59 @@ +package awais.instagrabber.repositories.responses.giphy; + +import java.util.Objects; + +public class GiphyGifImage { + private final int height; + private final int width; + private final long webpSize; + private final String webp; + + public GiphyGifImage(final int height, final int width, final long webpSize, final String webp) { + this.height = height; + this.width = width; + this.webpSize = webpSize; + this.webp = webp; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public long getWebpSize() { + return webpSize; + } + + public String getWebp() { + return webp; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGifImage that = (GiphyGifImage) o; + return height == that.height && + width == that.width && + webpSize == that.webpSize && + Objects.equals(webp, that.webp); + } + + @Override + public int hashCode() { + return Objects.hash(height, width, webpSize, webp); + } + + @Override + public String toString() { + return "GiphyGifImage{" + + "height=" + height + + ", width=" + width + + ", webpSize=" + webpSize + + ", webp='" + webp + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImages.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImages.java new file mode 100644 index 00000000..230b17a1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifImages.java @@ -0,0 +1,40 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +import awais.instagrabber.repositories.responses.AnimatedMediaFixedHeight; + +public class GiphyGifImages { + private final AnimatedMediaFixedHeight fixedHeight; + + public GiphyGifImages(final AnimatedMediaFixedHeight fixedHeight) { + this.fixedHeight = fixedHeight; + } + + public AnimatedMediaFixedHeight getFixedHeight() { + return fixedHeight; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGifImages that = (GiphyGifImages) o; + return Objects.equals(fixedHeight, that.fixedHeight); + } + + @Override + public int hashCode() { + return Objects.hash(fixedHeight); + } + + @NonNull + @Override + public String toString() { + return "GiphyGifImages{" + + "fixedHeight=" + fixedHeight + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResponse.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResponse.java new file mode 100644 index 00000000..d9fa5d0c --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResponse.java @@ -0,0 +1,46 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public class GiphyGifResponse { + private final GiphyGifResults results; + private final String status; + + public GiphyGifResponse(final GiphyGifResults results, final String status) { + this.results = results; + this.status = status; + } + + public GiphyGifResults getResults() { + return results; + } + + public String getStatus() { + return status; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGifResponse that = (GiphyGifResponse) o; + return Objects.equals(results, that.results) && + Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(results, status); + } + + @NonNull + @Override + public String toString() { + return "GiphyGifResponse{" + + "results=" + results + + ", status='" + status + '\'' + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResults.java b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResults.java new file mode 100644 index 00000000..3f6fd94d --- /dev/null +++ b/app/src/main/java/awais/instagrabber/repositories/responses/giphy/GiphyGifResults.java @@ -0,0 +1,47 @@ +package awais.instagrabber.repositories.responses.giphy; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Objects; + +public class GiphyGifResults { + private final List giphyGifs; + private final List giphy; + + public GiphyGifResults(final List giphyGifs, final List giphy) { + this.giphyGifs = giphyGifs; + this.giphy = giphy; + } + + public List getGiphyGifs() { + return giphyGifs; + } + + public List getGiphy() { + return giphy; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final GiphyGifResults that = (GiphyGifResults) o; + return Objects.equals(giphyGifs, that.giphyGifs) && + Objects.equals(giphy, that.giphy); + } + + @Override + public int hashCode() { + return Objects.hash(giphyGifs, giphy); + } + + @NonNull + @Override + public String toString() { + return "GiphyGifResults{" + + "giphyGifs=" + giphyGifs + + ", giphy=" + giphy + + '}'; + } +} diff --git a/app/src/main/java/awais/instagrabber/utils/DMUtils.java b/app/src/main/java/awais/instagrabber/utils/DMUtils.java index 6ff6126a..81429ff7 100644 --- a/app/src/main/java/awais/instagrabber/utils/DMUtils.java +++ b/app/src/main/java/awais/instagrabber/utils/DMUtils.java @@ -15,6 +15,7 @@ import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia; import awais.instagrabber.repositories.responses.directmessages.DirectItemReelShare; import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia; import awais.instagrabber.repositories.responses.directmessages.DirectThread; @@ -84,11 +85,16 @@ public final class DMUtils { message = item.getPlaceholder().getMessage(); break; case MEDIA_SHARE: - subtitle = resources.getString(R.string.dms_inbox_shared_post, username != null ? username : "", - item.getMediaShare().getUser().getUsername()); + final User mediaShareUser = item.getMediaShare().getUser(); + subtitle = resources.getString(R.string.dms_inbox_shared_post, + username != null ? username : "", + mediaShareUser == null ? "" : mediaShareUser.getUsername()); break; case ANIMATED_MEDIA: - subtitle = resources.getString(R.string.dms_inbox_shared_gif, username != null ? username : ""); + final DirectItemAnimatedMedia animatedMedia = item.getAnimatedMedia(); + subtitle = resources.getString(animatedMedia.isSticker() ? R.string.dms_inbox_shared_sticker + : R.string.dms_inbox_shared_gif, + username != null ? username : ""); break; case PROFILE: subtitle = resources @@ -111,8 +117,10 @@ public final class DMUtils { final int format = reelType.equals("highlight_reel") ? R.string.dms_inbox_shared_highlight : R.string.dms_inbox_shared_story; - subtitle = resources.getString(format, username != null ? username : "", - item.getStoryShare().getMedia().getUser().getUsername()); + final User storyShareMediaUser = item.getStoryShare().getMedia().getUser(); + subtitle = resources.getString(format, + username != null ? username : "", + storyShareMediaUser == null ? "" : storyShareMediaUser.getUsername()); } break; } @@ -126,12 +134,16 @@ public final class DMUtils { subtitle = item.getVideoCallEvent().getDescription(); break; case CLIP: - subtitle = resources.getString(R.string.dms_inbox_shared_clip, username != null ? username : "", - item.getClip().getClip().getUser().getUsername()); + final User clipUser = item.getClip().getClip().getUser(); + subtitle = resources.getString(R.string.dms_inbox_shared_clip, + username != null ? username : "", + clipUser == null ? "" : clipUser.getUsername()); break; case FELIX_SHARE: - subtitle = resources.getString(R.string.dms_inbox_shared_igtv, username != null ? username : "", - item.getFelixShare().getVideo().getUser().getUsername()); + final User felixShareVideoUser = item.getFelixShare().getVideo().getUser(); + subtitle = resources.getString(R.string.dms_inbox_shared_igtv, + username != null ? username : "", + felixShareVideoUser == null ? "" : felixShareVideoUser.getUsername()); break; case RAVEN_MEDIA: subtitle = getRavenMediaSubtitle(item, resources, username); diff --git a/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java b/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java index 4b319032..85e6f9ae 100644 --- a/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java +++ b/app/src/main/java/awais/instagrabber/utils/DirectItemFactory.java @@ -8,13 +8,16 @@ import java.util.UUID; import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.models.enums.MediaItemType; +import awais.instagrabber.repositories.responses.AnimatedMediaImages; import awais.instagrabber.repositories.responses.Audio; import awais.instagrabber.repositories.responses.ImageVersions2; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.MediaCandidate; import awais.instagrabber.repositories.responses.VideoVersion; import awais.instagrabber.repositories.responses.directmessages.DirectItem; +import awais.instagrabber.repositories.responses.directmessages.DirectItemAnimatedMedia; import awais.instagrabber.repositories.responses.directmessages.DirectItemVoiceMedia; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; public final class DirectItemFactory { @@ -213,4 +216,45 @@ public final class DirectItemFactory { 0, false); } + + public static DirectItem createAnimatedMedia(final long userId, + final String clientContext, + final GiphyGif giphyGif) { + final AnimatedMediaImages animatedImages = new AnimatedMediaImages(giphyGif.getImages().getFixedHeight()); + final DirectItemAnimatedMedia animateMedia = new DirectItemAnimatedMedia( + giphyGif.getId(), + animatedImages, + false, + giphyGif.isSticker() + ); + return new DirectItem( + UUID.randomUUID().toString(), + userId, + System.currentTimeMillis() * 1000, + DirectItemType.ANIMATED_MEDIA, + null, + null, + null, + clientContext, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + animateMedia, + null, + null, + null, + null, + 0, + false + ); + } } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java index 860728c2..f2646504 100644 --- a/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java +++ b/app/src/main/java/awais/instagrabber/viewmodels/DirectThreadViewModel.java @@ -26,6 +26,7 @@ import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; import awais.instagrabber.utils.Constants; import awais.instagrabber.utils.CookieUtils; import awais.instagrabber.utils.DirectoryUtils; @@ -219,6 +220,10 @@ public class DirectThreadViewModel extends AndroidViewModel { return threadManager.unsend(item); } + public LiveData> sendAnimatedMedia(@NonNull final GiphyGif giphyGif) { + return threadManager.sendAnimatedMedia(giphyGif); + } + public User getCurrentUser() { return currentUser; } diff --git a/app/src/main/java/awais/instagrabber/viewmodels/GifPickerViewModel.java b/app/src/main/java/awais/instagrabber/viewmodels/GifPickerViewModel.java new file mode 100644 index 00000000..e78b19db --- /dev/null +++ b/app/src/main/java/awais/instagrabber/viewmodels/GifPickerViewModel.java @@ -0,0 +1,121 @@ +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 com.google.common.collect.ImmutableList; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import awais.instagrabber.models.Resource; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; +import awais.instagrabber.repositories.responses.giphy.GiphyGifResponse; +import awais.instagrabber.repositories.responses.giphy.GiphyGifResults; +import awais.instagrabber.webservices.GifService; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class GifPickerViewModel extends ViewModel { + private static final String TAG = GifPickerViewModel.class.getSimpleName(); + + private final MutableLiveData>> images = new MutableLiveData<>(Resource.success(Collections.emptyList())); + private final GifService gifService; + + private Call searchRequest; + + public GifPickerViewModel() { + gifService = GifService.getInstance(); + search(null); + } + + public LiveData>> getImages() { + return images; + } + + public void search(final String query) { + final Resource> currentValue = images.getValue(); + if (currentValue != null && currentValue.status == Resource.Status.LOADING) { + cancelSearchRequest(); + } + images.postValue(Resource.loading(getCurrentImages())); + searchRequest = gifService.searchGiphyGifs(query, query != null); + searchRequest.enqueue(new Callback() { + @Override + public void onResponse(@NonNull final Call call, + @NonNull final Response response) { + if (response.isSuccessful()) { + parseResponse(response); + return; + } + if (response.errorBody() != null) { + try { + final String string = response.errorBody().string(); + final String msg = String.format(Locale.US, + "onResponse: url: %s, responseCode: %d, errorBody: %s", + call.request().url().toString(), + response.code(), + string); + images.postValue(Resource.error(msg, getCurrentImages())); + Log.e(TAG, msg); + } catch (IOException e) { + images.postValue(Resource.error(e.getMessage(), getCurrentImages())); + Log.e(TAG, "onResponse: ", e); + } + } + images.postValue(Resource.error("request was not successful and response error body was null", getCurrentImages())); + } + + @Override + public void onFailure(@NonNull final Call call, + @NonNull final Throwable t) { + images.postValue(Resource.error(t.getMessage(), getCurrentImages())); + Log.e(TAG, "enqueueRequest: onFailure: ", t); + } + }); + } + + private void parseResponse(final Response response) { + final GiphyGifResponse giphyGifResponse = response.body(); + if (giphyGifResponse == null) { + images.postValue(Resource.error("Response body was null", getCurrentImages())); + return; + } + final GiphyGifResults results = giphyGifResponse.getResults(); + images.postValue(Resource.success( + ImmutableList.builder() + .addAll(results.getGiphy() == null ? Collections.emptyList() : results.getGiphy()) + .addAll(results.getGiphyGifs() == null ? Collections.emptyList() : results.getGiphyGifs()) + .build() + )); + } + + // @NonNull + // private List getGiphyGifImages(@NonNull final List giphy) { + // return giphy.stream() + // .map(giphyGif -> { + // final GiphyGifImages images = giphyGif.getImages(); + // if (images == null) return null; + // return images.getOriginal(); + // }) + // .filter(Objects::nonNull) + // .collect(Collectors.toList()); + // } + + private List getCurrentImages() { + final Resource> value = images.getValue(); + return value == null ? Collections.emptyList() : value.data; + } + + public void cancelSearchRequest() { + if (searchRequest == null) return; + searchRequest.cancel(); + } +} diff --git a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java index 2b3670c9..3d70c240 100644 --- a/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java +++ b/app/src/main/java/awais/instagrabber/webservices/DirectMessagesService.java @@ -17,6 +17,7 @@ import java.util.UUID; import java.util.stream.Collectors; import awais.instagrabber.repositories.DirectMessagesRepository; +import awais.instagrabber.repositories.requests.directmessages.AnimatedMediaBroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions; import awais.instagrabber.repositories.requests.directmessages.BroadcastOptions.ThreadIdOrUserIds; import awais.instagrabber.repositories.requests.directmessages.LinkBroadcastOptions; @@ -34,6 +35,7 @@ import awais.instagrabber.repositories.responses.directmessages.DirectThreadDeta import awais.instagrabber.repositories.responses.directmessages.DirectThreadFeedResponse; import awais.instagrabber.repositories.responses.directmessages.DirectThreadParticipantRequestsResponse; import awais.instagrabber.repositories.responses.directmessages.RankedRecipientsResponse; +import awais.instagrabber.repositories.responses.giphy.GiphyGif; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import retrofit2.Call; @@ -182,6 +184,11 @@ public class DirectMessagesService extends BaseService { return broadcast(new ReactionBroadcastOptions(clientContext, threadIdOrUserIds, itemId, emoji, delete)); } + public Call broadcastAnimatedMedia(final String clientContext, + final ThreadIdOrUserIds threadIdOrUserIds, + final GiphyGif giphyGif) { + return broadcast(new AnimatedMediaBroadcastOptions(clientContext, threadIdOrUserIds, giphyGif)); + } private Call broadcast(@NonNull final BroadcastOptions broadcastOptions) { if (TextUtils.isEmpty(broadcastOptions.getClientContext())) { diff --git a/app/src/main/java/awais/instagrabber/webservices/GifService.java b/app/src/main/java/awais/instagrabber/webservices/GifService.java new file mode 100644 index 00000000..6485efd1 --- /dev/null +++ b/app/src/main/java/awais/instagrabber/webservices/GifService.java @@ -0,0 +1,33 @@ +package awais.instagrabber.webservices; + +import awais.instagrabber.repositories.GifRepository; +import awais.instagrabber.repositories.responses.giphy.GiphyGifResponse; +import retrofit2.Call; +import retrofit2.Retrofit; + +public class GifService extends BaseService { + + private final GifRepository repository; + + private static GifService instance; + + private GifService() { + final Retrofit retrofit = getRetrofitBuilder() + .baseUrl("https://i.instagram.com") + .build(); + repository = retrofit.create(GifRepository.class); + } + + public static GifService getInstance() { + if (instance == null) { + instance = new GifService(); + } + return instance; + } + + public Call searchGiphyGifs(final String query, + final boolean includeGifs) { + final String mediaTypes = includeGifs ? "[\"giphy_gifs\",\"giphy\"]" : "[\"giphy\"]"; + return repository.searchGiphyGifs("direct", query, mediaTypes); + } +} diff --git a/app/src/main/res/drawable/ic_round_gif_24.xml b/app/src/main/res/drawable/ic_round_gif_24.xml new file mode 100644 index 00000000..c2b5340c --- /dev/null +++ b/app/src/main/res/drawable/ic_round_gif_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/fragment_direct_messages_thread.xml b/app/src/main/res/layout/fragment_direct_messages_thread.xml index 79a1091c..56674d95 100644 --- a/app/src/main/res/layout/fragment_direct_messages_thread.xml +++ b/app/src/main/res/layout/fragment_direct_messages_thread.xml @@ -120,7 +120,8 @@ app:layout_constraintBottom_toBottomOf="@id/input" app:layout_constraintEnd_toStartOf="@id/send" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="@id/input" /> + app:layout_constraintTop_toTopOf="@id/input" + tools:visibility="visible" /> + app:strokeWidth="1dp" + tools:visibility="visible" /> + app:layout_goneMarginEnd="24dp" + tools:visibility="visible" /> + + + tools:visibility="visible" /> + tools:visibility="visible" /> + tools:visibility="visible" /> + app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.Button.Circle" + tools:visibility="visible" /> + app:layout_constraintStart_toStartOf="parent" + tools:visibility="visible" /> + tools:visibility="gone" /> + tools:visibility="gone" /> + tools:visibility="gone" /> \ No newline at end of file diff --git a/app/src/main/res/layout/layout_gif_picker.xml b/app/src/main/res/layout/layout_gif_picker.xml new file mode 100644 index 00000000..b8b736b9 --- /dev/null +++ b/app/src/main/res/layout/layout_gif_picker.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 676aa79d..da8ae5ea 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -185,6 +185,7 @@ %s shared a video %s sent a message %s shared a gif + %s shared a sticker %s shared a profile: @%s %s shared a location: %s %s shared a story highlight by @%s @@ -490,4 +491,5 @@ Auto refresh every secs mins + Search GIPHY