<template>
  <div :class="{'chat-root': true, 'fill-height': true, 'd-flex': true, 'flex-column': true, 'channel': roomDisplayType == ROOM_TYPE_CHANNEL}" :style="chatContainerStyle">
    <ChatHeaderPrivate class="chat-header flex-grow-0 flex-shrink-0"
      v-on:header-click="onHeaderClick"
      v-on:view-room-details="viewRoomDetails"
      v-on:purge="showPurgeConfirmation = true"
      v-if="!useFileModeNonAdmin && $matrix.isDirectRoom(room)" />
      <ChatHeader class="chat-header flex-grow-0 flex-shrink-0"
      v-on:header-click="onHeaderClick"
      v-on:view-room-details="viewRoomDetails"
      v-on:purge="showPurgeConfirmation = true"
      v-else-if="!useFileModeNonAdmin" />
    <AudioLayout ref="chatContainer" class="auto-audio-player-root" v-if="useVoiceMode" :room="room"
      :events="events" :autoplay="!showRecorder"
      :timelineSet="timelineSet"
      :readMarker="readMarker"
      :recordingMembers="typingMembers"
      v-on:start-recording="setShowRecorder()"
      v-on:loadnext="handleScrolledToLatest(false)"
      v-on:loadprevious="handleScrolledToOldest()"
      v-on:mark-read="sendRR"
      v-on:sendclap="sendClapReactionAtTime"
      />
      <VoiceRecorder class="audio-layout" v-if="useVoiceMode" :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
          v-on:close="showRecorder = false" v-on:file="onVoiceRecording" :sendTypingIndicators="useVoiceMode" />

    <FileDropLayout class="file-drop-root" v-if="useFileModeNonAdmin" :room="room"
      v-on:pick-file="showAttachmentPicker()"
      v-on:add-file="addAttachment($event)"
      v-on:remove-file="currentFileInputs.splice($event, 1)"
      v-on:reset="resetAttachments"
      :attachments="currentFileInputs"
    />

    <div v-if="!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
      v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
      <div ref="messageOperationsStrut" class="message-operations-strut">
        <component :is="messageOperationsComponent" ref="messageOperations" :style="opStyle" 
          v-if="showMessageOperations" 
          v-on:close="showContextMenu = false;" 
          :emojis="recentEmojis" 
          :originalEvent="selectedEvent" 
          :timelineSet="timelineSet"
          v-on:pin="pin(selectedEvent)"
          v-on:unpin="unpin(selectedEvent)"
          v-on:addreaction="addReaction" 
          v-on:addquickreaction="addQuickReaction"
          v-on:addreply="addReply(selectedEvent)" 
          v-on:edit="edit(selectedEvent)" 
          v-on:redact="redact(selectedEvent)"
          v-on:download="download(selectedEvent)" 
          v-on:more="
            isEmojiQuickReaction= true
            showMoreMessageOperations({event: selectedEvent, anchor: $event.anchor})
          " 
          :userCanSendReactionAndAnswerPoll="$matrix.userCanSendReactionAndAnswerPollInCurrentRoom"
          :userCanSendMessage="$matrix.userCanSendMessageInCurrentRoom"
          :userCanPin="canCreatePoll"
        />
      </div>

      <!-- Handle resizes, e.g. when soft keyboard is shown/hidden -->
      <resize-observer ref="chatContainerResizer" @notify="handleChatContainerResize" />

      <component :is="roomWelcomeHeader" v-on:close="closeRoomWelcomeHeader"></component>

      <!-- If we have a retention timer, it means we have active message retention. Show header. -->
      <WelcomeHeaderChannelUser v-if="retentionTimer && !roomWelcomeHeader && newlyJoinedRoom" />

      <div v-for="(event) in filteredEvents" :key="event.getId()" :eventId="event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER ? event.getId() : undefined">
               
        <!-- DAY Marker, shown for every new day in the timeline -->
        <div v-if="!reverseOrder && showDayMarkerBeforeEvent(event) && !!event.component && event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>

        <div :ref="event.getId()">
          <MessageErrorHandler>
            <div class="message-wrapper" v-on:touchstart="
              (e) => {
                touchStart(e, event);
              }
              " v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">

              <!-- Note: For threaded media messages, IF there is only one item we show that media item as a single component.
                We might therefore get calls to v-on:context-menu that has the event set to that single media item, not the top level thread event
                that is really displayed in the flow. Therefore, we rewrite these events with "{event: event, anchor: $event.anchor}",
                see below. Otherwise things like context menus won't work as designed.
              -->
              <component :is="event.component" :room="room" :originalEvent="event" :nextEvent="event.nextDisplayedEvent"
                :timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
                :componentFn="componentForEvent"
                v-on:context-menu="showContextMenuForEvent({event: event, anchor: $event.anchor})"
                v-on:own-avatar-clicked="viewProfile"
                v-on:other-avatar-clicked="showAvatarMenuForEvent({event: event, anchor: $event.anchor})"
                v-on:download="download(event)"
                v-on:poll-closed="pollWasClosed(event)"
                v-on:more="
                  isEmojiQuickReaction = true
                  showMoreMessageOperations({event: event, anchor: $event.anchor})
                  "
                v-on:layout-change="onLayoutChange"
                v-on:addQuickHeartReaction="addQuickHeartReaction({event, position: $event.position})"
                />
              <!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
              <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}<br /><br /></div> -->
            </div>
          </MessageErrorHandler>
        </div>

        <!-- Day marker when reverseOrder is set -->
        <div v-if="reverseOrder && showDayMarkerBeforeEvent(event) && !!event.component && event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER" class="day-marker"><div class="line"></div><div class="text">{{ dayForEvent(event) }}</div><div class="line"></div></div>

      </div>

      <NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />

        <!-- "Scroll to end"-button -->
        <v-btn v-if="!useVoiceMode" :class="{'scroll-to-end': true, 'reversed': reverseOrder}" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
          @click.stop="scrollToEndOfTimeline">
          <v-icon color="white">arrow_downward</v-icon>
        </v-btn>


    </div>

    <!-- Input area -->
    <v-container v-if="!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
      <div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
        <v-row class="ma-0 pa-0">
          <div v-if="replyToEvent" class="row">
            <div class="col">
              <div class="font-weight-medium">{{ $t("message.replying_to", { user: senderDisplayName }) }}</div>
              <div v-if="replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body">
                {{ replyToEvent.getContent().body | latestReply }}
              </div>
              <div v-if="replyToContentType === 'm.thread' || replyToContentType === 'io.element.thread'">{{ replyToThreadMessage }}</div>
              <div v-if="replyToContentType === 'm.image'">{{ $t("message.reply_image") }}</div>
              <div v-if="replyToContentType === 'm.audio'">{{ $t("message.reply_audio_message") }}</div>
              <div v-if="replyToContentType === 'm.video'">{{ $t("message.reply_video") }}</div>
              <div v-if="replyToContentType === 'm.poll'">{{ $t("message.reply_poll") }}</div>
            </div>
            <div class="col col-auto" v-if="replyToContentType !== 'm.text'">
              <img v-if="replyToContentType === 'm.image'" width="50px" height="50px" :src="replyToImg"
                class="rounded" />
              <v-img v-if="replyToContentType === 'm.audio'" src="@/assets/icons/audio_message.svg" />
              <v-img v-if="replyToContentType === 'm.video'" src="@/assets/icons/video_message.svg" />
              <v-icon v-if="replyToContentType === 'm.poll'" light>$vuetify.icons.poll</v-icon>
            </div>
            <div class="col col-auto">
              <v-btn fab x-small elevation="0" color="black" @click.stop="cancelEditReply">
                <v-icon color="white">cancel</v-icon>
              </v-btn>
            </div>
          </div>

          <!-- CONTACT IS TYPING -->
          <div class="typing">
            {{ typingMembersString }}
          </div>
        </v-row>
        <v-row class="input-area-inner align-center" v-show="!showRecorder" v-if="$matrix.userCanSendMessageInCurrentRoom">
          <v-col class="flex-grow-1 flex-shrink-1 ma-0 pa-0">
            <v-textarea height="undefined" ref="messageInput" full-width auto-grow rows="1" v-model="currentInput"
              no-resize class="input-area-text" :placeholder="$t('message.your_message')" hide-details
              background-color="white" v-on:keydown.enter.prevent="
                () => {
                  sendCurrentTextMessage();
                }
              " />
          </v-col>

          <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-if="editedEvent">
            <v-btn fab small elevation="0" color="black" @click.stop="cancelEditReply">
              <v-icon color="white">cancel</v-icon>
            </v-btn>
          </v-col>

          <v-col v-if="(!currentInput || currentInput.length == 0) && canCreatePoll && !replyToEvent"
            class="input-area-button text-center flex-grow-0 flex-shrink-1">
            <v-btn icon large color="black" @click="showCreatePollDialog = true">
              <v-icon dark>$vuetify.icons.poll</v-icon>
            </v-btn>
          </v-col>

          <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1"
            v-if="!currentInput || currentInput.length == 0 || showRecorder">
            <v-btn v-if="canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v-blur
              v-longTap:250="[showRecordingUI, startRecording]">
              <v-icon :color="showRecorder ? 'white' : 'black'">mic</v-icon>
            </v-btn>
            <v-btn v-else class="mic-button" ref="mic_button" fab small elevation="0" v-blur
              @click.stop="showNoRecordingAvailableDialog = true">
              <v-icon :color="showRecorder ? 'white' : 'black'">mic</v-icon>
            </v-btn>
          </v-col>

          <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1" v-else>
            <v-btn fab small elevation="0" color="black" @click.stop="sendCurrentTextMessage"
              :disabled="sendButtonDisabled">
              <v-icon color="white">{{ editedEvent ? "save" : "arrow_upward" }}</v-icon>
            </v-btn>
          </v-col>

          <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1 input-more-icon">
            <v-btn fab small elevation="0" v-blur @click.stop="
              isEmojiQuickReaction = false
              showMoreMessageOperations($event)
            ">
              <v-icon>$vuetify.icons.addReaction</v-icon>
            </v-btn>
          </v-col>

          <v-col v-if="$config.shortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1">
            <v-btn id="btn-attach" icon large color="black" @click="showStickerPicker"
              :disabled="attachButtonDisabled">
              <v-icon large>face</v-icon>
            </v-btn>
          </v-col>

          <v-col class="input-area-button text-center flex-grow-0 flex-shrink-1">
            <label icon flat ref="attachmentLabel">
              <v-btn icon large color="black" @click="showAttachmentPicker"
                :disabled="attachButtonDisabled">
                <v-icon x-large>add_circle_outline</v-icon>
              </v-btn>
            </label>
          </v-col>
        </v-row>
        <VoiceRecorder :micButtonRef="$refs.mic_button" :ptt="showRecorderPTT" :show="showRecorder"
          v-on:close="showRecorder = false" v-on:file="onVoiceRecording" />
      </div>
      <div v-if="!useVoiceMode && room && !$matrix.userCanSendMessageInCurrentRoom" class="input-area-read-only">{{ $t("message.not_allowed_to_send") }}</div>
    </v-container>

    <input ref="attachment" type="file" name="attachment" @change="handlePickedAttachment($event)"
                accept="image/*,audio/*,video/*,.mp3,.mp4,.wav,.m4a,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class="d-none" multiple/>

    <div v-if="currentFileInputsDialog && !useFileModeNonAdmin">
      <v-dialog v-model="currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable>
        <v-card class="ma-0 pa-0">
          <v-card-text v-if="!currentFileInputs.length">
            {{ this.$t("message.preparing_to_upload")}}
            <v-progress-linear
              indeterminate
              class="mb-0"
            ></v-progress-linear>
          </v-card-text>
          <template v-else>
            <v-card-title>
              <div v-if="currentSendErrorExceededFile" class="red--text">{{ currentSendErrorExceededFile }}</div>
              <span v-else> {{ $t('message.send_attachements_dialog_title') }} </span>
            </v-card-title>
            <v-divider></v-divider>
            <template v-if="imageFiles && imageFiles.length">
              <v-card-title v-if="imageFiles.length > 1"> {{ $t('message.images') }} </v-card-title>
              <v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': imageFiles.length > 1}">
                <div :class="{'col-4': imageFiles.length > 1}" v-for="(currentImageInput, id) in imageFiles" :key="id">
                  <div style="position: relative">
                    <v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
                      contain class="current-image-input-path" />
                    <v-progress-linear :style="{ position: 'absolute', left: '0', right: '0', bottom: '0', opacity: currentImageInput.sendInfo ? '1' : '0' }" :value="currentImageInput.sendInfo ? currentImageInput.sendInfo.progress : 0"></v-progress-linear>
                  </div>
                  <div>
                    <span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
                      {{ currentImageInput.scaledDimensions.width }} x {{ currentImageInput.scaledDimensions.height }}</span>
                    <span v-else-if="currentImageInput && currentImageInput.dimensions">
                      {{ currentImageInput.dimensions.width }} x {{ currentImageInput.dimensions.height }}
                    </span>

                    <span v-if="currentImageInput && currentImageInput.scaled && currentImageInput.useScaled">
                      ({{ formatBytes(currentImageInput.scaledSize) }})
                    </span>
                    <span v-else>
                      ({{ formatBytes(currentImageInput.actualSize) }})
                    </span>

                    <v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
                      v-model="currentImageInput.useScaled" :disabled="currentImageInput && currentImageInput.sendInfo !== undefined" />
                  </div>
                </div>
              </v-card-text>
            </template>
            <template v-if="Array.isArray(currentFileInputs) && currentFileInputs.length">
              <v-card-title v-if="nonImageFiles.length > 1">{{ $t('message.files') }}</v-card-title>
              <v-card-text>
                <div v-for="(currentImageInputPath, id) in currentFileInputs" :key="id">
                  <div v-if="!currentImageInputPath.type.includes('image/')">
                    <span> {{ $t('message.file') }}: {{ currentImageInputPath.name }}</span>
                    <span> ({{ formatBytes(currentImageInputPath.size) }})</span>
                    <v-progress-linear :style="{ opacity: currentImageInputPath.sendInfo ? '1' : '0' }" :value="currentImageInputPath.sendInfo ? currentImageInputPath.sendInfo.progress : 0"></v-progress-linear>
                  </div>
                </div>
              </v-card-text>
            </template>
            <v-divider></v-divider>
            <v-textarea v-if="showAttachmentCaptionInput" v-model="attachmentCaption" ref="attachmentCaption" color="black" background-color="transparent"
                solo full-width auto-grow rows="1" no-resize hide-details :placeholder="$t('file_mode.add_a_message')"></v-textarea>
            <v-card-actions>
              <v-spacer>
                <div v-if="currentSendError">{{ currentSendError }}</div>
              </v-spacer>
              <v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel" :disabled="sendingStatus != sendStatuses.SENDING && sendingStatus != sendStatuses.INITIAL">
                {{ $t("menu.cancel") }}
              </v-btn>
              <v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment(attachmentCaption)"
                v-if="currentSendShowSendButton" :disabled="currentSendShowSendButton && sendingStatus != sendStatuses.INITIAL">{{ $t("menu.send") }}</v-btn>
            </v-card-actions>
           </template>
        </v-card>
      </v-dialog>
    </div>

    <MessageOperationsBottomSheet ref="messageOperationsSheet">
      <VEmojiPicker ref="emojiPicker" @select="emojiSelected" :i18n="i18nEmoji"/>
    </MessageOperationsBottomSheet>

    <StickerPickerBottomSheet ref="stickerPickerSheet" v-on:selectSticker="sendSticker" />

    <!-- Loading indicator -->
    <v-container fluid class="loading-indicator" fill-height v-if="!initialLoadDone || loading">
      <v-row align="center" justify="center">
        <v-col class="text-center">
          <v-progress-circular indeterminate color="primary"></v-progress-circular>
        </v-col>
      </v-row>
    </v-container>

    <RoomInfoBottomSheet ref="roomInfoSheet" />

    <!-- Dialog for audio recording not supported! -->
    <v-dialog v-model="showNoRecordingAvailableDialog" class="ma-0 pa-0" width="80%">
      <v-card>
        <v-card-title>{{ $t("voice_recorder.not_supported_title") }}</v-card-title>
        <v-card-text>{{ $t("voice_recorder.not_supported_text") }} </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn id="btn-ok" color="primary" text @click="showNoRecordingAvailableDialog = false">{{
          $t("menu.ok")
          }}</v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

    <CreatePollDialog :show="showCreatePollDialog" @close="showCreatePollDialog = false" />

    <UserProfileDialog
      :show="showProfileDialog"
      :activeMember="compActiveMember"
      :room="room"
      @close="showProfileDialog = false"
    />

    <!-- PURGE ROOM POPUP -->
    <PurgeRoomDialog :show="showPurgeConfirmation" :room="room" @close="showPurgeConfirmation = false" />

    <!-- Heart animation -->
    <div :class="['heart-wrapper', { 'is-active': heartAnimation }]" :style="hearAnimationPosition">
      <div :class="['heart', { 'is-active': heartAnimation }]" />
    </div>
  </div>
