package awais.instagrabber.fragments.directmessages; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Animatable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.text.Editable; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; import androidx.activity.OnBackPressedCallback; import androidx.activity.OnBackPressedDispatcher; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.appcompat.view.menu.ActionMenuItemView; import androidx.core.view.ViewCompat; import androidx.core.view.WindowInsetsAnimationCompat; import androidx.core.view.WindowInsetsAnimationControlListenerCompat; import androidx.core.view.WindowInsetsAnimationControllerCompat; import androidx.core.view.WindowInsetsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProvider; import androidx.navigation.NavBackStackEntry; import androidx.navigation.NavController; import androidx.navigation.NavDirections; import androidx.navigation.fragment.NavHostFragment; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.SimpleItemAnimator; import androidx.transition.TransitionManager; import androidx.vectordrawable.graphics.drawable.Animatable2Compat; import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat; import com.google.android.material.badge.BadgeDrawable; import com.google.android.material.badge.BadgeUtils; import com.google.android.material.internal.ToolbarUtils; import com.google.android.material.snackbar.Snackbar; import com.google.common.collect.ImmutableList; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.function.Function; import awais.instagrabber.ProfileNavGraphDirections; import awais.instagrabber.R; import awais.instagrabber.UserSearchNavGraphDirections; import awais.instagrabber.activities.CameraActivity; import awais.instagrabber.activities.MainActivity; import awais.instagrabber.adapters.DirectItemsAdapter; import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemCallback; import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemLongClickListener; import awais.instagrabber.adapters.DirectItemsAdapter.DirectItemOrHeader; import awais.instagrabber.adapters.DirectReactionsAdapter; import awais.instagrabber.adapters.viewholder.directmessages.DirectItemViewHolder; import awais.instagrabber.animations.CubicBezierInterpolator; import awais.instagrabber.customviews.InsetsAnimationLinearLayout; import awais.instagrabber.customviews.KeyNotifyingEmojiEditText; import awais.instagrabber.customviews.RecordView; import awais.instagrabber.customviews.Tooltip; import awais.instagrabber.customviews.emoji.Emoji; import awais.instagrabber.customviews.emoji.EmojiBottomSheetDialog; import awais.instagrabber.customviews.emoji.EmojiPicker; import awais.instagrabber.customviews.helpers.ControlFocusInsetsAnimationCallback; import awais.instagrabber.customviews.helpers.EmojiPickerInsetsAnimationCallback; import awais.instagrabber.customviews.helpers.HeaderItemDecoration; import awais.instagrabber.customviews.helpers.RecyclerLazyLoaderAtEdge; import awais.instagrabber.customviews.helpers.SimpleImeAnimationController; import awais.instagrabber.customviews.helpers.SwipeAndRestoreItemTouchHelperCallback; import awais.instagrabber.customviews.helpers.TextWatcherAdapter; import awais.instagrabber.customviews.helpers.TranslateDeferringInsetsAnimationCallback; import awais.instagrabber.databinding.FragmentDirectMessagesThreadBinding; import awais.instagrabber.dialogs.DirectItemReactionDialogFragment; import awais.instagrabber.dialogs.GifPickerBottomDialogFragment; import awais.instagrabber.fragments.PostViewV2Fragment; import awais.instagrabber.fragments.UserSearchFragment; import awais.instagrabber.fragments.UserSearchFragmentDirections; import awais.instagrabber.fragments.settings.PreferenceKeys; import awais.instagrabber.models.Resource; import awais.instagrabber.models.enums.DirectItemType; import awais.instagrabber.models.enums.MediaItemType; import awais.instagrabber.repositories.requests.StoryViewerOptions; import awais.instagrabber.repositories.responses.Media; import awais.instagrabber.repositories.responses.User; import awais.instagrabber.repositories.responses.directmessages.DirectItem; import awais.instagrabber.repositories.responses.directmessages.DirectItemEmojiReaction; import awais.instagrabber.repositories.responses.directmessages.DirectItemStoryShare; import awais.instagrabber.repositories.responses.directmessages.DirectItemVisualMedia; import awais.instagrabber.repositories.responses.directmessages.DirectThread; import awais.instagrabber.repositories.responses.directmessages.RankedRecipient; import awais.instagrabber.utils.AppExecutors; import awais.instagrabber.utils.DMUtils; import awais.instagrabber.utils.DownloadUtils; import awais.instagrabber.utils.PermissionUtils; import awais.instagrabber.utils.ResponseBodyUtils; import awais.instagrabber.utils.TextUtils; import awais.instagrabber.utils.Utils; import awais.instagrabber.viewmodels.AppStateViewModel; import awais.instagrabber.viewmodels.DirectThreadViewModel; import awais.instagrabber.viewmodels.factories.DirectThreadViewModelFactory; public class DirectMessageThreadFragment extends Fragment implements DirectReactionsAdapter.OnReactionClickListener, EmojiPicker.OnEmojiClickListener { private static final String TAG = DirectMessageThreadFragment.class.getSimpleName(); private static final int AUDIO_RECORD_PERM_REQUEST_CODE = 1000; private static final int CAMERA_REQUEST_CODE = 200; private static final int FILE_PICKER_REQUEST_CODE = 500; private static final String TRANSLATION_Y = "translationY"; private DirectItemsAdapter itemsAdapter; private MainActivity fragmentActivity; private DirectThreadViewModel viewModel; private InsetsAnimationLinearLayout root; private boolean shouldRefresh = true; private List itemOrHeaders; private List users; private FragmentDirectMessagesThreadBinding binding; private Tooltip tooltip; private float initialSendX; private ActionBar actionBar; private AppStateViewModel appStateViewModel; private Runnable prevTitleRunnable; private AnimatorSet animatorSet; private boolean isRecording; private DirectItemReactionDialogFragment reactionDialogFragment; private DirectItem itemToForward; private MutableLiveData backStackSavedStateResultLiveData; private int prevLength; private BadgeDrawable pendingRequestCountBadgeDrawable; private boolean isPendingRequestCountBadgeAttached = false; private ItemTouchHelper itemTouchHelper; private LiveData pendingLiveData; private LiveData threadLiveData; private LiveData inputModeLiveData; private LiveData threadTitleLiveData; private LiveData> fetchingLiveData; private LiveData> itemsLiveData; private LiveData replyToItemLiveData; private LiveData pendingRequestsCountLiveData; private LiveData> usersLiveData; private boolean autoMarkAsSeen = false; private MenuItem markAsSeenMenuItem; private Media tempMedia; private DirectItem addReactionItem; private TranslateDeferringInsetsAnimationCallback inputHolderAnimationCallback; private TranslateDeferringInsetsAnimationCallback chatsAnimationCallback; private EmojiPickerInsetsAnimationCallback emojiPickerAnimationCallback; private boolean hasKbOpenedOnce; private boolean wasToggled; private final AppExecutors appExecutors = AppExecutors.INSTANCE; private final Animatable2Compat.AnimationCallback micToSendAnimationCallback = new Animatable2Compat.AnimationCallback() { @Override public void onAnimationEnd(final Drawable drawable) { AnimatedVectorDrawableCompat.unregisterAnimationCallback(drawable, this); setSendToMicIcon(); } }; private final Animatable2Compat.AnimationCallback sendToMicAnimationCallback = new Animatable2Compat.AnimationCallback() { @Override public void onAnimationEnd(final Drawable drawable) { AnimatedVectorDrawableCompat.unregisterAnimationCallback(drawable, this); setMicToSendIcon(); } }; private final DirectItemCallback directItemCallback = new DirectItemCallback() { @Override public void onHashtagClick(final String hashtag) { final NavDirections action = DirectMessageThreadFragmentDirections.actionGlobalHashTagFragment(hashtag); NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action); } @Override public void onMentionClick(final String mention) { navigateToUser(mention); } @Override public void onLocationClick(final long locationId) { final NavDirections action = DirectMessageThreadFragmentDirections.actionGlobalLocationFragment(locationId); NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action); } @Override public void onURLClick(final String url) { final Context context = getContext(); if (context == null) return; Utils.openURL(context, url); } @Override public void onEmailClick(final String email) { final Context context = getContext(); if (context == null) return; Utils.openEmailAddress(context, email); } @Override public void onMediaClick(final Media media, final int index) { if (media.isReelMedia()) { final String pk = media.getPk(); try { final long mediaId = Long.parseLong(pk); final User user = media.getUser(); if (user == null) return; final String username = user.getUsername(); final NavDirections action = DirectMessageThreadFragmentDirections .actionThreadToStory(StoryViewerOptions.forStory(mediaId, username)); NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action); } catch (NumberFormatException e) { Log.e(TAG, "onMediaClick (story): ", e); } return; } final NavController navController = NavHostFragment.findNavController(DirectMessageThreadFragment.this); final Bundle bundle = new Bundle(); bundle.putSerializable(PostViewV2Fragment.ARG_MEDIA, media); bundle.putInt(PostViewV2Fragment.ARG_SLIDER_POSITION, index); try { navController.navigate(R.id.action_global_post_view, bundle); } catch (Exception e) { Log.e(TAG, "openPostDialog: ", e); } } @Override public void onStoryClick(final DirectItemStoryShare storyShare) { final String pk = storyShare.getReelId(); try { final long mediaId = Long.parseLong(pk); final User user = storyShare.getMedia().getUser(); if (user == null) return; final String username = user.getUsername(); final NavDirections action = DirectMessageThreadFragmentDirections .actionThreadToStory(StoryViewerOptions.forUser(mediaId, username)); NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(action); } catch (NumberFormatException e) { Log.e(TAG, "onStoryClick: ", e); } } @Override public void onReaction(final DirectItem item, final Emoji emoji) { if (item == null || emoji == null) return; final LiveData> resourceLiveData = viewModel.sendReaction(item, emoji); if (resourceLiveData != null) { resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } } @Override public void onReactionClick(final DirectItem item, final int position) { showReactionsDialog(item); } @Override public void onOptionSelect(final DirectItem item, final int itemId, final Function cb) { if (itemId == R.id.unsend) { handleSentMessage(viewModel.unsend(item)); return; } if (itemId == R.id.forward) { itemToForward = item; final UserSearchNavGraphDirections.ActionGlobalUserSearch actionGlobalUserSearch = UserSearchFragmentDirections .actionGlobalUserSearch() .setTitle(getString(R.string.forward)) .setActionLabel(getString(R.string.send)) .setShowGroups(true) .setMultiple(true) .setSearchMode(UserSearchFragment.SearchMode.RAVEN); final NavController navController = NavHostFragment.findNavController(DirectMessageThreadFragment.this); navController.navigate(actionGlobalUserSearch); } if (itemId == R.id.download) { downloadItem(item); return; } // otherwise call callback if present if (cb != null) { cb.apply(item); } } @Override public void onAddReactionListener(final DirectItem item) { if (item == null) return; addReactionItem = item; final EmojiBottomSheetDialog emojiBottomSheetDialog = EmojiBottomSheetDialog.newInstance(); emojiBottomSheetDialog.show(getChildFragmentManager(), EmojiBottomSheetDialog.TAG); } }; private final DirectItemLongClickListener directItemLongClickListener = position -> { // viewModel.setSelectedPosition(position); }; private final Observer backStackSavedStateObserver = result -> { if (result == null) return; if (result instanceof Uri) { final Uri uri = (Uri) result; handleSentMessage(viewModel.sendUri(uri)); } else if ((result instanceof RankedRecipient)) { // Log.d(TAG, "result: " + result); if (itemToForward != null) { viewModel.forward((RankedRecipient) result, itemToForward); } } else if ((result instanceof Set)) { try { // Log.d(TAG, "result: " + result); if (itemToForward != null) { //noinspection unchecked viewModel.forward((Set) result, itemToForward); } } catch (Exception e) { Log.e(TAG, "forward result: ", e); } } // clear result backStackSavedStateResultLiveData.postValue(null); }; private final MutableLiveData inputLength = new MutableLiveData<>(0); private final MutableLiveData emojiPickerVisible = new MutableLiveData<>(false); private final MutableLiveData kbVisible = new MutableLiveData<>(false); private final OnBackPressedCallback onEmojiPickerBackPressedCallback = new OnBackPressedCallback(false) { @Override public void handleOnBackPressed() { emojiPickerVisible.postValue(false); } }; @Override public void onCreate(@Nullable final Bundle savedInstanceState) { super.onCreate(savedInstanceState); fragmentActivity = (MainActivity) requireActivity(); appStateViewModel = new ViewModelProvider(fragmentActivity).get(AppStateViewModel.class); autoMarkAsSeen = Utils.settingsHelper.getBoolean(PreferenceKeys.DM_MARK_AS_SEEN); final Bundle arguments = getArguments(); if (arguments == null) return; final DirectMessageThreadFragmentArgs fragmentArgs = DirectMessageThreadFragmentArgs.fromBundle(arguments); final Resource currentUserResource = appStateViewModel.getCurrentUser(); if (currentUserResource == null) return; final User currentUser = currentUserResource.data; if (currentUser == null) return; final DirectThreadViewModelFactory viewModelFactory = new DirectThreadViewModelFactory( fragmentActivity.getApplication(), fragmentArgs.getThreadId(), fragmentArgs.getPending(), currentUser ); viewModel = new ViewModelProvider(this, viewModelFactory).get(DirectThreadViewModel.class); setHasOptionsMenu(true); } @Override public View onCreateView(@NonNull final LayoutInflater inflater, final ViewGroup container, final Bundle savedInstanceState) { if (root != null) { shouldRefresh = false; return root; } binding = FragmentDirectMessagesThreadBinding.inflate(inflater, container, false); binding.send.setRecordView(binding.recordView); root = binding.getRoot(); final Context context = getContext(); if (context == null) { return root; } tooltip = new Tooltip(context, root, getResources().getColor(R.color.grey_400), getResources().getColor(R.color.black)); // todo check has camera and remove view return root; } @Override public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) { // WindowCompat.setDecorFitsSystemWindows(fragmentActivity.getWindow(), false); if (!shouldRefresh) return; init(); binding.send.post(() -> initialSendX = binding.send.getX()); shouldRefresh = false; } @Override public void onCreateOptionsMenu(@NonNull final Menu menu, @NonNull final MenuInflater inflater) { inflater.inflate(R.menu.dm_thread_menu, menu); markAsSeenMenuItem = menu.findItem(R.id.mark_as_seen); if (markAsSeenMenuItem != null) { if (autoMarkAsSeen) { markAsSeenMenuItem.setVisible(false); } else { markAsSeenMenuItem.setEnabled(false); } } } @Override public boolean onOptionsItemSelected(@NonNull final MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.info) { final DirectMessageThreadFragmentDirections.ActionThreadToSettings directions = DirectMessageThreadFragmentDirections .actionThreadToSettings(viewModel.getThreadId(), null); final Boolean pending = viewModel.isPending().getValue(); directions.setPending(pending != null && pending); NavHostFragment.findNavController(this).navigate(directions); return true; } if (itemId == R.id.mark_as_seen) { handleMarkAsSeen(item); return true; } if (itemId == R.id.refresh && viewModel != null) { viewModel.refreshChats(); return true; } return super.onOptionsItemSelected(item); } private void handleMarkAsSeen(@NonNull final MenuItem item) { final LiveData> resourceLiveData = viewModel.markAsSeen(); resourceLiveData.observe(getViewLifecycleOwner(), new Observer>() { @Override public void onChanged(final Resource resource) { try { if (resource == null) return; final Context context = getContext(); if (context == null) return; switch (resource.status) { case SUCCESS: Toast.makeText(context, R.string.marked_as_seen, Toast.LENGTH_SHORT).show(); case LOADING: item.setEnabled(false); break; case ERROR: item.setEnabled(true); if (resource.message != null) { Snackbar.make(context, binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); return; } if (resource.resId != 0) { Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); return; } break; } } finally { resourceLiveData.removeObserver(this); } } }); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == FILE_PICKER_REQUEST_CODE && resultCode == Activity.RESULT_OK) { if (data == null || data.getData() == null) { Log.w(TAG, "data is null!"); return; } final Context context = getContext(); if (context == null) { Log.w(TAG, "conetxt is null!"); return; } final Uri uri = data.getData(); final String mimeType = Utils.getMimeType(uri, context.getContentResolver()); if (mimeType.startsWith("image")) { navigateToImageEditFragment(uri); return; } handleSentMessage(viewModel.sendUri(uri)); } if (requestCode == CAMERA_REQUEST_CODE && resultCode == Activity.RESULT_OK) { if (data == null || data.getData() == null) { Log.w(TAG, "data is null!"); return; } final Uri uri = data.getData(); navigateToImageEditFragment(uri); } } @Override public void onRequestPermissionsResult(final int requestCode, @NonNull final String[] permissions, @NonNull final int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); final Context context = getContext(); if (context == null) return; if (requestCode == AUDIO_RECORD_PERM_REQUEST_CODE) { if (PermissionUtils.hasAudioRecordPerms(context)) { Toast.makeText(context, "You can send voice messages now!", Toast.LENGTH_LONG).show(); return; } Toast.makeText(context, "Require RECORD_AUDIO permission", Toast.LENGTH_LONG).show(); } } @Override public void onPause() { if (isRecording) { binding.recordView.cancelRecording(binding.send); } emojiPickerVisible.postValue(false); kbVisible.postValue(false); binding.inputHolder.setTranslationY(0); binding.chats.setTranslationY(0); binding.emojiPicker.setTranslationY(0); removeObservers(); super.onPause(); } @Override public void onResume() { super.onResume(); if (initialSendX != 0) { binding.send.setX(initialSendX); } binding.send.stopScale(); final OnBackPressedDispatcher onBackPressedDispatcher = fragmentActivity.getOnBackPressedDispatcher(); onBackPressedDispatcher.addCallback(onEmojiPickerBackPressedCallback); setupBackStackResultObserver(); setObservers(); // attachPendingRequestsBadge(viewModel.getPendingRequestsCount().getValue()); } @Override public void onDestroyView() { super.onDestroyView(); cleanup(); } @Override public void onDestroy() { viewModel.deleteThreadIfRequired(); super.onDestroy(); } @SuppressLint("UnsafeOptInUsageError") private void cleanup() { if (prevTitleRunnable != null) { appExecutors.getMainThread().cancel(prevTitleRunnable); } for (int childCount = binding.chats.getChildCount(), i = 0; i < childCount; ++i) { final RecyclerView.ViewHolder holder = binding.chats.getChildViewHolder(binding.chats.getChildAt(i)); if (holder == null) continue; if (holder instanceof DirectItemViewHolder) { ((DirectItemViewHolder) holder).cleanup(); } } isPendingRequestCountBadgeAttached = false; if (pendingRequestCountBadgeDrawable != null) { @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info); if (menuItemView != null) { BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); } pendingRequestCountBadgeDrawable = null; } } private void init() { final Context context = getContext(); if (context == null) return; if (getArguments() == null) return; actionBar = fragmentActivity.getSupportActionBar(); setupList(); root.post(this::setupInput); } private void setupList() { final Context context = getContext(); if (context == null) return; binding.chats.setItemViewCacheSize(20); final LinearLayoutManager layoutManager = new LinearLayoutManager(context); layoutManager.setReverseLayout(true); // layoutManager.setStackFromEnd(false); // binding.messageList.addItemDecoration(new VerticalSpaceItemDecoration(3)); final RecyclerView.ItemAnimator animator = binding.chats.getItemAnimator(); if (animator instanceof SimpleItemAnimator) { final SimpleItemAnimator itemAnimator = (SimpleItemAnimator) animator; itemAnimator.setSupportsChangeAnimations(false); } binding.chats.setLayoutManager(layoutManager); binding.chats.addOnScrollListener(new RecyclerLazyLoaderAtEdge(layoutManager, true, page -> viewModel.fetchChats())); final HeaderItemDecoration headerItemDecoration = new HeaderItemDecoration(binding.chats, itemPosition -> { if (itemOrHeaders == null || itemOrHeaders.isEmpty()) return false; try { final DirectItemOrHeader itemOrHeader = itemOrHeaders.get(itemPosition); return itemOrHeader.isHeader(); } catch (IndexOutOfBoundsException e) { return false; } }); binding.chats.addItemDecoration(headerItemDecoration); final SwipeAndRestoreItemTouchHelperCallback touchHelperCallback = new SwipeAndRestoreItemTouchHelperCallback( context, (adapterPosition, viewHolder) -> { if (itemsAdapter == null) return; final DirectItemOrHeader directItemOrHeader = itemsAdapter.getList().get(adapterPosition); if (directItemOrHeader.isHeader()) return; viewModel.setReplyToItem(directItemOrHeader.item); } ); final Integer inputMode = viewModel.getInputMode().getValue(); if (inputMode != null && inputMode != 1) { itemTouchHelper = new ItemTouchHelper(touchHelperCallback); itemTouchHelper.attachToRecyclerView(binding.chats); } } private void setObservers() { threadLiveData = viewModel.getThread(); // if (threadLiveData == null) { // final NavController navController = NavHostFragment.findNavController(this); // navController.navigateUp(); // return; // } pendingLiveData = viewModel.isPending(); pendingLiveData.observe(getViewLifecycleOwner(), isPending -> { if (isPending == null) { hideInput(); return; } if (isPending) { showPendingOptions(); return; } hidePendingOptions(); final Integer inputMode = viewModel.getInputMode().getValue(); if (inputMode != null && inputMode == 1) return; showInput(); }); inputModeLiveData = viewModel.getInputMode(); inputModeLiveData.observe(getViewLifecycleOwner(), inputMode -> { final Boolean isPending = viewModel.isPending().getValue(); if (isPending != null && isPending) return; if (inputMode == null || inputMode == 0) return; if (inputMode == 1) { hideInput(); } }); threadTitleLiveData = viewModel.getThreadTitle(); threadTitleLiveData.observe(getViewLifecycleOwner(), this::setTitle); fetchingLiveData = viewModel.isFetching(); fetchingLiveData.observe(getViewLifecycleOwner(), fetchingResource -> { if (fetchingResource == null) return; switch (fetchingResource.status) { case SUCCESS: case ERROR: setTitle(viewModel.getThreadTitle().getValue()); if (fetchingResource.message != null) { Snackbar.make(binding.getRoot(), fetchingResource.message, Snackbar.LENGTH_LONG).show(); } if (fetchingResource.resId != 0) { Snackbar.make(binding.getRoot(), fetchingResource.resId, Snackbar.LENGTH_LONG).show(); } break; case LOADING: setTitle(getString(R.string.dms_thread_updating)); break; } }); // final ItemsAdapterDataMerger itemsAdapterDataMerger = new ItemsAdapterDataMerger(appStateViewModel.getCurrentUser(), viewModel.getThread()); // itemsAdapterDataMerger.observe(getViewLifecycleOwner(), userThreadPair -> { // viewModel.setCurrentUser(userThreadPair.first); // setupItemsAdapter(userThreadPair.first, userThreadPair.second); // }); threadLiveData.observe(getViewLifecycleOwner(), this::setupItemsAdapter); itemsLiveData = viewModel.getItems(); itemsLiveData.observe(getViewLifecycleOwner(), this::submitItemsToAdapter); replyToItemLiveData = viewModel.getReplyToItem(); replyToItemLiveData.observe(getViewLifecycleOwner(), item -> { if (item == null) { if (binding.input.length() == 0) { showExtraInputOption(true); } binding.getRoot().post(() -> { TransitionManager.beginDelayedTransition(binding.getRoot()); binding.replyBg.setVisibility(View.GONE); binding.replyInfo.setVisibility(View.GONE); binding.replyPreviewImage.setVisibility(View.GONE); binding.replyCancel.setVisibility(View.GONE); binding.replyPreviewText.setVisibility(View.GONE); }); return; } showExtraInputOption(false); binding.getRoot().postDelayed(() -> { binding.replyBg.setVisibility(View.VISIBLE); binding.replyInfo.setVisibility(View.VISIBLE); binding.replyPreviewImage.setVisibility(View.VISIBLE); binding.replyCancel.setVisibility(View.VISIBLE); binding.replyPreviewText.setVisibility(View.VISIBLE); if (item.getUserId() == viewModel.getViewerId()) { binding.replyInfo.setText(R.string.replying_to_yourself); } else { final User user = viewModel.getUser(item.getUserId()); if (user != null) { binding.replyInfo.setText(getString(R.string.replying_to_user, user.getFullName())); } else { binding.replyInfo.setVisibility(View.GONE); } } final String previewText = getDirectItemPreviewText(item); binding.replyPreviewText.setText(TextUtils.isEmpty(previewText) ? getString(R.string.message) : previewText); final String previewImageUrl = getDirectItemPreviewImageUrl(item); if (TextUtils.isEmpty(previewImageUrl)) { binding.replyPreviewImage.setVisibility(View.GONE); } else { binding.replyPreviewImage.setImageURI(previewImageUrl); } binding.replyCancel.setOnClickListener(v -> viewModel.setReplyToItem(null)); }, 200); }); inputLength.observe(getViewLifecycleOwner(), length -> { if (length == null) return; final boolean hasReplyToItem = viewModel.getReplyToItem().getValue() != null; if (hasReplyToItem) { prevLength = length; return; } if ((prevLength == 0 && length != 0) || (prevLength != 0 && length == 0)) { showExtraInputOption(length == 0); } prevLength = length; }); pendingRequestsCountLiveData = viewModel.getPendingRequestsCount(); pendingRequestsCountLiveData.observe(getViewLifecycleOwner(), this::attachPendingRequestsBadge); usersLiveData = viewModel.getUsers(); usersLiveData.observe(getViewLifecycleOwner(), users -> { if (users == null || users.isEmpty()) return; final User user = users.get(0); binding.acceptPendingRequestQuestion.setText(getString(R.string.accept_request_from_user, user.getUsername(), user.getFullName())); }); } private void removeObservers() { pendingLiveData.removeObservers(getViewLifecycleOwner()); inputModeLiveData.removeObservers(getViewLifecycleOwner()); threadTitleLiveData.removeObservers(getViewLifecycleOwner()); fetchingLiveData.removeObservers(getViewLifecycleOwner()); threadLiveData.removeObservers(getViewLifecycleOwner()); itemsLiveData.removeObservers(getViewLifecycleOwner()); replyToItemLiveData.removeObservers(getViewLifecycleOwner()); inputLength.removeObservers(getViewLifecycleOwner()); pendingRequestsCountLiveData.removeObservers(getViewLifecycleOwner()); usersLiveData.removeObservers(getViewLifecycleOwner()); } private void hidePendingOptions() { binding.acceptPendingRequestQuestion.setVisibility(View.GONE); binding.decline.setVisibility(View.GONE); binding.accept.setVisibility(View.GONE); } private void showPendingOptions() { binding.acceptPendingRequestQuestion.setVisibility(View.VISIBLE); binding.decline.setVisibility(View.VISIBLE); binding.accept.setVisibility(View.VISIBLE); binding.accept.setOnClickListener(v -> { final LiveData> resourceLiveData = viewModel.acceptRequest(); handlePendingChangeResource(resourceLiveData, false); }); binding.decline.setOnClickListener(v -> { final LiveData> resourceLiveData = viewModel.declineRequest(); handlePendingChangeResource(resourceLiveData, true); }); } private void handlePendingChangeResource(final LiveData> resourceLiveData, final boolean isDecline) { resourceLiveData.observe(getViewLifecycleOwner(), resource -> { if (resource == null) return; final Resource.Status status = resource.status; switch (status) { case SUCCESS: resourceLiveData.removeObservers(getViewLifecycleOwner()); if (isDecline) { removeObservers(); viewModel.removeThread(); final NavController navController = NavHostFragment.findNavController(this); navController.navigateUp(); return; } removeObservers(); viewModel.moveFromPending(); setObservers(); break; case LOADING: break; case ERROR: if (resource.message != null) { Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); } if (resource.resId != 0) { Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); } resourceLiveData.removeObservers(getViewLifecycleOwner()); break; } }); } 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); binding.inputBg.setVisibility(View.GONE); binding.recordView.setVisibility(View.GONE); binding.send.setVisibility(View.GONE); if (itemTouchHelper != null) { itemTouchHelper.attachToRecyclerView(null); } } 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); binding.inputBg.setVisibility(View.VISIBLE); binding.recordView.setVisibility(View.VISIBLE); binding.send.setVisibility(View.VISIBLE); if (itemTouchHelper != null) { itemTouchHelper.attachToRecyclerView(binding.chats); } } @SuppressLint("UnsafeOptInUsageError") private void attachPendingRequestsBadge(@Nullable final Integer count) { if (pendingRequestCountBadgeDrawable == null) { final Context context = getContext(); if (context == null) return; pendingRequestCountBadgeDrawable = BadgeDrawable.create(context); } if (count == null || count == 0) { @SuppressLint("RestrictedApi") final ActionMenuItemView menuItemView = ToolbarUtils .getActionMenuItemView(fragmentActivity.getToolbar(), R.id.info); if (menuItemView != null) { BadgeUtils.detachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); } isPendingRequestCountBadgeAttached = false; pendingRequestCountBadgeDrawable.setNumber(0); return; } if (pendingRequestCountBadgeDrawable.getNumber() == count) return; pendingRequestCountBadgeDrawable.setNumber(count); if (!isPendingRequestCountBadgeAttached) { BadgeUtils.attachBadgeDrawable(pendingRequestCountBadgeDrawable, fragmentActivity.getToolbar(), R.id.info); isPendingRequestCountBadgeAttached = true; } } private void showExtraInputOption(final boolean show) { if (show) { if (!binding.send.isListenForRecord()) { binding.send.setListenForRecord(true); startIconAnimation(); } 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.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); binding.gallery.setVisibility(View.GONE); } private String getDirectItemPreviewText(final DirectItem item) { switch (item.getItemType()) { case TEXT: return item.getText(); case LINK: return item.getLink().getText(); case MEDIA: { final Media media = item.getMedia(); return getMediaPreviewTextString(media); } case RAVEN_MEDIA: { final DirectItemVisualMedia visualMedia = item.getVisualMedia(); final Media media = visualMedia.getMedia(); return getMediaPreviewTextString(media); } case VOICE_MEDIA: return getString(R.string.voice_message); case MEDIA_SHARE: return getString(R.string.post); case REEL_SHARE: return item.getReelShare().getText(); } return ""; } @NonNull private String getMediaPreviewTextString(final Media media) { final MediaItemType mediaType = media.getMediaType(); switch (mediaType) { case MEDIA_TYPE_IMAGE: return getString(R.string.photo); case MEDIA_TYPE_VIDEO: return getString(R.string.video); default: return ""; } } private String getDirectItemPreviewImageUrl(final DirectItem item) { switch (item.getItemType()) { case TEXT: case LINK: case VOICE_MEDIA: case REEL_SHARE: return null; case MEDIA: { final Media media = item.getMedia(); return ResponseBodyUtils.getThumbUrl(media); } case RAVEN_MEDIA: { final DirectItemVisualMedia visualMedia = item.getVisualMedia(); final Media media = visualMedia.getMedia(); return ResponseBodyUtils.getThumbUrl(media); } case MEDIA_SHARE: { final Media media = item.getMediaShare(); return ResponseBodyUtils.getThumbUrl(media); } } return null; } private void setupBackStackResultObserver() { final NavController navController = NavHostFragment.findNavController(this); final NavBackStackEntry backStackEntry = navController.getCurrentBackStackEntry(); if (backStackEntry != null) { backStackSavedStateResultLiveData = backStackEntry.getSavedStateHandle().getLiveData("result"); backStackSavedStateResultLiveData.observe(getViewLifecycleOwner(), backStackSavedStateObserver); } } private void submitItemsToAdapter(final List items) { binding.chats.post(() -> { if (autoMarkAsSeen) { viewModel.markAsSeen(); return; } final DirectThread thread = threadLiveData.getValue(); if (thread == null) return; if (markAsSeenMenuItem != null) { markAsSeenMenuItem.setEnabled(!DMUtils.isRead(thread)); } }); if (itemsAdapter == null) return; itemsAdapter.submitList(items, () -> { itemOrHeaders = itemsAdapter.getList(); binding.chats.post(() -> { final RecyclerView.LayoutManager layoutManager = binding.chats.getLayoutManager(); if (layoutManager instanceof LinearLayoutManager) { final int position = ((LinearLayoutManager) layoutManager).findLastCompletelyVisibleItemPosition(); if (position < 0) return; if (position == itemsAdapter.getItemCount() - 1) { viewModel.fetchChats(); } } }); }); } private void setupItemsAdapter(final DirectThread thread) { if (thread == null) return; if (itemsAdapter != null) { if (itemsAdapter.getThread() == thread) return; itemsAdapter.setThread(thread); return; } final Resource currentUserResource = appStateViewModel.getCurrentUser(); if (currentUserResource == null) return; final User currentUser = currentUserResource.data; if (currentUser == null) return; itemsAdapter = new DirectItemsAdapter(currentUser, thread, directItemCallback, directItemLongClickListener); itemsAdapter.setHasStableIds(true); itemsAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); binding.chats.setAdapter(itemsAdapter); registerDataObserver(); users = thread.getUsers(); final List items = viewModel.getItems().getValue(); if (items != null && itemsAdapter.getItems() != items) { submitItemsToAdapter(items); } } private void registerDataObserver() { itemsAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onItemRangeInserted(final int positionStart, final int itemCount) { super.onItemRangeInserted(positionStart, itemCount); final LinearLayoutManager layoutManager = (LinearLayoutManager) binding.chats.getLayoutManager(); if (layoutManager == null) return; int firstVisiblePosition = layoutManager.findFirstCompletelyVisibleItemPosition(); if ((firstVisiblePosition == -1 || firstVisiblePosition == 0) && (positionStart == 0)) { binding.chats.scrollToPosition(0); } } }); } private void setupInput() { final Integer inputMode = viewModel.getInputMode().getValue(); if (inputMode != null && inputMode == 1) return; final Context context = getContext(); if (context == null) return; tooltip.setText(R.string.dms_thread_audio_hint); setMicToSendIcon(); binding.recordView.setMinMillis(1000); binding.recordView.setOnRecordListener(new RecordView.OnRecordListener() { @Override public void onStart() { isRecording = true; binding.input.setHint(null); binding.gif.setVisibility(View.GONE); binding.camera.setVisibility(View.GONE); binding.gallery.setVisibility(View.GONE); if (PermissionUtils.hasAudioRecordPerms(context)) { viewModel.startRecording(); return; } PermissionUtils.requestAudioRecordPerms(DirectMessageThreadFragment.this, AUDIO_RECORD_PERM_REQUEST_CODE); } @Override public void onCancel() { Log.d(TAG, "onCancel"); // binding.input.setHint("Message"); viewModel.stopRecording(true); isRecording = false; } @Override public void onFinish(final long recordTime) { Log.d(TAG, "onFinish"); binding.input.setHint("Message"); binding.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); binding.gallery.setVisibility(View.VISIBLE); viewModel.stopRecording(false); isRecording = false; } @Override public void onLessThanMin() { Log.d(TAG, "onLessThanMin"); binding.input.setHint("Message"); if (PermissionUtils.hasAudioRecordPerms(context)) { tooltip.show(binding.send); } 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.gif.setVisibility(View.VISIBLE); binding.camera.setVisibility(View.VISIBLE); binding.gallery.setVisibility(View.VISIBLE); }); binding.input.addTextChangedListener(new TextWatcherAdapter() { // int prevLength = 0; @Override public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { final int length = s.length(); inputLength.postValue(length); } }); binding.send.setOnRecordClickListener(v -> { final Editable text = binding.input.getText(); if (TextUtils.isEmpty(text)) return; final LiveData> resourceLiveData = viewModel.sendText(text.toString()); resourceLiveData.observe(getViewLifecycleOwner(), resource -> handleSentMessage(resourceLiveData)); binding.input.setText(""); viewModel.setReplyToItem(null); }); binding.send.setOnRecordLongClickListener(v -> { Log.d(TAG, "setOnRecordLongClickListener"); return true; }); binding.input.setOnFocusChangeListener((v, hasFocus) -> { if (!hasFocus) return; final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue(); if (emojiPickerVisibleValue == null || !emojiPickerVisibleValue) return; inputHolderAnimationCallback.setShouldTranslate(false); chatsAnimationCallback.setShouldTranslate(false); emojiPickerAnimationCallback.setShouldTranslate(false); }); setupInsetsCallback(); setupEmojiPicker(); binding.gallery.setOnClickListener(v -> { final Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.setType("*/*"); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.putExtra(Intent.EXTRA_MIME_TYPES, new String[]{ "image/*", "video/mp4" }); startActivityForResult(intent, FILE_PICKER_REQUEST_CODE); }); 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"); }); binding.camera.setOnClickListener(v -> { final Intent intent = new Intent(context, CameraActivity.class); startActivityForResult(intent, CAMERA_REQUEST_CODE); }); } private void setupInsetsCallback() { inputHolderAnimationCallback = new TranslateDeferringInsetsAnimationCallback( binding.inputHolder, WindowInsetsCompat.Type.systemBars(), WindowInsetsCompat.Type.ime(), WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE ); ViewCompat.setWindowInsetsAnimationCallback(binding.inputHolder, inputHolderAnimationCallback); chatsAnimationCallback = new TranslateDeferringInsetsAnimationCallback( binding.chats, WindowInsetsCompat.Type.systemBars(), WindowInsetsCompat.Type.ime() ); ViewCompat.setWindowInsetsAnimationCallback(binding.chats, chatsAnimationCallback); emojiPickerAnimationCallback = new EmojiPickerInsetsAnimationCallback( binding.emojiPicker, WindowInsetsCompat.Type.systemBars(), WindowInsetsCompat.Type.ime() ); emojiPickerAnimationCallback.setKbVisibilityListener(this::onKbVisibilityChange); ViewCompat.setWindowInsetsAnimationCallback(binding.emojiPicker, emojiPickerAnimationCallback); ViewCompat.setWindowInsetsAnimationCallback( binding.input, new ControlFocusInsetsAnimationCallback(binding.input) ); final SimpleImeAnimationController imeAnimController = root.getImeAnimController(); if (imeAnimController != null) { imeAnimController.setAnimationControlListener(new WindowInsetsAnimationControlListenerCompat() { @Override public void onReady(@NonNull final WindowInsetsAnimationControllerCompat controller, final int types) {} @Override public void onFinished(@NonNull final WindowInsetsAnimationControllerCompat controller) { checkKbVisibility(); } @Override public void onCancelled(@Nullable final WindowInsetsAnimationControllerCompat controller) { checkKbVisibility(); } private void checkKbVisibility() { final WindowInsetsCompat rootWindowInsets = ViewCompat.getRootWindowInsets(binding.getRoot()); final boolean visible = rootWindowInsets != null && rootWindowInsets.isVisible(WindowInsetsCompat.Type.ime()); onKbVisibilityChange(visible); } }); } } private void onKbVisibilityChange(final boolean kbVisible) { this.kbVisible.postValue(kbVisible); if (wasToggled) { emojiPickerVisible.postValue(!kbVisible); wasToggled = false; return; } final Boolean emojiPickerVisibleValue = emojiPickerVisible.getValue(); if (kbVisible && emojiPickerVisibleValue != null && emojiPickerVisibleValue) { emojiPickerVisible.postValue(false); return; } if (!kbVisible) { emojiPickerVisible.postValue(false); } } private void startIconAnimation() { final Drawable icon = binding.send.getIcon(); if (icon instanceof Animatable) { final Animatable animatable = (Animatable) icon; if (animatable.isRunning()) { animatable.stop(); } animatable.start(); } } private void navigateToImageEditFragment(final String path) { navigateToImageEditFragment(Uri.fromFile(new File(path))); } private void navigateToImageEditFragment(final Uri uri) { final NavDirections navDirections = DirectMessageThreadFragmentDirections.actionThreadToImageEdit(uri); final NavController navController = NavHostFragment.findNavController(this); navController.navigate(navDirections); } private void handleSentMessage(final LiveData> resourceLiveData) { final Resource resource = resourceLiveData.getValue(); if (resource == null) return; final Resource.Status status = resource.status; switch (status) { case SUCCESS: resourceLiveData.removeObservers(getViewLifecycleOwner()); break; case LOADING: break; case ERROR: if (resource.message != null) { Snackbar.make(binding.getRoot(), resource.message, Snackbar.LENGTH_LONG).show(); } if (resource.resId != 0) { Snackbar.make(binding.getRoot(), resource.resId, Snackbar.LENGTH_LONG).show(); } resourceLiveData.removeObservers(getViewLifecycleOwner()); break; } } private void setupEmojiPicker() { root.post(() -> binding.emojiPicker.init( root, (view, emoji) -> { final KeyNotifyingEmojiEditText input = binding.input; final int start = input.getSelectionStart(); final int end = input.getSelectionEnd(); if (start < 0) { input.append(emoji.getUnicode()); return; } input.getText().replace( Math.min(start, end), Math.max(start, end), emoji.getUnicode(), 0, emoji.getUnicode().length() ); }, () -> binding.input.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)) )); binding.emojiToggle.setOnClickListener(v -> { Boolean isEmojiPickerVisible = emojiPickerVisible.getValue(); if (isEmojiPickerVisible == null) isEmojiPickerVisible = false; Boolean isKbVisible = kbVisible.getValue(); if (isKbVisible == null) isKbVisible = false; wasToggled = isEmojiPickerVisible || isKbVisible; if (isEmojiPickerVisible) { if (hasKbOpenedOnce && binding.emojiPicker.getTranslationY() != 0) { inputHolderAnimationCallback.setShouldTranslate(false); chatsAnimationCallback.setShouldTranslate(false); emojiPickerAnimationCallback.setShouldTranslate(false); } // trigger ime. // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here showKeyboard(); return; } if (isKbVisible) { // hide the keyboard, but don't translate the views // Since the kb visibility listener will toggle the emojiPickerVisible live data, we do not explicitly toggle it here inputHolderAnimationCallback.setShouldTranslate(false); chatsAnimationCallback.setShouldTranslate(false); emojiPickerAnimationCallback.setShouldTranslate(false); hideKeyboard(); } emojiPickerVisible.postValue(true); }); final LiveData> emojiKbVisibilityLD = Utils.zipLiveData(emojiPickerVisible, kbVisible); emojiKbVisibilityLD.observe(getViewLifecycleOwner(), pair -> { Boolean isEmojiPickerVisible = pair.first; Boolean isKbVisible = pair.second; if (isEmojiPickerVisible == null) isEmojiPickerVisible = false; if (isKbVisible == null) isKbVisible = false; root.setScrollImeOffScreenWhenVisible(!isEmojiPickerVisible); root.setScrollImeOnScreenWhenNotVisible(!isEmojiPickerVisible); onEmojiPickerBackPressedCallback.setEnabled(isEmojiPickerVisible && !isKbVisible); if (isEmojiPickerVisible && !isKbVisible) { animatePan(binding.emojiPicker.getMeasuredHeight(), unused -> { binding.emojiPicker.setAlpha(1); binding.emojiToggle.setIconResource(R.drawable.ic_keyboard_24); return null; }, null); return; } if (!isEmojiPickerVisible && !isKbVisible) { animatePan(0, null, unused -> { binding.emojiPicker.setAlpha(0); binding.emojiToggle.setIconResource(R.drawable.ic_face_24); return null; }); return; } // isKbVisible will always be true going forward hasKbOpenedOnce = true; if (!isEmojiPickerVisible) { binding.emojiToggle.setIconResource(R.drawable.ic_face_24); binding.emojiPicker.setAlpha(0); return; } binding.emojiPicker.setAlpha(1); }); } public void showKeyboard() { final Context context = getContext(); if (context == null) return; final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) return; if (!binding.input.isFocused()) { binding.input.requestFocus(); } final boolean shown = imm.showSoftInput(binding.input, InputMethodManager.SHOW_IMPLICIT); if (!shown) { Log.e(TAG, "showKeyboard: System did not display the keyboard"); } } public void hideKeyboard() { final Context context = getContext(); if (context == null) return; final InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm == null) return; imm.hideSoftInputFromWindow(binding.input.getWindowToken(), InputMethodManager.RESULT_UNCHANGED_SHOWN); } private void setSendToMicIcon() { final Context context = getContext(); if (context == null) return; final Drawable sendToMicDrawable = Utils.getAnimatableDrawable(context, R.drawable.avd_send_to_mic_anim); if (sendToMicDrawable instanceof Animatable) { AnimatedVectorDrawableCompat.registerAnimationCallback(sendToMicDrawable, sendToMicAnimationCallback); } binding.send.setIcon(sendToMicDrawable); } private void setMicToSendIcon() { final Context context = getContext(); if (context == null) return; final Drawable micToSendDrawable = Utils.getAnimatableDrawable(context, R.drawable.avd_mic_to_send_anim); if (micToSendDrawable instanceof Animatable) { AnimatedVectorDrawableCompat.registerAnimationCallback(micToSendDrawable, micToSendAnimationCallback); } binding.send.setIcon(micToSendDrawable); } private void setTitle(final String title) { if (actionBar == null) return; if (prevTitleRunnable != null) { appExecutors.getMainThread().cancel(prevTitleRunnable); } prevTitleRunnable = () -> actionBar.setTitle(title); // set title delayed to avoid title blink if fetch is fast appExecutors.getMainThread().execute(prevTitleRunnable, 1000); } private void downloadItem(final DirectItem item) { final Context context = getContext(); if (context == null) return; final DirectItemType itemType = item.getItemType(); //noinspection SwitchStatementWithTooFewBranches switch (itemType) { case VOICE_MEDIA: downloadItem(context, item.getVoiceMedia() == null ? null : item.getVoiceMedia().getMedia()); break; } } // currently ONLY for voice private void downloadItem(@NonNull final Context context, final Media media) { if (media == null) { Toast.makeText(context, R.string.downloader_unknown_error, Toast.LENGTH_SHORT).show(); return; } DownloadUtils.download(context, media); Toast.makeText(context, R.string.downloader_downloading_media, Toast.LENGTH_SHORT).show(); } @Nullable private User getUser(final long userId) { for (final User user : users) { if (userId != user.getPk()) continue; return user; } return null; } // Sets the translationY of views to height with animation private void animatePan(final int height, @Nullable final Function onAnimationStart, @Nullable final Function onAnimationEnd) { if (animatorSet != null && animatorSet.isStarted()) { animatorSet.cancel(); } final ImmutableList.Builder builder = ImmutableList.builder(); builder.add( ObjectAnimator.ofFloat(binding.chats, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.inputHolder, TRANSLATION_Y, -height), ObjectAnimator.ofFloat(binding.emojiPicker, TRANSLATION_Y, -height) ); // if (headerItemDecoration != null && headerItemDecoration.getCurrentHeader() != null) { // builder.add(ObjectAnimator.ofFloat(headerItemDecoration.getCurrentHeader(), TRANSLATION_Y, height)); // } animatorSet = new AnimatorSet(); animatorSet.playTogether(builder.build()); animatorSet.setDuration(200); animatorSet.setInterpolator(CubicBezierInterpolator.EASE_IN); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(final Animator animation) { super.onAnimationStart(animation); if (onAnimationStart != null) { onAnimationStart.apply(null); } } @Override public void onAnimationEnd(final Animator animation) { super.onAnimationEnd(animation); animatorSet = null; if (onAnimationEnd != null) { onAnimationEnd.apply(null); } } }); animatorSet.start(); } private void showReactionsDialog(final DirectItem item) { final LiveData> users = viewModel.getUsers(); final LiveData> leftUsers = viewModel.getLeftUsers(); final ArrayList allUsers = new ArrayList<>(); allUsers.add(viewModel.getCurrentUser()); if (users != null && users.getValue() != null) { allUsers.addAll(users.getValue()); } if (leftUsers != null && leftUsers.getValue() != null) { allUsers.addAll(leftUsers.getValue()); } reactionDialogFragment = DirectItemReactionDialogFragment .newInstance(viewModel.getViewerId(), allUsers, item.getItemId(), item.getReactions()); reactionDialogFragment.show(getChildFragmentManager(), "reactions_dialog"); } @Override public void onReactionClick(final String itemId, final DirectItemEmojiReaction reaction) { if (reactionDialogFragment != null) { reactionDialogFragment.dismiss(); } if (itemId == null || reaction == null) return; if (reaction.getSenderId() == viewModel.getViewerId()) { final LiveData> resourceLiveData = viewModel.sendDeleteReaction(itemId); if (resourceLiveData != null) { resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } return; } // navigate to user final User user = viewModel.getUser(reaction.getSenderId()); if (user == null) return; navigateToUser(user.getUsername()); } private void navigateToUser(@NonNull final String username) { final ProfileNavGraphDirections.ActionGlobalProfileFragment direction = ProfileNavGraphDirections .actionGlobalProfileFragment("@" + username); NavHostFragment.findNavController(DirectMessageThreadFragment.this).navigate(direction); } @Override public void onClick(final View view, final Emoji emoji) { if (addReactionItem == null || emoji == null) return; final LiveData> resourceLiveData = viewModel.sendReaction(addReactionItem, emoji); if (resourceLiveData != null) { resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData)); } } }