2020-11-09 10:26:56 +01:00
< template >
2023-10-18 15:05:50 +00:00
< div class = "chat-root fill-height d-flex flex-column" :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"
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"
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()"
2023-01-30 08:36:02 +00:00
v - on : loadnext = "handleScrolledToBottom(false)"
v - on : loadprevious = "handleScrolledToTop()"
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
2023-07-17 15:00:40 +03:00
< FileDropLayout class = "file-drop-root" v-if = "useFileModeNonAdmin" :room="room"
2023-07-06 11:18:26 +02:00
v - on : pick - file = "showAttachmentPicker()"
v - on : add - file = "addAttachment($event)"
2023-07-17 15:00:40 +03:00
v - on : remove - file = "currentFileInputs.splice($event, 1)"
2023-06-28 12:14:44 +00:00
v - on : reset = "resetAttachments"
: attachments = "currentFileInputs"
/ >
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" >
2023-01-30 08:36:02 +00:00
< message-operations ref = "messageOperations" :style = "opStyle" :emojis = "recentEmojis" v -on : close = "
showContextMenu = false ;
showContextMenuAnchor = null ;
" v-if=" showMessageOperations " v-on:addreaction=" addReaction " v-on:addquickreaction=" addQuickReaction "
v - on : addreply = "addReply(selectedEvent)" v - on : edit = "edit(selectedEvent)" v - on : redact = "redact(selectedEvent)"
v - on : download = "download(selectedEvent)" v - on : more = "
2022-06-11 12:30:50 +03:00
isEmojiQuickReaction = true
2023-11-06 15:28:26 +00:00
showMoreMessageOperations ( { event : selectedEvent , anchor : $event . anchor } )
2024-02-13 12:13:34 +01:00
" :originalEvent=" selectedEvent " :timelineSet=" timelineSet "
: readOnlyRoom = "$matrix.currentRoomIsReadOnlyForUser"
/ >
2021-02-17 11:59:07 +01:00
< / div >
2020-12-14 16:11:45 +01:00
2021-05-10 16:11:03 +02:00
< div ref = "avatarOperationsStrut" class = "avatar-operations-strut" >
2023-01-30 08:36:02 +00:00
< avatar-operations ref = "avatarOperations" :style = "avatarOpStyle" v -on : close = "
showAvatarMenu = false ;
showAvatarMenuAnchor = null ;
" v-on:start-private-chat=" startPrivateChat ( $event ) " v-if=" selectedEvent && showAvatarMenu " :room=" room "
: originalEvent = "selectedEvent" / >
2021-05-10 16:11:03 +02:00
< / div >
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" / >
< CreatedRoomWelcomeHeader v-if = "showCreatedRoomWelcomeHeader" v-on:close="closeCreateRoomWelcomeHeader" / >
2023-08-30 10:50:45 +02:00
< DirectChatWelcomeHeader v-if = "showDirectChatWelcomeHeader" v-on:close="closeDirectChatWelcomeHeader" / >
2022-05-17 15:16:53 +00:00
2023-04-27 07:09:20 +00:00
< div v-for = "(event, index) in filteredEvents" :key="event.getId()" :eventId="event.getId()" >
2021-02-17 11:59:07 +01:00
<!-- DAY Marker , shown for every new day in the timeline -- >
2023-10-18 15:05:50 +00:00
< div v-if = "showDayMarkerBeforeEvent(event) && !!componentForEvent(event, isForExport = false)" 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
2022-11-30 08:16:23 +00:00
< div v-if = "!event.isRelation() && !event.isRedaction()" :ref="event.getId()" >
2023-01-30 08:36:02 +00:00
< div class = "message-wrapper" v -on : touchstart = "
( e ) => {
touchStart ( e , event ) ;
}
" v-on:touchend=" touchEnd " v-on:touchcancel=" touchCancel " v-on:touchmove=" touchMove " >
2023-10-25 15:08:23 +02:00
<!-- Note : For threaded media messages , IF there is only one item we show that media item as a single component .
We might therefore get calls to v - on : context - menu that has the event set to that single media item , not the top level thread event
that is really displayed in the flow . Therefore , we rewrite these events with "{event: event, anchor: $event.anchor}" ,
see below . Otherwise things like context menus won ' t work as designed .
-- >
2023-05-11 10:37:06 +02:00
< component :is = "componentForEvent(event)" :room = "room" :originalEvent = "event" : nextEvent = "filteredEvents[index + 1]"
2023-02-08 14:02:08 +00:00
: timelineSet = "timelineSet" v - on : send - quick - reaction . stop = "sendQuickReaction"
2023-10-25 10:44:25 +00:00
: componentFn = "componentForEvent"
2023-12-07 20:53:24 +02:00
v - on : context - menu = "showContextMenuForEvent({event: event, anchor: $event.anchor})"
2023-10-25 15:08:23 +02:00
v - on : own - avatar - clicked = "viewProfile"
2023-12-07 20:53:24 +02:00
v - on : other - avatar - clicked = "showAvatarMenuForEvent({event: event, anchor: $event.anchor})"
2023-10-25 15:08:23 +02:00
v - on : download = "download(event)"
2023-04-16 13:45:35 +03:00
v - on : poll - closed = "pollWasClosed(event)"
v - on : more = "
isEmojiQuickReaction = true
2023-10-25 15:08:23 +02:00
showMoreMessageOperations ( { event : event , anchor : $event . anchor } )
2023-04-16 13:45:35 +03:00
"
2023-11-06 15:28:26 +00:00
v - on : layout - change = "onLayoutChange"
2023-04-16 13:45:35 +03:00
/ >
2021-04-13 19:30:35 +02:00
<!-- < div v-if = "debugging" style="user-select:text" > EventID : {{ event.getId ( ) }} < / div > - - >
2021-04-13 21:55:25 +02:00
<!-- < div v-if = "debugging" style="user-select:text" > Event : {{ JSON.stringify ( event ) }} < / div > - - >
2023-10-18 15:05:50 +00:00
< div v-if = "event.getId() == readMarker && index < filteredEvents.length - 1" class="read-marker"><div class="line"></div><div class="text">{{ $t('message.unread_messages') }}</div><div class="line" > < / div > < / div >
2020-11-25 14:42:50 +01:00
< / div >
2020-12-03 10:00:23 +01:00
< / div >
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
< / div >
<!-- Input area -- >
2023-06-28 12:14:44 +00:00
< v-container v-if = "!useVoiceMode && !useFileModeNonAdmin && room" fluid :class="['input-area-outer', replyToEvent ? 'reply-to' : '']" >
2022-05-17 15:16:53 +00:00
< div : class = "[replyToEvent ? 'iput-area-inner-box' : '']" >
2022-02-11 09:58:36 +00:00
<!-- "Scroll to end" - button -- >
2023-02-17 22:00:47 +01:00
< v-btn v-if = "!useVoiceMode" class="scroll-to-end" v-show="showScrollToEnd" fab x-small elevation="0" color="black"
2023-01-30 08:36:02 +00:00
@ click . stop = "scrollToEndOfTimeline" >
2022-02-11 09:58:36 +00:00
< v-icon color = "white" > arrow _downward < / v-icon >
< / v-btn >
< v-row class = "ma-0 pa-0" >
< div v-if = "replyToEvent" class="row" >
< div class = "col" >
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" >
{ { replyToEvent . getContent ( ) . body | latestReply } }
< / div >
2023-11-06 15:28:26 +00:00
< div v-if = "replyToContentType === 'm.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" / >
2022-05-17 15:16:53 +00:00
< v-img v-if = "replyToContentType === 'm.audio'" src="@/assets/icons/audio_message.svg" / >
< v-img v-if = "replyToContentType === 'm.video'" src="@/assets/icons/video_message.svg" / >
2022-11-30 08:25:32 +00:00
< v-icon v-if = "replyToContentType === 'm.poll'" 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" >
2022-05-17 15:16:53 +00:00
< v-btn fab x -small elevation = "0" color = "black" @click.stop ="cancelEditReply" >
2022-03-06 14:28:16 +02:00
< v-icon color = "white" > cancel < / v-icon >
< / 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 >
2023-10-25 20:59:56 +00:00
< v-row class = "input-area-inner align-center" v-show = "!showRecorder" v-if="!$matrix.currentRoomIsReadOnlyForUser" >
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"
no - resize class = "input-area-text" : placeholder = "$t('message.your_message')" hide - details
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" >
< v-btn fab small elevation = "0" color = "black" @click.stop ="cancelEditReply" >
2022-02-11 09:58:36 +00:00
< v-icon color = "white" > cancel < / v-icon >
< / 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" >
2022-06-08 18:53:50 +00:00
< v-btn icon large color = "black" @ click = "showCreatePollDialog = true" >
2023-01-05 09:35:47 +00:00
< v-icon dark > $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"
v - if = "!currentInput || currentInput.length == 0 || showRecorder" >
< v-btn v-if = "canRecordAudio" class="mic-button" ref="mic_button" fab small elevation="0" v -blur
v - longTap : 250 = "[showRecordingUI, startRecording]" >
2022-02-11 09:58:36 +00:00
< v-icon : color = "showRecorder ? 'white' : 'black'" > mic < / v-icon >
< / v-btn >
2023-01-30 08:36:02 +00:00
< v-btn v -else class = "mic-button" ref = "mic_button" fab small elevation = "0" v -blur
@ click . stop = "showNoRecordingAvailableDialog = true" >
2022-02-11 09:58:36 +00:00
< v-icon : color = "showRecorder ? 'white' : 'black'" > mic < / v-icon >
< / v-btn >
< / v-col >
2021-02-23 22:07:57 +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-else >
2023-01-30 08:36:02 +00:00
< v-btn fab small elevation = "0" color = "black" @click.stop ="sendCurrentTextMessage"
: disabled = "sendButtonDisabled" >
2022-05-17 15:16:53 +00:00
< v-icon color = "white" > { { editedEvent ? "save" : "arrow_upward" } } < / v-icon >
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" >
< v-btn fab small elevation = "0" v -blur @ click.stop = "
isEmojiQuickReaction = false
showMoreMessageOperations ( $event )
" >
2022-06-11 12:30:50 +03:00
< v-icon > $vuetify . icons . addReaction < / v-icon >
< / 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" >
2023-04-09 15:31:04 +03:00
< v-btn id = "btn-attach" icon large color = "black" @click ="showStickerPicker"
2023-01-30 08:36:02 +00:00
: disabled = "attachButtonDisabled" >
2022-02-11 09:58:36 +00:00
< v-icon large > face < / v-icon >
2021-02-23 22:07:57 +01:00
< / v-btn >
2022-02-11 09:58:36 +00:00
< / v-col >
< v-col class = "input-area-button text-center flex-grow-0 flex-shrink-1" >
< label icon flat ref = "attachmentLabel" >
2023-04-09 15:31:04 +03:00
< v-btn icon large color = "black" @click ="showAttachmentPicker"
2023-01-30 08:36:02 +00:00
: disabled = "attachButtonDisabled" >
2022-02-11 09:58:36 +00:00
< v-icon x -large > add _circle _outline < / v-icon >
< / 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 >
2023-03-16 15:23:26 +01:00
< div v-if = "!useVoiceMode && room && $matrix.currentRoomIsReadOnlyForUser" 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
2023-01-30 08:36:02 +00:00
< input ref = "attachment" type = "file" name = "attachment" @change ="handlePickedAttachment($event)"
2023-12-04 11:29:23 +01:00
accept = "image/*,audio/*,video/*,.pdf,application/pdf,.apk,application/vnd.android.package-archive,.ipa,.zip,application/zip,application/x-zip-compressed,multipart/x-zip" class = "d-none" multiple / >
2023-01-30 08:36:02 +00:00
2023-06-28 12:14:44 +00:00
< div v-if = "currentFileInputsDialog && !useFileModeNonAdmin" >
2023-05-28 20:37:52 +03:00
< v-dialog v-model = "currentFileInputsDialog" class="ma-0 pa-0" :width="$vuetify.breakpoint.smAndUp ? '50%' : '85%'" persistent scrollable >
2020-11-17 20:02:42 +01:00
< v-card class = "ma-0 pa-0" >
2023-08-12 11:54:43 +03:00
< v-card-text v-if = "!currentFileInputs.length" >
{ { this . $t ( "message.preparing_to_upload" ) } }
< v-progress-linear
indeterminate
class = "mb-0"
> < / v-progress-linear >
< / v-card-text >
< template v-else >
< v-card-title >
< div v-if = "currentSendErrorExceededFile" class="red--text" > {{ currentSendErrorExceededFile }} < / div >
< span v-else > {{ $ t ( ' message.send_attachements_dialog_title ' ) }} < / span >
< / v-card-title >
< v-divider > < / v-divider >
< template v-if = "imageFiles && imageFiles.length" >
< v-card-title v-if = "imageFiles.length > 1" > {{ $ t ( ' message.images ' ) }} < / v -card -title >
< v-card-text : class = "{'ma-0 pa-2' : true, 'd-flex flex-wrap justify-center': imageFiles.length > 1}" >
< div : class = "{'col-4': imageFiles.length > 1}" v-for ="(currentImageInput, id) in imageFiles" :key ="id" >
< div style = "position: relative" >
< v-img v-if = "currentImageInput && currentImageInput.image" :aspect-ratio="1" :src="currentImageInput.image"
contain class = "current-image-input-path" / >
< v-progress-linear : style = "{ position: 'absolute', left: '0', right: '0', bottom: '0', opacity: currentImageInput.sendInfo ? '1' : '0' }" : value = "currentImageInput.sendInfo ? currentImageInput.sendInfo.progress : 0" > < / v-progress-linear >
< / div >
< div >
< span v-if = "currentImageInput && currentImageInput.scaled && currentImageInput.useScaled" >
{ { currentImageInput . scaledDimensions . width } } x { { currentImageInput . scaledDimensions . height } } < / span >
< span v -else -if = " currentImageInput & & currentImageInput.dimensions " >
{ { currentImageInput . dimensions . width } } x { { currentImageInput . dimensions . height } }
< / span >
< span v-if = "currentImageInput && currentImageInput.scaled && currentImageInput.useScaled" >
( { { formatBytes ( currentImageInput . scaledSize ) } } )
< / span >
< span v-else >
( { { formatBytes ( currentImageInput . actualSize ) } } )
< / span >
< v-switch v-if = "currentImageInput && currentImageInput.scaled" :label="$t('message.scale_image')"
v - model = "currentImageInput.useScaled" : disabled = "currentImageInput.sendInfo" / >
< / div >
2023-05-06 14:03:15 +03:00
< / div >
2023-08-12 11:54:43 +03:00
< / v-card-text >
< / template >
< template v-if = "Array.isArray(currentFileInputs) && currentFileInputs.length" >
< v-card-title v-if = "nonImageFiles.length > 1" > {{ $ t ( ' message.files ' ) }} < / v -card -title >
< v-card-text >
< div v-for ="(currentImageInputPath, id) in currentFileInputs" :key ="id" >
< div v-if = "!currentImageInputPath.type.includes('image/')" >
< span > { { $t ( 'message.file' ) } } : { { currentImageInputPath . name } } < / span >
< span > ( { { formatBytes ( currentImageInputPath . size ) } } ) < / span >
< v-progress-linear : style = "{ opacity: currentImageInputPath.sendInfo ? '1' : '0' }" : value = "currentImageInputPath.sendInfo ? currentImageInputPath.sendInfo.progress : 0" > < / v-progress-linear >
< / div >
2023-05-28 20:37:52 +03:00
< / div >
2023-08-12 11:54:43 +03:00
< / v-card-text >
< / template >
< v-divider > < / v-divider >
< v-card-actions >
< v-spacer >
< div v-if = "currentSendError" > {{ currentSendError }} < / div >
< / v-spacer >
< v-btn color = "primary" text @click ="cancelSendAttachment" id = "btn-attachment-cancel" : disabled = "sendingStatus != sendStatuses.SENDING && sendingStatus != sendStatuses.INITIAL" >
{ { $t ( "menu.cancel" ) } }
< / v-btn >
< v-btn id = "btn-attachment-send" color = "primary" text @click ="sendAttachment(undefined)"
v - if = "currentSendShowSendButton" : disabled = "sendingStatus != sendStatuses.INITIAL" > { { $t ( "menu.send" ) } } < / v-btn >
< / v-card-actions >
< / template >
2020-11-17 20:02:42 +01:00
< / v-card >
< / v-dialog >
< / div >
2020-11-25 14:42:50 +01:00
2022-05-17 15:16:53 +00:00
< MessageOperationsBottomSheet ref = "messageOperationsSheet" >
2023-05-20 11:31:28 +03:00
< VEmojiPicker ref = "emojiPicker" @select ="emojiSelected" :i18n = "i18nEmoji" / >
2021-04-09 14:27:27 +02:00
< / MessageOperationsBottomSheet >
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 -- >
2022-05-17 15:16:53 +00: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 >
2022-05-17 15:16:53 +00:00
< v-btn id = "btn-ok" color = "primary" 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
2022-05-17 15:16:53 +00:00
< CreatePollDialog :show = "showCreatePollDialog" @ close = "showCreatePollDialog = false" / >
2023-07-17 15:00:40 +03:00
2020-11-09 10:26:56 +01:00
< / div >
< / template >
< script >
2021-05-11 21:03:54 +02:00
import Vue from "vue" ;
2023-05-11 10:11:34 +02:00
import { TimelineWindow , EventTimeline } from "matrix-js-sdk" ;
2023-06-28 12:14:44 +00:00
import util , { ROOM _TYPE _VOICE _MODE , ROOM _TYPE _FILE _MODE } from "../plugins/utils" ;
2020-11-25 14:42:50 +01:00
import MessageOperations from "./messages/MessageOperations.vue" ;
2021-05-10 16:11:03 +02:00
import AvatarOperations from "./messages/AvatarOperations.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" ;
2021-04-01 22:59:19 +02:00
import CreatedRoomWelcomeHeader from "./CreatedRoomWelcomeHeader" ;
2023-08-30 10:50:45 +02:00
import DirectChatWelcomeHeader from "./DirectChatWelcomeHeader" ;
2023-04-27 07:09:20 +00:00
import NoHistoryRoomWelcomeHeader from "./NoHistoryRoomWelcomeHeader.vue" ;
2021-05-11 21:03:54 +02:00
import MessageOperationsBottomSheet from "./MessageOperationsBottomSheet" ;
import StickerPickerBottomSheet from "./StickerPickerBottomSheet" ;
import BottomSheet from "./BottomSheet.vue" ;
import ImageResize from "image-resize" ;
2022-05-03 09:40:02 +00:00
import CreatePollDialog from "./CreatePollDialog.vue" ;
2022-05-23 15:19:55 +00:00
import chatMixin from "./chatMixin" ;
2023-08-09 15:24:50 +00:00
import sendAttachmentsMixin from "./sendAttachmentsMixin" ;
2023-01-30 08:36:02 +00:00
import AudioLayout from "./AudioLayout.vue" ;
2023-06-28 12:14:44 +00:00
import FileDropLayout from "./file_mode/FileDropLayout" ;
2023-08-07 14:13:35 +00:00
import roomTypeMixin from "./roomTypeMixin" ;
2023-08-30 10:50:45 +02:00
import roomMembersMixin from "./roomMembersMixin" ;
2022-05-03 09:40:02 +00:00
2021-05-11 21:03:54 +02:00
const sizeOf = require ( "image-size" ) ;
const dataUriToBuffer = require ( "data-uri-to-buffer" ) ;
const prettyBytes = require ( "pretty-bytes" ) ;
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" ,
2023-10-01 14:31:00 +03:00
mixins : [ chatMixin , roomTypeMixin , sendAttachmentsMixin , 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 ,
2021-04-09 14:27:27 +02:00
CreatedRoomWelcomeHeader ,
2023-08-30 10:50:45 +02:00
DirectChatWelcomeHeader ,
2023-04-27 07:09:20 +00:00
NoHistoryRoomWelcomeHeader ,
2021-04-15 11:44:58 +02:00
MessageOperationsBottomSheet ,
StickerPickerBottomSheet ,
BottomSheet ,
2021-05-10 16:11:03 +02:00
AvatarOperations ,
2022-05-17 15:16:53 +00:00
CreatePollDialog ,
2023-06-28 12:14:44 +00:00
AudioLayout ,
FileDropLayout
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 {
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 ,
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 ,
2023-05-28 20:37:52 +03:00
currentFileInputs : null ,
2021-03-10 17:24:48 +01:00
currentSendShowSendButton : true ,
2020-11-19 22:48:08 +01:00
currentSendError : null ,
2023-08-06 12:03:50 +03:00
currentSendErrorExceededFile : null ,
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-05-10 16:11:03 +02:00
showAvatarMenu : false ,
showAvatarMenuAnchor : 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 */
2023-08-30 10:50:45 +02:00
hideCreatedRoomWelcomeHeader : false ,
/** For direct chats, show a small welcome header with info about the other party */
hideDirectChatWelcomeHeader : 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 ,
i18nEmoji : {
search : this . $t ( "emoji.search" ) ,
categories : {
Activity : this . $t ( "emoji.categories.activity" ) ,
Flags : this . $t ( "emoji.categories.flags" ) ,
Foods : this . $t ( "emoji.categories.foods" ) ,
Frequently : this . $t ( "emoji.categories.frequently" ) ,
Objects : this . $t ( "emoji.categories.objects" ) ,
Nature : this . $t ( "emoji.categories.nature" ) ,
Peoples : this . $t ( "emoji.categories.peoples" ) ,
Symbols : this . $t ( "emoji.categories.symbols" ) ,
Places : this . $t ( "emoji.categories.places" )
}
2023-07-17 15:00:40 +03:00
} ,
2024-02-06 10:22:35 +00:00
/ * *
* A timer to handle message retention / auto deletion
* /
retentionTimer : null
2020-11-19 22:48:08 +01:00
} ;
} ,
2020-11-09 10:26:56 +01:00
2022-04-10 10:32:25 +03:00
filters : {
latestReply ( contents ) {
2022-05-17 15:16:53 +00:00
const contentArr = contents . split ( "\n" ) . reverse ( ) ;
if ( contentArr [ 0 ] === "" ) {
2022-04-10 10:32:25 +03:00
contentArr . shift ( ) ;
}
2023-11-06 15:28:26 +00:00
return ( contentArr && contentArr . length > 0 ) ? contentArr [ 0 ] . replace ( /^> (<.*> )?/g , "" ) : "" ;
2022-05-17 15:16:53 +00:00
} ,
2022-04-10 10:32:25 +03:00
} ,
2020-11-11 17:35:14 +01:00
mounted ( ) {
2023-05-26 15:56:59 +00:00
this . $root . $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
} ,
2021-01-15 11:50:21 +01:00
beforeDestroy ( ) {
2023-05-26 15:56:59 +00:00
this . $root . $off ( 'audio-playback-ended' , this . audioPlaybackEnded ) ;
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 ;
}
2021-01-15 11:50:21 +01:00
} ,
2020-11-11 17:35:14 +01:00
destroyed ( ) {
this . $matrix . off ( "Room.timeline" , this . onEvent ) ;
this . $matrix . off ( "RoomMember.typing" , this . onUserTyping ) ;
} ,
2020-11-09 10:26:56 +01:00
computed : {
2023-05-28 20:37:52 +03:00
nonImageFiles ( ) {
2023-08-06 12:03:50 +03:00
return this . isCurrentFileInputsAnArray && this . currentFileInputs . filter ( file => ! file ? . type . includes ( "image/" ) )
2023-05-06 14:03:15 +03:00
} ,
2023-06-28 12:14:44 +00:00
imageFiles ( ) {
2023-08-06 12:03:50 +03:00
return this . isCurrentFileInputsAnArray && this . currentFileInputs . filter ( file => file ? . type . includes ( "image/" ) )
2023-06-28 12:14:44 +00:00
} ,
2023-05-28 20:37:52 +03:00
isCurrentFileInputsAnArray ( ) {
return Array . isArray ( this . currentFileInputs )
} ,
currentFileInputsDialog : {
2023-05-06 14:03:15 +03:00
get ( ) {
2023-05-28 20:37:52 +03:00
return this . isCurrentFileInputsAnArray
2023-05-06 14:03:15 +03:00
} ,
set ( ) {
2023-05-28 20:37:52 +03:00
this . currentFileInputs = null
2023-05-06 14:03:15 +03:00
}
} ,
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-05-10 16:11:03 +02:00
avatarOpStyle ( ) {
// Calculate where to show the context menu.
//
const ref = this . selectedEvent && this . $refs [ this . selectedEvent . getId ( ) ] ;
var top = 0 ;
2023-05-23 10:20:42 +02:00
var left = "unset" ;
var right = "unset" ;
2021-05-10 16:11:03 +02:00
if ( ref && ref [ 0 ] ) {
if ( this . showAvatarMenuAnchor ) {
var rectAnchor = this . showAvatarMenuAnchor . getBoundingClientRect ( ) ;
2022-05-17 15:16:53 +00:00
var rectChat = this . $refs . avatarOperationsStrut . getBoundingClientRect ( ) ;
2021-05-10 16:11:03 +02:00
top = rectAnchor . top - rectChat . top ;
2023-05-23 10:20:42 +02:00
if ( this . $vuetify . rtl ) {
right = ( rectAnchor . right - rectChat . right ) + "px" ;
} else {
left = ( rectAnchor . left - rectChat . left ) + "px" ;
}
2021-05-10 16:11:03 +02:00
// if (left + 250 > rectChat.right) {
// left = rectChat.right - 250; // Pretty ugly, but we want to make sure it does not escape the screen, and we don't have the exakt width of it (yet)!
// }
}
}
2023-05-23 10:20:42 +02:00
return "top:" + top + "px;left:" + left + ";right:" + right ;
2021-05-10 16:11:03 +02:00
} ,
2021-03-18 11:58:46 +01:00
canRecordAudio ( ) {
return util . browserCanRecordAudio ( ) ;
2021-03-26 12:16:13 +01:00
} ,
2021-04-13 19:30:35 +02:00
debugging ( ) {
2023-02-08 14:02:08 +00:00
return false ; //(window.location.host || "").startsWith("localhost");
2021-05-11 21:03:54 +02: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 ( ) {
if ( ! this . $config . experimental _file _mode ) return false ;
2023-08-07 14:13:35 +00:00
return ( util . roomDisplayTypeOverride ( this . room ) || 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 ( ) {
if ( this . room && this . $matrix . matrixClient . isRoomEncrypted ( this . room . roomId ) ) {
if ( this . room . getHistoryVisibility ( ) == "joined" ) {
// For encrypted rooms where history is set to "joined" we can't read old events.
// We might, however, have old status events from room creation etc.
// We filter out anything that happened before our own join event.
for ( let idx = this . events . length - 1 ; idx >= 0 ; idx -- ) {
const e = this . events [ idx ] ;
if ( e . getType ( ) == "m.room.member" &&
e . getContent ( ) . membership == "join" &&
( ! e . getPrevContent ( ) || e . getPrevContent ( ) . membership != "join" ) &&
e . getStateKey ( ) == this . $matrix . currentUserId ) {
// Our own join event.
return this . events . slice ( idx + 1 ) ;
}
}
}
}
return this . events ;
2023-08-30 10:50:45 +02:00
} ,
roomCreatedByUsRecently ( ) {
const createEvent = this . room && this . room . currentState . getStateEvents ( "m.room.create" , "" ) ;
if ( createEvent ) {
const creatorId = createEvent . getContent ( ) . creator ;
return ( creatorId == this . $matrix . currentUserId && createEvent . getLocalAge ( ) < 5 * 60000 /* 5 minutes */ ) ;
}
return false ;
} ,
isDirectRoom ( ) {
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
} ,
showCreatedRoomWelcomeHeader ( ) {
return ! this . hideCreatedRoomWelcomeHeader && this . roomCreatedByUsRecently && ! this . isDirectRoom ;
} ,
showDirectChatWelcomeHeader ( ) {
return ! this . hideDirectChatWelcomeHeader && this . roomCreatedByUsRecently && this . isDirectRoom ;
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 "" ;
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 ;
2023-08-30 10:50:45 +02:00
this . hideDirectChatWelcomeHeader = false ;
this . hideCreatedRoomWelcomeHeader = 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 ) {
this . onRoomJoined ( this . readMarker ) ;
} else {
this . waitingForRoomObject = true ;
return ; // no room, wait for it (we know we are joined so need to wait for sync to complete)
}
}
} ) ;
} else {
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 ;
this . onRoomJoined ( this . readMarker ) ;
}
} ,
2021-07-19 10:33:25 +02:00
showMessageOperations ( ) {
if ( this . showMessageOperations ) {
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 ( ) ;
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
}
}
}
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
/ * *
* Set initialLoadDone to 'true' . First process all events , setting threadParent and replyEvent if needed .
* /
setInitialLoadDone ( ) {
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 ) ) ;
this . initialLoadDone = true ;
console . log ( "Loading finished!" ) ;
} ,
2024-02-06 10:22:35 +00:00
/ * *
* Set events to display . At the same time , filter out messages that are past rentention period etc .
* /
setEvents ( events ) {
this . events = this . removeTimedOutEvents ( events ) ;
} ,
updateRetentionTimer ( maybeEvent ) {
const retentionEvent = maybeEvent || ( this . room && this . room . currentState . getStateEvents ( "m.room.retention" , "" ) ) ;
if ( retentionEvent ) {
const maxLifetime = parseInt ( retentionEvent . getContent ( ) . max _lifetime ) ;
if ( maxLifetime ) {
if ( ! this . retentionTimer ) {
this . retentionTimer = setInterval ( this . onRetentionTimer , 60000 ) ;
}
return ;
}
}
if ( this . retentionTimer ) {
clearInterval ( this . retentionTimer ) ;
this . retentionTimer = null ;
}
} ,
removeTimedOutEvents ( events ) {
const retentionEvent = this . room && this . room . currentState . getStateEvents ( "m.room.retention" , "" ) ;
let maxLifetime = 0 ;
if ( retentionEvent ) {
maxLifetime = parseInt ( retentionEvent . getContent ( ) . max _lifetime ) ;
}
return events . filter ( ( e ) => {
if ( maxLifetime > 0 && ! e . isState ( ) ) { // Keep all state events
return e . getLocalAge ( ) < maxLifetime ;
}
return true ;
} ) ;
} ,
onRetentionTimer ( ) {
const events = this . removeTimedOutEvents ( this . events ) ;
if ( events . length != this . events . length ) {
this . events = events ; // Changed
}
} ,
2021-03-01 21:47:22 +01:00
onRoomJoined ( initialEventId ) {
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 ( ) ;
if ( initialEventId && ! this . showCreatedRoomWelcomeHeader ) {
const event = this . room . findEventById ( initialEventId ) ;
this . $nextTick ( ( ) => {
if ( event && event . parentThread ) {
self . scrollToEvent ( event . parentThread . getId ( ) ) ;
} else {
self . scrollToEvent ( initialEventId ) ;
}
} ) ;
2023-08-30 10:50:45 +02:00
} else if ( this . showCreatedRoomWelcomeHeader || this . showDirectChatWelcomeHeader ) {
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 ) ;
if ( err . errcode == "M_UNKNOWN" && initialEventId ) {
// 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" ,
params : { roomId : util . sanitizeRoomId ( this . roomAliasOrId ) } ,
} ,
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!
this . smoothScrollToEnd ( ) ;
}
} ,
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 ( ) {
2021-04-09 14:03:40 +02:00
this . updateRecentEmojis ( ) ;
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 ) {
2020-11-17 20:02:42 +01:00
this . handleScrolledToTop ( ) ;
}
} ) ;
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
this . handleScrolledToTop ( ) ;
2022-05-17 15:16:53 +00:00
} else if ( container . scrollHeight - container . scrollTop . toFixed ( 0 ) - container . clientHeight <= bufferHeight ) {
2020-11-17 20:02:42 +01:00
this . handleScrolledToBottom ( false ) ;
2020-11-11 17:35:14 +01:00
}
2022-05-23 14:24:43 +00:00
2022-06-08 18:53:50 +00:00
this . showScrollToEnd =
container . scrollHeight === container . clientHeight
? false
: container . scrollHeight - container . scrollTop . toFixed ( 0 ) > container . clientHeight ||
2023-01-30 08:36:02 +00:00
( this . timelineWindow && this . timelineWindow . canPaginate ( EventTimeline . FORWARDS ) ) ;
2021-02-17 11:59:07 +01:00
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 ) {
Vue . set ( parentEvent , "isMxThread" , true ) ;
Vue . set ( event , "parentThread" , parentEvent ) ;
} else {
// Try to load from server.
this . $matrix . matrixClient . getEventTimeline ( this . timelineSet , event . threadRootId ) . then ( ( tl ) => {
if ( tl ) {
const parentEvent = tl . getEvents ( ) . find ( ( e ) => e . getId ( ) === event . threadRootId ) ;
if ( parentEvent ) {
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2023-11-06 15:28:26 +00:00
const fn = ( ) => {
Vue . set ( parentEvent , "isMxThread" , true ) ;
Vue . set ( event , "parentThread" , parentEvent ) ;
} ;
if ( this . initialLoadDone ) {
const sel = "[eventId=\"" + parentEvent . getId ( ) + "\"]" ;
const element = document . querySelector ( sel ) ;
if ( element ) {
this . onLayoutChange ( fn , element ) ;
} else {
fn ( ) ;
2023-12-07 20:53:24 +02:00
}
2023-11-06 15:28:26 +00:00
} else {
fn ( ) ;
}
}
}
} ) ;
}
} ,
setReplyToEvent ( event ) {
const parentEvent = this . timelineSet . findEventById ( event . replyEventId ) || this . room . findEventById ( event . replyEventId ) ;
if ( parentEvent ) {
Vue . set ( event , "replyEvent" , parentEvent ) ;
} else {
// Try to load from server.
this . $matrix . matrixClient . getEventTimeline ( this . timelineSet , event . replyEventId )
. then ( ( tl ) => {
if ( tl ) {
const parentEvent = tl . getEvents ( ) . find ( ( e ) => e . getId ( ) === event . replyEventId ) ;
if ( parentEvent ) {
2024-02-06 10:22:35 +00:00
this . setEvents ( this . timelineWindow . getEvents ( ) ) ;
2023-11-06 15:28:26 +00:00
const fn = ( ) => { Vue . set ( event , "replyEvent" , parentEvent ) ; } ;
if ( this . initialLoadDone ) {
const sel = "[eventId=\"" + parentEvent . getId ( ) + "\"]" ;
const element = document . querySelector ( sel ) ;
if ( element ) {
this . onLayoutChange ( fn , element ) ;
} else {
fn ( ) ;
}
} else {
fn ( ) ;
}
}
}
} ) . catch ( e => console . error ( e ) ) ;
}
} ,
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-02-06 10:22:35 +00: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...
var scrollToSeeNew = event . getSender ( ) == this . $matrix . currentUserId ; // When we sent, scroll
const container = this . chatContainer ;
2023-06-28 12:14:44 +00:00
if ( container ) {
if ( container . scrollHeight - container . scrollTop . toFixed ( 0 ) == container . clientHeight ) {
scrollToSeeNew = true ;
}
2023-01-30 08:36:02 +00:00
}
2020-12-03 22:12:50 +01:00
this . handleScrolledToBottom ( scrollToSeeNew ) ;
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 ) {
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
* /
showAttachmentPicker ( ) {
2021-02-17 11:59:07 +01:00
this . $refs . attachment . click ( ) ;
2020-12-10 12:37:06 +01:00
} ,
2023-08-06 12:03:50 +03:00
optimizeImage ( evt , file ) {
let fileObj = { }
fileObj . image = evt . target . result ;
fileObj . dimensions = null ;
fileObj . type = file . type ;
fileObj . actualSize = file . size ;
2023-08-07 08:48:34 +03:00
fileObj . actualFile = file
2023-05-06 14:03:15 +03:00
try {
2023-08-06 12:03:50 +03:00
fileObj . dimensions = sizeOf ( dataUriToBuffer ( evt . target . result ) ) ;
2023-05-06 14:03:15 +03:00
// Need to resize?
2023-08-06 12:03:50 +03:00
const w = fileObj . dimensions . width ;
const h = fileObj . dimensions . height ;
2023-05-06 14:03:15 +03:00
if ( w > 640 || h > 640 ) {
var aspect = w / h ;
var newWidth = parseInt ( ( w > h ? 640 : 640 * aspect ) . toFixed ( ) ) ;
var newHeight = parseInt ( ( w > h ? 640 / aspect : 640 ) . toFixed ( ) ) ;
var imageResize = new ImageResize ( {
format : "png" ,
width : newWidth ,
height : newHeight ,
outputType : "blob" ,
} ) ;
imageResize
2023-08-06 12:03:50 +03:00
. play ( evt . target . result )
2023-05-06 14:03:15 +03:00
. then ( ( img ) => {
Vue . set (
2023-08-06 12:03:50 +03:00
fileObj ,
2023-05-06 14:03:15 +03:00
"scaled" ,
new File ( [ img ] , file . name , {
type : img . type ,
lastModified : Date . now ( ) ,
} )
) ;
2023-08-06 12:03:50 +03:00
Vue . set ( fileObj , "useScaled" , true ) ;
Vue . set ( fileObj , "scaledSize" , img . size ) ;
Vue . set ( fileObj , "scaledDimensions" , {
2023-05-06 14:03:15 +03:00
width : newWidth ,
height : newHeight ,
} ) ;
} )
. catch ( ( err ) => {
console . error ( "Resize failed:" , err ) ;
} ) ;
}
} catch ( error ) {
console . error ( "Failed to get image dimensions: " + error ) ;
}
2023-08-06 12:03:50 +03:00
return fileObj
2023-05-06 14:03:15 +03:00
} ,
2023-08-06 12:03:50 +03:00
handleFileReader ( file ) {
2023-05-06 14:03:15 +03:00
if ( file ) {
2023-08-06 12:03:50 +03:00
let optimizedFileObj ;
2020-11-17 20:02:42 +01:00
var reader = new FileReader ( ) ;
2023-08-06 12:03:50 +03:00
reader . onload = ( evt ) => {
2021-05-11 21:03:54 +02:00
if ( file . type . startsWith ( "image/" ) ) {
2023-08-06 12:03:50 +03:00
optimizedFileObj = this . optimizeImage ( evt , file )
} else {
optimizedFileObj = file
2021-05-11 21:03:54 +02:00
}
2023-08-06 12:03:50 +03:00
this . currentFileInputs = Array . isArray ( this . currentFileInputs ) ? [ ... this . currentFileInputs , optimizedFileObj ] : [ optimizedFileObj ] ;
2020-11-17 20:02:42 +01:00
} ;
2023-05-06 14:03:15 +03:00
reader . readAsDataURL ( file ) ;
2020-11-17 20:02:42 +01:00
}
} ,
2023-05-06 14:03:15 +03:00
/ * *
* Handle picked attachment
* /
handlePickedAttachment ( event ) {
2023-08-12 11:54:43 +03:00
this . currentFileInputs = [ ]
2023-08-06 12:03:50 +03:00
const uploadedFiles = Object . values ( event . target . files ) ;
this . $matrix . matrixClient . getMediaConfig ( ) . then ( ( config ) => {
const configUploadSize = config [ "m.upload.size" ] ;
const configFormattedUploadSize = this . formatBytes ( configUploadSize ) ;
uploadedFiles . every ( file => {
if ( configUploadSize && file . size > configUploadSize ) {
2023-08-12 11:54:43 +03:00
this . currentSendError = this . $t ( "message.upload_file_too_large" ) ;
this . currentSendErrorExceededFile = this . $t ( "message.upload_exceeded_file_limit" , { configFormattedUploadSize } ) ;
2023-08-06 12:03:50 +03:00
this . currentSendShowSendButton = false ;
return false ;
} else {
this . currentSendShowSendButton = true ;
}
return true ;
} ) ;
uploadedFiles . forEach ( file => this . handleFileReader ( file ) ) ;
} ) ;
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
} ,
2021-03-10 17:24:48 +01:00
sendAttachment ( withText ) {
2021-05-11 21:03:54 +02:00
this . $refs . attachment . value = null ;
2023-05-28 20:37:52 +03:00
if ( this . isCurrentFileInputsAnArray ) {
2023-08-09 15:24:50 +00:00
const text = withText || "" ;
const promise = this . sendAttachments ( text , this . currentFileInputs ) ;
promise . then ( ( ) => {
2023-08-07 08:48:34 +03:00
this . currentFileInputs = null ;
2023-08-09 15:24:50 +00:00
this . sendingStatus = this . sendStatuses . INITIAL ;
2023-08-07 08:48:34 +03:00
} )
. catch ( ( err ) => {
if ( err . name === "AbortError" || err === "Abort" ) {
this . currentSendError = null ;
this . currentSendErrorExceededFile = null ;
} else {
this . currentSendError = err . LocaleString ( ) ;
this . currentSendErrorExceededFile = err . LocaleString ( ) ;
}
} ) ;
2020-11-17 20:02:42 +01:00
}
} ,
2020-11-21 14:57:43 +01:00
cancelSendAttachment ( ) {
2021-05-11 21:03:54 +02:00
this . $refs . attachment . value = null ;
2023-08-09 15:24:50 +00:00
this . cancelSendAttachments ( ) ;
2023-05-28 20:37:52 +03:00
this . currentFileInputs = null ;
2020-12-03 10:00:23 +01:00
this . currentSendError = null ;
2023-08-06 12:03:50 +03:00
this . currentSendErrorExceededFile = null ;
2023-08-09 15:24:50 +00:00
this . sendingStatus = this . sendStatuses . INITIAL ;
2020-11-09 10:26:56 +01:00
} ,
2023-07-06 11:18:26 +02:00
addAttachment ( file ) {
this . handleFileReader ( null , file ) ;
} ,
2023-06-28 12:14:44 +00:00
resetAttachments ( ) {
this . cancelSendAttachment ( ) ;
} ,
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 .
* /
onLayoutChange ( action , element ) {
if ( ! element || ! element . parentElemen || this . useVoiceMode || this . useFileModeNonAdmin ) {
action ( ) ;
return
}
const container = this . chatContainer ;
this . scrollPosition . prepareFor ( element . parentElement . offsetTop >= container . scrollTop ? "down" : "up" ) ;
action ( ) ;
this . $nextTick ( ( ) => {
// restore scroll position!
this . scrollPosition . restore ( ) ;
} ) ;
} ,
2020-11-11 17:35:14 +01:00
handleScrolledToTop ( ) {
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 ) {
2020-11-11 17:35:14 +01:00
this . scrollPosition . prepareFor ( "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
} ) ;
}
} ,
2020-11-17 20:02:42 +01:00
handleScrolledToBottom ( scrollToEnd ) {
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 ) {
2023-01-30 08:36:02 +00:00
this . scrollPosition . prepareFor ( "down" ) ;
this . $nextTick ( ( ) => {
// restore scroll position!
console . log ( "Restore scroll!" ) ;
this . scrollPosition . restore ( ) ;
if ( scrollToEnd ) {
this . smoothScrollToEnd ( ) ;
}
} ) ;
}
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 ) {
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 ) {
offsetY = Math . max ( 0 , ( parent . bottom - parent . top ) - ref [ 0 ] . clientHeight ) ;
}
const targetY = parent . top + offsetY ;
const currentY = item . top ;
const y = container . scrollTop + ( currentY - targetY ) ;
this . $nextTick ( ( ) => {
container . scrollTo ( 0 , y ) ;
} ) ;
2021-01-14 16:17:05 +01:00
}
} ,
2020-11-17 20:02:42 +01:00
smoothScrollToEnd ( ) {
2023-01-30 08:36:02 +00:00
this . $nextTick ( function ( ) {
const container = this . chatContainer ;
if ( container && container . children . length > 0 ) {
2020-11-17 20:02:42 +01:00
const lastChild = container . children [ container . children . length - 1 ] ;
console . log ( "Scroll into view" , lastChild ) ;
window . requestAnimationFrame ( ( ) => {
lastChild . scrollIntoView ( {
behavior : "smooth" ,
block : "start" ,
inline : "nearest" ,
} ) ;
} ) ;
}
} ) ;
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 } ) ;
} ,
2022-02-11 09:58:36 +00:00
setReplyToImage ( event ) {
util
2023-12-04 15:28:03 +01:00
. getThumbnail ( this . $matrix . matrixClient , 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 ) {
this . replyToContentType = 'm.thread' ;
} 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 ) {
2021-02-17 11:59:07 +01:00
this . $matrix . matrixClient
. redactEvent ( event . getRoomId ( ) , event . getId ( ) )
. 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 ) ) ;
children . forEach ( child => util . download ( this . $matrix . matrixClient , child ) ) ;
} else {
util . download ( this . $matrix . matrixClient , event ) ;
}
2021-01-12 09:25:39 +01: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 ;
this . sendQuickReaction ( { reaction : e . data , event : event } ) ;
}
} else {
// When text input emoji picker is clicked
2023-01-07 11:04:07 +02:00
this . currentInput = ` ${ this . currentInput } ${ e . data } ` ;
2022-06-11 12:30:50 +03:00
this . $refs . messageInput . focus ( ) ;
}
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 ) ;
} ) ;
} ,
2020-11-25 14:42:50 +01:00
sendQuickReaction ( e ) {
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 ) ;
} ) ;
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 ;
2020-12-14 16:11:45 +01:00
this . selectedEvent = event ;
2021-04-09 14:03:40 +02:00
this . updateRecentEmojis ( ) ;
2022-04-25 08:43:27 +00:00
this . showContextMenu = ! this . showContextMenu ;
2021-01-11 17:42:58 +01:00
this . showContextMenuAnchor = e . anchor ;
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 ;
this . showAvatarMenu = true ;
this . showAvatarMenuAnchor = e . anchor ;
} ,
2021-03-04 12:17:21 +01:00
viewProfile ( ) {
this . $navigation . push ( { name : "Profile" } , 1 ) ;
} ,
2021-05-10 16:11:03 +02:00
startPrivateChat ( e ) {
2022-04-13 09:02:51 +00:00
this . loading = true ;
2021-05-11 21:03:54 +02:00
this . $matrix
. getOrCreatePrivateChat ( e . event . getSender ( ) )
. then ( ( room ) => {
2021-05-10 16:11:03 +02:00
this . $nextTick ( ( ) => {
this . $navigation . push (
{
name : "Chat" ,
2021-05-11 21:03:54 +02:00
params : {
2022-05-17 15:16:53 +00:00
roomId : util . sanitizeRoomId ( room . getCanonicalAlias ( ) || room . roomId ) ,
2021-05-11 21:03:54 +02:00
} ,
2021-05-10 16:11:03 +02:00
} ,
- 1
) ;
} ) ;
} )
2021-05-11 21:03:54 +02:00
. catch ( ( err ) => {
2021-05-10 16:11:03 +02:00
console . error ( err ) ;
2022-04-13 09:02:51 +00:00
} )
. finally ( ( ) => {
this . loading = false ;
2021-05-11 21:03:54 +02:00
} ) ;
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 ;
e . preventDefault ( ) ;
}
if ( this . showAvatarMenu ) {
this . showAvatarMenu = false ;
this . showAvatarMenuAnchor = null ;
2020-12-14 16:11:45 +01: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-01-30 08:36:02 +00:00
let eventIdFirst = null ;
let eventIdLast = null ;
2023-06-28 12:14:44 +00:00
if ( ! this . useVoiceMode && ! this . useFileModeNonAdmin ) {
2023-01-30 08:36:02 +00:00
const container = this . chatContainer ;
2023-02-28 10:29:53 +00:00
const elFirst = util . getFirstVisibleElement ( container , ( item ) => item . hasAttribute ( "eventId" ) ) ;
const elLast = util . getLastVisibleElement ( container , ( item ) => item . hasAttribute ( "eventId" ) ) ;
2023-01-30 08:36:02 +00:00
if ( elFirst && elLast ) {
eventIdFirst = elFirst . getAttribute ( "eventId" ) ;
eventIdLast = elLast . getAttribute ( "eventId" ) ;
}
}
if ( eventIdFirst && eventIdLast ) {
this . rrTimer = setTimeout ( ( ) => { this . rrTimerElapsed ( eventIdFirst , eventIdLast ) } , READ _RECEIPT _TIMEOUT ) ;
}
2021-01-14 16:17:05 +01:00
} ,
2023-01-30 08:36:02 +00:00
rrTimerElapsed ( eventIdFirst , eventIdLast ) {
2021-03-03 12:29:55 +01:00
this . rrTimer = null ;
2023-01-30 08:36:02 +00:00
this . sendRR ( eventIdFirst , eventIdLast ) ;
this . restartRRTimer ( ) ;
} ,
2021-03-03 12:29:55 +01:00
2023-01-30 08:36:02 +00:00
sendRR ( eventIdFirst , eventIdLast ) {
console . log ( "SEND RR" , eventIdFirst , eventIdLast ) ;
if ( eventIdLast && this . room ) {
var event = this . room . findEventById ( eventIdLast ) ;
const index = this . events . indexOf ( event ) ;
2021-03-03 12:29:55 +01:00
2023-01-30 08:36:02 +00:00
// Walk backwards through visible events to the first one that is incoming
//
var lastTimestamp = 0 ;
if ( this . lastRR ) {
lastTimestamp = this . lastRR . getTs ( ) ;
}
2021-03-03 12:29:55 +01:00
2023-01-30 08:36:02 +00:00
for ( var i = index ; i >= 0 ; i -- ) {
event = this . events [ i ] ;
if ( event == this . lastRR || event . getTs ( ) <= lastTimestamp ) {
// Already sent this or too old...
break ;
}
// Make sure it's not a local echo event...
if ( ! event . getId ( ) . startsWith ( "~" ) ) {
// Send read receipt
this . $matrix . matrixClient
. sendReadReceipt ( event )
. then ( ( ) => {
this . $matrix . matrixClient . setRoomReadMarkers ( this . room . roomId , event . getId ( ) ) ;
} )
. then ( ( ) => {
console . log ( "RR sent for event: " + event . getId ( ) ) ;
this . lastRR = event ;
} )
. catch ( ( err ) => {
console . log ( "Failed to update read marker: " , err ) ;
} )
. finally ( ( ) => {
this . restartRRTimer ( ) ;
} ) ;
return ; // Bail out here
}
// Stop iterating at first visible
if ( event . getId ( ) == eventIdFirst ) {
break ;
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 ) {
2021-03-10 17:24:48 +01:00
this . currentSendShowSendButton = false ;
2023-05-28 20:37:52 +03:00
this . currentFileInputs = Array . isArray ( this . currentFileInputs ) ? [ ... this . currentFileInputs , event . file ] : [ 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 = "" ;
}
this . sendAttachment ( text ) ;
this . showRecorder = false ;
2021-04-15 17:06:11 +02:00
2022-06-30 08:38:58 +00:00
// Log event
this . $analytics . event ( "Audio" , "Voice message sent" ) ;
2021-03-04 12:48:32 +01:00
} ,
2021-04-01 22:59:19 +02:00
closeCreateRoomWelcomeHeader ( ) {
2023-11-15 16:00:59 +01:00
this . hideCreatedRoomWelcomeHeader = true ;
2023-08-30 10:50:45 +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 ( ) ;
} ) ;
} ,
closeDirectChatWelcomeHeader ( ) {
this . hideDirectChatWelcomeHeader = 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
} ,
updateRecentEmojis ( ) {
if ( this . $refs . emojiPicker ) {
this . recentEmojis = this . $refs . emojiPicker . mapEmojis [ "Frequently" ] ;
2021-06-29 14:25:57 +02:00
if ( this . recentEmojis . length < 20 ) {
let peoples = this . $refs . emojiPicker . mapEmojis [ "Peoples" ] ;
for ( var p of peoples ) {
this . recentEmojis . push ( p ) ;
}
2021-06-27 13:42:45 +05:30
}
2021-04-09 14:03:40 +02:00
return ;
}
this . recentEmojis = [ ] ;
2021-05-11 21:03:54 +02:00
} ,
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.
const filteredEvents = this . filteredEvents ;
const index = filteredEvents . findIndex ( e => e . getId ( ) === matrixEventId ) ;
if ( index >= 0 && index < ( filteredEvents . length - 1 ) ) {
const nextEvent = filteredEvents [ index + 1 ] ;
if ( nextEvent . getContent ( ) . msgtype === "m.audio" ) {
// Yes, audio event!
2023-06-07 09:39:05 +02:00
this . $audioPlayer . play ( nextEvent , this . timelineSet ) ;
2023-05-26 15:56:59 +00:00
}
}
}
}
2020-11-09 10:26:56 +01:00
} ,
} ;
< / script >
< style lang = "scss" >
@ import "@/assets/css/chat.scss" ;
2022-05-17 15:16:53 +00:00
< / style >