</template>

<script>
import Vue from "vue";
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
import util, { ROOM_TYPE_VOICE_MODE, ROOM_TYPE_FILE_MODE, ROOM_TYPE_CHANNEL } from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue";
import ChatHeader from "./ChatHeader";
import ChatHeaderPrivate from "./ChatHeaderPrivate.vue";
import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom";
import WelcomeHeaderDirectChat from "./welcome_headers/WelcomeHeaderDirectChat";
import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel";
import WelcomeHeaderChannelUser from "./welcome_headers/WelcomeHeaderChannelUser";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
import UserProfileDialog from "./UserProfileDialog.vue"
import BottomSheet from "./BottomSheet.vue";
import ImageResize from "image-resize";
import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin, { ROOM_READ_MARKER_EVENT_PLACEHOLDER } from "./chatMixin";
import sendAttachmentsMixin from "./sendAttachmentsMixin";
import AudioLayout from "./AudioLayout.vue";
import FileDropLayout from "./file_mode/FileDropLayout";
import roomTypeMixin from "./roomTypeMixin";
import roomMembersMixin from "./roomMembersMixin";
import PurgeRoomDialog from "../components/PurgeRoomDialog";
import MessageErrorHandler from "./MessageErrorHandler";
import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue';

const sizeOf = require("image-size");
const dataUriToBuffer = require("data-uri-to-buffer");
const prettyBytes = require("pretty-bytes");

