diff options
Diffstat (limited to 'src/components')
30 files changed, 731 insertions, 35 deletions
diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js new file mode 100644 index 00000000..c10c7d90 --- /dev/null +++ b/src/components/announcement/announcement.js @@ -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 diff --git a/src/components/announcement/announcement.vue b/src/components/announcement/announcement.vue new file mode 100644 index 00000000..5f64232a --- /dev/null +++ b/src/components/announcement/announcement.vue @@ -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> diff --git a/src/components/announcement_editor/announcement_editor.js b/src/components/announcement_editor/announcement_editor.js new file mode 100644 index 00000000..79a03afe --- /dev/null +++ b/src/components/announcement_editor/announcement_editor.js @@ -0,0 +1,13 @@ +import Checkbox from '../checkbox/checkbox.vue' + +const AnnouncementEditor = { + components: { + Checkbox + }, + props: { + announcement: Object, + disabled: Boolean + } +} + +export default AnnouncementEditor diff --git a/src/components/announcement_editor/announcement_editor.vue b/src/components/announcement_editor/announcement_editor.vue new file mode 100644 index 00000000..0f29f9f7 --- /dev/null +++ b/src/components/announcement_editor/announcement_editor.vue @@ -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> diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js new file mode 100644 index 00000000..0bb4892e --- /dev/null +++ b/src/components/announcements_page/announcements_page.js @@ -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 diff --git a/src/components/announcements_page/announcements_page.vue b/src/components/announcements_page/announcements_page.vue new file mode 100644 index 00000000..b1489dec --- /dev/null +++ b/src/components/announcements_page/announcements_page.vue @@ -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> diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index fb8ffa30..cdbbb812 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -54,7 +54,7 @@ const MobileNav = { isChat () { return this.$route.name === 'chat' }, - ...mapGetters(['unreadChatCount']), + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']), chatsPinned () { return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') } diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index d642008b..0f1fe621 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -19,7 +19,7 @@ icon="bars" /> <div - v-if="unreadChatCount && !chatsPinned" + v-if="(unreadChatCount && !chatsPinned) || unreadAnnouncementCount" class="alert-dot" /> </button> diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index b54f2fa2..8c9c3b11 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -18,7 +18,8 @@ import { faBell, faInfoCircle, faStream, - faList + faList, + faBullhorn } from '@fortawesome/free-solid-svg-icons' library.add( @@ -32,7 +33,8 @@ library.add( faBell, faInfoCircle, faStream, - faList + faList, + faBullhorn ) const NavPanel = { props: ['forceExpand', 'forceEditMode'], @@ -86,6 +88,7 @@ const NavPanel = { privateMode: state => state.instance.private, federating: state => state.instance.federating, pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + supportsAnnouncements: state => state.announcements.supportsAnnouncements, pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems), collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav }), @@ -96,6 +99,7 @@ const NavPanel = { .map(([k, v]) => ({ ...v, name: k })), { hasChats: this.pleromaChatMessagesAvailable, + hasAnnouncements: this.supportsAnnouncements, isFederating: this.federating, isPrivate: this.privateMode, currentUser: this.currentUser @@ -109,13 +113,14 @@ const NavPanel = { .map(([k, v]) => ({ ...v, name: k })), { hasChats: this.pleromaChatMessagesAvailable, + hasAnnouncements: this.supportsAnnouncements, isFederating: this.federating, isPrivate: this.privateMode, currentUser: this.currentUser } ) }, - ...mapGetters(['unreadChatCount']) + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) } } diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js index 31b55486..e8e77f8f 100644 --- a/src/components/navigation/filter.js +++ b/src/components/navigation/filter.js @@ -1,11 +1,12 @@ -export const filterNavigation = (list = [], { hasChats, isFederating, isPrivate, currentUser }) => { +export const filterNavigation = (list = [], { hasChats, hasAnnouncements, isFederating, isPrivate, currentUser }) => { return list.filter(({ criteria, anon, anonRoute }) => { const set = new Set(criteria || []) if (!isFederating && set.has('federating')) return false - if (isPrivate && set.has('!private')) return false + if (!currentUser && isPrivate && set.has('!private')) return false if (!currentUser && !(anon || anonRoute)) return false if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false if (!hasChats && set.has('chats')) return false + if (!hasAnnouncements && set.has('announcements')) return false return true }) } diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js index f66dd981..7f096316 100644 --- a/src/components/navigation/navigation.js +++ b/src/components/navigation/navigation.js @@ -71,5 +71,12 @@ export const ROOT_ITEMS = { anon: true, icon: 'info-circle', label: 'nav.about' + }, + announcements: { + route: 'announcements', + icon: 'bullhorn', + label: 'nav.announcements', + badgeGetter: 'unreadAnnouncementCount', + criteria: ['announcements'] } } diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index 57b8d589..9dd795aa 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -56,11 +56,17 @@ const NavPanel = { }), pinnedList () { if (!this.currentUser) { - return [ + return filterNavigation([ { ...TIMELINES.public, name: 'public' }, { ...TIMELINES.twkn, name: 'twkn' }, { ...ROOT_ITEMS.about, name: 'about' } - ] + ], + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + }) } return filterNavigation( [ diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index ddba560e..265aaee0 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -20,7 +20,9 @@ import { faUserPlus, faEyeSlash, faUser, - faSuitcaseRolling + faSuitcaseRolling, + faExpandAlt, + faCompressAlt } from '@fortawesome/free-solid-svg-icons' library.add( @@ -31,13 +33,15 @@ library.add( faUserPlus, faUser, faEyeSlash, - faSuitcaseRolling + faSuitcaseRolling, + faExpandAlt, + faCompressAlt ) const Notification = { data () { return { - userExpanded: false, + statusExpanded: false, betterShadow: this.$store.state.interface.browserSupport.cssFilter, unmuted: false } @@ -55,8 +59,8 @@ const Notification = { UserLink }, methods: { - toggleUserExpanded () { - this.userExpanded = !this.userExpanded + toggleStatusExpanded () { + this.statusExpanded = !this.statusExpanded }, generateUserProfileLink (user) { return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 84f3f7de..f1aa5420 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -144,13 +144,25 @@ <router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }" - class="faint-link" + class="timeago-link faint-link" > <Timeago :time="notification.created_at" :auto-update="240" /> </router-link> + <button + class="button-unstyled expand-icon" + @click.prevent="toggleStatusExpanded" + :title="$t('tool_tip.toggle_expand')" + :aria-expanded="statusExpanded" + > + <FAIcon + class="fa-scale-110" + fixed-width + :icon="statusExpanded ? 'compress-alt' : 'expand-alt'" + /> + </button> </div> <div v-else @@ -166,6 +178,8 @@ <button v-if="needMute" class="button-unstyled" + :title="$t('tool_tip.toggle_mute')" + :aria-expanded="!unmuted" @click.prevent="toggleMute" > <FAIcon @@ -222,8 +236,8 @@ /> <template v-else> <StatusContent - class="faint" - :compact="true" + :class="{ faint: !statusExpanded }" + :compact="!statusExpanded" :status="notification.action" /> </template> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index c3acd9e0..d499d3d6 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -69,7 +69,7 @@ const Notifications = { return this.unseenNotifications.length }, unseenCountTitle () { - return this.unseenCount + (this.unreadChatCount) + return this.unseenCount + (this.unreadChatCount) + this.unreadAnnouncementCount }, loading () { return this.$store.state.statuses.notifications.loading @@ -94,13 +94,16 @@ const Notifications = { return this.filteredNotifications.slice(0, this.unseenCount + this.seenToDisplayCount) }, noSticky () { return this.$store.getters.mergedConfig.disableStickyHeaders }, - ...mapGetters(['unreadChatCount']) + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) }, mounted () { this.scrollerRef = this.$refs.root.closest('.column.-scrollable') if (!this.scrollerRef) { this.scrollerRef = this.$refs.root.closest('.mobile-notifications') } + if (!this.scrollerRef) { + this.scrollerRef = this.$refs.root.closest('.column.main') + } this.scrollerRef.addEventListener('scroll', this.updateScrollPosition) }, unmounted () { diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index f71f9b76..9b241565 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -112,6 +112,16 @@ min-width: 3em; text-align: right; } + + .timeago-link { + margin-right: 0.2em; + } + + .expand-icon { + .svg-inline--fa { + margin-left: 0.25em; + } + } } .emoji-reaction-emoji { diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index d78d8da9..24d9b59b 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -158,10 +158,10 @@ class="form-error" > <ul> - <li v-if="!v$.user.confirm.required"> + <li v-if="v$.user.confirm.required.$invalid"> <span>{{ $t('registration.validations.password_confirmation_required') }}</span> </li> - <li v-if="!v$.user.confirm.sameAsPassword"> + <li v-if="v$.user.confirm.sameAs.$invalid"> <span>{{ $t('registration.validations.password_confirmation_match') }}</span> </li> </ul> diff --git a/src/components/rich_content/rich_content.jsx b/src/components/rich_content/rich_content.jsx index ca075270..7881e365 100644 --- a/src/components/rich_content/rich_content.jsx +++ b/src/components/rich_content/rich_content.jsx @@ -150,6 +150,7 @@ export default { if (Array.isArray(item)) { const [opener, children, closer] = item const Tag = getTagName(opener) + const fullAttrs = getAttrs(opener, () => true) const attrs = getAttrs(opener) const previouslyMentions = currentMentions !== null /* During grouping of mentions we trim all the empty text elements @@ -171,7 +172,7 @@ export default { return ['', [mentionsLinePadding, renderImage(opener)], ''] case 'a': // replace mentions with MentionLink if (!this.handleLinks) break - if (attrs['class'] && attrs['class'].includes('mention')) { + if (fullAttrs.class && fullAttrs.class.includes('mention')) { // Handling mentions here return renderMention(attrs, children) } else { @@ -179,7 +180,7 @@ export default { break } case 'span': - if (this.handleLinks && attrs['class'] && attrs['class'].includes('h-card')) { + if (this.handleLinks && fullAttrs.class && fullAttrs.class.includes('h-card')) { return ['', children.map(processItem), ''] } } @@ -213,13 +214,14 @@ export default { const [opener, children] = item const Tag = opener === '' ? '' : getTagName(opener) switch (Tag) { - case 'a': // replace mentions with MentionLink + case 'a': { // replace mentions with MentionLink if (!this.handleLinks) break - const attrs = getAttrs(opener) + const fullAttrs = getAttrs(opener, () => true) + const attrs = getAttrs(opener, () => true) // should only be this if ( - (attrs['class'] && attrs['class'].includes('hashtag')) || // Pleroma style - (attrs['rel'] === 'tag') // Mastodon style + (fullAttrs.class && fullAttrs.class.includes('hashtag')) || // Pleroma style + (fullAttrs.rel === 'tag') // Mastodon style ) { return renderHashtag(attrs, children, encounteredTextReverse) } else { @@ -230,6 +232,7 @@ export default { { newChildren } </a> } + } case '': return [...children].reverse().map(processItemReverse).reverse() } diff --git a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue index c515d542..ed4b15a4 100644 --- a/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue +++ b/src/components/settings_modal/tabs/mutes_and_blocks_tab.vue @@ -56,7 +56,7 @@ <div :label="$t('settings.mutes_tab')"> <tab-switcher> - <div label="Users"> + <div :label="$t('settings.user_mutes')"> <div class="usersearch-wrapper"> <Autosuggest :filter="filterUnMutedUsers" diff --git a/src/components/settings_modal/tabs/security_tab/security_tab.vue b/src/components/settings_modal/tabs/security_tab/security_tab.vue index c74a0c67..6e03bef4 100644 --- a/src/components/settings_modal/tabs/security_tab/security_tab.vue +++ b/src/components/settings_modal/tabs/security_tab/security_tab.vue @@ -241,7 +241,7 @@ class="btn button-default" @click="confirmDelete" > - {{ $t('settings.save') }} + {{ $t('settings.delete_account') }} </button> </div> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index 282cb384..4a739f73 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -279,6 +279,9 @@ export default { opacity ) + // Temporary patch for null-y value errors + if (layers.flat().some(v => v == null)) return acc + return { ...acc, ...textColors.reduce((acc, textColorKey) => { @@ -300,6 +303,7 @@ export default { return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {}) } catch (e) { console.warn('Failure computing contrasts', e) + return {} } }, previewRules () { diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index bb22446b..27019577 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -95,9 +95,10 @@ const SideDrawer = { } }, ...mapState({ - pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + supportsAnnouncements: state => state.announcements.supportsAnnouncements }), - ...mapGetters(['unreadChatCount']) + ...mapGetters(['unreadChatCount', 'unreadAnnouncementCount']) }, methods: { toggleDrawer () { diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index cbeafdd2..887596f8 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -192,6 +192,26 @@ </a> </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 v-if="currentUser" @click="toggleDrawer" > diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index d74fbf4e..5a2a86c2 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -1,8 +1,10 @@ import Popover from '../popover/popover.vue' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import { mapState } from 'vuex' import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { TIMELINES } from 'src/components/navigation/navigation.js' +import { filterNavigation } from 'src/components/navigation/filter.js' import { faChevronDown } from '@fortawesome/free-solid-svg-icons' @@ -29,8 +31,7 @@ const TimelineMenu = { }, data () { return { - isOpen: false, - timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })) + isOpen: false } }, created () { @@ -42,6 +43,22 @@ const TimelineMenu = { useListsMenu () { const route = this.$route.name return route === 'lists-timeline' + }, + ...mapState({ + currentUser: state => state.users.currentUser, + privateMode: state => state.instance.private, + federating: state => state.instance.federating + }), + timelinesList () { + return filterNavigation( + Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })), + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ) } }, methods: { diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 8b64a07e..67879307 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -4,6 +4,7 @@ import ProgressButton from '../progress_button/progress_button.vue' import FollowButton from '../follow_button/follow_button.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' +import UserNote from '../user_note/user_note.vue' import Select from '../select/select.vue' import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' @@ -39,7 +40,8 @@ export default { 'rounded', 'bordered', 'avatarAction', // default - open profile, 'zoom' - zoom, function - call function - 'onClose' + 'onClose', + 'hasNoteEditor' ], data () { return { @@ -129,6 +131,12 @@ export default { const privileges = this.loggedIn.privileges return this.loggedIn.role === 'admin' || privileges.includes('users_manage_activation_state') || privileges.includes('users_delete') || privileges.includes('users_manage_tags') }, + hasNote () { + return this.relationship.note + }, + supportsNote () { + return 'note' in this.relationship + }, ...mapGetters(['mergedConfig']) }, components: { @@ -140,7 +148,8 @@ export default { FollowButton, Select, RichContent, - UserLink + UserLink, + UserNote }, methods: { muteUser () { diff --git a/src/components/user_card/user_card.scss b/src/components/user_card/user_card.scss index a0bbc6a6..cdb8cb57 100644 --- a/src/components/user_card/user_card.scss +++ b/src/components/user_card/user_card.scss @@ -315,6 +315,10 @@ margin: 0; } } + + .user-note { + margin: 0 .75em .6em 0; + } } .sidebar .edit-profile-button { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 897d89f9..349c7cb1 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -268,6 +268,12 @@ > <RemoteFollow :user="user" /> </div> + <UserNote + v-if="loggedIn && isOtherUser && (hasNote || (hasNoteEditor && supportsNote))" + :user="user" + :relationship="relationship" + :editable="hasNoteEditor" + /> </div> </div> <div diff --git a/src/components/user_note/user_note.js b/src/components/user_note/user_note.js new file mode 100644 index 00000000..830b2e59 --- /dev/null +++ b/src/components/user_note/user_note.js @@ -0,0 +1,45 @@ +const UserNote = { + props: { + user: Object, + relationship: Object, + editable: Boolean + }, + data () { + return { + localNote: '', + editing: false, + frozen: false + } + }, + computed: { + shouldShow () { + return this.relationship.note || this.editing + } + }, + methods: { + startEditing () { + this.localNote = this.relationship.note + this.editing = true + }, + cancelEditing () { + this.editing = false + }, + finalizeEditing () { + this.frozen = true + + this.$store.dispatch('editUserNote', { + id: this.user.id, + comment: this.localNote + }) + .then(() => { + this.frozen = false + this.editing = false + }) + .catch(() => { + this.frozen = false + }) + } + } +} + +export default UserNote diff --git a/src/components/user_note/user_note.vue b/src/components/user_note/user_note.vue new file mode 100644 index 00000000..4286e017 --- /dev/null +++ b/src/components/user_note/user_note.vue @@ -0,0 +1,88 @@ +<template> + <div + class="user-note" + > + <div class="heading"> + <span>{{ $t('user_card.note') }}</span> + <div class="buttons"> + <button + v-show="!editing && editable" + class="button-default btn" + @click="startEditing" + > + {{ $t('user_card.edit_note') }} + </button> + <button + v-show="editing" + class="button-default btn" + :disabled="frozen" + @click="finalizeEditing" + > + {{ $t('user_card.edit_note_apply') }} + </button> + <button + v-show="editing" + class="button-default btn" + :disabled="frozen" + @click="cancelEditing" + > + {{ $t('user_card.edit_note_cancel') }} + </button> + </div> + </div> + <textarea + v-show="editing" + v-model="localNote" + class="note-text" + /> + <span + v-show="!editing" + class="note-text" + :class="{ '-blank': !relationship.note }" + > + {{ relationship.note || $t('user_card.note_blank') }} + </span> + </div> +</template> + +<script src="./user_note.js"></script> + +<style lang="scss"> +@import '../../variables'; + +.user-note { + display: flex; + flex-direction: column; + + .heading { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75em; + + .btn { + min-width: 95px; + } + + .buttons { + display: flex; + flex-direction: row; + justify-content: right; + + .btn { + margin-left: 0.5em; + } + } + } + + .note-text { + align-self: stretch; + } + + .note-text.-blank { + font-style: italic; + color: var(--faint, $fallback--faint); + } +} +</style> diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index d0da2b5b..d5e8d230 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -10,6 +10,7 @@ :selected="timeline.viewing" avatar-action="zoom" rounded="top" + :has-note-editor="true" /> <div v-if="user.fields_html && user.fields_html.length > 0" |
