Skip to content

Commit

Permalink
feat(calendar): preview all upcoming events
Browse files Browse the repository at this point in the history
Signed-off-by: Maksim Sukharev <[email protected]>
  • Loading branch information
Antreesy committed Jan 7, 2025
1 parent 27f6859 commit fd069f0
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 19 deletions.
174 changes: 174 additions & 0 deletions src/components/CalendarEventsDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<!--
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->

<script setup lang="ts">
import { computed, onBeforeMount, ref } from 'vue'

import IconCalendarBlank from 'vue-material-design-icons/CalendarBlank.vue'
import IconCalendarRefresh from 'vue-material-design-icons/CalendarRefresh.vue'

import { t } from '@nextcloud/l10n'
import moment from '@nextcloud/moment'

import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
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 usernameToColor from '@nextcloud/vue/dist/Functions/usernameToColor.js'

import { useGroupwareStore } from '../stores/groupware.ts'

const props = defineProps<{
token: string,
container?: string,
}>()
const emit = defineEmits<{
(event: 'close'): void,
}>()

const groupwareStore = useGroupwareStore()

const open = ref(false)
const loading = ref(Object.keys(groupwareStore.calendars).length === 0)

const calendars = computed(() => groupwareStore.calendars)
const upcomingEvents = computed(() => {
const now = moment().unix()
return groupwareStore.getAllEvents(props.token)
.sort((a, b) => (a.start && b.start) ? (a.start - b.start) : 0)
.map(event => {
const start = event.start
? (event.start <= now) ? t('spreed', 'Now') : moment(event.start * 1000).calendar()
: ''
const color = calendars.value[event.calendarUri]?.color ?? usernameToColor(event.calendarUri).color

return { ...event, start, color, href: event.calendarAppUrl ?? undefined }
})
})

onBeforeMount(() => {
getCalendars()
})

/**
* Show upcoming events dialog
*/
function openDialog() {
open.value = true
}

/**
* Get user's calendars to identify belonging of known and future events
*/
async function getCalendars() {
await groupwareStore.getPersonalCalendars()
loading.value = false
}
</script>

<template>
<div>
<slot name="trigger" :on-click="openDialog">
<NcButton :title="t('spreed', 'Upcoming events')"
:aria-label="t('spreed', 'Upcoming events')"
@click="openDialog">
<template #icon>
<IconCalendarBlank :size="20" />
</template>
</NcButton>
</slot>

<NcDialog :open.sync="open"
class="calendar-events"
:name="t('spreed', 'Upcoming events')"
size="normal"
close-on-click-outside
:container="container">
<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"
:href="event.href"
:title="t('spreed', 'Open Calendar')"
target="_blank">
<IconCalendarRefresh v-if="event.recurrenceId" :size="20" />
<IconCalendarBlank v-else :size="20" />
<div class="calendar-badge" :style="{ backgroundColor: event.color }" />
<div class="calendar-events__content">
<p class="calendar-events__header">
{{ event.summary }}
</p>
<p>{{ event.start }}</p>
</div>
</a>
</li>
</ul>
</template>
<NcEmptyContent v-else>
<template #icon>
<NcLoadingIcon v-if="loading" />
<IconCalendarBlank v-else />
</template>

<template #description>
<p>{{ loading ? t('spreed', 'Loading …') : t('spreed', 'No upcoming events') }}</p>
</template>
</NcEmptyContent>
</NcDialog>
</div>
</template>

<style lang="scss" scoped>
.calendar-events {
margin-block-end: calc(var(--default-grid-baseline) * 2);

:deep(.dialog__content) {
padding-block-end: calc(var(--default-grid-baseline) * 3);
}

&__list {
display: flex;
flex-direction: column;
margin: var(--default-grid-baseline);
gap: calc(var(--default-grid-baseline) * 2);
}

&__item {
display: flex;
flex-direction: row;
align-items: center;
gap: calc(var(--default-grid-baseline) * 2);
padding: 0 calc(var(--default-grid-baseline) * 2);
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light);
height: 100%;
border-radius: var(--border-radius);

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

&__content {
display: flex;
flex-direction: column;
justify-content: center;
line-height: 20px;
}

&__header {
font-weight: 500;
}
}

.calendar-badge {
display: inline-block;
width: var(--default-font-size);
height: var(--default-font-size);
border-radius: 50%;
background-color: var(--primary-color);
}
</style>
51 changes: 32 additions & 19 deletions src/components/TopBar/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,22 +56,25 @@

<TasksCounter v-if="conversation.type === CONVERSATION.TYPE.NOTE_TO_SELF" />

<!-- Upcoming event -->
<a v-if="showUpcomingEvent"
class="upcoming-event"
:href="nextEvent.calendarAppUrl"
:title="t('spreed', 'Open Calendar')"
target="_blank">
<div class="icon">
<IconCalendarBlank :size="20" />
</div>
<div class="event-info">
<p class="event-info__header">
{{ t('spreed', 'Next call') }}
</p>
<p> {{ eventInfo }} </p>
</div>
</a>
<!-- Upcoming events -->
<CalendarEventsDialog v-if="showCalendarEvents" :token="token">
<template v-if="showUpcomingEvent" #trigger="{ onClick }">
<a class="upcoming-event"
role="button"
:title="t('spreed', 'Upcoming events')"
@click="onClick">
<div class="icon">
<IconCalendarBlank :size="20" />
</div>
<div class="event-info">
<p class="event-info__header">
{{ t('spreed', 'Next call') }}
</p>
<p> {{ eventInfo }} </p>
</div>
</a>
</template>
</CalendarEventsDialog>

<!-- Call time -->
<CallTime v-if="isInCall"
Expand Down Expand Up @@ -139,6 +142,7 @@ import TasksCounter from './TasksCounter.vue'
import TopBarMediaControls from './TopBarMediaControls.vue'
import TopBarMenu from './TopBarMenu.vue'
import BreakoutRoomsEditor from '../BreakoutRoomsEditor/BreakoutRoomsEditor.vue'
import CalendarEventsDialog from '../CalendarEventsDialog.vue'
import ConversationIcon from '../ConversationIcon.vue'

import { useGetParticipants } from '../../composables/useGetParticipants.js'
Expand All @@ -155,6 +159,7 @@ export default {
components: {
// Components
BreakoutRoomsEditor,
CalendarEventsDialog,
CallButton,
CallTime,
ConversationIcon,
Expand Down Expand Up @@ -300,9 +305,12 @@ export default {
return moment(this.nextEvent.start * 1000).calendar()
},

showCalendarEvents() {
return !this.isInCall && !this.isSidebar && this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF
},

showUpcomingEvent() {
return this.nextEvent && !this.isInCall && !this.isSidebar && !this.isMobile
&& this.conversation.type !== CONVERSATION.TYPE.NOTE_TO_SELF
return this.nextEvent && !this.isMobile
},

getUserId() {
Expand Down Expand Up @@ -453,9 +461,14 @@ export default {
flex-direction: row;
gap: calc(var(--default-grid-baseline) * 2);
padding: 0 calc(var(--default-grid-baseline) * 2);
background-color: rgba(var(--color-info-rgb), 0.1);
color: var(--color-primary-element-light-text);
background-color: var(--color-primary-element-light);
height: 100%;
border-radius: var(--border-radius);

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

.event-info {
Expand Down

0 comments on commit fd069f0

Please sign in to comment.