Merge branch 'from/develop/tusooa/announcements' into 'develop'
Announcements See merge request pleroma/pleroma-fe!1466
This commit is contained in:
commit
00f4e20492
@ -374,6 +374,7 @@ const afterStoreSetup = async ({ store, i18n }) => {
|
|||||||
|
|
||||||
// Start fetching things that don't need to block the UI
|
// Start fetching things that don't need to block the UI
|
||||||
store.dispatch('fetchMutes')
|
store.dispatch('fetchMutes')
|
||||||
|
store.dispatch('startFetchingAnnouncements')
|
||||||
getTOS({ store })
|
getTOS({ store })
|
||||||
getStickers({ store })
|
getStickers({ store })
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import Lists from 'components/lists/lists.vue'
|
|||||||
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
import ListsTimeline from 'components/lists_timeline/lists_timeline.vue'
|
||||||
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
import ListsEdit from 'components/lists_edit/lists_edit.vue'
|
||||||
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
|
||||||
|
import AnnouncementsPage from 'components/announcements_page/announcements_page.vue'
|
||||||
|
|
||||||
export default (store) => {
|
export default (store) => {
|
||||||
const validateAuthenticatedRoute = (to, from, next) => {
|
const validateAuthenticatedRoute = (to, from, next) => {
|
||||||
@ -76,6 +77,7 @@ export default (store) => {
|
|||||||
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
{ name: 'search', path: '/search', component: Search, props: (route) => ({ query: route.query.query }) },
|
||||||
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
{ name: 'who-to-follow', path: '/who-to-follow', component: WhoToFollow, beforeEnter: validateAuthenticatedRoute },
|
||||||
{ name: 'about', path: '/about', component: About },
|
{ name: 'about', path: '/about', component: About },
|
||||||
|
{ name: 'announcements', path: '/announcements', component: AnnouncementsPage },
|
||||||
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
{ name: 'user-profile', path: '/users/:name', component: UserProfile },
|
||||||
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
{ name: 'legacy-user-profile', path: '/:name', component: UserProfile },
|
||||||
{ name: 'lists', path: '/lists', component: Lists },
|
{ name: 'lists', path: '/lists', component: Lists },
|
||||||
|
105
src/components/announcement/announcement.js
Normal file
105
src/components/announcement/announcement.js
Normal file
@ -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
|
136
src/components/announcement/announcement.vue
Normal file
136
src/components/announcement/announcement.vue
Normal file
@ -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>
|
13
src/components/announcement_editor/announcement_editor.js
Normal file
13
src/components/announcement_editor/announcement_editor.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Checkbox from '../checkbox/checkbox.vue'
|
||||||
|
|
||||||
|
const AnnouncementEditor = {
|
||||||
|
components: {
|
||||||
|
Checkbox
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
announcement: Object,
|
||||||
|
disabled: Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AnnouncementEditor
|
60
src/components/announcement_editor/announcement_editor.vue
Normal file
60
src/components/announcement_editor/announcement_editor.vue
Normal file
@ -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>
|
55
src/components/announcements_page/announcements_page.js
Normal file
55
src/components/announcements_page/announcements_page.js
Normal file
@ -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
|
79
src/components/announcements_page/announcements_page.vue
Normal file
79
src/components/announcements_page/announcements_page.vue
Normal file
@ -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>
|
@ -54,7 +54,7 @@ const MobileNav = {
|
|||||||
isChat () {
|
isChat () {
|
||||||
return this.$route.name === 'chat'
|
return this.$route.name === 'chat'
|
||||||
},
|
},
|
||||||
...mapGetters(['unreadChatCount']),
|
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']),
|
||||||
chatsPinned () {
|
chatsPinned () {
|
||||||
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats')
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
icon="bars"
|
icon="bars"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="unreadChatCount && !chatsPinned"
|
v-if="(unreadChatCount && !chatsPinned) || unreadAnnouncementCount"
|
||||||
class="alert-dot"
|
class="alert-dot"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
@ -18,7 +18,8 @@ import {
|
|||||||
faBell,
|
faBell,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faStream,
|
faStream,
|
||||||
faList
|
faList,
|
||||||
|
faBullhorn
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
|
||||||
library.add(
|
library.add(
|
||||||
@ -32,7 +33,8 @@ library.add(
|
|||||||
faBell,
|
faBell,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faStream,
|
faStream,
|
||||||
faList
|
faList,
|
||||||
|
faBullhorn
|
||||||
)
|
)
|
||||||
const NavPanel = {
|
const NavPanel = {
|
||||||
props: ['forceExpand', 'forceEditMode'],
|
props: ['forceExpand', 'forceEditMode'],
|
||||||
@ -86,6 +88,7 @@ const NavPanel = {
|
|||||||
privateMode: state => state.instance.private,
|
privateMode: state => state.instance.private,
|
||||||
federating: state => state.instance.federating,
|
federating: state => state.instance.federating,
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||||
|
supportsAnnouncements: state => state.announcements.supportsAnnouncements,
|
||||||
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems),
|
||||||
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
|
collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav
|
||||||
}),
|
}),
|
||||||
@ -96,6 +99,7 @@ const NavPanel = {
|
|||||||
.map(([k, v]) => ({ ...v, name: k })),
|
.map(([k, v]) => ({ ...v, name: k })),
|
||||||
{
|
{
|
||||||
hasChats: this.pleromaChatMessagesAvailable,
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
hasAnnouncements: this.supportsAnnouncements,
|
||||||
isFederating: this.federating,
|
isFederating: this.federating,
|
||||||
isPrivate: this.privateMode,
|
isPrivate: this.privateMode,
|
||||||
currentUser: this.currentUser
|
currentUser: this.currentUser
|
||||||
@ -109,13 +113,14 @@ const NavPanel = {
|
|||||||
.map(([k, v]) => ({ ...v, name: k })),
|
.map(([k, v]) => ({ ...v, name: k })),
|
||||||
{
|
{
|
||||||
hasChats: this.pleromaChatMessagesAvailable,
|
hasChats: this.pleromaChatMessagesAvailable,
|
||||||
|
hasAnnouncements: this.supportsAnnouncements,
|
||||||
isFederating: this.federating,
|
isFederating: this.federating,
|
||||||
isPrivate: this.privateMode,
|
isPrivate: this.privateMode,
|
||||||
currentUser: this.currentUser
|
currentUser: this.currentUser
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
...mapGetters(['unreadChatCount'])
|
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate, currentUser }) => {
|
export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser }) => {
|
||||||
return list.filter(({ criteria, anon, anonRoute }) => {
|
return list.filter(({ criteria, anon, anonRoute }) => {
|
||||||
const set = new Set(criteria || [])
|
const set = new Set(criteria || [])
|
||||||
if (!isFederating && set.has('federating')) return false
|
if (!isFederating && set.has('federating')) return false
|
||||||
@ -6,6 +6,7 @@ export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate,
|
|||||||
if (!currentUser && !(anon || anonRoute)) return false
|
if (!currentUser && !(anon || anonRoute)) return false
|
||||||
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false
|
||||||
if (!hasChats && set.has('chats')) return false
|
if (!hasChats && set.has('chats')) return false
|
||||||
|
if (!hasAnnouncements && set.has('announcements')) return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -71,5 +71,12 @@ export const ROOT_ITEMS = {
|
|||||||
anon: true,
|
anon: true,
|
||||||
icon: 'info-circle',
|
icon: 'info-circle',
|
||||||
label: 'nav.about'
|
label: 'nav.about'
|
||||||
|
},
|
||||||
|
announcements: {
|
||||||
|
route: 'announcements',
|
||||||
|
icon: 'bullhorn',
|
||||||
|
label: 'nav.announcements',
|
||||||
|
badgeGetter: 'unreadAnnouncementCount',
|
||||||
|
criteria: ['announcements']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ const Notifications = {
|
|||||||
return this.unseenNotifications.length
|
return this.unseenNotifications.length
|
||||||
},
|
},
|
||||||
unseenCountTitle () {
|
unseenCountTitle () {
|
||||||
return this.unseenCount + (this.unreadChatCount)
|
return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount
|
||||||
},
|
},
|
||||||
loading () {
|
loading () {
|
||||||
return this.$store.state.statuses.notifications.loading
|
return this.$store.state.statuses.notifications.loading
|
||||||
@ -94,7 +94,7 @@ const Notifications = {
|
|||||||
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount)
|
||||||
},
|
},
|
||||||
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders },
|
||||||
...mapGetters(['unreadChatCount'])
|
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')
|
this.scrollerRef = this.$refs.root.closest('.column.-scrollable')
|
||||||
|
@ -95,9 +95,10 @@ const SideDrawer = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
...mapState({
|
...mapState({
|
||||||
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable
|
pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable,
|
||||||
|
supportsAnnouncements: state => state.announcements.supportsAnnouncements
|
||||||
}),
|
}),
|
||||||
...mapGetters(['unreadChatCount'])
|
...mapGetters(['unreadChatCount', 'unreadAnnouncementCount'])
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleDrawer () {
|
toggleDrawer () {
|
||||||
|
@ -191,6 +191,26 @@
|
|||||||
/> {{ $t("nav.administration") }}
|
/> {{ $t("nav.administration") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="currentUser && supportsAnnouncements"
|
||||||
|
@click="toggleDrawer"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'announcements' }"
|
||||||
|
>
|
||||||
|
<FAIcon
|
||||||
|
fixed-width
|
||||||
|
class="fa-scale-110 fa-old-padding"
|
||||||
|
icon="bullhorn"
|
||||||
|
/> {{ $t("nav.announcements") }}
|
||||||
|
<span
|
||||||
|
v-if="unreadAnnouncementCount"
|
||||||
|
class="badge badge-notification"
|
||||||
|
>
|
||||||
|
{{ unreadAnnouncementCount }}
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
<li
|
<li
|
||||||
v-if="currentUser"
|
v-if="currentUser"
|
||||||
@click="toggleDrawer"
|
@click="toggleDrawer"
|
||||||
|
@ -32,6 +32,27 @@
|
|||||||
},
|
},
|
||||||
"staff": "Staff"
|
"staff": "Staff"
|
||||||
},
|
},
|
||||||
|
"announcements": {
|
||||||
|
"page_header": "Announcements",
|
||||||
|
"title": "Announcement",
|
||||||
|
"mark_as_read_action": "Mark as read",
|
||||||
|
"post_form_header": "Post announcement",
|
||||||
|
"post_placeholder": "Type your announcement content here...",
|
||||||
|
"post_action": "Post",
|
||||||
|
"post_error": "Error: {error}",
|
||||||
|
"close_error": "Close",
|
||||||
|
"delete_action": "Delete",
|
||||||
|
"start_time_prompt": "Start time: ",
|
||||||
|
"end_time_prompt": "End time: ",
|
||||||
|
"all_day_prompt": "This is an all-day event",
|
||||||
|
"published_time_display": "Published at {time}",
|
||||||
|
"start_time_display": "Starts at {time}",
|
||||||
|
"end_time_display": "Ends at {time}",
|
||||||
|
"edit_action": "Edit",
|
||||||
|
"submit_edit_action": "Submit",
|
||||||
|
"cancel_edit_action": "Cancel",
|
||||||
|
"inactive_message": "This announcement is inactive"
|
||||||
|
},
|
||||||
"shoutbox": {
|
"shoutbox": {
|
||||||
"title": "Shoutbox"
|
"title": "Shoutbox"
|
||||||
},
|
},
|
||||||
@ -162,7 +183,8 @@
|
|||||||
"mobile_sidebar": "Toggle mobile sidebar",
|
"mobile_sidebar": "Toggle mobile sidebar",
|
||||||
"mobile_notifications": "Open notifications",
|
"mobile_notifications": "Open notifications",
|
||||||
"mobile_notifications": "Open notifications (there are unread ones)",
|
"mobile_notifications": "Open notifications (there are unread ones)",
|
||||||
"mobile_notifications_close": "Close notifications"
|
"mobile_notifications_close": "Close notifications",
|
||||||
|
"announcements": "Announcements"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"broken_favorite": "Unknown status, searching for it…",
|
"broken_favorite": "Unknown status, searching for it…",
|
||||||
|
@ -24,6 +24,7 @@ import editStatusModule from './modules/editStatus.js'
|
|||||||
import statusHistoryModule from './modules/statusHistory.js'
|
import statusHistoryModule from './modules/statusHistory.js'
|
||||||
|
|
||||||
import chatsModule from './modules/chats.js'
|
import chatsModule from './modules/chats.js'
|
||||||
|
import announcementsModule from './modules/announcements.js'
|
||||||
|
|
||||||
import { createI18n } from 'vue-i18n'
|
import { createI18n } from 'vue-i18n'
|
||||||
|
|
||||||
@ -91,7 +92,8 @@ const persistedStateOptions = {
|
|||||||
postStatus: postStatusModule,
|
postStatus: postStatusModule,
|
||||||
editStatus: editStatusModule,
|
editStatus: editStatusModule,
|
||||||
statusHistory: statusHistoryModule,
|
statusHistory: statusHistoryModule,
|
||||||
chats: chatsModule
|
chats: chatsModule,
|
||||||
|
announcements: announcementsModule
|
||||||
},
|
},
|
||||||
plugins,
|
plugins,
|
||||||
strict: false // Socket modifies itself, let's ignore this for now.
|
strict: false // Socket modifies itself, let's ignore this for now.
|
||||||
|
135
src/modules/announcements.js
Normal file
135
src/modules/announcements.js
Normal file
@ -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
|
@ -90,6 +90,8 @@ const MASTODON_DOMAIN_BLOCKS_URL = '/api/v1/domain_blocks'
|
|||||||
const MASTODON_LISTS_URL = '/api/v1/lists'
|
const MASTODON_LISTS_URL = '/api/v1/lists'
|
||||||
const MASTODON_STREAMING = '/api/v1/streaming'
|
const MASTODON_STREAMING = '/api/v1/streaming'
|
||||||
const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
const MASTODON_KNOWN_DOMAIN_LIST_URL = '/api/v1/instance/peers'
|
||||||
|
const MASTODON_ANNOUNCEMENTS_URL = '/api/v1/announcements'
|
||||||
|
const MASTODON_ANNOUNCEMENTS_DISMISS_URL = id => `/api/v1/announcements/${id}/dismiss`
|
||||||
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
const PLEROMA_EMOJI_REACTIONS_URL = id => `/api/v1/pleroma/statuses/${id}/reactions`
|
||||||
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
const PLEROMA_EMOJI_REACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||||
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
const PLEROMA_EMOJI_UNREACT_URL = (id, emoji) => `/api/v1/pleroma/statuses/${id}/reactions/${emoji}`
|
||||||
@ -100,6 +102,10 @@ const PLEROMA_CHAT_READ_URL = id => `/api/v1/pleroma/chats/${id}/read`
|
|||||||
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
const PLEROMA_DELETE_CHAT_MESSAGE_URL = (chatId, messageId) => `/api/v1/pleroma/chats/${chatId}/messages/${messageId}`
|
||||||
const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
|
const PLEROMA_ADMIN_REPORTS = '/api/pleroma/admin/reports'
|
||||||
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
const PLEROMA_BACKUP_URL = '/api/v1/pleroma/backups'
|
||||||
|
const PLEROMA_ANNOUNCEMENTS_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
|
const PLEROMA_POST_ANNOUNCEMENT_URL = '/api/v1/pleroma/admin/announcements'
|
||||||
|
const PLEROMA_EDIT_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
||||||
|
const PLEROMA_DELETE_ANNOUNCEMENT_URL = id => `/api/v1/pleroma/admin/announcements/${id}`
|
||||||
|
|
||||||
const oldfetch = window.fetch
|
const oldfetch = window.fetch
|
||||||
|
|
||||||
@ -1361,6 +1367,66 @@ const dismissNotification = ({ credentials, id }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminFetchAnnouncements = ({ credentials }) => {
|
||||||
|
return promisedRequest({ url: PLEROMA_ANNOUNCEMENTS_URL, credentials })
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchAnnouncements = ({ credentials }) => {
|
||||||
|
return promisedRequest({ url: MASTODON_ANNOUNCEMENTS_URL, credentials })
|
||||||
|
}
|
||||||
|
|
||||||
|
const dismissAnnouncement = ({ id, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: MASTODON_ANNOUNCEMENTS_DISMISS_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcementToPayload = ({ content, startsAt, endsAt, allDay }) => {
|
||||||
|
const payload = { content }
|
||||||
|
|
||||||
|
if (typeof startsAt !== 'undefined') {
|
||||||
|
payload.starts_at = startsAt ? new Date(startsAt).toISOString() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof endsAt !== 'undefined') {
|
||||||
|
payload.ends_at = endsAt ? new Date(endsAt).toISOString() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof allDay !== 'undefined') {
|
||||||
|
payload.all_day = allDay
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload
|
||||||
|
}
|
||||||
|
|
||||||
|
const postAnnouncement = ({ credentials, content, startsAt, endsAt, allDay }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_POST_ANNOUNCEMENT_URL,
|
||||||
|
credentials,
|
||||||
|
method: 'POST',
|
||||||
|
payload: announcementToPayload({ content, startsAt, endsAt, allDay })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const editAnnouncement = ({ id, credentials, content, startsAt, endsAt, allDay }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_EDIT_ANNOUNCEMENT_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'PATCH',
|
||||||
|
payload: announcementToPayload({ content, startsAt, endsAt, allDay })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteAnnouncement = ({ id, credentials }) => {
|
||||||
|
return promisedRequest({
|
||||||
|
url: PLEROMA_DELETE_ANNOUNCEMENT_URL(id),
|
||||||
|
credentials,
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
export const getMastodonSocketURI = ({ credentials, stream, args = {} }) => {
|
||||||
return Object.entries({
|
return Object.entries({
|
||||||
...(credentials
|
...(credentials
|
||||||
@ -1687,7 +1753,13 @@ const apiService = {
|
|||||||
readChat,
|
readChat,
|
||||||
deleteChatMessage,
|
deleteChatMessage,
|
||||||
setReportState,
|
setReportState,
|
||||||
fetchUserInLists
|
fetchUserInLists,
|
||||||
|
fetchAnnouncements,
|
||||||
|
dismissAnnouncement,
|
||||||
|
postAnnouncement,
|
||||||
|
editAnnouncement,
|
||||||
|
deleteAnnouncement,
|
||||||
|
adminFetchAnnouncements
|
||||||
}
|
}
|
||||||
|
|
||||||
export default apiService
|
export default apiService
|
||||||
|
Loading…
Reference in New Issue
Block a user