package ch.threema.app.fragments.composemessage;

import android.Manifest;
import android.animation.Animator;
import android.animation.LayoutTransition;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Vibrator;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.DateUtils;
import android.util.SparseBooleanArray;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.LinearInterpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.AbsListView;
import android.widget.Filter;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.getkeepsafe.taptargetview.TapTarget;
import com.getkeepsafe.taptargetview.TapTargetView;
import com.google.android.material.badge.BadgeDrawable;
import com.google.android.material.badge.BadgeUtils;
import com.google.android.material.badge.ExperimentalBadgeUtils;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.progressindicator.CircularProgressIndicator;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.util.concurrent.ListenableFuture;

import org.jetbrains.annotations.Contract;
import org.koin.android.compat.ViewModelCompat;
import org.koin.core.parameter.ParametersHolder;
import org.koin.java.KoinJavaComponent;
import org.slf4j.Logger;

import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.sql.SQLException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.AnyThread;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.view.ActionMode;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.compose.ui.platform.ComposeView;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.content.pm.ShortcutManagerCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.DefaultLifecycleObserver;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import androidx.transition.Slide;
import androidx.transition.Transition;
import androidx.transition.TransitionManager;
import ch.threema.android.ActivityExtensionsKt;
import ch.threema.android.FlowJavaCompat;
import ch.threema.android.LifecycleAwareAsyncTask;
import ch.threema.android.ToastDuration;
import ch.threema.app.AppConstants;
import ch.threema.app.BuildConfig;
import ch.threema.app.ExecutorServices;
import ch.threema.app.R;
import ch.threema.app.ThreemaApplication;
import ch.threema.app.actions.SendAction;
import ch.threema.app.actions.TextMessageSendAction;
import ch.threema.app.activities.ComposeMessageActivity;
import ch.threema.app.activities.DistributionListAddActivity;
import ch.threema.app.activities.GroupDetailActivity;
import ch.threema.app.activities.ImagePaintActivity;
import ch.threema.app.activities.RecipientListBaseActivity;
import ch.threema.app.activities.SendMediaActivity;
import ch.threema.app.activities.ThreemaActivity;
import ch.threema.app.activities.ThreemaToolbarActivity;
import ch.threema.app.activities.poll.PollOverviewActivity;
import ch.threema.app.activities.notificationpolicy.ContactNotificationsActivity;
import ch.threema.app.activities.notificationpolicy.GroupNotificationsActivity;
import ch.threema.app.adapters.ComposeMessageAdapter;
import ch.threema.app.adapters.decorators.AudioChatAdapterDecorator;
import ch.threema.app.adapters.decorators.PollChatAdapterDecorator;
import ch.threema.app.adapters.decorators.ChatAdapterDecoratorListener;
import ch.threema.app.adapters.decorators.FileChatAdapterDecorator;
import ch.threema.app.adapters.decorators.MessagePlayerFactory;
import ch.threema.app.adapters.decorators.VoipStatusDataChatAdapterDecorator;
import ch.threema.app.asynctasks.EmptyOrDeleteConversationsAsyncTask;
import ch.threema.app.asynctasks.LoadDecryptedMessageFileAsyncTask;
import ch.threema.app.asynctasks.SaveMediaAsyncTask;
import ch.threema.app.availabilitystatus.AvailabilityStatusContactBannerView;
import ch.threema.app.availabilitystatus.AvailabilityStatusTooltipPopupManager;
import ch.threema.app.availabilitystatus.ViewFullAvailabilityStatusBottomSheetDialog;
import ch.threema.app.cache.ThumbnailCache;
import ch.threema.app.compose.common.interop.ComposeJavaBridge;
import ch.threema.app.contactdetails.ContactDetailActivity;
import ch.threema.app.di.DependencyContainer;
import ch.threema.app.dialogs.BottomSheetGridDialog;
import ch.threema.app.dialogs.ExpandableTextEntryDialog;
import ch.threema.app.dialogs.GenericAlertDialog;
import ch.threema.app.dialogs.GenericProgressDialog;
import ch.threema.app.dialogs.ResendGroupMessageDialog;
import ch.threema.app.dialogs.SelectorDialog;
import ch.threema.app.drafts.DraftManager;
import ch.threema.app.drafts.DraftUpdateTextWatcher;
import ch.threema.app.drafts.MessageDraft;
import ch.threema.app.emojireactions.EmojiHintPopupManager;
import ch.threema.app.emojireactions.EmojiReactionsOverviewActivity;
import ch.threema.app.emojireactions.EmojiReactionsPickerActivity;
import ch.threema.app.emojireactions.EmojiReactionsPopup;
import ch.threema.app.emojis.EmojiButton;
import ch.threema.app.emojis.EmojiMarkupUtil;
import ch.threema.app.emojis.EmojiPicker;
import ch.threema.app.emojis.EmojiTextView;
import ch.threema.app.eventbus.GlobalEventBuses;
import ch.threema.app.eventbus.GlobalEventFlows;
import ch.threema.app.eventbus.events.ActionEvent;
import ch.threema.app.eventbus.events.ContactEvent;
import ch.threema.app.eventbus.events.ConversationEvent;
import ch.threema.app.eventbus.events.GroupEvent;
import ch.threema.app.eventbus.events.PollEvent;
import ch.threema.app.eventbus.events.VoipCallEvent;
import ch.threema.app.glide.AvatarOptions;
import ch.threema.app.home.HomeActivity;
import ch.threema.app.managers.ServiceManager;
import ch.threema.app.mediaattacher.MediaAttachActivity;
import ch.threema.app.mediaattacher.MediaFilterQuery;
import ch.threema.app.mediagallery.MediaGalleryActivity;
import ch.threema.app.messagedetails.MessageDetailsActivity;
import ch.threema.app.messagereceiver.ContactMessageReceiver;
import ch.threema.app.messagereceiver.DistributionListMessageReceiver;
import ch.threema.app.messagereceiver.GroupMessageReceiver;
import ch.threema.app.messagereceiver.MessageReceiver;
import ch.threema.app.messagereceiver.SendingPermissionValidationResult;
import ch.threema.app.multidevice.MultiDeviceManager;
import ch.threema.app.preference.service.PreferenceService;
import ch.threema.app.routines.MarkAsReadRoutine;
import ch.threema.app.services.ActivityService;
import ch.threema.app.services.BlockedIdentitiesService;
import ch.threema.app.services.ContactService;
import ch.threema.app.services.ConversationCategoryService;
import ch.threema.app.services.ConversationService;
import ch.threema.app.services.DeviceService;
import ch.threema.app.services.DistributionListService;
import ch.threema.app.services.DownloadService;
import ch.threema.app.services.FileService;
import ch.threema.app.services.GroupFlowDispatcher;
import ch.threema.app.services.GroupService;
import ch.threema.app.eventbus.events.MessageEvent;
import ch.threema.app.services.MessageService;
import ch.threema.app.services.RingtoneService;
import ch.threema.app.services.UserService;
import ch.threema.app.services.VoiceMessagePlayerService;
import ch.threema.app.services.WallpaperService;
import ch.threema.app.services.poll.PollService;
import ch.threema.app.services.license.LicenseService;
import ch.threema.app.services.messageplayer.MessagePlayer;
import ch.threema.app.services.messageplayer.MessagePlayerService;
import ch.threema.app.services.notification.NotificationService;
import ch.threema.app.typingindicator.TypingIndicatorProvider;
import ch.threema.app.ui.AvatarView;
import ch.threema.app.ui.BottomSheetItem;
import ch.threema.app.ui.ContentCommitComposeEditText;
import ch.threema.app.ui.ConversationListView;
import ch.threema.app.ui.DebouncedOnClickListener;
import ch.threema.app.ui.ListViewTouchSwipeListener;
import ch.threema.app.ui.LongToast;
import ch.threema.app.ui.MediaItem;
import ch.threema.app.ui.OngoingCallNoticeMode;
import ch.threema.app.ui.OngoingCallNoticeView;
import ch.threema.app.ui.OpenPollNoticeView;
import ch.threema.app.ui.QuotePopup;
import ch.threema.app.ui.ReportSpamView;
import ch.threema.app.ui.RootViewDeferringInsetsCallback;
import ch.threema.app.ui.ScrollButtonManager;
import ch.threema.app.ui.SelectorDialogItem;
import ch.threema.app.ui.SendButton;
import ch.threema.android.textwatchers.SimpleTextWatcher;
import ch.threema.app.ui.SingleToast;
import ch.threema.app.ui.TooltipPopup;
import ch.threema.app.ui.TranslateDeferringInsetsAnimationCallback;
import ch.threema.app.ui.TypingIndicatorTextWatcher;
import ch.threema.app.ui.VerificationLevelImageView;
import ch.threema.app.ui.listitemholder.ComposeMessageHolder;
import ch.threema.app.usecases.conversations.EmptyOrDeleteConversationsUseCase;
import ch.threema.app.utils.AnimationUtil;
import ch.threema.app.utils.FileProviderUtil;
import ch.threema.app.utils.PollUtil;
import ch.threema.app.utils.ConfigUtils;
import ch.threema.app.utils.ContactUtil;
import ch.threema.app.utils.DialogUtil;
import ch.threema.app.utils.EditTextUtil;
import ch.threema.app.utils.FileUtil;
import ch.threema.app.utils.GroupCallUtil;
import ch.threema.app.utils.GroupFeatureAdoptionRate;
import ch.threema.app.utils.GroupFeatureSupport;
import ch.threema.app.utils.IntentDataUtil;
import ch.threema.app.utils.LinkifyUtil;
import ch.threema.app.utils.LocaleUtil;
import ch.threema.app.utils.MessageUtil;
import ch.threema.app.utils.MessageUtilKt;
import ch.threema.app.utils.NavigationUtil;
import ch.threema.app.utils.QuoteUtil;
import ch.threema.app.utils.RuntimeUtil;
import ch.threema.app.utils.ShortcutUtil;
import ch.threema.app.utils.SoundEffectPlayer;
import ch.threema.app.utils.TapTargetViewUtil;
import ch.threema.app.utils.ToolbarUtil;
import ch.threema.app.voicemessage.VoiceRecorderActivity;
import ch.threema.app.voip.groupcall.GroupCallDescription;
import ch.threema.app.voip.groupcall.GroupCallManager;
import ch.threema.app.voip.groupcall.GroupCallObserver;
import ch.threema.app.voip.services.VoipCallService;
import ch.threema.app.voip.services.VoipStateService;
import ch.threema.app.voip.util.VoipUtil;
import ch.threema.app.webviews.WorkExplainActivity;
import ch.threema.common.TimeProvider;
import ch.threema.data.datatypes.AvailabilityStatus;
import ch.threema.data.datatypes.ContactConversationId;
import ch.threema.data.datatypes.ConversationId;
import ch.threema.data.datatypes.DistributionListConversationId;
import ch.threema.data.datatypes.GroupConversationId;
import ch.threema.data.datatypes.GroupNotificationTriggerPolicyOverridePolicy;
import ch.threema.data.datatypes.IdColor;
import ch.threema.data.datatypes.GroupNotificationTriggerPolicyOverride;
import ch.threema.data.datatypes.NotificationTriggerPolicyOverride;
import ch.threema.data.datatypes.PredefinedContact;
import ch.threema.data.models.ContactModelData;
import ch.threema.data.datatypes.GroupIdentity;
import ch.threema.data.models.GroupModel;
import ch.threema.data.models.GroupModelData;
import ch.threema.data.repositories.EmojiReactionsRepository;
import ch.threema.data.repositories.GroupModelRepository;
import ch.threema.domain.models.AcquaintanceLevel;
import ch.threema.domain.models.IdentityType;
import ch.threema.domain.models.MessageId;
import ch.threema.domain.models.VerificationLevel;
import ch.threema.data.datatypes.ConversationIdObfuscated;
import ch.threema.domain.protocol.ThreemaFeature;
import ch.threema.domain.protocol.csp.ProtocolDefines;
import ch.threema.domain.taskmanager.TriggerSource;
import ch.threema.storage.DatabaseService;
import ch.threema.storage.factories.RejectedGroupMessageFactory;
import ch.threema.storage.models.AbstractMessageModel;
import ch.threema.storage.models.ContactModel;
import ch.threema.storage.models.DateSeparatorMessageModel;
import ch.threema.storage.models.DistributionListMessageModel;
import ch.threema.storage.models.DistributionListModel;
import ch.threema.storage.models.FirstUnreadMessageModel;
import ch.threema.storage.models.MessageModel;
import ch.threema.storage.models.MessageState;
import ch.threema.storage.models.MessageType;
import ch.threema.storage.models.poll.PollModel;
import ch.threema.storage.models.data.DisplayTag;
import ch.threema.storage.models.data.MessageContentsType;
import ch.threema.storage.models.group.GroupMessageModel;
import ch.threema.storage.models.group.GroupModelOld;
import kotlin.Unit;
import kotlin.jvm.functions.Function0;

import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.widget.AbsListView.CHOICE_MODE_MULTIPLE;
import static android.widget.AbsListView.CHOICE_MODE_MULTIPLE_MODAL;
import static ch.threema.android.ToastKt.showToast;
import static ch.threema.app.ThreemaApplication.getAppContext;
import static ch.threema.app.adapters.ComposeMessageAdapter.MIN_CONSTRAINT_LENGTH;
import static ch.threema.app.adapters.decorators.PollChatAdapterDecorator.ACTION_CLOSE;
import static ch.threema.app.adapters.decorators.PollChatAdapterDecorator.ACTION_RESULTS;
import static ch.threema.app.adapters.decorators.PollChatAdapterDecorator.ACTION_VOTE;
import static ch.threema.app.messagereceiver.MessageReceiverExtensionsKt.isGatewayChat;
import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_AUDIORECORDER;
import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_LIFECYCLE;
import static ch.threema.app.services.messageplayer.MessagePlayer.SOURCE_VOIP;
import static ch.threema.app.ui.NotificationTriggerPolicyExtensionsKt.muteAppliesAt;
import static ch.threema.app.ui.ScrollButtonManager.SCROLLBUTTON_VIEW_TIMEOUT;
import static ch.threema.app.ui.ScrollButtonManager.TYPE_DOWN;
import static ch.threema.app.utils.ActiveScreenLoggerKt.logScreenVisibility;
import static ch.threema.app.utils.MessageUtil.canDeleteRemotely;
import static ch.threema.app.utils.ShortcutUtil.TYPE_CHAT;
import static ch.threema.base.utils.LoggingKt.getThreemaLogger;
import static ch.threema.common.JavaCompat.areEqual;
import static ch.threema.common.JavaCompat.isNullOrBlank;
import static ch.threema.common.JavaCompat.isNullOrEmpty;
import static ch.threema.common.TimeExtensionsKt.isSameDayAs;
import static ch.threema.storage.models.data.DisplayTag.DISPLAY_TAG_STARRED;

