aboutsummaryrefslogtreecommitdiff
path: root/src/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/components')
-rw-r--r--src/components/announcement/announcement.js105
-rw-r--r--src/components/announcement/announcement.vue136
-rw-r--r--src/components/announcement_editor/announcement_editor.js13
-rw-r--r--src/components/announcement_editor/announcement_editor.vue60
-rw-r--r--src/components/announcements_page/announcements_page.js55
-rw-r--r--src/components/announcements_page/announcements_page.vue79
-rw-r--r--src/components/mobile_nav/mobile_nav.js2
-rw-r--r--src/components/mobile_nav/mobile_nav.vue2
-rw-r--r--src/components/nav_panel/nav_panel.js11
-rw-r--r--src/components/navigation/filter.js5
-rw-r--r--src/components/navigation/navigation.js7
-rw-r--r--src/components/navigation/navigation_pins.js10
-rw-r--r--src/components/notification/notification.js14
-rw-r--r--src/components/notification/notification.vue20
-rw-r--r--src/components/notifications/notifications.js7
-rw-r--r--src/components/notifications/notifications.scss10
-rw-r--r--src/components/registration/registration.vue4
-rw-r--r--src/components/rich_content/rich_content.jsx15
-rw-r--r--src/components/settings_modal/tabs/mutes_and_blocks_tab.vue2
-rw-r--r--src/components/settings_modal/tabs/security_tab/security_tab.vue2
-rw-r--r--src/components/settings_modal/tabs/theme_tab/theme_tab.js4
-rw-r--r--src/components/side_drawer/side_drawer.js5
-rw-r--r--src/components/side_drawer/side_drawer.vue20
-rw-r--r--src/components/timeline_menu/timeline_menu.js21
-rw-r--r--src/components/user_card/user_card.js13
-rw-r--r--src/components/user_card/user_card.scss4
-rw-r--r--src/components/user_card/user_card.vue6
-rw-r--r--src/components/user_note/user_note.js45
-rw-r--r--src/components/user_note/user_note.vue88
-rw-r--r--src/components/user_profile/user_profile.vue1
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"