Skip to content

Commit

Permalink
Feature: iCalendar (ICS) viewer (#298)
Browse files Browse the repository at this point in the history
  • Loading branch information
axllent committed May 18, 2024
1 parent 09f50f6 commit ed4618a
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 7 deletions.
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"bootstrap5-tags": "^1.6.1",
"color-hash": "^2.0.2",
"dayjs": "^1.11.10",
"ical.js": "^2.0.1",
"modern-screenshot": "^4.4.30",
"prismjs": "^1.29.0",
"rapidoc": "^9.3.4",
Expand Down
138 changes: 131 additions & 7 deletions server/ui-src/components/message/Attachments.vue
Original file line number Diff line number Diff line change
@@ -1,23 +1,80 @@

<script>
import commonMixins from '../../mixins/CommonMixins'
import ICAL from "ical.js"
import dayjs from 'dayjs'
export default {
props: {
message: Object,
attachments: Object
},
mixins: [commonMixins]
mixins: [commonMixins],
data() {
return {
ical: false
}
},
methods: {
openAttachment: function (part, e) {
let filename = part.FileName
let contentType = part.ContentType
let href = this.resolve('/api/v1/message/' + this.message.ID + '/part/' + part.PartID)
if (filename.match(/\.ics$/i) || contentType == 'text/calendar') {
e.preventDefault()
this.get(href, null, (response) => {
let comp = new ICAL.Component(ICAL.parse(response.data))
let vevent = comp.getFirstSubcomponent('vevent')
if (!vevent) {
alert('Error parsing ICS file')
return
}
let event = new ICAL.Event(vevent)
let summary = {}
summary.link = href
summary.status = vevent.getFirstPropertyValue('status')
summary.url = vevent.getFirstPropertyValue('url')
summary.summary = event.summary
summary.description = event.description
summary.location = event.location
summary.start = dayjs(event.startDate).format('ddd, D MMM YYYY, h:mm a')
summary.end = dayjs(event.endDate).format('ddd, D MMM YYYY, h:mm a')
summary.isRecurring = event.isRecurring()
summary.organizer = event.organizer ? event.organizer.replace(/^mailto:/, '') : false
summary.attendees = []
event.attendees.forEach((a) => {
if (a.jCal[1].cn) {
summary.attendees.push(a.jCal[1].cn)
}
})
comp.getAllSubcomponents("vtimezone").forEach((vtimezone) => {
summary.timezone = vtimezone.getFirstPropertyValue("tzid")
})
this.ical = summary
// display modal
this.modal('ICSView').show()
})
}
}
},
}
</script>

<template>
<div class="mt-4 border-top pt-4">
<a v-for="part in attachments" :href="resolve('/api/v1/message/' + message.ID + '/part/' + part.PartID)"
class="card attachment float-start me-3 mb-3" target="_blank" style="width: 180px">
<img v-if="isImage(part)" :src="resolve('/api/v1/message/' + message.ID + '/part/' + part.PartID + '/thumb')"
class="card-img-top" alt="">
class="card attachment float-start me-3 mb-3" target="_blank" style="width: 180px"
@click="openAttachment(part, $event)">
<img v-if="isImage(part)"
:src="resolve('/api/v1/message/' + message.ID + '/part/' + part.PartID + '/thumb')" class="card-img-top"
alt="">
<img v-else
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALQAAAB4AQMAAABhKUq+AAAAA1BMVEX///+nxBvIAAAAGUlEQVQYGe3BgQAAAADDoPtTT+EA1QAAgFsLQAAB12s2WgAAAABJRU5ErkJggg=="
class="card-img-top" alt="">
Expand All @@ -30,12 +87,79 @@ export default {
<small>{{ getFileSize(part.Size) }}</small>
</p>
<p class="card-text mb-0 small">
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
{{ part.FileName != '' ? part.FileName : '[ unknown ]' + part.ContentType }}
</p>
</div>
<div class="card-footer small border-0 text-center text-truncate">
{{ part.FileName != '' ? part.FileName : '[ unknown ]' }}
{{ part.FileName != '' ? part.FileName : '[ unknown ]' + part.ContentType }}
</div>
</a>
</div>

<div class="modal fade" id="ICSView" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fs-5">
<i class="bi bi-calendar-event me-2"></i>
iCalendar summary
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" v-if="ical">
<table class="table">
<tbody>
<tr v-if="ical.summary">
<th>Summary</th>
<td>{{ ical.summary }}</td>
</tr>
<tr v-if="ical.description">
<th>Description</th>
<td>{{ ical.description }}</td>
</tr>
<tr>
<th>When</th>
<td>
{{ ical.start }} &mdash; {{ ical.end }}
<span v-if="ical.isRecurring">(recurring)</span>
</td>
</tr>
<tr v-if="ical.status">
<th>Status</th>
<td> {{ ical.status }}</td>
</tr>
<tr v-if="ical.location">
<th>Location</th>
<td>{{ ical.location }}</td>
</tr>
<tr v-if="ical.url">
<th>URL</th>
<td><a :href="ical.url" target="_blank">{{ ical.url }}</a></td>
</tr>
<tr v-if="ical.organizer">
<th>Organizer</th>
<td>{{ ical.organizer }}</td>
</tr>
<tr v-if="ical.attendees.length">
<th>Attendees</th>
<td>
<span v-for="(a, i) in ical.attendees">
<template v-if="i > 0">,</template>
{{ a }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<a class="btn btn-primary" target="_blank" :href="ical.link">
Download attachment
</a>
</div>
</div>
</div>
</div>

</template>
3 changes: 3 additions & 0 deletions server/ui-src/mixins/CommonMixins.js
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ export default {
if (['zip', 'tar', 'rar', 'bz2', 'gz', 'xz'].includes(ext)) {
return 'bi-file-zip-fill'
}
if (['ics'].includes(ext)) {
return 'bi-calendar-event'
}
if (a.ContentType.match(/^audio\//)) {
return 'bi-file-music-fill'
}
Expand Down

0 comments on commit ed4618a

Please sign in to comment.