diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts
index ae7f47262a4..48fc9036bca 100644
--- a/src/__mocks__/capabilities.ts
+++ b/src/__mocks__/capabilities.ts
@@ -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',
@@ -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: {
diff --git a/src/components/CalendarEventsDialog.vue b/src/components/CalendarEventsDialog.vue
index 3248d782fb8..49f79b9c00f 100644
--- a/src/components/CalendarEventsDialog.vue
+++ b/src/components/CalendarEventsDialog.vue
@@ -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'
@@ -339,24 +340,13 @@ async function submitNewMeeting() {
@@ -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;
& > * {
@@ -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);
diff --git a/src/components/RightSidebar/RightSidebarContent.vue b/src/components/RightSidebar/RightSidebarContent.vue
index d7febb291b0..f8b2ebc4ab8 100644
--- a/src/components/RightSidebar/RightSidebarContent.vue
+++ b/src/components/RightSidebar/RightSidebarContent.vue
@@ -18,10 +18,13 @@ 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'
@@ -29,9 +32,18 @@ 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<{
@@ -118,9 +130,32 @@ const profileInformation = computed(() => {
return fields
})
+const mutualEventsInformation = computed(() => {
+ 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 })
@@ -184,6 +219,18 @@ function joinFields(firstSubstring?: string | null, secondSubstring?: string | n
+
@@ -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) {
diff --git a/src/components/UIShared/CalendarEventSmall.vue b/src/components/UIShared/CalendarEventSmall.vue
new file mode 100644
index 00000000000..ea40edcec90
--- /dev/null
+++ b/src/components/UIShared/CalendarEventSmall.vue
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+ {{ start }}
+
+
+
+
+
+
diff --git a/src/services/groupwareService.ts b/src/services/groupwareService.ts
index e5d53751ec3..e462ce36aea 100644
--- a/src/services/groupwareService.ts
+++ b/src/services/groupwareService.ts
@@ -7,6 +7,7 @@ import axios from '@nextcloud/axios'
import { generateOcsUrl } from '@nextcloud/router'
import type {
+ getMutualEventsResponse,
OutOfOfficeResponse,
UpcomingEventsResponse,
scheduleMeetingParams,
@@ -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.
*
@@ -58,6 +68,7 @@ const scheduleMeeting = async function(token: string, { calendarUri, start, end,
}
export {
+ getMutualEvents,
getUpcomingEvents,
getUserAbsence,
scheduleMeeting,
diff --git a/src/stores/groupware.ts b/src/stores/groupware.ts
index c0e0d99e3a1..edfb8a16599 100644
--- a/src/stores/groupware.ts
+++ b/src/stores/groupware.ts
@@ -16,8 +16,10 @@ import {
getDefaultCalendarUri,
convertUrlToUri,
} from '../services/CalDavClient.ts'
+import { hasTalkFeature } from '../services/CapabilitiesManager.ts'
import { getUserProfile } from '../services/coreService.ts'
import {
+ getMutualEvents,
getUpcomingEvents,
getUserAbsence,
scheduleMeeting,
@@ -26,6 +28,7 @@ import type {
ApiErrorResponse,
Conversation,
DavCalendar,
+ DashboardEvent,
OutOfOfficeResult,
UpcomingEvent,
UserProfileData,
@@ -37,16 +40,20 @@ type State = {
calendars: Record,
defaultCalendarUri: string | null,
upcomingEvents: Record,
+ mutualEvents: Record,
supportProfileInfo: boolean,
profileInfo: Record,
}
+const supportsMutualEvents = hasTalkFeature('local', 'mutual-calendar-events')
+
export const useGroupwareStore = defineStore('groupware', {
state: (): State => ({
absence: {},
calendars: {},
defaultCalendarUri: null,
upcomingEvents: {},
+ mutualEvents: {},
supportProfileInfo: true,
profileInfo: {},
}),
@@ -174,6 +181,25 @@ export const useGroupwareStore = defineStore('groupware', {
}
},
+ /**
+ * Request and parse profile information
+ * @param conversation The conversation object
+ */
+ async getUserMutualEvents(conversation: Conversation) {
+ if (!supportsMutualEvents || !conversation.token
+ || conversation.type !== CONVERSATION.TYPE.ONE_TO_ONE) {
+ return
+ }
+
+ // FIXME cache results for 6/24 hours and do not fetch again
+ try {
+ const response = await getMutualEvents(conversation.token)
+ Vue.set(this.mutualEvents, conversation.token, response.data.ocs.data)
+ } catch (error) {
+ console.error(error)
+ }
+ },
+
/**
* Clears store for a deleted conversation
* @param token The conversation token
diff --git a/src/types/index.ts b/src/types/index.ts
index e202e903f3f..26da0280231 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -335,8 +335,11 @@ export type {
UpcomingEventsResponse,
} from './openapi/core/index.ts'
+export type DashboardEvent = components['schemas']['DashboardEvent']
+
export type scheduleMeetingParams = Required['requestBody']['content']['application/json']
export type scheduleMeetingResponse = ApiResponse
+export type getMutualEventsResponse = ApiResponse
export type EventTimeRange = {
start: number | null