From 51d8b9a1adf7c9554492111a00f6f6aabfbceb26 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Thu, 9 Jan 2025 10:19:51 +0100 Subject: [PATCH 1/3] feat(calendar): add schedule meeting API handling - align capabilities mock with Capabilities.php reference Signed-off-by: Maksim Sukharev --- src/__mocks__/capabilities.ts | 23 ++++++++++++++++++----- src/services/groupwareService.ts | 25 +++++++++++++++++++++++++ src/stores/groupware.ts | 8 ++++++++ src/types/index.ts | 3 +++ 4 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/__mocks__/capabilities.ts b/src/__mocks__/capabilities.ts index ce56a841562..36a4f3a9794 100644 --- a/src/__mocks__/capabilities.ts +++ b/src/__mocks__/capabilities.ts @@ -84,11 +84,13 @@ export const mockedCapabilities: Capabilities = { 'chat-reference-id', 'mention-permissions', 'edit-messages-note-to-self', - 'archived-conversations', - 'talk-polls-drafts', 'archived-conversations-v2', + 'talk-polls-drafts', 'download-call-participants', 'email-csv-import', + 'conversation-creation-password', + 'call-notification-state-api', + 'schedule-meeting', ], 'features-local': [ 'favorites', @@ -100,9 +102,10 @@ export const mockedCapabilities: Capabilities = { 'avatar', 'remind-me-later', 'note-to-self', - 'archived-conversations', 'archived-conversations-v2', 'chat-summary-api', + 'call-notification-state-api', + 'schedule-meeting', ], config: { attachments: { @@ -147,6 +150,7 @@ export const mockedCapabilities: Capabilities = { }, signaling: { 'session-ping-limit': 200, + 'hello-v2-token-key': '123', }, }, 'config-local': { @@ -163,17 +167,26 @@ export const mockedCapabilities: Capabilities = { chat: [ 'read-privacy', 'has-translation-providers', + 'has-translation-task-providers', 'typing-privacy', 'summary-threshold', ], conversations: [ 'can-create', ], - federation: [], + federation: [ + 'enabled', + 'incoming-enabled', + 'outgoing-enabled', + 'only-trusted-servers', + ], previews: [ 'max-gif-size', ], - signaling: [], + signaling: [ + 'session-ping-limit', + 'hello-v2-token-key', + ], }, version: '20.0.0-dev.0', } diff --git a/src/services/groupwareService.ts b/src/services/groupwareService.ts index 4a1a9cd53e9..23c737ca336 100644 --- a/src/services/groupwareService.ts +++ b/src/services/groupwareService.ts @@ -9,6 +9,8 @@ import { generateOcsUrl } from '@nextcloud/router' import type { OutOfOfficeResponse, UpcomingEventsResponse, + scheduleMeetingParams, + scheduleMeetingResponse, } from '../types/index.ts' /** @@ -31,7 +33,30 @@ const getUserAbsence = async (userId: string): OutOfOfficeResponse => { return axios.get(generateOcsUrl('/apps/dav/api/v1/outOfOffice/{userId}/now', { userId })) } +/** + * Schedule a new meeting for a given conversation. + * + * @param token The conversation token + * @param payload Function payload + * @param payload.calendarUri Last part of the calendar URI as seen by the participant + * @param payload.start Unix timestamp when the meeting starts + * @param payload.end Unix timestamp when the meeting ends, falls back to 60 minutes after start + * @param payload.title Title or summary of the event, falling back to the conversation name if none is given + * @param payload.description Description of the event, falling back to the conversation description if none is given + * @param options options object destructured + */ +const scheduleMeeting = async function(token: string, { calendarUri, start, end, title, description }: scheduleMeetingParams, options?: object): scheduleMeetingResponse { + return axios.post(generateOcsUrl('apps/spreed/api/v4/room/{token}/meeting', { token }, options), { + calendarUri, + start, + end, + title, + description, + } as scheduleMeetingParams, options) +} + export { getUpcomingEvents, getUserAbsence, + scheduleMeeting, } diff --git a/src/stores/groupware.ts b/src/stores/groupware.ts index c5e75c41294..1bc3d030746 100644 --- a/src/stores/groupware.ts +++ b/src/stores/groupware.ts @@ -18,11 +18,13 @@ import { import { getUpcomingEvents, getUserAbsence, + scheduleMeeting, } from '../services/groupwareService.ts' import type { DavCalendar, OutOfOfficeResult, UpcomingEvent, + scheduleMeetingParams, } from '../types/index.ts' type State = { @@ -111,6 +113,12 @@ export const useGroupwareStore = defineStore('groupware', { } }, + async scheduleMeeting(token: string, payload: scheduleMeetingParams) { + await scheduleMeeting(token, payload) + // Fetch updated list of events for this conversation + await this.getUpcomingEvents(token) + }, + /** * Drop an absence status from the store * @param token The conversation token diff --git a/src/types/index.ts b/src/types/index.ts index 5ad8b0c96b3..efb308e5b87 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -300,6 +300,9 @@ export type OutOfOfficeResult = { } export type OutOfOfficeResponse = ApiResponseUnwrapped +export type scheduleMeetingParams = Required['requestBody']['content']['application/json'] +export type scheduleMeetingResponse = ApiResponse + // User preferences response // from https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-user-preferences-api.html export type UserPreferencesResponse = ApiResponseUnwrapped From 7e3b52b8c8348799b91b788ebe6a49e923388184 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Thu, 9 Jan 2025 13:03:44 +0100 Subject: [PATCH 2/3] feat(calendar): allow moderators to schedule meeting Signed-off-by: Maksim Sukharev --- src/components/CalendarEventsDialog.vue | 195 +++++++++++++++++++++++- 1 file changed, 194 insertions(+), 1 deletion(-) diff --git a/src/components/CalendarEventsDialog.vue b/src/components/CalendarEventsDialog.vue index 96953325de9..911491b1549 100644 --- a/src/components/CalendarEventsDialog.vue +++ b/src/components/CalendarEventsDialog.vue @@ -4,20 +4,27 @@ --> + +
+

+ {{ t('spreed', 'Schedule new meeting') }} +

+ + +
+ + +
+ + + + +

+ {{ invalidHint }} +

+
+ + @@ -173,10 +324,52 @@ async function getCalendars() { } } +.calendar-meeting { + display: flex; + flex-direction: column; + margin: calc(var(--default-grid-baseline) / 2); + gap: var(--default-grid-baseline); + + &__header { + margin-block: calc(var(--default-grid-baseline) * 3); + text-align: center; + } + + &__invalid-hint { + color: var(--color-error); + } + + &__flex-wrapper { + display: flex; + gap: calc(var(--default-grid-baseline) * 2); + } + + // Overwrite default NcDateTimePickerNative styles + :deep(.native-datetime-picker) { + width: calc(50% - var(--default-grid-baseline)); + margin-bottom: var(--default-grid-baseline); + + label { + margin-bottom: 2px; + } + + input { + margin: 0; + border-width: 1px; + } + + &.invalid-time input { + border-width: 2px; + border-color: var(--color-error); + } + } +} + .calendar-badge { 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); } From 1f61bc0b3fbe8649ea622eaca8ff6874a4316b94 Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Thu, 9 Jan 2025 14:42:07 +0100 Subject: [PATCH 3/3] fix(calendar): move upcoming event to popover - get rid of slot #trigger Signed-off-by: Maksim Sukharev --- src/components/CalendarEventsDialog.vue | 197 +++++++++++++++--------- src/components/TopBar/TopBar.vue | 74 +-------- 2 files changed, 130 insertions(+), 141 deletions(-) diff --git a/src/components/CalendarEventsDialog.vue b/src/components/CalendarEventsDialog.vue index 911491b1549..eb78b8bde7d 100644 --- a/src/components/CalendarEventsDialog.vue +++ b/src/components/CalendarEventsDialog.vue @@ -7,8 +7,9 @@ import { computed, onBeforeMount, ref, watch } from 'vue' import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue' -import IconCalendarRefresh from 'vue-material-design-icons/CalendarRefresh.vue' import IconCheck from 'vue-material-design-icons/Check.vue' +import IconPlus from 'vue-material-design-icons/Plus.vue' +import IconReload from 'vue-material-design-icons/Reload.vue' import { t } from '@nextcloud/l10n' import moment from '@nextcloud/moment' @@ -18,9 +19,11 @@ import NcDateTimePickerNative from '@nextcloud/vue/dist/Components/NcDateTimePic import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import NcPopover from '@nextcloud/vue/dist/Components/NcPopover.js' import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js' import NcTextArea from '@nextcloud/vue/dist/Components/NcTextArea.js' import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' +import { useIsMobile } from '@nextcloud/vue/dist/Composables/useIsMobile.js' import usernameToColor from '@nextcloud/vue/dist/Functions/usernameToColor.js' import { useStore } from '../composables/useStore.js' @@ -35,10 +38,13 @@ const emit = defineEmits<{ (event: 'close'): void, }>() +const hideTriggers = (triggers: string[]) => [...triggers, 'click'] + const store = useStore() const groupwareStore = useGroupwareStore() +const isMobile = useIsMobile() -const open = ref(false) +const isFormOpen = ref(false) const loading = ref(Object.keys(groupwareStore.calendars).length === 0) const submitting = ref(false) @@ -93,7 +99,7 @@ onBeforeMount(() => { getCalendars() }) -watch(open, (value) => { +watch(isFormOpen, (value) => { if (!value) { return } @@ -111,13 +117,6 @@ watch([selectedCalendar, selectedDateTimeStart, selectedDateTimeEnd], () => { invalid.value = null }) -/** - * Show upcoming events dialog - */ -function openDialog() { - open.value = true -} - /** * Get user's calendars to identify belonging of known and future events */ @@ -153,6 +152,7 @@ async function submitNewMeeting() { title: newMeetingTitle.value || null, description: newMeetingDescription.value || null, }) + isFormOpen.value = false } catch (error) { // @ts-expect-error Vue: Property response does not exist invalid.value = error?.response?.data?.ocs?.data?.error ?? 'unknown' @@ -164,59 +164,75 @@ async function submitNewMeeting() {