<template>
  <div class="chat-root fill-height d-flex flex-column">
    <ChatHeader class="chat-header flex-grow-0 flex-shrink-0" v-on:header-click="onHeaderClick" v-on:view-room-details="viewRoomDetails" />
    <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="handleScrolledToBottom(false)"
      v-on:loadprevious="handleScrolledToTop()"
      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" />


    <div v-if="!useVoiceMode" class="chat-content flex-grow-1 flex-shrink-1" ref="chatContainer"
      v-on:scroll="onScroll" @click="closeContextMenusIfOpen">
      <div ref="messageOperationsStrut" class="message-operations-strut">
        <message-operations ref="messageOperations" :style="opStyle" :emojis="recentEmojis" v-on:close="
          showContextMenu = false;
          showContextMenuAnchor = null;
        " v-if="showMessageOperations" 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)
          " :originalEvent="selectedEvent" />
      </div>

      <div ref="avatarOperationsStrut" class="avatar-operations-strut">
        <avatar-operations ref="avatarOperations" :style="avatarOpStyle" v-on:close="
          showAvatarMenu = false;
          showAvatarMenuAnchor = null;
        " v-on:start-private-chat="startPrivateChat($event)" v-if="selectedEvent && showAvatarMenu" :room="room"
          :originalEvent="selectedEvent" />
      </div>

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

      <CreatedRoomWelcomeHeader v-if="showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" />

      <div v-for="(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()">
        <!-- DAY Marker, shown for every new day in the timeline -->
        <div v-if="showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" class="day-marker" :title="dayForEvent(event)" />

        <div v-if="!event.isRelation() && !event.isRedaction()" :ref="event.getId()">
          <div class="message-wrapper" v-on:touchstart="
            (e) => {
              touchStart(e, event);
            }
          " v-on:touchend="touchEnd" v-on:touchcancel="touchCancel" v-on:touchmove="touchMove">
            <component :is="componentForEvent(event)" :room="room" :originalEvent="event" :nextEvent="filteredEvents[index + 1]"
              :timelineSet="timelineSet" v-on:send-quick-reaction.stop="sendQuickReaction"
              v-on:context-menu="showContextMenuForEvent($event)" v-on:own-avatar-clicked="viewProfile"
              v-on:other-avatar-clicked="showAvatarMenuForEvent($event)" v-on:download="download(event)"
              v-on:poll-closed="pollWasClosed(event)"
              v-on:more="
                isEmojiQuickReaction = true
                showMoreMessageOperations($event)
                "
              />
            <!-- <div v-if="debugging" style="user-select:text">EventID: {{ event.getId() }}</div> -->
            <!-- <div v-if="debugging" style="user-select:text">Event: {{ JSON.stringify(event) }}</div> -->
            <div v-if="event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"
              :title="$t('message.unread_messages')" />
          </div>
        </div>
      </div>

      <NoHistoryRoomWelcomeHeader v-if="showNoHistoryRoomWelcomeHeader" />
    </div>

    <!-- Input area -->
    <v-container v-if="!useVoiceMode && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']">
      <div :class="[replyToEvent ? 'iput-area-inner-box' : '']">
        <!-- "Scroll to end"-button -->
        <v-btn v-if="!useVoiceMode" class="scroll-to-end" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
          @click.stop="scrollToEndOfTimeline">
          <v-icon color="white">arrow_downward</v-icon>
        </v-btn>

        <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.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-if="!showRecorder && !$matrix.currentRoomIsReadOnlyForUser">
          <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.currentRoomIsReadOnlyForUser" 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/*, .pdf" class="d-none" multiple/>

    <div v-if="currentFileInputsDialog">
      <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-title>{{ $t('message.send_attachements_dialog_title') }}</v-card-title>
          <v-divider></v-divider>
          <template v-if="Array.isArray(currentImageInputs) && currentImageInputs.length">
            <v-card-title v-if="currentImageInputs.length > 1"> {{ $t('message.images') }} </v-card-title>
            <v-card-text :class="{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': currentImageInputs.length > 1}">
              <div :class="{'col-4': currentImageInputs.length > 1}" v-for="(currentImageInput, id) in currentImageInputs" :key="id">
                <v-img v-if="currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
                  contain class="current-image-input-path" />
                <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>
                  <v-switch v-if="currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
                    v-model="currentImageInput.useScaled" />
                </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>
                </div>
              </div>
            </v-card-text>
          </template>
          <v-divider></v-divider>
          <v-card-actions>
            <v-spacer>
              <div v-if="currentSendError">{{ currentSendError }}</div>
              <div v-else>{{ currentSendProgress }}</div>
            </v-spacer>
            <v-btn color="primary" text @click="cancelSendAttachment" id="btn-attachment-cancel">
              {{ $t("menu.cancel") }}
            </v-btn>
            <v-btn id="btn-attachment-send" color="primary" text @click="sendAttachment"
              v-if="currentSendShowSendButton" :disabled="currentSendOperation != null">{{ $t("menu.send") }}</v-btn>
          </v-card-actions>
        </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" />
  </div>
