barinsta/app/src/main/java/awais/instagrabber/fragments/directmessages/DirectMessageThreadFragment...

1527 lines
68 KiB
Java

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<DirectItemOrHeader> itemOrHeaders;
private List<User> 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<Object> backStackSavedStateResultLiveData;
private int prevLength;
private BadgeDrawable pendingRequestCountBadgeDrawable;
private boolean isPendingRequestCountBadgeAttached = false;
private ItemTouchHelper itemTouchHelper;
private LiveData<Boolean> pendingLiveData;
private LiveData<DirectThread> threadLiveData;
private LiveData<Integer> inputModeLiveData;
private LiveData<String> threadTitleLiveData;
private LiveData<Resource<Object>> fetchingLiveData;
private LiveData<List<DirectItem>> itemsLiveData;
private LiveData<DirectItem> replyToItemLiveData;
private LiveData<Integer> pendingRequestsCountLiveData;
private LiveData<List<User>> 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<Resource<Object>> 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<DirectItem, Void> 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<Object> 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<RankedRecipient>) result, itemToForward);
}
} catch (Exception e) {
Log.e(TAG, "forward result: ", e);
}
}
// clear result
backStackSavedStateResultLiveData.postValue(null);
};
private final MutableLiveData<Integer> inputLength = new MutableLiveData<>(0);
private final MutableLiveData<Boolean> emojiPickerVisible = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> 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<User> 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<Resource<Object>> resourceLiveData = viewModel.markAsSeen();
resourceLiveData.observe(getViewLifecycleOwner(), new Observer<Resource<Object>>() {
@Override
public void onChanged(final Resource<Object> 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<Resource<Object>> resourceLiveData = viewModel.acceptRequest();
handlePendingChangeResource(resourceLiveData, false);
});
binding.decline.setOnClickListener(v -> {
final LiveData<Resource<Object>> resourceLiveData = viewModel.declineRequest();
handlePendingChangeResource(resourceLiveData, true);
});
}
private void handlePendingChangeResource(final LiveData<Resource<Object>> 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<DirectItem> 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<User> 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<DirectItem> 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<Resource<Object>> 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<Resource<Object>> resourceLiveData) {
final Resource<Object> 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<Pair<Boolean, Boolean>> 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<Void, Void> onAnimationStart,
@Nullable final Function<Void, Void> onAnimationEnd) {
if (animatorSet != null && animatorSet.isStarted()) {
animatorSet.cancel();
}
final ImmutableList.Builder<Animator> 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<List<User>> users = viewModel.getUsers();
final LiveData<List<User>> leftUsers = viewModel.getLeftUsers();
final ArrayList<User> 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<Resource<Object>> 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<Resource<Object>> resourceLiveData = viewModel.sendReaction(addReactionItem, emoji);
if (resourceLiveData != null) {
resourceLiveData.observe(getViewLifecycleOwner(), directItemResource -> handleSentMessage(resourceLiveData));
}
}
}