Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable28] feat(integration): show out-of-office message in 1-1 conversation #11049

Merged
merged 3 commits into from
Nov 30, 2023
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
2 changes: 1 addition & 1 deletion src/components/MessagesList/MessagesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -811,7 +811,7 @@
this.debounceUpdateReadMarkerPosition()
},

/**

Check warning on line 814 in src/components/MessagesList/MessagesList.vue

View workflow job for this annotation

GitHub Actions / NPM lint

JSDoc @return declaration present but return expression not available in function
* Finds the last message that is fully visible in the scroller viewport
*
* Starts searching forward after the given message element until we reach
Expand Down Expand Up @@ -1155,7 +1155,7 @@
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 <antreesy.web@gmail.com>
-
- @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
Loading