/*
* Copyright (C) 2015 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package org.thoughtcrime.securesms;
import static com.b44t.messenger.DcContact.DC_CONTACT_ID_SELF;
import static org.thoughtcrime.securesms.util.RelayUtil.setForwardingMessageIds;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.util.Log;
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.Window;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerView.OnScrollListener;
import com.b44t.messenger.DcChat;
import com.b44t.messenger.DcContact;
import com.b44t.messenger.DcContext;
import com.b44t.messenger.DcEvent;
import com.b44t.messenger.DcMsg;
import org.thoughtcrime.securesms.ConversationAdapter.ItemClickListener;
import org.thoughtcrime.securesms.components.reminder.DozeReminder;
import org.thoughtcrime.securesms.connect.DcEventCenter;
import org.thoughtcrime.securesms.connect.DcHelper;
import org.thoughtcrime.securesms.database.Address;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.reactions.AddReactionView;
import org.thoughtcrime.securesms.reactions.ReactionsDetailsFragment;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.AccessibilityUtil;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.ConversationAdaptiveActionsToolbar;
import org.thoughtcrime.securesms.videochat.VideochatUtil;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
@SuppressLint("StaticFieldLeak")
public class ConversationFragment extends MessageSelectorFragment
{
private static final String TAG = ConversationFragment.class.getSimpleName();
private static final int SCROLL_ANIMATION_THRESHOLD = 50;
private static final int CODE_ADD_EDIT_CONTACT = 77;
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
private final ItemClickListener selectionClickListener = new ConversationFragmentItemClickListener();
private ConversationFragmentListener listener;
private Recipient recipient;
private long chatId;
private int startingPosition;
private boolean firstLoad;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private StickyHeaderDecoration dateDecoration;
private View scrollToBottomButton;
private View floatingLocationButton;
private AddReactionView addReactionView;
private TextView noMessageTextView;
private Timer reloadTimer;
public boolean isPaused;
private Debouncer markseenDebouncer;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
this.dcContext = DcHelper.getContext(getContext());
DcEventCenter eventCenter = DcHelper.getEventCenter(getContext());
eventCenter.addObserver(DcContext.DC_EVENT_INCOMING_MSG, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSGS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_REACTIONS_CHANGED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_DELIVERED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_FAILED, this);
eventCenter.addObserver(DcContext.DC_EVENT_MSG_READ, this);
eventCenter.addObserver(DcContext.DC_EVENT_CHAT_MODIFIED, this);
markseenDebouncer = new Debouncer(800);
reloadTimer = new Timer("reloadTimer", false);
reloadTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
Util.runOnMain(ConversationFragment.this::reloadList);
}
}, 60 * 1000, 60 * 1000);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
list = ViewUtil.findById(view, android.R.id.list);
scrollToBottomButton = ViewUtil.findById(view, R.id.scroll_to_bottom_button);
floatingLocationButton = ViewUtil.findById(view, R.id.floating_location_button);
addReactionView = ViewUtil.findById(view, R.id.add_reaction_view);
noMessageTextView = ViewUtil.findById(view, R.id.no_messages_text_view);
scrollToBottomButton.setOnClickListener(v -> scrollToBottom());
final SetStartingPositionLinearLayoutManager layoutManager = new SetStartingPositionLinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, true);
list.setHasFixedSize(false);
list.setLayoutManager(layoutManager);
list.setItemAnimator(null);
new ConversationItemSwipeCallback(
msg -> actionMode == null,
this::handleReplyMessage
).attachToRecyclerView(list);
// setLayerType() is needed to allow larger items (long texts in our case)
// with hardware layers, drawing may result in errors as "OpenGLRenderer: Path too large to be rendered into a texture"
list.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
return view;
}
@Override
public void onActivityCreated(Bundle bundle) {
super.onActivityCreated(bundle);
initializeResources();
initializeListAdapter();
}
private void setNoMessageText() {
DcChat dcChat = getListAdapter().getChat();
if(dcChat.isMultiUser()){
if (dcChat.isBroadcast()) {
noMessageTextView.setText(R.string.chat_new_broadcast_hint);
} else if (dcChat.isUnpromoted()) {
noMessageTextView.setText(R.string.chat_new_group_hint);
}
else {
noMessageTextView.setText(R.string.chat_no_messages);
}
}
else if(dcChat.isSelfTalk()) {
noMessageTextView.setText(R.string.saved_messages_explain);
}
else if(dcChat.isDeviceTalk()) {
noMessageTextView.setText(R.string.device_talk_explain);
}
else {
String message = getString(R.string.chat_new_one_to_one_hint, dcChat.getName());
noMessageTextView.setText(message);
}
}
@Override
public void onDestroy() {
DcHelper.getEventCenter(getContext()).removeObservers(this);
reloadTimer.cancel();
super.onDestroy();
}
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
this.listener = (ConversationFragmentListener)activity;
}
@Override
public void onResume() {
super.onResume();
Util.runOnBackground(() -> dcContext.marknoticedChat((int) chatId));
if (list.getAdapter() != null) {
list.getAdapter().notifyDataSetChanged();
}
if (isPaused) {
isPaused = false;
markseenDebouncer.publish(() -> manageMessageSeenState());
}
}
@Override
public void onPause() {
super.onPause();
setLastSeen(System.currentTimeMillis());
isPaused = true;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
dateDecoration.onConfigurationChanged(newConfig);
}
public void onNewIntent() {
if (actionMode != null) {
actionMode.finish();
}
initializeResources();
initializeListAdapter();
if (chatId == -1) {
reloadList();
updateLocationButton();
}
}
public void moveToLastSeen() {
if (list == null || getListAdapter() == null) {
Log.w(TAG, "Tried to move to last seen position, but we hadn't initialized the view yet.");
return;
}
if (getListAdapter().getLastSeenPosition() < 0) {
return;
}
final int lastSeenPosition = getListAdapter().getLastSeenPosition() + 1;
if (lastSeenPosition > 0) {
list.post(() -> ((LinearLayoutManager)list.getLayoutManager()).scrollToPositionWithOffset(lastSeenPosition, list.getHeight()));
}
}
public void hideAddReactionView() {
addReactionView.hide();
}
private void initializeResources() {
this.chatId = this.getActivity().getIntent().getIntExtra(ConversationActivity.CHAT_ID_EXTRA, -1);
this.recipient = Recipient.from(getActivity(), Address.fromChat((int)this.chatId));
this.startingPosition = this.getActivity().getIntent().getIntExtra(ConversationActivity.STARTING_POSITION_EXTRA, -1);
this.firstLoad = true;
OnScrollListener scrollListener = new ConversationScrollListener(getActivity());
list.addOnScrollListener(scrollListener);
}
private void initializeListAdapter() {
if (this.recipient != null && this.chatId != -1) {
ConversationAdapter adapter = new ConversationAdapter(getActivity(), this.recipient.getChat(), GlideApp.with(this), selectionClickListener, this.recipient);
list.setAdapter(adapter);
if (dateDecoration != null) {
list.removeItemDecoration(dateDecoration);
}
dateDecoration = new StickyHeaderDecoration(adapter, false, false);
list.addItemDecoration(dateDecoration);
int freshMsgs = dcContext.getFreshMsgCount((int) chatId);
SetStartingPositionLinearLayoutManager layoutManager = (SetStartingPositionLinearLayoutManager) list.getLayoutManager();
if (startingPosition > -1) {
layoutManager.setStartingPosition(startingPosition);
} else if (freshMsgs > 0) {
layoutManager.setStartingPosition(freshMsgs - 1);
}
reloadList();
updateLocationButton();
if (lastSeenDecoration != null) {
list.removeItemDecoration(lastSeenDecoration);
}
if (freshMsgs > 0) {
getListAdapter().setLastSeenPosition(freshMsgs - 1);
lastSeenDecoration = new ConversationAdapter.LastSeenHeader(getListAdapter());
list.addItemDecoration(lastSeenDecoration);
}
}
}
@Override
protected void setCorrectMenuVisibility(Menu menu) {
Set messageRecords = getListAdapter().getSelectedItems();
if (actionMode != null && messageRecords.size() == 0) {
actionMode.finish();
return;
}
if (messageRecords.size() > 1) {
menu.findItem(R.id.menu_context_details).setVisible(false);
menu.findItem(R.id.menu_context_share).setVisible(false);
menu.findItem(R.id.menu_context_reply).setVisible(false);
menu.findItem(R.id.menu_context_edit).setVisible(false);
menu.findItem(R.id.menu_context_reply_privately).setVisible(false);
menu.findItem(R.id.menu_add_to_home_screen).setVisible(false);
menu.findItem(R.id.menu_toggle_save).setVisible(false);
} else {
DcMsg messageRecord = messageRecords.iterator().next();
DcChat chat = getListAdapter().getChat();
menu.findItem(R.id.menu_context_details).setVisible(true);
menu.findItem(R.id.menu_context_share).setVisible(messageRecord.hasFile());
boolean canReply = canReplyToMsg(messageRecord);
menu.findItem(R.id.menu_context_reply).setVisible(chat.canSend() && canReply);
boolean canEdit = canEditMsg(messageRecord);
menu.findItem(R.id.menu_context_edit).setVisible(chat.canSend() && canEdit);
boolean showReplyPrivately = chat.isMultiUser() && !messageRecord.isOutgoing() && canReply;
menu.findItem(R.id.menu_context_reply_privately).setVisible(showReplyPrivately);
menu.findItem(R.id.menu_add_to_home_screen).setVisible(messageRecord.getType() == DcMsg.DC_MSG_WEBXDC);
boolean saved = messageRecord.getSavedMsgId() != 0;
MenuItem toggleSave = menu.findItem(R.id.menu_toggle_save);
toggleSave.setVisible(messageRecord.canSave() && !chat.isSelfTalk());
toggleSave.setIcon(saved? R.drawable.baseline_bookmark_remove_24 : R.drawable.baseline_bookmark_border_24);
toggleSave.setTitle(saved? R.string.unsave : R.string.save);
}
// if one of the selected items cannot be saved, disable saving.
boolean canSave = true;
// if one of the selected items is not from self, disable resending.
boolean canResend = true;
for (DcMsg messageRecord : messageRecords) {
if (canSave && !messageRecord.hasFile()) {
canSave = false;
}
if (canResend && !messageRecord.isOutgoing()) {
canResend = false;
}
if (!canSave && !canResend) {
break;
}
}
menu.findItem(R.id.menu_context_save_attachment).setVisible(canSave);
menu.findItem(R.id.menu_resend).setVisible(canResend);
}
static boolean canReplyToMsg(DcMsg dcMsg) {
return !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_VIDEOCHAT_INVITATION;
}
static boolean canEditMsg(DcMsg dcMsg) {
return dcMsg.isOutgoing() && !dcMsg.isInfo() && dcMsg.getType() != DcMsg.DC_MSG_VIDEOCHAT_INVITATION && !dcMsg.hasHtml() && !dcMsg.getText().isEmpty();
}
public void handleClearChat() {
handleDeleteMessages((int) chatId, getListAdapter().getMessageIds());
}
private ConversationAdapter getListAdapter() {
return (ConversationAdapter) list.getAdapter();
}
public void reload(Recipient recipient, long chatId) {
this.recipient = recipient;
if (this.chatId != chatId) {
this.chatId = chatId;
initializeListAdapter();
}
}
public void scrollToTop() {
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
if (adapter.getItemCount()>0) {
final int pos = adapter.getItemCount()-1;
list.post(() -> {
list.getLayoutManager().scrollToPosition(pos);
});
}
}
public void scrollToBottom() {
if (((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition() < SCROLL_ANIMATION_THRESHOLD
&& !AccessibilityUtil.areAnimationsDisabled(getContext())) {
list.smoothScrollToPosition(0);
} else {
list.scrollToPosition(0);
}
}
void setLastSeen(long lastSeen) {
ConversationAdapter adapter = getListAdapter();
if (adapter != null) {
adapter.setLastSeen(lastSeen);
if (lastSeenDecoration != null) {
list.removeItemDecoration(lastSeenDecoration);
}
if (lastSeen > 0) {
lastSeenDecoration = new ConversationAdapter.LastSeenHeader(adapter);
list.addItemDecoration(lastSeenDecoration);
}
}
}
private void handleCopyMessage(final Set dcMsgsSet) {
List dcMsgsList = new LinkedList<>(dcMsgsSet);
Collections.sort(dcMsgsList, (lhs, rhs) -> Long.compare(lhs.getDateReceived(), rhs.getDateReceived()));
boolean singleMsg = dcMsgsList.size() == 1;
StringBuilder result = new StringBuilder();
DcMsg prevMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
for (DcMsg msg : dcMsgsList) {
if (result.length() > 0) {
result.append("\n\n");
}
if (msg.getFromId() != prevMsg.getFromId() && !singleMsg) {
DcContact contact = dcContext.getContact(msg.getFromId());
result.append(msg.getSenderName(contact)).append(":\n");
}
if (msg.getType() == DcMsg.DC_MSG_TEXT || (singleMsg && !msg.getText().isEmpty())) {
result.append(msg.getText());
} else {
result.append(msg.getSummarytext(10000000));
}
prevMsg = msg;
}
if (result.length() > 0) {
Util.writeTextToClipboard(getActivity(), result.toString());
Toast.makeText(getActivity(), getActivity().getResources().getString(R.string.copied_to_clipboard), Toast.LENGTH_LONG).show();
}
}
private void handleForwardMessage(final Set messageRecords) {
Intent composeIntent = new Intent();
int[] msgIds = DcMsg.msgSetToIds(messageRecords);
setForwardingMessageIds(composeIntent, msgIds);
ConversationListRelayingActivity.start(this, composeIntent);
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@SuppressLint("RestrictedApi")
private void handleReplyMessage(final DcMsg message) {
if (getActivity() != null) {
//noinspection ConstantConditions
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
}
listener.handleReplyMessage(message);
}
@SuppressLint("RestrictedApi")
private void handleEditMessage(final DcMsg message) {
if (getActivity() != null) {
//noinspection ConstantConditions
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
}
listener.handleEditMessage(message);
}
private void handleReplyMessagePrivately(final DcMsg msg) {
if (getActivity() != null) {
int privateChatId = dcContext.createChatByContactId(msg.getFromId());
DcMsg replyMsg = new DcMsg(dcContext, DcMsg.DC_MSG_TEXT);
replyMsg.setQuote(msg);
dcContext.setDraft(privateChatId, replyMsg);
Intent intent = new Intent(getActivity(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, privateChatId);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
getActivity().startActivity(intent);
} else {
Log.e(TAG, "Activity was null");
}
}
private void handleToggleSave(final Set messageRecords) {
DcMsg msg = getSelectedMessageRecord(messageRecords);
if (msg.getSavedMsgId() != 0) {
dcContext.deleteMsgs(new int[]{msg.getSavedMsgId()});
} else {
dcContext.saveMsgs(new int[]{msg.getId()});
}
}
private void reloadList() {
reloadList(false);
}
private void reloadList(boolean chatModified) {
ConversationAdapter adapter = getListAdapter();
if (adapter == null) {
return;
}
// if chat is a contact request and is accepted/blocked, the DcChat object must be reloaded, otherwise DcChat.canSend() returns wrong values
if (chatModified) {
adapter.reloadChat();
}
int oldCount = 0;
int oldIndex = 0;
int pixelOffset = 0;
if (!firstLoad) {
oldCount = adapter.getItemCount();
oldIndex = ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition();
View firstView = list.getLayoutManager().findViewByPosition(oldIndex);
pixelOffset = (firstView == null) ? 0 : list.getBottom() - firstView.getBottom() - list.getPaddingBottom();
}
if (getContext() == null) {
Log.e(TAG, "reloadList: getContext() was null");
return;
}
DcContext dcContext = DcHelper.getContext(getContext());
long startMs = System.currentTimeMillis();
int[] msgs = dcContext.getChatMsgs((int) chatId, 0, 0);
Log.i(TAG, "⏰ getChatMsgs(" + chatId + "): " + (System.currentTimeMillis() - startMs) + "ms");
adapter.changeData(msgs);
if (firstLoad) {
if (startingPosition >= 0) {
getListAdapter().pulseHighlightItem(startingPosition);
}
firstLoad = false;
} else if(oldIndex > 0) {
int newIndex = oldIndex + msgs.length - oldCount;
if (newIndex < 0) { newIndex = 0; pixelOffset = 0; }
else if (newIndex >= msgs.length) { newIndex = msgs.length - 1; pixelOffset = 0; }
((LinearLayoutManager) list.getLayoutManager()).scrollToPositionWithOffset(newIndex, pixelOffset);
}
if(!adapter.isActive()){
setNoMessageText();
noMessageTextView.setVisibility(View.VISIBLE);
}
else{
noMessageTextView.setVisibility(View.GONE);
}
if (!isPaused) {
markseenDebouncer.publish(() -> manageMessageSeenState());
}
}
private void updateLocationButton() {
floatingLocationButton.setVisibility(dcContext.isSendingLocationsToChat((int) chatId)? View.VISIBLE : View.GONE);
}
private void scrollAndHighlight(final int pos, boolean smooth) {
list.post(() -> {
if (smooth && !AccessibilityUtil.areAnimationsDisabled(getContext())) {
list.smoothScrollToPosition(pos);
} else {
list.scrollToPosition(pos);
}
getListAdapter().pulseHighlightItem(pos);
});
}
public void scrollToMsgId(final int msgId) {
ConversationAdapter adapter = (ConversationAdapter)list.getAdapter();
int position = adapter.msgIdToPosition(msgId);
if (position!=-1) {
scrollAndHighlight(position, false);
} else {
Log.e(TAG, "msgId {} not found for scrolling");
}
}
private void scrollMaybeSmoothToMsgId(final int msgId) {
LinearLayoutManager layout = ((LinearLayoutManager) list.getLayoutManager());
boolean smooth = false;
ConversationAdapter adapter = (ConversationAdapter) list.getAdapter();
if (adapter == null) return;
int position = adapter.msgIdToPosition(msgId);
if (layout != null) {
int distance1 = Math.abs(position - layout.findFirstVisibleItemPosition());
int distance2 = Math.abs(position - layout.findLastVisibleItemPosition());
int distance = Math.min(distance1, distance2);
smooth = distance < 15;
Log.i(TAG, "Scrolling to destMsg, smoth: " + smooth + ", distance: " + distance);
}
if (position != -1) {
scrollAndHighlight(position, smooth);
} else {
Log.e(TAG, "msgId not found for scrolling: " + msgId);
}
}
public interface ConversationFragmentListener {
void handleReplyMessage(DcMsg messageRecord);
void handleEditMessage(DcMsg messageRecord);
}
private class ConversationScrollListener extends OnScrollListener {
private final Animation scrollButtonInAnimation;
private final Animation scrollButtonOutAnimation;
private boolean wasAtBottom = true;
private boolean wasAtZoomScrollHeight = false;
//private long lastPositionId = -1;
ConversationScrollListener(@NonNull Context context) {
this.scrollButtonInAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_in);
this.scrollButtonOutAnimation = AnimationUtils.loadAnimation(context, R.anim.fade_scale_out);
this.scrollButtonInAnimation.setDuration(100);
this.scrollButtonOutAnimation.setDuration(50);
}
@Override
public void onScrolled(final RecyclerView rv, final int dx, final int dy) {
boolean currentlyAtBottom = isAtBottom();
boolean currentlyAtZoomScrollHeight = isAtZoomScrollHeight();
// int positionId = getHeaderPositionId();
if (currentlyAtZoomScrollHeight && !wasAtZoomScrollHeight) {
ViewUtil.animateIn(scrollToBottomButton, scrollButtonInAnimation);
} else if (currentlyAtBottom && !wasAtBottom) {
ViewUtil.animateOut(scrollToBottomButton, scrollButtonOutAnimation, View.INVISIBLE);
}
// if (positionId != lastPositionId) {
// bindScrollHeader(conversationDateHeader, positionId);
// }
wasAtBottom = currentlyAtBottom;
wasAtZoomScrollHeight = currentlyAtZoomScrollHeight;
// lastPositionId = positionId;
markseenDebouncer.publish(() -> manageMessageSeenState());
ConversationFragment.this.addReactionView.move(dy);
}
@Override
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
// if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
// conversationDateHeader.show();
// } else if (newState == RecyclerView.SCROLL_STATE_IDLE) {
// conversationDateHeader.hide();
// }
}
private boolean isAtBottom() {
if (list.getChildCount() == 0) return true;
View bottomView = list.getChildAt(0);
int firstVisibleItem = ((LinearLayoutManager) list.getLayoutManager()).findFirstVisibleItemPosition();
boolean isAtBottom = (firstVisibleItem == 0);
return isAtBottom && bottomView.getBottom() <= list.getHeight();
}
private boolean isAtZoomScrollHeight() {
return ((LinearLayoutManager) list.getLayoutManager()).findFirstCompletelyVisibleItemPosition() > 4;
}
// private int getHeaderPositionId() {
// return ((LinearLayoutManager)list.getLayoutManager()).findLastVisibleItemPosition();
// }
// private void bindScrollHeader(HeaderViewHolder headerViewHolder, int positionId) {
// if (((ConversationAdapter)list.getAdapter()).getHeaderId(positionId) != -1) {
// ((ConversationAdapter) list.getAdapter()).onBindHeaderViewHolder(headerViewHolder, positionId);
// }
// }
}
private void manageMessageSeenState() {
LinearLayoutManager layoutManager = (LinearLayoutManager)list.getLayoutManager();
int firstPos = layoutManager.findFirstVisibleItemPosition();
int lastPos = layoutManager.findLastVisibleItemPosition();
if(firstPos == RecyclerView.NO_POSITION || lastPos == RecyclerView.NO_POSITION) {
return;
}
int[] ids = new int[lastPos - firstPos + 1];
int index = 0;
for(int pos = firstPos; pos <= lastPos; pos++) {
DcMsg message = ((ConversationAdapter) list.getAdapter()).getMsg(pos);
if (message.getFromId() != DC_CONTACT_ID_SELF) {
ids[index] = message.getId();
index++;
}
}
Util.runOnAnyBackgroundThread(() -> dcContext.markseenMsgs(ids));
}
private class ConversationFragmentItemClickListener implements ItemClickListener {
@Override
public void onItemClick(DcMsg messageRecord) {
if (actionMode != null) {
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
if (getListAdapter().getSelectedItems().size() == 0) {
actionMode.finish();
} else {
hideAddReactionView();
Menu menu = actionMode.getMenu();
setCorrectMenuVisibility(menu);
ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedItems().size()));
actionMode.setTitleOptionalHint(false); // the title represents important information, also indicating implicitly, more items can be selected
}
}
else if (messageRecord.getType()==DcMsg.DC_MSG_VIDEOCHAT_INVITATION) {
new VideochatUtil().join(getActivity(), messageRecord.getId());
}
else if(DozeReminder.isDozeReminderMsg(getContext(), messageRecord)) {
DozeReminder.dozeReminderTapped(getContext());
}
else if(messageRecord.getInfoType() == DcMsg.DC_INFO_WEBXDC_INFO_MESSAGE) {
if (messageRecord.getParent() != null) {
// if the parent webxdc message still exists
WebxdcActivity.openWebxdcActivity(getContext(), messageRecord.getParent(), messageRecord.getWebxdcHref());
}
}
else {
int infoContactId = messageRecord.getInfoContactId();
if (infoContactId != 0 && infoContactId != DC_CONTACT_ID_SELF) {
Intent intent = new Intent(getContext(), ProfileActivity.class);
intent.putExtra(ProfileActivity.CONTACT_ID_EXTRA, infoContactId);
startActivity(intent);
}
else {
String self_mail = dcContext.getConfig("configured_mail_user");
if (self_mail != null && !self_mail.isEmpty()
&& messageRecord.getText().contains(self_mail)
&& getListAdapter().getChat().isDeviceTalk()) {
// This is a device message informing the user that the password is wrong
startActivity(new Intent(getActivity(), RegistrationActivity.class));
}
}
}
}
@Override
public void onItemLongClick(DcMsg messageRecord, View view) {
if (actionMode == null) {
((ConversationAdapter) list.getAdapter()).toggleSelection(messageRecord);
list.getAdapter().notifyDataSetChanged();
actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(actionModeCallback);
addReactionView.show(messageRecord, view, () -> {
if (actionMode != null) {
actionMode.finish();
}
});
}
}
private void jumpToOriginal(DcMsg original) {
if (original == null) {
Log.i(TAG, "Clicked on a quote or jump-to-original whose original message was deleted/non-existing.");
Toast.makeText(getContext(), R.string.ConversationFragment_quoted_message_not_found, Toast.LENGTH_SHORT).show();
return;
}
int foreignChatId = original.getChatId();
if (foreignChatId != 0 && foreignChatId != chatId) {
Intent intent = new Intent(getActivity(), ConversationActivity.class);
intent.putExtra(ConversationActivity.CHAT_ID_EXTRA, foreignChatId);
int start = DcMsg.getMessagePosition(original, dcContext);
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, start);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
((ConversationActivity) getActivity()).hideSoftKeyboard();
if (getActivity() != null) {
getActivity().startActivity(intent);
} else {
Log.e(TAG, "Activity was null");
}
} else {
scrollMaybeSmoothToMsgId(original.getId());
}
}
@Override
public void onJumpToOriginalClicked(DcMsg messageRecord) {
jumpToOriginal(dcContext.getMsg(messageRecord.getOriginalMsgId()));
}
@Override
public void onQuoteClicked(DcMsg messageRecord) {
jumpToOriginal(messageRecord.getQuotedMsg());
}
@Override
public void onShowFullClicked(DcMsg messageRecord) {
Intent intent = new Intent(getActivity(), FullMsgActivity.class);
intent.putExtra(FullMsgActivity.MSG_ID_EXTRA, messageRecord.getId());
intent.putExtra(FullMsgActivity.BLOCK_LOADING_REMOTE, getListAdapter().getChat().isHalfBlocked());
startActivity(intent);
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out);
}
@Override
public void onDownloadClicked(DcMsg messageRecord) {
dcContext.downloadFullMsg(messageRecord.getId());
}
@Override
public void onReactionClicked(DcMsg messageRecord) {
ReactionsDetailsFragment dialog = new ReactionsDetailsFragment(messageRecord.getId());
dialog.show(getActivity().getSupportFragmentManager(), null);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == CODE_ADD_EDIT_CONTACT && getContext() != null) {
// ApplicationContext.getInstance(getContext().getApplicationContext())
// .getJobManager()
// .add(new DirectoryRefreshJob(getContext().getApplicationContext(), false));
}
}
private class ActionModeCallback implements ActionMode.Callback {
private int statusBarColor;
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.conversation_context, menu);
mode.setTitle("1");
Window window = getActivity().getWindow();
statusBarColor = window.getStatusBarColor();
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
Util.redMenuItem(menu, R.id.menu_context_delete_message);
setCorrectMenuVisibility(menu);
ConversationAdaptiveActionsToolbar.adjustMenuActions(menu, 10, requireActivity().getWindow().getDecorView().getMeasuredWidth());
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
((ConversationAdapter)list.getAdapter()).clearSelection();
list.getAdapter().notifyDataSetChanged();
getActivity().getWindow().setStatusBarColor(statusBarColor);
actionMode = null;
hideAddReactionView();
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
hideAddReactionView();
int itemId = item.getItemId();
if (itemId == R.id.menu_context_copy) {
handleCopyMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_delete_message) {
handleDeleteMessages((int) chatId, getListAdapter().getSelectedItems());
return true;
} else if (itemId == R.id.menu_context_share) {
DcHelper.openForViewOrShare(getContext(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId(), Intent.ACTION_SEND);
return true;
} else if (itemId == R.id.menu_context_details) {
handleDisplayDetails(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_forward) {
handleForwardMessage(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
} else if (itemId == R.id.menu_add_to_home_screen) {
WebxdcActivity.addToHomeScreen(getActivity(), getSelectedMessageRecord(getListAdapter().getSelectedItems()).getId());
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_save_attachment) {
handleSaveAttachment(getListAdapter().getSelectedItems());
return true;
} else if (itemId == R.id.menu_context_reply) {
handleReplyMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_edit) {
handleEditMessage(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
actionMode.finish();
return true;
} else if (itemId == R.id.menu_context_reply_privately) {
handleReplyMessagePrivately(getSelectedMessageRecord(getListAdapter().getSelectedItems()));
return true;
} else if (itemId == R.id.menu_resend) {
handleResendMessage(getListAdapter().getSelectedItems());
return true;
} else if (itemId == R.id.menu_toggle_save) {
handleToggleSave(getListAdapter().getSelectedItems());
actionMode.finish();
return true;
}
return false;
}
}
@Override
public void handleEvent(@NonNull DcEvent event) {
switch (event.getId()) {
case DcContext.DC_EVENT_MSGS_CHANGED:
if (event.getData1Int() == 0 // deleted messages or batch insert
|| event.getData1Int() == chatId) {
reloadList();
}
break;
case DcContext.DC_EVENT_REACTIONS_CHANGED:
case DcContext.DC_EVENT_INCOMING_MSG:
case DcContext.DC_EVENT_MSG_DELIVERED:
case DcContext.DC_EVENT_MSG_FAILED:
case DcContext.DC_EVENT_MSG_READ:
if (event.getData1Int() == chatId) {
reloadList();
}
break;
case DcContext.DC_EVENT_CHAT_MODIFIED:
if (event.getData1Int() == chatId) {
updateLocationButton();
reloadList(true);
}
break;
}
// removing the "new message" marker on incoming messages may be a bit unexpected,
// esp. when a series of message is coming in and after the first, the screen is turned on,
// the "new message" marker will flash for a short moment and disappear.
/*if (eventId == DcContext.DC_EVENT_INCOMING_MSG && isResumed()) {
setLastSeen(-1);
}*/
}
}