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
9 changes: 9 additions & 0 deletions src/__mocks__/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ export const mockedCapabilities: Capabilities = {
'schedule-meeting',
'edit-draft-poll',
'conversation-creation-all',
'important-conversations',
'unbind-conversation',
'sip-direct-dialin',
'dashboard-event-rooms',
'mutual-calendar-events',
'upcoming-reminders',
// Conditional features
'message-expiration',
Expand All @@ -115,6 +119,11 @@ export const mockedCapabilities: Capabilities = {
'chat-summary-api',
'call-notification-state-api',
'schedule-meeting',
'conversation-creation-all',
'important-conversations',
'sip-direct-dialin',
'dashboard-event-rooms',
'mutual-calendar-events',
'upcoming-reminders',
],
config: {
Expand Down
70 changes: 9 additions & 61 deletions src/components/CalendarEventsDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile'
import usernameToColor from '@nextcloud/vue/functions/usernameToColor'

import SelectableParticipant from './BreakoutRoomsEditor/SelectableParticipant.vue'
import CalendarEventSmall from './UIShared/CalendarEventSmall.vue'
import ContactSelectionBubble from './UIShared/ContactSelectionBubble.vue'
import SearchBox from './UIShared/SearchBox.vue'
import TransitionWrapper from './UIShared/TransitionWrapper.vue'
Expand Down Expand Up @@ -339,24 +340,13 @@ async function submitNewMeeting() {
<template #default>
<template v-if="!loading && upcomingEvents.length">
<ul class="calendar-events__list">
<!-- Upcoming event -->
<li v-for="event in upcomingEvents" :key="event.uri">
<a class="calendar-events__item"
:class="{ 'calendar-events__item--thumb': !event.href }"
:href="event.href"
:title="t('spreed', 'Open Calendar')"
:tabindex="0"
target="_blank">
<span class="calendar-badge" :style="{ backgroundColor: event.color }" />
<span class="calendar-events__content">
<span class="calendar-events__header">
<span class="calendar-events__header-text">{{ event.summary }}</span>
<IconReload v-if="event.recurrenceId" :size="13" />
</span>
<span>{{ event.start }}</span>
</span>
</a>
</li>
<CalendarEventSmall v-for="event in upcomingEvents"
:key="event.uri"
:name="event.summary"
:start="event.start"
:href="event.href"
:color="event.color"
:is-recurring="!!event.recurrenceId" />
</ul>
</template>
<NcEmptyContent v-else class="calendar-events__empty-content">
Expand Down Expand Up @@ -523,6 +513,7 @@ async function submitNewMeeting() {
margin: calc(var(--default-grid-baseline) / 2);
line-height: 20px;
max-height: calc(4.5 * var(--item-height) + 4 * var(--default-grid-baseline));
max-width: 200px;
overflow-y: auto;

& > * {
Expand All @@ -534,49 +525,6 @@ async function submitNewMeeting() {
}
}

&__item {
display: flex;
flex-direction: row;
align-items: center;
margin-block: var(--default-grid-baseline);
padding-inline: var(--default-grid-baseline);
height: 100%;
border-radius: var(--border-radius);

&--thumb {
cursor: default;
}

&:hover {
background-color: var(--color-background-hover);
}
}

&__content {
display: flex;
flex-direction: column;
justify-content: center;
}

&__header {
display: flex;
gap: var(--default-grid-baseline);
max-width: 150px;
font-weight: 500;

&-text {
display: inline-block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

:deep(.material-design-icon) {
margin-top: 2px;
}
}

&__empty-content {
min-width: 150px;
margin-top: calc(var(--default-grid-baseline) * 3);
Expand Down
74 changes: 72 additions & 2 deletions src/components/RightSidebar/RightSidebarContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,32 @@ import { generateUrl } from '@nextcloud/router'

import NcActionLink from '@nextcloud/vue/components/NcActionLink'
import NcActions from '@nextcloud/vue/components/NcActions'
import NcAppNavigationCaption from '@nextcloud/vue/components/NcAppNavigationCaption'
import NcAppSidebarHeader from '@nextcloud/vue/components/NcAppSidebarHeader'
import NcButton from '@nextcloud/vue/components/NcButton'
import { useIsDarkTheme } from '@nextcloud/vue/composables/useIsDarkTheme'

import CalendarEventSmall from '../UIShared/CalendarEventSmall.vue'

import { useStore } from '../../composables/useStore.js'
import { CONVERSATION } from '../../constants.ts'
import { getConversationAvatarOcsUrl } from '../../services/avatarService.ts'
import { hasTalkFeature } from '../../services/CapabilitiesManager.ts'
import { useGroupwareStore } from '../../stores/groupware.ts'
import type {
Conversation,
DashboardEvent,
UserProfileData,
} from '../../types/index.ts'

import { convertToUnix } from '../../utils/formattedTime.ts'

type MutualEvent = {
uri: DashboardEvent['eventLink'],
name: DashboardEvent['eventName'],
start: string,
href: DashboardEvent['eventLink'],
color: string,
}
const supportsAvatar = hasTalkFeature('local', 'avatar')

const props = defineProps<{
Expand Down Expand Up @@ -118,9 +130,32 @@ const profileInformation = computed(() => {
return fields
})

const mutualEventsInformation = computed<MutualEvent[]>(() => {
if (!groupwareStore.mutualEvents[token.value]) {
return []
}

const now = convertToUnix(Date.now())
return groupwareStore.mutualEvents[token.value].map(event => {
const start = event.start
? (event.start <= now) ? t('spreed', 'Now') : moment(event.start * 1000).calendar()
: ''
return {
uri: event.eventLink,
name: event.eventName,
start,
href: event.eventLink,
color: event.calendars[0]?.calendarColor ?? 'var(--color-primary)',
}
})
})

watch(token, async () => {
profileLoading.value = true
await groupwareStore.getUserProfileInformation(conversation.value)
await Promise.all([
groupwareStore.getUserProfileInformation(conversation.value),
groupwareStore.getUserMutualEvents(conversation.value),
])
profileLoading.value = false
}, { immediate: true })

Expand Down Expand Up @@ -184,6 +219,18 @@ function joinFields(firstSubstring?: string | null, secondSubstring?: string | n
</div>
</div>
</div>
<div v-if="mode !== 'compact' && mutualEventsInformation.length"
class="content__events">
<NcAppNavigationCaption :name="t('spreed', 'Upcoming meetings')" />
<ul class="content__events-list">
<CalendarEventSmall v-for="event in mutualEventsInformation"
:key="event.uri"
:name="event.name"
:start="event.start"
:href="event.href"
:color="event.color" />
</ul>
</div>
</template>

<!-- Search messages in this conversation -->
Expand Down Expand Up @@ -404,6 +451,29 @@ function joinFields(firstSubstring?: string | null, secondSubstring?: string | n
}
}

&__events {
// To align with NcAppSidebarTab content width
margin-inline: 10px;

&-list {
--item-height: calc(2lh + var(--default-grid-baseline) * 3);
display: flex;
flex-direction: column;
margin: calc(var(--default-grid-baseline) / 2);
line-height: 20px;
max-height: calc(4.5 * var(--item-height) + 4 * var(--default-grid-baseline));
overflow-y: auto;

& > * {
margin-inline: calc(var(--default-grid-baseline) / 2);

&:not(:last-child) {
border-bottom: 1px solid var(--color-border-dark);
}
}
}
}

&__profile-action {
// Override NcActionLink styles
:deep(.action-link__longtext) {
Expand Down
95 changes: 95 additions & 0 deletions src/components/UIShared/CalendarEventSmall.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<!--
- SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import IconReload from 'vue-material-design-icons/Reload.vue'

import { t } from '@nextcloud/l10n'

const props = defineProps<{
name: string | null,
start: string | null,
color: string,
isRecurring?: boolean,
href?: string,
}>()
</script>

<template>
<li class="calendar-event">
<a class="calendar-event__item"
:class="{ 'calendar-event__item--thumb': !href }"
:href="href"
:title="t('spreed', 'Open Calendar')"
:tabindex="0"
target="_blank">
<span class="calendar-event__badge" :style="{ backgroundColor: color }" />
<span class="calendar-event__content">
<span class="calendar-event__header">
<span class="calendar-event__header-text">{{ name }}</span>
<IconReload v-if="isRecurring" :size="13" />
</span>
<span>{{ start }}</span>
</span>
</a>
</li>
</template>

<style lang="scss" scoped>
.calendar-event {
&__item {
display: flex;
flex-direction: row;
align-items: center;
margin-block: var(--default-grid-baseline);
padding-inline: var(--default-grid-baseline);
height: 100%;
border-radius: var(--border-radius);

&--thumb {
cursor: default;
}

&:hover {
background-color: var(--color-background-hover);
}
}

&__badge {
flex-shrink: 0;
display: inline-block;
width: var(--default-font-size);
height: var(--default-font-size);
margin-inline: calc((var(--default-clickable-area) - var(--default-font-size)) / 2);
border-radius: 50%;
background-color: var(--primary-color);
}

&__content {
display: flex;
flex-direction: column;
justify-content: center;
width: calc(100% - var(--default-clickable-area));
}

&__header {
display: flex;
gap: var(--default-grid-baseline);
font-weight: 500;

&-text {
display: inline-block;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

:deep(.material-design-icon) {
margin-top: 2px;
}
}
}
</style>
11 changes: 11 additions & 0 deletions src/services/groupwareService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'

import type {
getMutualEventsResponse,
OutOfOfficeResponse,
UpcomingEventsResponse,
scheduleMeetingParams,
Expand All @@ -33,6 +34,15 @@ const getUserAbsence = async (userId: string): OutOfOfficeResponse => {
return axios.get(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}/now', { userId }))
}

/**
* Get information about mutual events for a given 1-1 conversation.
*
* @param token The conversation token
*/
const getMutualEvents = async function(token: string): getMutualEventsResponse {
return axios.get(generateOcsUrl('apps/spreed/api/v4/room/{token}/mutual-events', { token }))
}

/**
* Schedule a new meeting for a given conversation.
*
Expand All @@ -58,6 +68,7 @@ const scheduleMeeting = async function(token: string, { calendarUri, start, end,
}

export {
getMutualEvents,
getUpcomingEvents,
getUserAbsence,
scheduleMeeting,
Expand Down
Loading