diff options
Diffstat (limited to 'src/components')
75 files changed, 1717 insertions, 694 deletions
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 99762562..735dd81c 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,6 +1,7 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' +import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faEllipsisV @@ -19,7 +20,8 @@ const AccountActions = { }, components: { ProgressButton, - Popover + Popover, + UserListMenu }, methods: { showRepeats () { diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 23547f2c..770740e0 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -28,6 +28,7 @@ class="dropdown-divider" /> </template> + <UserListMenu :user="user" /> <button v-if="relationship.blocking" class="btn button-default btn-block dropdown-item" diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index d62a4adc..5dc50475 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -129,6 +129,9 @@ const Attachment = { ...mapGetters(['mergedConfig']) }, watch: { + 'attachment.description' (newVal) { + this.localDescription = newVal + }, localDescription (newVal) { this.onEdit(newVal) } diff --git a/src/components/basic_user_card/basic_user_card.js b/src/components/basic_user_card/basic_user_card.js index 8b1a2c38..31de2d75 100644 --- a/src/components/basic_user_card/basic_user_card.js +++ b/src/components/basic_user_card/basic_user_card.js @@ -1,5 +1,6 @@ import UserPopover from '../user_popover/user_popover.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -10,7 +11,8 @@ const BasicUserCard = { components: { UserPopover, UserAvatar, - RichContent + RichContent, + UserLink }, methods: { userProfileLink (user) { diff --git a/src/components/basic_user_card/basic_user_card.vue b/src/components/basic_user_card/basic_user_card.vue index 9cca7840..418de926 100644 --- a/src/components/basic_user_card/basic_user_card.vue +++ b/src/components/basic_user_card/basic_user_card.vue @@ -30,12 +30,10 @@ /> </div> <div> - <router-link + <user-link class="basic-user-card-screen-name" - :to="userProfileLink(user)" - > - @{{ user.screen_name_ui }} - </router-link> + :user="user" + /> </div> <slot /> </div> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 712e2a2c..85e6d8ad 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -1,6 +1,8 @@ import { reduce, filter, findIndex, clone, get } from 'lodash' import Status from '../status/status.vue' import ThreadTree from '../thread_tree/thread_tree.vue' +import { WSConnectionStatus } from '../../services/api/api.service.js' +import { mapGetters, mapState } from 'vuex' import QuickFilterSettings from '../quick_filter_settings/quick_filter_settings.vue' import QuickViewSettings from '../quick_view_settings/quick_view_settings.vue' @@ -79,6 +81,9 @@ const conversation = { const maxDepth = this.$store.getters.mergedConfig.maxDepthInThread - 2 return maxDepth >= 1 ? maxDepth : 1 }, + streamingEnabled () { + return this.mergedConfig.useStreamingApi && this.mastoUserSocketStatus === WSConnectionStatus.JOINED + }, displayStyle () { return this.$store.getters.mergedConfig.conversationDisplay }, @@ -341,7 +346,11 @@ const conversation = { }, maybeHighlight () { return this.isExpanded ? this.highlight : null - } + }, + ...mapGetters(['mergedConfig']), + ...mapState({ + mastoUserSocketStatus: state => state.api.mastoUserSocketStatus + }) }, components: { Status, @@ -399,6 +408,11 @@ const conversation = { setHighlight (id) { if (!id) return this.highlight = id + + if (!this.streamingEnabled) { + this.$store.dispatch('fetchStatus', id) + } + this.$store.dispatch('fetchFavsAndRepeats', id) this.$store.dispatch('fetchEmojiReactionsBy', id) }, diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss index f6c7c43d..1ec25385 100644 --- a/src/components/desktop_nav/desktop_nav.scss +++ b/src/components/desktop_nav/desktop_nav.scss @@ -137,4 +137,8 @@ text-align: right; } } + + .spacer { + width: 1em; + } } diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue index f352c78c..5db7fc79 100644 --- a/src/components/desktop_nav/desktop_nav.vue +++ b/src/components/desktop_nav/desktop_nav.vue @@ -61,6 +61,7 @@ :title="$t('nav.administration')" /> </a> + <span class="spacer" /> <button v-if="currentUser" class="button-unstyled nav-icon" diff --git a/src/components/edit_status_modal/edit_status_modal.js b/src/components/edit_status_modal/edit_status_modal.js new file mode 100644 index 00000000..75adfea7 --- /dev/null +++ b/src/components/edit_status_modal/edit_status_modal.js @@ -0,0 +1,75 @@ +import PostStatusForm from '../post_status_form/post_status_form.vue' +import Modal from '../modal/modal.vue' +import statusPosterService from '../../services/status_poster/status_poster.service.js' +import get from 'lodash/get' + +const EditStatusModal = { + components: { + PostStatusForm, + Modal + }, + data () { + return { + resettingForm: false + } + }, + computed: { + isLoggedIn () { + return !!this.$store.state.users.currentUser + }, + modalActivated () { + return this.$store.state.editStatus.modalActivated + }, + isFormVisible () { + return this.isLoggedIn && !this.resettingForm && this.modalActivated + }, + params () { + return this.$store.state.editStatus.params || {} + } + }, + watch: { + params (newVal, oldVal) { + if (get(newVal, 'statusId') !== get(oldVal, 'statusId')) { + this.resettingForm = true + this.$nextTick(() => { + this.resettingForm = false + }) + } + }, + isFormVisible (val) { + if (val) { + this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus()) + } + } + }, + methods: { + doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) { + const params = { + store: this.$store, + statusId: this.$store.state.editStatus.params.statusId, + status, + spoilerText, + sensitive, + poll, + media, + contentType + } + + return statusPosterService.editStatus(params) + .then((data) => { + return data + }) + .catch((err) => { + console.error('Error editing status', err) + return { + error: err.message + } + }) + }, + closeModal () { + this.$store.dispatch('closeEditStatusModal') + } + } +} + +export default EditStatusModal diff --git a/src/components/edit_status_modal/edit_status_modal.vue b/src/components/edit_status_modal/edit_status_modal.vue new file mode 100644 index 00000000..1dbacaab --- /dev/null +++ b/src/components/edit_status_modal/edit_status_modal.vue @@ -0,0 +1,48 @@ +<template> + <Modal + v-if="isFormVisible" + class="edit-form-modal-view" + @backdropClicked="closeModal" + > + <div class="edit-form-modal-panel panel"> + <div class="panel-heading"> + {{ $t('post_status.edit_status') }} + </div> + <PostStatusForm + class="panel-body" + v-bind="params" + :post-handler="doEditStatus" + :disable-polls="true" + :disable-visibility-selector="true" + @posted="closeModal" + /> + </div> + </Modal> +</template> + +<script src="./edit_status_modal.js"></script> + +<style lang="scss"> +.modal-view.edit-form-modal-view { + align-items: flex-start; +} +.edit-form-modal-panel { + flex-shrink: 0; + margin-top: 25%; + margin-bottom: 2em; + width: 100%; + max-width: 700px; + + @media (orientation: landscape) { + margin-top: 8%; + } + + .form-bottom-left { + max-width: 6.5em; + + .emoji-icon { + justify-content: right; + } + } +} +</style> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index 5ba3907f..b664d6b3 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -1,5 +1,6 @@ import Completion from '../../services/completion/completion.js' import EmojiPicker from '../emoji_picker/emoji_picker.vue' +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { take } from 'lodash' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' @@ -120,7 +121,8 @@ const EmojiInput = { } }, components: { - EmojiPicker + EmojiPicker, + UnicodeDomainIndicator }, computed: { padEmoji () { diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 7d95ab7e..81b81913 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -50,7 +50,21 @@ <span v-else>{{ suggestion.replacement }}</span> </span> <div class="label"> - <span class="displayText">{{ suggestion.displayText }}</span> + <span + v-if="suggestion.user" + class="displayText" + > + {{ suggestion.displayText }}<UnicodeDomainIndicator + :user="suggestion.user" + :at="false" + /> + </span> + <span + v-if="!suggestion.user" + class="displayText" + > + {{ suggestion.displayText }} + </span> <span class="detailText">{{ suggestion.detailText }}</span> </div> </div> diff --git a/src/components/emoji_input/suggestor.js b/src/components/emoji_input/suggestor.js index e8efbd1e..0ddb4d68 100644 --- a/src/components/emoji_input/suggestor.js +++ b/src/components/emoji_input/suggestor.js @@ -116,11 +116,12 @@ export const suggestUsers = ({ dispatch, state }) => { return diff + nameAlphabetically + screenNameAlphabetically /* eslint-disable camelcase */ - }).map(({ screen_name, screen_name_ui, name, profile_image_url_original }) => ({ - displayText: screen_name_ui, - detailText: name, - imageUrl: profile_image_url_original, - replacement: '@' + screen_name + ' ' + }).map((user) => ({ + user, + displayText: user.screen_name_ui, + detailText: user.name, + imageUrl: user.profile_image_url_original, + replacement: '@' + user.screen_name + ' ' })) /* eslint-enable camelcase */ diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 68fa66ad..2e495423 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -7,6 +7,7 @@ import { faThumbtack, faShareAlt, faExternalLinkAlt, + faHistory, faPlus, faTimes } from '@fortawesome/free-solid-svg-icons' @@ -24,6 +25,7 @@ library.add( faShareAlt, faExternalLinkAlt, faFlag, + faHistory, faPlus, faTimes ) @@ -86,6 +88,25 @@ const ExtraButtons = { }, reportStatus () { this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] }) + }, + editStatus () { + this.$store.dispatch('fetchStatusSource', { id: this.status.id }) + .then(data => this.$store.dispatch('openEditStatusModal', { + statusId: this.status.id, + subject: data.spoiler_text, + statusText: data.text, + statusIsSensitive: this.status.nsfw, + statusPoll: this.status.poll, + statusFiles: [...this.status.attachments], + visibility: this.status.visibility, + statusContentType: data.content_type + })) + }, + showStatusHistory () { + const originalStatus = { ...this.status } + const stripFieldsList = ['attachments', 'created_at', 'emojis', 'text', 'raw_html', 'nsfw', 'poll', 'summary', 'summary_raw_html'] + stripFieldsList.forEach(p => delete originalStatus[p]) + this.$store.dispatch('openStatusHistoryModal', originalStatus) } }, computed: { @@ -109,7 +130,11 @@ const ExtraButtons = { }, statusLink () { return `${this.$store.state.instance.server}${this.$router.resolve({ name: 'conversation', params: { id: this.status.id } }).href}` - } + }, + isEdited () { + return this.status.edited_at !== null + }, + editingAvailable () { return this.$store.state.instance.editingAvailable } } } diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 011dff9b..b2fad1c9 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -78,6 +78,28 @@ </button> </template> <button + v-if="ownStatus && editingAvailable" + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="editStatus" + @click="close" + > + <FAIcon + fixed-width + icon="pen" + /><span>{{ $t("status.edit") }}</span> + </button> + <button + v-if="isEdited && editingAvailable" + class="button-default dropdown-item dropdown-item-icon" + @click.prevent="showStatusHistory" + @click="close" + > + <FAIcon + fixed-width + icon="history" + /><span>{{ $t("status.status_history") }}</span> + </button> + <button v-if="canDelete" class="button-default dropdown-item dropdown-item-icon" @click.prevent="deleteStatus" diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index 09904761..d828b819 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -29,10 +29,10 @@ .global-notice-list { position: fixed; - top: 50px; + top: calc(var(--navbar-height) + 0.5em); width: 100%; pointer-events: none; - z-index: var(--ZI_popovers); + z-index: var(--ZI_navbar_popovers); display: flex; flex-direction: column; align-items: center; diff --git a/src/components/lists/lists.js b/src/components/lists/lists.js index 791b99b2..56d68430 100644 --- a/src/components/lists/lists.js +++ b/src/components/lists/lists.js @@ -1,5 +1,4 @@ import ListsCard from '../lists_card/lists_card.vue' -import ListsNew from '../lists_new/lists_new.vue' const Lists = { data () { @@ -8,11 +7,7 @@ const Lists = { } }, components: { - ListsCard, - ListsNew - }, - created () { - this.$store.dispatch('startFetchingLists') + ListsCard }, computed: { lists () { diff --git a/src/components/lists/lists.vue b/src/components/lists/lists.vue index fcc56447..b8bab0a0 100644 --- a/src/components/lists/lists.vue +++ b/src/components/lists/lists.vue @@ -1,21 +1,15 @@ <template> - <div v-if="isNew"> - <ListsNew @cancel="cancelNewList" /> - </div> - <div - v-else - class="settings panel panel-default" - > + <div class="Lists panel panel-default"> <div class="panel-heading"> <div class="title"> {{ $t('lists.lists') }} </div> - <button - class="button-default" - @click="newList" + <router-link + :to="{ name: 'lists-new' }" + class="button-default btn new-list-button" > {{ $t("lists.new") }} - </button> + </router-link> </div> <div class="panel-body"> <ListsCard @@ -29,3 +23,11 @@ </template> <script src="./lists.js"></script> + +<style lang="scss"> +.Lists { + .new-list-button { + padding: 0 0.5em; + } +} +</style> diff --git a/src/components/lists_edit/lists_edit.js b/src/components/lists_edit/lists_edit.js index a68bb589..c22d1323 100644 --- a/src/components/lists_edit/lists_edit.js +++ b/src/components/lists_edit/lists_edit.js @@ -1,7 +1,9 @@ import { mapState, mapGetters } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import ListsUserSearch from '../lists_user_search/lists_user_search.vue' +import PanelLoading from 'src/components/panel_loading/panel_loading.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx' import { library } from '@fortawesome/fontawesome-svg-core' import { faSearch, @@ -17,22 +19,33 @@ const ListsNew = { components: { BasicUserCard, UserAvatar, - ListsUserSearch + ListsUserSearch, + TabSwitcher, + PanelLoading }, data () { return { title: '', - userIds: [], - selectedUserIds: [] + titleDraft: '', + membersUserIds: [], + removedUserIds: new Set([]), // users we added for members, to undo + searchUserIds: [], + addedUserIds: new Set([]), // users we added from search, to undo + searchLoading: false, + reallyDelete: false } }, created () { - this.$store.dispatch('fetchList', { id: this.id }) - .then(() => { this.title = this.findListTitle(this.id) }) - this.$store.dispatch('fetchListAccounts', { id: this.id }) + if (!this.id) return + this.$store.dispatch('fetchList', { listId: this.id }) .then(() => { - this.selectedUserIds = this.findListAccounts(this.id) - this.selectedUserIds.forEach(userId => { + this.title = this.findListTitle(this.id) + this.titleDraft = this.title + }) + this.$store.dispatch('fetchListAccounts', { listId: this.id }) + .then(() => { + this.membersUserIds = this.findListAccounts(this.id) + this.membersUserIds.forEach(userId => { this.$store.dispatch('fetchUserIfMissing', userId) }) }) @@ -41,11 +54,12 @@ const ListsNew = { id () { return this.$route.params.id }, - users () { - return this.userIds.map(userId => this.findUser(userId)) + membersUsers () { + return [...this.membersUserIds, ...this.addedUserIds] + .map(userId => this.findUser(userId)).filter(user => user) }, - selectedUsers () { - return this.selectedUserIds.map(userId => this.findUser(userId)).filter(user => user) + searchUsers () { + return this.searchUserIds.map(userId => this.findUser(userId)).filter(user => user) }, ...mapState({ currentUser: state => state.users.currentUser @@ -56,33 +70,73 @@ const ListsNew = { onInput () { this.search(this.query) }, - selectUser (user) { - if (this.selectedUserIds.includes(user.id)) { - this.removeUser(user.id) + toggleRemoveMember (user) { + if (this.removedUserIds.has(user.id)) { + this.id && this.addUser(user) + this.removedUserIds.delete(user.id) } else { - this.addUser(user) + this.id && this.removeUser(user.id) + this.removedUserIds.add(user.id) } }, - isSelected (user) { - return this.selectedUserIds.includes(user.id) + toggleAddFromSearch (user) { + if (this.addedUserIds.has(user.id)) { + this.id && this.removeUser(user.id) + this.addedUserIds.delete(user.id) + } else { + this.id && this.addUser(user) + this.addedUserIds.add(user.id) + } + }, + isRemoved (user) { + return this.removedUserIds.has(user.id) + }, + isAdded (user) { + return this.addedUserIds.has(user.id) }, addUser (user) { - this.selectedUserIds.push(user.id) + this.$store.dispatch('addListAccount', { accountId: this.user.id, listId: this.id }) }, removeUser (userId) { - this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) + this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId: this.id }) }, - onResults (results) { - this.userIds = results + onSearchLoading (results) { + this.searchLoading = true }, - updateList () { - this.$store.dispatch('setList', { id: this.id, title: this.title }) - this.$store.dispatch('setListAccounts', { id: this.id, accountIds: this.selectedUserIds }) - - this.$router.push({ name: 'lists-timeline', params: { id: this.id } }) + onSearchLoadingDone (results) { + this.searchLoading = false + }, + onSearchResults (results) { + this.searchLoading = false + this.searchUserIds = results + }, + updateListTitle () { + this.$store.dispatch('setList', { listId: this.id, title: this.titleDraft }) + .then(() => { + this.title = this.findListTitle(this.id) + }) + }, + createList () { + this.$store.dispatch('createList', { title: this.titleDraft }) + .then((list) => { + return this + .$store + .dispatch('setListAccounts', { listId: list.id, accountIds: [...this.addedUserIds] }) + .then(() => list.id) + }) + .then((listId) => { + this.$router.push({ name: 'lists-timeline', params: { id: listId } }) + }) + .catch((e) => { + this.$store.dispatch('pushGlobalNotice', { + messageKey: 'lists.error', + messageArgs: [e.message], + level: 'error' + }) + }) }, deleteList () { - this.$store.dispatch('deleteList', { id: this.id }) + this.$store.dispatch('deleteList', { listId: this.id }) this.$router.push({ name: 'lists' }) } } diff --git a/src/components/lists_edit/lists_edit.vue b/src/components/lists_edit/lists_edit.vue index 69007b02..6521aba6 100644 --- a/src/components/lists_edit/lists_edit.vue +++ b/src/components/lists_edit/lists_edit.vue @@ -1,8 +1,8 @@ <template> - <div class="panel-default panel list-edit"> + <div class="panel-default panel ListEdit"> <div ref="header" - class="panel-heading" + class="panel-heading list-edit-heading" > <button class="button-unstyled go-back-button" @@ -13,54 +13,151 @@ icon="chevron-left" /> </button> + <div class="title"> + <i18n-t + v-if="id" + keypath="lists.editing_list" + > + <template #listTitle> + {{ title }} + </template> + </i18n-t> + <i18n-t + v-else + keypath="lists.creating_list" + /> + </div> </div> - <div class="input-wrap"> - <input - ref="title" - v-model="title" - :placeholder="$t('lists.title')" + <div class="panel-body"> + <div class="input-wrap"> + <label for="list-edit-title">{{ $t('lists.title') }}</label> + {{ ' ' }} + <input + id="list-edit-title" + ref="title" + v-model="titleDraft" + > + <button + v-if="id" + class="btn button-default follow-button" + @click="updateListTitle" + > + {{ $t('lists.update_title') }} + </button> + </div> + <tab-switcher + class="list-member-management" + :scrollable-tabs="true" > + <div + v-if="id || addedUserIds.size > 0" + :label="$t('lists.manage_members')" + class="members-list" + > + <div class="users-list"> + <div + v-for="user in membersUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + > + <button + class="btn button-default follow-button" + @click="toggleRemoveMember(user)" + > + {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }} + </button> + </BasicUserCard> + </div> + </div> + </div> + + <div + class="search-list" + :label="$t('lists.add_members')" + > + <ListsUserSearch + @results="onSearchResults" + @loading="onSearchLoading" + @loadingDone="onSearchLoadingDone" + /> + <div + v-if="searchLoading" + class="loading" + > + <PanelLoading /> + </div> + <div + v-else + class="users-list" + > + <div + v-for="user in searchUsers" + :key="user.id" + class="member" + > + <BasicUserCard + :user="user" + > + <span + v-if="membersUserIds.includes(user.id)" + > + {{ $t('lists.is_in_list') }} + </span> + <button + v-if="!membersUserIds.includes(user.id)" + class="btn button-default follow-button" + @click="toggleAddFromSearch(user)" + > + {{ isAdded(user) ? $t('general.undo') : $t('lists.add_to_list') }} + </button> + <button + v-else + class="btn button-default follow-button" + @click="toggleRemoveMember(user)" + > + {{ isRemoved(user) ? $t('general.undo') : $t('lists.remove_from_list') }} + </button> + </BasicUserCard> + </div> + </div> + </div> + </tab-switcher> </div> - <div class="member-list"> - <div - v-for="user in selectedUsers" - :key="user.id" - class="member" + <div class="panel-footer"> + <span class="spacer" /> + <button + v-if="!id" + class="btn button-default footer-button" + @click="createList" > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> - </div> - <ListsUserSearch @results="onResults" /> - <div class="member-list"> - <div - v-for="user in users" - :key="user.id" - class="member" + {{ $t('lists.create') }} + </button> + <button + v-else-if="!reallyDelete" + class="btn button-default footer-button" + @click="reallyDelete = true" > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> + {{ $t('lists.delete') }} + </button> + <template v-else> + {{ $t('lists.really_delete') }} + <button + class="btn button-default footer-button" + @click="deleteList" + > + {{ $t('general.yes') }} + </button> + <button + class="btn button-default footer-button" + @click="reallyDelete = false" + > + {{ $t('general.no') }} + </button> + </template> </div> - <button - :disabled="title && title.length === 0" - class="btn button-default" - @click="updateList" - > - {{ $t('lists.save') }} - </button> - <button - class="btn button-default" - @click="deleteList" - > - {{ $t('lists.delete') }} - </button> </div> </template> @@ -69,28 +166,43 @@ <style lang="scss"> @import '../../_variables.scss'; -.list-edit { - .input-wrap { +.ListEdit { + --panel-body-padding: 0.5em; + + height: calc(100vh - var(--navbar-height)); + overflow: hidden; + display: flex; + flex-direction: column; + + .list-edit-heading { + grid-template-columns: auto minmax(50%, 1fr); + } + + .panel-body { display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; + flex: 1; + flex-direction: column; + overflow: hidden; + } - input { - width: 100%; - } + .list-member-management { + flex: 1 0 auto; } .search-icon { margin-right: 0.3em; } - .member-list { + .users-list { padding-bottom: 0.7rem; + overflow-y: auto; } - .basic-user-card:hover, - .basic-user-card.selected { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); + & .search-list, + & .members-list { + overflow: hidden; + flex-direction: column; + min-height: 0; } .go-back-button { @@ -102,7 +214,15 @@ } .btn { - margin: 0.5em; + margin: 0 0.5em; + } + + .panel-footer { + grid-template-columns: minmax(10%, 1fr); + + .footer-button { + min-width: 9em; + } } } </style> diff --git a/src/components/lists_menu/lists_menu_content.js b/src/components/lists_menu/lists_menu_content.js index 37e7868c..97b32210 100644 --- a/src/components/lists_menu/lists_menu_content.js +++ b/src/components/lists_menu/lists_menu_content.js @@ -1,28 +1,17 @@ import { mapState } from 'vuex' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -} from '@fortawesome/free-solid-svg-icons' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import { getListEntries } from 'src/components/navigation/filter.js' -library.add( - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -) - -const ListsMenuContent = { - created () { - this.$store.dispatch('startFetchingLists') +export const ListsMenuContent = { + props: [ + 'showPin' + ], + components: { + NavigationEntry }, computed: { ...mapState({ - lists: state => state.lists.allLists, + lists: getListEntries, currentUser: state => state.users.currentUser, privateMode: state => state.instance.private, federating: state => state.instance.federating diff --git a/src/components/lists_menu/lists_menu_content.vue b/src/components/lists_menu/lists_menu_content.vue index e910d6eb..f93e80c9 100644 --- a/src/components/lists_menu/lists_menu_content.vue +++ b/src/components/lists_menu/lists_menu_content.vue @@ -1,16 +1,11 @@ <template> <ul> - <li - v-for="list in lists.slice().reverse()" - :key="list.id" - > - <router-link - class="menu-item" - :to="{ name: 'lists-timeline', params: { id: list.id } }" - > - {{ list.title }} - </router-link> - </li> + <NavigationEntry + v-for="item in lists" + :key="item.name" + :show-pin="showPin" + :item="item" + /> </ul> </template> diff --git a/src/components/lists_new/lists_new.js b/src/components/lists_new/lists_new.js deleted file mode 100644 index 63dc28ad..00000000 --- a/src/components/lists_new/lists_new.js +++ /dev/null @@ -1,79 +0,0 @@ -import { mapState, mapGetters } from 'vuex' -import BasicUserCard from '../basic_user_card/basic_user_card.vue' -import UserAvatar from '../user_avatar/user_avatar.vue' -import ListsUserSearch from '../lists_user_search/lists_user_search.vue' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faSearch, - faChevronLeft -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faSearch, - faChevronLeft -) - -const ListsNew = { - components: { - BasicUserCard, - UserAvatar, - ListsUserSearch - }, - data () { - return { - title: '', - userIds: [], - selectedUserIds: [] - } - }, - computed: { - users () { - return this.userIds.map(userId => this.findUser(userId)) - }, - selectedUsers () { - return this.selectedUserIds.map(userId => this.findUser(userId)) - }, - ...mapState({ - currentUser: state => state.users.currentUser - }), - ...mapGetters(['findUser']) - }, - methods: { - goBack () { - this.$emit('cancel') - }, - onInput () { - this.search(this.query) - }, - selectUser (user) { - if (this.selectedUserIds.includes(user.id)) { - this.removeUser(user.id) - } else { - this.addUser(user) - } - }, - isSelected (user) { - return this.selectedUserIds.includes(user.id) - }, - addUser (user) { - this.selectedUserIds.push(user.id) - }, - removeUser (userId) { - this.selectedUserIds = this.selectedUserIds.filter(id => id !== userId) - }, - onResults (results) { - this.userIds = results - }, - createList () { - // the API has two different endpoints for "creating a list with a name" - // and "updating the accounts on the list". - this.$store.dispatch('createList', { title: this.title }) - .then((list) => { - this.$store.dispatch('setListAccounts', { id: list.id, accountIds: this.selectedUserIds }) - this.$router.push({ name: 'lists-timeline', params: { id: list.id } }) - }) - } - } -} - -export default ListsNew diff --git a/src/components/lists_new/lists_new.vue b/src/components/lists_new/lists_new.vue deleted file mode 100644 index 4733bdde..00000000 --- a/src/components/lists_new/lists_new.vue +++ /dev/null @@ -1,95 +0,0 @@ -<template> - <div class="panel-default panel list-new"> - <div - ref="header" - class="panel-heading" - > - <button - class="button-unstyled go-back-button" - @click="goBack" - > - <FAIcon - size="lg" - icon="chevron-left" - /> - </button> - </div> - <div class="input-wrap"> - <input - ref="title" - v-model="title" - :placeholder="$t('lists.title')" - > - </div> - - <div class="member-list"> - <div - v-for="user in selectedUsers" - :key="user.id" - class="member" - > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> - </div> - <ListsUserSearch - @results="onResults" - /> - <div - v-for="user in users" - :key="user.id" - class="member" - > - <BasicUserCard - :user="user" - :class="isSelected(user) ? 'selected' : ''" - @click.capture.prevent="selectUser(user)" - /> - </div> - - <button - :disabled="title && title.length === 0" - class="btn button-default" - @click="createList" - > - {{ $t('lists.create') }} - </button> - </div> -</template> - -<script src="./lists_new.js"></script> - -<style lang="scss"> -@import '../../_variables.scss'; - -.list-new { - .search-icon { - margin-right: 0.3em; - } - - .member-list { - padding-bottom: 0.7rem; - } - - .basic-user-card:hover, - .basic-user-card.selected { - cursor: pointer; - background-color: var(--selectedPost, $fallback--lightBg); - } - - .go-back-button { - text-align: center; - line-height: 1; - height: 100%; - align-self: start; - width: var(--__panel-heading-height-inner); - } - - .btn { - margin: 0.5em; - } -} -</style> diff --git a/src/components/lists_timeline/lists_timeline.js b/src/components/lists_timeline/lists_timeline.js index 7534420d..c3f408bd 100644 --- a/src/components/lists_timeline/lists_timeline.js +++ b/src/components/lists_timeline/lists_timeline.js @@ -17,14 +17,14 @@ const ListsTimeline = { this.listId = route.params.id this.$store.dispatch('stopFetchingTimeline', 'list') this.$store.commit('clearTimeline', { timeline: 'list' }) - this.$store.dispatch('fetchList', { id: this.listId }) + this.$store.dispatch('fetchList', { listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) } } }, created () { this.listId = this.$route.params.id - this.$store.dispatch('fetchList', { id: this.listId }) + this.$store.dispatch('fetchList', { listId: this.listId }) this.$store.dispatch('startFetchingTimeline', { timeline: 'list', listId: this.listId }) }, unmounted () { diff --git a/src/components/lists_user_search/lists_user_search.js b/src/components/lists_user_search/lists_user_search.js index 5798841a..c92ec0ee 100644 --- a/src/components/lists_user_search/lists_user_search.js +++ b/src/components/lists_user_search/lists_user_search.js @@ -15,6 +15,7 @@ const ListsUserSearch = { components: { Checkbox }, + emits: ['loading', 'loadingDone', 'results'], data () { return { loading: false, @@ -33,12 +34,16 @@ const ListsUserSearch = { } this.loading = true + this.$emit('loading') this.userIds = [] this.$store.dispatch('search', { q: query, resolve: true, type: 'accounts', following: this.followingOnly }) .then(data => { - this.loading = false this.$emit('results', data.accounts.map(a => a.id)) }) + .finally(() => { + this.loading = false + this.$emit('loadingDone') + }) } } } diff --git a/src/components/lists_user_search/lists_user_search.vue b/src/components/lists_user_search/lists_user_search.vue index 03fb3ba6..8633170c 100644 --- a/src/components/lists_user_search/lists_user_search.vue +++ b/src/components/lists_user_search/lists_user_search.vue @@ -1,5 +1,5 @@ <template> - <div> + <div class="ListsUserSearch"> <div class="input-wrap"> <div class="input-search"> <FAIcon @@ -29,17 +29,19 @@ <style lang="scss"> @import '../../_variables.scss'; -.input-wrap { - display: flex; - margin: 0.7em 0.5em 0.7em 0.5em; +.ListsUserSearch { + .input-wrap { + display: flex; + margin: 0.7em 0.5em 0.7em 0.5em; - input { - width: 100%; + input { + width: 100%; + } } -} -.search-icon { - margin-right: 0.3em; + .search-icon { + margin-right: 0.3em; + } } </style> diff --git a/src/components/mention_link/mention_link.js b/src/components/mention_link/mention_link.js index 4a74fbe2..6515bd11 100644 --- a/src/components/mention_link/mention_link.js +++ b/src/components/mention_link/mention_link.js @@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { mapGetters, mapState } from 'vuex' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import UserAvatar from '../user_avatar/user_avatar.vue' +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { defineAsyncComponent } from 'vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -16,6 +17,7 @@ const MentionLink = { name: 'MentionLink', components: { UserAvatar, + UnicodeDomainIndicator, UserPopover: defineAsyncComponent(() => import('../user_popover/user_popover.vue')) }, props: { diff --git a/src/components/mention_link/mention_link.vue b/src/components/mention_link/mention_link.vue index 3af502ef..869a3257 100644 --- a/src/components/mention_link/mention_link.vue +++ b/src/components/mention_link/mention_link.vue @@ -47,6 +47,9 @@ class="serverName" :class="{ '-faded': shouldFadeDomain }" v-html="'@' + serverName" + /><UnicodeDomainIndicator + v-if="shouldShowFullUserName" + :user="user" /> </span> <span diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index 877d52a9..af47f032 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -2,6 +2,7 @@ import SideDrawer from '../side_drawer/side_drawer.vue' import Notifications from '../notifications/notifications.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' +import NavigationPins from 'src/components/navigation/navigation_pins.vue' import { mapGetters } from 'vuex' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -19,7 +20,8 @@ library.add( const MobileNav = { components: { SideDrawer, - Notifications + Notifications, + NavigationPins }, data: () => ({ notificationsCloseGesture: undefined, @@ -47,7 +49,10 @@ const MobileNav = { isChat () { return this.$route.name === 'chat' }, - ...mapGetters(['unreadChatCount']) + ...mapGetters(['unreadChatCount']), + chatsPinned () { + return new Set(this.$store.state.serverSideStorage.prefsStorage.collections.pinnedNavItems).has('chats') + } }, methods: { toggleMobileSidebar () { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index 949cf17e..9152879c 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -17,20 +17,12 @@ icon="bars" /> <div - v-if="unreadChatCount" + v-if="unreadChatCount && !chatsPinned" class="alert-dot" /> </button> - <router-link - v-if="!hideSitename" - class="site-name" - :to="{ name: 'root' }" - active-class="home" - > - {{ sitename }} - </router-link> - </div> - <div class="item right"> + <NavigationPins class="pins" /> + </div> <div class="item right"> <button v-if="currentUser" class="button-unstyled mobile-nav-button" @@ -94,6 +86,7 @@ grid-template-columns: 2fr auto; width: 100%; box-sizing: border-box; + a { color: var(--topBarLink, $fallback--link); } @@ -178,13 +171,20 @@ } } + .pins { + flex: 1; + + .pinned-item { + flex-grow: 1; + } + } + .mobile-notifications { margin-top: 50px; width: 100vw; height: calc(100vh - var(--navbar-height)); overflow-x: hidden; overflow-y: scroll; - color: $fallback--text; color: var(--text, $fallback--text); background-color: $fallback--bg; @@ -194,14 +194,17 @@ padding: 0; border-radius: 0; box-shadow: none; + .panel { border-radius: 0; margin: 0; box-shadow: none; } - .panel:after { + + .panel::after { border-radius: 0; } + .panel .panel-heading { border-radius: 0; box-shadow: none; diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js index ecf79b64..f7f96cd6 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.js +++ b/src/components/mobile_post_status_button/mobile_post_status_button.js @@ -10,7 +10,8 @@ library.add( const HIDDEN_FOR_PAGES = new Set([ 'chats', - 'chat' + 'chat', + 'lists-edit' ]) const MobilePostStatusButton = { diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index abeff6bf..b54f2fa2 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,6 +1,10 @@ -import TimelineMenuContent from '../timeline_menu/timeline_menu_content.vue' -import ListsMenuContent from '../lists_menu/lists_menu_content.vue' +import ListsMenuContent from 'src/components/lists_menu/lists_menu_content.vue' import { mapState, mapGetters } from 'vuex' +import { TIMELINES, ROOT_ITEMS } from 'src/components/navigation/navigation.js' +import { filterNavigation } from 'src/components/navigation/filter.js' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +import NavigationPins from 'src/components/navigation/navigation_pins.vue' +import Checkbox from 'src/components/checkbox/checkbox.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { @@ -30,21 +34,23 @@ library.add( faStream, faList ) - const NavPanel = { + props: ['forceExpand', 'forceEditMode'], created () { - if (this.currentUser && this.currentUser.locked) { - this.$store.dispatch('startFetchingFollowRequests') - } }, components: { - TimelineMenuContent, - ListsMenuContent + ListsMenuContent, + NavigationEntry, + NavigationPins, + Checkbox }, data () { return { + editMode: false, showTimelines: false, - showLists: false + showLists: false, + timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })), + rootList: Object.entries(ROOT_ITEMS).map(([k, v]) => ({ ...v, name: k })) } }, methods: { @@ -53,19 +59,62 @@ const NavPanel = { }, toggleLists () { this.showLists = !this.showLists + }, + toggleEditMode () { + this.editMode = !this.editMode + }, + toggleCollapse () { + this.$store.commit('setPreference', { path: 'simple.collapseNav', value: !this.collapsed }) + this.$store.dispatch('pushServerSideStorage') + }, + isPinned (item) { + return this.pinnedItems.has(item) + }, + togglePin (item) { + if (this.isPinned(item)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value: item }) + } + this.$store.dispatch('pushServerSideStorage') } }, computed: { - listsNavigation () { - return this.$store.getters.mergedConfig.listsNavigation - }, ...mapState({ currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, federating: state => state.instance.federating, - pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems), + collapsed: state => state.serverSideStorage.prefsStorage.simple.collapseNav }), + timelinesItems () { + return filterNavigation( + Object + .entries({ ...TIMELINES }) + .map(([k, v]) => ({ ...v, name: k })), + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ) + }, + rootItems () { + return filterNavigation( + Object + .entries({ ...ROOT_ITEMS }) + .map(([k, v]) => ({ ...v, name: k })), + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ) + }, ...mapGetters(['unreadChatCount']) } } diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 9322e233..7373ca63 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -1,135 +1,99 @@ <template> <div class="NavPanel"> <div class="panel panel-default"> - <ul> - <li v-if="currentUser || !privateMode"> - <button - class="button-unstyled menu-item" - @click="toggleTimelines" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="stream" - />{{ $t("nav.timelines") }} - <FAIcon - class="timelines-chevron" - fixed-width - :icon="showTimelines ? 'chevron-up' : 'chevron-down'" - /> - </button> - <div - v-show="showTimelines" - class="timelines-background" - > - <TimelineMenuContent class="timelines" /> - </div> - </li> - <li v-if="currentUser && listsNavigation"> - <button - class="button-unstyled menu-item" - @click="toggleLists" - > - <router-link - :to="{ name: 'lists' }" - @click.stop - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="list" - />{{ $t("nav.lists") }} - </router-link> - <FAIcon - class="timelines-chevron" - fixed-width - :icon="showLists ? 'chevron-up' : 'chevron-down'" + <div + v-if="!forceExpand" + class="panel-heading nav-panel-heading" + > + <NavigationPins :limit="6" /> + <div class="spacer" /> + <button + class="button-unstyled" + @click="toggleCollapse" + > + <FAIcon + class="timelines-chevron" + fixed-width + :icon="collapsed ? 'chevron-down' : 'chevron-up'" + /> + </button> + </div> + <ul + v-if="!collapsed || forceExpand" + class="panel-body" + > + <NavigationEntry + v-if="currentUser || !privateMode" + :show-pin="false" + :item="{ icon: 'stream', label: 'nav.timelines' }" + :aria-expanded="showTimelines ? 'true' : 'false'" + @click="toggleTimelines" + > + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showTimelines ? 'chevron-up' : 'chevron-down'" + /> + </NavigationEntry> + <div + v-show="showTimelines" + class="timelines-background" + > + <div class="timelines"> + <NavigationEntry + v-for="item in timelinesItems" + :key="item.name" + :show-pin="editMode || forceEditMode" + :item="item" /> - </button> - <div - v-show="showLists" - class="timelines-background" - > - <ListsMenuContent class="timelines" /> </div> - </li> - <li v-if="currentUser && !listsNavigation"> + </div> + <NavigationEntry + v-if="currentUser" + :show-pin="false" + :item="{ icon: 'list', label: 'nav.lists' }" + :aria-expanded="showLists ? 'true' : 'false'" + @click="toggleLists" + > <router-link + :title="$t('lists.manage_lists')" + class="extra-button" :to="{ name: 'lists' }" @click.stop > - <button - class="button-unstyled menu-item" - @click="toggleLists" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="list" - />{{ $t("nav.lists") }} - </button> - </router-link> - </li> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'interactions', params: { username: currentUser.screen_name } }" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="bell" - />{{ $t("nav.interactions") }} - </router-link> - </li> - <li v-if="currentUser && pleromaChatMessagesAvailable"> - <router-link - class="menu-item" - :to="{ name: 'chats', params: { username: currentUser.screen_name } }" - > - <div - v-if="unreadChatCount" - class="badge badge-notification" - > - {{ unreadChatCount }} - </div> - <FAIcon - fixed-width - class="fa-scale-110" - icon="comments" - />{{ $t("nav.chats") }} - </router-link> - </li> - <li v-if="currentUser && currentUser.locked"> - <router-link - class="menu-item" - :to="{ name: 'friend-requests' }" - > <FAIcon + class="extra-button" fixed-width - class="fa-scale-110" - icon="user-plus" - />{{ $t("nav.friend_requests") }} - <span - v-if="followRequestCount > 0" - class="badge badge-notification" - > - {{ followRequestCount }} - </span> - </router-link> - </li> - <li> - <router-link - class="menu-item" - :to="{ name: 'about' }" - > - <FAIcon - fixed-width - class="fa-scale-110" - icon="info-circle" - />{{ $t("nav.about") }} + icon="wrench" + /> </router-link> - </li> + <FAIcon + class="timelines-chevron" + fixed-width + :icon="showLists ? 'chevron-up' : 'chevron-down'" + /> + </NavigationEntry> + <div + v-show="showLists" + class="timelines-background" + > + <ListsMenuContent + :show-pin="editMode || forceEditMode" + class="timelines" + /> + </div> + <NavigationEntry + v-for="item in rootItems" + :key="item.name" + :show-pin="editMode || forceEditMode" + :item="item" + /> + <NavigationEntry + v-if="!forceEditMode && currentUser" + :show-pin="false" + :item="{ label: editMode ? $t('nav.edit_finish') : $t('nav.edit_pinned'), icon: editMode ? 'check' : 'wrench' }" + @click="toggleEditMode" + /> </ul> </div> </div> @@ -157,7 +121,6 @@ border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); - padding: 0; } > li { @@ -180,46 +143,9 @@ border: none; } - .menu-item { - display: block; - box-sizing: border-box; - height: 3.5em; - line-height: 3.5em; - padding: 0 1em; - width: 100%; - color: $fallback--link; - color: var(--link, $fallback--link); - - &:hover { - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--link; - color: var(--selectedMenuText, $fallback--link); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - } - - &.router-link-active { - font-weight: bolder; - background-color: $fallback--lightBg; - background-color: var(--selectedMenu, $fallback--lightBg); - color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - --faint: var(--selectedMenuFaintText, $fallback--faint); - --faintLink: var(--selectedMenuFaintLink, $fallback--faint); - --lightText: var(--selectedMenuLightText, $fallback--lightText); - --icon: var(--selectedMenuIcon, $fallback--icon); - - &:hover { - text-decoration: underline; - } - } - } - .timelines-chevron { margin-left: 0.8em; + margin-right: 0.8em; font-size: 1.1em; } @@ -227,7 +153,7 @@ padding: 0 0 0 0.6em; background-color: $fallback--lightBg; background-color: var(--selectedMenu, $fallback--lightBg); - border-top: 1px solid; + border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); } @@ -237,14 +163,9 @@ background-color: var(--bg, $fallback--bg); } - .fa-scale-110 { - margin-right: 0.8em; - } - - .badge { - position: absolute; - right: 0.6rem; - top: 1.25em; + .nav-panel-heading { + // breaks without a unit + --panel-heading-height-padding: 0em; } } </style> diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js new file mode 100644 index 00000000..31b55486 --- /dev/null +++ b/src/components/navigation/filter.js @@ -0,0 +1,18 @@ +export const filterNavigation = (list = [], { hasChats, 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 && !(anon || anonRoute)) return false + if ((!currentUser || !currentUser.locked) && set.has('lockedUser')) return false + if (!hasChats && set.has('chats')) return false + return true + }) +} + +export const getListEntries = state => state.lists.allLists.map(list => ({ + name: 'list-' + list.id, + routeObject: { name: 'lists-timeline', params: { id: list.id } }, + labelRaw: list.title, + iconLetter: list.title[0] +})) diff --git a/src/components/navigation/navigation.js b/src/components/navigation/navigation.js new file mode 100644 index 00000000..f66dd981 --- /dev/null +++ b/src/components/navigation/navigation.js @@ -0,0 +1,75 @@ +export const USERNAME_ROUTES = new Set([ + 'bookmarks', + 'dms', + 'interactions', + 'notifications', + 'chat', + 'chats', + 'user-profile' +]) + +export const TIMELINES = { + home: { + route: 'friends', + icon: 'home', + label: 'nav.home_timeline', + criteria: ['!private'] + }, + public: { + route: 'public-timeline', + anon: true, + icon: 'users', + label: 'nav.public_tl', + criteria: ['!private'] + }, + twkn: { + route: 'public-external-timeline', + anon: true, + icon: 'globe', + label: 'nav.twkn', + criteria: ['!private', 'federating'] + }, + bookmarks: { + route: 'bookmarks', + icon: 'bookmark', + label: 'nav.bookmarks' + }, + favorites: { + routeObject: { name: 'user-profile', query: { tab: 'favorites' } }, + icon: 'star', + label: 'user_card.favorites' + }, + dms: { + route: 'dms', + icon: 'envelope', + label: 'nav.dms' + } +} + +export const ROOT_ITEMS = { + interactions: { + route: 'interactions', + icon: 'bell', + label: 'nav.interactions' + }, + chats: { + route: 'chats', + icon: 'comments', + label: 'nav.chats', + badgeGetter: 'unreadChatCount', + criteria: ['chats'] + }, + friendRequests: { + route: 'friend-requests', + icon: 'user-plus', + label: 'nav.friend_requests', + criteria: ['lockedUser'], + badgeGetter: 'followRequestCount' + }, + about: { + route: 'about', + anon: true, + icon: 'info-circle', + label: 'nav.about' + } +} diff --git a/src/components/navigation/navigation_entry.js b/src/components/navigation/navigation_entry.js new file mode 100644 index 00000000..81cc936a --- /dev/null +++ b/src/components/navigation/navigation_entry.js @@ -0,0 +1,51 @@ +import { mapState } from 'vuex' +import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import OptionalRouterLink from 'src/components/optional_router_link/optional_router_link.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faThumbtack } from '@fortawesome/free-solid-svg-icons' + +library.add(faThumbtack) + +const NavigationEntry = { + props: ['item', 'showPin'], + components: { + OptionalRouterLink + }, + methods: { + isPinned (value) { + return this.pinnedItems.has(value) + }, + togglePin (value) { + if (this.isPinned(value)) { + this.$store.commit('removeCollectionPreference', { path: 'collections.pinnedNavItems', value }) + } else { + this.$store.commit('addCollectionPreference', { path: 'collections.pinnedNavItems', value }) + } + this.$store.dispatch('pushServerSideStorage') + } + }, + computed: { + routeTo () { + if (!this.item.route && !this.item.routeObject) return null + let route + if (this.item.routeObject) { + route = this.item.routeObject + } else { + route = { name: (this.item.anon || this.currentUser) ? this.item.route : this.item.anonRoute } + } + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: this.currentUser.screen_name, name: this.currentUser.screen_name } + } + return route + }, + getters () { + return this.$store.getters + }, + ...mapState({ + currentUser: state => state.users.currentUser, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + }) + } +} + +export default NavigationEntry diff --git a/src/components/navigation/navigation_entry.vue b/src/components/navigation/navigation_entry.vue new file mode 100644 index 00000000..f4d53836 --- /dev/null +++ b/src/components/navigation/navigation_entry.vue @@ -0,0 +1,133 @@ +<template> + <OptionalRouterLink + v-slot="{ isActive, href, navigate } = {}" + ass="ass" + :to="routeTo" + > + <li + class="NavigationEntry menu-item" + :class="{ '-active': isActive }" + v-bind="$attrs" + > + <component + :is="routeTo ? 'a' : 'button'" + class="main-link button-unstyled" + :href="href" + @click="navigate" + > + <span> + <FAIcon + v-if="item.icon" + fixed-width + class="fa-scale-110 menu-icon" + :icon="item.icon" + /> + </span> + <span + v-if="item.iconLetter" + class="icon iconLetter fa-scale-110 menu-icon" + >{{ item.iconLetter }} + </span> + <span class="label"> + {{ item.labelRaw || $t(item.label) }} + </span> + </component> + <slot /> + <div + v-if="item.badgeGetter && getters[item.badgeGetter]" + class="badge badge-notification" + > + {{ getters[item.badgeGetter] }} + </div> + <button + v-if="showPin && currentUser" + type="button" + class="button-unstyled extra-button" + :title="$t(isPinned ? 'general.unpin' : 'general.pin' )" + :aria-pressed="!!isPinned" + @click.stop.prevent="togglePin(item.name)" + > + <FAIcon + v-if="showPin && currentUser" + fixed-width + class="fa-scale-110" + :class="{ 'veryfaint': !isPinned(item.name) }" + :transform="!isPinned(item.name) ? 'rotate-45' : ''" + icon="thumbtack" + /> + </button> + </li> + </OptionalRouterLink> +</template> + +<script src="./navigation_entry.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.NavigationEntry { + display: flex; + box-sizing: border-box; + align-items: baseline; + height: 3.5em; + line-height: 3.5em; + padding: 0 1em; + width: 100%; + color: $fallback--link; + color: var(--link, $fallback--link); + + .timelines-chevron { + margin-right: 0; + } + + .main-link { + flex: 1; + } + + .menu-icon { + margin-right: 0.8em; + } + + .extra-button { + width: 3em; + text-align: center; + + &:last-child { + margin-right: -0.8em; + } + } + + &:hover { + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--link; + color: var(--selectedMenuText, $fallback--link); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + + .menu-icon { + --icon: var(--text, $fallback--icon); + } + } + + &.-active { + font-weight: bolder; + background-color: $fallback--lightBg; + background-color: var(--selectedMenu, $fallback--lightBg); + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + --faint: var(--selectedMenuFaintText, $fallback--faint); + --faintLink: var(--selectedMenuFaintLink, $fallback--faint); + --lightText: var(--selectedMenuLightText, $fallback--lightText); + + .menu-icon { + --icon: var(--text, $fallback--icon); + } + + &:hover { + text-decoration: underline; + } + } +} +</style> diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js new file mode 100644 index 00000000..57b8d589 --- /dev/null +++ b/src/components/navigation/navigation_pins.js @@ -0,0 +1,88 @@ +import { mapState } from 'vuex' +import { TIMELINES, ROOT_ITEMS, USERNAME_ROUTES } from 'src/components/navigation/navigation.js' +import { getListEntries, filterNavigation } from 'src/components/navigation/filter.js' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faComments, + faBell, + faInfoCircle, + faStream, + faList +) + +const NavPanel = { + props: ['limit'], + methods: { + getRouteTo (item) { + if (item.routeObject) { + return item.routeObject + } + const route = { name: (item.anon || this.currentUser) ? item.route : item.anonRoute } + if (USERNAME_ROUTES.has(route.name)) { + route.params = { username: this.currentUser.screen_name } + } + return route + } + }, + computed: { + getters () { + return this.$store.getters + }, + ...mapState({ + lists: getListEntries, + currentUser: state => state.users.currentUser, + followRequestCount: state => state.api.followRequests.length, + privateMode: state => state.instance.private, + federating: state => state.instance.federating, + pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable, + pinnedItems: state => new Set(state.serverSideStorage.prefsStorage.collections.pinnedNavItems) + }), + pinnedList () { + if (!this.currentUser) { + return [ + { ...TIMELINES.public, name: 'public' }, + { ...TIMELINES.twkn, name: 'twkn' }, + { ...ROOT_ITEMS.about, name: 'about' } + ] + } + return filterNavigation( + [ + ...Object + .entries({ ...TIMELINES }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })), + ...this.lists.filter((k) => this.pinnedItems.has(k.name)), + ...Object + .entries({ ...ROOT_ITEMS }) + .filter(([k]) => this.pinnedItems.has(k)) + .map(([k, v]) => ({ ...v, name: k })) + ], + { + hasChats: this.pleromaChatMessagesAvailable, + isFederating: this.federating, + isPrivate: this.privateMode, + currentUser: this.currentUser + } + ).slice(0, this.limit) + } + } +} + +export default NavPanel diff --git a/src/components/navigation/navigation_pins.vue b/src/components/navigation/navigation_pins.vue new file mode 100644 index 00000000..5b3fa6f4 --- /dev/null +++ b/src/components/navigation/navigation_pins.vue @@ -0,0 +1,76 @@ +<template> + <span class="NavigationPins"> + <router-link + v-for="item in pinnedList" + :key="item.name" + class="pinned-item" + :to="getRouteTo(item)" + :title="item.labelRaw || $t(item.label)" + > + <FAIcon + v-if="item.icon" + fixed-width + :icon="item.icon" + /> + <span + v-if="item.iconLetter" + class="iconLetter fa-scale-110 fa-old-padding" + >{{ item.iconLetter }}</span> + <div + v-if="item.badgeGetter && getters[item.badgeGetter]" + class="alert-dot" + /> + </router-link> + </span> +</template> + +<script src="./navigation_pins.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; +.NavigationPins { + display: flex; + flex-wrap: wrap; + overflow: hidden; + height: 100%; + + .alert-dot { + border-radius: 100%; + height: 0.5em; + width: 0.5em; + position: absolute; + right: calc(50% - 0.25em); + top: calc(50% - 0.25em); + margin-left: 6px; + margin-top: -6px; + background-color: $fallback--cRed; + background-color: var(--badgeNotification, $fallback--cRed); + } + + .pinned-item { + position: relative; + flex: 1 0 3em; + min-width: 2em; + text-align: center; + overflow: visible; + box-sizing: border-box; + height: 100%; + + & .svg-inline--fa, + & .iconLetter { + margin: 0; + } + + &.router-link-active { + color: $fallback--text; + color: var(--selectedMenuText, $fallback--text); + border-bottom: 4px solid; + + & .svg-inline--fa, + & .iconLetter { + color: inherit; + } + } + } +} +</style> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index f60a1a6a..ddba560e 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue' import UserCard from '../user_card/user_card.vue' import Timeago from '../timeago/timeago.vue' import Report from '../report/report.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import UserPopover from '../user_popover/user_popover.vue' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' @@ -50,7 +51,8 @@ const Notification = { Status, Report, RichContent, - UserPopover + UserPopover, + UserLink }, methods: { toggleUserExpanded () { diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 6a5417b3..26b174ff 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -11,9 +11,10 @@ class="Notification container -muted" > <small> - <router-link :to="userProfileLink"> - {{ notification.from_profile.screen_name_ui }} - </router-link> + <user-link + :user="notification.from_profile" + :at="false" + /> </small> <button class="button-unstyled unmute" @@ -174,12 +175,10 @@ v-if="notification.type === 'follow' || notification.type === 'follow_request'" class="follow-text" > - <router-link - :to="userProfileLink" + <user-link class="follow-name" - > - @{{ notification.from_profile.screen_name_ui }} - </router-link> + :user="notification.from_profile" + /> <div v-if="notification.type === 'follow_request'" style="white-space: nowrap;" @@ -210,9 +209,9 @@ v-else-if="notification.type === 'move'" class="move-text" > - <router-link :to="targetUserProfileLink"> - @{{ notification.target.screen_name_ui }} - </router-link> + <user-link + :user="notification.target" + /> </div> <Report v-else-if="notification.type === 'pleroma:report'" diff --git a/src/components/optional_router_link/optional_router_link.vue b/src/components/optional_router_link/optional_router_link.vue new file mode 100644 index 00000000..d56ad268 --- /dev/null +++ b/src/components/optional_router_link/optional_router_link.vue @@ -0,0 +1,23 @@ +<template> + <!-- eslint-disable vue/no-multiple-template-root --> + <router-link + v-if="to" + v-slot="props" + :to="to" + custom + > + <slot + v-bind="props" + /> + </router-link> + <slot + v-else + v-bind="{}" + /> +</template> + +<script> +export default { + props: ['to'] +} +</script> diff --git a/src/components/popover/popover.js b/src/components/popover/popover.js index d2af59fe..dd332c35 100644 --- a/src/components/popover/popover.js +++ b/src/components/popover/popover.js @@ -4,7 +4,7 @@ const Popover = { // Action to trigger popover: either 'hover' or 'click' trigger: String, - // Either 'top' or 'bottom' + // 'top', 'bottom', 'left', 'right' placement: String, // Takes object with properties 'x' and 'y', values of these can be @@ -84,6 +84,8 @@ const Popover = { const anchorStyle = getComputedStyle(anchorEl) const topPadding = parseFloat(anchorStyle.paddingTop) const bottomPadding = parseFloat(anchorStyle.paddingBottom) + const rightPadding = parseFloat(anchorStyle.paddingRight) + const leftPadding = parseFloat(anchorStyle.paddingLeft) // Screen position of the origin point for popover = center of the anchor const origin = { @@ -170,7 +172,7 @@ const Popover = { if (overlayCenter) { translateX = origin.x + horizOffset translateY = origin.y + vertOffset - } else { + } else if (this.placement !== 'right' && this.placement !== 'left') { // Default to whatever user wished with placement prop let usingTop = this.placement !== 'bottom' @@ -189,6 +191,25 @@ const Popover = { const xOffset = (this.offset && this.offset.x) || 0 translateX = origin.x + horizOffset + xOffset + } else { + // Default to whatever user wished with placement prop + let usingRight = this.placement !== 'left' + + // Handle special cases, first force to displaying on top if there's not space on bottom, + // regardless of what placement value was. Then check if there's not space on top, and + // force to bottom, again regardless of what placement value was. + const rightBoundary = origin.x - anchorWidth * 0.5 + (this.removePadding ? rightPadding : 0) + const leftBoundary = origin.x + anchorWidth * 0.5 - (this.removePadding ? leftPadding : 0) + if (leftBoundary + content.offsetWidth > xBounds.max) usingRight = true + if (rightBoundary - content.offsetWidth < xBounds.min) usingRight = false + + const xOffset = (this.offset && this.offset.x) || 0 + translateX = usingRight + ? rightBoundary - xOffset - content.offsetWidth + : leftBoundary + xOffset + + const yOffset = (this.offset && this.offset.y) || 0 + translateY = origin.y + vertOffset + yOffset } this.styles = { diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue index bd59cade..623af8d2 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -126,6 +126,13 @@ } } + &.-has-submenu { + .chevron-icon { + margin-right: 0.25rem; + margin-left: 2rem; + } + } + &:active, &:hover { background-color: $fallback--lightBg; background-color: var(--selectedMenuPopover, $fallback--lightBg); diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index c0d80b20..77f73d04 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -55,6 +55,14 @@ const pxStringToNumber = (str) => { const PostStatusForm = { props: [ + 'statusId', + 'statusText', + 'statusIsSensitive', + 'statusPoll', + 'statusFiles', + 'statusMediaDescriptions', + 'statusScope', + 'statusContentType', 'replyTo', 'repliedUser', 'attentions', @@ -62,6 +70,7 @@ const PostStatusForm = { 'subject', 'disableSubject', 'disableScopeSelector', + 'disableVisibilitySelector', 'disableNotice', 'disableLockWarning', 'disablePolls', @@ -125,22 +134,38 @@ const PostStatusForm = { const { postContentType: contentType, sensitiveByDefault } = this.$store.getters.mergedConfig + let statusParams = { + spoilerText: this.subject || '', + status: statusText, + nsfw: !!sensitiveByDefault, + files: [], + poll: {}, + mediaDescriptions: {}, + visibility: scope, + contentType + } + + if (this.statusId) { + const statusContentType = this.statusContentType || contentType + statusParams = { + spoilerText: this.subject || '', + status: this.statusText || '', + nsfw: this.statusIsSensitive || !!sensitiveByDefault, + files: this.statusFiles || [], + poll: this.statusPoll || {}, + mediaDescriptions: this.statusMediaDescriptions || {}, + visibility: this.statusScope || scope, + contentType: statusContentType + } + } + return { dropFiles: [], uploadingFiles: false, error: null, posting: false, highlighted: 0, - newStatus: { - spoilerText: this.subject || '', - status: statusText, - nsfw: !!sensitiveByDefault, - files: [], - poll: {}, - mediaDescriptions: {}, - visibility: scope, - contentType - }, + newStatus: statusParams, caret: 0, pollFormVisible: false, showDropIcon: 'hide', @@ -236,6 +261,9 @@ const PostStatusForm = { uploadFileLimitReached () { return this.newStatus.files.length >= this.fileLimit }, + isEdit () { + return typeof this.statusId !== 'undefined' && this.statusId.trim() !== '' + }, ...mapGetters(['mergedConfig']), ...mapState({ mobileLayout: state => state.interface.mobileLayout diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 62613bd1..f65058f4 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -67,6 +67,13 @@ <span v-else>{{ $t('post_status.direct_warning_to_all') }}</span> </p> <div + v-if="isEdit" + class="visibility-notice edit-warning" + > + <p>{{ $t('post_status.edit_remote_warning') }}</p> + <p>{{ $t('post_status.edit_unsupported_warning') }}</p> + </div> + <div v-if="!disablePreview" class="preview-heading faint" > @@ -170,6 +177,7 @@ class="visibility-tray" > <scope-selector + v-if="!disableVisibilitySelector" :show-all="showAllScopes" :user-default="userDefaultScope" :original-scope="copyMessageScope" @@ -410,6 +418,16 @@ align-items: baseline; } + .visibility-notice.edit-warning { + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + } + .media-upload-icon, .poll-icon, .emoji-icon { font-size: 1.85em; line-height: 1.1; diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue index 222f57ba..199a7500 100644 --- a/src/components/search_bar/search_bar.vue +++ b/src/components/search_bar/search_bar.vue @@ -47,6 +47,8 @@ class="cancel-icon fa-scale-110 fa-old-padding" /> </button> + <span class="spacer" /> + <span class="spacer" /> </template> </div> </template> diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 50e2bc44..8561647b 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -102,11 +102,6 @@ </BooleanSetting> </li> <li> - <BooleanSetting path="listsNavigation"> - {{ $t('settings.lists_navigation') }} - </BooleanSetting> - </li> - <li> <h3>{{ $t('settings.columns') }}</h3> </li> <li> diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 913fa695..bb22446b 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -2,6 +2,7 @@ import { mapState, mapGetters } from 'vuex' import UserCard from '../user_card/user_card.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' +import { USERNAME_ROUTES } from 'src/components/navigation/navigation.js' import { library } from '@fortawesome/fontawesome-svg-core' import { faSignInAlt, @@ -15,6 +16,7 @@ import { faTachometerAlt, faCog, faInfoCircle, + faCompass, faList } from '@fortawesome/free-solid-svg-icons' @@ -30,6 +32,7 @@ library.add( faTachometerAlt, faCog, faInfoCircle, + faCompass, faList ) @@ -80,10 +83,16 @@ const SideDrawer = { return this.$store.state.instance.federating }, timelinesRoute () { + let name if (this.$store.state.interface.lastTimeline) { - return this.$store.state.interface.lastTimeline + name = this.$store.state.interface.lastTimeline + } + name = this.currentUser ? 'friends' : 'public-timeline' + if (USERNAME_ROUTES.has(name)) { + return { name, params: { username: this.currentUser.screen_name } } + } else { + return { name } } - return this.currentUser ? 'friends' : 'public-timeline' }, ...mapState({ pleromaChatMessagesAvailable: state => state.instance.pleromaChatMessagesAvailable diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index 7d9d36d7..cbeafdd2 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -47,7 +47,7 @@ v-if="currentUser || !privateMode" @click="toggleDrawer" > - <router-link :to="{ name: timelinesRoute }"> + <router-link :to="timelinesRoute"> <FAIcon fixed-width class="fa-scale-110 fa-old-padding" @@ -195,6 +195,18 @@ v-if="currentUser" @click="toggleDrawer" > + <router-link :to="{ name: 'edit-navigation' }"> + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="compass" + /> {{ $t("nav.edit_nav_mobile") }} + </router-link> + </li> + <li + v-if="currentUser" + @click="toggleDrawer" + > <button class="button-unstyled -link -fullwidth" @click="doLogout" diff --git a/src/components/status/status.js b/src/components/status/status.js index 384063a7..9a9bca7a 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -13,6 +13,7 @@ import StatusPopover from '../status_popover/status_popover.vue' import UserPopover from '../user_popover/user_popover.vue' import UserListPopover from '../user_list_popover/user_list_popover.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' +import UserLink from '../user_link/user_link.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionLink from 'src/components/mention_link/mention_link.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' @@ -115,7 +116,8 @@ const Status = { RichContent, MentionLink, MentionsLine, - UserPopover + UserPopover, + UserLink }, props: [ 'statusoid', @@ -393,6 +395,12 @@ const Status = { }, visibilityLocalized () { return this.$i18n.t('general.scope_in_timeline.' + this.status.visibility) + }, + isEdited () { + return this.status.edited_at !== null + }, + editingAvailable () { + return this.$store.state.instance.editingAvailable } }, methods: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index b3ad3818..ada9841e 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -156,7 +156,8 @@ margin-right: 0.2em; } - & .heading-reply-row { + & .heading-reply-row, + & .heading-edited-row { position: relative; align-content: baseline; font-size: 0.85em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 967a966c..82eb7ac6 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -25,9 +25,10 @@ class="fa-scale-110 fa-old-padding repeat-icon" icon="retweet" /> - <router-link :to="userProfileLink"> - {{ status.user.screen_name_ui }} - </router-link> + <user-link + :user="status.user" + :at="false" + /> </small> <small v-if="showReasonMutedThread" @@ -164,13 +165,12 @@ > {{ status.user.name }} </h4> - <router-link + <user-link class="account-name" :title="status.user.screen_name_ui" - :to="userProfileLink" - > - {{ status.user.screen_name_ui }} - </router-link> + :user="status.user" + :at="false" + /> <img v-if="!!(status.user && status.user.favicon)" class="status-favicon" @@ -327,6 +327,24 @@ class="mentions-line" /> </div> + <div + v-if="isEdited && editingAvailable && !isPreview" + class="heading-edited-row" + > + <i18n-t + keypath="status.edited_at" + tag="span" + > + <template #time> + <Timeago + template-key="time.in_past" + :time="status.edited_at" + :auto-update="60" + :long-format="true" + /> + </template> + </i18n-t> + </div> </div> <StatusContent diff --git a/src/components/status_history_modal/status_history_modal.js b/src/components/status_history_modal/status_history_modal.js new file mode 100644 index 00000000..3941a56f --- /dev/null +++ b/src/components/status_history_modal/status_history_modal.js @@ -0,0 +1,60 @@ +import { get } from 'lodash' +import Modal from '../modal/modal.vue' +import Status from '../status/status.vue' + +const StatusHistoryModal = { + components: { + Modal, + Status + }, + data () { + return { + statuses: [] + } + }, + computed: { + modalActivated () { + return this.$store.state.statusHistory.modalActivated + }, + params () { + return this.$store.state.statusHistory.params + }, + statusId () { + return this.params.id + }, + historyCount () { + return this.statuses.length + }, + history () { + return this.statuses + } + }, + watch: { + params (newVal, oldVal) { + const newStatusId = get(newVal, 'id') !== get(oldVal, 'id') + if (newStatusId) { + this.resetHistory() + } + + if (newStatusId || get(newVal, 'edited_at') !== get(oldVal, 'edited_at')) { + this.fetchStatusHistory() + } + } + }, + methods: { + resetHistory () { + this.statuses = [] + }, + fetchStatusHistory () { + this.$store.dispatch('fetchStatusHistory', this.params) + .then(data => { + this.statuses = data + }) + }, + closeModal () { + this.$store.dispatch('closeStatusHistoryModal') + } + } +} + +export default StatusHistoryModal diff --git a/src/components/status_history_modal/status_history_modal.vue b/src/components/status_history_modal/status_history_modal.vue new file mode 100644 index 00000000..990be35b --- /dev/null +++ b/src/components/status_history_modal/status_history_modal.vue @@ -0,0 +1,46 @@ +<template> + <Modal + v-if="modalActivated" + class="status-history-modal-view" + @backdropClicked="closeModal" + > + <div class="status-history-modal-panel panel"> + <div class="panel-heading"> + {{ $t('status.status_history') }} ({{ historyCount }}) + </div> + <div class="panel-body"> + <div + v-if="historyCount > 0" + class="history-body" + > + <status + v-for="status in history" + :key="status.id" + :statusoid="status" + :is-preview="true" + class="conversation-status status-fadein panel-body" + /> + </div> + </div> + </div> + </Modal> +</template> + +<script src="./status_history_modal.js"></script> + +<style lang="scss"> +.modal-view.status-history-modal-view { + align-items: flex-start; +} +.status-history-modal-panel { + flex-shrink: 0; + margin-top: 25%; + margin-bottom: 2em; + width: 100%; + max-width: 700px; + + @media (orientation: landscape) { + margin-top: 8%; + } +} +</style> diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss index 7a086b26..d930368c 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -17,6 +17,7 @@ overflow-x: auto; padding-top: 5px; flex-direction: row; + flex: 0 0 auto; &::after, &::before { content: ''; diff --git a/src/components/timeago/timeago.vue b/src/components/timeago/timeago.vue index 2b487dfd..b5f49515 100644 --- a/src/components/timeago/timeago.vue +++ b/src/components/timeago/timeago.vue @@ -3,7 +3,7 @@ :datetime="time" :title="localeDateString" > - {{ $tc(relativeTime.key, relativeTime.num, [relativeTime.num]) }} + {{ relativeTimeString }} </time> </template> @@ -13,7 +13,7 @@ import localeService from 'src/services/locale/locale.service.js' export default { name: 'Timeago', - props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold'], + props: ['time', 'autoUpdate', 'longFormat', 'nowThreshold', 'templateKey'], data () { return { relativeTime: { key: 'time.now', num: 0 }, @@ -26,6 +26,23 @@ export default { return typeof this.time === 'string' ? new Date(Date.parse(this.time)).toLocaleString(browserLocale) : this.time.toLocaleString(browserLocale) + }, + relativeTimeString () { + const timeString = this.$i18n.tc(this.relativeTime.key, this.relativeTime.num, [this.relativeTime.num]) + + if (typeof this.templateKey === 'string' && this.relativeTime.key !== 'time.now') { + return this.$i18n.t(this.templateKey, [timeString]) + } + + return timeString + } + }, + watch: { + time (newVal, oldVal) { + if (oldVal !== newVal) { + clearTimeout(this.interval) + this.refreshRelativeTimeObject() + } } }, created () { diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 627cafbb..f842240b 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -1,7 +1,10 @@ <template> <div :class="['Timeline', classes.root]"> <div :class="classes.header"> - <TimelineMenu v-if="!embedded" /> + <TimelineMenu + v-if="!embedded" + :timeline-name="timelineName" + /> <button v-if="showLoadButton" class="button-default loadmore-button" diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 5a67b0a5..d74fbf4e 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -1,6 +1,8 @@ import Popover from '../popover/popover.vue' -import TimelineMenuContent from './timeline_menu_content.vue' +import NavigationEntry from 'src/components/navigation/navigation_entry.vue' +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 { faChevronDown } from '@fortawesome/free-solid-svg-icons' @@ -22,11 +24,13 @@ export const timelineNames = () => { const TimelineMenu = { components: { Popover, - TimelineMenuContent + NavigationEntry, + ListsMenuContent }, data () { return { - isOpen: false + isOpen: false, + timelinesList: Object.entries(TIMELINES).map(([k, v]) => ({ ...v, name: k })) } }, created () { @@ -34,6 +38,12 @@ const TimelineMenu = { this.$store.dispatch('setLastTimeline', this.$route.name) } }, + computed: { + useListsMenu () { + const route = this.$route.name + return route === 'lists-timeline' + } + }, methods: { openMenu () { // $nextTick is too fast, animation won't play back but diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index c24b9d72..e7250282 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -10,7 +10,19 @@ @close="() => isOpen = false" > <template #content> - <TimelineMenuContent /> + <ListsMenuContent + v-if="useListsMenu" + :show-pin="false" + class="timelines" + /> + <ul v-else> + <NavigationEntry + v-for="item in timelinesList" + :key="item.name" + :show-pin="false" + :item="item" + /> + </ul> </template> <template #trigger> <span class="button-unstyled title timeline-menu-title"> @@ -138,8 +150,7 @@ background-color: $fallback--lightBg; background-color: var(--selectedMenu, $fallback--lightBg); color: $fallback--text; - color: var(--selectedMenuText, $fallback--text); - --faint: var(--selectedMenuFaintText, $fallback--faint); + color: var(--selectedMenuText, $fallback--text); --faint: var(--selectedMenuFaintText, $fallback--faint); --faintLink: var(--selectedMenuFaintLink, $fallback--faint); --lightText: var(--selectedMenuLightText, $fallback--lightText); --icon: var(--selectedMenuIcon, $fallback--icon); diff --git a/src/components/timeline_menu/timeline_menu_content.js b/src/components/timeline_menu/timeline_menu_content.js deleted file mode 100644 index 671570dd..00000000 --- a/src/components/timeline_menu/timeline_menu_content.js +++ /dev/null @@ -1,29 +0,0 @@ -import { mapState } from 'vuex' -import { library } from '@fortawesome/fontawesome-svg-core' -import { - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -} from '@fortawesome/free-solid-svg-icons' - -library.add( - faUsers, - faGlobe, - faBookmark, - faEnvelope, - faHome -) - -const TimelineMenuContent = { - computed: { - ...mapState({ - currentUser: state => state.users.currentUser, - privateMode: state => state.instance.private, - federating: state => state.instance.federating - }) - } -} - -export default TimelineMenuContent diff --git a/src/components/timeline_menu/timeline_menu_content.vue b/src/components/timeline_menu/timeline_menu_content.vue deleted file mode 100644 index 59e9e43c..00000000 --- a/src/components/timeline_menu/timeline_menu_content.vue +++ /dev/null @@ -1,66 +0,0 @@ -<template> - <ul> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'friends' }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="home" - />{{ $t("nav.home_timeline") }} - </router-link> - </li> - <li v-if="currentUser || !privateMode"> - <router-link - class="menu-item" - :to="{ name: 'public-timeline' }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="users" - />{{ $t("nav.public_tl") }} - </router-link> - </li> - <li v-if="federating && (currentUser || !privateMode)"> - <router-link - class="menu-item" - :to="{ name: 'public-external-timeline' }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="globe" - />{{ $t("nav.twkn") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'bookmarks'}" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="bookmark" - />{{ $t("nav.bookmarks") }} - </router-link> - </li> - <li v-if="currentUser"> - <router-link - class="menu-item" - :to="{ name: 'dms', params: { username: currentUser.screen_name } }" - > - <FAIcon - fixed-width - class="fa-scale-110 fa-old-padding " - icon="envelope" - />{{ $t("nav.dms") }} - </router-link> - </li> - </ul> -</template> - -<script src="./timeline_menu_content.js"></script> diff --git a/src/components/unicode_domain_indicator/unicode_domain_indicator.vue b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue new file mode 100644 index 00000000..8f35245f --- /dev/null +++ b/src/components/unicode_domain_indicator/unicode_domain_indicator.vue @@ -0,0 +1,26 @@ +<template> + <FAIcon + v-if="user && user.screen_name_ui_contains_non_ascii" + icon="code" + :title="$t('unicode_domain_indicator.tooltip')" + /> +</template> + +<script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCode +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCode +) + +const UnicodeDomainIndicator = { + props: { + user: Object + } +} + +export default UnicodeDomainIndicator +</script> diff --git a/src/components/update_notification/update_notification.js b/src/components/update_notification/update_notification.js index c389750d..06241688 100644 --- a/src/components/update_notification/update_notification.js +++ b/src/components/update_notification/update_notification.js @@ -41,7 +41,7 @@ const UpdateNotification = { return !this.$store.state.instance.disableUpdateNotification && this.$store.state.users.currentUser && this.$store.state.serverSideStorage.flagStorage.updateCounter < CURRENT_UPDATE_COUNTER && - !this.$store.state.serverSideStorage.flagStorage.dontShowUpdateNotifs + !this.$store.state.serverSideStorage.prefsStorage.simple.dontShowUpdateNotifs } }, methods: { @@ -51,7 +51,7 @@ const UpdateNotification = { neverShowAgain () { this.toggleShow() this.$store.commit('setFlag', { flag: 'updateCounter', value: CURRENT_UPDATE_COUNTER }) - this.$store.commit('setFlag', { flag: 'dontShowUpdateNotifs', value: 1 }) + this.$store.commit('setPreference', { path: 'simple.dontShowUpdateNotifs', value: true }) this.$store.dispatch('pushServerSideStorage') }, dismiss () { diff --git a/src/components/update_notification/update_notification.vue b/src/components/update_notification/update_notification.vue index d0e2499c..00841af2 100644 --- a/src/components/update_notification/update_notification.vue +++ b/src/components/update_notification/update_notification.vue @@ -60,7 +60,7 @@ <template #linkToArtist> <a target="_blank" - href="https://post.ebin.club/pipivovott" + href="https://post.ebin.club/users/pipivovott" >pipivovott</a> </template> </i18n-t> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 4c81e2c7..37458ba2 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -5,6 +5,7 @@ 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 Select from '../select/select.vue' +import UserLink from '../user_link/user_link.vue' import RichContent from 'src/components/rich_content/rich_content.jsx' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' @@ -134,7 +135,8 @@ export default { ProgressButton, FollowButton, Select, - RichContent + RichContent, + UserLink }, methods: { muteUser () { diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index ace89c51..b84ff27d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -106,13 +106,10 @@ </button> </div> <div class="bottom-line"> - <router-link + <user-link class="user-screen-name" - :title="user.screen_name_ui" - :to="userProfileLink(user)" - > - @{{ user.screen_name_ui }} - </router-link> + :user="user" + /> <template v-if="!hideBio"> <span v-if="user.deactivated" diff --git a/src/components/user_link/user_link.vue b/src/components/user_link/user_link.vue new file mode 100644 index 00000000..efd96e12 --- /dev/null +++ b/src/components/user_link/user_link.vue @@ -0,0 +1,38 @@ +<template> + <router-link + :title="user.screen_name_ui" + :to="userProfileLink(user)" + > + {{ at ? '@' : '' }}{{ user.screen_name_ui }}<UnicodeDomainIndicator + :user="user" + /> + </router-link> +</template> + +<script> +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' +import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' + +const UserLink = { + props: { + user: Object, + at: { + type: Boolean, + default: true + } + }, + components: { + UnicodeDomainIndicator + }, + methods: { + userProfileLink (user) { + return generateProfileLink( + user.id, user.screen_name, + this.$store.state.instance.restrictedNicknames + ) + } + } +} + +export default UserLink +</script> diff --git a/src/components/user_list_menu/user_list_menu.js b/src/components/user_list_menu/user_list_menu.js new file mode 100644 index 00000000..21996031 --- /dev/null +++ b/src/components/user_list_menu/user_list_menu.js @@ -0,0 +1,93 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faChevronRight } from '@fortawesome/free-solid-svg-icons' +import { mapState } from 'vuex' + +import DialogModal from '../dialog_modal/dialog_modal.vue' +import Popover from '../popover/popover.vue' + +library.add(faChevronRight) + +const UserListMenu = { + props: [ + 'user' + ], + data () { + return {} + }, + components: { + DialogModal, + Popover + }, + created () { + this.$store.dispatch('fetchUserInLists', this.user.id) + }, + computed: { + ...mapState({ + allLists: state => state.lists.allLists + }), + inListsSet () { + return new Set(this.user.inLists.map(x => x.id)) + }, + lists () { + if (!this.user.inLists) return [] + return this.allLists.map(list => ({ + ...list, + inList: this.inListsSet.has(list.id) + })) + } + }, + methods: { + toggleList (listId) { + if (this.inListsSet.has(listId)) { + this.$store.dispatch('removeListAccount', { accountId: this.user.id, listId }).then((response) => { + if (!response.ok) { return } + this.$store.dispatch('fetchUserInLists', this.user.id) + }) + } else { + this.$store.dispatch('addListAccount', { accountId: this.user.id, listId }).then((response) => { + if (!response.ok) { return } + this.$store.dispatch('fetchUserInLists', this.user.id) + }) + } + }, + toggleRight (right) { + const store = this.$store + if (this.user.rights[right]) { + store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => { + if (!response.ok) { return } + store.commit('updateRight', { user: this.user, right, value: false }) + }) + } else { + store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => { + if (!response.ok) { return } + store.commit('updateRight', { user: this.user, right, value: true }) + }) + } + }, + toggleActivationStatus () { + this.$store.dispatch('toggleActivationStatus', { user: this.user }) + }, + deleteUserDialog (show) { + this.showDeleteUserDialog = show + }, + deleteUser () { + const store = this.$store + const user = this.user + const { id, name } = user + store.state.api.backendInteractor.deleteUser({ user }) + .then(e => { + this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id) + const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile' + const isTargetUser = this.$route.params.name === name || this.$route.params.id === id + if (isProfile && isTargetUser) { + window.history.back() + } + }) + }, + setToggled (value) { + this.toggled = value + } + } +} + +export default UserListMenu diff --git a/src/components/user_list_menu/user_list_menu.vue b/src/components/user_list_menu/user_list_menu.vue new file mode 100644 index 00000000..06947ab7 --- /dev/null +++ b/src/components/user_list_menu/user_list_menu.vue @@ -0,0 +1,38 @@ +<template> + <div class="UserListMenu"> + <Popover + trigger="hover" + placement="left" + remove-padding + > + <template #content> + <div class="dropdown-menu"> + <button + v-for="list in lists" + :key="list.id" + class="button-default dropdown-item" + @click="toggleList(list.id)" + > + <span + class="menu-checkbox" + :class="{ 'menu-checkbox-checked': list.inList }" + /> + {{ list.title }} + </button> + </div> + </template> + <template #trigger> + <button class="btn button-default dropdown-item -has-submenu"> + {{ $t('lists.manage_lists') }} + <FAIcon + class="chevron-icon" + size="lg" + icon="chevron-right" + /> + </button> + </template> + </Popover> + </div> +</template> + +<script src="./user_list_menu.js"></script> diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js index e24eb9f7..046e0abd 100644 --- a/src/components/user_list_popover/user_list_popover.js +++ b/src/components/user_list_popover/user_list_popover.js @@ -1,5 +1,6 @@ import { defineAsyncComponent } from 'vue' import RichContent from 'src/components/rich_content/rich_content.jsx' +import UnicodeDomainIndicator from '../unicode_domain_indicator/unicode_domain_indicator.vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' @@ -15,6 +16,7 @@ const UserListPopover = { ], components: { RichContent, + UnicodeDomainIndicator, Popover: defineAsyncComponent(() => import('../popover/popover.vue')), UserAvatar: defineAsyncComponent(() => import('../user_avatar/user_avatar.vue')) }, diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue index a3ce54c3..635dc7f6 100644 --- a/src/components/user_list_popover/user_list_popover.vue +++ b/src/components/user_list_popover/user_list_popover.vue @@ -29,7 +29,7 @@ :emoji="user.emoji" /> <!-- eslint-enable vue/no-v-html --> - <span class="user-list-screen-name">{{ user.screen_name_ui }}</span> + <span class="user-list-screen-name">{{ user.screen_name_ui }}</span><UnicodeDomainIndicator :user="user" /> </div> </div> </template> diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js index 85ffc661..67fde084 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.js +++ b/src/components/user_reporting_modal/user_reporting_modal.js @@ -2,13 +2,15 @@ import Status from '../status/status.vue' import List from '../list/list.vue' import Checkbox from '../checkbox/checkbox.vue' import Modal from '../modal/modal.vue' +import UserLink from '../user_link/user_link.vue' const UserReportingModal = { components: { Status, List, Checkbox, - Modal + Modal, + UserLink }, data () { return { diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue index 429a66e2..8c42ab7b 100644 --- a/src/components/user_reporting_modal/user_reporting_modal.vue +++ b/src/components/user_reporting_modal/user_reporting_modal.vue @@ -5,9 +5,13 @@ > <div class="user-reporting-panel panel"> <div class="panel-heading"> - <div class="title"> - {{ $t('user_reporting.title', [user.screen_name_ui]) }} - </div> + <i18n-t + tag="div" + keypath="user_reporting.title" + class="title" + > + <UserLink :user="user" /> + </i18n-t> </div> <div class="panel-body"> <div class="user-reporting-panel-left"> |
