Skip to content

Commit

Permalink
Merge pull request #11049 from nextcloud/backport/10902/stable28
Browse files Browse the repository at this point in the history
[stable28] feat(integration): show out-of-office message in 1-1 conversation
  • Loading branch information
nickvergessen authored Nov 30, 2023
2 parents 4b09a62 + d87c509 commit 1ae976f
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/components/MessagesList/MessagesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -1155,7 +1155,7 @@ export default {
position: relative;
flex: 1 0;
padding-top: 20px;
overflow-y: auto;
overflow-y: scroll;
overflow-x: hidden;
border-bottom: 1px solid var(--color-border);
transition: $transition;
Expand Down
59 changes: 51 additions & 8 deletions src/components/NewMessage/NewMessage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@

<!-- Input area -->
<div class="new-message-form__input">
<NewMessageAbsenceInfo v-if="userAbsence"
:user-absence="userAbsence"
:display-name="conversation.displayName" />

<div class="new-message-form__emoji-picker">
<NcEmojiPicker v-if="!disabled"
:container="container"
Expand Down Expand Up @@ -167,6 +171,7 @@ import NcEmojiPicker from '@nextcloud/vue/dist/Components/NcEmojiPicker.js'
import NcRichContenteditable from '@nextcloud/vue/dist/Components/NcRichContenteditable.js'

import Quote from '../Quote.vue'
import NewMessageAbsenceInfo from './NewMessageAbsenceInfo.vue'
import NewMessageAttachments from './NewMessageAttachments.vue'
import NewMessageAudioRecorder from './NewMessageAudioRecorder.vue'
import NewMessageNewFileDialog from './NewMessageNewFileDialog.vue'
Expand All @@ -177,6 +182,7 @@ import { CONVERSATION, PARTICIPANT, PRIVACY } from '../../constants.js'
import { EventBus } from '../../services/EventBus.js'
import { shareFile } from '../../services/filesSharingServices.js'
import { searchPossibleMentions } from '../../services/mentionsService.js'
import { useChatExtrasStore } from '../../stores/chatExtras.js'
import { useSettingsStore } from '../../stores/settings.js'
import { fetchClipboardContent } from '../../utils/clipboard.js'
import { isDarkTheme } from '../../utils/isDarkTheme.js'
Expand All @@ -201,6 +207,7 @@ export default {
NcButton,
NcEmojiPicker,
NcRichContenteditable,
NewMessageAbsenceInfo,
NewMessageAttachments,
NewMessageAudioRecorder,
NewMessageNewFileDialog,
Expand Down Expand Up @@ -261,9 +268,11 @@ export default {
expose: ['focusInput'],

setup() {
const chatExtrasStore = useChatExtrasStore()
const settingsStore = useSettingsStore()

return {
chatExtrasStore,
settingsStore,
supportTypingStatus,
}
Expand Down Expand Up @@ -297,6 +306,10 @@ export default {
return this.conversation.readOnly === CONVERSATION.STATE.READ_ONLY
},

isOneToOneConversation() {
return this.conversation.type === CONVERSATION.TYPE.ONE_TO_ONE
},

noChatPermission() {
return (this.conversation.permissions & PARTICIPANT.PERMISSIONS.CHAT) === 0
},
Expand Down Expand Up @@ -373,10 +386,15 @@ export default {
showAudioRecorder() {
return !this.hasText && this.canUploadFiles && !this.broadcast && !this.upload
},

showTypingStatus() {
return this.hasTypingIndicator && this.supportTypingStatus
&& this.settingsStore.typingStatusPrivacy === PRIVACY.PUBLIC
},

userAbsence() {
return this.chatExtrasStore.absence[this.token]
},
},

watch: {
Expand All @@ -388,13 +406,18 @@ export default {
this.$store.dispatch('setCurrentMessageInput', { token: this.token, text: newValue })
},

token(token) {
if (token) {
this.text = this.$store.getters.currentMessageInput(token)
} else {
this.text = ''
token: {
immediate: true,
handler(token) {
if (token) {
this.text = this.$store.getters.currentMessageInput(token)
} else {
this.text = ''
}
this.clearTypingInterval()

this.checkAbsenceStatus()
}
this.clearTypingInterval()
},
},

Expand Down Expand Up @@ -797,6 +820,26 @@ export default {
isMobile() {
return /Android|iPhone|iPad|iPod/i.test(navigator.userAgent)
},

async checkAbsenceStatus() {
if (!this.isOneToOneConversation) {
return
}

// TODO replace with status message id 'vacationing'
if (this.conversation.status === 'dnd') {
// Fetch actual absence status from server
await this.chatExtrasStore.getUserAbsence({
token: this.token,
userId: this.conversation.name,
})
} else {
// Remove stored absence status
this.chatExtrasStore.resetUserAbsence({
token: this.token,
})
}
}
},
}
</script>
Expand All @@ -817,7 +860,7 @@ export default {

<style lang="scss" scoped>
.wrapper {
padding: 12px 0;
padding: 12px 12px 12px 0;
min-height: 69px;
}

Expand Down Expand Up @@ -855,7 +898,7 @@ export default {
}

&__quote {
margin: 0 16px 12px 24px;
margin: 0 16px 12px;
background-color: var(--color-background-hover);
padding: 8px;
border-radius: var(--border-radius-large);
Expand Down
160 changes: 160 additions & 0 deletions src/components/NewMessage/NewMessageAbsenceInfo.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
<!--
- @copyright Copyright (c) 2023 Maksim Sukharev <antreesy.web@gmail.com>
-
- @author Maksim Sukharev <[email protected]>
-
- @license AGPL-3.0-or-later
-
- 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/>.
-->

<!-- eslint-disable vue/singleline-html-element-content-newline -->
<template>
<NcNoteCard type="info" class="absence-reminder">
<div class="absence-reminder__content">
<AvatarWrapper :id="userAbsence.userId"
:name="displayName"
:size="AVATAR.SIZE.EXTRA_SMALL"
source="users"
disable-menu
disable-tooltip />
<h4 class="absence-reminder__caption">{{ userAbsenceCaption }}</h4>
<NcButton v-if="userAbsenceMessage"
class="absence-reminder__button"
type="tertiary"
@click="toggleCollapsed">
<template #icon>
<ChevronDown class="icon" :class="{'icon--reverted': !collapsed}" :size="20" />
</template>
</NcButton>
</div>
<p class="absence-reminder__message" :class="{'absence-reminder__message--collapsed': collapsed}">{{ userAbsenceMessage }}</p>
</NcNoteCard>
</template>

<script>
import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
import NcNoteCard from '@nextcloud/vue/dist/Components/NcNoteCard.js'

import AvatarWrapper from '../AvatarWrapper/AvatarWrapper.vue'

import { AVATAR } from '../../constants.js'

export default {
name: 'NewMessageAbsenceInfo',

components: {
AvatarWrapper,
ChevronDown,
NcButton,
NcNoteCard,
},

props: {
userAbsence: {
type: Object,
required: true,
},

displayName: {
type: String,
required: true,
},
},

setup() {
return { AVATAR }
},

data() {
return {
collapsed: true,
}
},

computed: {
userAbsenceCaption() {
return t('spreed', '{user} is out of office and might not respond.', { user: this.displayName })
},

userAbsenceMessage() {
return this.userAbsence.message || this.userAbsence.status
},
},

methods: {
toggleCollapsed() {
this.collapsed = !this.collapsed
},
},
}
</script>

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

.absence-reminder {
margin: 0 16px 12px;
padding: 10px 10px 10px 6px;
border-radius: var(--border-radius-large);

// FIXME upstream: allow to hide or replace NoteCard default icon
& :deep(.notecard__icon) {
display: none;
}

& > :deep(div) {
width: 100%;
}

&__content {
display: flex;
align-items: center;
gap: 4px;
width: 100%;
}

&__caption {
font-weight: bold;
}

&__message {
padding-left: 26px;
white-space: pre-line;
word-wrap: break-word;

&--collapsed {
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
}

&__button {
margin-left: auto;

& .icon {
transition: $transition;

&--reverted {
transform: rotate(180deg);
}
}
}
}
</style>
1 change: 0 additions & 1 deletion src/components/TopBar/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,6 @@ export default {

<style lang="scss" scoped>
.top-bar {
right: 12px; /* needed so we can still use the scrollbar */
display: flex;
z-index: 10;
justify-content: flex-end;
Expand Down
10 changes: 10 additions & 0 deletions src/services/participantsService.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ const setTyping = (typing) => {
signalingSetTyping(typing)
}

/**
* Get absence information for a user (in a given 1-1 conversation).
*
* @param {string} userId user id
*/
const getUserAbsence = async (userId) => {
return axios.get(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}', { userId }))
}

export {
joinConversation,
rejoinConversation,
Expand All @@ -261,4 +270,5 @@ export {
setPermissions,
setSessionState,
setTyping,
getUserAbsence,
}
Loading

0 comments on commit 1ae976f

Please sign in to comment.