2020-11-09 10:26:56 +01:00
< template >
2024-10-11 17:04:32 +02:00
< div : class = "{'chat-root': true, 'fill-height': true, 'd-flex': true, 'flex-column': true, 'channel': roomDisplayType == ROOM_TYPE_CHANNEL}" :style = "chatContainerStyle" >
2023-08-30 10:50:45 +02:00
< ChatHeaderPrivate class = "chat-header flex-grow-0 flex-shrink-0"
2023-07-17 15:00:40 +03:00
v - on : header - click = "onHeaderClick"
v - on : view - room - details = "viewRoomDetails"
2024-04-11 11:27:07 +02:00
v - on : purge = "showPurgeConfirmation = true"
2025-03-31 16:33:54 +02:00
v - on : download = "downloadingChat = true"
2023-08-30 10:50:45 +02:00
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"
2024-04-11 11:27:07 +02:00
v - on : purge = "showPurgeConfirmation = true"
2025-03-31 16:33:54 +02:00
v - on : download = "downloadingChat = true"
2023-08-30 10:50:45 +02:00
v - else - if = "!useFileModeNonAdmin" / >
2023-02-17 22:00:47 +01:00
< AudioLayout ref = "chatContainer" class = "auto-audio-player-root" v-if = "useVoiceMode" :room="room"
2023-04-02 10:39:16 +03:00
: events = "events" : autoplay = "!showRecorder"
2023-01-30 08:36:02 +00:00
: timelineSet = "timelineSet"
: readMarker = "readMarker"
2023-02-17 22:30:15 +01:00
: recordingMembers = "typingMembers"
2023-03-16 15:23:26 +01:00
v - on : start - recording = "setShowRecorder()"
2024-09-17 09:43:53 +02:00
v - on : loadnext = "handleScrolledToLatest(false)"
v - on : loadprevious = "handleScrolledToOldest()"
2023-01-30 08:36:02 +00:00
v - on : mark - read = "sendRR"
2023-06-07 09:39:05 +02:00
v - on : sendclap = "sendClapReactionAtTime"
2023-01-30 08:36:02 +00:00
/ >
2023-02-17 22:00:47 +01:00
< 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" / >
2023-01-30 08:36:02 +00:00
2025-08-07 12:00:09 +02:00
< RoomUpgradePrompt v-if = "roomUpgradeInfo" :roomId="roomId" :urgent="roomUpgradeInfo.urgent" :version="roomUpgradeInfo.version" / >
2025-10-19 14:05:29 +03:00
< SendAttachmentsLayout
v - if = "room && useFileModeNonAdmin"
2025-06-11 18:04:56 +02:00
: room = "room"
2025-06-09 09:44:37 +02:00
v - on : pick - file = "showAttachmentPicker(false)"
v - on : add - files = "(files) => addAttachments(files)"
2025-06-11 18:04:56 +02:00
: batch = "uploadBatch"
2025-01-14 11:14:11 +00:00
v - on : close = "closeFileMode"
2025-10-17 17:06:00 +02:00
v - on : reset = "resetFileMode"
2025-08-25 11:38:41 +02:00
: fileModeMode = "true"
2025-06-26 12:11:58 +02:00
: defaultRootMessageText = "$t('file_mode.files')"
2023-06-28 12:14:44 +00:00
/ >
2023-01-30 08:36:02 +00:00
2023-11-06 15:28:26 +00:00
< div v-if = "!useVoiceMode && !useFileModeNonAdmin" :class="{'chat-content': true, 'flex-grow-1': true, 'flex-shrink-1': true, 'invisible': !initialLoadDone}" ref="chatContainer"
2023-01-30 08:36:02 +00:00
v - on : scroll = "onScroll" @ click = "closeContextMenusIfOpen" >
2021-01-11 17:42:58 +01:00
< div ref = "messageOperationsStrut" class = "message-operations-strut" >
2025-10-19 14:05:29 +03:00
< component :is = "messageOperationsComponent" ref = "messageOperations" :style = "opStyle"
v - if = "showMessageOperations"
v - on : close = "showContextMenu = false;"
: emojis = "recentEmojis"
: originalEvent = "selectedEvent"
2024-10-16 15:33:35 +02:00
: timelineSet = "timelineSet"
v - on : pin = "pin(selectedEvent)"
v - on : unpin = "unpin(selectedEvent)"
2025-10-19 14:05:29 +03:00
v - on : addreaction = "addReaction"
2024-10-16 15:33:35 +02:00
v - on : addquickreaction = "addQuickReaction"
2025-10-19 14:05:29 +03:00
v - on : addreply = "addReply(selectedEvent)"
v - on : edit = "edit(selectedEvent)"
2025-11-08 22:48:59 +02:00
v - on : redact = "showDeletePostPopup = true"
2025-10-19 14:05:29 +03:00
v - on : download = "download(selectedEvent)"
2025-11-03 16:47:41 +01:00
v - on : report = "reportEvent(selectedEvent)"
2024-10-16 15:33:35 +02:00
v - on : more = "
2025-05-06 09:27:53 +02:00
isEmojiQuickReaction = true ;
2023-11-06 15:28:26 +00:00
showMoreMessageOperations ( { event : selectedEvent , anchor : $event . anchor } )
2025-10-19 14:05:29 +03:00
"
2024-04-26 16:44:06 +02:00
: userCanSendReactionAndAnswerPoll = "$matrix.userCanSendReactionAndAnswerPollInCurrentRoom"
: userCanSendMessage = "$matrix.userCanSendMessageInCurrentRoom"
2024-10-16 15:33:35 +02:00
: userCanPin = "canCreatePoll"
/ >
2021-02-17 11:59:07 +01:00
< / div >
2020-12-14 16:11:45 +01:00
2020-12-10 12:51:04 +01:00
<!-- Handle resizes , e . g . when soft keyboard is shown / hidden -- >
2022-05-17 15:16:53 +00:00
< resize-observer ref = "chatContainerResizer" @notify ="handleChatContainerResize" / >
2024-04-03 09:35:20 +02:00
< component :is = "roomWelcomeHeader" v -on :close = "closeRoomWelcomeHeader" > < / component >
2022-05-17 15:16:53 +00:00
2024-06-05 15:44:34 +02:00
<!-- If we have a retention timer , it means we have active message retention . Show header . -- >
2024-07-23 09:36:57 +00:00
< WelcomeHeaderChannelUser v-if = "retentionTimer && !roomWelcomeHeader && newlyJoinedRoom" / >
2024-06-05 15:44:34 +02:00
2024-10-11 17:04:32 +02:00
< div v-for = "(event) in filteredEvents" :key="event.getId()" :eventId="event !== ROOM_READ_MARKER_EVENT_PLACEHOLDER ? event.getId() : undefined" >
2025-10-19 14:05:29 +03:00
2021-02-17 11:59:07 +01:00
<!-- DAY Marker , shown for every new day in the timeline -- >
2024-10-11 17:04:32 +02:00
< 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 >
2021-02-17 11:59:07 +01:00
2024-10-11 17:04:32 +02:00
< div :ref = "event.getId()" >
2024-06-07 15:03:29 +02:00
< MessageErrorHandler >
< div class = "message-wrapper" v -on : touchstart = "
( e ) => {
touchStart ( e , event ) ;
}
" v-on:touchend=" touchEnd " v-on:touchcancel=" touchCancel " v-on:touchmove=" touchMove " >
2024-09-17 09:43:53 +02:00
<!-- Note : For threaded media messages , IF there is only one item we show that media item as a single component .
2025-07-15 12:10:35 +02:00
We might therefore get calls to v - on : contextMenu that has the event set to that single media item , not the top level thread event
2024-06-07 15:03:29 +02:00
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 .
-- >
2024-10-11 17:04:32 +02:00
< component :is = "event.component" :room = "room" :originalEvent = "event" :nextEvent = "event.nextDisplayedEvent"
2025-06-10 13:35:51 +02:00
: timelineSet = "timelineSet" v - on : send - quick - reaction = "sendQuickReaction"
2024-06-07 15:03:29 +02:00
: componentFn = "componentForEvent"
2025-07-15 12:10:35 +02:00
v - on : contextMenu = "showContextMenuForEvent({event: event, anchor: $event.anchor})"
2024-06-07 15:03:29 +02:00
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 = "
2025-05-06 09:27:53 +02:00
isEmojiQuickReaction = true ;
2024-06-07 15:03:29 +02:00
showMoreMessageOperations ( { event : event , anchor : $event . anchor } )
"
v - on : layout - change = "onLayoutChange"
2024-06-08 13:42:57 +03:00
v - on : addQuickHeartReaction = "addQuickHeartReaction({event, position: $event.position})"
2024-06-07 15:03:29 +02:00
/ >
<!-- < div v-if = "debugging" style="user-select:text" > EventID : {{ event.getId ( ) }} < / div > - - >
2024-07-02 11:08:03 +02:00
<!-- < div v-if = "debugging" style="user-select:text" > Event : {{ JSON.stringify ( event ) }} < br / > < br / > < / div > -- >
2024-06-07 15:03:29 +02:00
< / div >
< / MessageErrorHandler >
2020-12-03 10:00:23 +01:00
< / div >
2024-09-17 09:43:53 +02:00
<!-- Day marker when reverseOrder is set -- >
2024-10-11 17:04:32 +02:00
< 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 >
2024-09-17 09:43:53 +02:00
2020-11-09 10:26:56 +01:00
< / div >
2023-04-27 07:09:20 +00:00
< NoHistoryRoomWelcomeHeader v-if = "showNoHistoryRoomWelcomeHeader" / >
2020-11-09 10:26:56 +01:00
2024-09-17 09:43:53 +02:00
< / div >
2025-06-10 13:47:07 +02:00
<!-- "Scroll to end" - button -- >
2025-06-19 10:47:16 +02:00
< v-btn v-if = "!useVoiceMode" icon="arrow_downward" :class="{'scroll-to-end': true, 'reversed': reverseOrder, 'hidden': !showScrollToEnd}" theme="dark" size="x-small" elevation="0" color="black"
2025-06-10 13:47:07 +02:00
@ click . stop = "scrollToEndOfTimeline" >
< / v-btn >
2024-09-17 09:43:53 +02:00
<!-- Input area -- >
2025-08-04 10:02:44 +02:00
< v-container v-if = "!useVoiceMode && !useFileModeNonAdmin && room && room.currentState.getStateEvents('m.room.tombstone').length == 0" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']" >
2024-09-17 09:43:53 +02:00
< div : class = "[replyToEvent ? 'iput-area-inner-box' : '']" >
2022-02-11 09:58:36 +00:00
< v-row class = "ma-0 pa-0" >
< div v-if = "replyToEvent" class="row" >
< div class = "col" >
2022-11-20 13:39:20 +02:00
< div class = "font-weight-medium" > { { $t ( "message.replying_to" , { user : senderDisplayName } ) } } < / div >
2022-05-17 15:16:53 +00:00
< div v-if = "replyToContentType === 'm.text'" class="reply-text" :title="replyToEvent.getContent().body" >
2025-05-08 11:52:39 +02:00
{ { latestReply } }
2022-05-17 15:16:53 +00:00
< / div >
2024-06-28 11:49:55 +02:00
< div v-if = "replyToContentType === 'm.thread' || replyToContentType === 'io.element.thread'" > {{ replyToThreadMessage }} < / div >
2022-05-17 15:16:53 +00:00
< 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 >
2023-01-30 08:36:02 +00:00
< div v-if = "replyToContentType === 'm.poll'" > {{ $ t ( " message.reply_poll " ) }} < / div >
2022-02-11 09:58:36 +00:00
< / div >
< div class = "col col-auto" v-if = "replyToContentType !== 'm.text'" >
2023-01-30 08:36:02 +00:00
< img v-if = "replyToContentType === 'm.image'" width="50px" height="50px" :src="replyToImg"
class = "rounded" / >
2025-05-08 13:10:06 +02:00
< v-icon v-if = "replyToContentType === 'm.audio'" > $ vuetify.icons.audio_message < / v-icon >
< v-icon v-if = "replyToContentType === 'm.video'" > $ vuetify.icons.video_message < / v-icon >
2025-05-08 11:52:39 +02:00
< v-icon v-if = "replyToContentType === 'm.poll'" theme="light" > $ vuetify.icons.poll < / v-icon >
2022-02-11 09:58:36 +00:00
< / div >
2022-03-06 14:28:16 +02:00
< div class = "col col-auto" >
2025-05-08 11:52:39 +02:00
< v-btn icon = "cancel" size = "default" elevation = "0" color = "black" @click.stop ="cancelEditReply" >
2022-03-06 14:28:16 +02:00
< / v-btn >
< / div >
2022-02-11 09:58:36 +00:00
< / div >
2020-12-15 17:06:26 +01:00
2022-02-11 09:58:36 +00:00
<!-- CONTACT IS TYPING -- >
< div class = "typing" >
{ { typingMembersString } }
< / div >
< / v-row >
2024-04-26 16:44:06 +02:00
< v-row class = "input-area-inner align-center" v-show = "!showRecorder" v-if="$matrix.userCanSendMessageInCurrentRoom" >
2022-02-11 09:58:36 +00:00
< v-col class = "flex-grow-1 flex-shrink-1 ma-0 pa-0" >
2023-01-30 08:36:02 +00:00
< v-textarea height = "undefined" ref = "messageInput" full -width auto -grow rows = "1" v-model = "currentInput"
2025-05-08 11:52:39 +02:00
no - resize class = "input-area-text input-plain" : placeholder = "$t('message.your_message')" hide - details
2023-01-30 08:36:02 +00:00
background - color = "white" v - on : keydown . enter . prevent = "
2022-02-11 09:58:36 +00:00
( ) => {
sendCurrentTextMessage ( ) ;
}
2023-01-30 08:36:02 +00:00
" / >
2022-02-11 09:58:36 +00:00
< / v-col >
2020-12-04 12:15:47 +01:00
2022-05-17 15:16:53 +00:00
< v-col class = "input-area-button text-center flex-grow-0 flex-shrink-1" v-if = "editedEvent" >
2025-05-08 11:52:39 +02:00
< v-btn icon = "cancel" elevation = "0" color = "black" @click.stop ="cancelEditReply" >
2022-02-11 09:58:36 +00:00
< / v-btn >
< / v-col >
2021-02-23 22:07:57 +01:00
2023-04-10 08:38:04 +00:00
< v-col v-if = "(!currentInput || currentInput.length == 0) && canCreatePoll && !replyToEvent"
2023-01-30 08:36:02 +00:00
class = "input-area-button text-center flex-grow-0 flex-shrink-1" >
2025-05-08 11:52:39 +02:00
< v-btn icon @ click = "showCreatePollDialog = true" >
< v-icon size = "24" > $vuetify . icons . poll < / v-icon >
2022-06-08 18:53:50 +00:00
< / v-btn >
< / v-col >
2023-01-30 08:36:02 +00:00
< v-col class = "input-area-button text-center flex-grow-0 flex-shrink-1"
2025-04-23 17:19:35 +02:00
v - if = "!$config.disableMediaSharing && (!currentInput || currentInput.length == 0 || showRecorder)" >
2025-05-08 11:52:39 +02:00
< v-btn icon = "mic" : color = "showRecorder ? 'black' : 'white'" v-if = "canRecordAudio" class="mic-button" ref="mic_button" elevation="0" v -blur
2023-01-30 08:36:02 +00:00
v - longTap : 250 = "[showRecordingUI, startRecording]" >
2022-02-11 09:58:36 +00:00
< / v-btn >
2025-05-08 11:52:39 +02:00
< v-btn icon = "mic" : color = "showRecorder ? 'black' : 'white'" v -else class = "mic-button" ref = "mic_button" elevation = "0" v -blur
2023-01-30 08:36:02 +00:00
@ click . stop = "showNoRecordingAvailableDialog = true" >
2022-02-11 09:58:36 +00:00
< / v-btn >
< / v-col >
2021-02-23 22:07:57 +01:00
2025-04-23 17:19:35 +02:00
< v-col class = "input-area-button text-center flex-grow-0 flex-shrink-1" v -else -if = " currentInput & & currentInput.length > 0 " >
2025-05-08 11:52:39 +02:00
< v-btn : icon = "editedEvent ? 'save' : 'arrow_upward'" size = "default" elevation = "0" color = "black" @click.stop ="sendCurrentTextMessage"
2023-01-30 08:36:02 +00:00
: disabled = "sendButtonDisabled" >
2022-02-11 09:58:36 +00:00
< / v-btn >
< / v-col >
2021-02-23 22:07:57 +01:00
2023-01-30 08:36:02 +00:00
< v-col class = "input-area-button text-center flex-grow-0 flex-shrink-1 input-more-icon" >
2025-05-08 11:52:39 +02:00
< v-btn size = "small" elevation = "0" v -blur @ click.stop = "
2025-05-06 09:27:53 +02:00
isEmojiQuickReaction = false ;
2023-01-30 08:36:02 +00:00
showMoreMessageOperations ( $event )
2025-05-08 11:52:39 +02:00
" icon=" $vuetify . icons . addReaction " >
2022-06-11 12:30:50 +03:00
< / v-btn >
< / v-col >
2022-05-17 15:16:53 +00:00
< v-col v-if = "$config.shortCodeStickers" class="input-area-button text-center flex-grow-0 flex-shrink-1" >
2025-05-08 11:52:39 +02:00
< v-btn id = "btn-stickers" icon = "face" @click ="showStickerPicker"
2023-01-30 08:36:02 +00:00
: disabled = "attachButtonDisabled" >
2021-02-23 22:07:57 +01:00
< / v-btn >
2022-02-11 09:58:36 +00:00
< / v-col >
2025-04-23 17:19:35 +02:00
< v-col v-if = "!$config.disableMediaSharing" class="input-area-button text-center flex-grow-0 flex-shrink-1" >
2022-02-11 09:58:36 +00:00
< label icon flat ref = "attachmentLabel" >
2025-06-09 09:44:37 +02:00
< v-btn icon @ click = "() => showAttachmentPicker(true)"
2023-01-30 08:36:02 +00:00
: disabled = "attachButtonDisabled" >
2025-05-08 11:52:39 +02:00
< v-icon size = "36" > add _circle _outline < / v-icon >
2022-02-11 09:58:36 +00:00
< / v-btn >
< / label >
< / v-col >
< / v-row >
2023-01-30 08:36:02 +00:00
< VoiceRecorder :micButtonRef = "$refs.mic_button" :ptt = "showRecorderPTT" :show = "showRecorder"
v - on : close = "showRecorder = false" v - on : file = "onVoiceRecording" / >
2022-02-11 09:58:36 +00:00
< / div >
2024-04-26 16:44:06 +02:00
< div v-if = "!useVoiceMode && room && !$matrix.userCanSendMessageInCurrentRoom" class="input-area-read-only" > {{ $ t ( " message.not_allowed_to_send " ) }} < / div >
2020-12-04 12:15:47 +01:00
< / v-container >
2020-11-17 20:02:42 +01:00
2025-07-16 15:49:26 +02:00
< form ref = "attachmentForm" >
< 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 / >
< / form >
2023-01-30 08:36:02 +00:00
2025-10-19 14:05:29 +03:00
< SendAttachmentsLayout
v - if = "uploadBatch && uploadBatch.attachments.length > 0 && !useFileModeNonAdmin"
2025-06-09 09:44:37 +02:00
: room = "room"
v - on : pick - file = "showAttachmentPicker(false)"
v - on : add - files = "(files) => addAttachments(files)"
2025-06-11 14:59:34 +02:00
: batch = "uploadBatch"
v - on : close = "() => { uploadBatch = undefined }"
2025-07-03 15:45:49 +02:00
: title = "room.name"
2025-06-09 09:44:37 +02:00
/ >
2020-11-25 14:42:50 +01:00
2025-10-19 14:05:29 +03:00
< BottomSheet ref = "messageOperationsSheet" >
< EmojiPicker ref = "emojiPicker"
2025-05-19 17:33:04 +02:00
: native = "true"
2025-10-19 14:05:29 +03:00
@ select = "emojiSelected"
: additional - groups = "additionalEmojiGroups"
: group - names = "emojiGroupNames"
2025-05-12 17:15:11 +02:00
: group - icons = "additionalEmojiGroupIcons"
: group - order = "['recently_used']"
2025-10-19 14:05:29 +03:00
disable - skin - tones
2025-05-12 17:15:11 +02:00
: static - texts = "{ placeholder: $t('emoji.search')}" / >
2025-08-04 09:44:06 +02:00
< / BottomSheet >
2020-12-10 12:37:06 +01:00
2022-05-17 15:16:53 +00:00
< StickerPickerBottomSheet ref = "stickerPickerSheet" v -on :selectSticker = "sendSticker" / >
2021-04-15 11:44:58 +02:00
2021-02-17 17:12:16 +01:00
<!-- Loading indicator -- >
2025-05-12 12:39:48 +02:00
< v-container fluid class = "loading-indicator fill-height" v-if = "!initialLoadDone || loading" >
2021-02-17 17:12:16 +01:00
< v-row align = "center" justify = "center" >
< v-col class = "text-center" >
2022-05-17 15:16:53 +00:00
< v-progress-circular indeterminate color = "primary" > < / v-progress-circular >
2021-02-17 17:12:16 +01:00
< / v-col >
< / v-row >
< / v-container >
2021-03-11 13:55:10 +01:00
< RoomInfoBottomSheet ref = "roomInfoSheet" / >
2021-05-25 12:04:51 +02:00
<!-- Dialog for audio recording not supported ! -- >
2022-05-17 15:16:53 +00:00
< v-dialog v-model = "showNoRecordingAvailableDialog" class="ma-0 pa-0" width="80%" >
2021-05-25 12:04:51 +02:00
< v-card >
2022-05-17 15:16:53 +00:00
< v-card-title > { { $t ( "voice_recorder.not_supported_title" ) } } < / v-card-title >
< v-card-text > { { $t ( "voice_recorder.not_supported_text" ) } } < / v-card-text >
2021-05-25 12:04:51 +02:00
< v-divider > < / v-divider >
< v-card-actions >
< v-spacer > < / v-spacer >
2025-05-09 10:17:59 +02:00
< v-btn id = "btn-ok" color = "primary" variant = "text" @ click = "showNoRecordingAvailableDialog = false" > { {
2023-01-30 08:36:02 +00:00
$t ( "menu.ok" )
2022-05-17 15:16:53 +00:00
} } < / v-btn >
2021-05-25 12:04:51 +02:00
< / v-card-actions >
< / v-card >
< / v-dialog >
2022-05-03 09:40:02 +00:00
2025-05-09 10:17:59 +02:00
< CreatePollDialog v-model = "showCreatePollDialog" :room="room" / >
2023-07-17 15:00:40 +03:00
2024-03-24 11:35:05 +02:00
< UserProfileDialog
2025-05-08 11:52:39 +02:00
v - model = "showProfileDialog"
2024-03-24 11:35:05 +02:00
: activeMember = "compActiveMember"
: room = "room"
/ >
2024-04-11 11:27:07 +02:00
<!-- PURGE ROOM POPUP -- >
2025-05-08 11:52:39 +02:00
< PurgeRoomDialog v-model = "showPurgeConfirmation" :room="room" / >
2024-06-08 13:42:57 +03:00
2025-03-31 16:33:54 +02:00
< RoomExport :room = "room" v-if = "downloadingChat" v-on:close="downloadingChat = false" / >
2025-11-03 16:47:41 +01:00
< ReportRoomOrEventDialog v-model = "reportingEventShown" :room="room" :eventId="reportingEventId" / >
2025-03-31 16:33:54 +02:00
2024-06-08 13:42:57 +03:00
<!-- Heart animation -- >
< div : class = "['heart-wrapper', { 'is-active': heartAnimation }]" :style = "hearAnimationPosition" >
2024-05-18 22:13:07 +03:00
< div : class = "['heart', { 'is-active': heartAnimation }]" / >
< / div >
2025-11-08 22:48:59 +02:00
<!-- Delete post dialog -- >
< DeletePostDialog v-model = "showDeletePostPopup" v -on :deletePost = "onDeletePost" / >
2020-11-09 10:26:56 +01:00
< / div >
< / template >
< script >
2023-05-11 10:11:34 +02:00
import { TimelineWindow , EventTimeline } from "matrix-js-sdk" ;
2025-05-12 17:15:11 +02:00
import { toRaw } from "vue" ;
2024-04-03 09:35:20 +02:00
import util , { ROOM _TYPE _VOICE _MODE , ROOM _TYPE _FILE _MODE , ROOM _TYPE _CHANNEL } from "../plugins/utils" ;
2020-11-25 14:42:50 +01:00
import MessageOperations from "./messages/MessageOperations.vue" ;
2020-12-04 17:15:18 +01:00
import ChatHeader from "./ChatHeader" ;
2023-08-30 10:50:45 +02:00
import ChatHeaderPrivate from "./ChatHeaderPrivate.vue" ;
2021-02-22 16:34:19 +01:00
import VoiceRecorder from "./VoiceRecorder" ;
2021-03-11 13:55:10 +01:00
import RoomInfoBottomSheet from "./RoomInfoBottomSheet" ;
2024-04-03 09:35:20 +02:00
import WelcomeHeaderRoom from "./welcome_headers/WelcomeHeaderRoom" ;
import WelcomeHeaderDirectChat from "./welcome_headers/WelcomeHeaderDirectChat" ;
import WelcomeHeaderChannel from "./welcome_headers/WelcomeHeaderChannel" ;
2024-06-05 15:44:34 +02:00
import WelcomeHeaderChannelUser from "./welcome_headers/WelcomeHeaderChannelUser" ;
2023-04-27 07:09:20 +00:00
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue" ;
2021-05-11 21:03:54 +02:00
import StickerPickerBottomSheet from "./StickerPickerBottomSheet" ;
2024-03-24 11:35:05 +02:00
import UserProfileDialog from "./UserProfileDialog.vue"
2025-08-07 12:00:09 +02:00
import RoomUpgradePrompt from "./messages/composition/RoomUpgradePrompt.vue" ;
2021-05-11 21:03:54 +02:00
import BottomSheet from "./BottomSheet.vue" ;
2022-05-03 09:40:02 +00:00
import CreatePollDialog from "./CreatePollDialog.vue" ;
2025-11-03 16:47:41 +01:00
import ReportRoomOrEventDialog from "./ReportRoomOrEventDialog.vue" ;
2024-10-11 17:04:32 +02:00
import chatMixin , { ROOM _READ _MARKER _EVENT _PLACEHOLDER } from "./chatMixin" ;
2023-01-30 08:36:02 +00:00
import AudioLayout from "./AudioLayout.vue" ;
2025-06-09 09:44:37 +02:00
import SendAttachmentsLayout from "./file_mode/SendAttachmentsLayout.vue" ;
2023-08-07 14:13:35 +00:00
import roomTypeMixin from "./roomTypeMixin" ;
2023-08-30 10:50:45 +02:00
import roomMembersMixin from "./roomMembersMixin" ;
2024-04-11 11:27:07 +02:00
import PurgeRoomDialog from "../components/PurgeRoomDialog" ;
2024-06-07 15:03:29 +02:00
import MessageErrorHandler from "./MessageErrorHandler" ;
2024-10-11 17:04:32 +02:00
import MessageOperationsChannel from './messages/channel/MessageOperationsChannel.vue' ;
2025-03-31 16:33:54 +02:00
import prettyBytes from "pretty-bytes" ;
import RoomExport from "./RoomExport.vue" ;
2025-11-08 22:48:59 +02:00
import DeletePostDialog from "./DeletePostDialog.vue"
2025-05-06 09:27:53 +02:00
import EmojiPicker from 'vue3-emoji-picker' ;
2025-05-12 17:15:11 +02:00
import 'vue3-emoji-picker/css' ;
2025-05-06 11:34:53 +02:00
import emitter from 'tiny-emitter/instance' ;
2025-05-06 12:02:56 +02:00
import { markRaw } from "vue" ;
2025-05-12 17:15:11 +02:00
import timerIcon from '@/assets/icons/ic_timer.svg' ;
2025-06-09 09:44:37 +02:00
import proofmode from "../plugins/proofmode.js" ;
2025-06-10 13:35:51 +02:00
import { consoleWarn } from "vuetify/lib/util/console.mjs" ;
2020-11-11 17:35:14 +01:00
2021-01-14 16:17:05 +01:00
const READ _RECEIPT _TIMEOUT = 5000 ; /* How long a message must have been visible before the read marker is updated */
2021-05-11 21:03:54 +02:00
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! */
2021-01-14 16:17:05 +01:00
2020-11-11 17:35:14 +01:00
// from https://kirbysayshi.com/2013/08/19/maintaining-scroll-position-knockoutjs-list.html
function ScrollPosition ( node ) {
this . node = node ;
this . previousScrollHeightMinusTop = 0 ;
2020-11-17 20:02:42 +01:00
this . previousScrollTop = 0 ;
2020-11-11 17:35:14 +01:00
this . readyFor = "up" ;
}
2023-01-30 08:36:02 +00:00
ScrollPosition . prototype . restore = function ( ) {
2020-11-11 17:35:14 +01:00
if ( this . readyFor === "up" ) {
2022-05-17 15:16:53 +00:00
this . node . scrollTop = this . node . scrollHeight - this . previousScrollHeightMinusTop ;
2020-11-17 20:02:42 +01:00
} else {
this . node . scrollTop = this . previousScrollTop ;
2020-11-11 17:35:14 +01:00
}
} ;
2023-01-30 08:36:02 +00:00
ScrollPosition . prototype . prepareFor = function ( direction ) {
2020-11-11 17:35:14 +01:00
this . readyFor = direction || "up" ;
2020-11-17 20:02:42 +01:00
if ( this . readyFor === "up" ) {
2022-05-17 15:16:53 +00:00
this . previousScrollHeightMinusTop = this . node . scrollHeight - this . node . scrollTop ;
2020-11-17 20:02:42 +01:00
} else {
this . previousScrollTop = this . node . scrollTop ;
}
2020-11-11 17:35:14 +01:00
} ;
2020-11-09 10:26:56 +01:00
export default {
name : "Chat" ,
2025-06-11 18:04:56 +02:00
mixins : [ chatMixin , roomTypeMixin , roomMembersMixin ] ,
2020-11-17 20:32:37 +01:00
components : {
2020-12-04 17:15:18 +01:00
ChatHeader ,
2023-08-30 10:50:45 +02:00
ChatHeaderPrivate ,
2020-11-25 14:42:50 +01:00
MessageOperations ,
2021-03-04 12:48:32 +01:00
VoiceRecorder ,
2021-03-26 12:16:13 +01:00
RoomInfoBottomSheet ,
2024-04-03 09:35:20 +02:00
WelcomeHeaderRoom ,
WelcomeHeaderDirectChat ,
2023-04-27 07:09:20 +00:00
NoHistoryRoomWelcomeHeader ,
2021-04-15 11:44:58 +02:00
StickerPickerBottomSheet ,
BottomSheet ,
2022-05-17 15:16:53 +00:00
CreatePollDialog ,
2023-06-28 12:14:44 +00:00
AudioLayout ,
2025-06-09 09:44:37 +02:00
SendAttachmentsLayout ,
2024-04-11 11:27:07 +02:00
UserProfileDialog ,
PurgeRoomDialog ,
2024-06-07 15:03:29 +02:00
WelcomeHeaderChannelUser ,
2024-10-11 17:04:32 +02:00
MessageErrorHandler ,
2025-03-31 16:33:54 +02:00
MessageOperationsChannel ,
RoomExport ,
2025-06-09 09:44:37 +02:00
EmojiPicker ,
2025-11-03 16:47:41 +01:00
RoomUpgradePrompt ,
2025-11-08 22:48:59 +02:00
ReportRoomOrEventDialog ,
DeletePostDialog
2020-11-17 20:32:37 +01:00
} ,
2020-11-19 22:48:08 +01:00
data ( ) {
2021-05-11 21:03:54 +02:00
return {
2024-10-11 17:04:32 +02:00
ROOM _TYPE _CHANNEL : ROOM _TYPE _CHANNEL ,
ROOM _READ _MARKER _EVENT _PLACEHOLDER : ROOM _READ _MARKER _EVENT _PLACEHOLDER ,
2023-04-04 14:30:50 +00:00
waitingForRoomObject : false ,
2020-11-19 22:48:08 +01:00
events : [ ] ,
currentInput : "" ,
2020-12-09 21:50:53 +01:00
typingMembers : [ ] ,
2021-09-14 11:57:49 +02:00
timelineSet : null ,
2020-11-19 22:48:08 +01:00
timelineWindow : null ,
2025-08-07 12:00:09 +02:00
roomUpgradeInfo : undefined ,
2021-05-11 21:03:54 +02:00
/** true if we are currently paginating */
2021-04-09 16:40:03 +02:00
timelineWindowPaginating : false ,
2020-11-19 22:48:08 +01:00
scrollPosition : null ,
2025-06-19 10:47:16 +02:00
scrollUpdateTimer : null ,
2025-06-11 14:59:34 +02:00
uploadBatch : undefined ,
2020-11-25 14:42:50 +01:00
showEmojiPicker : false ,
selectedEvent : null ,
2020-12-14 16:11:45 +01:00
editedEvent : null ,
2020-12-15 17:06:26 +01:00
replyToEvent : null ,
2022-02-11 09:58:36 +00:00
replyToImg : null ,
replyToContentType : null ,
2022-05-03 09:40:02 +00:00
showCreatePollDialog : false ,
2021-05-25 12:04:51 +02:00
showNoRecordingAvailableDialog : false ,
2020-12-04 10:44:46 +01:00
showContextMenu : false ,
2021-01-11 17:42:58 +01:00
showContextMenuAnchor : null ,
2021-01-14 16:17:05 +01:00
initialLoadDone : false ,
2021-04-09 16:20:57 +02:00
loading : false , // Set this to true during long operations to show a "spinner" overlay
2021-02-22 16:34:19 +01:00
showRecorder : false ,
2021-03-05 22:34:00 +01:00
showRecorderPTT : false , // True to open the voice recorder in push-to-talk mode.
2021-01-11 17:42:58 +01:00
2020-12-04 10:44:46 +01:00
/ * *
* 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 ,
2020-12-10 12:37:06 +01:00
2021-02-17 11:59:07 +01:00
/ * *
* 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 ,
2021-01-14 16:17:05 +01:00
/** A timer for read receipts. */
rrTimer : null ,
2021-01-15 11:50:21 +01:00
/** Last event we sent a Read Receipt/Read Marker for */
lastRR : null ,
2021-04-01 22:59:19 +02:00
/** If we just created this room, show a small welcome header with info */
2024-04-03 09:35:20 +02:00
hideRoomWelcomeHeader : false ,
2024-07-23 09:36:57 +00:00
newlyJoinedRoom : false ,
2021-04-09 14:03:40 +02:00
/** An array of recent emojis. Used in the "message operations" popup. */
2021-05-11 21:03:54 +02:00
recentEmojis : [ ] ,
2021-07-19 10:33:25 +02:00
/** Calculated style for message operations. We position the "popup" at the selected message. */
opStyle : "" ,
2022-06-11 12:30:50 +03:00
2023-05-20 11:31:28 +03:00
isEmojiQuickReaction : true ,
2025-05-12 17:15:11 +02:00
emojiGroupNames : {
"smileys_people" : this . $t ( "emoji.categories.peoples" ) ,
"animals_nature" : this . $t ( "emoji.categories.nature" ) ,
"food_drink" : this . $t ( "emoji.categories.foods" ) ,
"activities" : this . $t ( "emoji.categories.activity" ) ,
"travel_places" : this . $t ( "emoji.categories.places" ) ,
"objects" : this . $t ( "emoji.categories.objects" ) ,
"symbols" : this . $t ( "emoji.categories.symbols" ) ,
"flags" : this . $t ( "emoji.categories.flags" ) ,
"recently_used" : this . $t ( "emoji.categories.frequently" ) ,
2023-07-17 15:00:40 +03:00
} ,
2024-02-06 10:22:35 +00:00
/ * *
* A timer to handle message retention / auto deletion
* /
2024-03-24 11:35:05 +02:00
retentionTimer : null ,
2024-04-11 11:27:07 +02:00
showProfileDialog : false ,
showPurgeConfirmation : false ,
2024-06-08 13:42:57 +03:00
heartAnimation : false ,
heartPosition : {
top : 0 ,
left : 0
2024-09-17 09:43:53 +02:00
} ,
2025-03-31 16:33:54 +02:00
reverseOrder : false ,
2025-11-03 16:47:41 +01:00
downloadingChat : false ,
reportingEventId : null ,
2025-11-08 22:48:59 +02:00
showDeletePostPopup : false ,
2020-11-19 22:48:08 +01:00
} ;
} ,
2020-11-09 10:26:56 +01:00
2020-11-11 17:35:14 +01:00
mounted ( ) {
2025-05-06 11:34:53 +02:00
emitter . on ( 'audio-playback-ended' , this . audioPlaybackEnded ) ;
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
if ( container ) {
this . scrollPosition = new ScrollPosition ( container ) ;
if ( this . $refs . chatContainerResizer ) {
this . chatContainerSize = this . $refs . chatContainerResizer . $el . clientHeight ;
}
}
2020-11-11 17:35:14 +01:00
} ,
2025-05-13 16:12:05 +02:00
beforeUnmount ( ) {
2025-05-06 11:34:53 +02:00
emitter . off ( 'audio-playback-ended' , this . audioPlaybackEnded ) ;
2023-05-26 15:56:59 +00:00
this . $audioPlayer . pause ( ) ;
2021-01-15 11:50:21 +01:00
this . stopRRTimer ( ) ;
2024-02-06 10:22:35 +00:00
if ( this . retentionTimer ) {
clearInterval ( this . retentionTimer ) ;
this . retentionTimer = null ;
2024-03-24 11:35:05 +02:00
}
2021-01-15 11:50:21 +01:00
} ,
2025-05-13 16:12:05 +02:00
unmounted ( ) {
2020-11-11 17:35:14 +01:00
this . $matrix . off ( "Room.timeline" , this . onEvent ) ;
this . $matrix . off ( "RoomMember.typing" , this . onUserTyping ) ;
} ,
2020-11-09 10:26:56 +01:00
computed : {
2025-05-12 17:15:11 +02:00
additionalEmojiGroups ( ) {
return { 'recently_used' : this . recentEmojis }
} ,
additionalEmojiGroupIcons ( ) {
return { 'recently_used' : timerIcon }
} ,
2025-05-08 11:52:39 +02:00
latestReply ( ) {
const contents = this . replyToEvent ? this . replyToEvent . getContent ( ) . body : "" ;
const contentArr = contents . split ( "\n" ) . reverse ( ) ;
if ( contentArr [ 0 ] === "" ) {
contentArr . shift ( ) ;
}
return ( contentArr && contentArr . length > 0 ) ? contentArr [ 0 ] . replace ( /^> (<.*> )?/g , "" ) : "" ;
} ,
2024-05-18 22:13:07 +03:00
heartEmoji ( ) {
2025-06-10 13:35:51 +02:00
return "❤️" ;
2024-05-18 22:13:07 +03:00
} ,
2024-03-24 11:35:05 +02:00
compActiveMember ( ) {
const currentUserId = this . selectedEvent ? . sender . userId || this . $matrix . currentUserId
return this . joinedAndInvitedMembers . find ( ( { userId } ) => userId === currentUserId )
} ,
2023-01-30 08:36:02 +00:00
chatContainer ( ) {
const container = this . $refs . chatContainer ;
2023-02-17 22:00:47 +01:00
if ( this . useVoiceMode ) {
2023-01-30 08:36:02 +00:00
return container . $el ;
}
return container ;
} ,
2022-11-20 13:39:20 +02:00
senderDisplayName ( ) {
return this . room . getMember ( this . replyToEvent . sender . userId ) . name ;
} ,
2021-01-11 17:42:58 +01:00
currentUser ( ) {
return this . $store . state . auth . user ;
} ,
2020-12-09 15:20:50 +01:00
room ( ) {
return this . $matrix . currentRoom ;
} ,
2020-11-09 10:26:56 +01:00
roomId ( ) {
2021-02-25 14:21:21 +01:00
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".
2021-02-17 17:12:16 +01:00
return null ; // Not ready yet...
}
2021-01-15 11:50:21 +01:00
if ( this . room ) {
return this . room . roomId ;
}
2021-01-11 17:42:58 +01:00
return this . $matrix . currentRoomId ;
2020-11-09 10:26:56 +01:00
} ,
2021-01-28 22:13:08 +01:00
roomAliasOrId ( ) {
if ( this . room ) {
return this . room . getCanonicalAlias ( ) || this . room . roomId ;
}
return this . $matrix . currentRoomId ;
} ,
2021-01-14 16:17:05 +01:00
readMarker ( ) {
2021-01-15 11:50:21 +01:00
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 ( ) ;
}
2022-05-17 15:16:53 +00:00
return this . fullyReadMarker || this . room . getEventReadUpTo ( this . $matrix . currentUserId , false ) ;
2021-01-14 16:17:05 +01:00
} ,
fullyReadMarker ( ) {
2023-03-16 08:17:29 +00:00
const readEvent = this . room && this . room . getAccountData ( "m.fully_read" ) ;
2021-01-14 16:17:05 +01:00
if ( readEvent ) {
return readEvent . getContent ( ) . event _id ;
}
return null ;
} ,
2020-12-14 17:12:29 +01:00
attachButtonDisabled ( ) {
2022-05-17 15:16:53 +00:00
return this . editedEvent != null || this . replyToEvent != null || this . currentInput . length > 0 ;
2020-12-14 17:12:29 +01:00
} ,
2020-11-09 10:26:56 +01:00
sendButtonDisabled ( ) {
return this . currentInput . length == 0 ;
} ,
2020-12-09 21:50:53 +01:00
typingMembersString ( ) {
const count = this . typingMembers . length ;
if ( count > 1 ) {
2021-05-25 12:04:51 +02:00
return this . $t ( "message.users_are_typing" , { count : count } ) ;
2020-12-09 21:50:53 +01:00
} else if ( count > 0 ) {
2021-05-25 12:04:51 +02:00
return this . $t ( "message.user_is_typing" , {
user : this . typingMembers [ 0 ] . name ,
} ) ;
2020-12-09 21:50:53 +01:00
} else {
return "" ;
}
2020-12-14 16:11:45 +01:00
} ,
2021-07-19 10:33:25 +02:00
showMessageOperations ( ) {
return this . selectedEvent && this . showContextMenu ;
2021-02-17 11:59:07 +01:00
} ,
2021-03-18 11:58:46 +01:00
canRecordAudio ( ) {
return util . browserCanRecordAudio ( ) ;
2021-03-26 12:16:13 +01:00
} ,
2022-05-03 09:40:02 +00:00
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 ) ;
2022-05-17 15:16:53 +00:00
let isAdmin =
me && this . room . currentState && this . room . currentState . hasSufficientPowerLevelFor ( "redact" , me . powerLevel ) ;
2022-05-03 09:40:02 +00:00
return isAdmin ;
2022-05-17 15:16:53 +00:00
} ,
2023-02-17 22:00:47 +01:00
useVoiceMode : {
2023-01-30 08:36:02 +00:00
get : function ( ) {
2023-02-17 22:00:47 +01:00
if ( ! this . $config . experimental _voice _mode ) return false ;
2023-08-07 14:13:35 +00:00
return ( util . roomDisplayTypeOverride ( this . room ) || this . roomDisplayType ) === ROOM _TYPE _VOICE _MODE ;
2023-01-30 08:36:02 +00:00
} ,
2023-02-28 10:29:53 +00:00
} ,
2023-06-28 12:14:44 +00:00
useFileModeNonAdmin : {
get : function ( ) {
2025-01-14 11:14:11 +00:00
return this . roomDisplayType === ROOM _TYPE _FILE _MODE && ! this . canCreatePoll ; // TODO - Check user or admin
2023-06-28 12:14:44 +00:00
}
} ,
2023-04-27 07:09:20 +00:00
/ * *
* 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 ( ) {
2024-10-11 17:04:32 +02:00
let events = this . events ;
2023-04-27 07:09:20 +00:00
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.
2024-10-11 17:04:32 +02:00
events = this . events . slice ( idx + 1 ) ;
2023-04-27 07:09:20 +00:00
}
}
}
}
2024-10-11 17:04:32 +02:00
// Filter out relations and redactions
events = this . events . toReversed ( ) . filter ( ( e ) => ! e . isRelation ( ) && ! e . isRedaction ( ) ) ;
2024-10-17 10:22:24 +02:00
// If Channel, remove all redacted events as well.
if ( this . room && this . room . displayType == ROOM _TYPE _CHANNEL ) {
events = events . filter ( ( e ) => ! e . isRedacted ( ) ) ;
}
2024-10-11 17:04:32 +02:00
// Add read marker, if it is not newer than the "latest" message we are going to display
//
2024-10-16 15:33:35 +02:00
let showReadMarker = false ;
2024-10-11 17:04:32 +02:00
let lastDisplayedEvent = undefined ;
events = events . flatMap ( ( e ) => {
let result = [ ] ;
2025-05-13 16:32:09 +02:00
if ( e . isEncrypted ( ) ) {
if ( e . getClearContent ( ) ) {
e . component = this . componentForEvent ( e , false ) ;
} else {
this . $matrix . matrixClient . decryptEventIfNeeded ( e ) . then ( ( ) => {
e . component = this . componentForEvent ( e , false ) ;
} ) ;
}
} else {
e . component = this . componentForEvent ( e , false ) ;
}
2024-10-16 15:33:35 +02:00
if ( e . getId ( ) == this . readMarker && showReadMarker ) {
2024-10-11 17:04:32 +02:00
const readMarkerEvent = ROOM _READ _MARKER _EVENT _PLACEHOLDER ;
2025-05-06 09:27:53 +02:00
readMarkerEvent [ "component" ] = this . componentForEvent ( readMarkerEvent , false ) ;
2024-10-11 17:04:32 +02:00
if ( readMarkerEvent . component ) {
2025-05-06 09:27:53 +02:00
e [ "nextDisplayedEvent" ] = lastDisplayedEvent ;
2024-10-11 17:04:32 +02:00
}
result . push ( readMarkerEvent ) ;
}
if ( e . component ) {
2025-05-06 09:27:53 +02:00
e [ "nextDisplayedEvent" ] = lastDisplayedEvent ;
2024-10-11 17:04:32 +02:00
lastDisplayedEvent = e ;
2024-10-16 15:33:35 +02:00
if ( e . getSender ( ) !== this . $matrix . currentUserId ) {
showReadMarker = true ;
}
2024-10-11 17:04:32 +02:00
}
result . push ( e ) ;
return result ;
} )
return ( this . reverseOrder ? events : events . toReversed ( ) ) // Reverse back if needed
2023-08-30 10:50:45 +02:00
} ,
roomCreatedByUsRecently ( ) {
const createEvent = this . room && this . room . currentState . getStateEvents ( "m.room.create" , "" ) ;
if ( createEvent ) {
2025-08-06 12:07:50 +02:00
const creatorId = createEvent . getSender ( ) ;
2023-08-30 10:50:45 +02:00
return ( creatorId == this . $matrix . currentUserId && createEvent . getLocalAge ( ) < 5 * 60000 /* 5 minutes */ ) ;
}
return false ;
} ,
isDirectRoom ( ) {
2023-10-18 15:05:50 +00:00
return this . room && this . room . getJoinRule ( ) == "invite" && this . joinedAndInvitedMembers . length == 2 ;
} ,
isPublicRoom ( ) {
return this . room && this . room . getJoinRule ( ) == "public" ;
2023-08-30 10:50:45 +02:00
} ,
2024-04-03 09:35:20 +02:00
showRoomWelcomeHeader ( ) {
return this . roomWelcomeHeader != null ;
2023-08-30 10:50:45 +02:00
} ,
2024-04-03 09:35:20 +02:00
roomWelcomeHeader ( ) {
if ( ! this . hideRoomWelcomeHeader && this . roomCreatedByUsRecently ) {
if ( this . roomDisplayType == ROOM _TYPE _CHANNEL ) {
2025-05-08 11:52:39 +02:00
return markRaw ( WelcomeHeaderChannel ) ;
2024-04-03 09:35:20 +02:00
}
if ( this . isDirectRoom ) {
2025-05-08 11:52:39 +02:00
return markRaw ( WelcomeHeaderDirectChat ) ;
2024-04-03 09:35:20 +02:00
}
2025-05-08 11:52:39 +02:00
return markRaw ( WelcomeHeaderRoom ) ;
2024-04-03 09:35:20 +02:00
}
return null ;
2023-10-18 15:05:50 +00:00
} ,
2024-10-16 15:33:35 +02:00
messageOperationsComponent ( ) {
if ( this . room . displayType == ROOM _TYPE _CHANNEL ) {
2025-05-08 11:52:39 +02:00
return markRaw ( MessageOperationsChannel ) ;
2024-10-16 15:33:35 +02:00
}
2025-05-08 11:52:39 +02:00
return markRaw ( MessageOperations ) ;
2024-10-16 15:33:35 +02:00
} ,
2023-10-18 15:05:50 +00:00
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 "" ;
2023-11-06 15:28:26 +00:00
} ,
/ * *
* 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 "" ;
2024-06-08 13:42:57 +03:00
} ,
hearAnimationPosition ( ) {
return {
'--top' : this . heartPosition . top ,
'--left' : this . heartPosition . left
} ;
2025-11-03 16:47:41 +01:00
} ,
reportingEventShown : {
get ( ) {
return this . reportingEventId != null ;
} ,
set ( newValue ) {
if ( ! newValue ) {
this . reportingEventId = null ;
}
}
2023-04-27 07:09:20 +00:00
}
2020-11-09 10:26:56 +01:00
} ,
watch : {
2023-01-30 08:36:02 +00:00
initialLoadDone : {
immediate : true ,
handler ( value , oldValue ) {
if ( value && ! oldValue ) {
2023-11-06 15:28:26 +00:00
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 ) ) ;
2023-01-30 08:36:02 +00:00
console . log ( "Loading finished!" ) ;
2024-02-06 10:22:35 +00:00
this . updateRetentionTimer ( ) ;
} else if ( ! value ) {
if ( this . retentionTimer ) {
clearInterval ( this . retentionTimer ) ;
this . retentionTimer = null ;
}
2023-01-30 08:36:02 +00:00
}
}
} ,
2021-01-28 22:13:08 +01:00
roomId : {
2020-12-15 17:06:26 +01:00
immediate : true ,
2021-01-28 22:13:08 +01:00
handler ( value , oldValue ) {
if ( value && value == oldValue ) {
2021-01-12 11:26:01 +01:00
return ; // No change.
}
2022-05-17 15:16:53 +00:00
console . log ( "Chat: Current room changed to " + ( value ? value : "null" ) ) ;
2020-11-09 10:26:56 +01:00
2020-11-25 14:42:50 +01:00
// Clear old events
2021-03-04 11:31:21 +01:00
this . $matrix . off ( "Room.timeline" , this . onEvent ) ;
this . $matrix . off ( "RoomMember.typing" , this . onUserTyping ) ;
2023-04-04 14:30:50 +00:00
this . waitingForRoomObject = false ;
2020-11-25 14:42:50 +01:00
this . events = [ ] ;
this . timelineWindow = null ;
2020-12-09 21:50:53 +01:00
this . typingMembers = [ ] ;
2021-01-14 16:17:05 +01:00
this . initialLoadDone = false ;
2024-04-03 09:35:20 +02:00
this . hideRoomWelcomeHeader = false ;
2024-07-23 09:36:57 +00:00
this . newlyJoinedRoom = false ;
2021-01-14 16:17:05 +01:00
2021-01-15 11:50:21 +01:00
// Stop RR timer
this . stopRRTimer ( ) ;
this . lastRR = null ;
2023-04-04 14:30:50 +00:00
if ( this . roomId ) {
this . $matrix . isJoinedToRoom ( this . roomId ) . then ( joined => {
if ( ! joined ) {
this . onRoomNotJoined ( ) ;
} else {
if ( this . room ) {
2024-09-24 11:17:17 +02:00
this . onRoomJoined ( this . roomDisplayType == ROOM _TYPE _CHANNEL ? null : this . readMarker ) ;
2023-04-04 14:30:50 +00:00
} else {
this . waitingForRoomObject = true ;
return ; // no room, wait for it (we know we are joined so need to wait for sync to complete)
}
}
} ) ;
} else {
2023-11-06 15:28:26 +00:00
this . setInitialLoadDone ( ) ;
2020-11-25 14:42:50 +01:00
return ; // no room
}
2021-02-17 11:59:07 +01:00
} ,
2021-01-11 17:42:58 +01:00
} ,
2023-04-04 14:30:50 +00:00
room ( ) {
// Were we waiting?
if ( this . room && this . room . roomId == this . roomId && this . waitingForRoomObject ) {
this . waitingForRoomObject = false ;
2024-09-24 11:17:17 +02:00
this . onRoomJoined ( this . roomDisplayType == ROOM _TYPE _CHANNEL ? null : this . readMarker ) ;
2023-04-04 14:30:50 +00:00
}
} ,
2024-07-22 10:39:53 +02:00
showMessageOperations ( show ) {
if ( show ) {
2021-07-19 10:33:25 +02:00
this . $nextTick ( ( ) => {
// Calculate where to show the context menu.
//
2022-05-17 15:16:53 +00:00
const ref = this . selectedEvent && this . $refs [ this . selectedEvent . getId ( ) ] ;
2021-07-19 10:33:25 +02:00
var top = 0 ;
var left = 0 ;
if ( ref && ref [ 0 ] ) {
if ( this . showContextMenuAnchor ) {
2022-05-17 15:16:53 +00:00
var rectAnchor = this . showContextMenuAnchor . getBoundingClientRect ( ) ;
var rectChat = this . $refs . messageOperationsStrut . getBoundingClientRect ( ) ;
var rectOps = this . $refs . messageOperations . $el . getBoundingClientRect ( ) ;
2024-10-11 17:04:32 +02:00
if ( this . room . displayType == ROOM _TYPE _CHANNEL ) {
top = rectAnchor . top - rectChat . top ;
2025-10-19 14:05:29 +03:00
let right = rectChat . right - rectAnchor . right ;
2024-10-11 17:04:32 +02:00
this . opStyle = "top:" + top + "px;right:" + right + "px" ;
return ;
} else {
2022-04-25 08:43:27 +00:00
top = rectAnchor . top - rectChat . top - 50 ;
2023-04-10 10:05:46 +03:00
left = rectAnchor . left - rectChat . left - 75 ;
2023-10-25 15:08:23 +02:00
if ( left + rectOps . width + 10 >= rectChat . right ) {
2021-07-19 10:33:25 +02:00
left = rectChat . right - rectOps . width - 10 ; // No overflow
2023-10-25 15:08:23 +02:00
} else if ( left < 0 ) {
left = 0 ;
2021-07-19 10:33:25 +02:00
}
}
2024-10-11 17:04:32 +02:00
}
2021-07-19 10:33:25 +02:00
}
this . opStyle = "top:" + top + "px;left:" + left + "px" ;
} ) ;
}
} ,
2023-02-17 22:00:47 +01:00
showRecorder ( show ) {
if ( this . useVoiceMode ) {
// Send typing indicators when recorder UI is opened/closed
this . $matrix . matrixClient . sendTyping ( this . roomId , show , 10 * 60 * 1000 ) ;
}
}
2021-01-11 17:42:58 +01:00
} ,
methods : {
2023-11-06 15:28:26 +00:00
/ * *
2024-07-02 11:08:03 +02:00
* Set initialLoadDone to 'true' . This will process all events , setting threadParent and replyEvent if needed ( see watcher for "initialLoadDone" )
2023-11-06 15:28:26 +00:00
* /
setInitialLoadDone ( ) {
this . initialLoadDone = true ;
} ,
2024-02-06 10:22:35 +00:00
/ * *
2024-10-11 17:04:32 +02:00
* Set events to display . At the same time , filter out messages that are past rentention period etc . Also , filter pinned events "at the top"
2024-02-06 10:22:35 +00:00
* /
2024-10-11 17:04:32 +02:00
setEvents ( events , onlyIfLengthChanges = false ) {
let updated = this . filterOutOldAndInvisible ( events ) ;
// Handle pinning
//
2024-10-17 10:22:24 +02:00
if ( this . room ) {
const pinnedEvents = this . $matrix . getPinnedEvents ( this . room ) ;
updated . forEach ( ( e ) => {
2025-05-06 09:27:53 +02:00
e [ "isPinned" ] = pinnedEvents . includes ( e . threadParent ? e . threadParent . getId ( ) : e . getId ( ) ) ;
e [ "isChannelMessage" ] = ( this . room && this . roomDisplayType == ROOM _TYPE _CHANNEL ) ;
2024-10-17 10:22:24 +02:00
} ) ;
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 )
}
} ) ;
}
2024-10-11 17:04:32 +02:00
if ( ! onlyIfLengthChanges || updated . length != this . events . length ) {
this . events = updated ; // Changed
}
2024-04-09 14:34:07 +02:00
} ,
filterOutOldAndInvisible ( events ) {
return this . removeTimedOutEvents ( events . filter ( ( e ) => e . messageVisibility ( ) . visible ) ) ;
2024-02-06 10:22:35 +00:00
} ,
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 ;
2024-03-24 11:35:05 +02:00
}
2024-02-06 10:22:35 +00:00
} ,
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
2024-04-09 14:34:07 +02:00
if ( e . getLocalAge ( ) < maxLifetime ) {
return true ;
}
e . applyVisibilityEvent ( { visible : false , eventId : e . getId ( ) , reason : null } ) ;
return true ;
2024-02-06 10:22:35 +00:00
}
return true ;
} ) ;
} ,
onRetentionTimer ( ) {
2024-10-11 17:04:32 +02:00
this . setEvents ( this . events , true ) ;
2024-02-06 10:22:35 +00:00
} ,
2021-03-01 21:47:22 +01:00
onRoomJoined ( initialEventId ) {
2024-07-23 09:36:57 +00:00
// If our own join event is less than a minute old, consider this a "newly joined" room.
2025-10-19 14:05:29 +03:00
//
2024-07-23 09:36:57 +00:00
// 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 */ ;
}
2024-09-24 11:17:17 +02:00
this . reverseOrder = ( this . room && this . roomDisplayType == ROOM _TYPE _CHANNEL ) ;
2025-05-06 09:27:53 +02:00
this . room [ "displayType" ] = this . roomDisplayType ;
2025-08-07 12:00:09 +02:00
this . roomUpgradeInfo = undefined ;
if ( this . room . userMayUpgradeRoom ( this . $matrix . currentUserId ) ) {
this . room . getRecommendedVersion ( ) . then ( ( info ) => {
2025-08-13 16:04:29 +02:00
const thisVersion = this . room . getVersion ( ) ;
if ( thisVersion != info . version ) {
this . roomUpgradeInfo = info ;
}
2025-08-07 12:00:09 +02:00
} ) ;
}
2024-09-24 11:17:17 +02:00
2021-03-04 11:31:21 +01:00
// Listen to events
this . $matrix . on ( "Room.timeline" , this . onEvent ) ;
this . $matrix . on ( "RoomMember.typing" , this . onUserTyping ) ;
2021-01-14 16:17:05 +01:00
console . log ( "Read up to " + initialEventId ) ;
2021-04-09 16:40:03 +02:00
//initialEventId = null;
2021-09-14 11:57:49 +02:00
this . timelineSet = this . room . getUnfilteredTimelineSet ( ) ;
2022-05-17 15:16:53 +00:00
this . timelineWindow = new TimelineWindow ( this . $matrix . matrixClient , this . timelineSet , { } ) ;
2021-01-28 22:13:08 +01:00
const self = this ;
2021-03-04 12:48:32 +01:00
this . timelineWindow
. load ( initialEventId , 20 )
. then ( ( ) => {
2024-02-06 10:22:35 +00:00
self . setEvents ( self . timelineWindow . getEvents ( ) ) ;
2021-03-04 12:48:32 +01:00
const getMoreIfNeeded = function _getMoreIfNeeded ( ) {
const container = self . $refs . chatContainer ;
if (
2023-01-30 08:36:02 +00:00
container &&
2022-05-17 15:16:53 +00:00
container . scrollHeight <= ( 1 + 2 * WINDOW _BUFFER _SIZE ) * container . clientHeight &&
2021-03-04 12:48:32 +01:00
self . timelineWindow &&
self . timelineWindow . canPaginate ( EventTimeline . BACKWARDS )
) {
2022-05-17 15:16:53 +00:00
return self . timelineWindow . paginate ( EventTimeline . BACKWARDS , 10 , true , 5 ) . then ( ( success ) => {
2024-02-06 10:22:35 +00:00
self . setEvents ( self . timelineWindow . getEvents ( ) ) ;
2022-05-17 15:16:53 +00:00
if ( success ) {
return _getMoreIfNeeded . call ( self ) ;
} else {
return Promise . reject ( "Failed to paginate" ) ;
}
} ) ;
2021-03-04 12:48:32 +01:00
} else {
return Promise . resolve ( "Done" ) ;
}
} . bind ( self ) ;
getMoreIfNeeded ( )
. catch ( ( err ) => {
console . log ( "ERROR " + err ) ;
} )
. finally ( ( ) => {
2023-11-06 15:28:26 +00:00
// 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 ( ) ;
2024-04-03 09:35:20 +02:00
if ( initialEventId && ! this . showRoomWelcomeHeader ) {
2023-11-06 15:28:26 +00:00
const event = this . room . findEventById ( initialEventId ) ;
this . $nextTick ( ( ) => {
if ( event && event . parentThread ) {
self . scrollToEvent ( event . parentThread . getId ( ) ) ;
} else {
self . scrollToEvent ( initialEventId ) ;
}
} ) ;
2024-04-03 09:35:20 +02:00
} else if ( this . showRoomWelcomeHeader ) {
2021-04-01 22:59:19 +02:00
self . onScroll ( ) ;
2021-03-04 12:48:32 +01:00
}
self . restartRRTimer ( ) ;
} ) ;
} )
. catch ( ( err ) => {
console . log ( "Error fetching events!" , err , this ) ;
2024-06-28 11:49:55 +02:00
if ( initialEventId ) {
2021-03-04 12:48:32 +01:00
// Try again without initial event!
this . onRoomJoined ( null ) ;
2021-02-17 11:59:07 +01:00
} else {
2021-03-04 12:48:32 +01:00
// Error. Done loading.
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2023-11-06 15:28:26 +00:00
this . setInitialLoadDone ( ) ;
2021-02-17 11:59:07 +01:00
}
2022-03-23 14:11:50 +01:00
} )
. finally ( ( ) => {
for ( var event of this . events ) {
this . $matrix . matrixClient . decryptEventIfNeeded ( event , { } ) ;
}
2021-03-04 12:48:32 +01:00
} ) ;
2020-11-09 10:26:56 +01:00
} ,
2021-01-11 17:42:58 +01:00
onRoomNotJoined ( ) {
2021-02-17 11:59:07 +01:00
this . $navigation . push (
{
name : "Join" ,
2025-10-19 14:05:29 +03:00
params : {
roomId : util . sanitizeRoomId ( this . roomAliasOrId ) ,
2025-05-28 12:29:04 +02:00
join : this . $route . params . join
} ,
query : this . $route . query
2021-02-17 11:59:07 +01:00
} ,
0
) ;
2021-01-11 17:42:58 +01:00
} ,
2021-04-09 16:20:57 +02:00
scrollToEndOfTimeline ( ) {
2022-05-17 15:16:53 +00:00
if ( this . timelineWindow && this . timelineWindow . canPaginate ( EventTimeline . FORWARDS ) ) {
2021-04-09 16:20:57 +02:00
this . loading = true ;
2021-05-11 21:03:54 +02:00
// Instead of paging though ALL history, just reload a timeline at the live marker...
2021-09-14 11:57:49 +02:00
var timelineSet = this . room . getUnfilteredTimelineSet ( ) ;
2022-05-17 15:16:53 +00:00
var timelineWindow = new TimelineWindow ( this . $matrix . matrixClient , timelineSet , { } ) ;
2021-05-11 21:03:54 +02:00
const self = this ;
timelineWindow
. load ( null , 20 )
. then ( ( ) => {
2021-09-14 11:57:49 +02:00
self . timelineSet = timelineSet ;
2021-05-11 21:03:54 +02:00
self . timelineWindow = timelineWindow ;
2024-02-06 10:22:35 +00:00
self . setEvents ( self . timelineWindow . getEvents ( ) ) ;
2021-05-11 21:03:54 +02:00
} )
. finally ( ( ) => {
this . loading = false ;
} ) ;
2021-04-09 16:20:57 +02:00
} else {
// Can't paginate, just scroll to bottom of window!
2024-09-17 09:43:53 +02:00
this . smoothScrollToLatest ( ) ;
2021-04-09 16:20:57 +02:00
}
} ,
2020-12-03 22:12:50 +01:00
touchX ( event ) {
2020-12-04 10:44:46 +01:00
if ( event . type . indexOf ( "mouse" ) !== - 1 ) {
2020-12-03 22:12:50 +01:00
return event . clientX ;
2020-12-03 10:00:23 +01:00
}
2020-12-03 22:12:50 +01:00
return event . touches [ 0 ] . clientX ;
2020-12-03 10:00:23 +01:00
} ,
2020-12-03 22:12:50 +01:00
touchY ( event ) {
2020-12-04 10:44:46 +01:00
if ( event . type . indexOf ( "mouse" ) !== - 1 ) {
2020-12-03 22:12:50 +01:00
return event . clientY ;
}
return event . touches [ 0 ] . clientY ;
} ,
touchStart ( e , event ) {
if ( this . selectedEvent != event ) {
this . showContextMenu = false ;
}
2020-12-03 10:00:23 +01:00
this . selectedEvent = event ;
2020-12-03 22:12:50 +01:00
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 ;
2020-12-04 10:44:46 +01:00
var touchMoved =
Math . abs ( this . touchStartX - this . touchCurrentX ) > tapTolerance ||
Math . abs ( this . touchStartY - this . touchCurrentY ) > tapTolerance ;
if ( touchMoved ) {
2020-12-03 22:12:50 +01:00
this . touchTimer && clearTimeout ( this . touchTimer ) ;
}
} ,
2020-12-04 10:44:46 +01:00
/ * *
2021-03-05 22:34:00 +01:00
* Triggered when our "long tap" timer hits .
2020-12-04 10:44:46 +01:00
* /
2020-12-03 22:12:50 +01:00
touchTimerElapsed ( ) {
2020-12-03 10:00:23 +01:00
this . showContextMenu = true ;
} ,
2020-12-04 10:44:46 +01:00
/ * *
* 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 .
* /
2020-12-04 12:15:47 +01:00
handleChatContainerResize ( { ignoredWidth , height } ) {
2020-12-04 10:44:46 +01:00
const delta = height - this . chatContainerSize ;
this . chatContainerSize = height ;
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
if ( container && delta < 0 ) {
2020-12-04 10:44:46 +01:00
container . scrollTop -= delta ;
}
} ,
2020-11-11 17:35:14 +01:00
paginateBackIfNeeded ( ) {
2020-11-17 20:02:42 +01:00
this . $nextTick ( ( ) => {
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
if ( container && container . scrollHeight <= container . clientHeight ) {
2024-09-17 09:43:53 +02:00
this . handleScrolledToOldest ( ) ;
2020-11-17 20:02:42 +01:00
}
} ) ;
2020-11-11 17:35:14 +01:00
} ,
onScroll ( ignoredevent ) {
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
2021-01-15 11:50:21 +01:00
if ( ! container ) {
return ;
}
2021-04-09 16:40:03 +02:00
const bufferHeight = container . clientHeight * WINDOW _BUFFER _SIZE ;
if ( container . scrollTop <= bufferHeight ) {
2020-11-11 17:35:14 +01:00
// Scrolled to top
2024-09-17 09:43:53 +02:00
if ( this . reverseOrder ) {
this . handleScrolledToLatest ( false ) ;
} else {
this . handleScrolledToOldest ( ) ;
}
2022-05-17 15:16:53 +00:00
} else if ( container . scrollHeight - container . scrollTop . toFixed ( 0 ) - container . clientHeight <= bufferHeight ) {
2024-09-17 09:43:53 +02:00
if ( this . reverseOrder ) {
this . handleScrolledToOldest ( ) ;
} else {
this . handleScrolledToLatest ( false ) ;
}
2020-11-11 17:35:14 +01:00
}
2025-06-19 10:47:16 +02:00
this . delayedUpdateOfScrollToBottom ( ) ;
2021-01-14 16:17:05 +01:00
this . restartRRTimer ( ) ;
2020-11-11 17:35:14 +01:00
} ,
2023-11-06 15:28:26 +00:00
setParentThread ( event ) {
const parentEvent = this . timelineSet . findEventById ( event . threadRootId ) || this . room . findEventById ( event . threadRootId ) ;
if ( parentEvent ) {
2025-05-06 09:27:53 +02:00
parentEvent [ "isMxThread" ] = true ;
event [ "parentThread" ] = parentEvent ;
2023-11-06 15:28:26 +00:00
} else {
// Try to load from server.
2024-06-28 11:49:55 +02:00
this . $matrix . matrixClient . getEventTimeline ( this . timelineSet , event . threadRootId )
. then ( ( tl ) => {
2023-11-06 15:28:26 +00:00
if ( tl ) {
const parentEvent = tl . getEvents ( ) . find ( ( e ) => e . getId ( ) === event . threadRootId ) ;
if ( parentEvent ) {
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2023-11-06 15:28:26 +00:00
const fn = ( ) => {
2025-05-06 09:27:53 +02:00
parentEvent [ "isMxThread" ] = true ;
event [ "parentThread" ] = parentEvent ;
2023-11-06 15:28:26 +00:00
} ;
if ( this . initialLoadDone ) {
const sel = "[eventId=\"" + parentEvent . getId ( ) + "\"]" ;
const element = document . querySelector ( sel ) ;
if ( element ) {
2025-06-10 13:35:51 +02:00
this . onLayoutChange ( { action : fn , element : element } ) ;
2023-11-06 15:28:26 +00:00
} else {
fn ( ) ;
2023-12-07 20:53:24 +02:00
}
2023-11-06 15:28:26 +00:00
} else {
fn ( ) ;
}
}
}
2024-06-28 11:49:55 +02:00
} ) . catch ( e => console . error ( e ) ) ;
2023-11-06 15:28:26 +00:00
}
} ,
setReplyToEvent ( event ) {
const parentEvent = this . timelineSet . findEventById ( event . replyEventId ) || this . room . findEventById ( event . replyEventId ) ;
if ( parentEvent ) {
2025-05-06 09:27:53 +02:00
event [ "replyEvent" ] = parentEvent ;
2023-11-06 15:28:26 +00:00
} 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 ) {
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2025-05-06 09:27:53 +02:00
const fn = ( ) => { event [ "replyEvent" ] = parentEvent ; } ;
2023-11-06 15:28:26 +00:00
if ( this . initialLoadDone ) {
const sel = "[eventId=\"" + parentEvent . getId ( ) + "\"]" ;
const element = document . querySelector ( sel ) ;
if ( element ) {
2025-06-10 13:35:51 +02:00
this . onLayoutChange ( { action : fn , element : element } ) ;
2023-11-06 15:28:26 +00:00
} else {
fn ( ) ;
}
} else {
fn ( ) ;
}
}
}
} ) . catch ( e => console . error ( e ) ) ;
}
} ,
2025-06-19 10:47:16 +02:00
delayedUpdateOfScrollToBottom ( ) {
if ( this . scrollUpdateTimer ) {
clearTimeout ( this . scrollUpdateTimer ) ;
}
this . scrollUpdateTimer = setTimeout ( ( ) => {
this . scrollUpdateTimer = null ;
const container = this . chatContainer ;
if ( ! container ) {
return ;
}
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 ) ) ;
} , 1000 ) ;
} ,
2020-11-09 15:08:36 +01:00
onEvent ( event ) {
2021-05-20 12:33:59 +02:00
//console.log("OnEvent", JSON.stringify(event));
2020-11-09 15:08:36 +01:00
if ( event . getRoomId ( ) !== this . roomId ) {
return ; // Not for this room
}
2021-01-14 16:17:05 +01:00
2023-11-06 15:28:26 +00:00
if ( this . initialLoadDone && event . threadRootId && ! event . parentThread ) {
this . setParentThread ( event ) ;
}
if ( this . initialLoadDone && event . replyEventId && ! event . replyEvent ) {
this . setReplyToEvent ( event ) ;
}
2023-01-30 08:36:02 +00:00
const loadingDone = this . initialLoadDone ;
2022-03-23 14:11:50 +01:00
this . $matrix . matrixClient . decryptEventIfNeeded ( event , { } ) ;
2023-02-17 22:00:47 +01:00
if ( this . initialLoadDone && ! this . useVoiceMode ) {
2021-01-14 16:17:05 +01:00
this . paginateBackIfNeeded ( ) ;
}
2020-11-17 20:02:42 +01:00
2024-10-22 11:59:07 +02:00
if ( loadingDone && event . forwardLooking && ( ! event . isRelation ( ) || event . isMxThread || event . threadRootId || event . parentThread ) ) {
2023-01-30 08:36:02 +00:00
// If we are at bottom, scroll to see new events...
2024-10-22 11:59:07 +02:00
var scrollToSeeNew = ! event . isRedaction ( ) && event . getSender ( ) == this . $matrix . currentUserId ; // When we sent, scroll
2025-06-19 10:47:16 +02:00
this . handleScrolledToLatest ( scrollToSeeNew || ! this . showScrollToEnd ) ;
2023-06-08 13:25:02 +00:00
// If kick or ban event, redirect to "goodbye"...
2023-07-17 15:00:40 +03:00
if ( event . getType ( ) === "m.room.member" &&
2023-06-08 13:25:02 +00:00
event . getStateKey ( ) == this . $matrix . currentUserId &&
( event . getPrevContent ( ) || { } ) . membership == "join" &&
(
( event . getContent ( ) . membership == "leave" && event . getSender ( ) != this . currentUserId ) ||
2024-02-06 10:22:35 +00:00
( 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 ) ;
2023-06-08 13:25:02 +00:00
}
2024-02-06 10:22:35 +00:00
else if ( event . getType ( ) === "m.room.retention" ) {
this . updateRetentionTimer ( event ) ;
}
}
2020-11-09 15:08:36 +01:00
} ,
2020-12-09 21:50:53 +01:00
onUserTyping ( event , member ) {
if ( member . roomId !== this . roomId ) {
2020-11-11 17:35:14 +01:00
return ; // Not for this room
2020-11-09 10:26:56 +01:00
}
2020-12-09 21:50:53 +01:00
if ( member . typing ) {
if ( ! this . typingMembers . includes ( member ) ) {
this . typingMembers . push ( member ) ;
}
} else {
const index = this . typingMembers . indexOf ( member ) ;
if ( index > - 1 ) {
2021-02-17 11:59:07 +01:00
this . typingMembers . splice ( index , 1 ) ;
2020-12-09 21:50:53 +01:00
}
}
2021-05-20 12:33:59 +02:00
//console.log("Typing: ", this.typingMembers);
2020-11-09 10:26:56 +01:00
} ,
2021-03-10 17:24:48 +01:00
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 ) {
2025-08-06 12:07:50 +02:00
//TEMP TEMP TEMP
if ( text . startsWith ( "/version" ) && this . room && this . room . userMayUpgradeRoom ( this . $matrix . currentUserId ) ) {
this . room . getRecommendedVersion ( ) . then ( ( v ) => {
console . error ( "Room version:" , this . room . getVersion ( ) ) ;
console . error ( "Recommended:" , v ) ;
} ) ;
this . $matrix . matrixClient . getCapabilities ( ) . then ( ( c ) => {
console . error ( "Capabilities:" , c ) ;
} ) ;
return ;
}
if ( text . startsWith ( "/upgrade " ) && this . room && this . room . userMayUpgradeRoom ( this . $matrix . currentUserId ) ) {
const version = text . substring ( 9 ) ;
this . $matrix . matrixClient . upgradeRoom ( this . roomId , version ) . then ( ( r ) => {
console . error ( "Upgrade result" , JSON . stringify ( r ) ) ;
} )
return ;
}
2021-03-10 17:24:48 +01:00
if ( text && text . length > 0 ) {
2020-11-25 14:42:50 +01:00
util
2022-05-17 15:16:53 +00:00
. sendTextMessage ( this . $matrix . matrixClient , this . roomId , text , this . editedEvent , this . replyToEvent )
2020-11-25 14:42:50 +01:00
. then ( ( ) => {
console . log ( "Sent message" ) ;
} )
. catch ( ( err ) => {
console . log ( "Failed to send:" , err ) ;
} ) ;
2020-11-09 10:26:56 +01:00
}
} ,
2020-11-17 20:02:42 +01:00
/ * *
2020-12-10 12:37:06 +01:00
* Show attachment picker to select file
* /
2025-06-09 09:44:37 +02:00
showAttachmentPicker ( reset ) {
if ( reset ) {
2025-06-11 18:04:56 +02:00
this . uploadBatch ? . cancel ( ) ;
this . uploadBatch = null ;
2025-06-09 09:44:37 +02:00
}
2021-02-17 11:59:07 +01:00
this . $refs . attachment . click ( ) ;
2020-12-10 12:37:06 +01:00
} ,
2023-05-06 14:03:15 +03:00
/ * *
* Handle picked attachment
* /
handlePickedAttachment ( event ) {
2025-06-09 09:44:37 +02:00
this . addAttachments ( Object . values ( event . target . files ) ) ;
2025-07-16 15:49:26 +02:00
// Reset value
this . $refs . attachmentForm . reset ( ) ;
2025-06-09 09:44:37 +02:00
} ,
2023-08-06 12:03:50 +03:00
2025-06-09 09:44:37 +02:00
addAttachments ( files ) {
2025-07-17 11:22:26 +02:00
if ( ! this . uploadBatch ) {
this . uploadBatch = this . $matrix . attachmentManager . createUpload ( this . room ) ;
}
2025-09-02 09:29:05 +02:00
this . uploadBatch ? . addFiles ( files ) ;
2023-05-06 14:03:15 +03:00
} ,
2020-11-17 20:02:42 +01:00
2021-04-15 11:44:58 +02:00
showStickerPicker ( ) {
2021-05-11 21:03:54 +02:00
this . $refs . stickerPickerSheet . open ( ) ;
2021-04-15 11:44:58 +02:00
} ,
2023-11-06 15:28:26 +00:00
/ * *
* 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 .
* /
2025-06-10 13:35:51 +02:00
onLayoutChange ( event ) {
const { action , element } = event ;
2024-12-03 10:11:26 +01:00
if ( ! element || ! element . parentElement || this . useVoiceMode || this . useFileModeNonAdmin ) {
2023-11-06 15:28:26 +00:00
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 ( ) ;
} ) ;
} ,
2024-09-17 09:43:53 +02:00
handleScrolledToOldest ( ) {
2020-11-11 17:35:14 +01:00
if (
this . timelineWindow &&
2021-04-09 16:40:03 +02:00
this . timelineWindow . canPaginate ( EventTimeline . BACKWARDS ) &&
! this . timelineWindowPaginating
2020-11-11 17:35:14 +01:00
) {
2021-04-09 16:40:03 +02:00
this . timelineWindowPaginating = true ;
2020-11-11 17:35:14 +01:00
this . timelineWindow
. paginate ( EventTimeline . BACKWARDS , 10 , true )
. then ( ( success ) => {
2023-08-30 10:50:45 +02:00
if ( success && this . scrollPosition ) {
2024-09-17 09:43:53 +02:00
this . scrollPosition . prepareFor ( this . reverseOrder ? "down" : "up" ) ;
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2020-11-11 17:35:14 +01:00
this . $nextTick ( ( ) => {
// restore scroll position!
console . log ( "Restore scroll!" ) ;
this . scrollPosition . restore ( ) ;
} ) ;
}
2021-04-09 16:40:03 +02:00
} )
. finally ( ( ) => {
this . timelineWindowPaginating = false ;
2020-11-11 17:35:14 +01:00
} ) ;
}
} ,
2024-09-17 09:43:53 +02:00
handleScrolledToLatest ( smoothScrollToLatest ) {
2020-11-17 20:02:42 +01:00
if (
this . timelineWindow &&
2021-04-09 16:40:03 +02:00
this . timelineWindow . canPaginate ( EventTimeline . FORWARDS ) &&
! this . timelineWindowPaginating
2020-11-17 20:02:42 +01:00
) {
2021-04-09 16:40:03 +02:00
this . timelineWindowPaginating = true ;
2020-11-17 20:02:42 +01:00
this . timelineWindow
. paginate ( EventTimeline . FORWARDS , 10 , true )
. then ( ( success ) => {
if ( success ) {
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2023-08-30 10:50:45 +02:00
if ( ! this . useVoiceMode && this . scrollPosition ) {
2024-09-17 09:43:53 +02:00
this . scrollPosition . prepareFor ( this . reverseOrder ? "up" : "down" ) ;
2023-01-30 08:36:02 +00:00
this . $nextTick ( ( ) => {
// restore scroll position!
console . log ( "Restore scroll!" ) ;
this . scrollPosition . restore ( ) ;
2024-09-17 09:43:53 +02:00
if ( smoothScrollToLatest ) {
this . smoothScrollToLatest ( ) ;
2023-01-30 08:36:02 +00:00
}
} ) ;
}
2020-11-17 20:02:42 +01:00
}
2021-04-09 16:40:03 +02:00
} )
. finally ( ( ) => {
this . timelineWindowPaginating = false ;
2020-11-17 20:02:42 +01:00
} ) ;
}
} ,
2021-01-14 16:17:05 +01:00
/ * *
2021-02-17 11:59:07 +01:00
* Scroll so that the given event is at the middle of the chat view ( if more events ) or else at the bottom .
* /
2021-01-14 16:17:05 +01:00
scrollToEvent ( eventId ) {
2024-09-17 09:43:53 +02:00
console . log ( "Scroll to event" , eventId ) ;
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
2021-01-14 16:17:05 +01:00
const ref = this . $refs [ eventId ] ;
if ( container && ref ) {
2023-11-06 15:28:26 +00:00
const parent = container . getBoundingClientRect ( ) ;
const item = ref [ 0 ] . getBoundingClientRect ( ) ;
let offsetY = ( parent . bottom - parent . top ) / 2 ;
if ( ref [ 0 ] . clientHeight > offsetY ) {
2024-09-17 09:43:53 +02:00
offsetY = this . reverseOrder ? 0 : Math . max ( 0 , ( parent . bottom - parent . top ) - ref [ 0 ] . clientHeight ) ;
2023-11-06 15:28:26 +00:00
}
const targetY = parent . top + offsetY ;
const currentY = item . top ;
const y = container . scrollTop + ( currentY - targetY ) ;
this . $nextTick ( ( ) => {
container . scrollTo ( 0 , y ) ;
} ) ;
2021-01-14 16:17:05 +01:00
}
} ,
2024-09-17 09:43:53 +02:00
smoothScrollToLatest ( ) {
2025-06-19 10:47:16 +02:00
this . $nextTick ( ( ) => {
// TODO - fix this. We need to wait until the "lastChild" below has been
// laid out correctly. If we do it "too early" it will have zero height and
// thus not correctly scrolled into the viewport. So we wait a few ticks...
window . requestAnimationFrame ( ( ) => {
window . requestAnimationFrame ( ( ) => {
window . requestAnimationFrame ( ( ) => {
window . requestAnimationFrame ( ( ) => {
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
if ( container && container . children . length > 0 ) {
2024-09-17 09:43:53 +02:00
if ( this . reverseOrder ) {
const firstChild = container . children [ 0 ] ;
2025-06-19 10:47:16 +02:00
window . requestAnimationFrame ( ( ) => {
firstChild . scrollIntoView ( {
behavior : "smooth" ,
block : "end" ,
inline : "nearest" ,
} ) ;
2024-09-17 09:43:53 +02:00
} ) ;
} else {
2025-06-19 10:47:16 +02:00
const lastChild = container . children [ container . children . length - 1 ] ;
window . requestAnimationFrame ( ( ) => {
lastChild . scrollIntoView ( {
behavior : "smooth" ,
block : "start" ,
inline : "nearest" ,
} ) ;
2020-11-17 20:02:42 +01:00
} ) ;
2025-06-19 10:47:16 +02:00
}
2020-11-17 20:02:42 +01:00
}
} ) ;
2025-06-19 10:47:16 +02:00
} )
} )
} )
} )
2020-11-11 17:35:14 +01:00
} ,
2020-11-25 14:42:50 +01:00
2021-04-09 14:03:40 +02:00
showMoreMessageOperations ( e ) {
this . addReaction ( e ) ;
} ,
2021-05-11 21:03:54 +02:00
2020-11-25 14:42:50 +01:00
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 ;
2021-04-09 14:27:27 +02:00
this . $refs . messageOperationsSheet . open ( ) ;
2020-11-25 14:42:50 +01:00
this . showEmojiPicker = true ;
} ,
2021-04-09 14:03:40 +02:00
addQuickReaction ( e ) {
this . sendQuickReaction ( { reaction : e . emoji , event : e . event } ) ;
} ,
2024-06-08 13:42:57 +03:00
addQuickHeartReaction ( e ) {
this . heartPosition = e . position
this . sendQuickReaction ( { reaction : this . heartEmoji , event : e . event } , true ) ;
2024-05-18 22:13:07 +03:00
} ,
2022-02-11 09:58:36 +00:00
setReplyToImage ( event ) {
util
2025-03-31 16:33:54 +02:00
. getThumbnail ( this . $matrix . matrixClient , this . $matrix . useAuthedMedia , event , this . $config )
2022-05-17 15:16:53 +00:00
. then ( ( url ) => {
this . replyToImg = url ;
} )
. catch ( ( err ) => {
console . log ( "Failed to fetch thumbnail: " , err ) ;
} ) ;
2022-02-11 09:58:36 +00:00
} ,
2020-12-15 17:06:26 +01:00
addReply ( event ) {
this . replyToEvent = event ;
this . $refs . messageInput . focus ( ) ;
2023-11-06 15:28:26 +00:00
if ( event . parentThread || event . isThreadRoot || event . isMxThread ) {
2024-06-28 11:49:55 +02:00
this . replyToContentType = util . threadMessageType ( ) ;
2023-11-06 15:28:26 +00:00
} else {
this . replyToContentType = event . getContent ( ) . msgtype || 'm.poll' ;
}
2022-02-11 09:58:36 +00:00
this . setReplyToImage ( event ) ;
2020-12-15 17:06:26 +01:00
} ,
2020-12-14 16:11:45 +01:00
edit ( event ) {
this . editedEvent = event ;
this . currentInput = event . getContent ( ) . body ;
2020-12-14 16:30:27 +01:00
this . $refs . messageInput . focus ( ) ;
2020-12-14 16:11:45 +01:00
} ,
2021-02-08 15:31:09 +01:00
redact ( event ) {
2024-07-02 11:08:03 +02:00
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 ) => {
2024-07-11 15:52:45 +02:00
return this . $matrix . matrixClient . redactEvent ( c . getRoomId ( ) , c . getId ( ) , undefined , { reason : "redactedMedia" } ) ;
2024-07-02 11:08:03 +02:00
} ) ;
}
2024-07-11 15:52:45 +02:00
promises . push ( this . $matrix . matrixClient . redactEvent ( event . getRoomId ( ) , event . getId ( ) , undefined , { reason : "redactedThread" } ) ) ;
2024-07-02 11:08:03 +02:00
Promise . allSettled ( promises )
2021-02-17 11:59:07 +01:00
. then ( ( ) => {
console . log ( "Message redacted" ) ;
} )
. catch ( ( err ) => {
console . log ( "Redaction failed: " , err ) ;
} ) ;
2021-02-08 15:31:09 +01:00
} ,
2021-01-12 09:25:39 +01:00
download ( event ) {
2023-11-06 15:28:26 +00:00
if ( ( event . isThreadRoot || event . isMxThread ) && this . timelineSet ) {
const children = this . timelineSet . relations . getAllChildEventsForEvent ( event . getId ( ) ) . filter ( e => util . downloadableTypes ( ) . includes ( e . getContent ( ) . msgtype ) ) ;
2025-03-31 16:33:54 +02:00
children . forEach ( child => util . download ( this . $matrix . matrixClient , this . $matrix . useAuthedMedia , child ) ) ;
2023-11-06 15:28:26 +00:00
} else {
2025-03-31 16:33:54 +02:00
util . download ( this . $matrix . matrixClient , this . $matrix . useAuthedMedia , event ) ;
2023-11-06 15:28:26 +00:00
}
2021-01-12 09:25:39 +01:00
} ,
2025-11-03 16:47:41 +01:00
reportEvent ( event ) {
this . reportingEventId = event . getId ( ) ;
} ,
2024-10-11 17:04:32 +02:00
pin ( event ) {
2024-10-17 10:22:24 +02:00
const eventToPin = event . parentThread ? event . parentThread : event ;
this . $matrix . setEventPinned ( this . room , eventToPin , true ) ;
2024-10-11 17:04:32 +02:00
} ,
unpin ( event ) {
2024-10-17 10:22:24 +02:00
const eventToUnpin = event . parentThread ? event . parentThread : event ;
this . $matrix . setEventPinned ( this . room , eventToUnpin , false ) ;
2024-10-11 17:04:32 +02:00
} ,
2020-12-15 17:06:26 +01:00
cancelEditReply ( ) {
2020-12-14 17:12:29 +01:00
this . currentInput = "" ;
this . editedEvent = null ;
2020-12-15 17:06:26 +01:00
this . replyToEvent = null ;
2020-12-14 17:12:29 +01:00
} ,
2020-11-25 14:42:50 +01:00
emojiSelected ( e ) {
2023-01-30 08:36:02 +00:00
if ( this . isEmojiQuickReaction ) {
// When quick emoji picker is clicked
2022-06-11 12:30:50 +03:00
if ( this . selectedEvent ) {
const event = this . selectedEvent ;
this . selectedEvent = null ;
2025-05-12 17:15:11 +02:00
this . sendQuickReaction ( { reaction : e . i , event : event } ) ;
2022-06-11 12:30:50 +03:00
}
} else {
// When text input emoji picker is clicked
2025-05-12 17:15:11 +02:00
this . currentInput = ` ${ this . currentInput } ${ e . i } ` ;
2022-06-11 12:30:50 +03:00
this . $refs . messageInput . focus ( ) ;
}
2025-05-12 17:15:11 +02:00
let emojis = this . recentEmojis ;
const existingIdx = emojis . findIndex ( emoji => emoji . u == e . u ) ;
if ( existingIdx >= 0 ) {
emojis . splice ( existingIdx , 1 ) ;
}
emojis . splice ( 0 , 0 , markRaw ( e ) ) ;
if ( emojis . length > 5 ) {
emojis . splice ( 5 , emojis . length - 5 ) ;
}
this . recentEmojis = emojis ;
2020-11-25 14:42:50 +01:00
this . showEmojiPicker = false ;
2022-03-21 07:50:31 +00:00
this . $refs . messageOperationsSheet . close ( ) ;
2020-11-25 14:42:50 +01:00
} ,
2023-06-07 09:39:05 +02:00
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 ) ;
} ) ;
} ,
2024-05-18 22:13:07 +03:00
showHeartAnimation ( ) {
const self = this ;
this . heartAnimation = true ;
setTimeout ( ( ) => {
self . heartAnimation = false ;
} , 1000 )
} ,
sendQuickReaction ( e , heartAnimationFlag = false ) {
2023-02-08 14:02:08 +00:00
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 {
2020-11-25 14:42:50 +01:00
util
2022-05-17 15:16:53 +00:00
. sendQuickReaction ( this . $matrix . matrixClient , this . roomId , e . reaction , e . event )
2020-12-04 10:44:46 +01:00
. then ( ( ) => {
console . log ( "Quick reaction message" ) ;
} )
. catch ( ( err ) => {
console . log ( "Failed to send quick reaction:" , err ) ;
} ) ;
2024-05-18 22:13:07 +03:00
if ( heartAnimationFlag ) {
this . showHeartAnimation ( ) ;
}
2023-02-08 14:02:08 +00:00
}
2020-12-04 10:44:46 +01:00
} ,
2020-12-14 16:11:45 +01:00
2021-04-15 11:44:58 +02:00
sendSticker ( stickerShortCode ) {
this . sendMessage ( stickerShortCode ) ;
} ,
2021-01-11 17:42:58 +01:00
showContextMenuForEvent ( e ) {
const event = e . event ;
2024-07-19 14:38:06 +02:00
if ( this . selectedEvent == event ) {
2024-11-05 10:24:08 +01:00
this . showContextMenuAnchor = e . anchor ;
2024-07-19 14:38:06 +02:00
this . showContextMenu = ! this . showContextMenu ;
} else {
this . showContextMenu = false ;
this . $nextTick ( ( ) => {
this . selectedEvent = event ;
this . showContextMenuAnchor = e . anchor ;
2024-11-05 10:24:08 +01:00
this . showContextMenu = true ;
2024-07-19 14:38:06 +02:00
} )
}
2020-12-14 16:11:45 +01:00
} ,
2021-05-10 16:11:03 +02:00
showAvatarMenuForEvent ( e ) {
const event = e . event ;
this . selectedEvent = event ;
2024-03-24 11:35:05 +02:00
this . showProfileDialog = true
2021-05-10 16:11:03 +02:00
} ,
2021-03-04 12:17:21 +01:00
viewProfile ( ) {
this . $navigation . push ( { name : "Profile" } , 1 ) ;
} ,
2021-05-10 16:11:03 +02:00
closeContextMenusIfOpen ( e ) {
2020-12-14 16:11:45 +01:00
if ( this . showContextMenu ) {
this . showContextMenu = false ;
2021-05-10 16:11:03 +02:00
this . showContextMenuAnchor = null ;
2024-07-19 14:38:06 +02:00
this . selectedEvent = null ;
2021-05-10 16:11:03 +02:00
e . preventDefault ( ) ;
}
2021-01-14 16:17:05 +01:00
} ,
2021-01-15 11:50:21 +01:00
/** Stop Read Receipt timer */
stopRRTimer ( ) {
2021-01-14 16:17:05 +01:00
if ( this . rrTimer ) {
2021-03-03 12:29:55 +01:00
clearTimeout ( this . rrTimer ) ;
2021-01-14 16:17:05 +01:00
this . rrTimer = null ;
}
2021-01-15 11:50:21 +01:00
} ,
2021-02-17 11:59:07 +01:00
2021-01-15 11:50:21 +01:00
/ * *
2021-02-17 11:59:07 +01:00
* Start / restart the timer to Read Receipts .
* /
2021-01-15 11:50:21 +01:00
restartRRTimer ( ) {
this . stopRRTimer ( ) ;
2023-01-30 08:36:02 +00:00
2023-02-08 11:22:12 +01:00
if ( this . $matrix . currentRoomBeingPurged ) {
return ;
}
2023-06-28 12:14:44 +00:00
if ( ! this . useVoiceMode && ! this . useFileModeNonAdmin ) {
2024-10-25 11:48:54 +02:00
this . rrTimer = setTimeout ( ( ) => { this . rrTimerElapsed ( ) } , READ _RECEIPT _TIMEOUT ) ;
2023-01-30 08:36:02 +00:00
}
2021-01-14 16:17:05 +01:00
} ,
2024-10-25 11:48:54 +02:00
rrTimerElapsed ( ) {
2021-03-03 12:29:55 +01:00
this . rrTimer = null ;
2024-10-25 11:48:54 +02:00
this . sendRR ( ) ;
2023-01-30 08:36:02 +00:00
this . restartRRTimer ( ) ;
} ,
2021-03-03 12:29:55 +01:00
2024-10-25 11:48:54 +02:00
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).
2025-01-07 11:25:53 +01:00
let visibleEventIds = ( util . findVisibleElements ( this . chatContainer ) || [ ] ) . filter ( el => el . hasAttribute ( "eventId" ) ) . map ( el => el . getAttribute ( "eventId" ) ) ;
2024-10-25 11:48:54 +02:00
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 ( ) ) ;
}
2023-01-30 08:36:02 +00:00
}
2024-10-25 11:48:54 +02:00
return false ;
} ) ;
}
2023-01-30 08:36:02 +00:00
2024-10-25 11:48:54 +02:00
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 ( ) ;
} ) ;
2021-01-14 16:17:05 +01:00
}
2021-01-20 11:32:21 +01:00
} ,
2021-03-05 22:34:00 +01:00
showRecordingUI ( ) {
this . showRecorderPTT = false ;
this . showRecorder = true ;
} ,
2021-02-22 16:34:19 +01:00
startRecording ( ) {
2021-03-05 22:34:00 +01:00
this . showRecorderPTT = true ;
2021-02-22 16:34:19 +01:00
this . showRecorder = true ;
} ,
onVoiceRecording ( event ) {
2025-06-11 18:04:56 +02:00
const batch = this . $matrix . attachmentManager . createUpload ( this . room ) ;
2025-09-02 09:29:05 +02:00
batch . addFiles ( [ event . file ] ) ;
2021-03-10 17:24:48 +01:00
var text = undefined ;
if ( this . currentInput && this . currentInput . length > 0 ) {
text = this . currentInput ;
this . currentInput = "" ;
}
2025-06-11 18:04:56 +02:00
batch . send ( text )
. then ( ( ) => {
this . showRecorder = false ;
// Log event
this . $analytics . event ( "Audio" , "Voice message sent" ) ;
} )
. catch ( ( err ) => {
console . error ( "Failed to send voice message" , err ) ;
} )
2021-03-04 12:48:32 +01:00
} ,
2021-04-01 22:59:19 +02:00
2024-04-03 09:35:20 +02:00
closeRoomWelcomeHeader ( ) {
this . hideRoomWelcomeHeader = true ;
2021-04-01 22:59:19 +02:00
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 ( ) ;
} ) ;
2021-04-09 14:03:40 +02:00
} ,
2021-05-11 21:03:54 +02:00
formatBytes ( bytes ) {
return prettyBytes ( bytes ) ;
} ,
2021-07-18 12:17:15 +02:00
onHeaderClick ( ) {
2023-03-03 14:43:53 +00:00
const invitations = this . $matrix . invites . length ;
2021-07-18 12:17:15 +02:00
const joinedRooms = this . $matrix . joinedRooms ;
2023-03-03 14:43:53 +00:00
if ( invitations == 0 && joinedRooms && joinedRooms . length == 1 && joinedRooms [ 0 ] . roomId == this . room . roomId ) {
2021-07-18 12:17:15 +02:00
// Only joined to this room, go directly to room details!
this . $navigation . push ( { name : "RoomInfo" } ) ;
return ;
}
this . $refs . roomInfoSheet . open ( ) ;
} ,
2023-03-03 14:43:53 +00:00
viewRoomDetails ( ) {
this . $navigation . push ( { name : "RoomInfo" } ) ;
2022-05-03 09:40:02 +00:00
} ,
2022-06-08 18:53:50 +00:00
pollWasClosed ( ignoredE ) {
let div = document . createElement ( "div" ) ;
div . classList . add ( "toast" ) ;
div . innerText = this . $t ( "poll_create.results_shared" ) ;
2023-01-30 08:36:02 +00:00
this . chatContainer . parentElement . appendChild ( div ) ;
2022-06-08 18:53:50 +00:00
setTimeout ( ( ) => {
2023-01-30 08:36:02 +00:00
this . chatContainer . parentElement . removeChild ( div ) ;
2022-06-08 18:53:50 +00:00
} , 3000 ) ;
2023-03-16 15:23:26 +01:00
} ,
setShowRecorder ( ) {
2023-04-02 10:39:16 +03:00
if ( this . canRecordAudio ) {
2023-03-16 15:23:26 +01:00
this . showRecorder = true ;
2023-04-02 10:39:16 +03:00
} else {
2023-03-16 15:23:26 +01:00
this . showNoRecordingAvailableDialog = true ;
}
2023-05-26 15:56:59 +00:00
} ,
2023-03-16 15:23:26 +01:00
2023-05-26 15:56:59 +00:00
/ * *
* 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.
2024-10-25 11:48:54 +02:00
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 ) ;
}
}
2023-05-26 15:56:59 +00:00
}
}
}
2025-01-14 11:14:11 +00:00
} ,
2025-10-17 17:06:00 +02:00
resetFileMode ( ) {
this . uploadBatch = this . $matrix . attachmentManager . createUpload ( this . room ) ;
} ,
2025-01-14 11:14:11 +00:00
closeFileMode ( ) {
2025-06-11 18:04:56 +02:00
this . uploadBatch ? . cancel ( ) ;
this . uploadBatch = undefined ;
2025-01-14 11:14:11 +00:00
this . $matrix . leaveRoomAndNavigate ( this . room . roomId )
. catch ( ( err ) => {
console . log ( "Error leaving" , err ) ;
} ) ;
2025-11-08 22:48:59 +02:00
} ,
onDeletePost ( ) {
this . redact ( this . selectedEvent ) ;
this . showDeletePostPopup = false ;
2023-05-26 15:56:59 +00:00
}
2020-11-09 10:26:56 +01:00
} ,
} ;
< / script >
< style lang = "scss" >
2025-05-13 22:38:10 +02:00
@ use "@/assets/css/chat.scss" as * ;
2024-05-18 22:13:07 +03:00
. heart - wrapper {
position : fixed ;
z - index : - 1 ;
. heart {
width : 100 px ;
height : 100 px ;
background : url ( "../assets/heart.png" ) no - repeat ;
background - position : 0 0 ;
cursor : pointer ;
transition : background - position 1 s steps ( 28 ) ;
transition - duration : 0 s ;
visibility : hidden ;
& . is - active {
transition - duration : 1 s ;
background - position : - 2800 px 0 ;
visibility : visible ;
z - index : 10000 ;
}
}
& . is - active {
z - index : 1000 ;
2024-06-08 13:42:57 +03:00
top : var ( -- top ) ;
left : var ( -- left ) ;
2024-05-18 22:13:07 +03:00
}
}
2022-05-17 15:16:53 +00:00
< / style >