</template>

<script>
import Vue from "vue";
import { TimelineWindow, EventTimeline } from "matrix-js-sdk";
import util from "../plugins/utils";
import MessageOperations from "./messages/MessageOperations.vue";
import AvatarOperations from "./messages/AvatarOperations.vue";
import ChatHeader from "./ChatHeader";
import VoiceRecorder from "./VoiceRecorder";
import RoomInfoBottomSheet from "./RoomInfoBottomSheet";
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader";
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue";
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet";
import StickerPickerBottomSheet from "./StickerPickerBottomSheet";
import BottomSheet from "./BottomSheet.vue";
import ImageResize from "image-resize";
import CreatePollDialog from "./CreatePollDialog.vue";
import chatMixin from "./chatMixin";
import AudioLayout from "./AudioLayout.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],
  components: {
    ChatHeader,
    MessageOperations,
    VoiceRecorder,
    RoomInfoBottomSheet,
    CreatedRoomWelcomeHeader,
    NoHistoryRoomWelcomeHeader,
    MessageOperationsBottomSheet,
    StickerPickerBottomSheet,
    BottomSheet,
    AvatarOperations,
    CreatePollDialog,
    AudioLayout
  },

  data() {
    return {
      waitingForRoomObject: false,
      events: [],
      currentInput: "",
      typingMembers: [],
      timelineSet: null,
      timelineWindow: null,

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

      scrollPosition: null,
      currentImageInputs: null,
      currentFileInputs: null,
      currentSendOperation: null,
      currentSendProgress: null,
      currentSendShowSendButton: true,
      currentSendError: null,
      showEmojiPicker: false,
      selectedEvent: null,
      editedEvent: null,
      replyToEvent: null,
      replyToImg: null,
      replyToContentType: null,
      showCreatePollDialog: false,
      showNoRecordingAvailableDialog: false,
      showContextMenu: false,
      showContextMenuAnchor: null,
      showAvatarMenu: false,
      showAvatarMenuAnchor: 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 */
      showCreatedRoomWelcomeHeader: 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")
        }
      }
    };
  },

  filters: {
    latestReply(contents) {
      const contentArr = contents.split("\n").reverse();
      if (contentArr[0] === "") {
        contentArr.shift();
      }
      return 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();
  },

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

  computed: {
    nonImageFiles() {
      return this.isCurrentFileInputsAnArray && this.currentFileInputs.filter(file => !file.type.includes("image/"))
    },
    isCurrentFileInputsAnArray() {
      return Array.isArray(this.currentFileInputs)
    },
    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;
    },
    avatarOpStyle() {
      // Calculate where to show the context menu.
      //
      const ref = this.selectedEvent && this.$refs[this.selectedEvent.getId()];
      var top = 0;
      var left = "unset";
      var right = "unset";
      if (ref && ref[0]) {
        if (this.showAvatarMenuAnchor) {
          var rectAnchor = this.showAvatarMenuAnchor.getBoundingClientRect();
          var rectChat = this.$refs.avatarOperationsStrut.getBoundingClientRect();
          top = rectAnchor.top - rectChat.top;
          if (this.$vuetify.rtl) {
            right = (rectAnchor.right - rectChat.right)+ "px";
          } else {
            left = (rectAnchor.left - rectChat.left) + "px";
          }
          // if (left + 250 > rectChat.right) {
          //   left = rectChat.right - 250; // Pretty ugly, but we want to make sure it does not escape the screen, and we don't have the exakt width of it (yet)!
          // }
        }
      }
      return "top:" + top + "px;left:" + left + ";right:" + right;
    },
    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.useVoiceMode(this.room);
      },
    },
    /**
     * 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() {
      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.
              return this.events.slice(idx + 1);
            }
          }
        }
      }
      return this.events;
    }
  },

  watch: {
    initialLoadDone: {
      immediate: true,
      handler(value, oldValue) {
        if (value && !oldValue) {
          console.log("Loading finished!");
        }
      }
    },
    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.showCreatedRoomWelcomeHeader = 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.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.initialLoadDone = true;
          return; // no room
        }
      },
    },
    room() {
      // Were we waiting?
      if (this.room && this.room.roomId == this.roomId && this.waitingForRoomObject) {
        this.waitingForRoomObject = false;
        this.onRoomJoined(this.readMarker);
      }
    },
    showMessageOperations() {
      if (this.showMessageOperations) {
        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();
              top = rectAnchor.top - rectChat.top - 50;
              left = rectAnchor.left - rectChat.left - 75;
              if (left + rectOps.width >= rectChat.right) {
                left = rectChat.right - rectOps.width - 10; // No overflow
              }
            }
          }
          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: {
    onRoomJoined(initialEventId) {
      // Was this room just created (by you)? Show a small info header in
      // that case!
      const createEvent = this.room.currentState.getStateEvents("m.room.create", "");
      if (createEvent) {
        const creatorId = createEvent.getContent().creator;
        if (creatorId == this.$matrix.currentUserId && createEvent.getLocalAge() < 5 * 60000 /* 5 minutes */) {
          this.showCreatedRoomWelcomeHeader = true;
        }
      }

      // 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.events = 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.events = 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(() => {
              self.initialLoadDone = true;
              if (initialEventId && !this.showCreatedRoomWelcomeHeader) {
                self.scrollToEvent(initialEventId);
              } else if (this.showCreatedRoomWelcomeHeader) {
                self.onScroll();
              }
              self.restartRRTimer();
            });
        })
        .catch((err) => {
          console.log("Error fetching events!", err, this);
          if (err.errcode == "M_UNKNOWN" && initialEventId) {
            // Try again without initial event!
            this.onRoomJoined(null);
          } else {
            // Error. Done loading.
            this.events = this.timelineWindow.getEvents();
            this.initialLoadDone = true;
          }
        })
        .finally(() => {
          for (var event of this.events) {
            this.$matrix.matrixClient.decryptEventIfNeeded(event, {});
          }
        });
    },

    onRoomNotJoined() {
      this.$navigation.push(
        {
          name: "Join",
          params: { roomId: util.sanitizeRoomId(this.roomAliasOrId) },
        },
        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.events = self.timelineWindow.getEvents();
          })
          .finally(() => {
            this.loading = false;
          });
      } else {
        // Can't paginate, just scroll to bottom of window!
        this.smoothScrollToEnd();
      }
    },

    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.handleScrolledToTop();
        }
      });
    },
    onScroll(ignoredevent) {
      const container = this.chatContainer;
      if (!container) {
        return;
      }
      const bufferHeight = container.clientHeight * WINDOW_BUFFER_SIZE;
      if (container.scrollTop <= bufferHeight) {
        // Scrolled to top
        this.handleScrolledToTop();
      } else if (container.scrollHeight - container.scrollTop.toFixed(0) - container.clientHeight <= bufferHeight) {
        this.handleScrolledToBottom(false);
      }

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

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

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

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

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

    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(e,event,file) {
      let currentImageInput = {
        image: e.target.result,
        dimensions: null,
      };
      try {
        currentImageInput.dimensions = sizeOf(dataUriToBuffer(e.target.result));

        // Need to resize?
        const w = currentImageInput.dimensions.width;
        const h = currentImageInput.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(event.target)
            .then((img) => {
              Vue.set(
                currentImageInput,
                "scaled",
                new File([img], file.name, {
                  type: img.type,
                  lastModified: Date.now(),
                })
              );
              Vue.set(currentImageInput, "useScaled", true);
              Vue.set(currentImageInput, "scaledSize", img.size);
              Vue.set(currentImageInput, "scaledDimensions", {
                width: newWidth,
                height: newHeight,
              });
            })
            .catch((err) => {
              console.error("Resize failed:", err);
            });
        }
      } catch (error) {
        console.error("Failed to get image dimensions: " + error);
      }
      return currentImageInput
    },
    handleFileReader(event, file) {
      if (file) {
        var reader = new FileReader();
        reader.onload = (e) => {
          if (file.type.startsWith("image/")) {
            const currentImageInput = this.optimizeImage(e, event, file)
            this.currentImageInputs = Array.isArray(this.currentImageInputs) ? [...this.currentImageInputs, currentImageInput] : [currentImageInput]
          }
          this.$matrix.matrixClient.getMediaConfig().then((config) => {
            this.currentFileInputs = Array.isArray(this.currentFileInputs) ? [...this.currentFileInputs, file] : [file];
            if (config["m.upload.size"] && file.size > config["m.upload.size"]) {
              this.currentSendError = this.$t("message.upload_file_too_large");
              this.currentSendShowSendButton = false;
            } else {
              this.currentSendShowSendButton = true;
            }
          });
        };
        reader.readAsDataURL(file);
      }
    },
    /**
     * Handle picked attachment
     */
    handlePickedAttachment(event) {
      Object.values(event.target.files).forEach(file => this.handleFileReader(event, file));
    },

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

    onUploadProgress(p) {
      if (p.total) {
        this.currentSendProgress = this.$t("message.upload_progress_with_total", {
          count: p.loaded || 0,
          total: p.total,
        });
      } else {
        this.currentSendProgress = this.$t("message.upload_progress", {
          count: p.loaded || 0,
        });
      }
    },
    sendAttachment(withText) {
      this.$refs.attachment.value = null;
      if (this.isCurrentFileInputsAnArray) {
        let inputFiles = this.currentFileInputs;
        if (Array.isArray(this.currentImageInputs) && this.currentImageInputs.scaled && this.currentImageInputs.useScaled) {
          // Send scaled version of image instead!
          inputFiles = this.currentImageInputs.map(({scaled}) => scaled)
        }

      const promises = inputFiles.map(inputFile => util.sendImage(this.$matrix.matrixClient, this.roomId, inputFile, this.onUploadProgress));

      Promise.all(promises).then(() => {
        this.currentSendOperation = null;
        this.currentImageInputs = null;
        this.currentFileInputs = null;
        this.currentSendProgress = null;
        if (withText) {
          this.sendMessage(withText);
        }
      })
      .catch((err) => {
        if (err.name === "AbortError" || err === "Abort") {
          this.currentSendError = null;
        } else {
          this.currentSendError = err.LocaleString();
        }
        this.currentSendOperation = null;
        this.currentSendProgress = null;
      });
      }
    },

    cancelSendAttachment() {
      this.$refs.attachment.value = null;
      if (this.currentSendOperation) {
        this.currentSendOperation.abort();
      }
      this.currentSendOperation = null;
      this.currentImageInputs = null;
      this.currentFileInputs = null;
      this.currentSendProgress = null;
      this.currentSendError = null;
    },

    handleScrolledToTop() {
      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.prepareFor("up");
              this.events = this.timelineWindow.getEvents();
              this.$nextTick(() => {
                // restore scroll position!
                console.log("Restore scroll!");
                this.scrollPosition.restore();
              });
            }
          })
          .finally(() => {
            this.timelineWindowPaginating = false;
          });
      }
    },

    handleScrolledToBottom(scrollToEnd) {
      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.events = this.timelineWindow.getEvents();
              if (!this.useVoiceMode) {
                this.scrollPosition.prepareFor("down");
                this.$nextTick(() => {
                  // restore scroll position!
                  console.log("Restore scroll!");
                  this.scrollPosition.restore();
                  if (scrollToEnd) {
                    this.smoothScrollToEnd();
                  }
                });
              }
            }
          })
          .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) {
      const container = this.chatContainer;
      const ref = this.$refs[eventId];
      if (container && ref) {
        const targetY = container.clientHeight / 2;
        const sourceY = ref[0].offsetTop;
        container.scrollTo(0, sourceY - targetY);
      }
    },

    smoothScrollToEnd() {
      this.$nextTick(function () {
        const container = this.chatContainer;
        if (container && container.children.length > 0) {
          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 });
    },

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

    addReply(event) {
      this.replyToEvent = event;
      this.$refs.messageInput.focus();
      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) {
      this.$matrix.matrixClient
        .redactEvent(event.getRoomId(), event.getId())
        .then(() => {
          console.log("Message redacted");
        })
        .catch((err) => {
          console.log("Redaction failed: ", err);
        });
    },

    download(event) {
      util
        .getAttachment(this.$matrix.matrixClient, event)
        .then((url) => {
          const link = document.createElement("a");
          link.href = url;
          link.target = "_blank";
          link.download = event.getContent().body || this.$t("fallbacks.download_name");
          document.body.appendChild(link);
          link.click();

          setTimeout(function () {
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
          }, 200);
        })
        .catch((err) => {
          console.log("Failed to fetch attachment: ", err);
        });
    },

    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);
        });
    },

    sendQuickReaction(e) {
      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);
        });
      }
    },

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

    showContextMenuForEvent(e) {
      const event = e.event;
      this.selectedEvent = event;
      this.updateRecentEmojis();
      this.showContextMenu = !this.showContextMenu;
      this.showContextMenuAnchor = e.anchor;
    },

    showAvatarMenuForEvent(e) {
      const event = e.event;
      this.selectedEvent = event;
      this.showAvatarMenu = true;
      this.showAvatarMenuAnchor = e.anchor;
    },

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

    startPrivateChat(e) {
      this.loading = true;
      this.$matrix
        .getOrCreatePrivateChat(e.event.getSender())
        .then((room) => {
          this.$nextTick(() => {
            this.$navigation.push(
              {
                name: "Chat",
                params: {
                  roomId: util.sanitizeRoomId(room.getCanonicalAlias() || room.roomId),
                },
              },
              -1
            );
          });
        })
        .catch((err) => {
          console.error(err);
        })
        .finally(() => {
          this.loading = false;
        });
    },

    closeContextMenusIfOpen(e) {
      if (this.showContextMenu) {
        this.showContextMenu = false;
        this.showContextMenuAnchor = null;
        e.preventDefault();
      }
      if (this.showAvatarMenu) {
        this.showAvatarMenu = false;
        this.showAvatarMenuAnchor = 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;
      }

      let eventIdFirst = null;
      let eventIdLast = null;
      if (!this.useVoiceMode) {
        const container = this.chatContainer;
        const elFirst = util.getFirstVisibleElement(container, (item) => item.hasAttribute("eventId"));
        const elLast = util.getLastVisibleElement(container, (item) => item.hasAttribute("eventId"));
        if (elFirst && elLast) {
          eventIdFirst = elFirst.getAttribute("eventId");
          eventIdLast = elLast.getAttribute("eventId");
        }
      }
      if (eventIdFirst && eventIdLast) {
        this.rrTimer = setTimeout(() => { this.rrTimerElapsed(eventIdFirst, eventIdLast) }, READ_RECEIPT_TIMEOUT);
      }
    },

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

    sendRR(eventIdFirst, eventIdLast) {
      console.log("SEND RR", eventIdFirst, eventIdLast);
      if (eventIdLast && this.room) {
        var event = this.room.findEventById(eventIdLast);
        const index = this.events.indexOf(event);

        // Walk backwards through visible events to the first one that is incoming
        //
        var lastTimestamp = 0;
        if (this.lastRR) {
          lastTimestamp = this.lastRR.getTs();
        }

        for (var i = index; i >= 0; i--) {
          event = this.events[i];
          if (event == this.lastRR || event.getTs() <= lastTimestamp) {
            // Already sent this or too old...
            break;
          }
          // Make sure it's not a local echo event...
          if (!event.getId().startsWith("~")) {
            // Send read receipt
            this.$matrix.matrixClient
              .sendReadReceipt(event)
              .then(() => {
                this.$matrix.matrixClient.setRoomReadMarkers(this.room.roomId, event.getId());
              })
              .then(() => {
                console.log("RR sent for event: " + event.getId());
                this.lastRR = event;
              })
              .catch((err) => {
                console.log("Failed to update read marker: ", err);
              })
              .finally(() => {
                this.restartRRTimer();
              });
            return; // Bail out here
          }

          // Stop iterating at first visible
          if (event.getId() == eventIdFirst) {
            break;
          }
        }
      }
    },

    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");
    },

    closeCreateRoomWelcomeHeader() {
      this.showCreatedRoomWelcomeHeader = false;
      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.
        const filteredEvents = this.filteredEvents;
        const index = filteredEvents.findIndex(e => e.getId() === matrixEventId);
        if (index >= 0 && index < (filteredEvents.length - 1)) {
          const nextEvent = filteredEvents[index + 1];
          if (nextEvent.getContent().msgtype === "m.audio") {
            // Yes, audio event!
            this.$audioPlayer.play(nextEvent, this.timelineSet);
          }
        }
      }
    }
  },
};
</script>

<style lang="scss">
@import "@/assets/css/chat.scss";
</style>
