Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions src/assets/buttons.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/**
* @copyright Copyright (c) 2020 Marco Ambrosini <[email protected]>
*
* @author Marco Ambrosini <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

@import 'variables.scss';

.nc-button {
width: $clickable-area;
height: $clickable-area;
flex-shrink: 0;
border: 0;
padding: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
&:not(.primary) {
background-color: transparent;
}
&__main {
&:hover,
&:focus {
background-color: var(--color-background-hover);
}
&:disabled {
&:hover {
background-color: var(--color-primary-element);
}
}
}
// Used on top of gray background such as hovered messages
&__main--dark {
&:hover,
&:focus {
background-color: var(--color-background-darker);
}
}

}
66 changes: 63 additions & 3 deletions src/components/MessagesList/MessagesGroup/Message/Message.vue
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,35 @@ the main body of the message as well as a quote.
:style="{'visibility': hasDate ? 'visible' : 'hidden'}"
:class="{'date--self': showSentIcon}">{{ messageTime }}</span>
<!-- Message delivery status indicators -->
<div v-if="isTemporary && !isTemporaryUpload"
<div v-if="sendingFailure"
v-tooltip.auto="sendingErrorIconTooltip"
class="message-status sending-failed"
:class="{'retry-option': sendingErrorCanRetry}"
:aria-label="sendingErrorIconTooltip"
tabindex="0"
@mouseover="showReloadButton = true"
@focus="showReloadButton = true"
@mouseleave="showReloadButton = true"
@blur="showReloadButton = true">
<button
v-if="sendingErrorCanRetry && showReloadButton"
class="nc-button nc-button__main--dark"
@click="handleRetry">
<Reload
decorative
title=""
:size="16" />
</button>
<AlertCircle v-else
decorative
title=""
:size="16" />
</div>
<div v-else-if="isTemporary && !isTemporaryUpload"
v-tooltip.auto="loadingIconTooltip"
class="icon-loading-small message-status"
:aria-label="loadingIconTooltip" />
<div v-if="showCommonReadIcon"
<div v-else-if="showCommonReadIcon"
v-tooltip.auto="commonReadIconTooltip"
class="message-status"
:aria-label="commonReadIconTooltip">
Expand Down Expand Up @@ -111,6 +135,7 @@ import DefaultParameter from './MessagePart/DefaultParameter'
import FilePreview from './MessagePart/FilePreview'
import Mention from './MessagePart/Mention'
import RichText from '@juliushaertl/vue-richtext'
import AlertCircle from 'vue-material-design-icons/AlertCircle'
import Check from 'vue-material-design-icons/Check'
import CheckAll from 'vue-material-design-icons/CheckAll'
import Quote from '../../../Quote'
Expand All @@ -119,6 +144,7 @@ import { EventBus } from '../../../../services/EventBus'
import emojiRegex from 'emoji-regex'
import { PARTICIPANT, CONVERSATION } from '../../../../constants'
import moment from '@nextcloud/moment'
import Reload from 'vue-material-design-icons/Reload'

export default {
name: 'Message',
Expand All @@ -133,8 +159,10 @@ export default {
CallButton,
Quote,
RichText,
AlertCircle,
Check,
CheckAll,
Reload,
},

mixins: [
Expand Down Expand Up @@ -243,13 +271,18 @@ export default {
type: Number,
default: 0,
},
sendingFailure: {
type: String,
default: '',
},
},

data() {
return {
showActions: false,
// Is tall enough for both actions and date upon hovering
isTallEnough: false,
showReloadButton: false,
}
},

Expand Down Expand Up @@ -361,7 +394,12 @@ export default {

// Determines whether the date has to be displayed or not
hasDate() {
return this.isSystemMessage || (!this.isTemporary && !this.showActions) || this.isTallEnough
if (this.isTemporary || this.sendingFailure) {
// Never on temporary or failed messages
return false
}

return this.isSystemMessage || !this.showActions || this.isTallEnough
},

isTemporaryUpload() {
Expand All @@ -380,6 +418,17 @@ export default {
return t('spreed', 'Message read by everyone who shares their reading status')
},

sendingErrorCanRetry() {
return this.sendingFailure === 'timeout' || this.sendingFailure === 'other'
},

sendingErrorIconTooltip() {
if (this.sendingErrorCanRetry) {
return t('spreed', 'Failed to send the message. Click to try again')
}
return t('spreed', 'You can not send messages to this conversation at the moment.')
},

},

watch: {
Expand Down Expand Up @@ -416,6 +465,12 @@ export default {
// again another time
this.$refs.message.classList.remove('highlight-animation')
},
handleRetry() {
if (this.sendingErrorCanRetry) {
EventBus.$emit('retryMessage', this.id)
EventBus.$emit('focusChatInput')
}
},
handleReply() {
this.$store.dispatch('addMessageToBeReplied', {
id: this.id,
Expand All @@ -440,6 +495,7 @@ export default {

<style lang="scss" scoped>
@import '../../../../assets/variables';
@import '../../../../assets/buttons';

.message {
padding: 4px;
Expand Down Expand Up @@ -550,5 +606,9 @@ export default {
display: flex;
justify-content: center;
align-items: center;

&.retry-option {
cursor: pointer;
}
}
</style>
53 changes: 46 additions & 7 deletions src/components/NewMessageForm/NewMessageForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ import { CONVERSATION } from '../../constants'
import createTemporaryMessage from '../../utils/temporaryMessage'
import EmoticonOutline from 'vue-material-design-icons/EmoticonOutline'
import Send from 'vue-material-design-icons/Send'
import CancelableRequest from '../../utils/cancelableRequest'

const picker = getFilePickerBuilder(t('spreed', 'File to share'))
.setMultiSelect(false)
Expand Down Expand Up @@ -203,10 +204,12 @@ export default {

mounted() {
EventBus.$on('uploadStart', this.handleUploadStart)
EventBus.$on('retryMessage', this.handleRetryMessage)
},

beforeDestroy() {
EventBus.$off('uploadStart', this.handleUploadStart)
EventBus.$off('retryMessage', this.handleRetryMessage)
},

methods: {
Expand Down Expand Up @@ -266,7 +269,6 @@ export default {
*/
async handleSubmit() {
if (this.parsedText !== '') {
const oldMessage = this.parsedText
const temporaryMessage = createTemporaryMessage(this.parsedText, this.token)
this.$store.dispatch('addTemporaryMessage', temporaryMessage)
this.text = ''
Expand All @@ -275,9 +277,21 @@ export default {
EventBus.$emit('smoothScrollChatToBottom')
// Also remove the message to be replied for this conversation
this.$store.dispatch('removeMessageToBeReplied', this.token)
let timeout
try {
// Posts the message to the server
const response = await postNewMessage(temporaryMessage)
const { request, cancel } = CancelableRequest(postNewMessage)

timeout = setTimeout(() => {
cancel('canceled')
this.$store.dispatch('markTemporaryMessageAsFailed', {
message: temporaryMessage,
reason: 'timeout',
})
}, 30000)
const response = await request(temporaryMessage)
clearTimeout(timeout)

// If successful, deletes the temporary message from the store
this.$store.dispatch('deleteMessage', temporaryMessage)
// And adds the complete version of the message received
Expand All @@ -287,19 +301,44 @@ export default {
let statusCode = null
console.debug(`error while submitting message ${error}`, error)
if (error.isAxiosError) {
statusCode = error.response.status
statusCode = error?.response?.status
}

if (timeout) {
clearTimeout(timeout)
}

// 403 when room is read-only, 412 when switched to lobby mode
if (statusCode === 403 || statusCode === 412) {
if (statusCode === 403) {
showError(t('spreed', 'No permission to post messages in this conversation'))
this.$store.dispatch('markTemporaryMessageAsFailed', {
message: temporaryMessage,
reason: 'read-only',
})
} else if (statusCode === 412) {
showError(t('spreed', 'No permission to post messages in this conversation'))
this.$store.dispatch('markTemporaryMessageAsFailed', {
message: temporaryMessage,
reason: 'lobby',
})
} else {
showError(t('spreed', 'Could not post message: {errorMessage}', { errorMessage: error.message || error }))
this.$store.dispatch('markTemporaryMessageAsFailed', {
message: temporaryMessage,
reason: 'other',
})
}
}
}
},

// restore message to allow re-sending
handleRetryMessage(temporaryMessageId) {
if (this.parsedText === '') {
const temporaryMessage = this.$store.getters.message(this.token, temporaryMessageId)
if (temporaryMessage) {
this.text = temporaryMessage.message || this.text
this.parsedText = temporaryMessage.message || this.parsedText
this.$store.dispatch('deleteMessage', temporaryMessage)
this.text = oldMessage
this.parsedText = oldMessage
}
}
},
Expand Down
13 changes: 7 additions & 6 deletions src/services/messagesService.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,16 @@ const lookForNewMessages = async({ token, lastKnownMessageId }, options) => {
}

/**
* Posts a new messageto the server.
* Posts a new message to the server.
*
* @param {object} param0 The message object that is destructured
* @param {string} token The conversation token
* @param {string} message The message object
* @param {string} referenceId A reference id to identify the message later again
* @param {Number} parent The id of the message to be replied to
* @param {string} param0.token The conversation token
* @param {string} param0.message The message object
* @param {string} param0.referenceId A reference id to identify the message later again
* @param {Number} param0.parent The id of the message to be replied to
* @param {object} options options
*/
const postNewMessage = async function({ token, message, actorDisplayName, referenceId, parent }) {
const postNewMessage = async function({ token, message, actorDisplayName, referenceId, parent }, options) {
const response = await axios.post(generateOcsUrl('apps/spreed/api/v1/chat', 2) + token, { message, actorDisplayName, referenceId, replyTo: parent })

if ('x-chat-last-common-read' in response.headers) {
Expand Down
24 changes: 24 additions & 0 deletions src/store/messagesStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ const mutations = {
deleteMessage(state, message) {
Vue.delete(state.messages[message.token], message.id)
},

/**
* Adds a temporary message to the store.
* @param {object} state current store state;
Expand All @@ -135,6 +136,18 @@ const mutations = {
Vue.set(state.messages[message.token], message.id, message)
},

/**
* Adds a temporary message to the store.
* @param {object} state current store state;
* @param {object} message the temporary message;
* @param {string} reason the reason the temporary message failed;
*/
markTemporaryMessageAsFailed(state, { message, reason }) {
if (state.messages[message.token][message.id]) {
Vue.set(state.messages[message.token][message.id], 'sendingFailure', reason)
}
},

/**
* @param {object} state current store state;
* @param {string} token Token of the conversation
Expand Down Expand Up @@ -223,6 +236,17 @@ const actions = {
context.dispatch('updateConversationLastActive', message.token)
},

/**
* Mark a temporary message as failed to allow retrying it again
*
* @param {object} context default store context;
* @param {object} message the temporary message;
* @param {string} reason the reason the temporary message failed;
*/
markTemporaryMessageAsFailed(context, { message, reason }) {
context.commit('markTemporaryMessageAsFailed', { message, reason })
},

/**
* @param {object} context default store context;
* @param {string} token Token of the conversation
Expand Down
1 change: 1 addition & 0 deletions src/utils/temporaryMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const createTemporaryMessage = (text, token, uploadId, index, file, localUrl) =>
messageParameters,
token: token,
isReplyable: false,
sendingFailure: '',
referenceId: Hex.stringify(SHA1(tempId)),
})

Expand Down