const READ_RECEIPT_TIMEOUT = 5000; /* How long a message must have been visible before the read marker is updated */
const WINDOW_BUFFER_SIZE = 0.3; /** Relative window height of when we start paginating. Always keep this much loaded before and after our scroll position!  */

// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
function ScrollPosition(node) {
  this.node = node;
  this.previousScrollHeightMinusTop = 0;
  this.previousScrollTop = 0;
  this.readyFor = "up";
}

ScrollPosition.prototype.restore = function () {
  if (this.readyFor === "up") {
    this.node.scrollTop = this.node.scrollHeight - this.previousScrollHeightMinusTop;
  } else {
    this.node.scrollTop = this.previousScrollTop;
  }
};

ScrollPosition.prototype.prepareFor = function (direction) {
  this.readyFor = direction || "up";
  if (this.readyFor === "up") {
    this.previousScrollHeightMinusTop = this.node.scrollHeight - this.node.scrollTop;
  } else {
    this.previousScrollTop = this.node.scrollTop;
  }
};

export default {
  name: "Chat",
  mixins: [chatMixin, roomTypeMixin, sendAttachmentsMixin, roomMembersMixin],
  components: {
    ChatHeader,
    ChatHeaderPrivate,
    MessageOperations,
    VoiceRecorder,
    RoomInfoBottomSheet,
    WelcomeHeaderRoom,
    WelcomeHeaderDirectChat,
    NoHistoryRoomWelcomeHeader,
    MessageOperationsBottomSheet,
    StickerPickerBottomSheet,
    BottomSheet,
    CreatePollDialog,
    AudioLayout,
    FileDropLayout,
    UserProfileDialog,
    PurgeRoomDialog,
    WelcomeHeaderChannelUser,
    MessageErrorHandler,
    MessageOperationsChannel
  },

  data() {
    return {
      ROOM_TYPE_CHANNEL: ROOM_TYPE_CHANNEL,
      ROOM_READ_MARKER_EVENT_PLACEHOLDER: ROOM_READ_MARKER_EVENT_PLACEHOLDER,

      waitingForRoomObject: false,
      events: [],
      currentInput: "",
      typingMembers: [],
      timelineSet: null,
      timelineWindow: null,

      /** true if we are currently paginating */
      timelineWindowPaginating: false,

      scrollPosition: null,
      currentFileInputs: null,
      currentSendShowSendButton: true,
      currentSendError: null,
      currentSendErrorExceededFile: null,
      attachmentCaption: undefined,
      showEmojiPicker: false,
      selectedEvent: null,
      editedEvent: null,
      replyToEvent: null,
      replyToImg: null,
      replyToContentType: null,
      showCreatePollDialog: false,
      showNoRecordingAvailableDialog: false,
      showContextMenu: false,
      showContextMenuAnchor: null,
      initialLoadDone: false,
      loading: false, // Set this to true during long operations to show a "spinner" overlay
      showRecorder: false,
      showRecorderPTT: false, // True to open the voice recorder in push-to-talk mode.

      /**
       * Current chat container size. We need to keep track of this so that if and when
       * a soft keyboard is shown/hidden we can restore the scroll position correctly.
       * If we don't, the keyboard will simply overflow the message we are answering to etc.
       */
      chatContainerSize: 0,

      /**
       * True if we should show the "scroll to end" marker in the chat. For now at least, we use a simple
       * method here, basically just "if we can scroll, show it".
       */
      showScrollToEnd: false,

      /** A timer for read receipts. */
      rrTimer: null,

      /** Last event we sent a Read Receipt/Read Marker for */
      lastRR: null,

      /** If we just created this room, show a small welcome header with info */
      hideRoomWelcomeHeader: false,
      newlyJoinedRoom: false,

      /** An array of recent emojis. Used in the "message operations" popup. */
      recentEmojis: [],

      /** Calculated style for message operations. We position the "popup" at the selected message. */
      opStyle: "",

      isEmojiQuickReaction: true,
      i18nEmoji: {
        search: this.$t("emoji.search"),
        categories: {
          Activity: this.$t("emoji.categories.activity"),
          Flags: this.$t("emoji.categories.flags"),
          Foods: this.$t("emoji.categories.foods"),
          Frequently: this.$t("emoji.categories.frequently"),
          Objects: this.$t("emoji.categories.objects"),
          Nature: this.$t("emoji.categories.nature"),
          Peoples: this.$t("emoji.categories.peoples"),
          Symbols: this.$t("emoji.categories.symbols"),
          Places: this.$t("emoji.categories.places")
        }
      },

      /**
       * A timer to handle message retention/auto deletion
       */
      retentionTimer: null,
      showProfileDialog: false,
      showPurgeConfirmation: false,
      heartAnimation: false,
      heartPosition: {
        top: 0,
        left: 0
      },
      reverseOrder: false
    };
  },

  filters: {
    latestReply(contents) {
      const contentArr = contents.split("\n").reverse();
      if (contentArr[0] === "") {
        contentArr.shift();
      }
      return (contentArr && contentArr.length > 0) ? contentArr[0].replace(/^> (<.*> )?/g, "") : "";
    },
  },

  mounted() {
    this.$root.$on('audio-playback-ended', this.audioPlaybackEnded);
    const container = this.chatContainer;
    if (container) {
      this.scrollPosition = new ScrollPosition(container);
      if (this.$refs.chatContainerResizer) {
        this.chatContainerSize = this.$refs.chatContainerResizer.$el.clientHeight;
      }
    }
  },

  beforeDestroy() {
    this.$root.$off('audio-playback-ended', this.audioPlaybackEnded);
    this.$audioPlayer.pause();
    this.stopRRTimer();
    if (this.retentionTimer) {
      clearInterval(this.retentionTimer);
      this.retentionTimer = null;
    }
  },

  destroyed() {
    this.$matrix.off("Room.timeline", this.onEvent);
    this.$matrix.off("RoomMember.typing", this.onUserTyping);
  },

  computed: {
    heartEmoji() {
      return this.$refs.emojiPicker.mapEmojis["Symbols"].find(({ aliases }) => aliases.includes('heart')).data;
    },
    compActiveMember() {
      const currentUserId= this.selectedEvent?.sender.userId || this.$matrix.currentUserId
      return this.joinedAndInvitedMembers.find(({userId}) => userId === currentUserId)
    },
    nonImageFiles() {
      return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file?.type.includes("image/"))
    },
    imageFiles() {
      return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => file?.type.includes("image/"))
    },
    isCurrentFileInputsAnArray() {
      return Array.isArray(this.currentFileInputs)
    },
    showAttachmentCaptionInput() {
      // IFF we are sending one PDF, add option to set caption.
      const imageFiles = this.imageFiles || [];
      const otherFiles = this.nonImageFiles || [];
      return imageFiles.length == 0 && otherFiles.length == 1 && (otherFiles[0].type === "application/pdf" || (otherFiles[0].name || "").endsWith(".pdf"));
    },
    currentFileInputsDialog: {
      get() {
        return this.isCurrentFileInputsAnArray
      },
      set() {
        this.currentFileInputs = null
      }
    },
    chatContainer() {
      const container = this.$refs.chatContainer;
      if (this.useVoiceMode) {
        return container.$el;
      }
      return container;
    },
    senderDisplayName() {
      return this.room.getMember(this.replyToEvent.sender.userId).name;
    },
    currentUser() {
      return this.$store.state.auth.user;
    },
    room() {
      return this.$matrix.currentRoom;
    },
    roomId() {
      if (!this.$matrix.ready && this.currentUser) {
        // If we have a user already, wait for ready state. If not, we
        // dont want to return here, because we want to redirect to "join".
        return null; // Not ready yet...
      }
      if (this.room) {
        return this.room.roomId;
      }
      return this.$matrix.currentRoomId;
    },
    roomAliasOrId() {
      if (this.room) {
        return this.room.getCanonicalAlias() || this.room.roomId;
      }
      return this.$matrix.currentRoomId;
    },
    readMarker() {
      if (this.lastRR) {
        // If we have sent a RR, use that as read marker (so we don't have to wait for server round trip)
        return this.lastRR.getId();
      }
      return this.fullyReadMarker || this.room.getEventReadUpTo(this.$matrix.currentUserId, false);
    },
    fullyReadMarker() {
      const readEvent = this.room && this.room.getAccountData("m.fully_read");
      if (readEvent) {
        return readEvent.getContent().event_id;
      }
      return null;
    },
    attachButtonDisabled() {
      return this.editedEvent != null || this.replyToEvent != null || this.currentInput.length > 0;
    },
    sendButtonDisabled() {
      return this.currentInput.length == 0;
    },
    typingMembersString() {
      const count = this.typingMembers.length;
      if (count > 1) {
        return this.$t("message.users_are_typing", { count: count });
      } else if (count > 0) {
        return this.$t("message.user_is_typing", {
          user: this.typingMembers[0].name,
        });
      } else {
        return "";
      }
    },
    showMessageOperations() {
      return this.selectedEvent && this.showContextMenu;
    },
    canRecordAudio() {
      return util.browserCanRecordAudio();
    },
    debugging() {
      return false; //(window.location.host || "").startsWith("localhost");
    },
    canCreatePoll() {
      // We say that if you can redact events, you are allowed to create polls.
      const me = this.room && this.room.getMember(this.$matrix.currentUserId);
      let isAdmin =
        me && this.room.currentState && this.room.currentState.hasSufficientPowerLevelFor("redact", me.powerLevel);
      return isAdmin;
    },
    useVoiceMode: {
      get: function () {
        if (!this.$config.experimental_voice_mode) return false;
        return (util.roomDisplayTypeOverride(this.room) || this.roomDisplayType) === ROOM_TYPE_VOICE_MODE;
      },
    },
    useFileModeNonAdmin: {
      get: function() {
        if (!this.$config.experimental_file_mode) return false;
        return (util.roomDisplayTypeOverride(this.room) || this.roomDisplayType) === ROOM_TYPE_FILE_MODE && !this.canCreatePoll; // TODO - Check user or admin
      }
    },

    /**
     * If we have no events and the room is encrypted, show info about this
     * to the user.
     */
    showNoHistoryRoomWelcomeHeader() {
      return this.filteredEvents.length == 0 && this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId);
    },

    filteredEvents() {
      let events = this.events;

      if (this.room && this.$matrix.matrixClient.isRoomEncrypted(this.room.roomId)) {
        if (this.room.getHistoryVisibility() == "joined") {
          // For encrypted rooms where history is set to "joined" we can't read old events.
          // We might, however, have old status events from room creation etc.
          // We filter out anything that happened before our own join event.
          for (let idx = this.events.length - 1; idx >= 0; idx--) {
            const e = this.events[idx];
            if (e.getType() == "m.room.member" &&
              e.getContent().membership == "join" &&
              (!e.getPrevContent() || e.getPrevContent().membership != "join") &&
              e.getStateKey() == this.$matrix.currentUserId) {
              // Our own join event.
              events = this.events.slice(idx + 1);
            }
          }
        }
      }

      // Filter out relations and redactions
      events = this.events.toReversed().filter((e) => !e.isRelation() && !e.isRedaction());

      // If Channel, remove all redacted events as well.
      if (this.room && this.room.displayType == ROOM_TYPE_CHANNEL) {
        events = events.filter((e) => !e.isRedacted());
      }

      // Add read marker, if it is not newer than the "latest" message we are going to display
      //
      let showReadMarker = false;
      let lastDisplayedEvent = undefined;
      events = events.flatMap((e) => {
        let result = [];
        Vue.set(e, "component", this.componentForEvent(e, false));
        if (e.getId() == this.readMarker && showReadMarker) {
          const readMarkerEvent = ROOM_READ_MARKER_EVENT_PLACEHOLDER;
          Vue.set(readMarkerEvent, "component", this.componentForEvent(readMarkerEvent, false));
          if (readMarkerEvent.component) {
            Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
          }
          result.push(readMarkerEvent);
        }
        if (e.component) {
          Vue.set(e, "nextDisplayedEvent", lastDisplayedEvent);
          lastDisplayedEvent = e;
          if (e.getSender() !== this.$matrix.currentUserId) {
            showReadMarker = true;
          }
        }
        result.push(e);
        return result;
      })
      return (this.reverseOrder ? events : events.toReversed()) // Reverse back if needed
    },

    roomCreatedByUsRecently() {
      const createEvent = this.room && this.room.currentState.getStateEvents("m.room.create", "");
      if (createEvent) {
        const creatorId = createEvent.getContent().creator;
        return (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */);
      }
      return false;
    },

    isDirectRoom() {
      return this.room && this.room.getJoinRule() == "invite" && this.joinedAndInvitedMembers.length == 2;
    },

    isPublicRoom() {
      return this.room && this.room.getJoinRule() == "public";
    },

    showRoomWelcomeHeader() {
      return this.roomWelcomeHeader != null;
    },
    roomWelcomeHeader() {
      if (!this.hideRoomWelcomeHeader && this.roomCreatedByUsRecently) {
        if (this.roomDisplayType == ROOM_TYPE_CHANNEL) {
          return WelcomeHeaderChannel;
        }
        if (this.isDirectRoom) {
          return WelcomeHeaderDirectChat;
        }
        return WelcomeHeaderRoom;
      }
      return null;
    },
    messageOperationsComponent() {
      if (this.room.displayType == ROOM_TYPE_CHANNEL) {
        return MessageOperationsChannel;
      }
      return MessageOperations;
    },
    chatContainerStyle() {
      if (this.$config.chat_backgrounds && this.room && this.roomId) {
        const roomType =  this.isDirectRoom ? "direct" : this.isPublicRoom ? "public" : "invite";
        let backgrounds = this.$config.chat_backgrounds[roomType] || this.$config.chat_backgrounds["all"];
        if (backgrounds) {
          const numBackgrounds = backgrounds.length;

          // If we have several backgrounds set, use the room ID to calculate
          // an int hash value, then take mod of that to select a background to use.
          // That way, we always get the same one, since room IDs don't change.

          // From: https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript
          const hashCode = function (s) {
            var hash = 0,
              i, chr;
            if (s.length === 0) return hash;
            for (i = 0; i < s.length; i++) {
              chr = s.charCodeAt(i);
              hash = ((hash << 5) - hash) + chr;
              hash |= 0; // Convert to 32bit integer
            }
            return hash;
          }

          // Adapted from: https://stackoverflow.com/questions/5717093/check-if-a-javascript-string-is-a-url
          const validUrl = function (s) {
            let url;
            try {
              url = new URL(s, window.location);
            } catch (err) {
              return false;
            }
            return url.protocol === "http:" || url.protocol === "https:" || url.protocol === "data:";
          }

          const index = Math.abs(hashCode(this.roomId)) % numBackgrounds;
          const background = backgrounds[index];
          if (background && validUrl(background)) {
            return "background-image: url(" + background + ");background-repeat: repeat";
          }
        }
      }
      return "";
    },

    /**
     * If we are replying to a (media) thread, this is the hint we show when replying.
     */
    replyToThreadMessage() {
      if (this.replyToEvent && this.timelineSet) {
        return this.$t("message.sent_media", {count: this.timelineSet.relations
          .getAllChildEventsForEvent(this.replyToEvent.getId())
          .filter((e) => util.downloadableTypes().includes(e.getContent().msgtype)).length});
      }
      return "";
    },
    hearAnimationPosition() {
      return {
        '--top': this.heartPosition.top,
        '--left': this.heartPosition.left
      };
    }
  },

  watch: {
    initialLoadDone: {
      immediate: true,
      handler(value, oldValue) {
        if (value && !oldValue) {
          this.events.filter(event => (event.threadRootId && !event.parentThread)).forEach(event => this.setParentThread(event));
          this.events.filter(event => (event.replyEventId && !event.replyEvent)).forEach(event => this.setReplyToEvent(event));
          console.log("Loading finished!");
          this.updateRetentionTimer();
        } else if (!value) {
          if (this.retentionTimer) {
            clearInterval(this.retentionTimer);
            this.retentionTimer = null;
          }
        }
      }
    },
    roomId: {
      immediate: true,
      handler(value, oldValue) {
        if (value && value == oldValue) {
          return; // No change.
        }
        console.log("Chat: Current room changed to " + (value ? value : "null"));

        // Clear old events
        this.$matrix.off("Room.timeline", this.onEvent);
        this.$matrix.off("RoomMember.typing", this.onUserTyping);

        this.waitingForRoomObject = false;
        this.events = [];
        this.timelineWindow = null;
        this.typingMembers = [];
        this.initialLoadDone = false;
        this.hideRoomWelcomeHeader = false;
        this.newlyJoinedRoom = false;

        // Stop RR timer
        this.stopRRTimer();
        this.lastRR = null;

        if (this.roomId) {
          this.$matrix.isJoinedToRoom(this.roomId).then(joined => {
            if (!joined) {
              this.onRoomNotJoined();
            } else {
              if (this.room) {
                this.onRoomJoined(this.roomDisplayType == ROOM_TYPE_CHANNEL ? null : this.readMarker);
              } else {
                this.waitingForRoomObject = true;
                return; // no room, wait for it (we know we are joined so need to wait for sync to complete)
              }
            }
          });
        } else {
          this.setInitialLoadDone();
          return; // no room
        }
      },
    },
    room() {
      // Were we waiting?
      if (this.room && this.room.roomId == this.roomId && this.waitingForRoomObject) {
        this.waitingForRoomObject = false;
        this.onRoomJoined(this.roomDisplayType == ROOM_TYPE_CHANNEL ? null : this.readMarker);
      }
    },
    showMessageOperations(show) {
      if (show) {
        this.$nextTick(() => {
          // Calculate where to show the context menu.
          //
          const ref = this.selectedEvent && this.$refs[this.selectedEvent.getId()];
          var top = 0;
          var left = 0;
          if (ref && ref[0]) {
            if (this.showContextMenuAnchor) {
              var rectAnchor = this.showContextMenuAnchor.getBoundingClientRect();
              var rectChat = this.$refs.messageOperationsStrut.getBoundingClientRect();
              var rectOps = this.$refs.messageOperations.$el.getBoundingClientRect();
              if (this.room.displayType == ROOM_TYPE_CHANNEL) {
                top = rectAnchor.top - rectChat.top;
                let right = rectChat.right - rectAnchor.right; 
                this.opStyle = "top:" + top + "px;right:" + right + "px";
                return;
              } else {
              top = rectAnchor.top - rectChat.top - 50;
              left = rectAnchor.left - rectChat.left - 75;
              if (left + rectOps.width + 10 >= rectChat.right) {
                left = rectChat.right - rectOps.width - 10; // No overflow
              } else if (left < 0) {
                left = 0;
              }
            }
            }
          }
          this.opStyle = "top:" + top + "px;left:" + left + "px";
        });
      }
    },
    showRecorder(show) {
      if (this.useVoiceMode) {
        // Send typing indicators when recorder UI is opened/closed
        this.$matrix.matrixClient.sendTyping(this.roomId, show, 10 * 60 * 1000);
      }
    }
  },

  methods: {
    /**
     * Set initialLoadDone to 'true'. This will process all events, setting threadParent and replyEvent if needed (see watcher for "initialLoadDone")
     */
    setInitialLoadDone() {
      this.initialLoadDone = true;
    },

    /**
     * Set events to display. At the same time, filter out messages that are past rentention period etc. Also, filter pinned events "at the top"
     */
    setEvents(events, onlyIfLengthChanges = false) {
      let updated = this.filterOutOldAndInvisible(events);

      // Handle pinning
      //
      if (this.room) {
        const pinnedEvents = this.$matrix.getPinnedEvents(this.room);
        updated.forEach((e) => {
          Vue.set(e, "isPinned", pinnedEvents.includes(e.threadParent ? e.threadParent.getId() : e.getId()));
          Vue.set(e, "isChannelMessage", (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL));
        });

        updated = updated.sort((e1, e2) => {
          if (!e1.isPinned && !e2.isPinned) return 0;
          else if (e1.isPinned && !e2.isPinned) return this.reverseOrder ? 1 : -1;
          else if (e2.isPinned && !e1.isPinned) return this.reverseOrder ? -1 : 1;
          else {
            // Look at order in "pinned" value in the m.room.pinned_events event!
            return pinnedEvents.indexOf(e1.getId()) < pinnedEvents.indexOf(e2.getId()) ? (this.reverseOrder ? 1 : -1) : (this.reverseOrder ? -1 : 1)
          }
        });
      }
      if (!onlyIfLengthChanges || updated.length != this.events.length) {
        this.events = updated; // Changed
      }
    },

    filterOutOldAndInvisible(events) {
      return this.removeTimedOutEvents(events.filter((e) => e.messageVisibility().visible));
    },

    updateRetentionTimer(maybeEvent) {
      const retentionEvent = maybeEvent || (this.room && this.room.currentState.getStateEvents("m.room.retention", ""));
      if (retentionEvent) {
        const maxLifetime = parseInt(retentionEvent.getContent().max_lifetime);
        if (maxLifetime) {
          if (!this.retentionTimer) {
            this.retentionTimer = setInterval(this.onRetentionTimer, 60000);
          }
          return;
        }
      }
      if (this.retentionTimer) {
          clearInterval(this.retentionTimer);
          this.retentionTimer = null;
        }
      },

    removeTimedOutEvents(events) {
      const retentionEvent = this.room && this.room.currentState.getStateEvents("m.room.retention", "");
      let maxLifetime = 0;
      if (retentionEvent) {
        maxLifetime = parseInt(retentionEvent.getContent().max_lifetime);
      }
      return events.filter((e) => {
        if (maxLifetime > 0 && !e.isState()) { // Keep all state events
          if (e.getLocalAge() < maxLifetime) {
            return true;
          }
          e.applyVisibilityEvent({ visible: false, eventId: e.getId(), reason: null});
          return true;
        }
        return true;
      });
    },

    onRetentionTimer() {
      this.setEvents(this.events, true);
    },

    onRoomJoined(initialEventId) {
      // If our own join event is less than a minute old, consider this a "newly joined" room.
      // 
      // Previously tried to look at initialEventId, but it seems like "this.room.getEventReadUpTo(this.$matrix.currentUserId, false)"
      // always returns an event id? Strange. I would expect it to be null on a fresh room.
      //
      const joinEvent = this.room && this.room.currentState.getStateEvents("m.room.member", this.$matrix.currentUserId);
      if (joinEvent) {
        this.newlyJoinedRoom = joinEvent.getLocalAge() < 1 * 60000 /* 1 minute */;
      }

      this.reverseOrder = (this.room && this.roomDisplayType == ROOM_TYPE_CHANNEL);
      Vue.set(this.room, "displayType", this.roomDisplayType);

      // Listen to events
      this.$matrix.on("Room.timeline", this.onEvent);
      this.$matrix.on("RoomMember.typing", this.onUserTyping);

      console.log("Read up to " + initialEventId);

      //initialEventId = null;
      this.timelineSet = this.room.getUnfilteredTimelineSet();
      this.timelineWindow = new TimelineWindow(this.$matrix.matrixClient, this.timelineSet, {});
      const self = this;
      this.timelineWindow
        .load(initialEventId, 20)
        .then(() => {
          self.setEvents(self.timelineWindow.getEvents());

          const getMoreIfNeeded = function _getMoreIfNeeded() {
            const container = self.$refs.chatContainer;
            if (
              container &&
              container.scrollHeight <= (1 + 2 * WINDOW_BUFFER_SIZE) * container.clientHeight &&
              self.timelineWindow &&
              self.timelineWindow.canPaginate(EventTimeline.BACKWARDS)
            ) {
              return self.timelineWindow.paginate(EventTimeline.BACKWARDS, 10, true, 5).then((success) => {
                self.setEvents(self.timelineWindow.getEvents());
                if (success) {
                  return _getMoreIfNeeded.call(self);
                } else {
                  return Promise.reject("Failed to paginate");
                }
              });
            } else {
              return Promise.resolve("Done");
            }
          }.bind(self);

          getMoreIfNeeded()
            .catch((err) => {
              console.log("ERROR " + err);
            })
            .finally(() => {
//                 const [timelineEvents, threadedEvents, unknownRelations] =
//                         this.room.partitionThreadedEvents(self.events);
// this.$matrix.matrixClient.processAggregatedTimelineEvents(this.room, timelineEvents);
//                     //room.addEventsToTimeline(timelineEvents, true, room.getLiveTimeline());
//                     this.$matrix.matrixClient.processThreadEvents(this.room, threadedEvents, true);
//                     unknownRelations.forEach((event) => this.room.relations.aggregateChildEvent(event));

              this.setInitialLoadDone();
                if (initialEventId && !this.showRoomWelcomeHeader) {
                  const event = this.room.findEventById(initialEventId);
                  this.$nextTick(() => {
                    if (event && event.parentThread) {
                    self.scrollToEvent(event.parentThread.getId());
                  } else {
                    self.scrollToEvent(initialEventId);
                  }
                  });
              } else if (this.showRoomWelcomeHeader) {
                self.onScroll();
              }
              self.restartRRTimer();
            });
        })
        .catch((err) => {
          console.log("Error fetching events!", err, this);
          if (initialEventId) {
            // Try again without initial event!
            this.onRoomJoined(null);
          } else {
            // Error. Done loading.
            this.setEvents(this.timelineWindow.getEvents());
            this.setInitialLoadDone();
          }
        })
        .finally(() => {
          for (var event of this.events) {
            this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
          }
        });
    },

    onRoomNotJoined() {
      this.$navigation.push(
        {
          name: "Join",
          params: { roomId: util.sanitizeRoomId(this.roomAliasOrId), join: this.$route.params.join },
        },
        0
      );
    },

    scrollToEndOfTimeline() {
      if (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS)) {
        this.loading = true;
        // Instead of paging though ALL history, just reload a timeline at the live marker...
        var timelineSet = this.room.getUnfilteredTimelineSet();
        var timelineWindow = new TimelineWindow(this.$matrix.matrixClient, timelineSet, {});
        const self = this;
        timelineWindow
          .load(null, 20)
          .then(() => {
            self.timelineSet = timelineSet;
            self.timelineWindow = timelineWindow;
            self.setEvents(self.timelineWindow.getEvents());
          })
          .finally(() => {
            this.loading = false;
          });
      } else {
        // Can't paginate, just scroll to bottom of window!
        this.smoothScrollToLatest();
      }
    },

    touchX(event) {
      if (event.type.indexOf("mouse") !== -1) {
        return event.clientX;
      }
      return event.touches[0].clientX;
    },
    touchY(event) {
      if (event.type.indexOf("mouse") !== -1) {
        return event.clientY;
      }
      return event.touches[0].clientY;
    },
    touchStart(e, event) {
      if (this.selectedEvent != event) {
        this.showContextMenu = false;
      }
      this.selectedEvent = event;
      this.touchStartX = this.touchX(e);
      this.touchStartY = this.touchY(e);
      this.touchTimer = setTimeout(this.touchTimerElapsed, 500);
    },
    touchEnd() {
      this.touchTimer && clearTimeout(this.touchTimer);
    },
    touchCancel() {
      this.touchTimer && clearTimeout(this.touchTimer);
    },
    touchMove(e) {
      this.touchCurrentX = this.touchX(e);
      this.touchCurrentY = this.touchY(e);
      var tapTolerance = 4;
      var touchMoved =
        Math.abs(this.touchStartX - this.touchCurrentX) > tapTolerance ||
        Math.abs(this.touchStartY - this.touchCurrentY) > tapTolerance;
      if (touchMoved) {
        this.touchTimer && clearTimeout(this.touchTimer);
      }
    },

    /**
     * Triggered when our "long tap" timer hits.
     */
    touchTimerElapsed() {
      this.updateRecentEmojis();
      this.showContextMenu = true;
    },

    /**
     * If chat container is shrunk (probably because soft keyboard is shown) adjust
     * the scroll position so that e.g. if we were looking at the last message when
     * moving focus to the input field, we would still see the last message. Otherwise
     * if would be hidden behind the keyboard.
     */
    handleChatContainerResize({ ignoredWidth, height }) {
      const delta = height - this.chatContainerSize;
      this.chatContainerSize = height;
      const container = this.chatContainer;
      if (container && delta < 0) {
        container.scrollTop -= delta;
      }
    },

    paginateBackIfNeeded() {
      this.$nextTick(() => {
        const container = this.chatContainer;
        if (container && container.scrollHeight <= container.clientHeight) {
          this.handleScrolledToOldest();
        }
      });
    },
    onScroll(ignoredevent) {
      const container = this.chatContainer;
      if (!container) {
        return;
      }
      const bufferHeight = container.clientHeight * WINDOW_BUFFER_SIZE;
      if (container.scrollTop <= bufferHeight) {
        // Scrolled to top
        if (this.reverseOrder) {
          this.handleScrolledToLatest(false);
        } else {
          this.handleScrolledToOldest();
        }
      } else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
        if (this.reverseOrder) {
          this.handleScrolledToOldest();
        } else {
          this.handleScrolledToLatest(false);
        }
      }

      this.showScrollToEnd =
        container.scrollHeight === container.clientHeight
          ? false
          : (this.reverseOrder ? (container.scrollTop.toFixed(0) > 0) : (container.scrollHeight - container.scrollTop.toFixed(0) > container.clientHeight)) ||
          (this.timelineWindow && this.timelineWindow.canPaginate(EventTimeline.FORWARDS));

      this.restartRRTimer();
    },

    setParentThread(event) {
        const parentEvent = this.timelineSet.findEventById(event.threadRootId) || this.room.findEventById(event.threadRootId);
        if (parentEvent) {
          Vue.set(parentEvent, "isMxThread", true);
          Vue.set(event, "parentThread", parentEvent);
        } else {
          // Try to load from server.
          this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.threadRootId)
            .then((tl) => {
            if (tl) {
              const parentEvent = tl.getEvents().find((e) => e.getId() === event.threadRootId);
              if (parentEvent) {
                this.setEvents(this.timelineWindow.getEvents());
                const fn = () => {
                  Vue.set(parentEvent, "isMxThread", true);
                  Vue.set(event, "parentThread", parentEvent);
                };
                if (this.initialLoadDone) {
                  const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
                  const element = document.querySelector(sel);
                  if (element) {
                    this.onLayoutChange(fn, element);
                  } else {
                    fn();
                  }
                } else {
                  fn();
                }
              }
            }
          }).catch(e => console.error(e));
        }
    },

    setReplyToEvent(event) {
        const parentEvent = this.timelineSet.findEventById(event.replyEventId) || this.room.findEventById(event.replyEventId);
        if (parentEvent) {
          Vue.set(event, "replyEvent", parentEvent);
        } else {
          // Try to load from server.
          this.$matrix.matrixClient.getEventTimeline(this.timelineSet, event.replyEventId)
            .then((tl) => {
            if (tl) {
              const parentEvent = tl.getEvents().find((e) => e.getId() === event.replyEventId);
              if (parentEvent) {
                this.setEvents(this.timelineWindow.getEvents());
                const fn = () => {Vue.set(event, "replyEvent", parentEvent);};
                if (this.initialLoadDone) {
                  const sel = "[eventId=\"" + parentEvent.getId() + "\"]";
                  const element = document.querySelector(sel);
                  if (element) {
                    this.onLayoutChange(fn, element);
                  } else {
                    fn();
                  }
                } else {
                  fn();
                }
              }
            }
          }).catch(e => console.error(e));
        }
    },

    onEvent(event) {
      //console.log("OnEvent", JSON.stringify(event));
      if (event.getRoomId() !== this.roomId) {
        return; // Not for this room
      }

      if (this.initialLoadDone && event.threadRootId && !event.parentThread) {
        this.setParentThread(event);
      }
      if (this.initialLoadDone && event.replyEventId && !event.replyEvent) {
        this.setReplyToEvent(event);
      }

      const loadingDone = this.initialLoadDone;
      this.$matrix.matrixClient.decryptEventIfNeeded(event, {});

      if (this.initialLoadDone && !this.useVoiceMode) {
        this.paginateBackIfNeeded();
      }

      if (loadingDone && event.forwardLooking && (!event.isRelation() || event.isMxThread || event.threadRootId || event.parentThread)) {
        // If we are at bottom, scroll to see new events...
        var scrollToSeeNew = !event.isRedaction() && event.getSender() == this.$matrix.currentUserId; // When we sent, scroll
        const container = this.chatContainer;
        if (container) {
          if (this.reverseOrder && container.scrollTop.toFixed(0) == 0) {
            scrollToSeeNew = true;
          }
          else if (!this.reverseOrder && container.scrollHeight - container.scrollTop.toFixed(0) == container.clientHeight) {
            scrollToSeeNew = true;
          }
        }
        this.handleScrolledToLatest(scrollToSeeNew);

        // If kick or ban event, redirect to "goodbye"...
        if (event.getType() === "m.room.member" &&
          event.getStateKey() == this.$matrix.currentUserId &&
          (event.getPrevContent() || {}).membership == "join" &&
          (
            (event.getContent().membership == "leave" && event.getSender() != this.currentUserId) ||
            (event.getContent().membership == "ban"))
        ) {
          this.$store.commit("setCurrentRoomId", null);
          const wasPurged = event.getContent().reason == "Room Deleted";
          this.$navigation.push({ name: "Goodbye", params: { roomWasPurged: wasPurged } }, -1);
        }

        else if (event.getType() === "m.room.retention") {
          this.updateRetentionTimer(event);
        }
      }
    },

    onUserTyping(event, member) {
      if (member.roomId !== this.roomId) {
        return; // Not for this room
      }
      if (member.typing) {
        if (!this.typingMembers.includes(member)) {
          this.typingMembers.push(member);
        }
      } else {
        const index = this.typingMembers.indexOf(member);
        if (index > -1) {
          this.typingMembers.splice(index, 1);
        }
      }
      //console.log("Typing: ", this.typingMembers);
    },

    sendCurrentTextMessage() {
      // DOn't have "enter" send messages while in recorder.
      if (this.currentInput.length > 0 && !this.showRecorder) {
        this.sendMessage(this.currentInput);
        this.currentInput = "";
        this.editedEvent = null; //TODO - Is this a good place to reset this?
        this.replyToEvent = null;
      }
    },

    sendMessage(text) {
      if (text && text.length > 0) {
        util
          .sendTextMessage(this.$matrix.matrixClient, this.roomId, text, this.editedEvent, this.replyToEvent)
          .then(() => {
            console.log("Sent message");
          })
          .catch((err) => {
            console.log("Failed to send:", err);
          });
      }
    },

    /**
     * Show attachment picker to select file
     */
    showAttachmentPicker() {
      this.$refs.attachment.click();
    },

    optimizeImage(evt,file) {
      let fileObj = {}
      fileObj.image = evt.target.result;
      fileObj.dimensions = null;
      fileObj.type = file.type;
      fileObj.actualSize = file.size;
      fileObj.actualFile = file
      try {
        fileObj.dimensions = sizeOf(dataUriToBuffer(evt.target.result));

        // Need to resize?
        const w = fileObj.dimensions.width;
        const h = fileObj.dimensions.height;
        if (w > 640 || h > 640) {
          var aspect = w / h;
          var newWidth = parseInt((w > h ? 640 : 640 * aspect).toFixed());
          var newHeight = parseInt((w > h ? 640 / aspect : 640).toFixed());
          var imageResize = new ImageResize({
            format: "png",
            width: newWidth,
            height: newHeight,
            outputType: "blob",
          });
          imageResize
            .play(evt.target.result)
            .then((img) => {
              Vue.set(
                fileObj,
                "scaled",
                new File([img], file.name, {
                  type: img.type,
                  lastModified: Date.now(),
                })
              );
              Vue.set(fileObj, "useScaled", true);
              Vue.set(fileObj, "scaledSize", img.size);
              Vue.set(fileObj, "scaledDimensions", {
                width: newWidth,
                height: newHeight,
              });
            })
            .catch((err) => {
              console.error("Resize failed:", err);
            });
        }
      } catch (error) {
        console.error("Failed to get image dimensions: " + error);
      }
      return fileObj
    },
    handleFileReader(file) {
      if (file) {
        let optimizedFileObj;
        var reader = new FileReader();
        reader.onload = (evt) => {
          if (file.type.startsWith("image/")) {
            optimizedFileObj = this.optimizeImage(evt, file)
          } else {
            optimizedFileObj = file
          }
          this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, optimizedFileObj] : [optimizedFileObj];
        };
        reader.readAsDataURL(file);
      }
    },
    /**
     * Handle picked attachment
     */
    handlePickedAttachment(event) {
       this.currentFileInputs = []
        const uploadedFiles = Object.values(event.target.files);

        this.$matrix.matrixClient.getMediaConfig().then((config) => {
          const configUploadSize = config["m.upload.size"];
          const configFormattedUploadSize = this.formatBytes(configUploadSize);

          uploadedFiles.every(file => {
            if (configUploadSize && file.size > configUploadSize) {
              this.currentSendError = this.$t("message.upload_file_too_large");
              this.currentSendErrorExceededFile = this.$t("message.upload_exceeded_file_limit", { configFormattedUploadSize });
              this.currentSendShowSendButton = false;
              return false;
            } else {
              this.currentSendShowSendButton = true;
            }
            return true;
          });

          uploadedFiles.forEach(file => this.handleFileReader(file));

        });
    },

    showStickerPicker() {
      this.$refs.stickerPickerSheet.open();
    },

    sendAttachment(withText) {
      this.$refs.attachment.value = null;
      if (this.isCurrentFileInputsAnArray) {
        const text = withText || "";
        const promise = this.sendAttachments(text, this.currentFileInputs);
        promise.then(() => {
          this.currentFileInputs = null;
          this.attachmentCaption = undefined;
          this.sendingStatus = this.sendStatuses.INITIAL;
        })
        .catch((err) => {
          if (err.name === "AbortError" || err === "Abort") {
            this.currentSendError = null;
            this.currentSendErrorExceededFile = null;
          } else {
            this.currentSendError = err.LocaleString();
            this.currentSendErrorExceededFile = err.LocaleString();
          }
        });
      }
    },

    cancelSendAttachment() {
      this.$refs.attachment.value = null;
      if (this.sendingStatus != this.sendStatuses.INITIAL) {
        this.cancelSendAttachments();
      }
      this.currentFileInputs = null;
      this.attachmentCaption = undefined;
      this.currentSendError = null;
      this.currentSendErrorExceededFile = null;
      this.sendingStatus = this.sendStatuses.INITIAL;
    },

    addAttachment(file) {
      this.handleFileReader(null, file);
    },

    resetAttachments() {
      this.cancelSendAttachment();
    },

    /**
     * Called by message components that need to change their layout. This will avoid "jumping" in the UI, because
     * we remember scroll position, apply the layout change, then restore the scroll.
     * NOTE: we use "parentElement" below, because it is expected to be called with "element" set to the message component
     * and the message component in turn being wrapped by a "message-wrapper" element (see html above).
     * @param {} action A function that performs desired layout changes.
     * @param {*} element Root element for the chat message.
     */
    onLayoutChange(action, element) {
      if (!element || !element.parentElement || this.useVoiceMode || this.useFileModeNonAdmin) {
        action();
        return
      }
      const container = this.chatContainer;
      this.scrollPosition.prepareFor(element.parentElement.offsetTop >= container.scrollTop ? "down" : "up");
      action();
      this.$nextTick(() => {
        // restore scroll position!
        this.scrollPosition.restore();
      });
    },

    handleScrolledToOldest() {
      if (
        this.timelineWindow &&
        this.timelineWindow.canPaginate(EventTimeline.BACKWARDS) &&
        !this.timelineWindowPaginating
      ) {
        this.timelineWindowPaginating = true;
        this.timelineWindow
          .paginate(EventTimeline.BACKWARDS, 10, true)
          .then((success) => {
            if (success && this.scrollPosition) {
              this.scrollPosition.prepareFor(this.reverseOrder ? "down" : "up");
              this.setEvents(this.timelineWindow.getEvents());
              this.$nextTick(() => {
                // restore scroll position!
                console.log("Restore scroll!");
                this.scrollPosition.restore();
              });
            }
          })
          .finally(() => {
            this.timelineWindowPaginating = false;
          });
      }
    },

    handleScrolledToLatest(smoothScrollToLatest) {
      if (
        this.timelineWindow &&
        this.timelineWindow.canPaginate(EventTimeline.FORWARDS) &&
        !this.timelineWindowPaginating
      ) {
        this.timelineWindowPaginating = true;
        this.timelineWindow
          .paginate(EventTimeline.FORWARDS, 10, true)
          .then((success) => {
            if (success) {
              this.setEvents(this.timelineWindow.getEvents());
              if (!this.useVoiceMode && this.scrollPosition) {
                this.scrollPosition.prepareFor(this.reverseOrder ? "up" : "down");
                this.$nextTick(() => {
                  // restore scroll position!
                  console.log("Restore scroll!");
                  this.scrollPosition.restore();
                  if (smoothScrollToLatest) {
                    this.smoothScrollToLatest();
                  }
                });
              }
            }
          })
          .finally(() => {
            this.timelineWindowPaginating = false;
          });
      }
    },

    /**
     * Scroll so that the given event is at the middle of the chat view (if more events) or else at the bottom.
     */
    scrollToEvent(eventId) {
      console.log("Scroll to event", eventId);
      const container = this.chatContainer;
      const ref = this.$refs[eventId];
      if (container && ref) {
        const parent = container.getBoundingClientRect();
        const item = ref[0].getBoundingClientRect();
        let offsetY = (parent.bottom - parent.top) / 2;
        if (ref[0].clientHeight > offsetY) {
          offsetY = this.reverseOrder ? 0 : Math.max(0, (parent.bottom - parent.top) - ref[0].clientHeight);
        }
        const targetY = parent.top + offsetY;
        const currentY = item.top;
        const y = container.scrollTop + (currentY - targetY);
        this.$nextTick(() => {
          container.scrollTo(0, y);
        });
      }
    },

    smoothScrollToLatest() {
      this.$nextTick(function () {
        const container = this.chatContainer;
        if (container && container.children.length > 0) {
          if (this.reverseOrder) {
            const firstChild = container.children[0];
          console.log("Scroll into view", firstChild);
          window.requestAnimationFrame(() => {
            firstChild.scrollIntoView({
              behavior: "smooth",
              block: "end",
              inline: "nearest",
            });
          });
          } else {
          const lastChild = container.children[container.children.length - 1];
          console.log("Scroll into view", lastChild);
          window.requestAnimationFrame(() => {
            lastChild.scrollIntoView({
              behavior: "smooth",
              block: "start",
              inline: "nearest",
            });
          });
        }
      }
      });
    },

    showMoreMessageOperations(e) {
      this.addReaction(e);
    },

    addReaction(e) {
      const event = e.event;
      // Store the event we are reacting to, so that we know where to
      // send when the picker closes.
      this.selectedEvent = event;
      this.$refs.messageOperationsSheet.open();
      this.showEmojiPicker = true;
    },

    addQuickReaction(e) {
      this.sendQuickReaction({ reaction: e.emoji, event: e.event });
    },

    addQuickHeartReaction(e) {
      this.heartPosition = e.position
      this.sendQuickReaction({ reaction: this.heartEmoji, event: e.event }, true);
    },

    setReplyToImage(event) {
      util
        .getThumbnail(this.$matrix.matrixClient, event, this.$config)
        .then((url) => {
          this.replyToImg = url;
        })
        .catch((err) => {
          console.log("Failed to fetch thumbnail: ", err);
        });
    },

    addReply(event) {
      this.replyToEvent = event;
      this.$refs.messageInput.focus();
      if (event.parentThread || event.isThreadRoot || event.isMxThread) {
        this.replyToContentType = util.threadMessageType();
      } else {
        this.replyToContentType = event.getContent().msgtype || 'm.poll';
      }
      this.setReplyToImage(event);
    },

    edit(event) {
      this.editedEvent = event;
      this.currentInput = event.getContent().body;
      this.$refs.messageInput.focus();
    },

    redact(event) {
      let promises = [];
      if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
        // If this is a thread message, make sure to redact all children as well.
        const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
        promises = children.map((c) => {
          return this.$matrix.matrixClient.redactEvent(c.getRoomId(), c.getId(), undefined, { reason: "redactedMedia"});
        });
      }
      promises.push(this.$matrix.matrixClient.redactEvent(event.getRoomId(), event.getId(), undefined, { reason: "redactedThread"}));
      Promise.allSettled(promises)
        .then(() => {
          console.log("Message redacted");
        })
        .catch((err) => {
          console.log("Redaction failed: ", err);
        });
    },

    download(event) {
      if ((event.isThreadRoot || event.isMxThread) && this.timelineSet) {
        const children = this.timelineSet.relations.getAllChildEventsForEvent(event.getId()).filter(e => util.downloadableTypes().includes(e.getContent().msgtype));
        children.forEach(child => util.download(this.$matrix.matrixClient, child));
      } else {
        util.download(this.$matrix.matrixClient, event);
      }
    },

    pin(event) {
      const eventToPin = event.parentThread ? event.parentThread : event;
      this.$matrix.setEventPinned(this.room, eventToPin, true);
    },

    unpin(event) {
      const eventToUnpin = event.parentThread ? event.parentThread : event;
      this.$matrix.setEventPinned(this.room, eventToUnpin, false);
    },

    cancelEditReply() {
      this.currentInput = "";
      this.editedEvent = null;
      this.replyToEvent = null;
    },

    emojiSelected(e) {
      if (this.isEmojiQuickReaction) {
        // When quick emoji picker is clicked
        if (this.selectedEvent) {
          const event = this.selectedEvent;
          this.selectedEvent = null;
          this.sendQuickReaction({ reaction: e.data, event: event });
        }
      } else {
        // When text input emoji picker is clicked
        this.currentInput = `${this.currentInput} ${e.data}`;
        this.$refs.messageInput.focus();
      }

      this.showEmojiPicker = false;
      this.$refs.messageOperationsSheet.close();
    },

    sendClapReactionAtTime(e) {
      util
        .sendQuickReaction(this.$matrix.matrixClient, this.roomId, "👏", e.event, { timeOffset: e.timeOffset.toFixed(0)})
        .then(() => {
          console.log("Send clap reaction at time", e.timeOffset);
        })
        .catch((err) => {
          console.log("Failed to send clap reaction:", err);
        });
    },

    showHeartAnimation() {
      const self = this;
      this.heartAnimation = true;
      setTimeout(() => {
        self.heartAnimation = false;
      }, 1000)
    },

    sendQuickReaction(e, heartAnimationFlag = false) {
      let previousReaction = null;

      // Figure out if we have already sent this emoji, in that case redact it again (toggle)
      //
      const reactions = this.timelineSet.relations.getChildEventsForEvent(e.event.getId(), 'm.annotation', 'm.reaction');
      if (reactions && reactions._eventsCount > 0) {
        const relations = reactions.getRelations();
        for (const r of relations) {
          const emoji = r.getRelation().key;
          const sender = r.getSender();
          if (emoji == e.reaction && sender == this.$matrix.currentUserId) {
            previousReaction = r.isRedacted() ? null : r;
          }
        }
      }
      if (previousReaction) {
        this.redact(previousReaction);
      } else {
      util
        .sendQuickReaction(this.$matrix.matrixClient, this.roomId, e.reaction, e.event)
        .then(() => {
          console.log("Quick reaction message");
        })
        .catch((err) => {
          console.log("Failed to send quick reaction:", err);
        });

        if(heartAnimationFlag) {
          this.showHeartAnimation();
        }
      }
    },

    sendSticker(stickerShortCode) {
      this.sendMessage(stickerShortCode);
    },

    showContextMenuForEvent(e) {
      const event = e.event;
      if (this.selectedEvent == event) {
        this.showContextMenuAnchor = e.anchor;
        this.showContextMenu = !this.showContextMenu;
      } else {
        this.showContextMenu = false;
        this.$nextTick(() => {
          this.selectedEvent = event;
          this.updateRecentEmojis();
          this.showContextMenuAnchor = e.anchor;
          this.showContextMenu = true;
        })
      }
    },

    showAvatarMenuForEvent(e) {
      const event = e.event;
      this.selectedEvent = event;

      this.showProfileDialog = true
    },

    viewProfile() {
      this.$navigation.push({ name: "Profile" }, 1);
    },

    closeContextMenusIfOpen(e) {
      if (this.showContextMenu) {
        this.showContextMenu = false;
        this.showContextMenuAnchor = null;
        this.selectedEvent = null;
        e.preventDefault();
      }
    },

    /** Stop Read Receipt timer */
    stopRRTimer() {
      if (this.rrTimer) {
        clearTimeout(this.rrTimer);
        this.rrTimer = null;
      }
    },

    /**
     * Start/restart the timer to Read Receipts.
     */
    restartRRTimer() {
      this.stopRRTimer();

      if (this.$matrix.currentRoomBeingPurged) {
        return;
      }
      if (!this.useVoiceMode && !this.useFileModeNonAdmin) {
        this.rrTimer = setTimeout(() => { this.rrTimerElapsed() }, READ_RECEIPT_TIMEOUT);
      }
    },

    rrTimerElapsed() {
      this.rrTimer = null;
      this.sendRR();
      this.restartRRTimer();
    },

    sendRR(onlyTheseEvents) {
      let lastTimestamp = 0;
      if (this.lastRR) {
        lastTimestamp = this.lastRR.getTs();
      }

      let eventsToConsider = onlyTheseEvents;
      if (!eventsToConsider) {
        // If events to consider is not given (used by Audio layout) then consider all visible events (with some restrictions).
        let visibleEventIds = util.findVisibleElements(this.chatContainer).filter(el => el.hasAttribute("eventId")).map(el => el.getAttribute("eventId"));

        eventsToConsider = this.events.filter(e => {
          if (e.getTs() > lastTimestamp &&
            // Make sure it's not a local echo event...
            !e.getId().startsWith("~")) {
            if (e.isRelation()) {
              return visibleEventIds.includes(e.getAssociatedId());
            } else if (e.getType() == "m.room.pinned_events") {
              // Ignore the pin event. If we pin an older message it does not automatically mean we have read the
              // messages between.
              return false;
            } else {
              return visibleEventIds.includes(e.getId());
            }
          }
          return false;
        });
      }

      eventsToConsider.sort((a, b) => b.getTs() - a.getTs());
      if (eventsToConsider.length > 0 && eventsToConsider[0] != this.lastRR) {
        const eventToSendRRFor = eventsToConsider[0];

        // Send read receipt
        this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, eventToSendRRFor.getId(), eventToSendRRFor)
          .then(() => {
            console.log("RR sent for event", eventToSendRRFor.getId(), eventToSendRRFor.getType());
            this.lastRR = eventToSendRRFor;
          })
          .catch((err) => {
            console.log("Failed to update read marker: ", err);
          })
          .finally(() => {
            this.restartRRTimer();
          });
      }
    },

    showRecordingUI() {
      this.showRecorderPTT = false;
      this.showRecorder = true;
    },

    startRecording() {
      this.showRecorderPTT = true;
      this.showRecorder = true;
    },

    onVoiceRecording(event) {
      this.currentSendShowSendButton = false;
      this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, event.file] : [event.file];
      var text = undefined;
      if (this.currentInput && this.currentInput.length > 0) {
        text = this.currentInput;
        this.currentInput = "";
      }
      this.sendAttachment(text);
      this.showRecorder = false;

      // Log event
      this.$analytics.event("Audio", "Voice message sent");
    },

    closeRoomWelcomeHeader() {
      this.hideRoomWelcomeHeader = true;
      this.$nextTick(() => {
        // We change the layout when removing the welcome header, so call
        // onScroll here to handle updates (e.g. remove the "scroll to last" if we now
        // can see all messages).
        this.onScroll();
      });
    },

    updateRecentEmojis() {
      if (this.$refs.emojiPicker) {
        this.recentEmojis = this.$refs.emojiPicker.mapEmojis["Frequently"];
        if (this.recentEmojis.length < 20) {
          let peoples = this.$refs.emojiPicker.mapEmojis["Peoples"];
          for (var p of peoples) {
            this.recentEmojis.push(p);
          }
        }
        return;
      }
      this.recentEmojis = [];
    },

    formatBytes(bytes) {
      return prettyBytes(bytes);
    },

    onHeaderClick() {
      const invitations = this.$matrix.invites.length;
      const joinedRooms = this.$matrix.joinedRooms;
      if (invitations == 0 && joinedRooms && joinedRooms.length == 1 && joinedRooms[0].roomId == this.room.roomId) {
        // Only joined to this room, go directly to room details!
        this.$navigation.push({ name: "RoomInfo" });
        return;
      }
      this.$refs.roomInfoSheet.open();
    },
    viewRoomDetails() {
      this.$navigation.push({ name: "RoomInfo" });
    },
    pollWasClosed(ignoredE) {
      let div = document.createElement("div");
      div.classList.add("toast");
      div.innerText = this.$t("poll_create.results_shared");
      this.chatContainer.parentElement.appendChild(div);
      setTimeout(() => {
        this.chatContainer.parentElement.removeChild(div);
      }, 3000);
    },
    setShowRecorder() {
      if (this.canRecordAudio) {
        this.showRecorder = true;
      } else {
        this.showNoRecordingAvailableDialog = true;
      }
    },

    /**
     * Called when an audio message has played to the end. We listen to this so we can optionally auto-play
     * the next audio event.
     * @param matrixEvent The event that stopped playing
     */
    audioPlaybackEnded(matrixEventId) {
      if (!this.useVoiceMode) { // Voice mode has own autoplay handling inside "AudioLayout"!
        // Auto play consecutive audio messages, either incoming or sent.
        let event = this.events.find(e => e.getId() === matrixEventId);
        if (event) {
          event = event.parentThread ? event.parentThread : event;
          if (event.nextDisplayedEvent) {
            const nextEvent = event.nextDisplayedEvent;
            if (nextEvent.getContent().msgtype === "m.audio") {
              // Yes, audio event!
              this.$audioPlayer.play(nextEvent, this.timelineSet);
            } else if (nextEvent.isMxThread) {
              const children = this.timelineSet.relations.getAllChildEventsForEvent(nextEvent.getId());
              if (children && children.length > 0 && children[0].getContent().msgtype === "m.audio") {
                // Yes, audio event!
                this.$audioPlayer.play(children[0], this.timelineSet);
              }
            }
          }
        }
      }
    }
  },
};
</script>

<style lang="scss">
@import "@/assets/css/chat.scss";
.heart-wrapper {
  position: fixed;
  z-index: -1;

  .heart {
    width: 100px;
    height: 100px;
    background: url("../assets/heart.png") no-repeat;
    background-position: 0 0;
    cursor: pointer;
    transition: background-position 1s steps(28);
    transition-duration: 0s;
    visibility: hidden;


    &.is-active {
      transition-duration: 1s;
      background-position: -2800px 0;
      visibility: visible;
      z-index: 10000;
    }
  }
  &.is-active {
    z-index: 1000;
    top: var(--top);
    left: var(--left);
  }
}
</style>
