Announcements See merge request pleroma/pleroma-fe!1466merge-requests/1699/head
commit
00f4e20492
@ -0,0 +1,105 @@
|
||||
import { mapState } from 'vuex'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
import RichContent from '../rich_content/rich_content.jsx'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
const Announcement = {
|
||||
components: {
|
||||
AnnouncementEditor,
|
||||
RichContent
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
editing: false,
|
||||
editedAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: undefined
|
||||
},
|
||||
editError: ''
|
||||
}
|
||||
},
|
||||
props: {
|
||||
announcement: Object
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
content () {
|
||||
return this.announcement.content
|
||||
},
|
||||
isRead () {
|
||||
return this.announcement.read
|
||||
},
|
||||
publishedAt () {
|
||||
const time = this.announcement.published_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
startsAt () {
|
||||
const time = this.announcement.starts_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
endsAt () {
|
||||
const time = this.announcement.ends_at
|
||||
if (!time) {
|
||||
return
|
||||
}
|
||||
|
||||
return this.formatTimeOrDate(time, localeService.internalToBrowserLocale(this.$i18n.locale))
|
||||
},
|
||||
inactive () {
|
||||
return this.announcement.inactive
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
markAsRead () {
|
||||
if (!this.isRead) {
|
||||
return this.$store.dispatch('markAnnouncementAsRead', this.announcement.id)
|
||||
}
|
||||
},
|
||||
deleteAnnouncement () {
|
||||
return this.$store.dispatch('deleteAnnouncement', this.announcement.id)
|
||||
},
|
||||
formatTimeOrDate (time, locale) {
|
||||
const d = new Date(time)
|
||||
return this.announcement.all_day ? d.toLocaleDateString(locale) : d.toLocaleString(locale)
|
||||
},
|
||||
enterEditMode () {
|
||||
this.editedAnnouncement.content = this.announcement.pleroma.raw_content
|
||||
this.editedAnnouncement.startsAt = this.announcement.starts_at
|
||||
this.editedAnnouncement.endsAt = this.announcement.ends_at
|
||||
this.editedAnnouncement.allDay = this.announcement.all_day
|
||||
this.editing = true
|
||||
},
|
||||
submitEdit () {
|
||||
this.$store.dispatch('editAnnouncement', {
|
||||
id: this.announcement.id,
|
||||
...this.editedAnnouncement
|
||||
})
|
||||
.then(() => {
|
||||
this.editing = false
|
||||
})
|
||||
.catch(error => {
|
||||
this.editError = error.error
|
||||
})
|
||||
},
|
||||
cancelEdit () {
|
||||
this.editing = false
|
||||
},
|
||||
clearError () {
|
||||
this.editError = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Announcement
|
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="announcement">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.title') }}</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
<rich-content
|
||||
v-if="!editing"
|
||||
:html="content"
|
||||
:emoji="announcement.emojis"
|
||||
:handle-links="true"
|
||||
/>
|
||||
<announcement-editor
|
||||
v-else
|
||||
:announcement="editedAnnouncement"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="times"
|
||||
>
|
||||
<span v-if="publishedAt">
|
||||
{{ $t('announcements.published_time_display', { time: publishedAt }) }}
|
||||
</span>
|
||||
<span v-if="startsAt">
|
||||
{{ $t('announcements.start_time_display', { time: startsAt }) }}
|
||||
</span>
|
||||
<span v-if="endsAt">
|
||||
{{ $t('announcements.end_time_display', { time: endsAt }) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="!editing"
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
v-if="currentUser"
|
||||
class="btn button-default"
|
||||
:class="{ toggled: isRead }"
|
||||
:disabled="inactive"
|
||||
:title="inactive ? $t('announcements.inactive_message') : ''"
|
||||
@click="markAsRead"
|
||||
>
|
||||
{{ $t('announcements.mark_as_read_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
class="btn button-default"
|
||||
@click="enterEditMode"
|
||||
>
|
||||
{{ $t('announcements.edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
class="btn button-default"
|
||||
@click="deleteAnnouncement"
|
||||
>
|
||||
{{ $t('announcements.delete_action') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="submitEdit"
|
||||
>
|
||||
{{ $t('announcements.submit_edit_action') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn button-default"
|
||||
@click="cancelEdit"
|
||||
>
|
||||
{{ $t('announcements.cancel_edit_action') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="editing && editError"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('announcements.edit_error', { error }) }}
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
:title="$t('announcements.close_error')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcement.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.announcement {
|
||||
border-bottom-width: 1px;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-color: var(--border, $fallback--border);
|
||||
border-radius: 0;
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.times {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.footer .actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
margin: 1em;
|
||||
max-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,13 @@
|
||||
import Checkbox from '../checkbox/checkbox.vue'
|
||||
|
||||
const AnnouncementEditor = {
|
||||
components: {
|
||||
Checkbox
|
||||
},
|
||||
props: {
|
||||
announcement: Object,
|
||||
disabled: Boolean
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementEditor
|
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="announcement-editor">
|
||||
<textarea
|
||||
ref="textarea"
|
||||
v-model="announcement.content"
|
||||
class="post-textarea"
|
||||
rows="1"
|
||||
cols="1"
|
||||
:placeholder="$t('announcements.post_placeholder')"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-start-time">{{ $t('announcements.start_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-start-time"
|
||||
v-model="announcement.startsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<label for="announcement-end-time">{{ $t('announcements.end_time_prompt') }}</label>
|
||||
<input
|
||||
id="announcement-end-time"
|
||||
v-model="announcement.endsAt"
|
||||
:type="announcement.allDay ? 'date' : 'datetime-local'"
|
||||
:disabled="disabled"
|
||||
>
|
||||
</span>
|
||||
<span class="announcement-metadata">
|
||||
<Checkbox
|
||||
id="announcement-all-day"
|
||||
v-model="announcement.allDay"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<label for="announcement-all-day">{{ $t('announcements.all_day_prompt') }}</label>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcement_editor.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.announcement-editor {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
.announcement-metadata {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.post-textarea {
|
||||
resize: vertical;
|
||||
height: 10em;
|
||||
overflow: none;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
import { mapState } from 'vuex'
|
||||
import Announcement from '../announcement/announcement.vue'
|
||||
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
|
||||
|
||||
const AnnouncementsPage = {
|
||||
components: {
|
||||
Announcement,
|
||||
AnnouncementEditor
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
newAnnouncement: {
|
||||
content: '',
|
||||
startsAt: undefined,
|
||||
endsAt: undefined,
|
||||
allDay: false
|
||||
},
|
||||
posting: false,
|
||||
error: undefined
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.$store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
currentUser: state => state.users.currentUser
|
||||
}),
|
||||
announcements () {
|
||||
return this.$store.state.announcements.announcements
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
postAnnouncement () {
|
||||
this.posting = true
|
||||
this.$store.dispatch('postAnnouncement', this.newAnnouncement)
|
||||
.then(() => {
|
||||
this.newAnnouncement.content = ''
|
||||
this.startsAt = undefined
|
||||
this.endsAt = undefined
|
||||
})
|
||||
.catch(error => {
|
||||
this.error = error.error
|
||||
})
|
||||
.finally(() => {
|
||||
this.posting = false
|
||||
})
|
||||
},
|
||||
clearError () {
|
||||
this.error = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AnnouncementsPage
|
@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="panel panel-default announcements-page">
|
||||
<div class="panel-heading">
|
||||
<span>
|
||||
{{ $t('announcements.page_header') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<section
|
||||
v-if="currentUser && currentUser.role === 'admin'"
|
||||
>
|
||||
<div class="post-form">
|
||||
<div class="heading">
|
||||
<h4>{{ $t('announcements.post_form_header') }}</h4>
|
||||
</div>
|
||||
<div class="body">
|
||||
<announcement-editor
|
||||
:announcement="newAnnouncement"
|
||||
:disabled="posting"
|
||||
/>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<button
|
||||
class="btn button-default post-button"
|
||||
:disabled="posting"
|
||||
@click.prevent="postAnnouncement"
|
||||
>
|
||||
{{ $t('announcements.post_action') }}
|
||||
</button>
|
||||
<div
|
||||
v-if="error"
|
||||
class="alert error"
|
||||
>
|
||||
{{ $t('announcements.post_error', { error }) }}
|
||||
<button
|
||||
class="button-unstyled"
|
||||
@click="clearError"
|
||||
>
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="times"
|
||||
:title="$t('announcements.close_error')"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
v-for="announcement in announcements"
|
||||
:key="announcement.id"
|
||||
>
|
||||
<announcement
|
||||
:announcement="announcement"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script src="./announcements_page.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
|
||||
.announcements-page {
|
||||
.post-form {
|
||||
padding: var(--status-margin, $status-margin);
|
||||
|
||||
.heading, .body {
|
||||
margin-bottom: var(--status-margin, $status-margin);
|
||||
}
|
||||
|
||||
.post-button {
|
||||
min-width: 10em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,135 @@
|
||||
const FETCH_ANNOUNCEMENT_INTERVAL_MS = 1000 * 60 * 5
|
||||
|
||||
export const defaultState = {
|
||||
announcements: [],
|
||||
supportsAnnouncements: true,
|
||||
fetchAnnouncementsTimer: undefined
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setAnnouncements (state, announcements) {
|
||||
state.announcements = announcements
|
||||
},
|
||||
setAnnouncementRead (state, { id, read }) {
|
||||
const index = state.announcements.findIndex(a => a.id === id)
|
||||
|
||||
if (index < 0) {
|
||||
return
|
||||
}
|
||||
|
||||
state.announcements[index].read = read
|
||||
},
|
||||
setFetchAnnouncementsTimer (state, timer) {
|
||||
state.fetchAnnouncementsTimer = timer
|
||||
},
|
||||
setSupportsAnnouncements (state, supportsAnnouncements) {
|
||||
state.supportsAnnouncements = supportsAnnouncements
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {
|
||||
unreadAnnouncementCount (state, _getters, rootState) {
|
||||
if (!rootState.users.currentUser) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const unread = state.announcements.filter(announcement => !(announcement.inactive || announcement.read))
|
||||
return unread.length
|
||||
}
|
||||
}
|
||||
|
||||
const announcements = {
|
||||
state: defaultState,
|
||||
mutations,
|
||||
getters,
|
||||
actions: {
|
||||
fetchAnnouncements (store) {
|
||||
if (!store.state.supportsAnnouncements) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
const currentUser = store.rootState.users.currentUser
|
||||
const isAdmin = currentUser && currentUser.role === 'admin'
|
||||
|
||||
const getAnnouncements = async () => {
|
||||
if (!isAdmin) {
|
||||
return store.rootState.api.backendInteractor.fetchAnnouncements()
|
||||
}
|
||||
|
||||
const all = await store.rootState.api.backendInteractor.adminFetchAnnouncements()
|
||||
const visible = await store.rootState.api.backendInteractor.fetchAnnouncements()
|
||||
const visibleObject = visible.reduce((a, c) => {
|
||||
a[c.id] = c
|
||||
return a
|
||||
}, {})
|
||||
const getWithinVisible = announcement => visibleObject[announcement.id]
|
||||
|
||||
all.forEach(announcement => {
|
||||
const visibleAnnouncement = getWithinVisible(announcement)
|
||||
if (!visibleAnnouncement) {
|
||||
announcement.inactive = true
|
||||
} else {
|
||||
announcement.read = visibleAnnouncement.read
|
||||
}
|
||||
})
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
return getAnnouncements()
|
||||
.then(announcements => {
|
||||
store.commit('setAnnouncements', announcements)
|
||||
})
|
||||
.catch(error => {
|
||||
// If and only if backend does not support announcements, it would return 404.
|
||||
// In this case, silently ignores it.
|
||||
if (error && error.statusCode === 404) {
|
||||
store.commit('setSupportsAnnouncements', false)
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
})
|
||||
},
|
||||
markAnnouncementAsRead (store, id) {
|
||||
return store.rootState.api.backendInteractor.dismissAnnouncement({ id })
|
||||
.then(() => {
|
||||
store.commit('setAnnouncementRead', { id, read: true })
|
||||
})
|
||||
},
|
||||
startFetchingAnnouncements (store) {
|
||||
if (store.state.fetchAnnouncementsTimer) {
|
||||
return
|
||||
}
|
||||
|
||||
const interval = setInterval(() => store.dispatch('fetchAnnouncements'), FETCH_ANNOUNCEMENT_INTERVAL_MS)
|
||||
store.commit('setFetchAnnouncementsTimer', interval)
|
||||
|
||||
return store.dispatch('fetchAnnouncements')
|
||||
},
|
||||
stopFetchingAnnouncements (store) {
|
||||
const interval = store.state.fetchAnnouncementsTimer
|
||||
store.commit('setFetchAnnouncementsTimer', undefined)
|
||||
clearInterval(interval)
|
||||
},
|
||||
postAnnouncement (store, { content, startsAt, endsAt, allDay }) {
|
||||
return store.rootState.api.backendInteractor.postAnnouncement({ content, startsAt, endsAt, allDay })
|
||||
.then(() => {
|
||||
return store.dispatch('fetchAnnouncements')
|
||||
})
|
||||
},
|
||||
editAnnouncement (store, { id, content, startsAt, endsAt, allDay }) {
|
||||
return store.rootState.api.backendInteractor.editAnnouncement({ id, content, startsAt, endsAt, allDay })
|
||||
.then(() => {
|
||||
return store.dispatch('fetchAnnouncements')
|
||||
})
|
||||
},
|
||||
deleteAnnouncement (store, id) {
|
||||
return store.rootState.api.backendInteractor.deleteAnnouncement({ id })
|
||||
.then(() => {
|
||||
return store.dispatch('fetchAnnouncements')
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default announcements
|
Loading…
Reference in new issue