public class ComposeMessageFragment extends Fragment implements
    LifecycleOwner,
    DefaultLifecycleObserver,
    SwipeRefreshLayout.OnRefreshListener,
    GenericAlertDialog.DialogClickListener,
    SelectorDialog.SelectorDialogClickListener,
    EmojiPicker.EmojiPickerListener,
    ReportSpamView.OnReportButtonClickListener,
    ThreemaToolbarActivity.OnSoftKeyboardChangedListener,
    ExpandableTextEntryDialog.ExpandableTextEntryDialogClickListener,
    VoipStatusDataChatAdapterDecorator.VoipStatusDataChatListener,
    PollChatAdapterDecorator.PollChatListener,
    ChatAdapterDecoratorListener,
    LinkifyUtil.LinkifyListener,
    MessagePlayerFactory,
    FileChatAdapterDecorator.DownloadAlertDialogListener,
    AudioChatAdapterDecorator.UserInteractionListener {

    private static final Logger logger = getThreemaLogger("ComposeMessageFragment");

    private static final String CONFIRM_TAG_DELETE_DISTRIBUTION_LIST = "deleteDistributionList";
    public static final String DIALOG_TAG_CONFIRM_CALL = "dtcc";
    private static final String DIALOG_TAG_CHOOSE_SHORTCUT_TYPE = "st";
    private static final String DIALOG_TAG_EMPTY_CHAT = "ccc";
    private static final String DIALOG_TAG_CONFIRM_BLOCK = "block";
    private static final String DIALOG_TAG_DECRYPTING_MESSAGES = "dcr";
    private static final String DIALOG_TAG_SEARCHING = "src";
    private static final String DIALOG_TAG_LOADING_MESSAGES = "loadm";
    private static final String DIALOG_TAG_MESSAGE_DETAIL = "messageLog";
    private static final String DIALOG_TAG_CONFIRM_MESSAGE_DELETE = "msgdel";
    private static final String DIALOG_TAG_CONFIRM_RESEND = "confirm_resend";
    private static final String DIALOG_TAG_EDIT_MESSAGES_UNSUPPORTED_WARNING = "editmsg_unsupported";
    private static final String DIALOG_TAG_DELETE_MESSAGES_UNSUPPORTED_WARNING = "deletemsg_unsupported";

    public static final String EXTRA_API_MESSAGE_ID = "apimsgid";
    public static final String EXTRA_SEARCH_QUERY = "searchQuery";
    public static final String EXTRA_OVERRIDE_BACK_TO_HOME_BEHAVIOR = "backOverride";
    public static final String EXTRA_LAST_MEDIA_SEARCH_QUERY = "searchMediaQuery";
    public static final String EXTRA_LAST_MEDIA_TYPE_QUERY = "searchMediaType";

    private static final int PERMISSION_REQUEST_SAVE_MESSAGE = 2;
    private static final int PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE = 7;
    private static final int PERMISSION_REQUEST_ATTACH_CAMERA = 8;
    private static final int PERMISSION_REQUEST_ATTACH_CAMERA_VIDEO = 11;

    private static final int ACTIVITY_ID_VOICE_RECORDER = 9731;

    public static final long VIBRATION_MSEC = 300;
    private static final long MESSAGE_PAGE_SIZE = 100;
    private static final int SMOOTHSCROLL_THRESHOLD = 10;
    private static final int MAX_SELECTED_ITEMS = 100; // may not be larger than MESSAGE_PAGE_SIZE
    public static final int MAX_FORWARDABLE_ITEMS = 50;

    private static final String CAMERA_URI = "camera_uri";
    private static final String BUNDLE_LIST_POSITION = "list_position";
    private static final String BUNDLE_LIST_CONVERSATION_ID_OBFUSCATED = "list_conversation_id_obfuscated";
    private static final String BUNDLE_LIST_TOP = "list_top";
    private static final String BUNDLE_LIST_LONG_CLICK_ITEM = "list_long_click_item";

    @NonNull
    private final DependencyContainer dependencies = KoinJavaComponent.get(DependencyContainer.class);

    private ComposeMessageViewModel viewModel;

    private ContentCommitComposeEditText messageText;
    private SendButton sendButton;
    private ImageButton attachButton, cameraButton, sendEditMessageButton;
    private ContactModel contactModel;
    private @Nullable MessageReceiver messageReceiver;

    private AudioManager audioManager;
    private ConversationListView convListView;
    private FrameLayout historyParent;
    private @Nullable ComposeMessageAdapter composeMessageAdapter;
    private View isTypingView;

    private MenuItem mutedMenuItem;
    private MenuItem blockMenuItem;
    private MenuItem deleteDistributionListItem;
    private MenuItem callItem;
    private MenuItem showOpenPollWindowMenuItem;
    private MenuItem showPollsMenuItem;
    private MenuItem showEmptyChatMenuItem;
    private TextView dateTextView;
    private TextInputLayout textInputLayout;
    private ConstraintLayout conversationParent;

    private ActionMode actionMode = null;
    private ActionMode searchActionMode = null;
    private ActionMode editMessageActionMode = null;
    private FrameLayout dateView = null;
    private FrameLayout bottomPanel = null;
    private String identity;
    private Long groupDatabaseId = 0L;
    private Long distributionListId = 0L;
    private Uri cameraUri;
    private int longClickItem = AbsListView.INVALID_POSITION;
    private int listViewTop = 0, lastFirstVisibleItem = -1;
    private TypingIndicatorTextWatcher typingIndicatorTextWatcher;
    private @Nullable DraftUpdateTextWatcher draftUpdateTextWatcher;
    private @NonNull Map<String, Integer> identityColors = Collections.emptyMap();
    private MediaFilterQuery lastMediaFilter;
    private RootViewDeferringInsetsCallback rootInsetsDeferringCallback = null;
    private TranslateDeferringInsetsAnimationCallback keyboardAnimationInsetsCallback = null;

    private MultiDeviceManager multiDeviceManager;
    private PreferenceService preferenceService;
    private ContactService contactService;
    private MessageService messageService;
    private NotificationService notificationService;
    private BlockedIdentitiesService blockedIdentitiesService;
    private ConversationService conversationService;
    private DeviceService deviceService;
    private WallpaperService wallpaperService;
    private ConversationCategoryService conversationCategoryService;
    private RingtoneService ringtoneService;
    private UserService userService;
    private FileService fileService;
    private VoipStateService voipStateService;
    private DownloadService downloadService;
    private LicenseService licenseService;
    private EmojiReactionsRepository emojiReactionsRepository;
    private DraftManager draftManager;
    private ComposeMessageFragmentUtils composeMessageFragmentUtils;
    private SoundEffectPlayer soundEffectPlayer;
    private final GlobalEventBuses globalEventBuses = KoinJavaComponent.get(GlobalEventBuses.class);
    private final GlobalEventFlows globalEventFlows = KoinJavaComponent.get(GlobalEventFlows.class);
    private final TypingIndicatorProvider typingIndicatorProvider = KoinJavaComponent.get(TypingIndicatorProvider.class);
    @NonNull
    private final TimeProvider timeProvider = KoinJavaComponent.get(TimeProvider.class);

    private ActivityResultLauncher<Intent> wallpaperLauncher;
    private final ActivityResultLauncher<Intent> emojiReactionsLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
        rootInsetsDeferringCallback.setEnabled(true);
        keyboardAnimationInsetsCallback.setEnabled(true);
        if (actionMode != null) {
            actionMode.finish();
        }
    });
    private final ActivityResultLauncher<Intent> imageReplyLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> {
        if (result.getResultCode() == Activity.RESULT_CANCELED) {
            logger.info("Canceled image reply");
            return;
        }

        Intent resultIntent = result.getData();
        if (resultIntent == null) {
            logger.error("Result intent must not be null");
            return;
        }

        @SuppressWarnings("rawtypes")
        MessageReceiver receiver = IntentDataUtil.getMessageReceiverFromIntent(getContext(), resultIntent);
        MediaItem mediaItem = resultIntent.getParcelableExtra(Intent.EXTRA_STREAM);
        if (receiver == null) {
            logger.error("The receiver must not be null");
            return;
        }
        if (mediaItem == null) {
            logger.error("The media item must not be null");
            return;
        }

        messageService.sendMediaAsync(Collections.singletonList(mediaItem), Collections.singletonList(receiver));
    });

    private boolean listUpdateInProgress = false, isPaused = false;
    @NonNull
    private final List<AbstractMessageModel> unreadMessages = new ArrayList<>();
    @NonNull
    private final List<AbstractMessageModel> messageValues = new ArrayList<>();
    @NonNull
    private final List<AbstractMessageModel> selectedMessages = new ArrayList<>(1);

    private EmojiMarkupUtil emojiMarkupUtil;
    private EmojiPicker emojiPicker;
    private EmojiButton emojiButton;
    private SwipeRefreshLayout swipeRefreshLayout;
    private Integer currentPageReferenceId = null;
    private EmojiTextView actionBarTitleTextView;
    private TextView actionBarSubtitleTextView;
    private VerificationLevelImageView actionBarSubtitleImageView;
    private AvatarView actionBarAvatarView;
    private ImageView wallpaperView;
    private ActionBar actionBar;
    private TooltipPopup workTooltipPopup;
    private TooltipPopup availabilityStatusTooltipPopup;
    private EmojiReactionsPopup emojiReactionsPopup;
    private QuotePopup quotePopup;
    private OpenPollNoticeView openPollNoticeView;
    private ReportSpamView reportSpamView;
    private AvailabilityStatusContactBannerView availabilityStatusBannerView;
    private ComposeMessageActivity activity;
    private View fragmentView;
    private FrameLayout coordinatorLayout;
    private PollService pollService;
    private DatabaseService databaseService;
    private LayoutInflater layoutInflater;
    private ListViewTouchSwipeListener listViewTouchSwipeListener;

    private GroupService groupService;
    private GroupModelRepository groupModelRepository;
    private GroupCallManager groupCallManager;
    private GroupFlowDispatcher groupFlowDispatcher;
    private boolean isGroupChat = false;
    private GroupModel groupModel;
    private Instant listInitializedAt;
    private boolean isDistributionListChat = false;
    private DistributionListService distributionListService;
    private DistributionListModel distributionListModel;
    private MessagePlayerService messagePlayerService;
    private int listInstancePosition = AbsListView.INVALID_POSITION;
    private int listInstanceTop = 0;
    private @Nullable ConversationIdObfuscated listInstanceConversationIdObfuscated = null;
    private ConversationId conversationId = null;
    private int unreadCount = 0, recentlyAddedCount = 0;
    private TextView searchCounter;
    private CircularProgressIndicator searchProgress;
    private ImageView searchNextButton, searchPreviousButton;
    private ViewGroup editMessageBubbleContainer;
    private View dimBackground;
    private ComposeView editMessageBubbleComposeView;

    private OngoingCallNoticeView ongoingCallNotice;
    private GroupCallObserver groupCallObserver;
    private ScrollButtonManager scrollButtonManager;
    private final EmojiHintPopupManager emojiHintPopupManager = new EmojiHintPopupManager(
        getAppContext(),
        () -> activity,
        () -> convListView
    );

    private ThumbnailCache<Integer> thumbnailCache = null;

    @Override
    public void onCreate(@NonNull LifecycleOwner owner) {
        logScreenVisibility(this, logger);
    }

    @Override
    public boolean isActionModeEnabled() {
        return actionMode != null;
    }

    @Override
    public boolean isInChoiceMode() {
        if (convListView == null) {
            return false;
        }
        int choiceMode = convListView.getChoiceMode();
        return choiceMode == CHOICE_MODE_MULTIPLE || choiceMode == CHOICE_MODE_MULTIPLE_MODAL;
    }

    @Override
    public boolean shouldHandleLinkClick() {
        return !isActionModeEnabled();
    }

    private final MessageService.MessageFilter nextMessageFilter = new MessageService.MessageFilter() {
        @Override
        public long getPageSize() {
            return MESSAGE_PAGE_SIZE;
        }

        @Override
        public Integer getPageReferenceId() {
            return getCurrentPageReferenceId();
        }

        @Override
        public boolean withStatusMessages() {
            return true;
        }

        @Override
        public boolean withUnsaved() {
            return false;
        }

        @Override
        public boolean onlyUnread() {
            return false;
        }

        @Override
        public boolean onlyDownloaded() {
            return false;
        }

        @Override
        public MessageType[] types() {
            return null;
        }

        @Override
        public int[] contentTypes() {
            return null;
        }

        @Override
        public int[] displayTags() {
            return null;
        }
    };

    // handler to remove dateview button after a certain time
    private final Handler dateViewHandler = new Handler();
    private final Runnable dateViewTask = () -> RuntimeUtil.runOnUiThread(() -> {
        if (dateView != null && dateView.getVisibility() == View.VISIBLE) {
            AnimationUtil.slideOutAnimation(dateView, false, 1f, null);
        }
    });

    private void onNewMessage(@NonNull final AbstractMessageModel newMessage) {
        if (isAdded() && !isDetached() && !isRemoving()) {
            if (newMessage.isOutbox()) {
                if (addMessageToList(newMessage)) {
                    if (!newMessage.isStatusMessage() && newMessage.getType() != MessageType.VOIP_STATUS && newMessage.getType() != MessageType.GROUP_CALL_STATUS) {
                        playSentSound();

                        if (reportSpamView != null && reportSpamView.getVisibility() == View.VISIBLE) {
                            reportSpamView.hide();
                        }
                    }
                }
            } else {
                if (addMessageToList(newMessage) && !isPaused) {
                    if (!newMessage.isStatusMessage() && newMessage.getType() != MessageType.VOIP_STATUS && newMessage.getType() != MessageType.GROUP_CALL_STATUS) {
                        playReceivedSound();
                    }

                    if (convListView != null) {
                        convListView.post(() -> {
                            if (convListView.canScrollList(View.SCROLL_AXIS_VERTICAL)) {
                                // list view is not fully scrolled to the bottom
                                if (scrollButtonManager != null) {
                                    scrollButtonManager.showButton(ScrollButtonManager.TYPE_DOWN, recentlyAddedCount);
                                }
                            }
                        });
                    }
                }
            }
        }
    }

    private void onMessagesUpdated(@NonNull List<? extends AbstractMessageModel> modifiedMessageModels) {
        final @NonNull List<AbstractMessageModel> safeModifiedMessageModels = modifiedMessageModels
            .stream()
            .filter(this::ensureMessageTypeIsCorrectForCurrentChat)
            .collect(Collectors.toUnmodifiableList());

        //replace model
        synchronized (messageValues) {
            for (final AbstractMessageModel modifiedMessageModel : safeModifiedMessageModels) {
                if (modifiedMessageModel.getId() != 0) {
                    for (int n = 0; n < messageValues.size(); n++) {
                        AbstractMessageModel listModel = messageValues.get(n);
                        if (listModel != null && listModel.getId() == modifiedMessageModel.getId()) {
                            //if the changed message is different to the created
                            if (modifiedMessageModel != listModel) {
                                //replace item
                                messageValues.set(n, modifiedMessageModel);
                            }
                            break;
                        }
                    }
                }
            }
        }
        if (composeMessageAdapter != null) {
            composeMessageAdapter.notifyItemsChanged(safeModifiedMessageModels);
        }
        if (safeModifiedMessageModels.size() == 1) {
            final @Nullable AbstractMessageModel modifiedMessageModel = safeModifiedMessageModels.get(0);
            if (modifiedMessageModel != null && modifiedMessageModel.isDeleted()) {
                updateActionModeIfNecessary(modifiedMessageModel);
                dismissEmojiReactionPopupIfMessageWasDeleted(modifiedMessageModel);
            }
        }
    }

    private void onMessageRemoved(final AbstractMessageModel removedMessageModel) {
        if (composeMessageAdapter != null && removedMessageModel != null) {
            composeMessageAdapter.remove(removedMessageModel);
        }
    }

    private boolean ensureMessageTypeIsCorrectForCurrentChat(final @NonNull AbstractMessageModel abstractMessageModel) {
        return abstractMessageModel instanceof FirstUnreadMessageModel
            || (isGroupChat && abstractMessageModel instanceof GroupMessageModel)
            || (isDistributionListChat && abstractMessageModel instanceof DistributionListMessageModel)
            || (abstractMessageModel instanceof MessageModel);
    }

    /**
     * If we currently selected one message we have to consider dismissing the emoji-reactions-popup
     * if the message was remote-deleted.
     *
     * @param deletedMessageModel The deleted message model
     */
    private void dismissEmojiReactionPopupIfMessageWasDeleted(final @NonNull AbstractMessageModel deletedMessageModel) {

        if (emojiReactionsPopup == null || !emojiReactionsPopup.isShowing() || selectedMessages.isEmpty()) {
            return;
        }

        // Determine if the newly remote-deleted message is currently selected
        final boolean deletedMessageIsCurrentlySelected = selectedMessages.stream().anyMatch(
            (selectedMessageModel -> selectedMessageModel.getId() == deletedMessageModel.getId())
        );

        if (deletedMessageIsCurrentlySelected) {
            emojiReactionsPopup.dismiss();
        }
    }

    /**
     * If we currently have some messages selected (actionMode is visible) we have to consider updating the
     * menu items it displays when a message was remote-deleted.
     */
    private void updateActionModeIfNecessary(final @NonNull AbstractMessageModel deletedMessageModel) {
        // It is only possible to remote-delete a single message at a time
        if (actionMode == null || selectedMessages.isEmpty()) {
            return;
        }

        // Determine if the newly remote-deleted message is currently selected
        final boolean deletedMessageIsCurrentlySelected = selectedMessages.stream().anyMatch(
            (selectedMessageModel -> selectedMessageModel.getId() == deletedMessageModel.getId())
        );

        if (deletedMessageIsCurrentlySelected) {
            actionMode.invalidate();
        }
    }

    /**
     * Updates both the legacy {@code groupModel} and the {@code messageReceiver} variables of this fragment. We do not refresh the
     * data from the database if the passed {@code groupIdentityOfChangedGroup} is not the currently displayed group in this chat.
     *
     * @return Whether the changed group affects the currently opened conversation
     */
    private boolean updateLocalGroupModelAndReceiver(@NonNull GroupIdentity groupIdentityOfChangedGroup) {
        if (!(messageReceiver instanceof GroupMessageReceiver)) {
            // We are not even in a group conversation
            return false;
        }
        if (!(((GroupMessageReceiver) messageReceiver).getGroup().getGroupIdentity().equals(groupIdentityOfChangedGroup))) {
            // The group chat that is currently opened is not the one that changed
            return false;
        }
        final @Nullable GroupModel currentGroupModel = groupModelRepository.getByGroupIdentity(groupIdentityOfChangedGroup);
        if (currentGroupModel != null) {
            messageReceiver = groupService.createReceiver(groupModel);
            composeMessageAdapter.setMessageReceiver(messageReceiver);
        }
        return true;
    }

    private void updateGroupCallObserverRegistration() {
        // groupModel may be null if Fragment was re-configured with a new intent
        if (isGroupChat && groupModel != null && groupModel.isMember()) {
            registerGroupCallObserver();
        } else {
            // Remove ongoing group call notice if not a member of the group anymore
            updateOngoingCallNotice();
            removeGroupCallObserver();
        }
    }

    @UiThread
    private void updateActionModeIfVisible() {
        if (actionMode != null) {
            actionMode.invalidate();
        }
    }

    private final QuotePopup.QuotePopupListener quotePopupListener = new QuotePopup.QuotePopupListener() {
        @Override
        public void onHeightSet(int height) {
            if (historyParent != null) {
                historyParent.postDelayed(() ->
                    historyParent.setPadding(
                        historyParent.getPaddingLeft(),
                        historyParent.getPaddingTop(),
                        historyParent.getPaddingRight(),
                        height), 30);
            }
        }

        @Override
        public void onDismiss() {
            if (historyParent != null) {
                historyParent.postDelayed(() ->
                    historyParent.setPadding(
                        historyParent.getPaddingLeft(),
                        historyParent.getPaddingTop(),
                        historyParent.getPaddingRight(),
                        0), 70);
            }
        }

        @Override
        public void onPostVisibilityChange() {
            if (messageText != null) {
                updateSendButton(messageText.getText());
                updateCameraButton();
            }
        }
    };

    @Override
    public void onRefresh() {
        logger.debug("onRefresh");
        if (actionMode != null || searchActionMode != null || editMessageActionMode != null) {
            swipeRefreshLayout.setRefreshing(false);
            return;
        }
        viewModel.loadNextRecords(messageReceiver, nextMessageFilter);
    }

    @Override
    public void onAttach(@NonNull Activity activity) {
        ((FragmentActivity) activity).getLifecycle().addObserver(this);
        logger.debug("onAttach");

        super.onAttach(activity);

        setHasOptionsMenu(true);

        this.activity = (ComposeMessageActivity) activity;
        this.audioManager = (AudioManager) activity.getSystemService(Context.AUDIO_SERVICE);

        if (bottomPanel != null) {
            bottomPanel.setVisibility(View.VISIBLE);
        }

        if (this.emojiPicker != null) {
            this.emojiPicker.init(this.activity, ServiceManager.require().getEmojiService(), true);
        }

        // resolution and layout may have changed after being attached to a new activity
        ConfigUtils.getPreferredThumbnailWidth(activity, true);
        ConfigUtils.getPreferredAudioMessageWidth(activity, true);
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        logger.info("onCreate");
        super.onCreate(savedInstanceState);

        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getContacts(), this::handleContactEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getMessages(), this::handleMessageEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getGroups(), this::handleGroupEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getConversations(), this::handleConversationEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getActions(), this::handleActionEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.STARTED, globalEventFlows.getPolls(), this::handlePollEvent);
        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, globalEventFlows.getVoipCalls(), this::handleVoipCallEvent);

        FlowJavaCompat.collect(this, Lifecycle.State.CREATED, typingIndicatorProvider.watchTypingIdentityStrings(), (typingContacts) -> {
            if (ComposeMessageFragment.this.contactModel != null) {
                contactTypingStateChanged(typingContacts.contains(ComposeMessageFragment.this.contactModel.getIdentity()));
            }
        });

        initializeMedia3Controller();
    }

    @UiThread
    private void handleContactEvent(@NonNull ContactEvent event) {
        if (event instanceof ContactEvent.ContactUpdated) {
            if (!event.getIdentityString().equals(ComposeMessageFragment.this.identity)) {
                // Another contact was updated
                return;
            }
            final ContactModel modifiedContactModel = contactService.getByIdentity(event.getIdentityString());
            if (modifiedContactModel != null) {
                updateContactModelData(modifiedContactModel);
            }
        } else if (event instanceof ContactEvent.ContactProfilePictureUpdated) {
            if (!event.getIdentityString().equals(ComposeMessageFragment.this.identity)) {
                // Another contact was updated
                return;
            }
            updateToolbarTitle();
        } else if (event instanceof ContactEvent.ContactRemoved) {
            if (contactModel != null && contactModel.getIdentity().equals(event.getIdentityString())) {
                // our contact has been removed. finish activity.
                finishActivity();
            }
        }
    }

    @UiThread
    private void handleMessageEvent(@NonNull MessageEvent event) {
        if (event instanceof MessageEvent.NewMessage) {
            onNewMessage(((MessageEvent.NewMessage) event).getMessage());
        } else if (event instanceof MessageEvent.MessagesUpdated) {
            onMessagesUpdated(((MessageEvent.MessagesUpdated) event).getMessages());
        } else if (event instanceof MessageEvent.MessageRemovedLocally) {
            onMessageRemoved(((MessageEvent.MessageRemovedLocally) event).getMessage());
        } else if (event instanceof MessageEvent.MessageDeletedForAll) {
            if (composeMessageAdapter != null) {
                composeMessageAdapter.notifyDataSetChanged();
            }
        }
    }

    @UiThread
    private void handleGroupEvent(@NonNull GroupEvent event) {
        if (event instanceof GroupEvent.MemberKicked) {
            var kickedEvent = (GroupEvent.MemberKicked) event;
            onMemberKicked(kickedEvent.getGroupIdentity(), kickedEvent.getIdentityString());
        } else if (event instanceof GroupEvent.GroupUpdated) {
            onGroupUpdated(((GroupEvent.GroupUpdated) event).getGroupIdentity());
        } else if (event instanceof GroupEvent.UserLeftGroup) {
            onLeftGroup(((GroupEvent.UserLeftGroup) event).getGroupIdentity());
        } else if (event instanceof GroupEvent.MemberLeft) {
            onGroupUpdated(((GroupEvent.MemberLeft) event).getGroupIdentity());
        } else if (event instanceof GroupEvent.GroupRenamed) {
            onGroupUpdated(((GroupEvent.GroupRenamed) event).getGroupIdentity());
        } else if (event instanceof GroupEvent.GroupProfilePictureUpdated) {
            onGroupUpdated(((GroupEvent.GroupProfilePictureUpdated) event).getGroupIdentity());
        } else if (event instanceof GroupEvent.NewMember) {
            var newMemberEvent = (GroupEvent.NewMember) event;
            onNewMember(newMemberEvent.getGroupIdentity(), newMemberEvent.getIdentityString());
        } else if (event instanceof GroupEvent.GroupRemoved) {
            onGroupRemoved(((GroupEvent.GroupRemoved) event).getGroupDbId());
        }
    }

    @UiThread
    private void handleActionEvent(@NonNull ActionEvent event) {
        if (event instanceof ActionEvent.QrCodeScanned) {
            onQrCodeScanned(((ActionEvent.QrCodeScanned) event).getScannedText());
        } else if (event instanceof ActionEvent.VoiceMessagePlaybackEnded) {
            onVoiceMessagePlaybackEnded(((ActionEvent.VoiceMessagePlaybackEnded) event).getMessage());
        }
    }

    @UiThread
    private void handleConversationEvent(@NonNull ConversationEvent event) {
        if (event instanceof ConversationEvent.ConversationRemoved) {
            var conversationModel = ((ConversationEvent.ConversationRemoved) event).getConversation();
            boolean itsMyConversation = false;
            if (contactModel != null) {
                itsMyConversation = (conversationModel.getContact() != null
                    && areEqual(conversationModel.getContact().getIdentity(), contactModel.getIdentity()));
            } else if (distributionListModel != null) {
                itsMyConversation = conversationModel.getDistributionList() != null
                    && conversationModel.getDistributionList().getId() == distributionListModel.getId();
            } else if (groupModel != null) {
                itsMyConversation = conversationModel.getGroup() != null
                    && conversationModel.getGroup().getId() == groupModel.getDatabaseId();
            }

            if (itsMyConversation) {
                if (getActivity() != null) {
                    getActivity().finish();
                }
            }
        }
    }

    @UiThread
    private void onNewMember(@NonNull GroupIdentity groupIdentity, String newIdentity) {
        final boolean changeAffectsCurrentGroup = updateLocalGroupModelAndReceiver(groupIdentity);
        if (!changeAffectsCurrentGroup) {
            return;
        }
        final boolean isMe = userService.isMe(newIdentity);
        updateToolbarTitle();
        // Update menus because the group may have been changed from a notes group to a regular group
        updateMenus();
        if (isMe) {
            updateGroupCallObserverRegistration();
            setupMessageTextClickListener();
            updateActionModeIfVisible();
        }
    }

    @UiThread
    private void onGroupRemoved(long groupDbId) {
        if (isGroupChat && groupDbId == this.groupDatabaseId) {
            finishActivity();
        }
    }

    @UiThread
    private void onMemberKicked(@NonNull GroupIdentity groupIdentity, String identityKicked) {
        final boolean changeAffectsCurrentGroup = updateLocalGroupModelAndReceiver(groupIdentity);
        if (!changeAffectsCurrentGroup) {
            return;
        }
        final boolean isMe = userService.isMe(identityKicked);
        updateToolbarTitle();
        // Update menus because the group may now be a notes group
        updateMenus();
        if (isMe) {
            updateGroupCallObserverRegistration();
            hideEmojiPopupIfShown();
            hideEmojiPickerIfShown();
            setupMessageTextClickListener();
            updateActionModeIfVisible();
            SingleToast.getInstance().showLongText(getString(R.string.you_are_not_a_member_of_this_group));
        }
    }

    @UiThread
    private void onGroupUpdated(@NonNull GroupIdentity groupIdentity) {
        final boolean changeAffectsCurrentGroup = updateLocalGroupModelAndReceiver(groupIdentity);
        if (!changeAffectsCurrentGroup) {
            return;
        }
        updateToolbarTitle();
        updateGroupCallObserverRegistration();
        updateMuteMenu();
    }

    @UiThread
    private void onLeftGroup(@NonNull GroupIdentity groupIdentity) {
        GroupModel group = groupModelRepository.getByGroupIdentity(groupIdentity);
        if (isGroupChat && groupDatabaseId != null && group != null && groupDatabaseId == group.getDatabaseId()) {
            finishActivity();
        }
    }

    @UiThread
    private void onQrCodeScanned(@NonNull String scannedText) {
        if (!scannedText.isEmpty() && conversationId != null) {
            draftManager.set(conversationId, scannedText);
        }
    }

    @UiThread
    private void onVoiceMessagePlaybackEnded(@NonNull AbstractMessageModel messageModel) {
        if (composeMessageAdapter != null) {
            int index = composeMessageAdapter.getNextVoiceMessage(messageModel);
            if (index != AbsListView.INVALID_POSITION) {
                logger.info("Playing next audio message at index {}", index);
                View view = composeMessageAdapter.getView(index, null, null);

                ComposeMessageHolder holder = (ComposeMessageHolder) view.getTag();
                if (holder.messagePlayer != null) {
                    holder.messagePlayer.open();
                    composeMessageAdapter.notifyDataSetChanged();
                }
            } else {
                if (mediaControllerFuture != null) {
                    try {
                        MediaController mediaController = mediaControllerFuture.get();
                        if (mediaController != null) {
                            mediaController.stop();
                            mediaController.clearMediaItems();
                        }
                    } catch (Exception e) {
                        logger.error("Unable to clear MediaController", e);
                    }
                }
            }
        }
    }

    @UiThread
    private void handlePollEvent(@NonNull PollEvent event) {
        if (event instanceof PollEvent.NewPoll) {
            if (userService.getIdentity().equals(event.getPoll().getCreatorIdentity())) {
                PollUtil.openDefaultActivity(
                    getContext(),
                    getFragmentManager(),
                    pollService.get(event.getPoll().getId()),
                    messageReceiver
                );
            }
        } else if (event instanceof PollEvent.PollClosed || event instanceof PollEvent.PollRemoved) {
            openPollNoticeView.update();
        }
    }

    @UiThread
    private void handleVoipCallEvent(@NonNull VoipCallEvent event) {
        if (event instanceof VoipCallEvent.Ringing) {
            return;
        }
        if (event instanceof VoipCallEvent.Started) {
            updateVoipCallMenuItem(false);
            if (messagePlayerService != null) {
                messagePlayerService.pauseAll(SOURCE_VOIP);
            }
        } else {
            updateVoipCallMenuItem(true);
            hideOngoingVoipCallNotice();
        }
    }

    @Override
    @ExperimentalBadgeUtils
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        logger.info("onCreateView");

        if (!requiredInstances()) {
            finishActivity();
            return this.fragmentView;
        }

        this.layoutInflater = inflater;

        if (this.fragmentView == null) {
            // set font size
            activity.getTheme().applyStyle(preferenceService.getFontStyle(), true);
            this.fragmentView = inflater.inflate(R.layout.fragment_compose_message, container, false);

            this.coordinatorLayout = fragmentView.findViewById(R.id.compose_root);

            this.convListView = fragmentView.findViewById(R.id.history);
            ViewCompat.setNestedScrollingEnabled(this.convListView, true);
            this.convListView.setDivider(null);
            this.convListView.setClipToPadding(false);
            this.convListView.getLayoutTransition().disableTransitionType(LayoutTransition.CHANGING);
            this.convListView.getLayoutTransition().disableTransitionType(LayoutTransition.CHANGE_APPEARING);
            this.convListView.getLayoutTransition().disableTransitionType(LayoutTransition.CHANGE_DISAPPEARING);

            if (ConfigUtils.isTabletLayout()) {
                this.convListView.setPadding(0, 0, 0, 0);
            }

            this.historyParent = fragmentView.findViewById(R.id.history_parent);

            this.listViewTop = this.convListView.getPaddingTop();
            this.swipeRefreshLayout = fragmentView.findViewById(R.id.ptr_layout);
            this.swipeRefreshLayout.setOnRefreshListener(this);
            this.swipeRefreshLayout.setProgressBackgroundColorSchemeColor(
                ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorSurfaceContainer)
            );
            this.swipeRefreshLayout.setColorSchemeColors(
                ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorPrimary)
            );
            this.swipeRefreshLayout.setSize(SwipeRefreshLayout.LARGE);
            this.messageText = fragmentView.findViewById(R.id.embedded_text_editor);

            this.sendEditMessageButton = this.fragmentView.findViewById(R.id.confirm_edit_button);
            this.sendButton = this.fragmentView.findViewById(R.id.send_button);
            this.attachButton = this.fragmentView.findViewById(R.id.attach_button);
            this.cameraButton = this.fragmentView.findViewById(R.id.camera_button);
            this.cameraButton.setOnClickListener(v -> {
                if (actionMode != null) {
                    actionMode.finish();
                }
                dismissQuotePopup();
                if (!validateSendingPermission()) {
                    return;
                }
                if (ConfigUtils.requestCameraPermissions(activity, this, PERMISSION_REQUEST_ATTACH_CAMERA)) {
                    attachCamera();
                }
            });
            updateCameraButton();

            this.emojiButton = this.fragmentView.findViewById(R.id.emoji_button);
            this.emojiButton.setOnClickListener(v -> {
                if (isGroupChatWhereUserIsNotMemberOf()) {
                    SingleToast.getInstance().showLongText(getString(R.string.you_are_not_a_member_of_this_group));
                } else {
                    showEmojiPicker();
                }
            });

            this.emojiMarkupUtil = EmojiMarkupUtil.getInstance();
            this.wallpaperView = this.fragmentView.findViewById(R.id.wallpaper_view);
            final MaterialButton quickscrollUpView = this.fragmentView.findViewById(R.id.quickscroll_top);
            final MaterialButton quickscrollDownView = this.fragmentView.findViewById(R.id.quickscroll_bottom);
            final FrameLayout quickscrollDownContainer = this.fragmentView.findViewById(R.id.quickscroll_bottom_container);
            this.dateView = this.fragmentView.findViewById(R.id.date_separator_container);
            this.dateTextView = this.fragmentView.findViewById(R.id.text_view);

            this.editMessageBubbleContainer = this.fragmentView.findViewById(R.id.edit_message_bubble_container);
            this.editMessageBubbleComposeView = this.fragmentView.findViewById(R.id.edit_message_bubble_compose_view);
            this.dimBackground = this.fragmentView.findViewById(R.id.dim_background);

            this.bottomPanel = this.fragmentView.findViewById(R.id.bottom_panel);
            this.openPollNoticeView = this.fragmentView.findViewById(R.id.open_polls_layout);
            this.reportSpamView = this.fragmentView.findViewById(R.id.report_spam_layout);
            this.reportSpamView.setListener(this);

            this.availabilityStatusBannerView = this.fragmentView.findViewById(R.id.availability_status_banner_view);

            quickscrollDownContainer.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    quickscrollDownContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);

                    Context context = getContext();
                    if (context != null) {
                        final BadgeDrawable quickscrollDownBadge = BadgeDrawable.createFromResource(context, R.xml.badge_compose);
                        quickscrollDownBadge.setHorizontalOffset(getResources().getDimensionPixelOffset(R.dimen.quickscroll_badge_offset));
                        quickscrollDownBadge.setVerticalOffset(getResources().getDimensionPixelOffset(R.dimen.quickscroll_badge_offset));
                        BadgeUtils.attachBadgeDrawable(quickscrollDownBadge, quickscrollDownView, quickscrollDownContainer);
                        quickscrollDownBadge.setVisible(false);

                        scrollButtonManager = new ScrollButtonManager(quickscrollUpView, quickscrollDownContainer, quickscrollDownBadge);
                    }
                }
            });

            quickscrollDownView.setOnClickListener(v -> {
                removeDateView();
                if (scrollButtonManager != null) {
                    scrollButtonManager.hideAllButtons();
                }
                scrollList(Integer.MAX_VALUE);

            });
            quickscrollUpView.setOnClickListener(v -> {
                removeDateView();
                if (scrollButtonManager != null) {
                    scrollButtonManager.hideAllButtons();
                }
                scrollList(0);
            });

            textInputLayout = fragmentView.findViewById(R.id.textinputlayout_compose);
            conversationParent = fragmentView.findViewById(R.id.conversation_parent);

            this.getValuesFromBundle(savedInstanceState);
            this.handleIntent(activity.getIntent());
            this.setupListeners();
        }

        if (!ConfigUtils.isDefaultEmojiStyle()) {
            // remove emoji button
            this.emojiButton.setVisibility(View.GONE);
            this.messageText.setPadding(getResources().getDimensionPixelSize(R.dimen.no_emoji_button_padding_left), this.messageText.getPaddingTop(), this.messageText.getPaddingRight(), this.messageText.getPaddingBottom());
        } else {
            try {
                final EmojiPicker.EmojiKeyListener emojiKeyListener = new EmojiPicker.EmojiKeyListener() {
                    @Override
                    public void onBackspaceClick() {
                        messageText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
                    }

                    @Override
                    public void onEmojiClick(String emojiCodeString) {
                        RuntimeUtil.runOnUiThread(() -> messageText.addEmoji(emojiCodeString));
                    }

                    @Override
                    public void onShowPicker() {
                        showEmojiPicker();
                    }
                };
                this.emojiPicker = (EmojiPicker) fragmentView.findViewById(R.id.emoji_picker);
                this.emojiPicker.init(activity, ServiceManager.require().getEmojiService(), true);
                this.emojiButton.attach(this.emojiPicker);
                this.emojiPicker.setEmojiKeyListener(emojiKeyListener);
                this.emojiPicker.addEmojiPickerListener(this);
            } catch (Exception e) {
                logger.error("Exception", e);
                finishActivity();
            }
        }

        emojiHintPopupManager.showOrDismissIfNecessary();

        return this.fragmentView;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        viewModel = ViewModelCompat.getViewModel(this, ComposeMessageViewModel.class);
        setObservers();
    }

    private void setObservers() {
        viewModel.getEvents().observe(getViewLifecycleOwner(), this::handleViewModelEvent);
        viewModel.getContactAvailabilityStatus().observe(getViewLifecycleOwner(), this::onContactAvailabilityStatusChanged);
    }

    private void handleViewModelEvent(@NonNull ComposeMessageEvent event) {
        if (event instanceof ComposeMessageEvent.NextRecordsLoaded) {
            onNextRecordsLoadedEvent((ComposeMessageEvent.NextRecordsLoaded) event);
        }
    }

    private void onContactAvailabilityStatusChanged(@NonNull AvailabilityStatus availabilityStatus) {
        if (!BuildConfig.AVAILABILITY_STATUS_ENABLED) {
            return;
        }
        // Toolbar avatar icon
        if (actionBarAvatarView != null) {
            actionBarAvatarView.setAvailabilityStatusBadgeState(
                availabilityStatus instanceof AvailabilityStatus.Set
                    ? (AvailabilityStatus.Set) availabilityStatus
                    : null
            );
        }
        // Banner
        if (availabilityStatusBannerView != null) {
            final @Nullable AvailabilityStatus.Set availabilityStatusSet = availabilityStatus instanceof AvailabilityStatus.Set
                ? (AvailabilityStatus.Set) availabilityStatus
                : null;
            final @Nullable Function0<Unit> onClickOnOverflowListener = availabilityStatusSet != null
                ? () -> onClickViewFullAvailabilityStatus(availabilityStatusSet)
                : null;
            final int bannerVisibility = availabilityStatusSet != null
                ? View.VISIBLE
                : View.GONE;
            availabilityStatusBannerView.setVisibility(bannerVisibility);
            availabilityStatusBannerView.setState(
                availabilityStatusSet,
                preferenceService.getEmojiStyle(),
                onClickOnOverflowListener
            );

            if (availabilityStatusSet != null) {
                availabilityStatusTooltipPopup = AvailabilityStatusTooltipPopupManager.showInConversation(requireActivity(), availabilityStatusBannerView);
                if (availabilityStatusTooltipPopup != null) {
                    emojiHintPopupManager.setSuppressed(true);
                }
            }
        }
    }

    private Unit onClickViewFullAvailabilityStatus(@NonNull AvailabilityStatus.Set availabilityStatusSet) {
        ViewFullAvailabilityStatusBottomSheetDialog
            .newInstance(
                availabilityStatusSet
            )
            .show(
                getParentFragmentManager(),
                "view-full-availability-status-bottom-sheet"
            );
        return Unit.INSTANCE;
    }

    private void onNextRecordsLoadedEvent(@NonNull ComposeMessageEvent.NextRecordsLoaded nextRecodsLoadedEvent) {
        valuesLoaded(nextRecodsLoadedEvent.messageModels);
        if (composeMessageAdapter != null) {
            int numberOfInsertedRecords = insertToList(nextRecodsLoadedEvent.messageModels, false, true, true);
            if (numberOfInsertedRecords > 0) {
                convListView.setSelection(convListView.getSelectedItemPosition() + numberOfInsertedRecords + 1);
            }
        }
        if (swipeRefreshLayout != null) {
            swipeRefreshLayout.setRefreshing(false);
            swipeRefreshLayout.setEnabled(nextRecodsLoadedEvent.hasMoreRecords);
        }
    }

    @AnyThread
    private void initOngoingCallState() {
        ongoingCallNotice = fragmentView.findViewById(R.id.ongoing_call_notice);
        if (ongoingCallNotice != null) {
            if (groupModel != null && groupModel.isMember()) {
                registerGroupCallObserver();
            } else {
                updateOngoingCallNotice();
            }
        }
    }

    @AnyThread
    private void updateOngoingCallNotice() {
        boolean hasRunningOOCall = VoipCallService.isRunning()
            && contactModel != null
            && contactModel.getIdentity().equals(VoipCallService.getOtherPartysIdentity());

        GroupCallDescription chosenCall = getChosenCall();
        boolean hasRunningGroupCall = chosenCall != null;
        boolean hasJoinedGroupCall = hasRunningGroupCall
            && groupCallManager.isJoinedCall(chosenCall);

        if (hasRunningOOCall && hasJoinedGroupCall) {
            logger.warn("Invalid state: joined 1:1 AND group call, not showing call notice");
            updateVoipCallMenuItem(true);
            hideOngoingCallNotice();
        } else if (hasRunningOOCall) {
            showOngoingVoipCallNotice();
        } else if (hasRunningGroupCall) {
            OngoingCallNoticeMode mode = hasJoinedGroupCall
                ? OngoingCallNoticeMode.MODE_GROUP_CALL_JOINED
                : OngoingCallNoticeMode.MODE_GROUP_CALL_RUNNING;
            showOngoingGroupCallNotice(mode, chosenCall);
        } else {
            updateVoipCallMenuItem(true);
            hideOngoingCallNotice();
        }
    }

    @Nullable
    private GroupCallDescription getChosenCall() {
        return ConfigUtils.isGroupCallsEnabled() && groupModel != null && groupCallManager != null
            ? groupCallManager.getCurrentChosenCall(groupModel)
            : null;
    }

    @AnyThread
    private void showOngoingVoipCallNotice() {
        if (ongoingCallNotice != null) {
            ongoingCallNotice.showVoip();
        }
    }

    @AnyThread
    private void hideOngoingVoipCallNotice() {
        if (ongoingCallNotice != null) {
            ongoingCallNotice.hideVoip();
        }
    }

    @AnyThread
    private void hideOngoingCallNotice() {
        if (ongoingCallNotice != null) {
            ongoingCallNotice.hide();
        }
    }

    @AnyThread
    private void registerGroupCallObserver() {
        removeGroupCallObserver();
        if (groupModel != null && groupCallManager != null) {
            groupCallObserver = call -> updateOngoingCallNotice();
            logger.info("Add group call observer for group {}", groupModel.getDatabaseId());
            groupCallManager.addGroupCallObserver(groupModel, groupCallObserver);
        }
    }

    @AnyThread
    private void showOngoingGroupCallNotice(OngoingCallNoticeMode mode, @NonNull GroupCallDescription call) {
        if (ongoingCallNotice != null) {
            ongoingCallNotice.showGroupCall(call, mode);
            updateVoipCallMenuItem(false);
        }
    }

    private boolean isEmojiPickerShown() {
        return emojiPicker != null && emojiPicker.isShown();
    }

    @UiThread
    private void hideEmojiPickerIfShown() {
        if (isEmojiPickerShown()) {
            emojiPicker.hide();
            addAllInsetsToInsetPaddingContainer();
        }
    }

    @UiThread
    private void hideEmojiPopupIfShown() {
        if (emojiReactionsPopup != null && emojiReactionsPopup.isShowing()) {
            emojiReactionsPopup.dismiss();
        }
    }

    private void showEmojiPicker() {
        logger.debug("Emoji button clicked");

        if (activity.isSoftKeyboardOpen() && !isEmojiPickerShown()) {
            if (rootInsetsDeferringCallback != null && keyboardAnimationInsetsCallback != null) {
                rootInsetsDeferringCallback.openingEmojiPicker = true;
                keyboardAnimationInsetsCallback.skipNextAnimation = true;
            }

            logger.debug("Show emoji picker after keyboard close");
            activity.runOnSoftKeyboardClose(() -> {
                if (emojiPicker != null) {
                    emojiPicker.show(activity.loadStoredSoftKeyboardHeight());
                    removeVerticalInsetsFromInsetPaddingContainer();
                }
            });

            messageText.post(() -> EditTextUtil.hideSoftKeyboard(messageText));
        } else {
            if (emojiPicker != null) {
                if (emojiPicker.isShown()) {
                    logger.debug("Emoji picker currently shown. Closing.");
                    if (ConfigUtils.isLandscape(activity) && !ConfigUtils.isTabletLayout()) {
                        emojiPicker.hide();
                        addAllInsetsToInsetPaddingContainer();
                    } else {
                        if (rootInsetsDeferringCallback != null && keyboardAnimationInsetsCallback != null) {
                            rootInsetsDeferringCallback.openingEmojiPicker = true;
                            keyboardAnimationInsetsCallback.skipNextAnimation = true;
                        }
                        activity.openSoftKeyboard(messageText);
                    }
                } else {
                    logger.debug("Show emoji picker immediately");
                    emojiPicker.show(activity.loadStoredSoftKeyboardHeight());
                    removeVerticalInsetsFromInsetPaddingContainer();
                }
            }
        }
    }

    /**
     * If the emoji picker is shown, we have to make sure that no vertical padding insets are applied.
     * The emoji picker has to handle the vertical insets internally.
     * <p>
     * This will remove any vertical padding of {@code inset_padding_container} while still respecting the horizontal insets.
     */
    private void removeVerticalInsetsFromInsetPaddingContainer() {
        final Insets insets = ActivityExtensionsKt.getCurrentInsets(
            activity,
            WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
        );
        final FrameLayout insetPaddingContainer = getView().findViewById(R.id.inset_padding_container);
        insetPaddingContainer.setPadding(insets.left, 0, insets.right, 0);
    }

    private void addAllInsetsToInsetPaddingContainer() {
        final @Nullable View fragmentView = getView();
        if (fragmentView == null) {
            return;
        }
        final Insets insets = ActivityExtensionsKt.getCurrentInsets(
            activity,
            WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
        );
        final @NonNull FrameLayout insetPaddingContainer = fragmentView.findViewById(R.id.inset_padding_container);
        insetPaddingContainer.setPadding(insets.left, 0, insets.right, insets.bottom);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        logger.debug("onActivityCreated");

        super.onActivityCreated(savedInstanceState);
        /*
         * This callback tells the fragment when it is fully associated with the new activity instance.
         * This is called after onCreateView(LayoutInflater, ViewGroup, Bundle) and before onViewStateRestored(Bundle).
         */
        if (preferenceService == null) {
            return;
        }

        final String tag = "compose-message-fragment";

        // Set inset listener that will effectively apply the final view paddings
        rootInsetsDeferringCallback = new RootViewDeferringInsetsCallback(
            tag,
            emojiPicker,
            activity,
            WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout()
        );
        final FrameLayout insetPaddingContainer = getView().findViewById(R.id.inset_padding_container);
        ViewCompat.setWindowInsetsAnimationCallback(insetPaddingContainer, rootInsetsDeferringCallback);
        ViewCompat.setOnApplyWindowInsetsListener(insetPaddingContainer, rootInsetsDeferringCallback);

        // Set inset listener to temporarily push up/down the chat views while an IME animation takes place
        keyboardAnimationInsetsCallback = new TranslateDeferringInsetsAnimationCallback(
            tag,
            conversationParent,
            emojiPicker,
            WindowInsetsCompat.Type.systemBars(),
            WindowInsetsCompat.Type.ime(),
            WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE
        );
        ViewCompat.setWindowInsetsAnimationCallback(conversationParent, keyboardAnimationInsetsCallback);

        activity.addOnSoftKeyboardChangedListener(this);
    }

    @Override
    public void onResume(@NonNull LifecycleOwner owner) {
        if (messageReceiver == null) {
            return;
        }

        this.notificationService.setVisibleConversation(conversationId);

        isPaused = false;

        // mark all unread messages as read
        if (!unreadMessages.isEmpty()) {
            logger.debug("markAllRead");
            new MarkAsReadRoutine(conversationService, messageService, notificationService)
                .runAsync(
                    unreadMessages,
                    messageReceiver,
                    /* onSuccess */ () -> {
                        unreadMessages.clear();
                        return Unit.INSTANCE;
                    }
                );
        }

        // update menus
        updateMuteMenu();
        if (isGroupChat) {
            updateGroupCallMenuItem();
        } else {
            updateBlockMenu();
        }

        // start media players again
        this.messagePlayerService.resumeAll(getActivity(), this.messageReceiver, SOURCE_LIFECYCLE);

        // make sure to remark the active chat
        if (ConfigUtils.isTabletLayout() && conversationId != null) {
            globalEventBuses.getActions().emit(new ActionEvent.ConversationOpened(conversationId));
        }

        // restore scroll position after orientation change
        if (getActivity() != null) {
            Intent intent = getActivity().getIntent();
            if (intent != null && !intent.hasExtra(EXTRA_API_MESSAGE_ID) && !intent.hasExtra(EXTRA_SEARCH_QUERY)) {
                convListView.post(() -> {
                    if (listInstancePosition != AbsListView.INVALID_POSITION &&
                        messageReceiver != null &&
                        messageReceiver.getConversationId().getObfuscated().equals(listInstanceConversationIdObfuscated)) {
                        logger.debug("restoring position {}", listInstancePosition);
                        convListView.setSelectionFromTop(listInstancePosition, listInstanceTop);
                        if (activity != null && convListView.getCheckedItemCount() > 0 && actionMode == null) {
                            SparseBooleanArray itemPositions = convListView.getCheckedItemPositions();
                            for (int i = 0; i < itemPositions.size(); i++) {
                                selectedMessages.add(composeMessageAdapter.getItem(itemPositions.keyAt(i)));
                            }
                            actionMode = activity.startSupportActionMode(new ComposeMessageAction(this.longClickItem));
                        }
                    } else {
                        jumpToFirstUnreadMessage();
                    }
                    // make sure it's not restored twice
                    listInstancePosition = AbsListView.INVALID_POSITION;
                    listInstanceConversationIdObfuscated = null;
                });
            }
        }

        updateOngoingCallNotice();

        viewModel.onResume(messageReceiver);
    }

    @Override
    public void onPause(@NonNull LifecycleOwner owner) {
        logger.info("onPause");
        isPaused = true;

        onEmojiPickerClose();

        if (this.notificationService != null) {
            this.notificationService.setVisibleConversation(null);
        }

        // save unfinished text
        if (editMessageActionMode == null) {
            updateMessageDraft();
        }

        if (this.typingIndicatorTextWatcher != null) {
            this.typingIndicatorTextWatcher.stopSending();
        }

        preserveListInstanceValues();
    }

    @Override
    public void onStop() {
        logger.info("onStop");

        if (this.typingIndicatorTextWatcher != null) {
            this.typingIndicatorTextWatcher.stopSending();
        }

        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
            // close keyboard to prevent layout corruption after unlocking phone
            if (this.messageText != null) {
                EditTextUtil.hideSoftKeyboard(this.messageText);
            }
        }
        super.onStop();
    }

    @Override
    public void onDetach() {
        logger.debug("onDetach");

        hideEmojiPickerIfShown();
        dismissMentionPopup();
        dismissQuotePopup();

        this.activity = null;

        super.onDetach();
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        removeGroupCallObserver();
    }

    @Override
    public void onDestroy() {
        logger.debug("onDestroy");

        try {
            if (scrollButtonManager != null) {
                scrollButtonManager.hideAllButtons();
            }
            emojiHintPopupManager.onDestroy();

            dismissTooltipPopup(workTooltipPopup, true);
            workTooltipPopup = null;
            if (availabilityStatusTooltipPopup != null) {
                availabilityStatusTooltipPopup.dismiss(true);
                availabilityStatusTooltipPopup = null;
            }

            dismissMentionPopup();
            dismissQuotePopup();

            if (this.emojiButton != null) {
                this.emojiButton.detach(this.emojiPicker);
            }

            if (this.emojiPicker != null) {
                this.emojiPicker.removeEmojiPickerListener(this);
            }

            if (!requiredInstances()) {
                super.onDestroy();
                return;
            }

            if (this.messagePlayerService != null) {
                this.messagePlayerService.release();
            }

            if (this.thumbnailCache != null) {
                this.thumbnailCache.flush();
            }

            if (this.messageText != null) {
                //remove typing change listener
                if (this.typingIndicatorTextWatcher != null) {
                    this.messageText.removeTextChangedListener(this.typingIndicatorTextWatcher);
                }
                if (draftUpdateTextWatcher != null) {
                    messageText.removeTextChangedListener(draftUpdateTextWatcher);
                }
                // http://stackoverflow.com/questions/18348049/android-edittext-memory-leak
                this.messageText.setText(null);
            }

            if (soundEffectPlayer != null) {
                soundEffectPlayer.destroy();
            }

            // remove wallpaper
            if (wallpaperView != null) {
                wallpaperView.setImageBitmap(null);
            }

            removeIsTypingFooter();
            this.isTypingView = null;

            //clear all records to remove all references
            if (this.composeMessageAdapter != null) {
                this.composeMessageAdapter.clear();
                this.composeMessageAdapter = null;
            }

            if (draftUpdateTextWatcher != null) {
                draftUpdateTextWatcher.stop();
            }

            releaseMedia3Controller();
        } catch (Exception x) {
            logger.error("Exception", x);
        }

        composeMessageFragmentUtils = null;

        super.onDestroy();
    }

    private void removeDateView() {
        if (dateView != null && dateView.getVisibility() == View.VISIBLE) {
            AnimationUtil.slideOutAnimation(dateView, false, 1f, null);
        }

        if (actionMode != null) {
            actionMode.finish();
        }
    }

    private void setupListeners() {
        setupConversationListScrollListener();
        setupConversationListSwipeListener();
        setupSendButtonClickListener();
        setupAttachButtonClickListener();
        setupMessageTextListeners();
    }

    private void setupConversationListScrollListener() {
        // Setting this scroll listener is required to ensure that during ListView scrolling,
        // we don't look for swipes or pulldowns
        this.convListView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
                if (listViewTouchSwipeListener != null) {
                    listViewTouchSwipeListener.setEnabled(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
                }

                if (!absListView.canScrollList(View.SCROLL_AXIS_VERTICAL)) {
                    if (scrollButtonManager != null) {
                        scrollButtonManager.hideButton(ScrollButtonManager.TYPE_DOWN);
                    }
                }

                if (!absListView.canScrollList(-View.SCROLL_AXIS_VERTICAL)) {
                    if (scrollButtonManager != null) {
                        scrollButtonManager.hideButton(ScrollButtonManager.TYPE_UP);
                    }
                }
                emojiHintPopupManager.setScrolling(
                    scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE
                );
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                if (view != null && view.getChildCount() > 0) {
                    View itemView = view.getChildAt(0);

                    boolean onTop = firstVisibleItem == 0 && itemView.getTop() == listViewTop;
                    swipeRefreshLayout.setEnabled(onTop);

                    if (firstVisibleItem != lastFirstVisibleItem) {
                        if (lastFirstVisibleItem < firstVisibleItem) {
                            // scrolling down
                            if (view.canScrollList(View.SCROLL_AXIS_VERTICAL)) {
                                if (scrollButtonManager != null) {
                                    scrollButtonManager.showButton(ScrollButtonManager.TYPE_DOWN, 0);
                                }
                            } else {
                                if (scrollButtonManager != null) {
                                    scrollButtonManager.hideButton(ScrollButtonManager.TYPE_DOWN);
                                }
                                recentlyAddedCount = 0;
                            }
                        } else {
                            // scrolling up
                            if (view.canScrollList(-View.SCROLL_AXIS_VERTICAL)) {
                                if (scrollButtonManager != null) {
                                    scrollButtonManager.showButton(ScrollButtonManager.TYPE_UP, 0);
                                }
                            } else {
                                if (scrollButtonManager != null) {
                                    scrollButtonManager.hideButton(ScrollButtonManager.TYPE_UP);
                                }
                            }
                        }

                        lastFirstVisibleItem = firstVisibleItem;

                        if (dateView.getVisibility() != View.VISIBLE && composeMessageAdapter != null && composeMessageAdapter.getCount() > 0) {
                            AnimationUtil.slideInAnimation(dateView, false, 200);
                        }

                        dateViewHandler.removeCallbacks(dateViewTask);
                        dateViewHandler.postDelayed(dateViewTask, SCROLLBUTTON_VIEW_TIMEOUT);

                        if (composeMessageAdapter != null) {
                            AbstractMessageModel abstractMessageModel = composeMessageAdapter.getItem(firstVisibleItem);
                            if (abstractMessageModel != null) {
                                final Instant createdAt = abstractMessageModel.getCreatedAt();
                                if (createdAt != null) {
                                    final String text = LocaleUtil.formatDateRelative(createdAt);
                                    dateTextView.setText(text);
                                    dateView.post(() -> dateTextView.setText(text));
                                }
                            }
                        }
                    }
                } else {
                    swipeRefreshLayout.setEnabled(false);
                }
            }
        });
    }

    private void setupConversationListSwipeListener() {
        listViewTouchSwipeListener = new ListViewTouchSwipeListener(
            this.convListView,
            new ListViewTouchSwipeListener.DismissCallbacks() {
                @Override
                public boolean canSwipe(int position) {
                    if (actionMode != null) {
                        return false;
                    }

                    if (messageReceiver == null || !messageReceiver.validateSendingPermission().isValid()) {
                        return false;
                    }

                    int viewType = composeMessageAdapter.getItemViewType(position);

                    if (viewType == ComposeMessageAdapter.TYPE_STATUS ||
                        viewType == ComposeMessageAdapter.TYPE_FIRST_UNREAD ||
                        viewType == ComposeMessageAdapter.TYPE_DATE_SEPARATOR ||
                        viewType == ComposeMessageAdapter.TYPE_GROUP_CALL_STATUS) {
                        return false;
                    }

                    AbstractMessageModel messageModel = composeMessageAdapter.getItem(position);

                    if (messageModel == null) {
                        return false;
                    }

                    return QuoteUtil.isQuoteable(messageModel);
                }

                @Override
                public void onSwiped(int position) {
                    if (composeMessageAdapter == null) {
                        return;
                    }

                    AbstractMessageModel abstractMessageModel = composeMessageAdapter.getItem(position);
                    if (preferenceService.isInAppVibrate()) {
                        if (isAdded() && !isDetached() && activity != null) {
                            Vibrator vibrator = (Vibrator) activity.getSystemService(Context.VIBRATOR_SERVICE);
                            if (vibrator != null && vibrator.hasVibrator()) {
                                vibrator.vibrate(100);
                            }
                        }
                    }
                    if (abstractMessageModel != null) {
                        if (isQuotePopupShown() && abstractMessageModel.equals(quotePopup.getQuoteInfo().getMessageModel())) {
                            dismissQuotePopup();
                        } else {
                            if (searchActionMode != null) {
                                searchActionMode.finish();
                            }
                            logger.info("Message swiped, showing quote popup");
                            showQuotePopup(abstractMessageModel, true);
                        }
                    }
                }
            }
        );
    }

    private void setupSendButtonClickListener() {
        if (sendButton != null) {
            sendButton.setOnClickListener(new DebouncedOnClickListener(500) {
                @Override
                public void onDebouncedClick(View v) {
                    logger.info("Send button clicked");
                    sendMessage();
                }
            });
        }
    }

    private void setupEditMessageButtonClickListener(@NonNull AbstractMessageModel messageModel) {
        if (sendEditMessageButton != null) {
            sendEditMessageButton.setOnClickListener(new DebouncedOnClickListener(500) {
                @Override
                public void onDebouncedClick(View v) {
                    logger.info("Send edit button clicked");
                    final @Nullable Editable messageTextEditable = messageText.getText();
                    if (messageTextEditable != null) {
                        onSendEditMessage(messageModel, messageTextEditable.toString());
                    }
                }
            });
        }
    }

    private void setupAttachButtonClickListener() {
        if (attachButton != null) {
            attachButton.setOnClickListener(new DebouncedOnClickListener(1000) {
                @Override
                public void onDebouncedClick(View v) {
                    if (validateSendingPermission()) {
                        logger.info("Attach media button clicked");
                        if (actionMode != null) {
                            actionMode.finish();
                        }

                        dismissQuotePopup();

                        Intent intent = new Intent(activity, MediaAttachActivity.class);
                        IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
                        if (ComposeMessageFragment.this.lastMediaFilter != null) {
                            intent = IntentDataUtil.addLastMediaFilterToIntent(intent, ComposeMessageFragment.this.lastMediaFilter);
                        }
                        startActivityForResult(intent, ThreemaActivity.ACTIVITY_ID_ATTACH_MEDIA);
                        activity.overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
                    }
                }
            });
        }
    }

    private void setupSendMessageTextActionListener() {
        this.messageText.setOnEditorActionListener(
            setupTextActionListener(this::sendMessage)
        );
    }

    @NonNull
    private TextView.OnEditorActionListener setupTextActionListener(@NonNull Runnable onAction) {
        return (view, actionId, event) -> {
            if ((actionId == EditorInfo.IME_ACTION_SEND) ||
                (event != null && event.getAction() == KeyEvent.ACTION_DOWN && event.getKeyCode() == KeyEvent.KEYCODE_ENTER && preferenceService.isEnterToSend())) {
                logger.info("Enter key pressed to send message");
                onAction.run();
                return true;
            }
            return false;
        };
    }

    private void setupMessageTextListeners() {
        setupSendMessageTextActionListener();

        setupMessageTextClickListener();

        this.messageText.addTextChangedListener(new SimpleTextWatcher() {
            @Override
            public void onTextChanged(@NonNull CharSequence text, int start, int before, int count) {
                ActivityService.activityUserInteract(activity);
                updateSendButton(text);
            }

            @Override
            public void afterTextChanged(@NonNull Editable editable) {
                updateCameraButton();
            }
        });
    }

    /**
     * Setup listener for text input field.
     * <p>
     * If this is a group chat and the user is not an active member of the group, we prevent any input.
     * <p>
     * If default emojis are enabled and the device is in portrait mode,
     * we will handle activation. Otherwise, leave it to the system
     */
    @UiThread
    private void setupMessageTextClickListener() {
        if (this.messageText == null) {
            return;
        }

        final boolean canUserComposeMessage = !isGroupChatWhereUserIsNotMemberOf();
        this.messageText.setFocusable(canUserComposeMessage);
        this.messageText.setFocusableInTouchMode(canUserComposeMessage);

        if (!canUserComposeMessage) {
            this.messageText.setOnClickListener(v -> {
                SingleToast.getInstance().showLongText(getString(R.string.you_are_not_a_member_of_this_group));
            });
        } else if (ConfigUtils.isDefaultEmojiStyle() && getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
            this.messageText.setOnClickListener(v -> {
                if (isEmojiPickerShown()) {
                    if (ConfigUtils.isLandscape(activity) && !ConfigUtils.isTabletLayout()) {
                        emojiPicker.hide();
                        addAllInsetsToInsetPaddingContainer();
                    } else {
                        if (rootInsetsDeferringCallback != null && keyboardAnimationInsetsCallback != null) {
                            rootInsetsDeferringCallback.openingEmojiPicker = true;
                            keyboardAnimationInsetsCallback.skipNextAnimation = true;
                        }
                    }
                }
                activity.openSoftKeyboard(messageText);
            });
        } else {
            this.messageText.setOnClickListener(null);
        }
    }

    private void updateCameraButton() {
        if (cameraButton == null || attachButton == null || messageText == null || editMessageActionMode != null) {
            return;
        }

        boolean isCameraPermissionGranted = true;

        if (ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {

            // shouldShowRequestPermissionRationale returns false if
            // a) the user selected "never ask again"; or
            // b) a permission dialog has never been shown
            // we hide the camera button only in case a)
            if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) && preferenceService.getCameraPermissionRequestShown()) {
                cameraButton.setVisibility(View.GONE);
                isCameraPermissionGranted = false;
            }
        }

        final int attachButtonVisibility = isQuotePopupShown() ?
            View.GONE : View.VISIBLE;

        final int cameraButtonVisibility =
            (messageText.getText() == null ||
                messageText.getText().length() == 0) &&
                !isQuotePopupShown() &&
                isCameraPermissionGranted ?
                View.VISIBLE : View.GONE;

        final boolean attachButtonVisibilityChange = attachButton.getVisibility() != attachButtonVisibility;
        final boolean cameraButtonVisibilityChange = cameraButton.getVisibility() != cameraButtonVisibility;

        if (cameraButtonVisibilityChange) {
            Transition cameraTransition = new Slide(Gravity.RIGHT);
            cameraTransition.setStartDelay(cameraButtonVisibility == View.VISIBLE && attachButtonVisibilityChange ? 100 : 0);
            cameraTransition.setDuration(120);
            cameraTransition.setInterpolator(new LinearInterpolator());
            cameraTransition.addTarget(R.id.camera_button);
            TransitionManager.beginDelayedTransition((ViewGroup) cameraButton.getParent(), cameraTransition);
            cameraButton.setVisibility(cameraButtonVisibility);
        }

        if (attachButtonVisibilityChange) {
            Transition attachTransition = new Slide(Gravity.RIGHT);
            attachTransition.setStartDelay(attachButtonVisibility == View.VISIBLE ? 0 : 100);
            attachTransition.setDuration(120);
            attachTransition.setInterpolator(new LinearInterpolator());
            attachTransition.addTarget(R.id.attach_button);
            TransitionManager.beginDelayedTransition((ViewGroup) attachButton.getParent(), attachTransition);
            attachButton.setVisibility(attachButtonVisibility);
        }

        messageText.postDelayed(() -> fixMessageTextPadding(cameraButtonVisibility, attachButtonVisibility), 50);
    }

    private void fixMessageTextPadding(int cameraButtonVisibility, int attachButtonVisibility) {
        if (isAdded()) {
            int marginRight = ThreemaApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.emoji_and_photo_button_width);

            if (cameraButtonVisibility != View.VISIBLE) {
                marginRight -= getResources().getDimensionPixelSize(R.dimen.emoji_button_width);
            }

            if (attachButtonVisibility != View.VISIBLE) {
                marginRight -= getResources().getDimensionPixelSize(R.dimen.emoji_button_width);
            }

            marginRight = Math.max(marginRight, getResources().getDimensionPixelSize(R.dimen.no_emoji_button_padding_left));

            messageText.setPadding(messageText.getPaddingLeft(), messageText.getPaddingTop(), marginRight, messageText.getPaddingBottom());
        }
    }

    private void updateSendButton(CharSequence text) {
        if (isQuotePopupShown()) {
            if (isNullOrBlank(text)) {
                sendButton.setEnabled(false);
            } else {
                sendButton.setSend();
                sendButton.setEnabled(true);
            }
        } else {
            if (isNullOrBlank(text)) {
                sendButton.setRecord();
                sendButton.setEnabled(true);
            } else {
                sendButton.setSend();
                sendButton.setEnabled(true);
            }
        }
        if (emojiButton != null) {
            emojiButton.setVisibility(ConfigUtils.isDefaultEmojiStyle() ? View.VISIBLE : View.GONE);
        }
        if (messageText != null) {
            messageText.setVisibility(View.VISIBLE);
        }
    }

    private void updateSendEditMessageButton(@Nullable String oldMessageText, @Nullable String newMessageText) {
        if (canSendEditMessage(oldMessageText, newMessageText)) {
            sendEditMessageButton.setEnabled(true);
            sendEditMessageButton.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.ic_circle_send));
            sendEditMessageButton.setColorFilter(ConfigUtils.getColorFromAttribute(requireContext(), R.attr.colorOnPrimary));
        } else {
            sendEditMessageButton.setEnabled(false);
            sendEditMessageButton.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.ic_circle_send_disabled));
            sendEditMessageButton.setColorFilter(ConfigUtils.getColorFromAttribute(requireContext(), R.attr.colorOnSurfaceVariant));
        }
    }

    /**
     * Check if the value of {@code edit} would be a valid change compared to the current value of {@code original}.
     *
     * @param original The body <b>or</b> caption of the original message before the edit. Can be {@code null} in case of a file message without a
     *                 caption
     * @param edited   The potentially changed body <b>or</b> caption text. Values of {@code null} or blank are never valid because even for a file
     *                 message we currently do not support removing a caption completely.
     * @see EditMessageActionMode#getEditableText
     */
    private boolean canSendEditMessage(@Nullable String original, @Nullable String edited) {
        return edited != null && !edited.isBlank() && !edited.equals(original);
    }

    private void setBackgroundWallpaper() {
        if (this.conversationId != null && isAdded() && this.wallpaperView != null) {
            wallpaperService.setupWallpaperBitmap(
                this.conversationId,
                this.wallpaperView,
                ConfigUtils.isLandscape(activity),
                ConfigUtils.isTheDarkSide(activity)
            );
        }
    }

    private void resetDefaultValues() {
        removeGroupCallObserver();

        this.distributionListId = 0L;
        this.groupDatabaseId = 0L;
        this.identity = null;

        this.groupModel = null;
        this.distributionListModel = null;
        this.contactModel = null;

        this.messageReceiver = null;
        composeMessageFragmentUtils = null;
        this.listInstancePosition = AbsListView.INVALID_POSITION;
        this.listInstanceConversationIdObfuscated = null;

        if (ConfigUtils.isTabletLayout()) {
            dismissQuotePopup();
        }

        // remove message detail dialog if still open
        DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_MESSAGE_DETAIL, true);
    }

    private void removeGroupCallObserver() {
        if (groupModel != null && groupCallObserver != null && groupCallManager != null) {
            logger.info("Remove group call observer for group {}", groupModel.getDatabaseId());
            groupCallManager.removeGroupCallObserver(groupModel, groupCallObserver);
            groupCallObserver = null;
        }
    }

    private void getValuesFromBundle(Bundle bundle) {
        if (bundle != null) {
            this.conversationId = bundle.getParcelable(AppConstants.INTENT_DATA_CONVERSATION_ID);
            this.cameraUri = bundle.getParcelable(CAMERA_URI);
            this.listInstancePosition = bundle.getInt(BUNDLE_LIST_POSITION);
            final @Nullable String bundleListInstanceConversationIdObfuscated = bundle.getString(BUNDLE_LIST_CONVERSATION_ID_OBFUSCATED);
            this.listInstanceConversationIdObfuscated = bundleListInstanceConversationIdObfuscated != null
                ? new ConversationIdObfuscated(bundleListInstanceConversationIdObfuscated)
                : null;
            this.listInstanceTop = bundle.getInt(BUNDLE_LIST_TOP);
            this.longClickItem = bundle.getInt(BUNDLE_LIST_LONG_CLICK_ITEM);
        }
    }

    public void onNewIntent(Intent intent) {
        logger.debug("onNewIntent");

        if (!requiredInstances()) {
            return;
        }

        if (this.messagePlayerService != null) {
            this.messagePlayerService.stopAll();
            this.messagePlayerService.release();
        }

        MediaController mediaController = getMedia3Controller();
        if (mediaController != null) {
            mediaController.stop();
            mediaController.clearMediaItems();
        }

        resetDefaultValues();

        this.dismissQuotePopup();

        handleIntent(intent);

        // initialize various toolbar items
        if (actionMode != null) {
            actionMode.finish();
        }
        if (searchActionMode != null) {
            searchActionMode.finish();
        }
        if (editMessageActionMode != null) {
            editMessageActionMode.finish();
        }
        this.updateToolbarTitle();
        this.updateMenus();
    }

    private void setupToolbar() {
        View actionBarTitleView = layoutInflater.inflate(R.layout.actionbar_compose_title, null);

        if (actionBarTitleView != null) {
            this.actionBarTitleTextView = actionBarTitleView.findViewById(R.id.title);
            this.actionBarSubtitleImageView = actionBarTitleView.findViewById(R.id.subtitle_image);
            this.actionBarSubtitleTextView = actionBarTitleView.findViewById(R.id.subtitle_text);
            this.actionBarAvatarView = actionBarTitleView.findViewById(R.id.avatar_view);
            final RelativeLayout actionBarTitleContainer = actionBarTitleView.findViewById(R.id.title_container);
            actionBarTitleContainer.setOnClickListener(v -> {
                @Nullable Intent intent = null;
                if (this.conversationId instanceof ContactConversationId) {
                    logger.info("Clicked title of contact conversation");
                    intent = ContactDetailActivity.createIntent(
                        activity,
                        ((ContactConversationId) conversationId).identity,
                        true
                    );
                } else if (this.conversationId instanceof GroupConversationId) {
                    logger.info("Clicked title of group conversation");
                    intent = GroupDetailActivity.createIntent(activity, ((GroupConversationId) conversationId).groupDatabaseId);
                } else if (this.conversationId instanceof DistributionListConversationId) {
                    logger.info("Clicked title of distribution list");
                    intent = DistributionListAddActivity.createIntent(
                        activity,
                        ((DistributionListConversationId) conversationId).distributionListId
                    );
                }
                if (intent != null) {
                    activity.startActivity(intent);
                }
            });

            if (BuildConfig.AVAILABILITY_STATUS_ENABLED && messageReceiver != null && messageReceiver instanceof ContactMessageReceiver) {
                final @Nullable ch.threema.data.models.ContactModel contactModel = ((ContactMessageReceiver) messageReceiver).getContactModel();
                final @Nullable ContactModelData contactModelData = contactModel != null ? contactModel.getData() : null;
                final @Nullable AvailabilityStatus availabilityStatus = contactModelData != null ? contactModelData.availabilityStatus : null;
                if (availabilityStatus != null && actionBarAvatarView != null) {
                    actionBarAvatarView.setAvailabilityStatusBadgeState(
                        availabilityStatus instanceof AvailabilityStatus.Set
                            ? (AvailabilityStatus.Set) availabilityStatus
                            : null
                    );
                }
            }

            if (contactModel != null) {
                if (contactModel.getIdentityType() == IdentityType.WORK) {
                    if (!ConfigUtils.isWorkBuild()) {
                        if (!preferenceService.getIsWorkHintTooltipShown()) {
                            actionBarTitleTextView.postDelayed(() -> {
                                if (getActivity() != null && isAdded()) {
                                    dismissTooltipPopup(workTooltipPopup, true);

                                    int[] location = new int[2];
                                    actionBarAvatarView.getLocationOnScreen(location);
                                    location[0] += actionBarAvatarView.getWidth() / 2;
                                    location[1] += actionBarAvatarView.getHeight();

                                    workTooltipPopup = new TooltipPopup(getActivity(), R.string.preferences__tooltip_work_hint_shown, this, R.drawable.ic_badge_work_24dp);
                                    workTooltipPopup.setListener(new TooltipPopup.TooltipPopupListener() {
                                        @Override
                                        public void onClicked(@NonNull TooltipPopup tooltipPopup) {
                                            logger.info("Clicked Threema Work tooltip");
                                            startActivity(WorkExplainActivity.createIntent(requireContext()));
                                        }
                                    });
                                    workTooltipPopup.show(getActivity(), actionBarAvatarView, null, getString(R.string.tooltip_work_hint), TooltipPopup.Alignment.BELOW_ANCHOR_ARROW_LEFT, location, 4000);
                                }
                            }, 1000);
                        }
                    }
                }
            } else if (groupModel != null) {
                if (ConfigUtils.isGroupCallsEnabled()) {
                    showTooltip();
                }
            }
        }

        if (activity == null) {
            activity = (ComposeMessageActivity) getActivity();
        }

        if (activity != null) {
            this.actionBar = activity.getSupportActionBar();
            if (actionBar != null) {
                actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM | ActionBar.DISPLAY_HOME_AS_UP);
                actionBar.setCustomView(actionBarTitleView);
            }
        }
    }

    @UiThread
    public void showTooltip() {
        if (activity == null) {
            return;
        }

        if (!preferenceService.getIsGroupCallsTooltipShown()) {
            Toolbar toolbar = activity.getToolbar();
            if (toolbar != null) {
                toolbar.postDelayed(() -> {
                    if (activity == null || !activity.hasWindowFocus() || !isGroupChat) {
                        return;
                    }
                    final View itemView = toolbar.findViewById(R.id.menu_threema_call);
                    final @ColorInt int textColor = ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorOnPrimary);

                    final ViewGroup contentView = activity.findViewById(R.id.main_content);

                    try {
                        TapTargetViewUtil.showFor(activity,
                            TapTarget.forView(itemView, getString(R.string.group_calls_tooltip_title), getString(R.string.group_calls_tooltip_text))
                                .outerCircleColorInt(ConfigUtils.getColorFromAttribute(getContext(), R.attr.colorPrimary))      // Specify a color for the outer circle
                                .outerCircleAlpha(0.96f)            // Specify the alpha amount for the outer circle
                                .targetCircleColor(android.R.color.white)   // Specify a color for the target circle
                                .titleTextSize(24)                  // Specify the size (in sp) of the title text
                                .titleTextColorInt(textColor)      // Specify the color of the title text
                                .descriptionTextSize(18)            // Specify the size (in sp) of the description text
                                .descriptionTextColorInt(textColor)  // Specify the color of the description text
                                .textColorInt(textColor)            // Specify a color for both the title and description text
                                .textTypeface(Typeface.SANS_SERIF)  // Specify a typeface for the text
                                .dimColor(android.R.color.black)            // If set, will dim behind the view with 30% opacity of the given color
                                .drawShadow(true)                   // Whether to draw a drop shadow or not
                                .cancelable(true)                  // Whether tapping outside the outer circle dismisses the view
                                .tintTarget(true)                   // Whether to tint the target view's color
                                .transparentTarget(false)           // Specify whether the target is transparent (displays the content underneath)
                                .targetRadius(50),                  // Specify the target radius (in dp)
                            new TapTargetView.Listener() {          // The listener can listen for regular clicks, long clicks or cancels
                                @Override
                                public void onTargetClick(TapTargetView view) {
                                    super.onTargetClick(view);
                                    itemView.performClick();
                                }
                            },
                            contentView);
                        preferenceService.setGroupCallsTooltipShown(true);
                    } catch (Exception ignore) {
                        // catch null typeface exception on CROSSCALL Action-X3
                    }
                }, 2000);
            }
        }
    }

    @UiThread
    private void handleIntent(Intent intent) {
        logger.debug("handleIntent");
        this.isGroupChat = false;
        this.isDistributionListChat = false;
        setCurrentPageReferenceId(null);
        this.reportSpamView.hide();

        //remove old indicator every time!
        //fix ANDR-432
        if (messageText != null) {
            if (typingIndicatorTextWatcher != null) {
                messageText.removeTextChangedListener(this.typingIndicatorTextWatcher);
            }
            if (draftUpdateTextWatcher != null) {
                messageText.removeTextChangedListener(draftUpdateTextWatcher);
            }
        }

        this.conversationId = ComposeMessageActivity.extractConversationId(intent);

        if (conversationId instanceof GroupConversationId || this.groupDatabaseId != 0) {
            this.isGroupChat = true;
            if (this.groupDatabaseId == 0) {
                this.groupDatabaseId = ((GroupConversationId) conversationId).groupDatabaseId;
            }
            this.groupModel = groupModelRepository.getByGroupDatabaseId(this.groupDatabaseId);

            if (this.groupModel == null || this.groupModel.isDeleted()) {
                logger.error("Group not found");
                showToast(requireContext(), R.string.group_not_found);
                finishActivity();
                return;
            }

            this.messageReceiver = this.groupService.createReceiver(this.groupModel);

            this.messageText.enableMentionPopup(
                requireActivity(),
                groupService,
                this.contactService,
                this.userService,
                this.preferenceService,
                groupModel,
                textInputLayout
            );
        } else if (conversationId instanceof DistributionListConversationId || this.distributionListId != 0) {
            this.isDistributionListChat = true;

            try {
                if (this.distributionListId == 0) {
                    this.distributionListId = ((DistributionListConversationId) conversationId).distributionListId;
                }
                this.distributionListModel = distributionListService.getById(this.distributionListId);

                if (this.distributionListModel == null) {
                    logger.error("Invalid distribution list");
                    finishActivity();
                    return;
                }

                this.messageReceiver = distributionListService.createReceiver(this.distributionListModel);
            } catch (Exception e) {
                logger.error("Exception", e);
                return;
            }
        } else if (conversationId instanceof ContactConversationId) {
            if (isNullOrEmpty(this.identity)) {
                this.identity = ((ContactConversationId) conversationId).identity;
            }

            if (this.identity == null && intent.getData() != null) {
                if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                    this.identity = ContactUtil.getIdentityFromViewIntent(activity, intent);
                } else {
                    LongToast.makeText(activity, R.string.permission_contacts_required, Toast.LENGTH_LONG).show();
                }
            }

            if (this.identity == null || this.identity.isEmpty() || this.identity.equals(this.userService.getIdentity())) {
                logger.error("no identity found");
                finishActivity();
                return;
            }

            this.contactModel = this.contactService.getByIdentity(this.identity);
            if (this.contactModel == null) {
                Toast.makeText(getContext(), getString(R.string.contact_not_found) + ": " + this.identity, Toast.LENGTH_LONG).show();
                Intent homeIntent = HomeActivity.createIntent(activity);
                homeIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                startActivity(homeIntent);
                activity.overridePendingTransition(0, 0);
                finishActivity();
                return;
            }
            this.messageReceiver = this.contactService.createReceiver(this.contactModel);
            this.typingIndicatorTextWatcher = new TypingIndicatorTextWatcher(
                isTyping -> {
                    contactService.sendTypingIndicator(identity, isTyping);
                    return Unit.INSTANCE;
                },
                this
            );
        }

        initOngoingCallState();

        if (this.messageReceiver == null) {
            logger.error("Invalid receiver");
            finishActivity();
            return;
        }

        composeMessageFragmentUtils = KoinJavaComponent.get(
            ComposeMessageFragmentUtils.class,
            null,
            () -> new ParametersHolder(List.of(ComposeMessageFragment.this, messageReceiver, isGroupChat), null)
        );

        // hide chat from view and prevent screenshots - may not work on some devices
        if (this.conversationCategoryService.isMarkedAsPrivate(this.messageReceiver.getConversationId())) {
            try {
                activity.getWindow().addFlags(FLAG_SECURE);
            } catch (Exception ignored) {
            }
        }

        // set wallpaper based on message receiver
        this.setBackgroundWallpaper();

        // report shortcut as used
        if (preferenceService.isDirectShare()) {
            var appContext = activity.getApplicationContext();
            var shortcutId = messageReceiver.getConversationId().getObfuscated().value;
            RuntimeUtil.runOnWorkerThread(() -> {
                try {
                    ShortcutManagerCompat.reportShortcutUsed(appContext, shortcutId);
                } catch (IllegalStateException e) {
                    logger.warn("Failed to report shortcut use", e);
                }
            });
        }

        this.initConversationList(intent.hasExtra(EXTRA_API_MESSAGE_ID) && intent.hasExtra(EXTRA_SEARCH_QUERY) ? () -> {
            String apiMessageId = intent.getStringExtra(EXTRA_API_MESSAGE_ID);
            String searchQuery = intent.getStringExtra(EXTRA_SEARCH_QUERY);

            AbstractMessageModel targetMessageModel = messageService.getMessageModelByApiMessageIdAndReceiver(apiMessageId, messageReceiver);

            if (targetMessageModel != null && !isNullOrEmpty(apiMessageId) && !isNullOrEmpty(searchQuery)) {
                String identity;
                if (targetMessageModel instanceof GroupMessageModel) {
                    identity = targetMessageModel.isOutbox() ? userService.getIdentity() : targetMessageModel.getIdentity();
                } else {
                    identity = targetMessageModel.getIdentity();
                }

                if (identity == null) {
                    logger.error("Identity is null");
                } else {
                    QuoteUtil.QuoteContent quoteContent = QuoteUtil.QuoteContent.createV2(
                        identity,
                        searchQuery,
                        searchQuery,
                        apiMessageId,
                        targetMessageModel,
                        messageReceiver,
                        null,
                        null
                    );

                    if (composeMessageAdapter != null) {
                        ComposeMessageAdapter.ConversationListFilter filter = (ComposeMessageAdapter.ConversationListFilter) composeMessageAdapter.getQuoteFilter(quoteContent);
                        searchV2Quote(apiMessageId, filter);

                        intent.removeExtra(EXTRA_API_MESSAGE_ID);
                    }
                }
            } else {
                Toast.makeText(ThreemaApplication.getAppContext(), R.string.message_not_found, Toast.LENGTH_SHORT).show();
            }
        } : () -> {
            if (isPossibleSpamContact(contactModel)) {
                reportSpamView.show(contactModel, preferenceService.getContactNameFormat());
            }
        });

        this.messageText.setText("");
        this.messageText.setMessageReceiver(this.messageReceiver);
        this.openPollNoticeView.setMessageReceiver(this.messageReceiver);
        this.openPollNoticeView.setOnCloseClickedListener(() -> {
            toggleOpenPollNoticeViewVisibility();
            getActivity().invalidateOptionsMenu();
        });


        // restore draft before setting predefined text
        restoreMessageDraft(false);

        String defaultText = intent.getStringExtra(AppConstants.INTENT_DATA_TEXT);
        if (!isNullOrEmpty(defaultText)) {
            this.messageText.setText(null);
            this.messageText.append(defaultText);
        }

        setupMessageTextClickListener();
        updateSendButton(this.messageText.getText());
        updateCameraButton();

        boolean editFocus = intent.getBooleanExtra(AppConstants.INTENT_DATA_EDITFOCUS, false);
        if (editFocus || this.unreadCount <= 0) {
            messageText.setSelected(true);
            messageText.requestFocus();
        }

        this.notificationService.setVisibleConversation(conversationId);

        if (!this.isGroupChat && !this.isDistributionListChat) {
            this.messageText.addTextChangedListener(this.typingIndicatorTextWatcher);
        }

        draftUpdateTextWatcher = new DraftUpdateTextWatcher(
            draftManager,
            messageReceiver.getConversationId(),
            () -> {
                var text = messageText.getText();
                return text != null ? text.toString() : null;
            },
            () -> {
                if (!isQuotePopupShown()) {
                    return null;
                }
                var quoteMessageModel = quotePopup.getQuoteInfo().getMessageModel();
                return quoteMessageModel != null
                    ? quoteMessageModel.getMessageId()
                    : null;
            }
        );
        messageText.addTextChangedListener(draftUpdateTextWatcher);

        if (ConfigUtils.isTabletLayout() && conversationId != null) {
            globalEventBuses.getActions().emit(new ActionEvent.ConversationOpened(conversationId));
        }
    }

    private boolean validateSendingPermission() {
        if (this.messageReceiver == null) {
            return false;
        }
        @NonNull final SendingPermissionValidationResult validationResult = this.messageReceiver.validateSendingPermission();
        if (validationResult.isDenied()) {
            @Nullable Integer errorResId = ((SendingPermissionValidationResult.Denied) validationResult).getErrorResId();
            if (errorResId != null) {
                RuntimeUtil.runOnUiThread(() -> SingleToast.getInstance().showLongText(getString(errorResId)));
            }
            return false;
        }
        return true;
    }

    private void showDeleteMessagesLocallyDialog() {
        if (selectedMessages.isEmpty()) {
            if (actionMode != null) {
                actionMode.finish();
            }
            return;
        }

        List<AbstractMessageModel> deletableMessages = new ArrayList<>(selectedMessages);

        selectedMessages.clear();
        if (actionMode != null) {
            actionMode.finish();
        }

        logger.info("Showing local deletion dialog for {} message(s) ", selectedMessages.size());
        GenericAlertDialog dialog = GenericAlertDialog.newInstance(
            null,
            getResources().getQuantityString(R.plurals.delete_messages, deletableMessages.size(), deletableMessages.size()),
            R.string.delete_from_this_device,
            R.string.cancel
        );
        dialog.setCallback((tag, data) -> {
            logger.info("Deletion of local messages confirmed");
            deleteMessages(deletableMessages);
        });
        dialog.show(getChildFragmentManager(), DIALOG_TAG_CONFIRM_MESSAGE_DELETE);
    }

    private void showDeleteMessagesForAllDialog(@NonNull AbstractMessageModel message) {
        if (actionMode != null) {
            actionMode.finish();
        }
        GenericAlertDialog dialog = GenericAlertDialog.newInstance(
            null,
            getResources().getQuantityString(R.plurals.delete_messages, 1, 1),
            R.string.delete_for_all,
            R.string.delete_from_this_device,
            R.string.cancel
        );
        dialog.setCallback(new GenericAlertDialog.DialogClickListener() {
            @Override
            public void onYes(@Nullable String tag, @Nullable Object data) {
                logger.info("Deletion of message for everyone confirmed");
                onConfirmDeleteMessageForAll(message);
            }

            @Override
            public void onNo(@Nullable String tag, @Nullable Object data) {
                logger.info("Deletion of message from device confirmed");
                deleteMessages(List.of(message));
            }
        });

        logger.info("Showing deletion dialog");
        dialog.show(getChildFragmentManager(), DIALOG_TAG_CONFIRM_MESSAGE_DELETE);
    }

    private void onConfirmDeleteMessageForAll(@NonNull AbstractMessageModel message) {
        if (messageReceiver == null) {
            return;
        }

        if (messageReceiver instanceof ContactMessageReceiver) {
            ContactMessageReceiver receiver = (ContactMessageReceiver) messageReceiver;
            deleteContactMessageForAll(receiver, message);
        } else if (messageReceiver instanceof GroupMessageReceiver && groupModel != null && groupModel.isMember()) {
            deleteGroupMessageForAll(message, groupModel);
        } else {
            logger.warn("Cannot delete message for receiver of type {}", messageReceiver.getClass().getName());
        }
    }

    private void deleteContactMessageForAll(@NonNull ContactMessageReceiver receiver, @NonNull AbstractMessageModel message) {
        if (ThreemaFeature.canDeleteMessages(receiver.getContact().getFeatureMask())) {
            sendDeleteMessage(message);
        } else {
            logger.warn("Tried to delete a message for a contact that does not support it");
        }
    }

    private void deleteGroupMessageForAll(
        @NonNull AbstractMessageModel message,
        @NonNull GroupModel groupModel
    ) {
        final @Nullable GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            logger.warn("Cannot delete message for all in a group where the data is null");
            return;
        }
        // If this is a notes group, the feature-support checks can be skipped
        if (Boolean.TRUE.equals(groupModel.isNotesGroup())) {
            sendDeleteMessage(message);
            return;
        }
        GroupFeatureSupport featureSupport = groupService.getFeatureSupport(groupModelData, ThreemaFeature.DELETE_MESSAGES);
        if (featureSupport.getAdoptionRate() == GroupFeatureAdoptionRate.ALL) {
            sendDeleteMessage(message);
        } else if (featureSupport.getAdoptionRate() == GroupFeatureAdoptionRate.PARTIAL) {
            List<ContactModel> membersWithoutFeatureSupport = featureSupport.getContactsWithoutFeatureSupport();
            GenericAlertDialog dialog = GenericAlertDialog.newInstance(
                R.string.warning,
                getString(
                    R.string.delete_message_not_supported_for_all_group_members,
                    ContactUtil.joinDisplayNames(getContext(), membersWithoutFeatureSupport, preferenceService.getContactNameFormat())
                ),
                R.string.ok,
                R.string.cancel
            );
            dialog.setCallback((tag, data) -> sendDeleteMessage(message));
            dialog.show(getChildFragmentManager(), DIALOG_TAG_DELETE_MESSAGES_UNSUPPORTED_WARNING);
        } else {
            logger.warn("Tried to delete a message for a group where none of the members support it");
        }
    }

    private void sendDeleteMessage(@NonNull AbstractMessageModel message) {
        if (messageReceiver == null) {
            return;
        }

        try {
            messageService.sendDeleteMessage(message, messageReceiver);
        } catch (Exception e) {
            logger.error("sendDeleteMessage failed", e);
        }
    }

    /**
     * Check if the clues indicate that the sender of this chat might be a spammer
     *
     * @param contactModel Contact model of possible spammer
     * @return true if the contact could be a spammer, false otherwise
     */
    private boolean isPossibleSpamContact(@Nullable ContactModel contactModel) {
        if (contactModel == null || composeMessageAdapter == null) {
            return false;
        }

        // No spam reporting in on-prem build
        if (ConfigUtils.isOnPremBuild()) {
            return false;
        }

        // Exclude verified contacts
        if (contactModel.verificationLevel != VerificationLevel.UNVERIFIED) {
            return false;
        }

        // Exclude contacts where the name is set
        if (!isNullOrEmpty(contactModel.getFirstName()) || !isNullOrEmpty(contactModel.getLastName())) {
            return false;
        }

        // Exclude blocked contacts
        if (blockedIdentitiesService.isBlocked(contactModel.getIdentity())) {
            return false;
        }

        // Exclude group or deleted contacts
        if (contactModel.getAcquaintanceLevel() == AcquaintanceLevel.GROUP_OR_DELETED) {
            return false;
        }

        // Exclude predefined contacts
        if (PredefinedContact.isPredefinedContact(contactModel.getIdentity())) {
            return false;
        }

        int numMessages = composeMessageAdapter.getCount();
        if (numMessages >= MESSAGE_PAGE_SIZE || numMessages < 2) {
            return false;
        }

        AbstractMessageModel firstMessageModel;
        int positionOfFirstIncomingMessage = 0;
        for (int i = 0; i < numMessages; i++) {
            firstMessageModel = composeMessageAdapter.getItem(i);
            if (firstMessageModel == null) {
                return false;
            }
            if (firstMessageModel.isOutbox()) {
                return false;
            }
            if (contactModel.getIdentity().equals(firstMessageModel.getIdentity())) {
                positionOfFirstIncomingMessage = i;
                break;
            }
        }

        AbstractMessageModel messageModel = composeMessageAdapter.getItem(positionOfFirstIncomingMessage);

        if (messageModel == null) {
            return false;
        }

        Instant contactCreated = contactModel.getDateCreated();
        Instant firstMessageDate = messageModel.getCreatedAt();

        if (contactCreated == null || firstMessageDate == null) {
            return false;
        }

        if (firstMessageDate.toEpochMilli() - contactCreated.toEpochMilli() > DateUtils.DAY_IN_MILLIS) {
            return false;
        }

        for (int i = positionOfFirstIncomingMessage; i < numMessages; i++) {
            AbstractMessageModel abstractMessageModel = composeMessageAdapter.getItem(i);
            if (abstractMessageModel == null || abstractMessageModel.isOutbox()) {
                return false;
            }
        }

        return true;
    }

    private synchronized void deleteMessages(@NonNull List<AbstractMessageModel> messages) {
        if (messages.isEmpty()) {
            return;
        }
        for (AbstractMessageModel m : messages) {
            messageService.remove(m);
        }
    }

    @UiThread
    private void contactTypingStateChanged(boolean isTyping) {
        if (isTypingView != null) {
            logger.debug("is typing {} footer view count {}", isTyping, convListView.getFooterViewsCount());
            if (isTyping) {
                // remove if another footer element added
                if (convListView.getFooterViewsCount() == 0) {
                    isTypingView.setVisibility(View.VISIBLE);
                    convListView.addFooterView(isTypingView, null, false);
                }
            } else {
                removeIsTypingFooter();
            }
        }
    }

    private void removeIsTypingFooter() {
        if (isTypingView != null) {
            isTypingView.setVisibility(View.GONE);
            if (convListView != null && convListView.getFooterViewsCount() > 0) {
                convListView.removeFooterView(isTypingView);
            }
        }
    }

    @UiThread
    private boolean addMessageToList(AbstractMessageModel message) {
        if (message == null || this.messageReceiver == null || this.composeMessageAdapter == null) {
            return false;
        }

        if (message.getType() == MessageType.POLL && !message.isOutbox()) {
            // If we receive a new poll message
            openPollNoticeView.update();
        }

        //check if the message already added
        if (this.listInitializedAt != null && message.getCreatedAt().isBefore(this.listInitializedAt)) {
            return false;
        }

        if (!this.messageReceiver.isMessageBelongsToMe(message)) {
            //do nothing, not my thread
            return false;
        }

        logger.debug("addMessageToList: started");

        this.composeMessageAdapter.removeFirstUnreadPosition();

        // if previous message is from another date, add a date separator
        synchronized (this.messageValues) {
            int size = this.messageValues.size();
            Instant date = Instant.now();
            Instant createdAt = size > 0 ? this.messageValues.get(size - 1).getCreatedAt() : Instant.EPOCH;

            if (!isSameDayAs(date, createdAt)) {
                final DateSeparatorMessageModel dateSeparatorMessageModel = new DateSeparatorMessageModel();
                dateSeparatorMessageModel.setCreatedAt(date);
                this.messageValues.add(size, dateSeparatorMessageModel);
            }
        }

        this.composeMessageAdapter.add(message);

        if (!this.isPaused) {
            this.recentlyAddedCount++;
            new MarkAsReadRoutine(conversationService, messageService, notificationService)
                .runAsync(Collections.singletonList(message), messageReceiver);
        } else {
            this.unreadMessages.add(message);
        }

        if (message.isOutbox()) {
            // scroll to bottom on outgoing message
            scrollList(Integer.MAX_VALUE);
        }

        if (!(message instanceof FirstUnreadMessageModel) && currentPageReferenceId == null) {
            setCurrentPageReferenceId(message.getId());
        }

        logger.debug("addMessageToList: finished");

        return true;
    }

    @UiThread
    private void scrollList(final int targetPosition) {
        logger.debug("scrollList {}", targetPosition);

        if (this.listUpdateInProgress) {
            logger.debug("Update in progress");
            return;
        }

        if (this.composeMessageAdapter == null) {
            return;
        }

        this.convListView.post(() -> {
            int topEntry = convListView.getFirstVisiblePosition();

            // update only if really necessary
            if (targetPosition != topEntry) {
                listUpdateInProgress = true;

                int listEntryCount = convListView.getCount();

                if (topEntry > targetPosition) {
                    // scroll up
                    int startPosition = targetPosition + SMOOTHSCROLL_THRESHOLD;

                    if (startPosition < listEntryCount) {
                        convListView.setSelection(targetPosition);
                    } else {
                        convListView.smoothScrollToPosition(targetPosition);
                    }
                } else {
                    // scroll down
                    int startPosition = listEntryCount - SMOOTHSCROLL_THRESHOLD;

                    if (listEntryCount - convListView.getLastVisiblePosition() > SMOOTHSCROLL_THRESHOLD && startPosition > 0) {
                        convListView.setSelection(targetPosition);
                    } else {
                        convListView.smoothScrollToPosition(targetPosition);
                    }
                }
                listUpdateInProgress = false;
            }
        });
    }

    /**
     * Loading the next records for the listview
     */
    @WorkerThread
    private List<AbstractMessageModel> getNextRecords() {
        List<AbstractMessageModel> messageModels = this.messageService.getMessagesForReceiver(this.messageReceiver, this.nextMessageFilter);
        this.valuesLoaded(messageModels);
        return messageModels;
    }

    @WorkerThread
    private List<AbstractMessageModel> getAllRecords() {
        List<AbstractMessageModel> messageModels = this.messageService.getMessagesForReceiver(this.messageReceiver);
        this.valuesLoaded(messageModels);
        return messageModels;
    }

    /**
     * Append records to the list, adding date separators if necessary
     * Locks list by calling setNotifyOnChange(false) on the adapter to speed up list ctrl
     * Don't forget to call notifyDataSetChanged() on the adapter in the UI thread after inserting
     *
     * @param values     MessageModels to insert
     * @param clear      Whether previous list entries should be cleared before appending
     * @param markasread Whether chat should be marked as read
     * @return Number of items that have been added to the list INCLUDING date separators and other decoration
     */
    @UiThread
    private int insertToList(final List<AbstractMessageModel> values, boolean clear, boolean markasread, boolean notify) {
        int insertedSize = 0;

        this.composeMessageAdapter.setNotifyOnChange(false);
        synchronized (this.messageValues) {
            int initialSize = this.messageValues.size();

            Instant date = Instant.now();
            if (clear) {
                this.messageValues.clear();
            } else {
                // prevent duplicate date separators when adding messages to an existing chat (e.g. after pull-to-refresh)
                if (!this.messageValues.isEmpty()) {
                    if (this.messageValues.get(0) instanceof DateSeparatorMessageModel) {
                        this.messageValues.remove(0);
                    }
                    AbstractMessageModel topmostMessage = this.messageValues.get(0);
                    if (topmostMessage != null) {
                        Instant topmostDate = topmostMessage.getCreatedAt();
                        if (topmostDate != null) {
                            date = topmostDate;
                        }
                    }
                }
            }

            for (AbstractMessageModel m : values) {
                Instant createdAt = m.getCreatedAt();
                if (createdAt != null) {
                    if (!isSameDayAs(date, createdAt)) {
                        if (!this.messageValues.isEmpty()) {
                            final DateSeparatorMessageModel dateSeparatorMessageModel = new DateSeparatorMessageModel();
                            dateSeparatorMessageModel.setCreatedAt(this.messageValues.get(0).getCreatedAt());
                            this.messageValues.add(0, dateSeparatorMessageModel);
                        }
                        date = createdAt;
                    }
                }

                this.messageValues.add(0, m);
            }

            if (!this.messageValues.isEmpty() && !(this.messageValues.get(0) instanceof DateSeparatorMessageModel)) {
                // add topmost date separator
                final DateSeparatorMessageModel dateSeparatorMessageModel = new DateSeparatorMessageModel();
                dateSeparatorMessageModel.setCreatedAt(this.messageValues.get(0).getCreatedAt());
                this.messageValues.add(0, dateSeparatorMessageModel);
            }

            this.listInitializedAt = Instant.now();

            insertedSize = this.messageValues.size() - initialSize;
        }

        if (clear) {
            composeMessageAdapter.setNotifyOnChange(true);
            composeMessageAdapter.notifyDataSetInvalidated();
        } else {
            if (notify) {
                composeMessageAdapter.notifyDataSetChanged();
            } else {
                composeMessageAdapter.setNotifyOnChange(true);
            }
        }

        if (markasread) {
            markAsRead();
        }
        return insertedSize;
    }

    private void valuesLoaded(List<AbstractMessageModel> values) {
        if (values != null && !values.isEmpty()) {
            AbstractMessageModel topMessageModel = values.get(values.size() - 1);
            // the topmost message may be a unread messages indicator. as it does not have an id, skip it.
            if (topMessageModel instanceof FirstUnreadMessageModel && values.size() > 1) {
                topMessageModel = values.get(values.size() - 2);
            }
            setCurrentPageReferenceId(topMessageModel.getId());
        }
    }

    /**
     * initialize conversation list and set the unread message count
     */
    @SuppressLint({"StaticFieldLeak", "WrongThread"})
    @UiThread
    private void initConversationList(@Nullable Runnable runAfter) {
        this.unreadCount = (int) this.messageReceiver.getUnreadMessagesCount();
        if (this.unreadCount > MESSAGE_PAGE_SIZE) {
            new LifecycleAwareAsyncTask<Void, List<AbstractMessageModel>>() {
                @Override
                protected void onPreExecute() {
                    GenericProgressDialog.newInstance(0, R.string.please_wait).show(getParentFragmentManager(), DIALOG_TAG_LOADING_MESSAGES);
                }

                @Override
                protected List<AbstractMessageModel> doInBackground(Void params) {
                    return messageService.getMessagesForReceiver(messageReceiver, new MessageService.MessageFilter() {
                        @Override
                        public long getPageSize() {
                            return unreadCount;
                        }

                        @Override
                        public Integer getPageReferenceId() {
                            return null;
                        }

                        @Override
                        public boolean withStatusMessages() {
                            return false;
                        }

                        @Override
                        public boolean withUnsaved() {
                            return false;
                        }

                        @Override
                        public boolean onlyUnread() {
                            return false;
                        }

                        @Override
                        public boolean onlyDownloaded() {
                            return false;
                        }

                        @Override
                        public MessageType[] types() {
                            return new MessageType[0];
                        }

                        @Override
                        public int[] contentTypes() {
                            return null;
                        }

                        @Override
                        public int[] displayTags() {
                            return null;
                        }
                    });
                }

                @Override
                protected void onPostExecute(List<AbstractMessageModel> values) {
                    valuesLoaded(values);
                    populateList(values);
                    DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_LOADING_MESSAGES, true);
                    if (runAfter != null) {
                        runAfter.run();
                    }
                }
            }.execute(this, null);
        } else {
            populateList(getNextRecords());
            if (runAfter != null) {
                runAfter.run();
            }
        }
    }

    /**
     * Populate ListView with provided message models
     */
    @UiThread
    private void populateList(List<AbstractMessageModel> values) {
        if (composeMessageAdapter != null) {
            // re-use existing adapter (for example on tablets)
            composeMessageAdapter.clear();
            composeMessageAdapter.setThumbnailWidth(ConfigUtils.getPreferredThumbnailWidth(getContext(), false));
            composeMessageAdapter.setGroupId(groupDatabaseId);
            composeMessageAdapter.setMessageReceiver(messageReceiver);
            composeMessageAdapter.setUnreadMessagesCount(unreadCount);
            insertToList(values, true, true, true);
            updateToolbarTitle();
        } else {
            thumbnailCache = new ThumbnailCache<Integer>(null);

            composeMessageAdapter = new ComposeMessageAdapter(
                requireContext(),
                messageValues,
                userService,
                contactService,
                fileService,
                messageService,
                pollService,
                preferenceService,
                downloadService,
                licenseService,
                emojiReactionsRepository,
                messageReceiver,
                convListView,
                thumbnailCache,
                ConfigUtils.getPreferredThumbnailWidth(getContext(), false),
                /* chatAdapterDecoratorListener = */ this,
                /* linkifyListener = */ this,
                unreadCount,
                mediaControllerFuture,
                /* voipStatusDataChatListener = */ this,
                /* pollChatListener = */this,
                /* messagePlayerFactory = */this,
                /* downloadAlertDialogListener = */this,
                /* userInteractionListener = */this
            );

            //adding footer before setting the list adapter (android < 4.4)
            if (null != convListView && !isGroupChat && !isDistributionListChat) {
                //create the istyping instance for later use
                isTypingView = layoutInflater.inflate(R.layout.conversation_list_item_typing, null);
                convListView.addFooterView(isTypingView, null, false);
            }

            composeMessageAdapter.setGroupId(groupDatabaseId);
            composeMessageAdapter.setOnClickListener(new ComposeMessageAdapter.OnClickListener() {
                @Override
                public void click(View view, int position, AbstractMessageModel messageModel) {
                    if (actionMode == null && messageModel.isOutbox() && (messageModel.getState() == MessageState.SENDFAILED || messageModel.getState() == MessageState.FS_KEY_MISMATCH) && messageReceiver.isMessageBelongsToMe(messageModel)) {
                        final Set<String> finalRecipientIdentities = new HashSet<>();

                        Runnable resendMessage = () -> ExecutorServices.getSendMessageExecutorService().execute(() -> {
                            try {
                                messageService.resendMessage(messageModel, messageReceiver, null, finalRecipientIdentities, MessageId.random(), TriggerSource.LOCAL);
                            } catch (Exception e) {
                                RuntimeUtil.runOnUiThread(() -> {
                                    if (isAdded()) {
                                        Toast.makeText(getContext(), R.string.original_file_no_longer_avilable, Toast.LENGTH_LONG).show();
                                    }
                                });
                            }
                        });

                        if (messageModel instanceof GroupMessageModel) {
                            // Check whether sending failed or a fs reject was received
                            if (messageModel.getState() == MessageState.SENDFAILED) {
                                logger.info("Failed group message clicked, trying to re-send");
                                // If sending failed, we try to resend it to every group member
                                finalRecipientIdentities.addAll(groupModel.getRecipients());
                                resendMessage.run();
                                return;
                            }

                            // For group messages we first show a dialog to indicate the affected
                            // recipients
                            MessageId messageId = messageModel.getMessageId();

                            RejectedGroupMessageFactory rejectedGroupMessageFactory = databaseService.getRejectedGroupMessageFactory();
                            finalRecipientIdentities.addAll(rejectedGroupMessageFactory.getMessageRejects(messageId, groupModel));

                            if (finalRecipientIdentities.isEmpty()) {
                                // If there is no rejected recipient, we can just update the message
                                // state as the rejected recipient is no longer a group member.
                                // Note that this should never happen.
                                messageService.updateOutgoingMessageState(messageModel, MessageState.SENT, Instant.now());
                                logger.warn("Resend for group members requested, although no member rejected it");
                                return;
                            }
                            ResendGroupMessageDialog.getInstance(
                                finalRecipientIdentities,
                                contactService,
                                preferenceService,
                                resendMessage::run
                            ).show(getParentFragmentManager(), DIALOG_TAG_CONFIRM_RESEND);
                        } else {
                            logger.info("Failed message clicked, trying to re-send");
                            finalRecipientIdentities.add(messageModel.getIdentity());
                            resendMessage.run();
                        }
                    } else {
                        logger.info("Message clicked");
                        onListItemClick(view, position, messageModel);
                    }
                }

                @Override
                public void longClick(View view, int position, AbstractMessageModel messageModel) {
                    logger.info("Message long-clicked");
                    onListItemLongClick(view, position);
                }

                @Override
                public boolean touch(View view, MotionEvent motionEvent, AbstractMessageModel messageModel) {
                    if (actionMode != null) {
                        return false;
                    }
                    if (listViewTouchSwipeListener != null) {
                        // performs (long) click manually
                        //  to propagate event to click listeners only after checking for swipe
                        return listViewTouchSwipeListener.onTouch(view, motionEvent);
                    }
                    return false;
                }

                @Override
                public void avatarClick(View view, int position, AbstractMessageModel messageModel) {
                    if (messageModel != null && messageModel.getIdentity() != null) {
                        ContactModel contactModel = contactService.getByIdentity(messageModel.getIdentity());
                        if (contactModel != null) {
                            Intent intent;
                            if (messageModel instanceof GroupMessageModel || messageModel instanceof DistributionListMessageModel) {
                                logger.info("Message avatar clicked in group chat or distribution list, opening compose screen for contact");
                                intent = ComposeMessageActivity.createIntent(
                                    getActivity(),
                                    new ContactConversationId(contactModel.getIdentity())
                                );
                                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                                requireActivity().finish();

                            } else {
                                logger.info("Message avatar clicked in contact chat, opening contact details");
                                intent = new Intent(getActivity(), ContactDetailActivity.class);
                                intent.putExtra(AppConstants.INTENT_DATA_CONTACT_READONLY, true);
                                IntentDataUtil.append(contactModel, intent);
                            }
                            getActivity().startActivity(intent);
                        }
                    }
                }

                @SuppressLint("DefaultLocale")
                @Override
                public void onSearchResultsUpdate(int searchResultsIndex, int searchResultsSize, final int queryLength) {
                    RuntimeUtil.runOnUiThread(() -> {
                        if (searchCounter != null) {
                            try {
                                if (queryLength < MIN_CONSTRAINT_LENGTH && searchResultsSize == 0) {
                                    searchCounter.setText(getString(R.string.min_n_chars, MIN_CONSTRAINT_LENGTH));
                                    searchCounter.setVisibility(View.VISIBLE);
                                    searchPreviousButton.setVisibility(View.INVISIBLE);
                                    searchNextButton.setVisibility(View.INVISIBLE);
                                } else {
                                    searchCounter.setText(String.format("%d / %d", searchResultsIndex, searchResultsSize));
                                    searchCounter.setVisibility(View.VISIBLE);
                                    searchPreviousButton.setVisibility(View.VISIBLE);
                                    searchNextButton.setVisibility(View.VISIBLE);
                                }
                            } catch (Exception e) {
                                //
                            }
                        }
                    });
                }

                @Override
                public void onSearchInProgress(boolean inProgress) {
                    RuntimeUtil.runOnUiThread(() -> {
                        if (searchNextButton != null && searchPreviousButton != null) {
                            try {
                                searchProgress.setVisibility(inProgress ? View.VISIBLE : View.INVISIBLE);
                            } catch (Exception e) {
                                //
                            }
                        }
                    });
                }

                @Override
                public void onEmojiReactionClick(@Nullable String emojiSequence, @Nullable AbstractMessageModel messageModel) {
                    if (isGroupChatWhereUserIsNotMemberOf()) {
                        SingleToast.getInstance().showLongText(getString(R.string.you_are_not_a_member_of_this_group));
                        return;
                    }

                    composeMessageFragmentUtils.onEmojiReactionClicked(emojiSequence, messageModel);
                }

                @Override
                public void onEmojiReactionLongClick(@Nullable String emojiSequence, @Nullable AbstractMessageModel messageModel) {
                    logger.info("Emoji reaction long-clicked");
                    showEmojiReactionsOverview(messageModel, emojiSequence);
                }

                @Override
                public void onSelectButtonClick(@Nullable AbstractMessageModel messageModel) {
                    if (isGroupChatWhereUserIsNotMemberOf()) {
                        RuntimeUtil.runOnUiThread(() ->
                            SingleToast.getInstance().showLongText(getString(R.string.you_are_not_a_member_of_this_group))
                        );
                    } else if (MessageUtil.canEmojiReact(messageModel)) {
                        logger.info("Emoji select button clicked, showing picker");
                        showEmojiReactionsPicker(messageModel);
                    }
                }

                @Override
                public void onMoreReactionsButtonClick(@Nullable AbstractMessageModel messageModel) {
                    logger.info("More reactions button clicked");
                    showEmojiReactionsOverview(messageModel, null);
                }
            });
            insertToList(
                values,
                false,
                !conversationCategoryService.isMarkedAsPrivate(conversationId),
                false
            );
            convListView.setAdapter(composeMessageAdapter);
            convListView.setItemsCanFocus(false);
            convListView.setVisibility(View.VISIBLE);
        }

        setIdentityColors();

        removeIsTypingFooter();
    }

    /**
     * @return {@code true} if the user is not a member of this group chat. In case we are in a group conversation, but we could not determine the
     * current member status we also return {@code true}.
     */
    private boolean isGroupChatWhereUserIsNotMemberOf() {
        if (!isGroupChat || groupModel == null) {
            return false;
        }
        return !groupModel.isMember();
    }

    private void showEmojiReactionsOverview(@Nullable AbstractMessageModel messageModel, @Nullable String emojiSequence) {
        if (messageModel == null) {
            logger.error("MessageModel is null");
            return;
        }

        Intent intent = new Intent(activity, EmojiReactionsOverviewActivity.class);
        IntentDataUtil.append(messageModel, intent);
        if (emojiSequence != null) {
            intent.putExtra(EmojiReactionsOverviewActivity.EXTRA_INITIAL_EMOJI, emojiSequence);
        }
        emojiReactionsLauncher.launch(intent);
        rootInsetsDeferringCallback.setEnabled(false);
        keyboardAnimationInsetsCallback.setEnabled(false);
    }

    /**
     * Jump to first unread message keeping in account shift caused by date separators and other decorations
     * Currently depends on various globals...
     */
    @UiThread
    private void jumpToFirstUnreadMessage() {
        if (unreadCount > 0) {
            synchronized (this.messageValues) {
                int entryCount = convListView.getCount();
                int position = Math.min(entryCount - unreadCount, this.messageValues.size() - 1);
                while (position >= 0) {
                    if (this.messageValues.get(position) instanceof FirstUnreadMessageModel) {
                        break;
                    }
                    position--;
                }
                final int finalUnreadCount = unreadCount;
                if (!isHidden()) {
                    unreadCount = 0;
                }

                if (position > 0) {
                    final int finalPosition = position;
                    logger.debug("jump to initial position {}", finalPosition);

                    convListView.setSelection(finalPosition);
                    convListView.postDelayed(() -> {
                        convListView.setSelection(finalPosition);
                        if (convListView.canScrollList(View.SCROLL_AXIS_VERTICAL)) {
                            if (scrollButtonManager != null) {
                                scrollButtonManager.showButton(TYPE_DOWN, finalUnreadCount);
                            }
                        }
                    }, 500);

                    return;
                }
            }
        }
        convListView.setSelection(Integer.MAX_VALUE);
    }

    private void setIdentityColors() {
        logger.debug("setIdentityColors");

        if (this.isGroupChat) {
            Map<String, IdColor> colorIndices = groupService.getGroupParticipantIDColors(groupModel);
            Map<String, Integer> colors = new HashMap<>();
            boolean darkTheme = ConfigUtils.isTheDarkSide(getContext());
            for (Map.Entry<String, IdColor> entry : colorIndices.entrySet()) {
                String memberIdentity = entry.getKey();
                IdColor memberIdColor = entry.getValue();
                int idColor = darkTheme
                    ? memberIdColor.getColorDark()
                    : memberIdColor.getColorLight();
                colors.put(memberIdentity, idColor);
            }

            this.identityColors = colors;
        } else {
            this.identityColors.clear();
        }
        this.composeMessageAdapter.setIdentityColors(this.identityColors);
    }

    private void onListItemClick(View view, int position, AbstractMessageModel messageModel) {
        if (view == null) {
            return;
        }

        if (actionMode != null) {
            if (selectedMessages.contains(messageModel)) {
                // remove from selection
                selectedMessages.remove(messageModel);
                convListView.setItemChecked(position, false);
                logger.info("Message deselected for action mode");
            } else {
                if (convListView.getCheckedItemCount() < MAX_SELECTED_ITEMS &&
                    isItemSelectable(composeMessageAdapter.getItemViewType(position), messageModel)) {
                    // add this to selection
                    selectedMessages.add(messageModel);
                    convListView.setItemChecked(position, true);
                    logger.info("Message selected for action mode");
                } else {
                    convListView.setItemChecked(position, false);
                    logger.info("Message deselected for action mode, limit reached or not selectable");
                }
            }

            final int checked = convListView.getCheckedItemCount();
            if (checked > 0) {
                // invalidate menu to update display => onPrepareActionMode()
                actionMode.invalidate();
            } else {
                actionMode.finish();
            }
        } else {
            if (view.isSelected()) {
                view.setSelected(false);
            }
            if (convListView.isItemChecked(position)) {
                convListView.setItemChecked(position, false);
            }
            // check if item is a quote
            if (QuoteUtil.isQuoteV1(messageModel.getBody())) {
                QuoteUtil.QuoteContent quoteContent = QuoteUtil.getQuoteContent(
                    messageModel,
                    messageReceiver,
                    false,
                    thumbnailCache,
                    getContext(),
                    this.messageService,
                    this.userService,
                    this.fileService,
                    this.preferenceService.getContactNameFormat()
                );

                if (quoteContent != null) {
                    if (searchActionMode != null) {
                        searchActionMode.finish();
                    }

                    ComposeMessageAdapter.ConversationListFilter filter = (ComposeMessageAdapter.ConversationListFilter) composeMessageAdapter.getQuoteFilter(quoteContent);
                    // search for quoted text
                    filter.filter(quoteContent.quotedText, count -> {
                        if (count == 0) {
                            SingleToast.getInstance().showShortText(getString(R.string.quote_not_found));
                        }
                    });
                }
            } else if (messageModel.getQuotedMessageId() != null) {
                QuoteUtil.QuoteContent quoteContent = QuoteUtil.getQuoteContent(
                    messageModel,
                    messageReceiver,
                    false,
                    thumbnailCache,
                    getContext(),
                    this.messageService,
                    this.userService,
                    this.fileService,
                    this.preferenceService.getContactNameFormat()
                );
                if (quoteContent != null) {
                    if (searchActionMode != null) {
                        searchActionMode.finish();
                    }

                    AbstractMessageModel quotedMessageModel = messageService.getMessageModelByApiMessageIdAndReceiver(messageModel.getQuotedMessageId(), messageReceiver);
                    logger.info("Trying to jump to quoted message");
                    if (quotedMessageModel != null) {
                        ComposeMessageAdapter.ConversationListFilter filter = (ComposeMessageAdapter.ConversationListFilter) composeMessageAdapter.getQuoteFilter(quoteContent);
                        searchV2Quote(quotedMessageModel.getApiMessageId(), filter);
                    } else {
                        Toast.makeText(getContext().getApplicationContext(), R.string.quoted_message_deleted, Toast.LENGTH_SHORT).show();
                    }
                }
            } else if ((messageModel.getType() == MessageType.TEXT && !messageModel.isStatusMessage()) || messageModel.isDeleted()) {
                logger.info("Opening message details screen");
                showMessageDetailScreen(messageModel);
            }
        }
    }

    /**
     * Recursively search for message with provided apiMessageId in chat and gradually load more records to Adapter until matching message is found by provided Filter
     *
     * @param apiMessageId to search for
     * @param filter       Filter to use for this search
     */
    @UiThread
    synchronized private void searchV2Quote(final String apiMessageId, final ComposeMessageAdapter.ConversationListFilter filter) {
        filter.filter("#" + apiMessageId, new Filter.FilterListener() {
            @SuppressLint("StaticFieldLeak")
            @Override
            public void onFilterComplete(int count) {
                if (count == 0) {
                    new LifecycleAwareAsyncTask<Void, Integer>() {
                        List<AbstractMessageModel> messageModels;

                        @Override
                        protected Integer doInBackground(Void params) {
                            messageModels = getNextRecords();
                            if (messageModels != null) {
                                return messageModels.size();
                            }
                            return null;
                        }

                        @Override
                        protected void onPostExecute(Integer result) {
                            if (getContext() != null) {
                                if (result != null && result > 0) {
                                    insertToList(messageModels, false, false, true);

                                    if (getFragmentManager() != null) {
                                        if (getFragmentManager().findFragmentByTag(DIALOG_TAG_SEARCHING) == null) {
                                            GenericProgressDialog.newInstance(R.string.searching, R.string.please_wait).show(getFragmentManager(), DIALOG_TAG_SEARCHING);
                                        }
                                        searchV2Quote(apiMessageId, filter);
                                    }
                                } else {
                                    SingleToast.getInstance().showShortText(getString(R.string.quote_not_found));
                                    swipeRefreshLayout.setEnabled(false);
                                    DialogUtil.dismissDialog(getParentFragmentManager(), DIALOG_TAG_SEARCHING, true);
                                }
                            }
                        }
                    }.execute(ComposeMessageFragment.this, null);
                } else {
                    DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_SEARCHING, true);
                }
            }
        });
    }

    @UiThread
    private void onListItemLongClick(@NonNull View view, final int position) {
        int viewType = composeMessageAdapter.getItemViewType(position);
        AbstractMessageModel selectedMessage = composeMessageAdapter.getItem(position);

        if (!isItemSelectable(viewType, selectedMessage)) {
            return;
        }

        selectedMessages.clear();
        selectedMessages.add(selectedMessage);

        if (actionMode != null) {
            convListView.clearChoices();
            convListView.setItemChecked(position, true);
            actionMode.invalidate();
        } else {
            convListView.setChoiceMode(CHOICE_MODE_MULTIPLE);
            convListView.setItemChecked(position, true);
            view.setSelected(true);
            actionMode = activity.startSupportActionMode(new ComposeMessageAction(position));
        }

        view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);

        // fix linkify on longclick problem
        // see: http://stackoverflow.com/questions/16047215/android-how-to-stop-linkify-on-long-press
        longClickItem = position;

        if (viewType == ComposeMessageAdapter.TYPE_STATUS_DATA_RECV ||
            viewType == ComposeMessageAdapter.TYPE_STATUS_DATA_SEND) {
            // Don't show popup for these view types (but allow them to be selected)
            return;
        }

        showEmojiReactionsPopup(view, selectedMessage);
    }

    private void showEmojiReactionsPopup(@NonNull View originView, @NonNull AbstractMessageModel messageModel) {
        if (messageReceiver == null) {
            logger.error("No MessageReceiver to show emoji reactions popup for");
            return;
        }

        // Don't even show popup in case of a DistributionList
        if (messageReceiver instanceof DistributionListMessageReceiver) {
            logger.debug("Cannot react on distribution list messages");
            return;
        }

        // Don't show the reaction popup if we are in a group chat and we are not an active group member anymore
        if (isGroupChatWhereUserIsNotMemberOf()) {
            logger.debug("Cannot react on group message because we are not an active group member anymore");
            return;
        }

        // check if we can react on this kind of message
        if (!MessageUtil.canEmojiReact(messageModel)) {
            return;
        }

        boolean isReactionsSupportNone = messageReceiver.getEmojiReactionSupport() == MessageReceiver.Reactions_NONE;

        if (messageReceiver instanceof ContactMessageReceiver
            && isReactionsSupportNone
            && messageModel.isOutbox()) {
            logger.debug("Cannot react on my own messages if reactions are not yet supported by recipient");
            return;
        }

        boolean isGatewayChat = isGatewayChat(messageReceiver);
        boolean isSendingReactionsAllowed = !isReactionsSupportNone && !isGatewayChat;
        emojiReactionsPopup = new EmojiReactionsPopup(
            requireContext(),
            convListView,
            getParentFragmentManager(),
            isSendingReactionsAllowed,
            isGatewayChat);
        emojiReactionsPopup.setListener(new EmojiReactionsPopup.EmojiReactionsPopupListener() {
            @Override
            public void onTopReactionClicked(@NonNull final AbstractMessageModel messageModel, @NonNull final String emojiSequence) {
                RuntimeUtil.runOnWorkerThread(() -> {
                    try {
                        messageService.sendEmojiReaction(messageModel, emojiSequence, Objects.requireNonNull(messageReceiver), false);
                    } catch (Exception e) {
                        logger.error("Failed to send emoji reaction", e);
                    }
                });

                if (actionMode == null) {
                    return;
                }
                actionMode.finish();
            }

            @Override
            public void onAddReactionClicked(@NonNull final AbstractMessageModel messageModel) {
                showEmojiReactionsPicker(messageModel);
            }
        });
        emojiReactionsPopup.show(originView.findViewById(R.id.message_block), selectedMessages.get(0));
    }

    private void showEmojiReactionsPicker(@Nullable AbstractMessageModel messageModel) {
        if (messageModel != null && messageReceiver != null) {
            Intent intent = new Intent(activity, EmojiReactionsPickerActivity.class);
            IntentDataUtil.append(messageModel, intent);
            emojiReactionsLauncher.launch(intent);
            rootInsetsDeferringCallback.setEnabled(false);
            keyboardAnimationInsetsCallback.setEnabled(false);
        } else {
            logger.debug("MessageModel or Receiver is null");
        }
    }

    /**
     * Check whether the selected item in the conversation list can be selected
     *
     * @param viewType        View type of the item
     * @param selectedMessage Message Model of the item
     * @return true if item is selectable, false otherwise
     */
    @Contract("_, null -> false")
    private boolean isItemSelectable(int viewType, @Nullable AbstractMessageModel selectedMessage) {
        if (viewType == ComposeMessageAdapter.TYPE_FIRST_UNREAD ||
            viewType == ComposeMessageAdapter.TYPE_DATE_SEPARATOR) {
            // Do not allow to select these view types
            return false;
        }

        if (selectedMessage == null) {
            return false;
        }

        if (viewType == ComposeMessageAdapter.TYPE_FILE_VIDEO_SEND && selectedMessage.getState() == MessageState.TRANSCODING) {
            // transcoding messages cannot be selected
            return false;
        }

        return true;
    }

    private boolean isMuted() {
        if (messageReceiver == null) {
            return false;
        }
        final @Nullable NotificationTriggerPolicyOverride<?> currentNotificationTriggerPolicyOverride = messageReceiver.getNotificationTriggerPolicyOverrideOrNull();
        return currentNotificationTriggerPolicyOverride != null && muteAppliesAt(currentNotificationTriggerPolicyOverride, timeProvider.get());
    }

    private boolean isMentionsOnly() {
        if (messageReceiver == null) {
            return false;
        }
        final @Nullable NotificationTriggerPolicyOverride<?> currentNotificationTriggerPolicyOverride = messageReceiver.getNotificationTriggerPolicyOverrideOrNull();
        if (currentNotificationTriggerPolicyOverride instanceof GroupNotificationTriggerPolicyOverride) {
            return currentNotificationTriggerPolicyOverride.getPolicy() == GroupNotificationTriggerPolicyOverridePolicy.MENTIONED;
        }
        return false;
    }

    private boolean isSilent() {
        if (conversationId != null && ringtoneService != null) {
            return ringtoneService.hasCustomRingtone(conversationId) && ringtoneService.isSilent(conversationId, isGroupChat);
        }
        return false;
    }

    private void playInAppSound(final int resId, final boolean isVibrate) {
        if (isMuted() || isSilent()) {
            //do not play
            return;
        }
        var context = requireContext().getApplicationContext();
        RuntimeUtil.runOnUiThread(() -> {
            if (preferenceService.isInAppSounds()) {
                soundEffectPlayer.play(resId);
            }

            if (preferenceService.isInAppVibrate() && isVibrate) {
                Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
                if (vibrator != null) {
                    switch (audioManager.getRingerMode()) {
                        case AudioManager.RINGER_MODE_VIBRATE:
                        case AudioManager.RINGER_MODE_NORMAL:
                            vibrator.vibrate(VIBRATION_MSEC);
                            break;
                        default:
                            break;
                    }
                }
            }
        });
    }

    private void playSentSound() {
        playInAppSound(R.raw.sent_message, false);
    }

    private void playReceivedSound() {
        playInAppSound(R.raw.received_message, true);
    }

    private void sendMessage() {
        if (typingIndicatorTextWatcher != null) {
            typingIndicatorTextWatcher.stopSending();
        }

        if (!this.validateSendingPermission()) {
            return;
        }

        if (!isNullOrBlank(this.messageText.getText())) {
            prepareSendTextMessage();
        } else {
            if (ConfigUtils.requestAudioPermissions(requireActivity(), this, PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE)) {
                attachVoiceMessage();
            }
        }
    }

    private void prepareSendTextMessage() {
        final CharSequence message;

        if (isQuotePopupShown()) {
            QuotePopup.QuoteInfo quoteInfo = quotePopup.getQuoteInfo();
            message = QuoteUtil.quote(
                this.messageText.getText().toString(),
                quoteInfo.getQuoteIdentity(),
                quoteInfo.getQuoteText(),
                quoteInfo.getMessageModel()
            );

            messageText.postDelayed(this::dismissQuotePopup, 500);
        } else {
            message = this.messageText.getText();
        }

        if (!isNullOrBlank(message)) {
            sendTextMessage(message);
        } else {
            logger.warn("Message text is empty");
        }
    }

    private void sendTextMessage(CharSequence message) {
        // block send button to avoid double posting
        this.messageText.setText("");

        if (typingIndicatorTextWatcher != null) {
            messageText.removeTextChangedListener(typingIndicatorTextWatcher);
        }

        if (typingIndicatorTextWatcher != null) {
            messageText.addTextChangedListener(typingIndicatorTextWatcher);
        }

        //send stopped typing message
        if (typingIndicatorTextWatcher != null) {
            typingIndicatorTextWatcher.stopSending();
        }

        new Thread(() -> TextMessageSendAction.getInstance()
            .sendTextMessage(new MessageReceiver[]{messageReceiver}, message.toString(), new SendAction.ActionHandler() {
                @Override
                public void onError(final String errorMessage) {
                    RuntimeUtil.runOnUiThread(() -> {
                        LongToast.makeText(getActivity(), errorMessage, Toast.LENGTH_LONG).show();
                        if (!isNullOrBlank(message)) {
                            messageText.setText(message);
                            messageText.setSelection(messageText.length());
                        }
                    });
                }

                @Override
                public void onCompleted() {
                    RuntimeUtil.runOnUiThread(() -> {
                        if (ConfigUtils.isTabletLayout() && conversationId != null) {
                            // remove draft right now to make sure conversations pane is updated
                            draftManager.remove(conversationId);
                        }
                    });
                }
            })).start();
    }

    private void attachVoiceMessage() {
        dismissQuotePopup();

        // stop all message players
        if (this.messagePlayerService != null) {
            this.messagePlayerService.pauseAll(SOURCE_AUDIORECORDER);
        }

        Intent intent = new Intent(activity, VoiceRecorderActivity.class);
        IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
        activity.startActivityForResult(intent, ACTIVITY_ID_VOICE_RECORDER);
        activity.overridePendingTransition(R.anim.fast_fade_in, R.anim.fast_fade_out);
    }

    private void tryEditingSelectedMessage() {
        if (selectedMessages.size() != 1) {
            logger.error("Cannot edit more than one selected message.");
            return;
        }

        AbstractMessageModel message = selectedMessages.get(0);

        if (messageReceiver instanceof ContactMessageReceiver) {
            ContactMessageReceiver receiver = (ContactMessageReceiver) messageReceiver;
            startEditingContactMessage(receiver, message);
        } else if (messageReceiver instanceof GroupMessageReceiver && groupModel != null) {
            startEditingGroupMessage(groupModel, message);
        } else if (messageReceiver != null) {
            logger.error("Cannot edit message for receiver of type {}", messageReceiver.getClass().getName());
        }
    }

    private void startEditingContactMessage(@NonNull ContactMessageReceiver receiver, @NonNull AbstractMessageModel message) {
        if (ThreemaFeature.canEditMessages(receiver.getContact().getFeatureMask())) {
            startMessageEditor(message);
        } else {
            LongToast.makeText(
                getContext(),
                getResources().getString(R.string.edit_message_not_supported_for_contact),
                Toast.LENGTH_LONG).show();
        }
    }

    private void startEditingGroupMessage(@NonNull GroupModel groupModel, @NonNull AbstractMessageModel message) {
        if (Boolean.TRUE.equals(groupModel.isNotesGroup())) {
            startMessageEditor(message);
            return;
        }

        GroupModelData groupModelData = groupModel.getData();
        if (groupModelData == null) {
            logger.warn("Cannot start editing group message of deleted group");
            return;
        }

        GroupFeatureSupport featureSupport = groupService.getFeatureSupport(groupModelData, ThreemaFeature.EDIT_MESSAGES);
        if (featureSupport.getAdoptionRate() == GroupFeatureAdoptionRate.ALL) {
            startMessageEditor(message);
        } else if (featureSupport.getAdoptionRate() == GroupFeatureAdoptionRate.PARTIAL) {
            List<ContactModel> membersWithoutFeatureSupport = featureSupport.getContactsWithoutFeatureSupport();
            GenericAlertDialog dialog = GenericAlertDialog.newInstance(
                R.string.warning,
                getString(
                    R.string.edit_message_not_supported_for_all_group_members,
                    ContactUtil.joinDisplayNames(getContext(), membersWithoutFeatureSupport, preferenceService.getContactNameFormat())
                ),
                R.string.ok,
                R.string.cancel
            );
            dialog.setCallback((tag, data) -> startMessageEditor(message));
            dialog.show(getChildFragmentManager(), DIALOG_TAG_EDIT_MESSAGES_UNSUPPORTED_WARNING);
        } else {
            LongToast.makeText(
                getContext(),
                getResources().getString(R.string.edit_message_not_supported_for_any_group_members),
                Toast.LENGTH_LONG).show();
        }
    }

    private void startMessageEditor(@NonNull AbstractMessageModel message) {
        if (quotePopup != null && message == quotePopup.getQuoteInfo().getMessageModel()) {
            quotePopup.dismiss();
        }
        editMessageActionMode = activity.startSupportActionMode(new EditMessageActionMode(message));
    }

    private void onSendEditMessage(@NonNull AbstractMessageModel messageModel, @NonNull String editedText) {
        if (editMessageActionMode != null) {
            editMessageActionMode.finish();
        }

        if (isGroupChatWhereUserIsNotMemberOf()) {
            SingleToast.getInstance().showLongText(getString(R.string.you_are_not_a_member_of_this_group));
            return;
        }

        try {
            // when message failed to send edit it locally only
            if (messageModel.getState() == MessageState.SENDFAILED) {
                messageService.saveEditedMessageText(messageModel, editedText, null);
            } else {
                Instant editedAt = Instant.now();
                messageService.sendEditedMessageText(messageModel, editedText, editedAt, Objects.requireNonNull(messageReceiver));
            }
        } catch (Exception e) {
            logger.error("Failed to edit message", e);
        }
    }

    private void copySelectedMessagesToClipboard() {
        if (selectedMessages.isEmpty()) {
            logger.error("no selected messages");
            return;
        }

        StringBuilder body = new StringBuilder();
        for (AbstractMessageModel message : selectedMessages) {
            if (body.length() > 0) {
                body.append("\n");
            }

            body.append(message.getType() == MessageType.TEXT
                ? QuoteUtil.getMessageBody(
                message.getType(),
                message.getBody(),
                message.getCaption(),
                message.isOutbox(),
                false,
                preferenceService.getContactNameFormat()
            )
                : message.getCaption());
        }

        try {
            ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
            if (clipboard != null) {
                ClipData clipData = ClipData.newPlainText(null, body.toString());
                clipboard.setPrimaryClip(clipData);
                Toast.makeText(
                    getContext(),
                    getResources().getQuantityString(R.plurals.message_copied, selectedMessages.size()),
                    Toast.LENGTH_SHORT
                ).show();
            }
        } catch (Exception e) {
            // Some Android 4.3 devices raise an IllegalStateException when writing to the clipboard
            // while there is an active clipboard listener
            // see https://code.google.com/p/android/issues/detail?id=58043
            logger.error("Exception", e);
        }
    }

    @SuppressLint("StaticFieldLeak")
    private void shareMessages() {
        if (selectedMessages.size() > 1) {
            new LifecycleAwareAsyncTask<Void, Void>() {
                @Override
                protected void onPreExecute() {
                    GenericProgressDialog.newInstance(R.string.decoding_message, R.string.please_wait).show(getFragmentManager(), DIALOG_TAG_DECRYPTING_MESSAGES);
                }

                @Override
                protected Void doInBackground(Void params) {
                    try {
                        var uris = fileService.decryptMessageFilesForSharing(selectedMessages);
                        shareMediaMessages(uris);
                    } catch (Exception e) {
                        logger.error("Failed to decrypt files for sharing", e);
                        showToast(activity, R.string.media_file_not_found, ToastDuration.LONG);
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Void aVoid) {
                    DialogUtil.dismissDialog(getFragmentManager(), DIALOG_TAG_DECRYPTING_MESSAGES, true);
                }
            }.execute(this, null);
        } else {
            final AbstractMessageModel messageModel = selectedMessages.get(0);

            if (messageModel != null) {
                new LoadDecryptedMessageFileAsyncTask(dependencies.getFileService()) {
                    @Override
                    public void onSuccess(File decryptedFile) {
                        if (decryptedFile != null) {
                            String filename = null;
                            if (messageModel.getType() == MessageType.FILE) {
                                filename = messageModel.getFileData().getFileName();
                            }
                            shareMediaMessages(Collections.singletonList(FileProviderUtil.getUriForFile(requireContext(), decryptedFile, filename)));
                        } else {
                            messageService.shareTextMessage(activity, messageModel);
                        }
                    }

                    @Override
                    public void onError(@NonNull Exception e) {
                        logger.error("Failed to share message", e);
                        showToast(activity, R.string.media_file_not_found, ToastDuration.LONG);
                    }
                }
                    .execute(this, messageModel);
            }
        }
    }

    private void shareMediaMessages(List<Uri> uris) {
        if (selectedMessages.size() == 1) {
            logger.info("Showing share dialog for {} message(s)", selectedMessages.size());
            ExpandableTextEntryDialog alertDialog = ExpandableTextEntryDialog.newInstance(
                getString(R.string.share_media),
                R.string.add_caption_hint, selectedMessages.get(0).getCaption(),
                R.string.label_continue, R.string.cancel, true);
            alertDialog.setData(uris);
            alertDialog.setTargetFragment(this, 0);
            alertDialog.show(getParentFragmentManager(), null);
        } else {
            messageService.shareMediaMessages(activity,
                new ArrayList<>(selectedMessages),
                new ArrayList<>(uris), null);
        }
    }

    @Override
    public void onYes(String tag, Object data, String text) {
        logger.info("Sharing dialog confirmed");
        List<Uri> uris = (List<Uri>) data;
        messageService.shareMediaMessages(activity,
            new ArrayList<>(selectedMessages),
            new ArrayList<>(uris), text);
    }

    @UiThread
    private void showQuotePopup(@Nullable AbstractMessageModel messageModel, boolean isUserInitiated) {
        final @NonNull AbstractMessageModel quotedMessageModel;
        if (messageModel == null) {
            quotedMessageModel = selectedMessages.get(0);
        } else {
            quotedMessageModel = messageModel;
        }
        if (quotedMessageModel == null) {
            return;
        }

        sendButton.setEnabled(messageText != null && !isNullOrBlank(messageText.getText()));

        dismissMentionPopup();
        dismissQuotePopup();

        String identity = quotedMessageModel.isOutbox() ? userService.getIdentity() : quotedMessageModel.getIdentity();

        /**
         *  This quote bar view will never have any selected or pressed state. Nevertheless, we use the defined color-state-list here.
         *  This way we have a centralized color definition for this component.
         */
        @NonNull ColorStateList barColor = getContext().getColorStateList(R.color.bubble_quote_bar_default_colorstatelist);
        if (!quotedMessageModel.isOutbox()) {
            if (isGroupChat && identityColors.containsKey(identity)) {
                barColor = ColorStateList.valueOf(identityColors.get(identity));
            } else if (contactModel != null) {
                barColor = ColorStateList.valueOf(contactModel.getIdColor().getThemedColor(requireContext()));
            }
        }

        quotePopup = new QuotePopup(
            activity,
            contactService,
            userService,
            fileService,
            preferenceService,
            thumbnailCache
        );

        final @NonNull ColorStateList barColorFinal = barColor;
        Runnable showPopup = () -> {
            if (isVisible()) {
                quotePopup.show(activity, messageText, textInputLayout, quotedMessageModel, identity, barColorFinal, quotePopupListener);
            }
        };

        if (!isUserInitiated) {
            // In case the user did not initiate opening the quote popup, then we show the popup
            // with a short delay. This prevents that the quote popup is shown before the edit text
            // is lay out with its new size.
            messageText.postDelayed(showPopup, 150);
        } else if (activity.isSoftKeyboardOpen() || isEmojiPickerShown()) {
            // In case the keyboard or the emoji picker is already shown, we can show the quote
            // popup immediately.
            messageText.requestFocus();
            showPopup.run();
        } else {
            // In case the keyboard is not shown and the user initiated opening the quote popup, we
            // open the keyboard and show the quote popup delayed for a smoother experience.
            EditTextUtil.focusWindowAndShowSoftKeyboard(messageText);
            messageText.postDelayed(showPopup, 550);
        }
    }

    private void reopenQuotePopup() {
        if (!isQuotePopupShown()) {
            return;
        }

        AbstractMessageModel quotedMessageModel = quotePopup.getQuoteInfo().getMessageModel();
        if (quotedMessageModel != null) {
            showQuotePopup(quotedMessageModel, false);
        }
    }

    private void dismissQuotePopup() {
        dismissQuotePopup(null);
    }

    private void dismissQuotePopup(@Nullable Runnable runAfterQuotePopupClosed) {
        if (isQuotePopupShown()) {
            try {
                quotePopup.dismiss();
                quotePopup = null;
            } catch (Exception e) {
                logger.error("Error dismissing quote popup", e);
            }
            updateSendButton(messageText.getText());
            if (runAfterQuotePopupClosed != null) {
                runAfterQuotePopupClosed.run();
            }
        }
    }

    private boolean isQuotePopupShown() {
        return quotePopup != null && quotePopup.isShowing();
    }

    private void startForwardMessage() {
        if (!selectedMessages.isEmpty()) {
            if (selectedMessages.size() == 1) {
                final AbstractMessageModel messageModel = selectedMessages.get(0);

                if (messageModel.getType() == MessageType.TEXT) {
                    // allow editing before sending if it's a single text message
                    String body = QuoteUtil.getMessageBody(
                        messageModel.getType(),
                        messageModel.getBody(),
                        messageModel.getCaption(),
                        messageModel.isOutbox(),
                        false,
                        preferenceService.getContactNameFormat()
                    );
                    Intent intent = new Intent(activity, RecipientListBaseActivity.class);
                    intent.setType("text/plain");
                    intent.setAction(Intent.ACTION_SEND);
                    intent.putExtra(Intent.EXTRA_TEXT, body);
                    intent.putExtra(AppConstants.INTENT_DATA_IS_FORWARD, true);
                    activity.startActivity(intent);
                    return;
                }
            }
            FileUtil.forwardMessages(activity, RecipientListBaseActivity.class, selectedMessages);
        }
    }

    /**
     * Toggles the "starred" flag for the provided message and saves it to the database
     *
     * @param messageModel AbstractMessageModel of the message
     */
    private void toggleStar(@Nullable AbstractMessageModel messageModel) {
        if (messageModel != null && messageReceiver != null) {
            messageModel.setDisplayTags(messageModel.getDisplayTags() ^ DISPLAY_TAG_STARRED);
            messageModel.setSaved(true);
            messageReceiver.saveLocalModel(messageModel);
        }
    }

    @UiThread
    private void updateToolbarTitle() {
        if (
            actionBar == null
                || actionBarSubtitleImageView == null
                || actionBarSubtitleTextView == null
                || actionBarTitleTextView == null
                || emojiMarkupUtil == null
                || messageReceiver == null
                || !isAdded()
                || getActivity() == null
                || !requiredInstances()
        ) {
            return;
        }

        this.actionBarSubtitleTextView.setVisibility(View.GONE);
        this.actionBarSubtitleImageView.setVisibility(View.GONE);
        this.actionBarAvatarView.setVisibility(View.VISIBLE);

        this.actionBarTitleTextView.setText(
            this.messageReceiver.getDisplayName(preferenceService.getContactNameFormat())
        );
        this.actionBarTitleTextView.setPaintFlags(this.actionBarTitleTextView.getPaintFlags() & ~Paint.STRIKE_THRU_TEXT_FLAG);

        if (this.isGroupChat) {
            if (groupModel != null && !groupModel.isMember()) {
                this.actionBarTitleTextView.setPaintFlags(this.actionBarTitleTextView.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
            }
            actionBarSubtitleTextView.setText(groupService.getMembersString(groupModel));
            actionBarSubtitleTextView.setVisibility(View.VISIBLE);
            if (actionBarAvatarView.getAvatarView().isAttachedToWindow()) {
                groupService.loadAvatarIntoImageView(
                    groupModel,
                    actionBarAvatarView.getAvatarView(),
                    AvatarOptions.PRESET_DEFAULT_FALLBACK,
                    Glide.with(requireActivity())
                );
            }
            actionBarAvatarView.setIdentityTypeBadgeVisible(false);
            setAvatarContentDescription(R.string.prefs_group_notifications);
        } else if (this.isDistributionListChat) {
            actionBarSubtitleTextView.setText(this.distributionListService.getMembersString(this.distributionListModel));
            actionBarSubtitleTextView.setVisibility(View.VISIBLE);
            if (this.distributionListModel.isHidden()) {
                actionBarAvatarView.setVisibility(View.GONE);
                actionBarTitleTextView.setText(getString(R.string.threema_message_to, ""));
            } else {
                if (actionBarAvatarView.getAvatarView().isAttachedToWindow()) {
                    distributionListService.loadAvatarIntoImage(
                        distributionListModel.getId(),
                        actionBarAvatarView.getAvatarView(),
                        AvatarOptions.PRESET_DEFAULT_AVATAR_NO_CACHE,
                        Glide.with(requireActivity())
                    );
                }
            }
            actionBarAvatarView.setIdentityTypeBadgeVisible(false);
            setAvatarContentDescription(R.string.distribution_list);
        } else {
            if (contactModel != null) {
                this.actionBarSubtitleImageView.setVerificationLevel(
                    contactModel.verificationLevel,
                    contactModel.getWorkVerificationLevel()
                );
                this.actionBarSubtitleImageView.setVisibility(View.VISIBLE);
                if (actionBarAvatarView.getAvatarView().isAttachedToWindow()) {
                    contactService.loadAvatarIntoImage(
                        contactModel.getIdentity(),
                        this.actionBarAvatarView.getAvatarView(),
                        AvatarOptions.PRESET_DEFAULT_FALLBACK,
                        Glide.with(requireActivity())
                    );
                }
                this.actionBarAvatarView.setIdentityTypeBadgeVisible(contactService.showIdentityTypeBadge(contactModel));
            }
            setAvatarContentDescription(R.string.prefs_header_chat);
        }
        this.actionBarTitleTextView.invalidate();
        this.actionBarSubtitleTextView.invalidate();
        this.actionBarSubtitleImageView.invalidate();
    }

    private void setAvatarContentDescription(@StringRes int stringRes) {
        try {
            actionBarAvatarView.setContentDescription(getString(stringRes));
        } catch (IllegalStateException e) {
            logger.error("Can't set content description", e);
        }
    }

    @Override
    @SuppressLint("StaticFieldLeak")
    public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
        inflater.inflate(R.menu.fragment_compose_message, menu);
        this.setupToolbar();

        super.onCreateOptionsMenu(menu, inflater);

        ConfigUtils.addIconsToOverflowMenu(menu);
    }

    @Override
    @Deprecated
    public void onPrepareOptionsMenu(@NonNull Menu menu) {
        this.callItem = menu.findItem(R.id.menu_threema_call);
        this.deleteDistributionListItem = menu.findItem(R.id.menu_delete_distribution_list);
        this.mutedMenuItem = menu.findItem(R.id.menu_muted);
        this.blockMenuItem = menu.findItem(R.id.menu_block_contact);
        this.showOpenPollWindowMenuItem = menu.findItem(R.id.menu_poll_window_show);
        this.showPollsMenuItem = menu.findItem(R.id.menu_poll_show_all);
        this.showEmptyChatMenuItem = menu.findItem(R.id.menu_empty_chat);

        // initialize menus
        updateMenus();
        updateMuteMenu();

        // initialize various toolbar items
        this.updateToolbarTitle();
    }

    @SuppressLint("StaticFieldLeak")
    private void updateMenus() {
        logger.debug("updateMenus");

        if (
            callItem == null
                || deleteDistributionListItem == null
                || mutedMenuItem == null
                || blockMenuItem == null
                || showOpenPollWindowMenuItem == null
                || showEmptyChatMenuItem == null
                || !isAdded()
        ) {
            return;
        }

        this.deleteDistributionListItem.setVisible(this.isDistributionListChat);
        this.mutedMenuItem.setVisible(!this.isDistributionListChat && !isNotesGroupChat());
        updateMuteMenu();

        if (contactModel != null) {
            this.blockMenuItem.setVisible(true);
            updateBlockMenu();
            contactTypingStateChanged(typingIndicatorProvider.isTypingWithIdentityString(contactModel.getIdentity()));
        } else {
            this.blockMenuItem.setVisible(false);
        }

        if (PollUtil.canVote(messageReceiver)) {
            new LifecycleAwareAsyncTask<Void, Long>() {
                @Override
                protected Long doInBackground(Void params) {
                    return pollService.countPolls(new PollService.PollFilter() {
                        @Override
                        public MessageReceiver getReceiver() {
                            return messageReceiver;
                        }

                        @Override
                        public PollModel.State[] getStates() {
                            return new PollModel.State[]{PollModel.State.OPEN};
                        }

                        @Override
                        public String createdOrNotVotedByIdentity() {
                            return userService.getIdentity();
                        }
                    });
                }

                @Override
                protected void onPostExecute(Long openBallots) {
                    showOpenPollWindowMenuItem.setVisible(openBallots > 0L);

                    if (preferenceService.getPollOverviewHidden()) {
                        showOpenPollWindowMenuItem.setIcon(R.drawable.ic_visibility);
                        showOpenPollWindowMenuItem.setTitle(R.string.ballot_window_show);
                    } else {
                        showOpenPollWindowMenuItem.setIcon(R.drawable.ic_visibility_off);
                        showOpenPollWindowMenuItem.setTitle(R.string.ballot_window_hide);
                    }
                    Context context = getContext();
                    if (context != null) {
                        ConfigUtils.tintMenuIcon(context, showOpenPollWindowMenuItem, R.attr.colorOnSurface);
                    }
                }
            }.execute(this, null);
        } else {
            showOpenPollWindowMenuItem.setVisible(false);
        }

        new LifecycleAwareAsyncTask<Void, Long>() {
            @Override
            protected Long doInBackground(Void params) {
                return pollService.countPolls(new PollService.PollFilter() {
                    @Override
                    public MessageReceiver getReceiver() {
                        return messageReceiver;
                    }

                    @Override
                    public PollModel.State[] getStates() {
                        return new PollModel.State[]{PollModel.State.OPEN, PollModel.State.CLOSED};
                    }
                });
            }

            @Override
            protected void onPostExecute(Long hasBallots) {
                showPollsMenuItem.setVisible(hasBallots > 0L);
            }
        }.execute(this, null);

        // Show "empty chat" only if chat is not empty
        this.showEmptyChatMenuItem.setVisible(composeMessageAdapter != null && !composeMessageAdapter.isEmpty());

        updateVoipCallMenuItem(null);
    }

    private boolean isNotesGroupChat() {
        return isGroupChat && groupModel != null && Boolean.TRUE.equals(groupModel.isNotesGroup());
    }

    @UiThread
    private void updateMuteMenu() {
        if (!isAdded() || this.mutedMenuItem == null) {
            // do not update if no longer attached to activity
            return;
        }
        if (isMentionsOnly()) {
            this.mutedMenuItem.setIcon(R.drawable.ic_dnd_mention_grey600_24dp);
            this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
        } else if (isMuted()) {
            this.mutedMenuItem.setIcon(R.drawable.ic_dnd);
            this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
        } else if (isSilent()) {
            this.mutedMenuItem.setIcon(R.drawable.ic_notifications_off_outline);
            this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
        } else {
            this.mutedMenuItem.setIcon(R.drawable.ic_notifications_active_outline);
            this.mutedMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
        }
    }

    private void updateBlockMenu() {
        if (!isAdded()) {
            // do not update if no longer attached to activity
            return;
        }
        if (blockMenuItem != null && blockedIdentitiesService != null && contactModel != null) {
            boolean hasBlockedThisIdentity = this.blockedIdentitiesService.isBlocked(this.contactModel.getIdentity());
            this.blockMenuItem.setTitle(hasBlockedThisIdentity ? getString(R.string.unblock_contact) : getString(R.string.block_contact));
            this.blockMenuItem.setShowAsAction(hasBlockedThisIdentity ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
            this.mutedMenuItem.setShowAsAction(hasBlockedThisIdentity ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_IF_ROOM);
            this.mutedMenuItem.setVisible(!hasBlockedThisIdentity);

            this.callItem.setShowAsAction(hasBlockedThisIdentity ? MenuItem.SHOW_AS_ACTION_NEVER : MenuItem.SHOW_AS_ACTION_ALWAYS);

            updateVoipCallMenuItem(!hasBlockedThisIdentity);
        }
    }

    @AnyThread
    private void updateVoipCallMenuItem(@Nullable final Boolean showVoipCallMenuItem) {
        RuntimeUtil.runOnUiThread(() -> {
            if (isGroupChat) {
                updateGroupCallMenuItem();
            } else if (callItem != null) {
                if (ContactUtil.canReceiveVoipMessages(contactModel, blockedIdentitiesService) && ConfigUtils.isCallsEnabled()) {
                    boolean isVoipStateIdle = voipStateService.getCallState().isIdle();
                    logger.debug("updateVoipMenu showVoipCallMenuItem: {}", showVoipCallMenuItem);
                    logger.debug("updateVoipMenu isVoipStateIdle: {}", isVoipStateIdle);
                    callItem.setIcon(R.drawable.ic_phone_locked_outline);
                    callItem.setTitle(R.string.threema_call);
                    callItem.setVisible(showVoipCallMenuItem != null ? showVoipCallMenuItem : isVoipStateIdle);
                } else {
                    callItem.setVisible(false);
                }
            }
        });
    }

    @UiThread
    private void updateGroupCallMenuItem() {
        if (groupModel == null) {
            logger.warn("Group model is null");
            return;
        }
        if (groupService == null) {
            logger.warn("Group service is null");
            return;
        }

        if (isGroupChat && callItem != null) {
            GroupModelOld legacyGroupModel = groupService.getByGroupIdentity(groupModel.getGroupIdentity());

            if (legacyGroupModel != null && GroupCallUtil.qualifiesForGroupCalls(groupService, legacyGroupModel)) {
                GroupCallDescription call = groupCallManager.getCurrentChosenCall(groupModel);
                callItem.setIcon(R.drawable.ic_phone_locked_outline);
                callItem.setTitle(R.string.group_call);
                callItem.setVisible(call == null);
            } else {
                callItem.setVisible(false);
            }
        }
    }

    private void attachCamera() {
        Intent previewIntent = IntentDataUtil.addMessageReceiversToIntent(new Intent(activity, SendMediaActivity.class), new MessageReceiver[]{this.messageReceiver});
        if (this.actionBarTitleTextView != null && this.actionBarTitleTextView.getText() != null) {
            previewIntent.putExtra(AppConstants.INTENT_DATA_TEXT, this.actionBarTitleTextView.getText().toString());
        }
        previewIntent.putExtra(AppConstants.INTENT_DATA_PICK_FROM_CAMERA, true);
        activity.startActivityForResult(previewIntent, ThreemaActivity.ACTIVITY_ID_SEND_MEDIA);
    }

    private void showPermissionRationale(int stringResource) {
        ConfigUtils.showPermissionRationale(getContext(), coordinatorLayout, stringResource);
    }

    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        final int id = item.getItemId();
        if (id == android.R.id.home) {
            if (activity != null && activity.getIntent() != null && activity.getIntent().hasExtra(EXTRA_OVERRIDE_BACK_TO_HOME_BEHAVIOR)) {
                activity.getOnBackPressedDispatcher().onBackPressed();
                return true;
            } else {
                logger.info("back button clicked, closing chat");
                NavigationUtil.navigateUpToHome(activity);
            }
        } else if (id == R.id.menu_search_messages) {
            logger.info("Search button clicked");
            searchActionMode = activity.startSupportActionMode(new SearchActionMode());
        } else if (id == R.id.menu_gallery) {
            logger.info("Gallery button clicked");
            final @NonNull Intent mediaGalleryIntent = new Intent(activity, MediaGalleryActivity.class);
            if (this.messageReceiver != null) {
                switch (messageReceiver.getType()) {
                    case MessageReceiver.Type_GROUP:
                        mediaGalleryIntent.putExtra(AppConstants.INTENT_DATA_GROUP_DATABASE_ID, groupDatabaseId);
                        break;
                    case MessageReceiver.Type_DISTRIBUTION_LIST:
                        mediaGalleryIntent.putExtra(AppConstants.INTENT_DATA_DISTRIBUTION_LIST_ID, distributionListModel.getId());
                        break;
                    case MessageReceiver.Type_CONTACT:
                    default:
                        mediaGalleryIntent.putExtra(AppConstants.INTENT_DATA_CONTACT, identity);
                }
                activity.startActivity(mediaGalleryIntent);
            }
        } else if (id == R.id.menu_threema_call) {
            logger.info("Call button clicked");
            initiateCall();
        } else if (id == R.id.menu_wallpaper) {
            logger.info("Wallpaper button clicked");
            wallpaperService.selectWallpaper(
                this,
                this.wallpaperLauncher,
                this.conversationId,
                () -> RuntimeUtil.runOnUiThread(this::setBackgroundWallpaper)
            );
        } else if (id == R.id.menu_muted) {
            logger.info("Muting button clicked");
            if (!isDistributionListChat) {
                Intent intent;
                int[] location = new int[2];

                String conversationName = null;
                if (messageReceiver != null) {
                    conversationName = messageReceiver.getDisplayName(preferenceService.getContactNameFormat());
                }
                if (isGroupChat) {
                    intent = GroupNotificationsActivity.createIntent(activity, groupDatabaseId, conversationName);
                } else {
                    intent = ContactNotificationsActivity.createIntent(activity, identity, conversationName);
                }

                if (ToolbarUtil.getMenuItemCenterPosition(activity.getToolbar(), R.id.menu_muted, location)) {
                    intent.putExtra((AppConstants.INTENT_DATA_ANIM_CENTER), location);
                }
                activity.startActivity(intent);
            }
        } else if (id == R.id.menu_block_contact) {
            if (this.blockedIdentitiesService.isBlocked(contactModel.getIdentity())) {
                logger.info("Unblock button clicked");
                this.blockedIdentitiesService.unblockIdentity(contactModel.getIdentity(), getContext());
                updateBlockMenu();
            } else {
                logger.info("Block button clicked");
                GenericAlertDialog
                    .newInstance(R.string.block_contact, R.string.really_block_contact, R.string.yes, R.string.no)
                    .setTargetFragment(this)
                    .show(getFragmentManager(), DIALOG_TAG_CONFIRM_BLOCK);
            }
        } else if (id == R.id.menu_delete_distribution_list) {
            logger.info("Delete distribution list button clicked, showing dialog");
            GenericAlertDialog.newInstance(R.string.really_delete_distribution_list,
                    R.string.really_delete_distribution_list_message,
                    R.string.ok,
                    R.string.cancel)
                .setTargetFragment(this)
                .setData(distributionListModel)
                .show(getFragmentManager(), CONFIRM_TAG_DELETE_DISTRIBUTION_LIST);
        } else if (id == R.id.menu_shortcut) {
            logger.info("Create shortcut button clicked");
            createShortcut();
        } else if (id == R.id.menu_empty_chat) {
            logger.info("Empty chat button clicked, showing dialog");
            GenericAlertDialog.newInstance(R.string.empty_chat_title,
                    R.string.empty_chat_confirm,
                    R.string.ok,
                    R.string.cancel)
                .setTargetFragment(this)
                .show(getFragmentManager(), DIALOG_TAG_EMPTY_CHAT);
        } else if (id == R.id.menu_poll_window_show) {
            toggleOpenPollNoticeViewVisibility();
        } else if (id == R.id.menu_poll_show_all) {
            logger.info("Show poll overview button clicked");
            Intent intent = new Intent(getContext(), PollOverviewActivity.class);
            IntentDataUtil.addMessageReceiverToIntent(intent, messageReceiver);
            startActivity(intent);
        }
        return false;
    }

    private void toggleOpenPollNoticeViewVisibility() {
        if (openPollNoticeView.isShown()) {
            preferenceService.setPollOverviewHidden(true);
            openPollNoticeView.hide(true);
        } else {
            preferenceService.setPollOverviewHidden(false);
            openPollNoticeView.show(true);
        }
    }

    private void initiateCall() {
        if (isGroupChat) {
            GroupModelOld legacyGroupModel = groupService.getByGroupIdentity(groupModel.getGroupIdentity());
            if (legacyGroupModel != null) {
                GroupCallUtil.initiateCall(activity, legacyGroupModel);
            } else {
                logger.error("Could not get legacy group model to initiate the group call");
            }
        } else {
            VoipUtil.initiateCall(activity, contactModel, false, null);
        }
    }

    private void emptyChat() {
        if (messageReceiver != null) {
            logger.info("Empty chat with receiver {} (type={}).", messageReceiver.getConversationId(), messageReceiver.getType());
        } else {
            logger.warn("Cannot empty chat, messageReceiver is null.");
        }
        new EmptyOrDeleteConversationsAsyncTask(
            EmptyOrDeleteConversationsUseCase.Mode.EMPTY,
            new MessageReceiver[]{messageReceiver},
            conversationService,
            distributionListService,
            groupModelRepository,
            groupFlowDispatcher,
            userService.getIdentity(),
            getParentFragmentManager(),
            null,
            () -> {
                if (isAdded() && messageReceiver != null) {
                    synchronized (messageValues) {
                        messageValues.clear();
                        composeMessageAdapter.notifyDataSetChanged();
                    }

                    draftManager.remove(messageReceiver.getConversationId());
                    messageText.setText(null);

                    setCurrentPageReferenceId(null);
                    onRefresh();

                    if (!isGroupChat) {
                        conversationService.reset();
                    }
                    globalEventBuses.getConversations().emit(ConversationEvent.AllConversationsUpdated.INSTANCE);
                }
            }).execute();
    }

    private void createShortcut() {
        if (!this.isGroupChat &&
            !this.isDistributionListChat &&
            ContactUtil.canReceiveVoipMessages(contactModel, blockedIdentitiesService) &&
            ConfigUtils.isCallsEnabled()) {
            ArrayList<SelectorDialogItem> items = new ArrayList<>();
            items.add(new SelectorDialogItem(getString(R.string.prefs_header_chat), R.drawable.ic_outline_chat_bubble_outline));
            items.add(new SelectorDialogItem(getString(R.string.threema_call), R.drawable.ic_call_outline));
            SelectorDialog selectorDialog = SelectorDialog.newInstance(
                getString(R.string.shortcut_choice_title),
                items,
                getString(R.string.cancel),
                (String) null
            );
            selectorDialog.setTargetFragment(this, 0);
            selectorDialog.show(getFragmentManager(), DIALOG_TAG_CHOOSE_SHORTCUT_TYPE);
        } else {
            RuntimeUtil.runOnWorkerThread(
                () -> ShortcutUtil.createPinnedShortcut(messageReceiver, TYPE_CHAT, preferenceService.getContactNameFormat())
            );
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode,
                                 final Intent intent) {
        if (requestCode == ACTIVITY_ID_VOICE_RECORDER) {
            if (this.messagePlayerService != null) {
                logger.info("Voice recording received for attaching");
                this.messagePlayerService.resumeAll(getActivity(), messageReceiver, SOURCE_AUDIORECORDER);
            }
        }
        if (requestCode == ThreemaActivity.ACTIVITY_ID_ATTACH_MEDIA) {
            restoreMessageDraft(true);
            if (resultCode == Activity.RESULT_OK) {
                logger.info("Media file(s) received for attaching");
                this.lastMediaFilter = IntentDataUtil.getLastMediaFilterFromIntent(intent);
            }
        }
    }

    private final SearchView.OnQueryTextListener queryTextListener = new SearchView.OnQueryTextListener() {
        // listener for search bar on top
        @Override
        public boolean onQueryTextChange(String newText) {
            composeMessageAdapter.getFilter().filter(newText);
            return true;
        }

        @Override
        public boolean onQueryTextSubmit(String query) {
            composeMessageAdapter.nextMatchPosition();
            return true;
        }
    };

    @Override
    public void onClick(String tag, int which, Object data) {
        if (DIALOG_TAG_CHOOSE_SHORTCUT_TYPE.equals(tag)) {
            logger.info("Creating shortcut");
            final int shortcutType = which + 1;
            RuntimeUtil.runOnWorkerThread(
                () -> ShortcutUtil.createPinnedShortcut(messageReceiver, shortcutType, preferenceService.getContactNameFormat())
            );
        }
    }

    @Override
    public void onCancel(String tag) {
    }

    @Override
    public void onNo(String tag) {
    }

    //region VoipStatusDataChatListener
    @Override
    public void showDialog(String name) {
        GenericAlertDialog dialog = GenericAlertDialog.newInstance(
            R.string.threema_call,
            String.format(getContext().getString(R.string.voip_call_confirm), name),
            R.string.ok,
            R.string.cancel
        );
        dialog.setTargetFragment(this, 0);
        dialog.show(getFragmentManager(), ComposeMessageFragment.DIALOG_TAG_CONFIRM_CALL);
    }
    //endregion

    //region PollChatListener
    @Override
    public void showSelectorDialog(
        ArrayList<Integer> action,
        String title,
        ArrayList<SelectorDialogItem> items,
        PollModel pollModel
    ) {
        SelectorDialog selectorDialog = SelectorDialog.newInstance(title, items, null, new SelectorDialog.SelectorDialogInlineClickListener() {
            @Override
            public void onClick(String tag, int which, Object data) {
                switch (action.get(which)) {
                    case ACTION_VOTE:
                        PollUtil.openVoteDialog(getFragmentManager(), pollModel);
                        break;
                    case ACTION_RESULTS:
                        PollUtil.openMatrixActivity(getContext(), pollModel);
                        break;
                    case ACTION_CLOSE:
                        PollUtil.requestClosePoll(pollModel, ComposeMessageFragment.this, null);
                        break;
                    default:
                        break;
                }
            }

            @Override
            public int describeContents() {
                return 0;
            }

            @Override
            public void writeToParcel(Parcel dest, int flags) {
            }
        });
        selectorDialog.show(getFragmentManager(), "chooseAction");
    }

    @Override
    public void onLinkNeedsConfirmation(@NonNull String warning, @NonNull Uri uri) {
        GenericAlertDialog dialog = GenericAlertDialog.newInstance(R.string.url_warning_title, warning, R.string.ok, R.string.cancel);
        dialog.setData(uri);
        dialog.setTargetFragment(this, 0);
        dialog.show(getFragmentManager(), DIALOG_TAG_CONFIRM_LINK);
    }

    @Override
    public void showBottomSheetGridDialog(ArrayList<BottomSheetItem> items) {
        BottomSheetGridDialog dialog = BottomSheetGridDialog.newInstance(R.string.add_contact_in, items);
        dialog.setCallback((tag, data) -> LinkifyUtil.launchAddContactActivity(requireContext(), tag, data));
        dialog.show(getParentFragmentManager(), "bsh");
    }

    @Override
    public @NonNull MessagePlayer create(
        @NonNull AbstractMessageModel messageModel,
        @Nullable ListenableFuture<MediaController> mediaControllerFuture
    ) {
        return messagePlayerService.createPlayer(
            messageModel,
            new WeakReference<>(activity),
            messageReceiver,
            mediaControllerFuture
        );
    }

    @Override
    public void showPrepareDownloadDialog(Runnable onConfirmed) {
        new MaterialAlertDialogBuilder(requireContext())
            .setTitle(R.string.download)
            .setMessage(R.string.send_as_files_warning)
            .setNegativeButton(R.string.cancel, null)
            .setPositiveButton(R.string.ok, (dialog, id) -> onConfirmed.run())
            .show();
    }

    @Override
    public void interact() {
        ActivityService.activityUserInteract(activity);
    }

    @Override
    public void openDefaultActivity(PollModel pollModel, boolean canVote) {
        PollUtil.openDefaultActivity(getContext(), getFragmentManager(), pollModel, canVote);
    }
    //endregion

    public class ComposeMessageAction implements ActionMode.Callback {
        private final int position;
        private MenuItem quoteItem, forwardItem, saveItem, copyItem, shareItem, infoItem, editItem, starItem, unStarItem, imageReplyItem, deleteItem;

        ComposeMessageAction(int position) {
            this.position = position;
            longClickItem = position;
        }

        private void updateActionMenu(Menu menu) {
            boolean isSingleMessage = selectedMessages.size() == 1;
            boolean isQuotable = isSingleMessage;
            boolean canShowInfo = isSingleMessage;
            boolean isForwardable = selectedMessages.size() <= MAX_FORWARDABLE_ITEMS;
            boolean isSaveable = !dependencies.getAppRestrictions().isShareMediaDisabled();
            boolean isCopyable = true;
            boolean isShareable = !dependencies.getAppRestrictions().isShareMediaDisabled();
            boolean isEditable = isSingleMessage
                && MessageUtilKt.canBeEdited(selectedMessages.get(0), isNotesGroupChat())
                && !isGroupChatWhereUserIsNotMemberOf();
            boolean canSendImageReply = isSingleMessage && MessageUtil.canSendImageReply(selectedMessages.get(0));
            boolean canStarMessage = isSingleMessage && MessageUtil.canStarMessage(selectedMessages.get(0));

            if (selectedMessages.stream().anyMatch(AbstractMessageModel::isDeleted)) {
                if (isSingleMessage) {
                    onlyShowItems(menu, R.id.menu_message_discard, R.id.menu_info);
                } else {
                    onlyShowItems(menu, R.id.menu_message_discard);
                }
                return;
            }

            for (AbstractMessageModel message : selectedMessages) {
                if (message == null) continue;
                isQuotable = isQuotable && isQuotable(message);
                isForwardable = isForwardable && isForwardable(message);
                isSaveable = isSaveable && isSaveable(message);
                isCopyable = isCopyable && isCopyable(message);
                isShareable = isShareable && isShareable(message);
            }

            // Sharing text message is only possible when there is exactly one selected message
            isShareable = isShareable && (isSingleMessage || !containsTextMessage(selectedMessages));

            quoteItem.setVisible(isQuotable);
            infoItem.setVisible(canShowInfo);
            forwardItem.setVisible(isForwardable);
            saveItem.setVisible(isSaveable);
            copyItem.setVisible(isCopyable);
            shareItem.setVisible(isShareable);
            editItem.setVisible(isEditable);
            imageReplyItem.setVisible(canSendImageReply);

            boolean isMessageCurrentlyStarred = !selectedMessages.isEmpty()
                && (selectedMessages.get(0).getDisplayTags() & DisplayTag.DISPLAY_TAG_STARRED) == DisplayTag.DISPLAY_TAG_STARRED;
            starItem.setVisible(canStarMessage && !isMessageCurrentlyStarred);
            unStarItem.setVisible(canStarMessage && isMessageCurrentlyStarred);

            deleteItem.setShowAsAction(isSingleMessage ? MenuItem.SHOW_AS_ACTION_IF_ROOM : MenuItem.SHOW_AS_ACTION_ALWAYS);
        }

        private void onlyShowItems(Menu menu, int... ids) {
            for (int i = 0; i < menu.size(); i++) {
                MenuItem item = menu.getItem(i);
                boolean show = Arrays.stream(ids).anyMatch(id -> id == item.getItemId());
                item.setVisible(show);
            }
        }

        private boolean isQuotable(@NonNull AbstractMessageModel message) {
            if (messageReceiver == null) {
                return false;
            }
            boolean isValidReceiver = messageReceiver.validateSendingPermission().isValid();
            return isValidReceiver && QuoteUtil.isQuoteable(message);
        }

        private boolean isForwardable(@NonNull AbstractMessageModel message) {
            return message.isAvailable()                                // if the media is downloaded
                && !message.isStatusMessage()                           // and the message is not status message (unread or status)
                && message.getType() != MessageType.POLL                // and not a poll
                && message.getType() != MessageType.VOIP_STATUS        // and not a voip status
                && message.getType() != MessageType.GROUP_CALL_STATUS;    // and not a group call status
        }

        private boolean isSaveable(@NonNull AbstractMessageModel message) {
            return message.isAvailable() && message.getType() == MessageType.FILE;
        }

        private boolean isShareable(@NonNull AbstractMessageModel message) {
            return message.isAvailable()
                && (message.getType() == MessageType.FILE || message.getType() == MessageType.TEXT);
        }

        private boolean isCopyable(@NonNull AbstractMessageModel message) {
            boolean isText = message.getType() == MessageType.TEXT && !message.isStatusMessage();
            boolean isFileWithCaption = message.getType() == MessageType.FILE
                && !TextUtils.isEmpty(message.getCaption());
            return isText || isFileWithCaption; // is text (not status) or a file with non-empty caption
        }

        private boolean containsTextMessage(@NonNull List<AbstractMessageModel> messages) {
            for (AbstractMessageModel message : messages) {
                if (message.getType() == MessageType.TEXT) {
                    return true;
                }
            }
            return false;
        }

        private boolean isMultiDeviceActive() {
            return multiDeviceManager.isMultiDeviceActive();
        }

        private boolean isDeletableRemotely(AbstractMessageModel message) {
            // check receiver support
            if (messageReceiver instanceof GroupMessageReceiver) {
                GroupModel groupModel = ((GroupMessageReceiver) messageReceiver).getGroupModel();
                if (groupModel == null || !groupModel.isMember()) {
                    return false;
                } else if (Boolean.TRUE.equals(groupModel.isNotesGroup()) && !isMultiDeviceActive()) {
                    // Notes group: remote delete does not make sense if multi device is not active
                    return false;
                } else if (Boolean.FALSE.equals(groupModel.isNotesGroup())) {
                    // Checking feature support only makes sense if it is not a notes group
                    GroupModelData groupModelData = groupModel.getData();
                    if (groupModelData == null) {
                        return false;
                    }
                    GroupFeatureSupport featureSupport = groupService.getFeatureSupport(groupModelData, ThreemaFeature.DELETE_MESSAGES);
                    if (featureSupport.getAdoptionRate() == GroupFeatureAdoptionRate.NONE) {
                        // no feature support in group
                        return false;
                    }
                }
            } else if (messageReceiver instanceof ContactMessageReceiver
                && !ThreemaFeature.canDeleteMessages(((ContactMessageReceiver) messageReceiver).getContact().getFeatureMask())
            ) {
                // no feature support in 1:1 chat
                return false;
            }
            // check message support
            return canDeleteRemotely(message, messageReceiver);
        }

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            if (this.position == AbsListView.INVALID_POSITION) {
                return false;
            }

            if (convListView.getCheckedItemCount() < 1) {
                return false;
            }

            MenuInflater inflater = mode.getMenuInflater();
            if (inflater != null) {
                inflater.inflate(R.menu.action_compose_message, menu);
            }

            ConfigUtils.addIconsToOverflowMenu(menu);

            forwardItem = menu.findItem(R.id.menu_message_forward);
            saveItem = menu.findItem(R.id.menu_message_save);
            copyItem = menu.findItem(R.id.menu_message_copy);
            shareItem = menu.findItem(R.id.menu_share);
            quoteItem = menu.findItem(R.id.menu_message_quote);
            infoItem = menu.findItem(R.id.menu_info);
            editItem = menu.findItem(R.id.menu_message_edit);
            starItem = menu.findItem(R.id.menu_message_star);
            unStarItem = menu.findItem(R.id.menu_message_unstar);
            imageReplyItem = menu.findItem(R.id.menu_message_image_reply);
            deleteItem = menu.findItem(R.id.menu_message_discard);

            updateActionMenu(menu);

            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            final int checked = convListView.getCheckedItemCount();

            mode.setTitle(Integer.toString(checked));
            updateActionMenu(menu);

            return true;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            if (selectedMessages.isEmpty()) {
                mode.finish();
                return true;
            }

            final int id = item.getItemId();
            if (id == R.id.menu_message_copy) {
                logger.info("Action menu: copy clicked");
                copySelectedMessagesToClipboard();
                mode.finish();
            } else if (id == R.id.menu_message_discard) {
                if (selectedMessages.size() == 1 && isDeletableRemotely(selectedMessages.get(0))) {
                    logger.info("Action menu: delete message for all clicked");
                    showDeleteMessagesForAllDialog(selectedMessages.get(0));
                } else {
                    logger.info("Action menu: delete message(s) locally clicked");
                    showDeleteMessagesLocallyDialog();
                }
            } else if (id == R.id.menu_message_forward) {
                logger.info("Action menu: forward message clicked");
                startForwardMessage();
                mode.finish();
            } else if (id == R.id.menu_message_save) {
                logger.info("Action menu: save media clicked");
                if (ConfigUtils.requestWriteStoragePermissions(activity, ComposeMessageFragment.this, PERMISSION_REQUEST_SAVE_MESSAGE)) {
                    saveSelectedMediaFiles();
                }
                mode.finish();
            } else if (id == R.id.menu_share) {
                logger.info("Action menu: share messages clicked");
                shareMessages();
                mode.finish();
            } else if (id == R.id.menu_message_quote) {
                logger.info("Action menu: quote clicked");
                showQuotePopup(null, true);
                mode.finish();
            } else if (id == R.id.menu_info) {
                logger.info("Action menu: show message details clicked");
                showMessageDetailScreen(selectedMessages.get(0));
                mode.finish();
            } else if (id == R.id.menu_message_star || id == R.id.menu_message_unstar) {
                logger.info("Action menu: (un)star clicked");
                toggleStar(selectedMessages.get(0));
                mode.finish();
            } else if (id == R.id.menu_message_edit) {
                logger.info("Action menu: edit clicked");
                tryEditingSelectedMessage();
                mode.finish();
            } else if (id == R.id.menu_message_image_reply) {
                logger.info("Action menu: reply clicked");
                sendImageReply();
                mode.finish();
            } else {
                return false;
            }
            return true;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            actionMode = null;
            longClickItem = AbsListView.INVALID_POSITION;

            // handle done button
            convListView.clearChoices();
            convListView.requestLayout();
            convListView.post(() -> convListView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE));

            if (emojiReactionsPopup != null) {
                emojiReactionsPopup.dismiss();
            }

            // If the action mode has been left without clearing up the selected messages, we need
            // to trigger a refresh so that linkified links work again (selectedMessages will be cleared lazily)
            if (!selectedMessages.isEmpty() && composeMessageAdapter != null) {
                composeMessageAdapter.notifyDataSetChanged();
            }
        }
    }

    private void setMessageTextMaxLength(int max) {
        var filters = messageText.getFilters();
        for (int i = 0; i < filters.length; i++) {
            if (filters[i] instanceof InputFilter.LengthFilter) {
                filters[i] = new InputFilter.LengthFilter(max);
                // We need to re-apply the filters after modifying the array, otherwise the EditText won't pick up the new filter
                messageText.setFilters(filters);
                break;
            }
        }
    }

    private class EditMessageActionMode implements ActionMode.Callback {

        private final AbstractMessageModel messageModel;
        private boolean shouldRestoreQuotePanel;

        private final TextWatcher onEditMessageTextChangedListener = new SimpleTextWatcher() {
            @Override
            public void onTextChanged(@NonNull CharSequence charSequence, int i, int i1, int i2) {
                updateSendEditMessageButton(getEditableText(messageModel), charSequence.toString());
            }
        };

        public EditMessageActionMode(AbstractMessageModel messageModel) {
            this.messageModel = messageModel;
        }

        @SuppressLint("ClickableViewAccessibility")
        @Override
        public boolean onCreateActionMode(@NonNull ActionMode mode, Menu menu) {
            MenuInflater inflater = mode.getMenuInflater();
            inflater.inflate(R.menu.action_edit_message, menu);

            mode.setTitle(R.string.edit_message);

            if (typingIndicatorTextWatcher != null) {
                messageText.removeTextChangedListener(typingIndicatorTextWatcher);
                typingIndicatorTextWatcher.stopSending();
            }
            if (draftUpdateTextWatcher != null) {
                messageText.removeTextChangedListener(draftUpdateTextWatcher);
            }

            updateMessageDraft();

            messageText.setText(getEditableText(messageModel));

            EditTextUtil.focusWindowAndShowSoftKeyboard(messageText);

            final @Nullable Editable messageTextEditable = messageText.getText();
            if (messageTextEditable != null) {
                messageText.setSelection(messageTextEditable.toString().length());
            }

            if (actionMode != null) {
                actionMode.finish();
            }

            ComposeJavaBridge.INSTANCE.setEditModeMessageBubble(
                editMessageBubbleComposeView,
                messageModel,
                preferenceService.getContactNameFormat(),
                ComposeMessageFragment.this
            );
            editMessageBubbleContainer.setVisibility(View.VISIBLE);

            if (isQuotePopupShown()) {
                shouldRestoreQuotePanel = true;
                dismissQuotePopup();
            }

            sendButton.setVisibility(View.GONE);
            attachButton.setVisibility(View.GONE);
            cameraButton.setVisibility(View.GONE);
            sendEditMessageButton.setVisibility(View.VISIBLE);

            dimBackground.setAlpha(0f);
            dimBackground.setVisibility(View.VISIBLE);
            dimBackground.animate().alpha(1f).setDuration(300).setListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(@NonNull Animator animation) {
                }

                @Override
                public void onAnimationEnd(@NonNull Animator animation) {
                    setupEditMessageTextActionListener(messageModel);
                    setupEditMessageButtonClickListener(messageModel);
                    messageText.addTextChangedListener(onEditMessageTextChangedListener);
                    final @Nullable Editable messageTextEditable = messageText.getText();
                    updateSendEditMessageButton(
                        getEditableText(messageModel),
                        (messageTextEditable != null) ? messageTextEditable.toString() : null
                    );
                }

                @Override
                public void onAnimationCancel(@NonNull Animator animation) {
                    if (editMessageActionMode != null) {
                        editMessageActionMode.finish();
                    }
                }

                @Override
                public void onAnimationRepeat(@NonNull Animator animation) {
                }
            });

            // usually messages are split into multiple messages if they are too long, but this is not possible when editing a message
            setMessageTextMaxLength(ProtocolDefines.MAX_TEXT_MESSAGE_LEN);

            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        private void setupEditMessageTextActionListener(@NonNull AbstractMessageModel messageModel) {
            messageText.setOnEditorActionListener(
                setupTextActionListener(
                    () -> {
                        final @Nullable Editable editedMessageTextEditable = messageText.getText();
                        if (editedMessageTextEditable == null) {
                            return;
                        }
                        final @Nullable String original = getEditableText(messageModel);
                        final @NonNull String edited = editedMessageTextEditable.toString();
                        if (canSendEditMessage(original, edited)) {
                            onSendEditMessage(messageModel, edited);
                        }
                    }
                )
            );
        }

        @SuppressLint("ClickableViewAccessibility")
        @Override
        public void onDestroyActionMode(ActionMode mode) {
            if (conversationId != null) {
                // Restore a potential message draft from before editing
                final @Nullable MessageDraft messageDraft = draftManager.get(conversationId);
                if (messageDraft != null) {
                    messageText.setText(messageDraft.getText());
                } else {
                    messageText.setText(null);
                }
                // Move the cursor to the end
                final @Nullable Editable messageTextEditable = messageText.getText();
                if (messageTextEditable != null) {
                    messageText.setSelection(messageTextEditable.toString().length());
                }
            }

            editMessageBubbleContainer.setVisibility(View.GONE);
            editMessageBubbleComposeView.disposeComposition();

            if (shouldRestoreQuotePanel) {
                showQuotePopup(messageModel, false);
            }

            sendEditMessageButton.setVisibility(View.GONE);

            messageText.removeTextChangedListener(onEditMessageTextChangedListener);

            setupSendMessageTextActionListener();

            dimBackground.animate().alpha(0f).setDuration(300).setListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(@NonNull Animator animation) {
                }

                @Override
                public void onAnimationEnd(@NonNull Animator animation) {
                    dimBackground.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationCancel(@NonNull Animator animation) {
                    dimBackground.setVisibility(View.GONE);
                }

                @Override
                public void onAnimationRepeat(@NonNull Animator animation) {
                }
            });

            // restore default max length so long messages can be split again
            int defaultEditTextMaxLength = getResources().getInteger(R.integer.message_edittext_max_length);
            setMessageTextMaxLength(defaultEditTextMaxLength);

            if (typingIndicatorTextWatcher != null) {
                messageText.addTextChangedListener(typingIndicatorTextWatcher);
            }
            if (draftUpdateTextWatcher != null) {
                messageText.addTextChangedListener(draftUpdateTextWatcher);
            }

            editMessageActionMode = null;

            sendButton.setVisibility(View.VISIBLE);
            attachButton.setVisibility(View.VISIBLE);
            updateCameraButton();
        }

        /**
         * Get the currently stored text for the given message model. For a text message, this is just
         * the message's text. In case of a file message, the caption is returned. For other message
         * types, null is returned.
         */
        @Nullable
        private String getEditableText(@Nullable AbstractMessageModel messageModel) {
            if (messageModel == null) {
                return null;
            }
            if (messageModel.getType() == MessageType.TEXT) {
                return messageModel.getBody();
            } else if (messageModel.getType() == MessageType.FILE) {
                return messageModel.getCaption();
            }
            return null;
        }
    }

    private void showMessageDetailScreen(AbstractMessageModel messageModel) {
        activity.startActivity(MessageDetailsActivity.createIntent(requireContext(), messageModel));
    }

    /**
     * Start the {@code ImagePaintActivity} to edit the (first) currently selected message.
     */
    private void sendImageReply() {
        final AbstractMessageModel messageModel = selectedMessages.get(0);
        if (messageModel == null || messageModel.getMessageContentsType() != MessageContentsType.IMAGE) {
            logger.error("Invalid message model: {}", messageModel);
            return;
        }
        new LoadDecryptedMessageFileAsyncTask(dependencies.getFileService()) {
            @Override
            public void onSuccess(File decryptedFile) {
                if (messageModel.isAvailable()) {
                    Uri uri = null;
                    if (decryptedFile != null) {
                        uri = Uri.fromFile(decryptedFile);
                    }
                    if (uri == null) {
                        logger.error("Uri is null");
                        return;
                    }

                    Context context = getContext();
                    if (context == null) {
                        logger.error("Context is null");
                        return;
                    }

                    MediaItem mediaItem = MediaItem.getFromUri(uri, getContext(), false);

                    File outputFile;
                    try {
                        outputFile = fileService.createTempFile(".imageReply", ".png");
                    } catch (IOException e) {
                        logger.error("Couldn't create temporary file", e);
                        return;
                    }


                    Intent imageReplyIntent = ImagePaintActivity.getImageReplyIntent(context, mediaItem, outputFile, messageReceiver, groupModel);
                    IntentDataUtil.addMessageReceiverToIntent(imageReplyIntent, messageReceiver);

                    imageReplyLauncher.launch(imageReplyIntent);
                }
            }

            @Override
            public void onError(@NonNull Exception e) {
                logger.error("Could not load message file", e);
                showToast(activity, R.string.an_error_occurred_during_send, ToastDuration.LONG);
            }
        }
            .execute(this, messageModel);
    }

    public boolean onBackPressed() {
        logger.info("onBackPressed");
        if (isEmojiPickerShown()) {
            // dismiss emoji keyboard if it's showing instead of leaving activity
            emojiPicker.hide();
            addAllInsetsToInsetPaddingContainer();
            return true;
        } else {
            if (messageText != null && messageText.isMentionPopupShowing()) {
                dismissMentionPopup();
                return true;
            }
            dismissQuotePopup();
            if (editMessageActionMode != null) {
                editMessageActionMode.finish();
                return true;
            }
            if (searchActionMode != null) {
                searchActionMode.finish();
                return true;
            }
            if (actionMode != null) {
                actionMode.finish();
                return true;
            } else if (ConfigUtils.isTabletLayout()) {
                if (actionBar != null) {
                    actionBar.setDisplayUseLogoEnabled(true);
                    actionBar.setDisplayShowCustomEnabled(false);
                }
            }
            return false;
        }
    }

    private void preserveListInstanceValues() {
        listInstancePosition = AbsListView.INVALID_POSITION;
        if (!isHidden()) {
            if (convListView != null && composeMessageAdapter != null) {
                if (convListView.getLastVisiblePosition() != composeMessageAdapter.getCount() - 1) {
                    listInstancePosition = convListView.getFirstVisiblePosition();
                    View v = convListView.getChildAt(0);
                    listInstanceTop = (v == null) ? 0 : (v.getTop() - convListView.getPaddingTop());
                    if (messageReceiver != null) {
                        listInstanceConversationIdObfuscated = messageReceiver.getConversationId().getObfuscated();
                    }
                }
            }
        }
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        logger.debug("onSaveInstanceState");

        // some phones destroy the retained fragment upon going in background so we have to persist some data
        outState.putParcelable(AppConstants.INTENT_DATA_CONVERSATION_ID, this.conversationId);
        outState.putParcelable(CAMERA_URI, cameraUri);
        outState.putInt(BUNDLE_LIST_POSITION, this.listInstancePosition);
        if (listInstanceConversationIdObfuscated != null) {
            outState.putString(BUNDLE_LIST_CONVERSATION_ID_OBFUSCATED, this.listInstanceConversationIdObfuscated.value);
        }
        outState.putInt(BUNDLE_LIST_TOP, this.listInstanceTop);
        outState.putInt(BUNDLE_LIST_LONG_CLICK_ITEM, this.longClickItem);

        super.onSaveInstanceState(outState);
    }

    private void setCurrentPageReferenceId(@Nullable Integer currentPageReferenceId) {
        this.currentPageReferenceId = currentPageReferenceId;
    }

    @Nullable
    private Integer getCurrentPageReferenceId() {
        return this.currentPageReferenceId;
    }

    private void configureSearchWidget(final MenuItem menuItem) {
        SearchView searchView = (SearchView) menuItem.getActionView();
        if (searchView != null) {
            searchView.setOnQueryTextListener(queryTextListener);
            searchView.setQueryHint(getString(R.string.hint_search_keyword));
            searchView.setIconified(false);
            searchView.setOnCloseListener(() -> {
                if (searchActionMode != null) {
                    searchActionMode.finish();
                }
                return false;
            });

            LinearLayout linearLayoutOfSearchView = (LinearLayout) searchView.getChildAt(0);
            if (linearLayoutOfSearchView != null) {
                linearLayoutOfSearchView.setGravity(Gravity.CENTER_VERTICAL);
                linearLayoutOfSearchView.setPadding(0, 0, 0, 0);

                searchCounter = (TextView) layoutInflater.inflate(R.layout.textview_search_action, null);
                linearLayoutOfSearchView.addView(searchCounter);

                FrameLayout searchPreviousLayout = (FrameLayout) layoutInflater.inflate(R.layout.button_search_action, null);
                searchPreviousButton = searchPreviousLayout.findViewById(R.id.search_button);
                searchPreviousButton.setScaleY(-1);
                searchPreviousButton.setOnClickListener(v -> composeMessageAdapter.previousMatchPosition());
                linearLayoutOfSearchView.addView(searchPreviousLayout);

                FrameLayout searchNextLayout = (FrameLayout) layoutInflater.inflate(R.layout.button_search_action, null);
                searchNextButton = searchNextLayout.findViewById(R.id.search_button);
                searchProgress = searchNextLayout.findViewById(R.id.next_progress);
                searchNextButton.setOnClickListener(v -> composeMessageAdapter.nextMatchPosition());
                linearLayoutOfSearchView.addView(searchNextLayout);
            }
        }
    }

    private class SearchActionMode implements ActionMode.Callback {

        @SuppressLint("StaticFieldLeak")
        @Override
        public boolean onCreateActionMode(ActionMode mode, final Menu menu) {
            composeMessageAdapter.clearFilter();

            activity.getMenuInflater().inflate(R.menu.action_compose_message_search, menu);

            final MenuItem item = menu.findItem(R.id.menu_action_search);
            final View actionView = item.getActionView();

            item.setActionView(R.layout.item_progress);
            item.expandActionView();

            if (bottomPanel != null) {
                bottomPanel.setVisibility(View.GONE);
            }

            hideEmojiPickerIfShown();
            dismissMentionPopup();
            dismissQuotePopup();

            // load all records
            new LifecycleAwareAsyncTask<Void, List<AbstractMessageModel>>() {
                @Override
                protected List<AbstractMessageModel> doInBackground(Void params) {
                    return getAllRecords();
                }

                @Override
                protected void onPostExecute(List<AbstractMessageModel> messageModels) {
                    if (messageModels != null && isAdded()) {
                        item.collapseActionView();
                        item.setActionView(actionView);
                        configureSearchWidget(menu.findItem(R.id.menu_action_search));

                        insertToList(messageModels, true, true, true);
                        convListView.setSelection(Integer.MAX_VALUE);
                    }
                }
            }.execute(ComposeMessageFragment.this, null);

            return true;
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            return false;
        }


        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
            return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            searchCounter = null;
            searchActionMode = null;
            if (composeMessageAdapter != null) {
                composeMessageAdapter.clearFilter();
            }
            if (bottomPanel != null) {
                bottomPanel.setVisibility(View.VISIBLE);
            }
        }
    }

    @UiThread
    private void updateContactModelData(final ContactModel contactModel) {
        // Sanity check
        if (!contactModel.getIdentity().equals(identity)) {
            logger.warn("updateContactModelData was called for mismatching identity");
            return;
        }

        if (this.contactModel != contactModel) {
            // Update the contact model (and the receiver) to have the current setting for
            // sending messages (forward security). This needs to be done if the contact model
            // cache has been reset and therefore a new contact model object has been created.
            this.contactModel = contactModel;
            messageReceiver = this.contactService.createReceiver(this.contactModel);
        }

        // Update header containing contact information
        updateToolbarTitle();

        // Update toolbar/menu icon states
        updateMuteMenu();

        // Reset cache
        if (composeMessageAdapter != null) {
            composeMessageAdapter.resetCachedContactModelData(contactModel);
        }
    }

    final protected boolean requiredInstances() {
        if (!this.checkInstances()) {
            this.instantiate();
        }
        return this.checkInstances();
    }

    protected boolean checkInstances() {
        return preferenceService != null
            && userService != null
            && contactService != null
            && groupService != null
            && groupModelRepository != null
            && groupCallManager != null
            && groupFlowDispatcher != null
            && messageService != null
            && fileService != null
            && notificationService != null
            && distributionListService != null
            && messagePlayerService != null
            && blockedIdentitiesService != null
            && pollService != null
            && conversationService != null
            && deviceService != null
            && wallpaperService != null
            && conversationCategoryService != null
            && ringtoneService != null
            && voipStateService != null
            && downloadService != null
            && licenseService != null
            && emojiReactionsRepository != null;
    }

    protected void instantiate() {
        ServiceManager serviceManager = ServiceManager.get();
        if (serviceManager != null) {
            try {
                this.multiDeviceManager = serviceManager.getMultiDeviceManager();
                this.preferenceService = serviceManager.getPreferenceService();
                this.userService = serviceManager.getUserService();
                this.contactService = serviceManager.getContactService();
                this.groupService = serviceManager.getGroupService();
                this.groupModelRepository = serviceManager.getModelRepositories().getGroups();
                this.groupCallManager = serviceManager.getGroupCallManager();
                this.groupFlowDispatcher = serviceManager.getGroupFlowDispatcher();
                this.messageService = serviceManager.getMessageService();
                this.fileService = serviceManager.getFileService();
                this.notificationService = serviceManager.getNotificationService();
                this.distributionListService = serviceManager.getDistributionListService();
                this.messagePlayerService = serviceManager.getMessagePlayerService();
                this.blockedIdentitiesService = serviceManager.getBlockedIdentitiesService();
                this.pollService = serviceManager.getPollService();
                this.databaseService = serviceManager.getDatabaseService();
                this.conversationService = serviceManager.getConversationService();
                this.deviceService = serviceManager.getDeviceService();
                this.wallpaperService = serviceManager.getWallpaperService();
                this.wallpaperLauncher = wallpaperService.getWallpaperActivityResultLauncher(
                    this,
                    this::setBackgroundWallpaper,
                    () -> conversationId
                );
                this.conversationCategoryService = serviceManager.getConversationCategoryService();
                this.ringtoneService = serviceManager.getRingtoneService();
                this.voipStateService = serviceManager.getVoipStateService();
                this.downloadService = serviceManager.getDownloadService();
                this.licenseService = serviceManager.getLicenseService();
                this.emojiReactionsRepository = serviceManager.getModelRepositories().getEmojiReaction();
                this.draftManager = KoinJavaComponent.get(DraftManager.class);
                this.soundEffectPlayer = KoinJavaComponent.get(SoundEffectPlayer.class);
            } catch (Exception e) {
                logger.error("Failed to instantiate dependencies", e);
                showToast(this, R.string.an_error_occurred);
            }
        }
    }

    // Dialog callbacks
    @Override
    public void onYes(@Nullable String tag, @Nullable Object data) {
        if (tag == null) {
            return;
        }
        switch (tag) {
            case CONFIRM_TAG_DELETE_DISTRIBUTION_LIST:
                logger.info("Deletion of distribution list confirmed");
                new EmptyOrDeleteConversationsAsyncTask(
                    EmptyOrDeleteConversationsUseCase.Mode.DELETE,
                    new MessageReceiver[]{messageReceiver},
                    conversationService,
                    distributionListService,
                    groupModelRepository,
                    groupFlowDispatcher,
                    userService.getIdentity(),
                    getParentFragmentManager(),
                    null,
                    this::finishActivity
                ).execute();
                break;
            case AppConstants.CONFIRM_TAG_CLOSE_POLL:
                logger.info("Closing poll confirmed");
                PollUtil.closePoll((AppCompatActivity) requireActivity(), (PollModel) data, pollService, MessageId.random(), TriggerSource.LOCAL);
                break;
            case DIALOG_TAG_CONFIRM_CALL:
                VoipUtil.initiateCall((AppCompatActivity) requireActivity(), contactModel, false, null);
                break;
            case DIALOG_TAG_EMPTY_CHAT:
                logger.info("Emptying of chat confirmed");
                emptyChat();
                break;
            case DIALOG_TAG_CONFIRM_BLOCK:
                logger.info("Blocking confirmed");
                blockedIdentitiesService.toggleBlocked(contactModel.getIdentity(), getContext());
                updateBlockMenu();
                break;
            case DIALOG_TAG_CONFIRM_LINK:
                logger.info("Link confirmed");
                Uri uri = (Uri) data;
                LinkifyUtil.getInstance().openLink(uri, requireContext(), this);
                break;
            default:
                break;
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode,
                                           @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            switch (requestCode) {
                case PERMISSION_REQUEST_SAVE_MESSAGE:
                    logger.info("Permissions granted for saving media files");
                    saveSelectedMediaFiles();
                    break;
                case PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE:
                    logger.info("Permissions granted for recording voice messages");
                    attachVoiceMessage();
                    break;
                case PERMISSION_REQUEST_ATTACH_CAMERA:
                    logger.info("Permissions granted for camera");
                    updateCameraButton();
                    attachCamera();
                    break;
            }
        } else {
            switch (requestCode) {
                case PERMISSION_REQUEST_SAVE_MESSAGE:
                    if (!shouldShowRequestPermissionRationale(Manifest.permission.READ_EXTERNAL_STORAGE)) {
                        showPermissionRationale(R.string.permission_storage_required);
                    }
                    break;
                case PERMISSION_REQUEST_ATTACH_VOICE_MESSAGE:
                    if (!shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) {
                        showPermissionRationale(R.string.permission_record_audio_required);
                    }
                    break;
                case PERMISSION_REQUEST_ATTACH_CAMERA:
                case PERMISSION_REQUEST_ATTACH_CAMERA_VIDEO:
                    preferenceService.setCameraPermissionRequestShown(true);
                    if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
                        showPermissionRationale(R.string.permission_camera_photo_required);
                    }
                    updateCameraButton();
                    break;
            }
        }
    }

    private void saveSelectedMediaFiles() {
        new SaveMediaAsyncTask(fileService, activity, coordinatorLayout, selectedMessages, false).execute();
    }

    /* properly dispose of popups */

    private void dismissMentionPopup() {
        if (messageText != null) {
            try {
                logger.info("Mention popup dismissed");
                messageText.dismissMentionPopup();
            } catch (Exception e) {
                logger.error("Error dismissing mention popup", e);
            }
        }
    }

    private void dismissTooltipPopup(TooltipPopup tooltipPopup, boolean immediate) {
        try {
            if (tooltipPopup != null) {
                tooltipPopup.dismiss(immediate);
            }
        } catch (final IllegalArgumentException e) {
            // whatever
        }
    }

    public void markAsRead() {
        if (messageReceiver != null) {
            try {
                @NonNull List<AbstractMessageModel> unreadMessages = messageReceiver.getUnreadMessages();
                new MarkAsReadRoutine(conversationService, messageService, notificationService)
                    .runAsync(unreadMessages, messageReceiver);
                notificationService.cancel(conversationId);
            } catch (SQLException e) {
                logger.error("Exception", e);
            }
        }
    }

    @Override
    public void onConfigurationChanged(@NonNull Configuration newConfig) {
        super.onConfigurationChanged(newConfig);

        hideEmojiPickerIfShown();
        hideEmojiPopupIfShown();
        EditTextUtil.hideSoftKeyboard(this.messageText);
        reopenQuotePopup();
        dismissMentionPopup();
        dismissTooltipPopup(workTooltipPopup, true);
        workTooltipPopup = null;
        if (availabilityStatusTooltipPopup != null) {
            availabilityStatusTooltipPopup.dismissForever(true);
            availabilityStatusTooltipPopup = null;
        }

        if (ConfigUtils.isTabletLayout()) {
            // make sure layout changes after rotate are reflected in thumbnail size etc.
            updateMessageDraft();
            this.handleIntent(activity.getIntent());
        } else {
            if (isAdded()) {
                // refresh wallpaper to reflect orientation change
                this.wallpaperService.setupWallpaperBitmap(
                    this.conversationId,
                    this.wallpaperView,
                    ConfigUtils.isLandscape(activity),
                    ConfigUtils.isTheDarkSide(activity)
                );
            }
        }

        setupMessageTextClickListener();
    }

    private void restoreMessageDraft(boolean force) {
        if (
            this.messageReceiver != null &&
                this.conversationId != null &&
                this.messageText != null &&
                (force || isNullOrBlank(this.messageText.getText()))
        ) {
            final @Nullable MessageDraft messageDraft = draftManager.get(conversationId);

            if (messageDraft != null) {
                this.messageText.setText("");
                this.messageText.append(messageDraft.getText());
                var quotedApiMessageId = messageDraft.getQuotedMessageId();
                if (quotedApiMessageId != null) {
                    final @Nullable AbstractMessageModel quotedMessageModel = messageService.getMessageModelByApiMessageIdAndReceiver(
                        quotedApiMessageId.toString(),
                        messageReceiver
                    );
                    if (quotedMessageModel != null && QuoteUtil.isQuoteable(quotedMessageModel)) {
                        showQuotePopup(quotedMessageModel, false);
                    }
                }
                // If the draft is just "@", then dismiss the mention popup when restoring the draft
                if (messageDraft.getText().equals("@")) {
                    dismissMentionPopup();
                }
            } else {
                this.messageText.setText("");
            }
        }
    }

    private void updateMessageDraft() {
        if (conversationId != null && messageText.getText() != null) {
            draftManager.set(
                conversationId,
                this.messageText.getText().toString(),
                isQuotePopupShown() && quotePopup.getQuoteInfo().getMessageModel() != null
                    ? quotePopup.getQuoteInfo().getMessageModel().getMessageId()
                    : null
            );

            // At this point, we don't know whether the draft has changed, so we need to notify the event bus regardless.
            globalEventBuses.getConversations().emit(ConversationEvent.AllConversationsUpdated.INSTANCE);
        }
    }

    @Override
    public void onKeyboardHidden() {
        if (getActivity() != null && isAdded()) {
            dismissMentionPopup();
            dismissTooltipPopup(workTooltipPopup, false);
            workTooltipPopup = null;

            if (emojiPicker != null) {
                emojiPicker.onKeyboardHidden();
            }
        }
    }

    @Override
    public void onKeyboardShown() {
        if (emojiPicker != null) {
            if (isEmojiPickerShown()) {
                emojiPicker.onKeyboardShown();
            }
            if (isResumed() &&
                !emojiPicker.isShown() &&
                searchActionMode == null &&
                messageText != null &&
                !messageText.hasFocus()) {
                // In some cases when the activity is launched where the previous activity finished with
                // an open keyboard, the messageText does not have focus even if the keyboard is shown
                // Only request focus if the emoji picker is hidden and the search bar is not shown,
                // otherwise the keyboard is needed to search emojis or the chat.
                messageText.requestFocus();
            }
        }
    }

    @Override
    public void onReportSpamClicked(@NonNull final ContactModel spammerContactModel, boolean block) {
        logger.info("Report spam clicked");
        contactService.reportSpam(
            spammerContactModel.getIdentity(),
            unused -> {
                if (isAdded()) {
                    LongToast.makeText(getContext(), R.string.spam_successfully_reported, Toast.LENGTH_LONG).show();
                }

                final String spammerIdentity = spammerContactModel.getIdentity();
                if (block) {
                    blockedIdentitiesService.blockIdentity(spammerIdentity, null);
                    ServiceManager.require()
                        .getExcludedSyncIdentitiesService()
                        .excludeFromSync(spammerIdentity, TriggerSource.LOCAL);

                    if (messageReceiver != null) {
                        new EmptyOrDeleteConversationsAsyncTask(
                            EmptyOrDeleteConversationsUseCase.Mode.DELETE,
                            new MessageReceiver[]{messageReceiver},
                            conversationService,
                            distributionListService,
                            groupModelRepository,
                            groupFlowDispatcher,
                            userService.getIdentity(),
                            null,
                            null,
                            () -> {
                                globalEventBuses.getConversations().emit(ConversationEvent.AllConversationsUpdated.INSTANCE);
                                globalEventBuses.getContacts().emit(ContactEvent.ContactUpdated.javaCreate(spammerIdentity));
                                if (isAdded()) {
                                    finishActivity();
                                }
                            }).execute();
                    }
                } else {
                    reportSpamView.hide();
                    globalEventBuses.getContacts().emit(ContactEvent.ContactUpdated.javaCreate(spammerIdentity));
                }
            },
            message -> {
                if (isAdded()) {
                    LongToast.makeText(getContext(), requireContext().getString(R.string.spam_error_reporting, message), Toast.LENGTH_LONG).show();
                }
            }
        );
    }

    private void finishActivity() {
        if (activity != null) {
            activity.finish();
        }
    }

    /*--------------------------------------------------------------------------------------------*/

    private ListenableFuture<MediaController> mediaControllerFuture;

    private void initializeMedia3Controller() {
        SessionToken sessionToken = new SessionToken(getAppContext(), new ComponentName(getAppContext(), VoiceMessagePlayerService.class));

        mediaControllerFuture = new MediaController.Builder(getAppContext(), sessionToken).buildAsync();
    }

    @Nullable
    private MediaController getMedia3Controller() {
        if (mediaControllerFuture.isDone()) {
            try {
                return mediaControllerFuture.get();
            } catch (ExecutionException e) {
                logger.error("Media Controller exception", e);
            } catch (InterruptedException e) {
                logger.error("Media Controller interrupted exception", e);
                Thread.currentThread().interrupt();
            }
        }
        return null;
    }

    private void releaseMedia3Controller() {
        MediaController mediaController = getMedia3Controller();
        if (mediaController != null) {
            mediaController.stop();
            mediaController.clearMediaItems();
            mediaController.release();
        }

        if (mediaControllerFuture != null) {
            MediaController.releaseFuture(mediaControllerFuture);
        }

        try {
            if (!getAppContext().stopService(new Intent(getAppContext(), VoiceMessagePlayerService.class))) {
                logger.debug("VoiceMessagePlayer already stopped.");
            }
        } catch (Exception e) {
            logger.error("Unable to stop VoiceMessagePlayer", e);
        }
    }
}

