diff options
Diffstat (limited to 'src/components')
130 files changed, 2557 insertions, 829 deletions
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js index 6d345bc7..395d6685 100644 --- a/src/components/account_actions/account_actions.js +++ b/src/components/account_actions/account_actions.js @@ -1,6 +1,14 @@ import { mapState } from 'vuex' import ProgressButton from '../progress_button/progress_button.vue' import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisV +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEllipsisV +) const AccountActions = { props: [ diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue index 987e94b7..e3ae376e 100644 --- a/src/components/account_actions/account_actions.vue +++ b/src/components/account_actions/account_actions.vue @@ -1,5 +1,5 @@ <template> - <div class="account-actions"> + <div class="AccountActions"> <Popover trigger="click" placement="bottom" @@ -63,7 +63,10 @@ slot="trigger" class="btn btn-default ellipsis-button" > - <i class="icon-ellipsis trigger-button" /> + <FAIcon + class="icon" + icon="ellipsis-v" + /> </div> </Popover> </div> @@ -73,22 +76,22 @@ <style lang="scss"> @import '../../_variables.scss'; -.account-actions { - margin: 0 .8em; -} +.AccountActions { + button.dropdown-item { + margin-left: 0; + } -.account-actions button.dropdown-item { - margin-left: 0; -} + .ellipsis-button { + cursor: pointer; + width: 2.5em; + margin: -0.5em 0; + padding: 0.5em 0; + text-align: center; -.account-actions .trigger-button { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - opacity: .8; - cursor: pointer; - &:hover { - color: $fallback--text; - color: var(--text, $fallback--text); + &:not(:hover) .icon { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } } } </style> diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index cb31020d..e23fcb1b 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -3,6 +3,20 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import nsfwImage from '../../assets/nsfw.png' import fileTypeService from '../../services/file_type/file_type.service.js' import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faFile, + faMusic, + faImage, + faVideo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faFile, + faMusic, + faImage, + faVideo +) const Attachment = { props: [ @@ -39,10 +53,10 @@ const Attachment = { return this.attachment.description }, placeholderIconClass () { - if (this.type === 'image') return 'icon-picture' - if (this.type === 'video') return 'icon-video' - if (this.type === 'audio') return 'icon-music' - return 'icon-doc' + if (this.type === 'image') return 'image' + if (this.type === 'video') return 'video' + if (this.type === 'audio') return 'music' + return 'file' }, referrerpolicy () { return this.$store.state.instance.mediaProxyAvailable ? '' : 'no-referrer' diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 63e0ceba..f1fac2c8 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -12,7 +12,7 @@ :alt="attachment.description" :title="attachment.description" > - <span :class="placeholderIconClass" /> + <FAIcon :icon="placeholderIconClass" /> <b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }} </a> </div> @@ -28,7 +28,7 @@ :href="attachment.url" :alt="attachment.description" :title="attachment.description" - @click.prevent="toggleHidden" + @click.prevent.stop="toggleHidden" > <img :key="nsfwImage" @@ -36,9 +36,10 @@ :src="nsfwImage" :class="{'small': isSmall}" > - <i + <FAIcon v-if="type === 'video'" - class="play-icon icon-play-circled" + class="play-icon" + icon="play-circle" /> </a> <div @@ -80,10 +81,13 @@ class="video" :attachment="attachment" :controls="allowPlay" + @play="$emit('play')" + @pause="$emit('pause')" /> - <i + <FAIcon v-if="!allowPlay" - class="play-icon icon-play-circled" + class="play-icon" + icon="play-circle" /> </a> @@ -93,6 +97,8 @@ :alt="attachment.description" :title="attachment.description" controls + @play="$emit('play')" + @pause="$emit('pause')" /> <div @@ -138,6 +144,10 @@ white-space: nowrap; text-overflow: ellipsis; max-width: 100%; + + svg { + color: inherit; + } } .nsfw-placeholder { diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js index 803abf69..e57fcb91 100644 --- a/src/components/chat/chat.js +++ b/src/components/chat/chat.js @@ -5,11 +5,25 @@ import ChatMessage from '../chat_message/chat_message.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import ChatTitle from '../chat_title/chat_title.vue' import chatService from '../../services/chat_service/chat_service.js' -import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.js' +import { promiseInterval } from '../../services/promise_interval/promise_interval.js' +import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight, isScrollable } from './chat_layout_utils.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' +import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js' + +library.add( + faChevronDown, + faChevronLeft +) const BOTTOMED_OUT_OFFSET = 10 const JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET = 150 const SAFE_RESIZE_TIME_OFFSET = 100 +const MARK_AS_READ_DELAY = 1500 +const MAX_RETRIES = 10 const Chat = { components: { @@ -23,7 +37,8 @@ const Chat = { hoveredMessageChainId: undefined, lastScrollPosition: {}, scrollableContainerHeight: '100%', - errorLoadingChat: false + errorLoadingChat: false, + messageRetriers: {} } }, created () { @@ -93,7 +108,7 @@ const Chat = { const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET) this.$nextTick(() => { if (bottomedOutBeforeUpdate) { - this.scrollDown({ forceRead: !document.hidden }) + this.scrollDown() } }) }, @@ -199,7 +214,7 @@ const Chat = { this.$nextTick(() => { scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior }) }) - if (forceRead || this.newMessageCount > 0) { + if (forceRead) { this.readChat() } }, @@ -207,7 +222,10 @@ const Chat = { if (!(this.currentChatMessageService && this.currentChatMessageService.maxId)) { return } if (document.hidden) { return } const lastReadId = this.currentChatMessageService.maxId - this.$store.dispatch('readChat', { id: this.currentChat.id, lastReadId }) + this.$store.dispatch('readChat', { + id: this.currentChat.id, + lastReadId + }) }, bottomedOut (offset) { return isBottomedOut(this.$refs.scrollable, offset) @@ -224,12 +242,18 @@ const Chat = { } else if (this.bottomedOut(JUMP_TO_BOTTOM_BUTTON_VISIBILITY_OFFSET)) { this.jumpToBottomButtonVisible = false if (this.newMessageCount > 0) { - this.readChat() + // Use a delay before marking as read to prevent situation where new messages + // arrive just as you're leaving the view and messages that you didn't actually + // get to see get marked as read. + window.setTimeout(() => { + // Don't mark as read if the element doesn't exist, user has left chat view + if (this.$el) this.readChat() + }, MARK_AS_READ_DELAY) } } else { this.jumpToBottomButtonVisible = true } - }, 100), + }, 200), handleScrollUp (positionBeforeLoading) { const positionAfterLoading = getScrollPosition(this.$refs.scrollable) this.$refs.scrollable.scrollTo({ @@ -246,7 +270,7 @@ const Chat = { const fetchOlderMessages = !!maxId const sinceId = fetchLatest && chatMessageService.maxId - this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) + return this.backendInteractor.chatMessages({ id: chatId, maxId, sinceId }) .then((messages) => { // Clear the current chat in case we're recovering from a ws connection loss. if (isFirstFetch) { @@ -263,6 +287,14 @@ const Chat = { if (isFirstFetch) { this.updateScrollableContainerHeight() } + + // In vertical screens, the first batch of fetched messages may not always take the + // full height of the scrollable container. + // If this is the case, we want to fetch the messages until the scrollable container + // is fully populated so that the user has the ability to scroll up and load the history. + if (!isScrollable(this.$refs.scrollable) && messages.length > 0) { + this.fetchChat({ maxId: this.currentChatMessageService.minId }) + } }) }) }) @@ -287,46 +319,78 @@ const Chat = { }, doStartFetching () { this.$store.dispatch('startFetchingCurrentChat', { - fetcher: () => setInterval(() => this.fetchChat({ fetchLatest: true }), 5000) + fetcher: () => promiseInterval(() => this.fetchChat({ fetchLatest: true }), 5000) }) this.fetchChat({ isFirstFetch: true }) }, - sendMessage ({ status, media }) { + handleAttachmentPosting () { + this.$nextTick(() => { + this.handleResize() + // When the posting form size changes because of a media attachment, we need an extra resize + // to account for the potential delay in the DOM update. + setTimeout(() => { + this.updateScrollableContainerHeight() + }, SAFE_RESIZE_TIME_OFFSET) + this.scrollDown({ forceRead: true }) + }) + }, + sendMessage ({ status, media, idempotencyKey }) { const params = { id: this.currentChat.id, - content: status + content: status, + idempotencyKey } if (media[0]) { params.mediaId = media[0].id } - return this.backendInteractor.sendChatMessage(params) + const fakeMessage = buildFakeMessage({ + attachments: media, + chatId: this.currentChat.id, + content: status, + userId: this.currentUser.id, + idempotencyKey + }) + + this.$store.dispatch('addChatMessages', { + chatId: this.currentChat.id, + messages: [fakeMessage] + }).then(() => { + this.handleAttachmentPosting() + }) + + return this.doSendMessage({ params, fakeMessage, retriesLeft: MAX_RETRIES }) + }, + doSendMessage ({ params, fakeMessage, retriesLeft = MAX_RETRIES }) { + if (retriesLeft <= 0) return + + this.backendInteractor.sendChatMessage(params) .then(data => { this.$store.dispatch('addChatMessages', { chatId: this.currentChat.id, - messages: [data], - updateMaxId: false - }).then(() => { - this.$nextTick(() => { - this.handleResize() - // When the posting form size changes because of a media attachment, we need an extra resize - // to account for the potential delay in the DOM update. - setTimeout(() => { - this.updateScrollableContainerHeight() - }, SAFE_RESIZE_TIME_OFFSET) - this.scrollDown({ forceRead: true }) - }) + updateMaxId: false, + messages: [{ ...data, fakeId: fakeMessage.id }] }) return data }) .catch(error => { console.error('Error sending message', error) - return { - error: this.$t('chats.error_sending_message') + this.$store.dispatch('handleMessageError', { + chatId: this.currentChat.id, + fakeId: fakeMessage.id, + isRetry: retriesLeft !== MAX_RETRIES + }) + if ((error.statusCode >= 500 && error.statusCode < 600) || error.message === 'Failed to fetch') { + this.messageRetriers[fakeMessage.id] = setTimeout(() => { + this.doSendMessage({ params, fakeMessage, retriesLeft: retriesLeft - 1 }) + }, 1000 * (2 ** (MAX_RETRIES - retriesLeft))) } + return {} }) + + return Promise.resolve(fakeMessage) }, goBack () { this.$router.push({ name: 'chats', params: { username: this.currentUser.screen_name } }) diff --git a/src/components/chat/chat.scss b/src/components/chat/chat.scss index 012a1b1d..aef58495 100644 --- a/src/components/chat/chat.scss +++ b/src/components/chat/chat.scss @@ -25,7 +25,7 @@ min-height: 100%; margin: 0 0 0 0; border-radius: 10px 10px 0 0; - border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0 ; + border-radius: var(--panelRadius, 10px) var(--panelRadius, 10px) 0 0; &::after { border-radius: 0; @@ -58,12 +58,10 @@ .go-back-button { cursor: pointer; - margin-right: 1.4em; - - i { - display: flex; - align-items: center; - } + width: 28px; + text-align: center; + padding: 0.6em; + margin: -0.6em 0.6em -0.6em -0.6em; } .jump-to-bottom-button { @@ -78,7 +76,7 @@ display: flex; justify-content: center; align-items: center; - box-shadow: 0px 1px 1px rgba(0, 0, 0, 0.3), 0px 2px 4px rgba(0, 0, 0, 0.3); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), 0 2px 4px rgba(0, 0, 0, 0.3); z-index: 10; transition: 0.35s all; transition-timing-function: cubic-bezier(0, 1, 0.5, 1); @@ -140,11 +138,21 @@ } .chat-view-heading { + box-sizing: border-box; position: static; z-index: 9999; top: 0; margin-top: 0; border-radius: 0; + + /* This practically overlays the panel heading color over panel background + * color. This is needed because we allow transparent panel background and + * it doesn't work well in this "disjointed panel header" case + */ + background: + linear-gradient(to top, var(--panel), var(--panel)), + linear-gradient(to top, var(--bg), var(--bg)); + height: 50px; } .scrollable-message-list { diff --git a/src/components/chat/chat.vue b/src/components/chat/chat.vue index 2e4538c8..94a0097c 100644 --- a/src/components/chat/chat.vue +++ b/src/components/chat/chat.vue @@ -14,7 +14,10 @@ class="go-back-button" @click="goBack" > - <i class="button-icon icon-left-open" /> + <FAIcon + size="lg" + icon="chevron-left" + /> </a> <div class="title text-center"> <ChatTitle @@ -58,14 +61,15 @@ :class="{ 'visible': jumpToBottomButtonVisible }" @click="scrollDown({ behavior: 'smooth' })" > - <i class="icon-down-open"> + <span> + <FAIcon icon="chevron-down" /> <div v-if="newMessageCount" class="badge badge-notification unread-chat-count unread-message-count" > {{ newMessageCount }} </div> - </i> + </span> </div> <PostStatusForm :disable-subject="true" @@ -76,6 +80,7 @@ :disable-sensitivity-checkbox="true" :disable-submit="errorLoadingChat || !currentChat" :disable-preview="true" + :optimistic-posting="true" :post-handler="sendMessage" :submit-on-enter="!mobileLayout" :preserve-focus="!mobileLayout" diff --git a/src/components/chat/chat_layout_utils.js b/src/components/chat/chat_layout_utils.js index 609dc0c9..50a933ac 100644 --- a/src/components/chat/chat_layout_utils.js +++ b/src/components/chat/chat_layout_utils.js @@ -24,3 +24,10 @@ export const isBottomedOut = (el, offset = 0) => { export const scrollableContainerHeight = (inner, header, footer) => { return inner.offsetHeight - header.clientHeight - footer.clientHeight } + +// Returns whether or not the scrollbar is visible. +export const isScrollable = (el) => { + if (!el) return + + return el.scrollHeight > el.clientHeight +} diff --git a/src/components/chat_list_item/chat_list_item.vue b/src/components/chat_list_item/chat_list_item.vue index 1f8ecdf6..cd3f436e 100644 --- a/src/components/chat_list_item/chat_list_item.vue +++ b/src/components/chat_list_item/chat_list_item.vue @@ -21,6 +21,12 @@ /> </span> <span class="heading-right" /> + <div class="time-wrapper"> + <Timeago + :time="chat.updated_at" + :auto-update="60" + /> + </div> </div> <div class="chat-preview"> <StatusContent @@ -35,12 +41,6 @@ </div> </div> </div> - <div class="time-wrapper"> - <Timeago - :time="chat.updated_at" - :auto-update="60" - /> - </div> </div> </template> diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js index be4a7c89..bb380f87 100644 --- a/src/components/chat_message/chat_message.js +++ b/src/components/chat_message/chat_message.js @@ -7,6 +7,16 @@ import LinkPreview from '../link-preview/link-preview.vue' import StatusContent from '../status_content/status_content.vue' import ChatMessageDate from '../chat_message_date/chat_message_date.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faEllipsisH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faEllipsisH +) const ChatMessage = { name: 'ChatMessage', diff --git a/src/components/chat_message/chat_message.scss b/src/components/chat_message/chat_message.scss index 7d4ff60c..5af744a3 100644 --- a/src/components/chat_message/chat_message.scss +++ b/src/components/chat_message/chat_message.scss @@ -24,7 +24,7 @@ } } - .icon-ellipsis { + .menu-icon { cursor: pointer; &:hover, .extra-button-popover.open & { @@ -101,6 +101,19 @@ } } + .pending { + .status-content.media-body, .created-at { + color: var(--faint); + } + } + + .error { + .status-content.media-body, .created-at { + color: $fallback--cRed; + color: var(--badgeNotification, $fallback--cRed); + } + } + .incoming { a { color: var(--chatMessageIncomingLink, $fallback--link); diff --git a/src/components/chat_message/chat_message.vue b/src/components/chat_message/chat_message.vue index e923d694..3849ab6e 100644 --- a/src/components/chat_message/chat_message.vue +++ b/src/components/chat_message/chat_message.vue @@ -32,7 +32,7 @@ > <div class="media status" - :class="{ 'without-attachment': !hasAttachment }" + :class="{ 'without-attachment': !hasAttachment, 'pending': chatViewItem.data.pending, 'error': chatViewItem.data.error }" style="position: relative" @mouseenter="hovered = true" @mouseleave="hovered = false" @@ -56,15 +56,16 @@ class="dropdown-item dropdown-item-icon" @click="deleteMessage" > - <i class="icon-cancel" /> {{ $t("chats.delete") }} + <FAIcon icon="times" /> {{ $t("chats.delete") }} </button> </div> </div> <button slot="trigger" + class="menu-icon" :title="$t('chats.more')" > - <i class="icon-ellipsis" /> + <FAIcon icon="ellipsis-h" /> </button> </Popover> </div> diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js index d023efc0..71585995 100644 --- a/src/components/chat_new/chat_new.js +++ b/src/components/chat_new/chat_new.js @@ -1,6 +1,16 @@ import { mapState, mapGetters } from 'vuex' import BasicUserCard from '../basic_user_card/basic_user_card.vue' import UserAvatar from '../user_avatar/user_avatar.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSearch, + faChevronLeft +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSearch, + faChevronLeft +) const chatNew = { components: { diff --git a/src/components/chat_new/chat_new.scss b/src/components/chat_new/chat_new.scss index 11305444..5506143d 100644 --- a/src/components/chat_new/chat_new.scss +++ b/src/components/chat_new/chat_new.scss @@ -8,9 +8,7 @@ } } - .icon-search { - font-size: 1.5em; - float: right; + .search-icon { margin-right: 0.3em; } @@ -25,5 +23,9 @@ .go-back-button { cursor: pointer; + width: 28px; + text-align: center; + padding: 0.6em; + margin: -0.6em 0.6em -0.6em -0.6em; } } diff --git a/src/components/chat_new/chat_new.vue b/src/components/chat_new/chat_new.vue index 3333dbf9..f3894a3a 100644 --- a/src/components/chat_new/chat_new.vue +++ b/src/components/chat_new/chat_new.vue @@ -11,12 +11,18 @@ class="go-back-button" @click="goBack" > - <i class="button-icon icon-left-open" /> + <FAIcon + size="lg" + icon="chevron-left" + /> </a> </div> <div class="input-wrap"> <div class="input-search"> - <i class="button-icon icon-search" /> + <FAIcon + class="search-icon fa-scale-110 fa-old-padding" + icon="search" + /> </div> <input ref="search" diff --git a/src/components/chat_panel/chat_panel.js b/src/components/chat_panel/chat_panel.js index f2e3adf0..c3887098 100644 --- a/src/components/chat_panel/chat_panel.js +++ b/src/components/chat_panel/chat_panel.js @@ -1,4 +1,14 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faBullhorn, + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faBullhorn, + faTimes +) const chatPanel = { props: [ 'floating' ], diff --git a/src/components/chat_panel/chat_panel.vue b/src/components/chat_panel/chat_panel.vue index 570435e7..7993c94d 100644 --- a/src/components/chat_panel/chat_panel.vue +++ b/src/components/chat_panel/chat_panel.vue @@ -11,9 +11,9 @@ > <div class="title"> <span>{{ $t('shoutbox.title') }}</span> - <i + <FAIcon v-if="floating" - class="icon-cancel" + icon="times" /> </div> </div> @@ -63,7 +63,10 @@ @click.stop.prevent="togglePanel" > <div class="title"> - <i class="icon-megaphone" /> + <FAIcon + class="icon" + icon="bullhorn" + /> {{ $t('shoutbox.title') }} </div> </div> @@ -87,7 +90,8 @@ .chat-panel { .chat-heading { cursor: pointer; - .icon-comment-empty { + + .icon { color: $fallback--text; color: var(--text, $fallback--text); } diff --git a/src/components/contrast_ratio/contrast_ratio.vue b/src/components/contrast_ratio/contrast_ratio.vue index 9dc871b6..374cb9ba 100644 --- a/src/components/contrast_ratio/contrast_ratio.vue +++ b/src/components/contrast_ratio/contrast_ratio.vue @@ -8,13 +8,13 @@ class="rating" > <span v-if="contrast.aaa"> - <i class="icon-thumbs-up-alt" /> + <FAIcon icon="thumbs-up" /> </span> <span v-if="!contrast.aaa && contrast.aa"> - <i class="icon-adjust" /> + <FAIcon icon="adjust" /> </span> <span v-if="!contrast.aaa && !contrast.aa"> - <i class="icon-attention" /> + <FAIcon icon="exclamation-triangle" /> </span> </span> <span @@ -23,19 +23,32 @@ :title="hint_18pt" > <span v-if="contrast.laaa"> - <i class="icon-thumbs-up-alt" /> + <FAIcon icon="thumbs-up" /> </span> <span v-if="!contrast.laaa && contrast.laa"> - <i class="icon-adjust" /> + <FAIcon icon="adjust" /> </span> <span v-if="!contrast.laaa && !contrast.laa"> - <i class="icon-attention" /> + <FAIcon icon="exclamation-triangle" /> </span> </span> </span> </template> <script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faAdjust, + faExclamationTriangle, + faThumbsUp +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faAdjust, + faExclamationTriangle, + faThumbsUp +) + export default { props: { large: { @@ -85,6 +98,7 @@ export default { .rating { display: inline-block; text-align: center; + margin-left: 0.5em; } } </style> diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js index 45fb2bf6..069c0b40 100644 --- a/src/components/conversation/conversation.js +++ b/src/components/conversation/conversation.js @@ -44,7 +44,8 @@ const conversation = { 'isPage', 'pinnedStatusIdsObject', 'inProfile', - 'profileUserId' + 'profileUserId', + 'virtualHidden' ], created () { if (this.isPage) { @@ -52,6 +53,13 @@ const conversation = { } }, computed: { + hideStatus () { + if (this.$refs.statusComponent && this.$refs.statusComponent[0]) { + return this.virtualHidden && this.$refs.statusComponent[0].suspendable + } else { + return this.virtualHidden + } + }, status () { return this.$store.state.statuses.allStatusesObject[this.statusId] }, @@ -102,6 +110,10 @@ const conversation = { }, isExpanded () { return this.expanded || this.isPage + }, + hiddenStyle () { + const height = (this.status && this.status.virtualHeight) || '120px' + return this.virtualHidden ? { height } : {} } }, components: { @@ -121,6 +133,12 @@ const conversation = { if (value) { this.fetchConversation() } + }, + virtualHidden (value) { + this.$store.dispatch( + 'setVirtualHeight', + { statusId: this.statusId, height: `${this.$el.clientHeight}px` } + ) } }, methods: { diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue index 997a4d10..e0b9fcc5 100644 --- a/src/components/conversation/conversation.vue +++ b/src/components/conversation/conversation.vue @@ -1,5 +1,7 @@ <template> <div + v-if="!hideStatus" + :style="hiddenStyle" class="Conversation" :class="{ '-expanded' : isExpanded, 'panel' : isExpanded }" > @@ -18,6 +20,7 @@ <status v-for="status in conversation" :key="status.id" + ref="statusComponent" :inline-expanded="collapsable && isExpanded" :statusoid="status" :expandable="!isExpanded" @@ -33,6 +36,10 @@ @toggleExpanded="toggleExpanded" /> </div> + <div + v-else + :style="hiddenStyle" + /> </template> <script src="./conversation.js"></script> @@ -53,8 +60,8 @@ .conversation-status { border-color: $fallback--border; border-color: var(--border, $fallback--border); - border-left: 4px solid $fallback--cRed; - border-left: 4px solid var(--cRed, $fallback--cRed); + border-left-color: $fallback--cRed; + border-left-color: var(--cRed, $fallback--cRed); } .conversation-status:last-child { diff --git a/src/components/desktop_nav/desktop_nav.js b/src/components/desktop_nav/desktop_nav.js new file mode 100644 index 00000000..e048f53d --- /dev/null +++ b/src/components/desktop_nav/desktop_nav.js @@ -0,0 +1,89 @@ +import SearchBar from 'components/search_bar/search_bar.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +) + +export default { + components: { + SearchBar + }, + data: () => ({ + searchBarHidden: true, + supportsMask: window.CSS && window.CSS.supports && ( + window.CSS.supports('mask-size', 'contain') || + window.CSS.supports('-webkit-mask-size', 'contain') || + window.CSS.supports('-moz-mask-size', 'contain') || + window.CSS.supports('-ms-mask-size', 'contain') || + window.CSS.supports('-o-mask-size', 'contain') + ) + }), + computed: { + enableMask () { return this.supportsMask && this.$store.state.instance.logoMask }, + logoStyle () { + return { + 'visibility': this.enableMask ? 'hidden' : 'visible' + } + }, + logoMaskStyle () { + return this.enableMask ? { + 'mask-image': `url(${this.$store.state.instance.logo})` + } : { + 'background-color': this.enableMask ? '' : 'transparent' + } + }, + logoBgStyle () { + return Object.assign({ + 'margin': `${this.$store.state.instance.logoMargin} 0`, + opacity: this.searchBarHidden ? 1 : 0 + }, this.enableMask ? {} : { + 'background-color': this.enableMask ? '' : 'transparent' + }) + }, + logo () { return this.$store.state.instance.logo }, + sitename () { return this.$store.state.instance.name }, + hideSitename () { return this.$store.state.instance.hideSitename }, + logoLeft () { return this.$store.state.instance.logoLeft }, + currentUser () { return this.$store.state.users.currentUser }, + privateMode () { return this.$store.state.instance.private } + }, + methods: { + scrollToTop () { + window.scrollTo(0, 0) + }, + logout () { + this.$router.replace('/main/public') + this.$store.dispatch('logout') + }, + onSearchBarToggled (hidden) { + this.searchBarHidden = hidden + }, + openSettingsModal () { + this.$store.dispatch('openSettingsModal') + } + } +} diff --git a/src/components/desktop_nav/desktop_nav.scss b/src/components/desktop_nav/desktop_nav.scss new file mode 100644 index 00000000..028692a9 --- /dev/null +++ b/src/components/desktop_nav/desktop_nav.scss @@ -0,0 +1,112 @@ +@import '../../_variables.scss'; + +.DesktopNav { + height: 50px; + width: 100%; + position: fixed; + + .inner-nav { + display: grid; + grid-template-rows: 50px; + grid-template-columns: 2fr auto 2fr; + grid-template-areas: "sitename logo actions"; + box-sizing: border-box; + padding: 0 1.2em; + margin: auto; + max-width: 980px; + } + + &.-logoLeft { + grid-template-columns: auto 2fr 2fr; + grid-template-areas: "logo sitename actions"; + } + + button { + &, svg { + color: $fallback--text; + color: var(--btnTopBarText, $fallback--text); + } + + &:active { + background-color: $fallback--fg; + background-color: var(--btnPressedTopBar, $fallback--fg); + color: $fallback--text; + color: var(--btnPressedTopBarText, $fallback--text); + } + + &:disabled { + color: $fallback--text; + color: var(--btnDisabledTopBarText, $fallback--text); + } + + &.toggled { + color: $fallback--text; + color: var(--btnToggledTopBarText, $fallback--text); + background-color: $fallback--fg; + background-color: var(--btnToggledTopBar, $fallback--fg) + } + } + + .logo { + grid-area: logo; + position: relative; + transition: opacity; + transition-timing-function: ease-out; + transition-duration: 100ms; + + @media all and (min-width: 800px) { + opacity: 1 !important; + } + + .mask { + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + background-color: $fallback--fg; + background-color: var(--topBarText, $fallback--fg); + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + } + + img { + display: inline-block; + height: 50px; + } + } + + .nav-icon { + margin-left: 0.2em; + width: 2em; + text-align: center; + } + + a, a svg { + color: $fallback--link; + color: var(--topBarLink, $fallback--link); + } + + .sitename { + grid-area: sitename; + } + + .actions { + grid-area: actions; + } + + .item { + flex: 1; + line-height: 50px; + height: 50px; + overflow: hidden; + display: flex; + flex-wrap: wrap; + + &.right { + justify-content: flex-end; + text-align: right; + } + } +} diff --git a/src/components/desktop_nav/desktop_nav.vue b/src/components/desktop_nav/desktop_nav.vue new file mode 100644 index 00000000..3a6e4033 --- /dev/null +++ b/src/components/desktop_nav/desktop_nav.vue @@ -0,0 +1,79 @@ +<template> + <nav + id="nav" + class="DesktopNav" + :class="{ '-logoLeft': logoLeft }" + @click="scrollToTop()" + > + <div class="inner-nav"> + <div class="item sitename"> + <router-link + v-if="!hideSitename" + class="site-name" + :to="{ name: 'root' }" + active-class="home" + > + {{ sitename }} + </router-link> + </div> + <router-link + class="logo" + :to="{ name: 'root' }" + :style="logoBgStyle" + > + <div + class="mask" + :style="logoMaskStyle" + /> + <img + :src="logo" + :style="logoStyle" + > + </router-link> + <div class="item right actions"> + <search-bar + v-if="currentUser || !privateMode" + @toggled="onSearchBarToggled" + @click.stop.native + /> + <a + href="#" + class="nav-icon" + @click.stop="openSettingsModal" + > + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="cog" + :title="$t('nav.preferences')" + /> + </a> + <a + v-if="currentUser && currentUser.role === 'admin'" + href="/pleroma/admin/#/login-pleroma" + class="nav-icon" + target="_blank" + ><FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="tachometer-alt" + :title="$t('nav.administration')" + /></a> + <a + v-if="currentUser" + href="#" + class="nav-icon" + @click.prevent="logout" + ><FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-out-alt" + :title="$t('login.logout')" + /></a> + </div> + </div> + </nav> +</template> +<script src="./desktop_nav.js"></script> + +<style src="./desktop_nav.scss" lang="scss"></style> diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js index f0123447..87303d08 100644 --- a/src/components/emoji_input/emoji_input.js +++ b/src/components/emoji_input/emoji_input.js @@ -3,6 +3,15 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue' import { take } from 'lodash' import { findOffset } from '../../services/offset_finder/offset_finder.service.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faSmileBeam +} from '@fortawesome/free-regular-svg-icons' + +library.add( + faSmileBeam +) + /** * EmojiInput - augmented inputs for emoji and autocomplete support in inputs * without having to give up the comfort of <input/> and <textarea/> elements diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index b9a74572..224e72cf 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -11,7 +11,7 @@ class="emoji-picker-icon" @click.prevent="togglePicker" > - <i class="icon-smile" /> + <FAIcon :icon="['far', 'smile-beam']" /> </div> <EmojiPicker v-if="enableEmojiPicker" diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js index 0f397b59..2716d93f 100644 --- a/src/components/emoji_picker/emoji_picker.js +++ b/src/components/emoji_picker/emoji_picker.js @@ -1,4 +1,16 @@ import Checkbox from '../checkbox/checkbox.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faBoxOpen, + faStickyNote, + faSmileBeam +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faBoxOpen, + faStickyNote, + faSmileBeam +) // At widest, approximately 20 emoji are visible in a row, // loading 3 rows, could be overkill for narrow picker @@ -8,7 +20,20 @@ const LOAD_EMOJI_BY = 60 const LOAD_EMOJI_MARGIN = 64 const filterByKeyword = (list, keyword = '') => { - return list.filter(x => x.displayText.includes(keyword)) + if (keyword === '') return list + + const keywordLowercase = keyword.toLowerCase() + let orderedEmojiList = [] + for (const emoji of list) { + const indexOfKeyword = emoji.displayText.toLowerCase().indexOf(keywordLowercase) + if (indexOfKeyword > -1) { + if (!Array.isArray(orderedEmojiList[indexOfKeyword])) { + orderedEmojiList[indexOfKeyword] = [] + } + orderedEmojiList[indexOfKeyword].push(emoji) + } + } + return orderedEmojiList.flat() } const EmojiPicker = { @@ -164,13 +189,13 @@ const EmojiPicker = { { id: 'custom', text: this.$t('emoji.custom'), - icon: 'icon-smile', + icon: 'smile-beam', emojis: customEmojis }, { id: 'standard', text: this.$t('emoji.unicode'), - icon: 'icon-picture', + icon: 'box-open', emojis: filterByKeyword(standardEmojis, this.keyword) } ] diff --git a/src/components/emoji_picker/emoji_picker.scss b/src/components/emoji_picker/emoji_picker.scss index 8bd07e45..ec711758 100644 --- a/src/components/emoji_picker/emoji_picker.scss +++ b/src/components/emoji_picker/emoji_picker.scss @@ -82,7 +82,7 @@ &.active { border-bottom: 4px solid; - i { + svg { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); } diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue index 191b9fa1..3262a3d9 100644 --- a/src/components/emoji_picker/emoji_picker.vue +++ b/src/components/emoji_picker/emoji_picker.vue @@ -13,7 +13,10 @@ :title="group.text" @click.prevent="highlight(group.id)" > - <i :class="group.icon" /> + <FAIcon + :icon="group.icon" + fixed-width + /> </span> </span> <span @@ -26,7 +29,10 @@ :title="$t('emoji.stickers')" @click.prevent="toggleStickers" > - <i class="icon-star" /> + <FAIcon + icon="sticky-note" + fixed-width + /> </span> </span> </div> diff --git a/src/components/exporter/exporter.js b/src/components/exporter/exporter.js index 8f507416..51912ac3 100644 --- a/src/components/exporter/exporter.js +++ b/src/components/exporter/exporter.js @@ -1,3 +1,10 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) + const Exporter = { props: { getContent: { diff --git a/src/components/exporter/exporter.vue b/src/components/exporter/exporter.vue index f5126dc1..ecd71bf1 100644 --- a/src/components/exporter/exporter.vue +++ b/src/components/exporter/exporter.vue @@ -1,7 +1,12 @@ <template> <div class="exporter"> <div v-if="processing"> - <i class="icon-spin4 animate-spin exporter-processing" /> + <FAIcon + icon="circle-notch" + size="lg" + spin + /> + <span>{{ processingMessage }}</span> </div> <button @@ -19,7 +24,6 @@ <style lang="scss"> .exporter { &-processing { - font-size: 1.5em; margin: 0.25em; } } diff --git a/src/components/extra_buttons/extra_buttons.js b/src/components/extra_buttons/extra_buttons.js index 5e0c36bb..1a8eef72 100644 --- a/src/components/extra_buttons/extra_buttons.js +++ b/src/components/extra_buttons/extra_buttons.js @@ -1,4 +1,24 @@ import Popover from '../popover/popover.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEllipsisH, + faBookmark, + faEyeSlash, + faThumbtack, + faShareAlt +} from '@fortawesome/free-solid-svg-icons' +import { + faBookmark as faBookmarkReg +} from '@fortawesome/free-regular-svg-icons' + +library.add( + faEllipsisH, + faBookmark, + faBookmarkReg, + faEyeSlash, + faThumbtack, + faShareAlt +) const ExtraButtons = { props: [ 'status' ], diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue index 7a4e8642..a33f6e87 100644 --- a/src/components/extra_buttons/extra_buttons.vue +++ b/src/components/extra_buttons/extra_buttons.vue @@ -15,14 +15,20 @@ class="dropdown-item dropdown-item-icon" @click.prevent="muteConversation" > - <i class="icon-eye-off" /><span>{{ $t("status.mute_conversation") }}</span> + <FAIcon + fixed-width + icon="eye-slash" + /><span>{{ $t("status.mute_conversation") }}</span> </button> <button v-if="canMute && status.thread_muted" class="dropdown-item dropdown-item-icon" @click.prevent="unmuteConversation" > - <i class="icon-eye-off" /><span>{{ $t("status.unmute_conversation") }}</span> + <FAIcon + fixed-width + icon="eye-slash" + /><span>{{ $t("status.unmute_conversation") }}</span> </button> <button v-if="!status.pinned && canPin" @@ -30,7 +36,10 @@ @click.prevent="pinStatus" @click="close" > - <i class="icon-pin" /><span>{{ $t("status.pin") }}</span> + <FAIcon + fixed-width + icon="thumbtack" + /><span>{{ $t("status.pin") }}</span> </button> <button v-if="status.pinned && canPin" @@ -38,7 +47,10 @@ @click.prevent="unpinStatus" @click="close" > - <i class="icon-pin" /><span>{{ $t("status.unpin") }}</span> + <FAIcon + fixed-width + icon="thumbtack" + /><span>{{ $t("status.unpin") }}</span> </button> <button v-if="!status.bookmarked" @@ -46,7 +58,10 @@ @click.prevent="bookmarkStatus" @click="close" > - <i class="icon-bookmark-empty" /><span>{{ $t("status.bookmark") }}</span> + <FAIcon + fixed-width + :icon="['far', 'bookmark']" + /><span>{{ $t("status.bookmark") }}</span> </button> <button v-if="status.bookmarked" @@ -54,7 +69,10 @@ @click.prevent="unbookmarkStatus" @click="close" > - <i class="icon-bookmark" /><span>{{ $t("status.unbookmark") }}</span> + <FAIcon + fixed-width + icon="bookmark" + /><span>{{ $t("status.unbookmark") }}</span> </button> <button v-if="canDelete" @@ -62,21 +80,29 @@ @click.prevent="deleteStatus" @click="close" > - <i class="icon-cancel" /><span>{{ $t("status.delete") }}</span> + <FAIcon + fixed-width + icon="times" + /><span>{{ $t("status.delete") }}</span> </button> <button class="dropdown-item dropdown-item-icon" @click.prevent="copyLink" @click="close" > - <i class="icon-share" /><span>{{ $t("status.copy_link") }}</span> + <FAIcon + fixed-width + icon="share-alt" + /><span>{{ $t("status.copy_link") }}</span> </button> </div> </div> - <i - slot="trigger" - class="icon-ellipsis button-icon" - /> + <span slot="trigger"> + <FAIcon + class="ExtraButtons fa-scale-110 fa-old-padding" + icon="ellipsis-h" + /> + </span> </Popover> </template> @@ -85,8 +111,9 @@ <style lang="scss"> @import '../../_variables.scss'; -.icon-ellipsis { +.ExtraButtons { cursor: pointer; + position: static; &:hover, .extra-button-popover.open & { diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js index 5014d84f..2a2ee84a 100644 --- a/src/components/favorite_button/favorite_button.js +++ b/src/components/favorite_button/favorite_button.js @@ -1,4 +1,14 @@ import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faStar } from '@fortawesome/free-solid-svg-icons' +import { + faStar as faStarRegular +} from '@fortawesome/free-regular-svg-icons' + +library.add( + faStar, + faStarRegular +) const FavoriteButton = { props: ['status', 'loggedIn'], @@ -23,9 +33,7 @@ const FavoriteButton = { computed: { classes () { return { - 'icon-star-empty': !this.status.favorited, - 'icon-star': this.status.favorited, - 'animate-spin': this.animated + '-favorited': this.status.favorited } }, ...mapGetters(['mergedConfig']) diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue index fbc90f84..dfe12f86 100644 --- a/src/components/favorite_button/favorite_button.vue +++ b/src/components/favorite_button/favorite_button.vue @@ -1,18 +1,21 @@ <template> <div v-if="loggedIn"> - <i + <FAIcon :class="classes" - class="button-icon favorite-button fav-active" + class="FavoriteButton fa-scale-110 fa-old-padding -interactive" :title="$t('tool_tip.favorite')" + :icon="[status.favorited ? 'fas' : 'far', 'star']" + :spin="animated" @click.prevent="favorite()" /> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> </div> <div v-else> - <i + <FAIcon :class="classes" - class="button-icon favorite-button" + class="FavoriteButton fa-scale-110 fa-old-padding" :title="$t('tool_tip.favorite')" + :icon="['far', 'star']" /> <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span> </div> @@ -23,18 +26,20 @@ <style lang="scss"> @import '../../_variables.scss'; -.fav-active { - cursor: pointer; - animation-duration: 0.6s; +.FavoriteButton { + &.-interactive { + cursor: pointer; + animation-duration: 0.6s; - &:hover { + &:hover { + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + } + + &.-favorited { color: $fallback--cOrange; color: var(--cOrange, $fallback--cOrange); } } - -.favorite-button.icon-star { - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); -} </style> diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue index 5fa4cf39..41f19db8 100644 --- a/src/components/follow_requests/follow_requests.vue +++ b/src/components/follow_requests/follow_requests.vue @@ -1,7 +1,9 @@ <template> <div class="settings panel panel-default"> <div class="panel-heading"> - {{ $t('nav.friend_requests') }} + <div class="title"> + {{ $t('nav.friend_requests') }} + </div> </div> <div class="panel-body"> <FollowRequestCard diff --git a/src/components/font_control/font_control.js b/src/components/font_control/font_control.js index 8e2b0e45..6274780b 100644 --- a/src/components/font_control/font_control.js +++ b/src/components/font_control/font_control.js @@ -1,4 +1,12 @@ import { set } from 'vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) export default { props: [ diff --git a/src/components/font_control/font_control.vue b/src/components/font_control/font_control.vue index 61f0384b..dd117ec0 100644 --- a/src/components/font_control/font_control.vue +++ b/src/components/font_control/font_control.vue @@ -41,7 +41,10 @@ {{ option === 'custom' ? $t('settings.style.fonts.custom') : option }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> <input v-if="isCustom" diff --git a/src/components/global_notice_list/global_notice_list.js b/src/components/global_notice_list/global_notice_list.js index 3af29c23..e93fba75 100644 --- a/src/components/global_notice_list/global_notice_list.js +++ b/src/components/global_notice_list/global_notice_list.js @@ -1,3 +1,11 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) const GlobalNoticeList = { computed: { diff --git a/src/components/global_notice_list/global_notice_list.vue b/src/components/global_notice_list/global_notice_list.vue index 0e4285cc..8a33b9eb 100644 --- a/src/components/global_notice_list/global_notice_list.vue +++ b/src/components/global_notice_list/global_notice_list.vue @@ -9,8 +9,9 @@ <div class="notice-message"> {{ $t(notice.messageKey, notice.messageArgs) }} </div> - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="closeNotice(notice)" /> </div> diff --git a/src/components/image_cropper/image_cropper.js b/src/components/image_cropper/image_cropper.js index 01361e25..59e4d07e 100644 --- a/src/components/image_cropper/image_cropper.js +++ b/src/components/image_cropper/image_cropper.js @@ -1,5 +1,15 @@ import Cropper from 'cropperjs' import 'cropperjs/dist/cropper.css' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faCircleNotch +) const ImageCropper = { props: { diff --git a/src/components/image_cropper/image_cropper.vue b/src/components/image_cropper/image_cropper.vue index 4e1b5927..75def612 100644 --- a/src/components/image_cropper/image_cropper.vue +++ b/src/components/image_cropper/image_cropper.vue @@ -31,9 +31,10 @@ @click="submit(false)" v-text="saveWithoutCroppingText" /> - <i + <FAIcon v-if="submitting" - class="icon-spin4 animate-spin" + spin + icon="circle-notch" /> </div> <div @@ -41,8 +42,9 @@ class="alert error" > {{ submitErrorMsg }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/importer/importer.js b/src/components/importer/importer.js index c5f9e4d2..59f9beb1 100644 --- a/src/components/importer/importer.js +++ b/src/components/importer/importer.js @@ -1,3 +1,14 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch, + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch, + faTimes +) + const Importer = { props: { submitHandler: { diff --git a/src/components/importer/importer.vue b/src/components/importer/importer.vue index ed923d59..c4fe5b00 100644 --- a/src/components/importer/importer.vue +++ b/src/components/importer/importer.vue @@ -7,9 +7,11 @@ @change="change" > </form> - <i + <FAIcon v-if="submitting" - class="icon-spin4 animate-spin importer-uploading" + class="importer-uploading" + spin + icon="circle-notch" /> <button v-else @@ -19,15 +21,15 @@ {{ submitButtonLabel }} </button> <div v-if="success"> - <i - class="icon-cross" + <FAIcon + icon="times" @click="dismiss" /> <p>{{ successMessage }}</p> </div> <div v-else-if="error"> - <i - class="icon-cross" + <FAIcon + icon="times" @click="dismiss" /> <p>{{ errorMessage }}</p> diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue index dd6800a3..d039e86b 100644 --- a/src/components/interface_language_switcher/interface_language_switcher.vue +++ b/src/components/interface_language_switcher/interface_language_switcher.vue @@ -19,7 +19,10 @@ {{ languageNames[i] }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </template> @@ -28,6 +31,14 @@ import languagesObject from '../../i18n/messages' import ISO6391 from 'iso-639-1' import _ from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) export default { computed: { diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 0d8f1da6..638bd812 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,5 +1,13 @@ import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' import oauthApi from '../../services/new_api/oauth.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) const LoginForm = { data: () => ({ diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index b4fdcefb..a1f77210 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -76,8 +76,9 @@ > <div class="alert error"> {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 24764e80..e7384c93 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -3,6 +3,16 @@ import VideoAttachment from '../video_attachment/video_attachment.vue' import Modal from '../modal/modal.vue' import fileTypeService from '../../services/file_type/file_type.service.js' import GestureService from '../../services/gesture_service/gesture_service' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronLeft, + faChevronRight +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronLeft, + faChevronRight +) const MediaModal = { components: { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index 46931667..ea7f7a7f 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -34,7 +34,10 @@ class="modal-view-button-arrow modal-view-button-arrow--prev" @click.stop.prevent="goPrev" > - <i class="icon-left-open arrow-icon" /> + <FAIcon + class="arrow-icon" + icon="chevron-left" + /> </button> <button v-if="canNavigate" @@ -42,7 +45,10 @@ class="modal-view-button-arrow modal-view-button-arrow--next" @click.stop.prevent="goNext" > - <i class="icon-right-open arrow-icon" /> + <FAIcon + class="arrow-icon" + icon="chevron-right" + /> </button> </Modal> </template> diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js index 7b8a76cc..669d8190 100644 --- a/src/components/media_upload/media_upload.js +++ b/src/components/media_upload/media_upload.js @@ -2,6 +2,14 @@ import statusPosterService from '../../services/status_poster/status_poster.service.js' import fileSizeFormatService from '../../services/file_size_format/file_size_format.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faUpload, faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faUpload, + faCircleNotch +) + const mediaUpload = { data () { return { diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue index c8865d77..88251a26 100644 --- a/src/components/media_upload/media_upload.vue +++ b/src/components/media_upload/media_upload.vue @@ -7,13 +7,16 @@ class="label" :title="$t('tool_tip.media_upload')" > - <i + <FAIcon v-if="uploading" - class="progress-icon icon-spin4 animate-spin" + class="progress-icon" + icon="circle-notch" + spin /> - <i + <FAIcon v-if="!uploading" - class="new-icon icon-upload" + class="new-icon" + icon="upload" /> <input v-if="uploadReady" @@ -40,15 +43,5 @@ .new-icon { cursor: pointer; } - - .progress-icon { - display: inline-block; - line-height: 0; - &::before { - /* Overriding fontello to achieve the perfect speeeen */ - margin: 0; - line-height: 0; - } - } } </style> diff --git a/src/components/mfa_form/recovery_form.js b/src/components/mfa_form/recovery_form.js index b25c65dd..01a62a50 100644 --- a/src/components/mfa_form/recovery_form.js +++ b/src/components/mfa_form/recovery_form.js @@ -1,5 +1,13 @@ import mfaApi from '../../services/new_api/mfa.js' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) export default { data: () => ({ diff --git a/src/components/mfa_form/recovery_form.vue b/src/components/mfa_form/recovery_form.vue index 57294630..78953649 100644 --- a/src/components/mfa_form/recovery_form.vue +++ b/src/components/mfa_form/recovery_form.vue @@ -54,8 +54,9 @@ > <div class="alert error"> {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/mfa_form/totp_form.js b/src/components/mfa_form/totp_form.js index b774f2d0..6ee823ed 100644 --- a/src/components/mfa_form/totp_form.js +++ b/src/components/mfa_form/totp_form.js @@ -1,5 +1,14 @@ import mfaApi from '../../services/new_api/mfa.js' import { mapState, mapGetters, mapActions, mapMutations } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) + export default { data: () => ({ code: null, diff --git a/src/components/mfa_form/totp_form.vue b/src/components/mfa_form/totp_form.vue index a344b395..9401cad5 100644 --- a/src/components/mfa_form/totp_form.vue +++ b/src/components/mfa_form/totp_form.vue @@ -56,8 +56,10 @@ > <div class="alert error"> {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + size="lg" + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js index b2b5d264..9e736cfb 100644 --- a/src/components/mobile_nav/mobile_nav.js +++ b/src/components/mobile_nav/mobile_nav.js @@ -3,6 +3,18 @@ import Notifications from '../notifications/notifications.vue' import { unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils' import GestureService from '../../services/gesture_service/gesture_service' import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faBell, + faBars +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faBell, + faBars +) const MobileNav = { components: { diff --git a/src/components/mobile_nav/mobile_nav.vue b/src/components/mobile_nav/mobile_nav.vue index abd95f09..5304a500 100644 --- a/src/components/mobile_nav/mobile_nav.vue +++ b/src/components/mobile_nav/mobile_nav.vue @@ -1,49 +1,53 @@ <template> - <div> + <div + class="MobileNav" + > <nav id="nav" - class="nav-bar container" + class="mobile-nav" :class="{ 'mobile-hidden': isChat }" + @click="scrollToTop()" > - <div - class="mobile-inner-nav" - @click="scrollToTop()" - > - <div class="item"> - <a - href="#" - class="mobile-nav-button" - @click.stop.prevent="toggleMobileSidebar()" - > - <i class="button-icon icon-menu" /> - <div - v-if="unreadChatCount" - class="alert-dot" - /> - </a> - <router-link - v-if="!hideSitename" - class="site-name" - :to="{ name: 'root' }" - active-class="home" - > - {{ sitename }} - </router-link> - </div> - <div class="item right"> - <a - v-if="currentUser" - class="mobile-nav-button" - href="#" - @click.stop.prevent="openMobileNotifications()" - > - <i class="button-icon icon-bell-alt" /> - <div - v-if="unseenNotificationsCount" - class="alert-dot" - /> - </a> - </div> + <div class="item"> + <a + href="#" + class="mobile-nav-button" + @click.stop.prevent="toggleMobileSidebar()" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="bars" + /> + <div + v-if="unreadChatCount" + class="alert-dot" + /> + </a> + <router-link + v-if="!hideSitename" + class="site-name" + :to="{ name: 'root' }" + active-class="home" + > + {{ sitename }} + </router-link> + </div> + <div class="item right"> + <a + v-if="currentUser" + class="mobile-nav-button" + href="#" + @click.stop.prevent="openMobileNotifications()" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="bell" + /> + <div + v-if="unseenNotificationsCount" + class="alert-dot" + /> + </a> </div> </nav> <div @@ -59,7 +63,10 @@ class="mobile-nav-button" @click.stop.prevent="closeMobileNotifications()" > - <i class="button-icon icon-cancel" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" + /> </a> </div> <div @@ -84,101 +91,124 @@ <style lang="scss"> @import '../../_variables.scss'; -.mobile-inner-nav { - width: 100%; - display: flex; - align-items: center; -} +.MobileNav { + .mobile-nav { + display: grid; + line-height: 50px; + height: 50px; + grid-template-rows: 50px; + grid-template-columns: 2fr auto; + width: 100%; + position: fixed; + box-sizing: border-box; + } -.mobile-nav-button { - display: flex; - justify-content: center; - width: 50px; - position: relative; - cursor: pointer; -} + .mobile-inner-nav { + width: 100%; + display: flex; + align-items: center; + } -.alert-dot { - border-radius: 100%; - height: 8px; - width: 8px; - position: absolute; - left: calc(50% - 4px); - top: calc(50% - 4px); - margin-left: 6px; - margin-top: -6px; - background-color: $fallback--cRed; - background-color: var(--badgeNotification, $fallback--cRed); -} + .mobile-nav-button { + display: inline-block; + text-align: center; + padding: 0 1em; + position: relative; + cursor: pointer; + } -.mobile-notifications-drawer { - width: 100%; - height: 100vh; - overflow-x: hidden; - position: fixed; - top: 0; - left: 0; - box-shadow: 1px 1px 4px rgba(0,0,0,.6); - box-shadow: var(--panelShadow); - transition-property: transform; - transition-duration: 0.25s; - transform: translateX(0); - z-index: 1001; - -webkit-overflow-scrolling: touch; - - &.closed { - transform: translateX(100%); + .site-name { + padding: 0 .3em; + display: inline-block; } -} -.mobile-notifications-header { - display: flex; - align-items: center; - justify-content: space-between; - z-index: 1; - width: 100%; - height: 50px; - line-height: 50px; - position: absolute; - color: var(--topBarText); - background-color: $fallback--fg; - background-color: var(--topBar, $fallback--fg); - box-shadow: 0px 0px 4px rgba(0,0,0,.6); - box-shadow: var(--topBarShadow); - - .title { - font-size: 1.3em; - margin-left: 0.6em; + .item { + /* moslty just to get rid of extra whitespaces */ + display: flex; } -} -.mobile-notifications { - margin-top: 50px; - width: 100vw; - height: calc(100vh - 50px); - overflow-x: hidden; - overflow-y: scroll; - - color: $fallback--text; - color: var(--text, $fallback--text); - background-color: $fallback--bg; - background-color: var(--bg, $fallback--bg); - - .notifications { - padding: 0; - border-radius: 0; - box-shadow: none; - .panel { - border-radius: 0; - margin: 0; - box-shadow: none; + .alert-dot { + border-radius: 100%; + height: 8px; + width: 8px; + position: absolute; + left: calc(50% - 4px); + top: calc(50% - 4px); + margin-left: 6px; + margin-top: -6px; + background-color: $fallback--cRed; + background-color: var(--badgeNotification, $fallback--cRed); + } + + .mobile-notifications-drawer { + width: 100%; + height: 100vh; + overflow-x: hidden; + position: fixed; + top: 0; + left: 0; + box-shadow: 1px 1px 4px rgba(0,0,0,.6); + box-shadow: var(--panelShadow); + transition-property: transform; + transition-duration: 0.25s; + transform: translateX(0); + z-index: 1001; + -webkit-overflow-scrolling: touch; + + &.closed { + transform: translateX(100%); } - .panel:after { - border-radius: 0; + } + + .mobile-notifications-header { + display: flex; + align-items: center; + justify-content: space-between; + z-index: 1; + width: 100%; + height: 50px; + line-height: 50px; + position: absolute; + color: var(--topBarText); + background-color: $fallback--fg; + background-color: var(--topBar, $fallback--fg); + box-shadow: 0px 0px 4px rgba(0,0,0,.6); + box-shadow: var(--topBarShadow); + + .title { + font-size: 1.3em; + margin-left: 0.6em; } - .panel .panel-heading { + } + + .mobile-notifications { + margin-top: 50px; + width: 100vw; + height: calc(100vh - 50px); + overflow-x: hidden; + overflow-y: scroll; + + color: $fallback--text; + color: var(--text, $fallback--text); + background-color: $fallback--bg; + background-color: var(--bg, $fallback--bg); + + .notifications { + padding: 0; border-radius: 0; box-shadow: none; + .panel { + border-radius: 0; + margin: 0; + box-shadow: none; + } + .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 6348277b..366ea89c 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 @@ -1,4 +1,12 @@ import { debounce } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faPen +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faPen +) const HIDDEN_FOR_PAGES = new Set([ 'chats', diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.vue b/src/components/mobile_post_status_button/mobile_post_status_button.vue index 9cf45de3..50529878 100644 --- a/src/components/mobile_post_status_button/mobile_post_status_button.vue +++ b/src/components/mobile_post_status_button/mobile_post_status_button.vue @@ -5,7 +5,7 @@ :class="{ 'hidden': isHidden }" @click="openPostForm" > - <i class="icon-edit" /> + <FAIcon icon="pen" /> </button> </div> </template> @@ -39,7 +39,7 @@ transform: translateY(150%); } - i { + svg { font-size: 1.5em; color: $fallback--text; color: var(--text, $fallback--text); diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue index b2d5acc5..60fa6ceb 100644 --- a/src/components/moderation_tools/moderation_tools.vue +++ b/src/components/moderation_tools/moderation_tools.vue @@ -178,7 +178,7 @@ box-shadow: var(--inputShadow); &.menu-checkbox-checked::after { - content: '✔'; + content: '✓'; } } diff --git a/src/components/nav_panel/nav_panel.js b/src/components/nav_panel/nav_panel.js index 623dfaec..81d49cc2 100644 --- a/src/components/nav_panel/nav_panel.js +++ b/src/components/nav_panel/nav_panel.js @@ -1,6 +1,29 @@ import { timelineNames } from '../timeline_menu/timeline_menu.js' import { mapState, mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faComments, + faBell, + faInfoCircle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faComments, + faBell, + faInfoCircle +) + const NavPanel = { created () { if (this.currentUser && this.currentUser.locked) { diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index f8459fd1..0c83d0fe 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -1,5 +1,5 @@ <template> - <div class="nav-panel"> + <div class="NavPanel"> <div class="panel panel-default"> <ul> <li v-if="currentUser || !privateMode"> @@ -7,31 +7,47 @@ :to="{ name: timelinesRoute }" :class="onTimelineRoute && 'router-link-active'" > - <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="home" + />{{ $t("nav.timelines") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="bell" + />{{ $t("nav.interactions") }} </router-link> </li> <li v-if="currentUser && pleromaChatMessagesAvailable"> <router-link :to="{ name: 'chats', params: { username: currentUser.screen_name } }"> <div v-if="unreadChatCount" - class="badge badge-notification unread-chat-count" + class="badge badge-notification" > {{ unreadChatCount }} </div> - <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="comments" + />{{ $t("nav.chats") }} </router-link> </li> <li v-if="currentUser && currentUser.locked"> <router-link :to="{ name: 'friend-requests' }"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="user-plus" + />{{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge follow-request-count" + class="badge badge-notification" > {{ followRequestCount }} </span> @@ -39,7 +55,11 @@ </li> <li> <router-link :to="{ name: 'about' }"> - <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} + <FAIcon + fixed-width + class="fa-scale-110" + icon="info-circle" + />{{ $t("nav.about") }} </router-link> </li> </ul> @@ -52,80 +72,88 @@ <style lang="scss"> @import '../../_variables.scss'; -.nav-panel .panel { - overflow: hidden; - box-shadow: var(--panelShadow); -} -.nav-panel ul { - list-style: none; - margin: 0; - padding: 0; -} - -.follow-request-count { - margin: -6px 10px; - background-color: $fallback--bg; - background-color: var(--input, $fallback--faint); -} - -.nav-panel li { - border-bottom: 1px solid; - border-color: $fallback--border; - border-color: var(--border, $fallback--border); - padding: 0; - - &:first-child a { - border-top-right-radius: $fallback--panelRadius; - border-top-right-radius: var(--panelRadius, $fallback--panelRadius); - border-top-left-radius: $fallback--panelRadius; - border-top-left-radius: var(--panelRadius, $fallback--panelRadius); +.NavPanel { + .panel { + overflow: hidden; + box-shadow: var(--panelShadow); } - &:last-child a { - border-bottom-right-radius: $fallback--panelRadius; - border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); - border-bottom-left-radius: $fallback--panelRadius; - border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); + ul { + list-style: none; + margin: 0; + padding: 0; } -} -.nav-panel li:last-child { - border: none; -} + li { + position: relative; + border-bottom: 1px solid; + border-color: $fallback--border; + border-color: var(--border, $fallback--border); + padding: 0; -.nav-panel a { - display: block; - padding: 0.8em 0.85em; + &:first-child a { + border-top-right-radius: $fallback--panelRadius; + border-top-right-radius: var(--panelRadius, $fallback--panelRadius); + border-top-left-radius: $fallback--panelRadius; + border-top-left-radius: var(--panelRadius, $fallback--panelRadius); + } - &: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); + &:last-child a { + border-bottom-right-radius: $fallback--panelRadius; + border-bottom-right-radius: var(--panelRadius, $fallback--panelRadius); + border-bottom-left-radius: $fallback--panelRadius; + border-bottom-left-radius: var(--panelRadius, $fallback--panelRadius); + } } - &.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); + li:last-child { + border: none; + } + + a { + display: block; + box-sizing: border-box; + align-items: stretch; + height: 3.5em; + line-height: 3.5em; + padding: 0 1em; &:hover { - text-decoration: underline; + 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; + } } } -} -.nav-panel .button-icon:before { - width: 1.1em; + .fa-scale-110 { + margin-right: 0.8em; + } + + .badge { + position: absolute; + right: 0.6rem; + top: 1.25em; + } } </style> diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js index bb906b50..4aa9affd 100644 --- a/src/components/notification/notification.js +++ b/src/components/notification/notification.js @@ -7,6 +7,28 @@ import Timeago from '../timeago/timeago.vue' import { isStatusNotification } from '../../services/notification_utils/notification_utils.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCheck, + faTimes, + faStar, + faRetweet, + faUserPlus, + faEyeSlash, + faUser, + faSuitcaseRolling +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCheck, + faTimes, + faStar, + faRetweet, + faUserPlus, + faUser, + faEyeSlash, + faSuitcaseRolling +) const Notification = { data () { diff --git a/src/components/notification/notification.scss b/src/components/notification/notification.scss index d0e63d81..f5905560 100644 --- a/src/components/notification/notification.scss +++ b/src/components/notification/notification.scss @@ -1,3 +1,5 @@ +@import '../../_variables.scss'; + // TODO Copypaste from Status, should unify it somehow .Notification { &.-muted { @@ -49,4 +51,34 @@ display: block; } } + + .type-icon { + margin: 0 0.1em; + } + + &.-type--repeat .type-icon { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); + } + + &.-type--follow .type-icon { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + + &.-type--follow-request .type-icon { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + + &.-type--like .type-icon { + color: orange; + color: $fallback--cOrange; + color: var(--cOrange, $fallback--cOrange); + } + + &.-type--move .type-icon { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } } diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue index 7fac3840..2bbde108 100644 --- a/src/components/notification/notification.vue +++ b/src/components/notification/notification.vue @@ -1,5 +1,5 @@ <template> - <status + <Status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status" @@ -18,12 +18,15 @@ href="#" class="unmute" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + ><FAIcon + class="fa-scale-110 fa-old-padding" + icon="eye-slash" + /></a> </div> <div v-else - class="non-mention" - :class="[userClass, { highlighted: userStyle }]" + class="Notification non-mention" + :class="[userClass, { highlighted: userStyle }, '-type--' + notification.type]" :style="[ userStyle ]" > <a @@ -60,26 +63,39 @@ :title="'@'+notification.from_profile.screen_name" >{{ notification.from_profile.name }}</span> <span v-if="notification.type === 'like'"> - <i class="fa icon-star lit" /> + <FAIcon + class="type-icon" + icon="star" + /> <small>{{ $t('notifications.favorited_you') }}</small> </span> <span v-if="notification.type === 'repeat'"> - <i - class="fa icon-retweet lit" + <FAIcon + class="type-icon" + icon="retweet" :title="$t('tool_tip.repeat')" /> <small>{{ $t('notifications.repeated_you') }}</small> </span> <span v-if="notification.type === 'follow'"> - <i class="fa icon-user-plus lit" /> + <FAIcon + class="type-icon" + icon="user-plus" + /> <small>{{ $t('notifications.followed_you') }}</small> </span> <span v-if="notification.type === 'follow_request'"> - <i class="fa icon-user lit" /> + <FAIcon + class="type-icon" + icon="user" + /> <small>{{ $t('notifications.follow_request') }}</small> </span> <span v-if="notification.type === 'move'"> - <i class="fa icon-arrow-curved lit" /> + <FAIcon + class="type-icon" + icon="suitcase-rolling" + /> <small>{{ $t('notifications.migrated_to') }}</small> </span> <span v-if="notification.type === 'pleroma:emoji_reaction'"> @@ -120,7 +136,10 @@ v-if="needMute" href="#" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + ><FAIcon + class="fa-scale-110 fa-old-padding" + icon="eye-slash" + /></a> </span> <div v-if="notification.type === 'follow' || notification.type === 'follow_request'" @@ -136,13 +155,15 @@ v-if="notification.type === 'follow_request'" style="white-space: nowrap;" > - <i - class="icon-ok button-icon follow-request-accept" + <FAIcon + icon="check" + class="fa-scale-110 fa-old-padding follow-request-accept" :title="$t('tool_tip.accept_follow_request')" @click="approveUser()" /> - <i - class="icon-cancel button-icon follow-request-reject" + <FAIcon + icon="times" + class="fa-scale-110 fa-old-padding follow-request-reject" :title="$t('tool_tip.reject_follow_request')" @click="denyUser()" /> diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index d951e2a8..4b479e13 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -6,6 +6,12 @@ import { filteredNotificationsFromStore, unseenNotificationsFromStore } from '../../services/notification_utils/notification_utils.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const DEFAULT_SEEN_TO_DISPLAY_COUNT = 30 diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss index c6b2a5b5..682ae127 100644 --- a/src/components/notifications/notifications.scss +++ b/src/components/notifications/notifications.scss @@ -158,37 +158,6 @@ margin-right: .2em; } - .icon-retweet.lit { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); - } - - .icon-user.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - - .icon-user-plus.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - - .icon-reply.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - - .icon-star.lit { - color: orange; - color: $fallback--cOrange; - color: var(--cOrange, $fallback--cOrange); - } - - .icon-arrow-curved.lit { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - .status-content { margin: 0; max-height: 300px; diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index d477a41b..bd875cca 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -61,7 +61,11 @@ v-else class="new-status-notification text-center panel-footer" > - <i class="icon-spin3 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="lg" + /> </div> </div> </div> diff --git a/src/components/panel_loading/panel_loading.vue b/src/components/panel_loading/panel_loading.vue index 4efebb3c..d916d8a6 100644 --- a/src/components/panel_loading/panel_loading.vue +++ b/src/components/panel_loading/panel_loading.vue @@ -1,12 +1,27 @@ <template> <div class="panel-loading"> <span class="loading-text"> - <i class="icon-spin4 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="3x" + /> {{ $t('general.loading') }} </span> </div> </template> +<script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) + +export default {} +</script> + <style lang="scss"> @import 'src/_variables.scss'; @@ -18,8 +33,7 @@ font-size: 2em; color: $fallback--text; color: var(--text, $fallback--text); - .loading-text i { - font-size: 3em; + .loading-text svg { line-height: 0; vertical-align: middle; color: $fallback--text; diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js index 5d21d720..3d94f5e7 100644 --- a/src/components/password_reset/password_reset.js +++ b/src/components/password_reset/password_reset.js @@ -1,5 +1,13 @@ import { mapState } from 'vuex' import passwordResetApi from '../../services/new_api/password_reset.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes +) const passwordReset = { data: () => ({ diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue index 713c9dce..0deb9ccf 100644 --- a/src/components/password_reset/password_reset.vue +++ b/src/components/password_reset/password_reset.vue @@ -63,10 +63,10 @@ > <span>{{ error }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissError()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> </div> @@ -122,7 +122,7 @@ padding-right: 2rem; } - .icon-cancel { + .dismiss { cursor: pointer; } } diff --git a/src/components/poll/poll_form.js b/src/components/poll/poll_form.js index df93f038..1f8df3f9 100644 --- a/src/components/poll/poll_form.js +++ b/src/components/poll/poll_form.js @@ -1,5 +1,17 @@ import * as DateUtils from 'src/services/date_utils/date_utils.js' import { uniq } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faChevronDown, + faPlus +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faChevronDown, + faPlus +) export default { name: 'PollForm', diff --git a/src/components/poll/poll_form.vue b/src/components/poll/poll_form.vue index d53f3837..31f204a0 100644 --- a/src/components/poll/poll_form.vue +++ b/src/components/poll/poll_form.vue @@ -12,6 +12,7 @@ <input :id="`poll-${index}`" v-model="options[index]" + size="1" class="poll-option-input" type="text" :placeholder="$t('polls.option')" @@ -24,8 +25,9 @@ v-if="options.length > 2" class="icon-container" > - <i - class="icon-cancel" + <FAIcon + icon="times" + class="delete" @click="deleteOption(index)" /> </div> @@ -35,7 +37,11 @@ class="add-option faint" @click="addOption" > - <i class="icon-plus" /> + <FAIcon + icon="plus" + size="sm" + /> + {{ $t("polls.add_option") }} </a> <div class="poll-type-expiry"> @@ -55,7 +61,10 @@ <option value="single">{{ $t('polls.single_choice') }}</option> <option value="multiple">{{ $t('polls.multiple_choices') }}</option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div @@ -83,7 +92,10 @@ {{ $t(`time.${unit}_short`, ['']) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </div> @@ -103,6 +115,7 @@ .add-option { align-self: flex-start; padding-top: 0.25em; + padding-left: 0.1em; cursor: pointer; } @@ -124,9 +137,17 @@ .icon-container { // Hack: Move the icon over the input box - width: 2em; - margin-left: -2em; + width: 1.5em; + margin-left: -1.5em; z-index: 1; + + .delete { + cursor: pointer; + + &:hover { + color: inherit; + } + } } .poll-type-expiry { diff --git a/src/components/popover/popover.vue b/src/components/popover/popover.vue index 5c99c509..9b8680e5 100644 --- a/src/components/popover/popover.vue +++ b/src/components/popover/popover.vue @@ -27,7 +27,7 @@ <script src="./popover.js" /> -<style lang=scss> +<style lang="scss"> @import '../../_variables.scss'; .popover { @@ -96,7 +96,7 @@ &-icon { padding-left: 0.5rem; - i { + svg { margin-right: 0.25rem; color: var(--menuPopoverIcon, $fallback--icon) } @@ -111,7 +111,7 @@ --faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint); --lightText: var(--selectedMenuPopoverLightText, $fallback--lightText); --icon: var(--selectedMenuPopoverIcon, $fallback--icon); - i { + svg { color: var(--selectedMenuPopoverIcon, $fallback--icon); } } diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index ad149506..de583269 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -12,6 +12,27 @@ import suggestor from '../emoji_input/suggestor.js' import { mapGetters, mapState } from 'vuex' import Checkbox from '../checkbox/checkbox.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faSmileBeam, + faPollH, + faUpload, + faBan, + faTimes, + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faSmileBeam, + faPollH, + faUpload, + faBan, + faTimes, + faCircleNotch +) + const buildMentionsString = ({ user, attentions = [] }, currentUser) => { let allAttentions = [...attentions] @@ -54,7 +75,8 @@ const PostStatusForm = { 'autoFocus', 'fileLimit', 'submitOnEnter', - 'emojiPickerPlacement' + 'emojiPickerPlacement', + 'optimisticPosting' ], components: { MediaUpload, @@ -251,7 +273,7 @@ const PostStatusForm = { if (this.preview) this.previewStatus() }, async postStatus (event, newStatus, opts = {}) { - if (this.posting) { return } + if (this.posting && !this.optimisticPosting) { return } if (this.disableSubmit) { return } if (this.emojiInputShown) { return } if (this.submitOnEnter) { @@ -259,6 +281,8 @@ const PostStatusForm = { event.preventDefault() } + if (this.optimisticPosting && (this.emptyStatus || this.isOverLengthLimit)) { return } + if (this.emptyStatus) { this.error = this.$t('post_status.empty_status_error') return diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index d67d9ae9..42d3152b 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -12,10 +12,11 @@ v-show="showDropIcon !== 'hide'" :style="{ animation: showDropIcon === 'show' ? 'fade-in 0.25s' : 'fade-out 0.5s' }" class="drop-indicator" - :class="[uploadFileLimitReached ? 'icon-block' : 'icon-upload']" @dragleave="fileDragStop" @drop.stop="fileDrop" - /> + > + <FAIcon :icon="uploadFileLimitReached ? 'ban' : 'upload'" /> + </div> <div class="form-group"> <i18n v-if="!$store.state.users.currentUser.locked && newStatus.visibility == 'private' && !disableLockWarning" @@ -36,10 +37,10 @@ > <span>{{ $t('post_status.scope_notice.public') }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissScopeNotice()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> <p @@ -48,10 +49,10 @@ > <span>{{ $t('post_status.scope_notice.unlisted') }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissScopeNotice()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> <p @@ -60,10 +61,10 @@ > <span>{{ $t('post_status.scope_notice.private') }}</span> <a - class="button-icon dismiss" + class="fa-scale-110 fa-old-padding dismiss" @click.prevent="dismissScopeNotice()" > - <i class="icon-cancel" /> + <FAIcon icon="times" /> </a> </p> <p @@ -82,12 +83,18 @@ @click.stop.prevent="togglePreview" > {{ $t('post_status.preview') }} - <i :class="showPreview ? 'icon-left-open' : 'icon-right-open'" /> + <FAIcon :icon="showPreview ? 'chevron-left' : 'chevron-right'" /> </a> - <i + <div v-show="previewLoading" - class="icon-spin3 animate-spin" - /> + class="preview-spinner" + > + <FAIcon + class="fa-old-padding" + spin + icon="circle-notch" + /> + </div> </div> <div v-if="showPreview" @@ -122,7 +129,8 @@ v-model="newStatus.spoilerText" type="text" :placeholder="$t('post_status.content_warning')" - :disabled="posting" + :disabled="posting && !optimisticPosting" + size="1" class="form-post-subject" > </EmojiInput> @@ -147,7 +155,7 @@ :placeholder="placeholder || $t('post_status.default')" rows="1" cols="1" - :disabled="posting" + :disabled="posting && !optimisticPosting" class="form-post-body" :class="{ 'scrollable-form': !!maxHeight }" @keydown.exact.enter="submitOnEnter && postStatus($event, newStatus)" @@ -198,7 +206,10 @@ {{ $t(`post_status.content_type["${postFormat}"]`) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div @@ -235,22 +246,22 @@ <div class="emoji-icon" > - <i + <div :title="$t('emoji.add_emoji')" - class="icon-smile btn btn-default" + class="btn btn-default" @click="showEmojiPicker" - /> + > + <FAIcon icon="smile-beam" /> + </div> </div> <div v-if="pollsAvailable" class="poll-icon" :class="{ selected: pollFormVisible }" + :title="$t('polls.add_poll')" + @click="togglePollForm" > - <i - :title="$t('polls.add_poll')" - class="icon-chart-bar btn btn-default" - @click="togglePollForm" - /> + <FAIcon icon="poll-h" /> </div> </div> <button @@ -283,8 +294,9 @@ class="alert error" > Error: {{ error }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearError" /> </div> @@ -294,8 +306,9 @@ :key="file.url" class="media-upload-wrapper" > - <i - class="fa button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="removeMediaFile(file)" /> <attachment @@ -375,24 +388,19 @@ } .preview-heading { - padding-left: 0.5em; display: flex; - width: 100%; - - .icon-spin3 { - margin-left: auto; - } + padding-left: 0.5em; } .preview-toggle { - display: flex; + flex: 1; cursor: pointer; user-select: none; &:hover { text-decoration: underline; } - i { + svg, i { margin-left: 0.2em; font-size: 0.8em; transform: rotate(90deg); @@ -434,18 +442,20 @@ .media-upload-icon, .poll-icon, .emoji-icon { font-size: 26px; + line-height: 1.1; flex: 1; + padding: 0 0.1em; &.selected, &:hover { // needs to be specific to override icon default color - i, label { + svg, i, label { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); } } &.disabled { - i { + svg, i { cursor: not-allowed; color: $fallback--icon; color: var(--btnDisabledText, $fallback--icon); @@ -474,7 +484,7 @@ text-align: right; } - .icon-chart-bar { + .poll-icon { cursor: pointer; } @@ -487,19 +497,6 @@ margin-bottom: .5em; width: 18em; - .icon-cancel { - display: inline-block; - position: static; - margin: 0; - padding-bottom: 0; - margin-left: $fallback--attachmentRadius; - margin-left: var(--attachmentRadius, $fallback--attachmentRadius); - background-color: $fallback--fg; - background-color: var(--btn, $fallback--fg); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - img, video { object-fit: contain; max-height: 10em; @@ -522,7 +519,7 @@ flex-direction: column; } - .media-upload-wrapper .attachments { + .attachments .media-upload-wrapper { padding: 0 0.5em; .attachment { @@ -531,14 +528,18 @@ position: relative; } - i { + .fa-scale-110 fa-old-padding { position: absolute; margin: 10px; - padding: 5px; + margin: .75em; + padding: .5em; background: rgba(230,230,230,0.6); + z-index: 2; + color: black; border-radius: $fallback--attachmentRadius; border-radius: var(--attachmentRadius, $fallback--attachmentRadius); font-weight: bold; + cursor: pointer; } } @@ -612,11 +613,6 @@ cursor: not-allowed; } - .icon-cancel { - cursor: pointer; - z-index: 4; - } - @keyframes fade-in { from { opacity: 0; } to { opacity: 0.6; } diff --git a/src/components/react_button/react_button.js b/src/components/react_button/react_button.js index abcf0455..de0df70c 100644 --- a/src/components/react_button/react_button.js +++ b/src/components/react_button/react_button.js @@ -1,5 +1,8 @@ import Popover from '../popover/popover.vue' -import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faSmileBeam } from '@fortawesome/free-regular-svg-icons' + +library.add(faSmileBeam) const ReactButton = { props: ['status'], @@ -29,13 +32,23 @@ const ReactButton = { emojis () { if (this.filterWord !== '') { const filterWordLowercase = this.filterWord.toLowerCase() - return this.$store.state.instance.emoji.filter(emoji => - emoji.displayText.toLowerCase().includes(filterWordLowercase) - ) + let orderedEmojiList = [] + for (const emoji of this.$store.state.instance.emoji) { + const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase) + if (indexOfFilterWord > -1) { + if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) { + orderedEmojiList[indexOfFilterWord] = [] + } + orderedEmojiList[indexOfFilterWord].push(emoji) + } + } + return orderedEmojiList.flat() } return this.$store.state.instance.emoji || [] }, - ...mapGetters(['mergedConfig']) + mergedConfig () { + return this.$store.getters.mergedConfig + } } } diff --git a/src/components/react_button/react_button.vue b/src/components/react_button/react_button.vue index 0b34add1..95d95b11 100644 --- a/src/components/react_button/react_button.vue +++ b/src/components/react_button/react_button.vue @@ -12,6 +12,7 @@ <div class="reaction-picker-filter"> <input v-model="filterWord" + size="1" :placeholder="$t('emoji.search_emoji')" > </div> @@ -36,9 +37,10 @@ <div class="reaction-bottom-fader" /> </div> </div> - <i + <FAIcon slot="trigger" - class="icon-smile button-icon add-reaction-button" + class="fa-scale-110 fa-old-padding add-reaction-button" + :icon="['far', 'smile-beam']" :title="$t('tool_tip.add_reaction')" /> </Popover> diff --git a/src/components/reply_button/reply_button.js b/src/components/reply_button/reply_button.js new file mode 100644 index 00000000..c7bd2a2b --- /dev/null +++ b/src/components/reply_button/reply_button.js @@ -0,0 +1,16 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faReply } from '@fortawesome/free-solid-svg-icons' + +library.add(faReply) + +const ReplyButton = { + name: 'ReplyButton', + props: ['status', 'replying'], + computed: { + loggedIn () { + return !!this.$store.state.users.currentUser + } + } +} + +export default ReplyButton diff --git a/src/components/reply_button/reply_button.vue b/src/components/reply_button/reply_button.vue new file mode 100644 index 00000000..a0ac8941 --- /dev/null +++ b/src/components/reply_button/reply_button.vue @@ -0,0 +1,39 @@ +<template> + <div> + <FAIcon + v-if="loggedIn" + class="ReplyButton fa-scale-110 fa-old-padding -interactive" + icon="reply" + :title="$t('tool_tip.reply')" + :class="{'-active': replying}" + @click.prevent="$emit('toggle')" + /> + <FAIcon + v-else + icon="reply" + class="ReplyButton fa-scale-110 fa-old-padding" + :title="$t('tool_tip.reply')" + /> + <span v-if="status.replies_count > 0"> + {{ status.replies_count }} + </span> + </div> +</template> + +<script src="./reply_button.js"></script> + +<style lang="scss"> +@import '../../_variables.scss'; + +.ReplyButton { + &.-interactive { + cursor: pointer; + + &:hover, + &.-active { + color: $fallback--cBlue; + color: var(--cBlue, $fallback--cBlue); + } + } +} +</style> diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js index d9a0f92e..5ee4179a 100644 --- a/src/components/retweet_button/retweet_button.js +++ b/src/components/retweet_button/retweet_button.js @@ -1,4 +1,7 @@ -import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faRetweet } from '@fortawesome/free-solid-svg-icons' + +library.add(faRetweet) const RetweetButton = { props: ['status', 'loggedIn', 'visibility'], @@ -23,12 +26,12 @@ const RetweetButton = { computed: { classes () { return { - 'retweeted': this.status.repeated, - 'retweeted-empty': !this.status.repeated, - 'animate-spin': this.animated + '-repeated': this.status.repeated } }, - ...mapGetters(['mergedConfig']) + mergedConfig () { + return this.$store.getters.mergedConfig + } } } diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue index 074f7747..b234f3d9 100644 --- a/src/components/retweet_button/retweet_button.vue +++ b/src/components/retweet_button/retweet_button.vue @@ -1,26 +1,30 @@ <template> <div v-if="loggedIn"> <template v-if="visibility !== 'private' && visibility !== 'direct'"> - <i + <FAIcon :class="classes" - class="button-icon retweet-button icon-retweet rt-active" + class="RetweetButton fa-scale-110 fa-old-padding -interactive" + icon="retweet" + :spin="animated" :title="$t('tool_tip.repeat')" @click.prevent="retweet()" /> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> </template> <template v-else> - <i + <FAIcon :class="classes" - class="button-icon icon-lock" + class="RetweetButton fa-scale-110 fa-old-padding" + icon="lock" :title="$t('timeline.no_retweet_hint')" /> </template> </div> <div v-else-if="!loggedIn"> - <i + <FAIcon :class="classes" - class="button-icon icon-retweet" + class="fa-scale-110 fa-old-padding" + icon="retweet" :title="$t('tool_tip.repeat')" /> <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span> @@ -31,16 +35,21 @@ <style lang="scss"> @import '../../_variables.scss'; -.rt-active { - cursor: pointer; - animation-duration: 0.6s; - &:hover { + +.RetweetButton { + &.-interactive { + cursor: pointer; + animation-duration: 0.6s; + + &:hover { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); + } + } + + &.-repeated { color: $fallback--cGreen; color: var(--cGreen, $fallback--cGreen); } } -.icon-retweet.retweeted { - color: $fallback--cGreen; - color: var(--cGreen, $fallback--cGreen); -} </style> diff --git a/src/components/scope_selector/scope_selector.js b/src/components/scope_selector/scope_selector.js index e9ccdefc..74bf7284 100644 --- a/src/components/scope_selector/scope_selector.js +++ b/src/components/scope_selector/scope_selector.js @@ -1,3 +1,18 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEnvelope, + faLock, + faLockOpen, + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEnvelope, + faGlobe, + faLock, + faLockOpen +) + const ScopeSelector = { props: [ 'showAll', diff --git a/src/components/scope_selector/scope_selector.vue b/src/components/scope_selector/scope_selector.vue index 291236f2..a22a4fda 100644 --- a/src/components/scope_selector/scope_selector.vue +++ b/src/components/scope_selector/scope_selector.vue @@ -1,36 +1,56 @@ <template> <div v-if="!showNothing" - class="scope-selector" + class="ScopeSelector" > - <i + <span v-if="showDirect" - class="icon-mail-alt" + class="scope" :class="css.direct" :title="$t('post_status.scope.direct')" @click="changeVis('direct')" - /> - <i + > + <FAIcon + icon="envelope" + class="fa-scale-110 fa-old-padding" + /> + </span> + <span v-if="showPrivate" - class="icon-lock" + class="scope" :class="css.private" :title="$t('post_status.scope.private')" @click="changeVis('private')" - /> - <i + > + <FAIcon + icon="lock" + class="fa-scale-110 fa-old-padding" + /> + </span> + <span v-if="showUnlisted" - class="icon-lock-open-alt" + class="scope" :class="css.unlisted" :title="$t('post_status.scope.unlisted')" @click="changeVis('unlisted')" - /> - <i + > + <FAIcon + icon="lock-open" + class="fa-scale-110 fa-old-padding" + /> + </span> + <span v-if="showPublic" - class="icon-globe" + class="scope" :class="css.public" :title="$t('post_status.scope.public')" @click="changeVis('public')" - /> + > + <FAIcon + icon="globe" + class="fa-scale-110 fa-old-padding" + /> + </span> </div> </template> @@ -39,12 +59,16 @@ <style lang="scss"> @import '../../_variables.scss'; -.scope-selector { - i { - font-size: 1.2em; +.ScopeSelector { + + .scope { + display: inline-block; cursor: pointer; + min-width: 1.3em; + min-height: 1.3em; + text-align: center; - &.selected { + &.selected svg { color: $fallback--lightText; color: var(--lightText, $fallback--lightText); } diff --git a/src/components/search/search.js b/src/components/search/search.js index 8e903052..b62bc2c5 100644 --- a/src/components/search/search.js +++ b/src/components/search/search.js @@ -2,6 +2,16 @@ import FollowCard from '../follow_card/follow_card.vue' import Conversation from '../conversation/conversation.vue' import Status from '../status/status.vue' import map from 'lodash/map' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch, + faSearch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch, + faSearch +) const Search = { components: { diff --git a/src/components/search/search.vue b/src/components/search/search.vue index 746bbaa2..665390f9 100644 --- a/src/components/search/search.vue +++ b/src/components/search/search.vue @@ -17,14 +17,18 @@ class="btn search-button" @click="newQuery(searchTerm)" > - <i class="icon-search" /> + <FAIcon icon="search" /> </button> </div> <div v-if="loading" class="text-center loading-icon" > - <i class="icon-spin3 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="lg" + /> </div> <div v-else-if="loaded"> <div class="search-nav-heading"> diff --git a/src/components/search_bar/search_bar.js b/src/components/search_bar/search_bar.js index d7d85676..551649c7 100644 --- a/src/components/search_bar/search_bar.js +++ b/src/components/search_bar/search_bar.js @@ -1,9 +1,19 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faSearch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faSearch +) + const SearchBar = { data: () => ({ searchTerm: undefined, hidden: true, - error: false, - loading: false + error: false }), watch: { '$route': function (route) { diff --git a/src/components/search_bar/search_bar.vue b/src/components/search_bar/search_bar.vue index 4d5a1aec..89a601c8 100644 --- a/src/components/search_bar/search_bar.vue +++ b/src/components/search_bar/search_bar.vue @@ -1,40 +1,47 @@ <template> - <div> - <div class="search-bar-container"> - <i - v-if="loading" - class="icon-spin4 finder-icon animate-spin-slow" - /> - <a - v-if="hidden" - href="#" - :title="$t('nav.search')" - ><i - class="button-icon icon-search" - @click.prevent.stop="toggleHidden" - /></a> - <template v-else> - <input - id="search-bar-input" - ref="searchInput" - v-model="searchTerm" - class="search-bar-input" - :placeholder="$t('nav.search')" - type="text" - @keyup.enter="find(searchTerm)" - > - <button - class="btn search-button" - @click="find(searchTerm)" - > - <i class="icon-search" /> - </button> - <i - class="button-icon icon-cancel" + <div + class="SearchBar" + :class="{ '-expanded': !hidden }" + > + <a + v-if="hidden" + href="#" + class="nav-icon" + :title="$t('nav.search')" + ><FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="search" + @click.prevent.stop="toggleHidden" + /></a> + <template v-else> + <input + id="search-bar-input" + ref="searchInput" + v-model="searchTerm" + class="search-bar-input" + :placeholder="$t('nav.search')" + type="text" + @keyup.enter="find(searchTerm)" + > + <button + class="btn search-button" + @click="find(searchTerm)" + > + <FAIcon + fixed-width + icon="search" + /> + </button> + <span> + <FAIcon + fixed-width + icon="times" + class="cancel-icon fa-scale-110 fa-old-padding" @click.prevent.stop="toggleHidden" /> - </template> - </div> + </span> + </template> </div> </template> @@ -43,30 +50,29 @@ <style lang="scss"> @import '../../_variables.scss'; -.search-bar-container { - max-width: 100%; +.SearchBar { display: inline-flex; align-items: baseline; vertical-align: baseline; justify-content: flex-end; + &.-expanded { + width: 100%; + } + .search-bar-input, .search-button { height: 29px; } .search-bar-input { - // TODO: do this properly without a rough guesstimate of 2 icons + paddings - max-width: calc(100% - 30px - 30px - 20px); - } - - .search-button { - margin-left: .5em; - margin-right: .5em; + flex: 1 0 auto; } - .icon-cancel { + .cancel-icon { cursor: pointer; + color: $fallback--text; + color: var(--btnTopBarText, $fallback--text); } } diff --git a/src/components/settings_modal/settings_modal_content.js b/src/components/settings_modal/settings_modal_content.js index ef1a5ffa..9dcf1b5a 100644 --- a/src/components/settings_modal/settings_modal_content.js +++ b/src/components/settings_modal/settings_modal_content.js @@ -10,6 +10,29 @@ import GeneralTab from './tabs/general_tab.vue' import VersionTab from './tabs/version_tab.vue' import ThemeTab from './tabs/theme_tab/theme_tab.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faWrench, + faUser, + faFilter, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faWrench, + faUser, + faFilter, + faPaintBrush, + faBell, + faDownload, + faEyeSlash, + faInfo +) + const SettingsModalContent = { components: { TabSwitcher, diff --git a/src/components/settings_modal/settings_modal_content.scss b/src/components/settings_modal/settings_modal_content.scss index a3fef1cf..f066234c 100644 --- a/src/components/settings_modal/settings_modal_content.scss +++ b/src/components/settings_modal/settings_modal_content.scss @@ -31,7 +31,7 @@ } .unavailable, - .unavailable i { + .unavailable svg { color: var(--cRed, $fallback--cRed); color: $fallback--cRed; } diff --git a/src/components/settings_modal/settings_modal_content.vue b/src/components/settings_modal/settings_modal_content.vue index bc30a0ff..c9ed2a38 100644 --- a/src/components/settings_modal/settings_modal_content.vue +++ b/src/components/settings_modal/settings_modal_content.vue @@ -37,7 +37,7 @@ </div> <div :label="$t('settings.theme')" - icon="brush" + icon="paint-brush" data-tab-name="theme" > <ThemeTab /> @@ -45,7 +45,7 @@ <div v-if="isLoggedIn" :label="$t('settings.notifications')" - icon="bell-ringing-o" + icon="bell" data-tab-name="notifications" > <NotificationsTab /> @@ -62,14 +62,14 @@ v-if="isLoggedIn" :label="$t('settings.mutes_and_blocks')" :fullHeight="true" - icon="eye-off" + icon="eye-slash" data-tab-name="mutesAndBlocks" > <MutesAndBlocksTab /> </div> <div :label="$t('settings.version.title')" - icon="info-circled" + icon="info" data-tab-name="version" > <VersionTab /> diff --git a/src/components/settings_modal/tabs/data_import_export_tab.js b/src/components/settings_modal/tabs/data_import_export_tab.js index 168f89e1..f4b736d2 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.js +++ b/src/components/settings_modal/tabs/data_import_export_tab.js @@ -1,6 +1,7 @@ import Importer from 'src/components/importer/importer.vue' import Exporter from 'src/components/exporter/exporter.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import { mapState } from 'vuex' const DataImportExportTab = { data () { @@ -18,21 +19,26 @@ const DataImportExportTab = { Checkbox }, computed: { - user () { - return this.$store.state.users.currentUser - } + ...mapState({ + backendInteractor: (state) => state.api.backendInteractor, + user: (state) => state.users.currentUser + }) }, methods: { getFollowsContent () { - return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id }) + return this.backendInteractor.exportFriends({ id: this.user.id }) .then(this.generateExportableUsersContent) }, getBlocksContent () { - return this.$store.state.api.backendInteractor.fetchBlocks() + return this.backendInteractor.fetchBlocks() + .then(this.generateExportableUsersContent) + }, + getMutesContent () { + return this.backendInteractor.fetchMutes() .then(this.generateExportableUsersContent) }, importFollows (file) { - return this.$store.state.api.backendInteractor.importFollows({ file }) + return this.backendInteractor.importFollows({ file }) .then((status) => { if (!status) { throw new Error('failed') @@ -40,7 +46,15 @@ const DataImportExportTab = { }) }, importBlocks (file) { - return this.$store.state.api.backendInteractor.importBlocks({ file }) + return this.backendInteractor.importBlocks({ file }) + .then((status) => { + if (!status) { + throw new Error('failed') + } + }) + }, + importMutes (file) { + return this.backendInteractor.importMutes({ file }) .then((status) => { if (!status) { throw new Error('failed') diff --git a/src/components/settings_modal/tabs/data_import_export_tab.vue b/src/components/settings_modal/tabs/data_import_export_tab.vue index b5d0f5ed..a406077d 100644 --- a/src/components/settings_modal/tabs/data_import_export_tab.vue +++ b/src/components/settings_modal/tabs/data_import_export_tab.vue @@ -36,6 +36,23 @@ :export-button-label="$t('settings.block_export_button')" /> </div> + <div class="setting-item"> + <h2>{{ $t('settings.mute_import') }}</h2> + <p>{{ $t('settings.import_mutes_from_a_csv_file') }}</p> + <Importer + :submit-handler="importMutes" + :success-message="$t('settings.mutes_imported')" + :error-message="$t('settings.mute_import_error')" + /> + </div> + <div class="setting-item"> + <h2>{{ $t('settings.mute_export') }}</h2> + <Exporter + :get-content="getMutesContent" + filename="mutes.csv" + :export-button-label="$t('settings.mute_export_button')" + /> + </div> </div> </template> diff --git a/src/components/settings_modal/tabs/filtering_tab.js b/src/components/settings_modal/tabs/filtering_tab.js index 3b2df556..5f38a5ae 100644 --- a/src/components/settings_modal/tabs/filtering_tab.js +++ b/src/components/settings_modal/tabs/filtering_tab.js @@ -2,6 +2,14 @@ import { filter, trim } from 'lodash' import Checkbox from 'src/components/checkbox/checkbox.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) const FilteringTab = { data () { diff --git a/src/components/settings_modal/tabs/filtering_tab.vue b/src/components/settings_modal/tabs/filtering_tab.vue index eea41514..813dc4cd 100644 --- a/src/components/settings_modal/tabs/filtering_tab.vue +++ b/src/components/settings_modal/tabs/filtering_tab.vue @@ -53,7 +53,10 @@ <option value="following">{{ $t('settings.reply_visibility_following') }}</option> <option value="self">{{ $t('settings.reply_visibility_self') }}</option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div> diff --git a/src/components/settings_modal/tabs/general_tab.js b/src/components/settings_modal/tabs/general_tab.js index 0eb37e44..df592a10 100644 --- a/src/components/settings_modal/tabs/general_tab.js +++ b/src/components/settings_modal/tabs/general_tab.js @@ -2,6 +2,16 @@ import Checkbox from 'src/components/checkbox/checkbox.vue' import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue' import SharedComputedObject from '../helpers/shared_computed_object.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown, + faGlobe +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faGlobe +) const GeneralTab = { data () { diff --git a/src/components/settings_modal/tabs/general_tab.vue b/src/components/settings_modal/tabs/general_tab.vue index 7f06d0bd..c1d0d0ec 100644 --- a/src/components/settings_modal/tabs/general_tab.vue +++ b/src/components/settings_modal/tabs/general_tab.vue @@ -58,6 +58,11 @@ {{ $t('settings.emoji_reactions_on_timeline') }} </Checkbox> </li> + <li> + <Checkbox v-model="virtualScrolling"> + {{ $t('settings.virtual_scrolling') }} + </Checkbox> + </li> </ul> </div> @@ -98,7 +103,10 @@ {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </li> @@ -122,7 +130,10 @@ {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </li> @@ -217,7 +228,7 @@ v-if="!loopSilentAvailable" class="unavailable" > - <i class="icon-globe" />! {{ $t('settings.limited_availability') }} + <FAIcon icon="globe" />! {{ $t('settings.limited_availability') }} </div> </li> </ul> diff --git a/src/components/settings_modal/tabs/profile_tab.js b/src/components/settings_modal/tabs/profile_tab.js index bd6bef6a..a3e4feaf 100644 --- a/src/components/settings_modal/tabs/profile_tab.js +++ b/src/components/settings_modal/tabs/profile_tab.js @@ -8,6 +8,18 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue' import suggestor from 'src/components/emoji_input/suggestor.js' import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faPlus, + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faPlus, + faCircleNotch +) const ProfileTab = { data () { diff --git a/src/components/settings_modal/tabs/profile_tab.scss b/src/components/settings_modal/tabs/profile_tab.scss index e14cf054..e821f952 100644 --- a/src/components/settings_modal/tabs/profile_tab.scss +++ b/src/components/settings_modal/tabs/profile_tab.scss @@ -119,10 +119,8 @@ &>.icon-container { width: 20px; - - &>.icon-cancel { - vertical-align: sub; - } + align-self: center; + margin: 0 .2em .5em; } } } diff --git a/src/components/settings_modal/tabs/profile_tab.vue b/src/components/settings_modal/tabs/profile_tab.vue index cf88c4e4..d62bc392 100644 --- a/src/components/settings_modal/tabs/profile_tab.vue +++ b/src/components/settings_modal/tabs/profile_tab.vue @@ -127,9 +127,9 @@ <div class="icon-container" > - <i + <FAIcon v-show="newFields.length > 1" - class="icon-cancel" + icon="times" @click="deleteField(i)" /> </div> @@ -139,7 +139,7 @@ class="add-field faint" @click="addField" > - <i class="icon-plus" /> + <FAIcon icon="plus" /> {{ $t("settings.profile_fields.add_field") }} </a> </div> @@ -166,10 +166,11 @@ :src="user.profile_image_url_original" class="current-avatar" > - <i + <FAIcon v-if="!isDefaultAvatar && pickAvatarBtnVisible" :title="$t('settings.reset_avatar')" - class="reset-button icon-cancel" + class="reset-button" + icon="times" type="button" @click="resetAvatar" /> @@ -194,10 +195,11 @@ <h2>{{ $t('settings.profile_banner') }}</h2> <div class="banner-background-preview"> <img :src="user.cover_photo"> - <i + <FAIcon v-if="!isDefaultBanner" :title="$t('settings.reset_profile_banner')" - class="reset-button icon-cancel" + class="reset-button" + icon="times" type="button" @click="resetBanner" /> @@ -214,9 +216,11 @@ @change="uploadFile('banner', $event)" > </div> - <i + <FAIcon v-if="bannerUploading" - class=" icon-spin4 animate-spin uploading" + class="uploading" + spin + icon="circle-notch" /> <button v-else-if="bannerPreview" @@ -230,8 +234,9 @@ class="alert error" > Error: {{ bannerUploadError }} - <i - class="button-icon icon-cancel" + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearUploadError('banner')" /> </div> @@ -240,10 +245,11 @@ <h2>{{ $t('settings.profile_background') }}</h2> <div class="banner-background-preview"> <img :src="user.background_image"> - <i + <FAIcon v-if="!isDefaultBackground" :title="$t('settings.reset_profile_background')" - class="reset-button icon-cancel" + class="reset-button" + icon="times" type="button" @click="resetBackground" /> @@ -260,9 +266,11 @@ @change="uploadFile('background', $event)" > </div> - <i + <FAIcon v-if="backgroundUploading" - class=" icon-spin4 animate-spin uploading" + class="uploading" + spin + icon="circle-notch" /> <button v-else-if="backgroundPreview" @@ -276,8 +284,10 @@ class="alert error" > Error: {{ backgroundUploadError }} - <i - class="button-icon icon-cancel" + <FAIcon + size="lg" + class="fa-scale-110 fa-old-padding" + icon="times" @click="clearUploadError('background')" /> </div> diff --git a/src/components/settings_modal/tabs/theme_tab/preview.vue b/src/components/settings_modal/tabs/theme_tab/preview.vue index 9d984659..02fea0b6 100644 --- a/src/components/settings_modal/tabs/theme_tab/preview.vue +++ b/src/components/settings_modal/tabs/theme_tab/preview.vue @@ -39,21 +39,29 @@ </i18n> <div class="icons"> - <i + <FAIcon + fixed-width style="color: var(--cBlue)" - class="button-icon icon-reply" + class="fa-scale-110 fa-old-padding" + icon="reply" /> - <i + <FAIcon + fixed-width style="color: var(--cGreen)" - class="button-icon icon-retweet" + class="fa-scale-110 fa-old-padding" + icon="retweet" /> - <i + <FAIcon + fixed-width style="color: var(--cOrange)" - class="button-icon icon-star" + class="fa-scale-110 fa-old-padding" + icon="star" /> - <i + <FAIcon + fixed-width style="color: var(--cRed)" - class="button-icon icon-cancel" + class="fa-scale-110 fa-old-padding" + icon="times" /> </div> </div> @@ -103,6 +111,25 @@ </div> </template> +<script> +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faStar, + faRetweet, + faReply +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faTimes, + faStar, + faRetweet, + faReply +) + +export default {} +</script> + <style lang="scss"> .preview-container { position: relative; diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.js b/src/components/settings_modal/tabs/theme_tab/theme_tab.js index e3c5e80a..6cf75fe7 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.js +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.js @@ -35,6 +35,14 @@ import ExportImport from 'src/components/export_import/export_import.vue' import Checkbox from 'src/components/checkbox/checkbox.vue' import Preview from './preview.vue' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown +) // List of color values used in v1 const v1OnlyNames = [ diff --git a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue index 5328c350..280e1955 100644 --- a/src/components/settings_modal/tabs/theme_tab/theme_tab.vue +++ b/src/components/settings_modal/tabs/theme_tab/theme_tab.vue @@ -80,7 +80,10 @@ {{ style[0] || style.name }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </template> @@ -907,7 +910,10 @@ {{ $t('settings.style.shadows.components.' + shadow) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> <div class="override"> diff --git a/src/components/shadow_control/shadow_control.js b/src/components/shadow_control/shadow_control.js index f9e7b985..800c39d5 100644 --- a/src/components/shadow_control/shadow_control.js +++ b/src/components/shadow_control/shadow_control.js @@ -2,6 +2,20 @@ import ColorInput from '../color_input/color_input.vue' import OpacityInput from '../opacity_input/opacity_input.vue' import { getCssShadow } from '../../services/style_setter/style_setter.js' import { hex2rgb } from '../../services/color_convert/color_convert.js' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faTimes, + faChevronDown, + faChevronUp, + faPlus +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faChevronDown, + faChevronUp, + faTimes, + faPlus +) const toModel = (object = {}) => ({ x: 0, diff --git a/src/components/shadow_control/shadow_control.vue b/src/components/shadow_control/shadow_control.vue index 815a9e59..78f0e544 100644 --- a/src/components/shadow_control/shadow_control.vue +++ b/src/components/shadow_control/shadow_control.vue @@ -78,35 +78,50 @@ {{ $t('settings.style.shadows.shadow_id', { value: index }) }} </option> </select> - <i class="icon-down-open" /> + <FAIcon + icon="chevron-down" + class="select-down-icon" + /> </label> <button class="btn btn-default" :disabled="!ready || !present" @click="del" > - <i class="icon-cancel" /> + <FAIcon + fixed-width + icon="times" + /> </button> <button class="btn btn-default" :disabled="!moveUpValid" @click="moveUp" > - <i class="icon-up-open" /> + <FAIcon + fixed-width + icon="chevron-up" + /> </button> <button class="btn btn-default" :disabled="!moveDnValid" @click="moveDn" > - <i class="icon-down-open" /> + <FAIcon + fixed-width + icon="chevron-down" + /> </button> <button class="btn btn-default" :disabled="usingFallback" @click="add" > - <i class="icon-plus" /> + <FAIcon + fixed-width + icon="plus" + /> </button> </div> <div diff --git a/src/components/side_drawer/side_drawer.js b/src/components/side_drawer/side_drawer.js index 281052e5..fe736168 100644 --- a/src/components/side_drawer/side_drawer.js +++ b/src/components/side_drawer/side_drawer.js @@ -2,6 +2,34 @@ 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 { library } from '@fortawesome/fontawesome-svg-core' +import { + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faSignInAlt, + faSignOutAlt, + faHome, + faComments, + faBell, + faUserPlus, + faBullhorn, + faSearch, + faTachometerAlt, + faCog, + faInfoCircle +) const SideDrawer = { props: [ 'logout' ], diff --git a/src/components/side_drawer/side_drawer.vue b/src/components/side_drawer/side_drawer.vue index eda5a68c..28c888fe 100644 --- a/src/components/side_drawer/side_drawer.vue +++ b/src/components/side_drawer/side_drawer.vue @@ -36,7 +36,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'login' }"> - <i class="button-icon icon-login" /> {{ $t("login.login") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-in-alt" + /> {{ $t("login.login") }} </router-link> </li> <li @@ -44,7 +48,11 @@ @click="toggleDrawer" > <router-link :to="{ name: timelinesRoute }"> - <i class="button-icon icon-home-2" /> {{ $t("nav.timelines") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="home" + /> {{ $t("nav.timelines") }} </router-link> </li> <li @@ -55,10 +63,14 @@ :to="{ name: 'chats', params: { username: currentUser.screen_name } }" style="position: relative" > - <i class="button-icon icon-chat" /> {{ $t("nav.chats") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="comments" + /> {{ $t("nav.chats") }} <span v-if="unreadChatCount" - class="badge badge-notification unread-chat-count" + class="badge badge-notification" > {{ unreadChatCount }} </span> @@ -68,7 +80,11 @@ <ul v-if="currentUser"> <li @click="toggleDrawer"> <router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="bell" + /> {{ $t("nav.interactions") }} </router-link> </li> <li @@ -76,10 +92,14 @@ @click="toggleDrawer" > <router-link to="/friend-requests"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="user-plus" + /> {{ $t("nav.friend_requests") }} <span v-if="followRequestCount > 0" - class="badge follow-request-count" + class="badge badge-notification" > {{ followRequestCount }} </span> @@ -90,7 +110,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'chat' }"> - <i class="button-icon icon-megaphone" /> {{ $t("shoutbox.title") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="bullhorn" + /> {{ $t("shoutbox.title") }} </router-link> </li> </ul> @@ -100,7 +124,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'search' }"> - <i class="button-icon icon-search" /> {{ $t("nav.search") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="search" + /> {{ $t("nav.search") }} </router-link> </li> <li @@ -108,7 +136,11 @@ @click="toggleDrawer" > <router-link :to="{ name: 'who-to-follow' }"> - <i class="button-icon icon-user-plus" /> {{ $t("nav.who_to_follow") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="user-plus" + /> {{ $t("nav.who_to_follow") }} </router-link> </li> <li @click="toggleDrawer"> @@ -116,12 +148,20 @@ href="#" @click="openSettingsModal" > - <i class="button-icon icon-cog" /> {{ $t("settings.settings") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="cog" + /> {{ $t("settings.settings") }} </a> </li> <li @click="toggleDrawer"> <router-link :to="{ name: 'about'}"> - <i class="button-icon icon-info-circled" /> {{ $t("nav.about") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="info-circle" + /> {{ $t("nav.about") }} </router-link> </li> <li @@ -132,7 +172,11 @@ href="/pleroma/admin/#/login-pleroma" target="_blank" > - <i class="button-icon icon-gauge" /> {{ $t("nav.administration") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="tachometer-alt" + /> {{ $t("nav.administration") }} </a> </li> <li @@ -143,7 +187,11 @@ href="#" @click="doLogout" > - <i class="button-icon icon-logout" /> {{ $t("login.logout") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding" + icon="sign-out-alt" + /> {{ $t("login.logout") }} </a> </li> </ul> @@ -224,8 +272,10 @@ --lightText: var(--popoverLightText, $fallback--lightText); --icon: var(--popoverIcon, $fallback--icon); - .button-icon:before { - width: 1.1em; + .badge { + position: absolute; + right: 0.7rem; + top: 1em; } } @@ -272,7 +322,6 @@ border-bottom: 1px solid; border-color: $fallback--border; border-color: var(--border, $fallback--border); - margin: 0.2em 0; } .side-drawer ul:last-child { @@ -283,8 +332,11 @@ padding: 0; a { + box-sizing: border-box; display: block; - padding: 0.5em 0.85em; + height: 3em; + line-height: 3em; + padding: 0 0.7em; &:hover { background-color: $fallback--lightBg; diff --git a/src/components/status/status.js b/src/components/status/status.js index d263da68..142e1fc6 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -1,3 +1,4 @@ +import ReplyButton from '../reply_button/reply_button.vue' import FavoriteButton from '../favorite_button/favorite_button.vue' import ReactButton from '../react_button/react_button.vue' import RetweetButton from '../retweet_button/retweet_button.vue' @@ -15,11 +16,48 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { muteWordHits } from '../../services/status_parser/status_parser.js' import { unescape, uniqBy } from 'lodash' -import { mapGetters, mapState } from 'vuex' + +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faEnvelope, + faLock, + faLockOpen, + faGlobe, + faTimes, + faRetweet, + faReply, + faExternalLinkSquareAlt, + faPlusSquare, + faSmileBeam, + faEllipsisH, + faStar, + faEyeSlash, + faEye, + faThumbtack +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faEnvelope, + faGlobe, + faLock, + faLockOpen, + faTimes, + faRetweet, + faReply, + faExternalLinkSquareAlt, + faPlusSquare, + faStar, + faSmileBeam, + faEllipsisH, + faEyeSlash, + faEye, + faThumbtack +) const Status = { name: 'Status', components: { + ReplyButton, FavoriteButton, ReactButton, RetweetButton, @@ -54,6 +92,8 @@ const Status = { replying: false, unmuted: false, userExpanded: false, + mediaPlaying: [], + suspendable: true, error: null } }, @@ -157,7 +197,7 @@ const Status = { return this.mergedConfig.hideFilteredStatuses }, hideStatus () { - return this.deleted || (this.muted && this.hideFilteredStatuses) + return (this.muted && this.hideFilteredStatuses) || this.virtualHidden }, isFocused () { // retweet or root of an expanded conversation @@ -207,23 +247,30 @@ const Status = { hidePostStats () { return this.mergedConfig.hidePostStats }, - ...mapGetters(['mergedConfig']), - ...mapState({ - betterShadow: state => state.interface.browserSupport.cssFilter, - currentUser: state => state.users.currentUser - }) + currentUser () { + return this.$store.state.users.currentUser + }, + betterShadow () { + return this.$store.state.interface.browserSupport.cssFilter + }, + mergedConfig () { + return this.$store.getters.mergedConfig + }, + isSuspendable () { + return !this.replying && this.mediaPlaying.length === 0 + } }, methods: { visibilityIcon (visibility) { switch (visibility) { case 'private': - return 'icon-lock' + return 'lock' case 'unlisted': - return 'icon-lock-open-alt' + return 'lock-open' case 'direct': - return 'icon-mail-alt' + return 'envelope' default: - return 'icon-globe' + return 'globe' } }, showError (error) { @@ -251,6 +298,12 @@ const Status = { }, generateUserProfileLink (id, name) { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) + }, + addMediaPlaying (id) { + this.mediaPlaying.push(id) + }, + removeMediaPlaying (id) { + this.mediaPlaying = this.mediaPlaying.filter(mediaId => mediaId !== id) } }, watch: { @@ -280,6 +333,9 @@ const Status = { if (this.isFocused && this.statusFromGlobalRepository.favoritedBy && this.statusFromGlobalRepository.favoritedBy.length !== num) { this.$store.dispatch('fetchFavs', this.status.id) } + }, + 'isSuspendable': function (val) { + this.suspendable = val } }, filters: { diff --git a/src/components/status/status.scss b/src/components/status/status.scss index 8d292d3f..0a94de32 100644 --- a/src/components/status/status.scss +++ b/src/components/status/status.scss @@ -7,8 +7,9 @@ $status-margin: 0.75em; min-width: 0; &:hover { - --still-image-img: visible; - --still-image-canvas: hidden; + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + --_still-image-label-visibility: hidden; } &.-focused { @@ -25,6 +26,23 @@ $status-margin: 0.75em; --icon: var(--selectedPostIcon, $fallback--icon); } + &.-conversation { + border-left-width: 4px; + border-left-style: solid; + } + + .gravestone { + padding: $status-margin; + color: $fallback--faint; + color: var(--faint, $fallback--faint); + display: flex; + + .deleted-text { + margin: 0.5em 0; + align-items: center; + } + } + .status-container { display: flex; padding: $status-margin; @@ -41,6 +59,15 @@ $status-margin: 0.75em; justify-content: flex-end; } + ._misclick-prevention & { + pointer-events: none; + + .attachments { + pointer-events: initial; + cursor: initial; + } + } + .left-side { margin-right: $status-margin; } @@ -139,11 +166,6 @@ $status-margin: 0.75em; text-overflow: ellipsis; overflow-x: hidden; } - - .icon-reply { - // mirror the icon - transform: scaleX(-1); - } } & .reply-to-popover, @@ -183,7 +205,6 @@ $status-margin: 0.75em; } .reply-to { - display: flex; position: relative; } @@ -191,7 +212,6 @@ $status-margin: 0.75em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - margin-left: 0.2em; } .replies-separator { @@ -215,16 +235,10 @@ $status-margin: 0.75em; .repeat-info { padding: 0.4em $status-margin; - line-height: 22px; - - .right-side { - display: flex; - align-content: center; - flex-wrap: wrap; - } - i { - padding: 0 0.2em; + .repeat-icon { + color: $fallback--cGreen; + color: var(--cGreen, $fallback--cGreen); } } @@ -274,18 +288,6 @@ $status-margin: 0.75em; } } - .button-reply { - &:not(.-disabled) { - cursor: pointer; - } - - &:not(.-disabled):hover, - &.-active { - color: $fallback--cBlue; - color: var(--cBlue, $fallback--cBlue); - } - } - .muted { padding: 0.25em 0.6em; height: 1.2em; diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 282ad37d..21412faa 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -10,17 +10,20 @@ class="alert error" > {{ error }} - <i - class="button-icon icon-cancel" + <span + class="fa-scale-110 fa-old-padding" @click="clearError" - /> + > + <FAIcon icon="times" /> + </span> </div> <template v-if="muted && !isPreview"> - <div class="status-csontainer muted"> + <div class="status-container muted"> <small class="status-username"> - <i + <FAIcon v-if="muted && retweet" - class="button-icon icon-retweet" + class="fa-scale-110 fa-old-padding repeat-icon" + icon="retweet" /> <router-link :to="userProfileLink"> {{ status.user.screen_name }} @@ -46,9 +49,14 @@ </small> <a href="#" - class="unmute" + class="unmute fa-scale-110 fa-old-padding" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + > + <FAIcon + icon="eye-slash" + class="fa-scale-110 fa-old-padding" + /> + </a> </div> </template> <template v-else> @@ -56,7 +64,10 @@ v-if="showPinned" class="pin" > - <i class="fa icon-pin faint" /> + <FAIcon + icon="thumbtack" + class="faint" + /> <span class="faint">{{ $t('status.pinned') }}</span> </div> <div @@ -86,8 +97,9 @@ :to="retweeterProfileLink" >{{ retweeter }}</router-link> </span> - <i - class="fa icon-retweet retweeted" + <FAIcon + icon="retweet" + class="repeat-icon" :title="$t('tool_tip.repeat')" /> {{ $t('timeline.repeated') }} @@ -95,6 +107,7 @@ </div> <div + v-if="!deleted" :class="[userClass, { highlighted: userStyle, '-repeat': retweet && !inConversation }]" :style="[ userStyle ]" class="status-container" @@ -166,15 +179,16 @@ :auto-update="60" /> </router-link> - <div + <span v-if="status.visibility" - class="button-icon visibility-icon" + class="visibility-icon" + :title="status.visibility | capitalize" > - <i - :class="visibilityIcon(status.visibility)" - :title="status.visibility | capitalize" + <FAIcon + class="fa-scale-110 fa-old-padding" + :icon="visibilityIcon(status.visibility)" /> - </div> + </span> <a v-if="!status.is_local && !isPreview" :href="status.external_url" @@ -182,22 +196,32 @@ class="source_url" title="Source" > - <i class="button-icon icon-link-ext-alt" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="external-link-square-alt" + /> + </a> + <a + v-if="expandable && !isPreview" + href="#" + title="Expand" + @click.prevent="toggleExpanded" + > + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="plus-square" + /> </a> - <template v-if="expandable && !isPreview"> - <a - href="#" - title="Expand" - @click.prevent="toggleExpanded" - > - <i class="button-icon icon-plus-squared" /> - </a> - </template> <a v-if="unmuted" href="#" @click.prevent="toggleMute" - ><i class="button-icon icon-eye-off" /></a> + > + <FAIcon + icon="eye-slash" + class="fa-scale-110 fa-old-padding" + /> + </a> </span> </div> @@ -219,7 +243,11 @@ :aria-label="$t('tool_tip.reply')" @click.prevent="gotoOriginal(status.in_reply_to_status_id)" > - <i class="button-icon reply-button icon-reply" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="reply" + flip="horizontal" + /> <span class="faint-link reply-to-text" > @@ -227,6 +255,7 @@ </span> </a> </StatusPopover> + <span v-else class="reply-to-no-popover" @@ -272,6 +301,8 @@ :no-heading="noHeading" :highlight="highlight" :focused="isFocused" + @mediaplay="addMediaPlaying($event)" + @mediapause="removeMediaPlaying($event)" /> <transition name="fade"> @@ -320,21 +351,11 @@ v-if="!noHeading && !isPreview" class="status-actions" > - <div> - <i - v-if="loggedIn" - class="button-icon button-reply icon-reply" - :title="$t('tool_tip.reply')" - :class="{'-active': replying}" - @click.prevent="toggleReplying" - /> - <i - v-else - class="button-icon button-reply -disabled icon-reply" - :title="$t('tool_tip.reply')" - /> - <span v-if="status.replies_count > 0">{{ status.replies_count }}</span> - </div> + <reply-button + :replying="replying" + :status="status" + @toggle="toggleReplying" + /> <retweet-button :visibility="status.visibility" :logged-in="loggedIn" @@ -357,6 +378,25 @@ </div> </div> <div + v-else + class="gravestone" + > + <div class="left-side"> + <UserAvatar :compact="compact" /> + </div> + <div class="right-side"> + <div class="deleted-text"> + {{ $t('status.status_deleted') }} + </div> + <reply-button + v-if="replying" + :replying="replying" + :status="status" + @toggle="toggleReplying" + /> + </div> + </div> + <div v-if="replying" class="status-container reply-form" > @@ -376,4 +416,5 @@ </template> <script src="./status.js" ></script> + <style src="./status.scss" lang="scss"></style> diff --git a/src/components/status_content/status_content.js b/src/components/status_content/status_content.js index df095de3..a6f79d76 100644 --- a/src/components/status_content/status_content.js +++ b/src/components/status_content/status_content.js @@ -7,6 +7,24 @@ import fileType from 'src/services/file_type/file_type.service' import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mapGetters, mapState } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch, + faFile, + faMusic, + faImage, + faLink, + faPollH +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch, + faFile, + faMusic, + faImage, + faLink, + faPollH +) const StatusContent = { name: 'StatusContent', diff --git a/src/components/status_content/status_content.vue b/src/components/status_content/status_content.vue index 76fe3278..321cd477 100644 --- a/src/components/status_content/status_content.vue +++ b/src/components/status_content/status_content.vue @@ -55,29 +55,29 @@ @click.prevent="toggleShowMore" > {{ $t("status.show_content") }} - <span + <FAIcon v-if="attachmentTypes.includes('image')" - class="icon-picture" + icon="image" /> - <span + <FAIcon v-if="attachmentTypes.includes('video')" - class="icon-video" + icon="video" /> - <span + <FAIcon v-if="attachmentTypes.includes('audio')" - class="icon-music" + icon="music" /> - <span + <FAIcon v-if="attachmentTypes.includes('unknown')" - class="icon-doc" + icon="file" /> - <span + <FAIcon v-if="status.poll && status.poll.options" - class="icon-chart-bar" + icon="poll-h" /> - <span + <FAIcon v-if="status.card" - class="icon-link" + icon="link" /> </a> <a @@ -107,6 +107,8 @@ :attachment="attachment" :allow-play="true" :set-media="setMedia()" + @play="$emit('mediaplay', attachment.id)" + @pause="$emit('mediapause', attachment.id)" /> <gallery v-if="galleryAttachments.length > 0" @@ -180,6 +182,10 @@ $status-margin: 0.75em; text-align: center; display: inline-block; word-break: break-all; + + svg { + color: inherit; + } } img, video { diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js index 51e7680c..c47f5631 100644 --- a/src/components/status_popover/status_popover.js +++ b/src/components/status_popover/status_popover.js @@ -1,4 +1,10 @@ import { find } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const StatusPopover = { name: 'StatusPopover', diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue index 162eb210..8237ce00 100644 --- a/src/components/status_popover/status_popover.vue +++ b/src/components/status_popover/status_popover.vue @@ -27,7 +27,11 @@ v-else class="status-preview-no-content" > - <i class="icon-spin4 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="2x" + /> </div> </div> </Popover> diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js index ab40bbd7..8044e994 100644 --- a/src/components/still-image/still-image.js +++ b/src/components/still-image/still-image.js @@ -19,14 +19,16 @@ const StillImage = { }, methods: { onLoad () { - this.imageLoadHandler && this.imageLoadHandler(this.$refs.src) + const image = this.$refs.src + if (!image) return + this.imageLoadHandler && this.imageLoadHandler(image) const canvas = this.$refs.canvas if (!canvas) return - const width = this.$refs.src.naturalWidth - const height = this.$refs.src.naturalHeight + const width = image.naturalWidth + const height = image.naturalHeight canvas.width = width canvas.height = height - canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height) + canvas.getContext('2d').drawImage(image, 0, 0, width, height) }, onError () { this.imageLoadError && this.imageLoadError() diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue index ad82210d..d3eb5925 100644 --- a/src/components/still-image/still-image.vue +++ b/src/components/still-image/still-image.vue @@ -42,7 +42,7 @@ width: 100%; height: 100%; object-fit: contain; - visibility: var(--still-image-canvas, visible); + visibility: var(--_still-image-canvas-visibility, visible); } img { @@ -66,16 +66,19 @@ border-radius: $fallback--tooltipRadius; border-radius: var(--tooltipRadius, $fallback--tooltipRadius); z-index: 2; - visibility: var(--still-image-label-visibility, visible); + visibility: var(--_still-image-label-visibility, visible); } &:hover canvas { display: none; } - &:hover::before, + &:hover::before { + visibility: var(--_still-image-label-visibility, hidden); + } + img { - visibility: var(--still-image-img, hidden); + visibility: var(--_still-image-img-visibility, hidden); } &:hover img { diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js index 9c1da354..6e6e8193 100644 --- a/src/components/tab_switcher/tab_switcher.js +++ b/src/components/tab_switcher/tab_switcher.js @@ -1,5 +1,6 @@ import Vue from 'vue' import { mapState } from 'vuex' +import { FontAwesomeIcon as FAIcon } from '@fortawesome/vue-fontawesome' import './tab_switcher.scss' @@ -107,7 +108,7 @@ export default Vue.component('tab-switcher', { class={classesTab.join(' ')} type="button" > - {!slot.data.attrs.icon ? '' : (<i class={'tab-icon icon-' + slot.data.attrs.icon}/>)} + {!slot.data.attrs.icon ? '' : (<FAIcon class="tab-icon" size="2x" fixed-width icon={slot.data.attrs.icon}/>)} <span class="text"> {slot.data.attrs.label} </span> diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss index d2ef4857..0ed614b7 100644 --- a/src/components/tab_switcher/tab_switcher.scss +++ b/src/components/tab_switcher/tab_switcher.scss @@ -4,7 +4,7 @@ display: flex; .tab-icon { - font-size: 2em; + margin: 0.2em auto; display: block; } @@ -91,7 +91,7 @@ flex-direction: column; @media all and (max-width: 800px) { - min-width: 1em; + min-width: 4em; } &:not(.active)::after { diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 5a7f7a78..cba46daf 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -2,7 +2,13 @@ import Status from '../status/status.vue' import timelineFetcher from '../../services/timeline_fetcher/timeline_fetcher.service.js' import Conversation from '../conversation/conversation.vue' import TimelineMenu from '../timeline_menu/timeline_menu.vue' -import { throttle, keyBy } from 'lodash' +import { debounce, throttle, keyBy } from 'lodash' +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) export const getExcludedStatusIdsByPinning = (statuses, pinnedStatusIds) => { const ids = [] @@ -33,7 +39,9 @@ const Timeline = { return { paused: false, unfocused: false, - bottomedOut: false + bottomedOut: false, + virtualScrollIndex: 0, + blockingClicks: false } }, components: { @@ -63,8 +71,10 @@ const Timeline = { } }, classes () { + let rootClasses = !this.embedded ? ['panel', 'panel-default'] : [] + if (this.blockingClicks) rootClasses = rootClasses.concat(['-blocked', '_misclick-prevention']) return { - root: ['timeline'].concat(!this.embedded ? ['panel', 'panel-default'] : []), + root: rootClasses, header: ['timeline-heading'].concat(!this.embedded ? ['panel-heading'] : []), body: ['timeline-body'].concat(!this.embedded ? ['panel-body'] : []), footer: ['timeline-footer'].concat(!this.embedded ? ['panel-footer'] : []) @@ -78,6 +88,16 @@ const Timeline = { }, pinnedStatusIdsObject () { return keyBy(this.pinnedStatusIds) + }, + statusesToDisplay () { + const amount = this.timeline.visibleStatuses.length + const statusesPerSide = Math.ceil(Math.max(3, window.innerHeight / 80)) + const min = Math.max(0, this.virtualScrollIndex - statusesPerSide) + const max = Math.min(amount, this.virtualScrollIndex + statusesPerSide) + return this.timeline.visibleStatuses.slice(min, max).map(_ => _.id) + }, + virtualScrollingEnabled () { + return this.$store.getters.mergedConfig.virtualScrolling } }, created () { @@ -85,7 +105,7 @@ const Timeline = { const credentials = store.state.users.currentUser.credentials const showImmediately = this.timeline.visibleStatuses.length === 0 - window.addEventListener('scroll', this.scrollLoad) + window.addEventListener('scroll', this.handleScroll) if (store.state.api.fetchers[this.timelineName]) { return false } @@ -104,14 +124,24 @@ const Timeline = { this.unfocused = document.hidden } window.addEventListener('keydown', this.handleShortKey) + setTimeout(this.determineVisibleStatuses, 250) }, destroyed () { - window.removeEventListener('scroll', this.scrollLoad) + window.removeEventListener('scroll', this.handleScroll) window.removeEventListener('keydown', this.handleShortKey) if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false) this.$store.commit('setLoading', { timeline: this.timelineName, value: false }) }, methods: { + stopBlockingClicks: debounce(function () { + this.blockingClicks = false + }, 1000), + blockClicksTemporarily () { + if (!this.blockingClicks) { + this.blockingClicks = true + } + this.stopBlockingClicks() + }, handleShortKey (e) { // Ignore when input fields are focused if (['textarea', 'input'].includes(e.target.tagName.toLowerCase())) return @@ -123,6 +153,7 @@ const Timeline = { this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 }) this.fetchOlderStatuses() } else { + this.blockClicksTemporarily() this.$store.commit('showNewStatuses', { timeline: this.timelineName }) this.paused = false } @@ -146,6 +177,48 @@ const Timeline = { } }) }, 1000, this), + determineVisibleStatuses () { + if (!this.$refs.timeline) return + if (!this.virtualScrollingEnabled) return + + const statuses = this.$refs.timeline.children + const cappedScrollIndex = Math.max(0, Math.min(this.virtualScrollIndex, statuses.length - 1)) + + if (statuses.length === 0) return + + const height = Math.max(document.body.offsetHeight, window.pageYOffset) + + const centerOfScreen = window.pageYOffset + (window.innerHeight * 0.5) + + // Start from approximating the index of some visible status by using the + // the center of the screen on the timeline. + let approxIndex = Math.floor(statuses.length * (centerOfScreen / height)) + let err = statuses[approxIndex].getBoundingClientRect().y + + // if we have a previous scroll index that can be used, test if it's + // closer than the previous approximation, use it if so + + const virtualScrollIndexY = statuses[cappedScrollIndex].getBoundingClientRect().y + if (Math.abs(err) > virtualScrollIndexY) { + approxIndex = cappedScrollIndex + err = virtualScrollIndexY + } + + // if the status is too far from viewport, check the next/previous ones if + // they happen to be better + while (err < -20 && approxIndex < statuses.length - 1) { + err += statuses[approxIndex].offsetHeight + approxIndex++ + } + while (err > window.innerHeight + 100 && approxIndex > 0) { + approxIndex-- + err -= statuses[approxIndex].offsetHeight + } + + // this status is now the center point for virtual scrolling and visible + // statuses will be nearby statuses before and after it + this.virtualScrollIndex = approxIndex + }, scrollLoad (e) { const bodyBRect = document.body.getBoundingClientRect() const height = Math.max(bodyBRect.height, -(bodyBRect.y)) @@ -155,6 +228,10 @@ const Timeline = { this.fetchOlderStatuses() } }, + handleScroll: throttle(function (e) { + this.determineVisibleStatuses() + this.scrollLoad(e) + }, 200), handleVisibilityChange () { this.unfocused = document.hidden } diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 2ff933e9..04859852 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -1,5 +1,5 @@ <template> - <div :class="[classes.root, 'timeline']"> + <div :class="[classes.root, 'Timeline']"> <div :class="classes.header"> <TimelineMenu v-if="!embedded" /> <div @@ -32,7 +32,10 @@ </div> </div> <div :class="classes.body"> - <div class="timeline"> + <div + ref="timeline" + class="timeline" + > <template v-for="statusId in pinnedStatusIds"> <conversation v-if="timeline.statusesObject[statusId]" @@ -54,6 +57,7 @@ :collapsable="true" :in-profile="inProfile" :profile-user-id="userId" + :virtual-hidden="virtualScrollingEnabled && !statusesToDisplay.includes(status.id)" /> </template> </div> @@ -88,7 +92,11 @@ v-else class="new-status-notification text-center panel-footer" > - <i class="icon-spin3 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="lg" + /> </div> </div> </div> @@ -99,10 +107,14 @@ <style lang="scss"> @import '../../_variables.scss'; -.timeline { +.Timeline { .loadmore-text { opacity: 1; } + + &.-blocked { + cursor: progress; + } } .timeline-heading { diff --git a/src/components/timeline_menu/timeline_menu.js b/src/components/timeline_menu/timeline_menu.js index 2be75b06..4ccd52b4 100644 --- a/src/components/timeline_menu/timeline_menu.js +++ b/src/components/timeline_menu/timeline_menu.js @@ -1,5 +1,23 @@ import Popover from '../popover/popover.vue' import { mapState } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faChevronDown +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faUsers, + faGlobe, + faBookmark, + faEnvelope, + faHome, + faChevronDown +) // Route -> i18n key mapping, exported andnot in the computed // because nav panel benefits from the same information. diff --git a/src/components/timeline_menu/timeline_menu.vue b/src/components/timeline_menu/timeline_menu.vue index be512d60..c46531be 100644 --- a/src/components/timeline_menu/timeline_menu.vue +++ b/src/components/timeline_menu/timeline_menu.vue @@ -1,7 +1,7 @@ <template> <Popover trigger="click" - class="timeline-menu" + class="TimelineMenu" :class="{ 'open': isOpen }" :margin="{ left: -15, right: -200 }" :bound-to="{ x: 'container' }" @@ -16,27 +16,47 @@ <ul> <li v-if="currentUser"> <router-link :to="{ name: 'friends' }"> - <i class="button-icon icon-home-2" />{{ $t("nav.timeline") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="home" + />{{ $t("nav.timeline") }} </router-link> </li> <li v-if="currentUser"> <router-link :to="{ name: 'bookmarks'}"> - <i class="button-icon icon-bookmark" />{{ $t("nav.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 :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> - <i class="button-icon icon-mail-alt" />{{ $t("nav.dms") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="envelope" + />{{ $t("nav.dms") }} </router-link> </li> <li v-if="currentUser || !privateMode"> <router-link :to="{ name: 'public-timeline' }"> - <i class="button-icon icon-users" />{{ $t("nav.public_tl") }} + <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 :to="{ name: 'public-external-timeline' }"> - <i class="button-icon icon-globe" />{{ $t("nav.twkn") }} + <FAIcon + fixed-width + class="fa-scale-110 fa-old-padding " + icon="globe" + />{{ $t("nav.twkn") }} </router-link> </li> </ul> @@ -46,7 +66,10 @@ class="title timeline-menu-title" > <span>{{ timelineName() }}</span> - <i class="icon-down-open" /> + <FAIcon + size="sm" + icon="chevron-down" + /> </div> </Popover> </template> @@ -56,17 +79,19 @@ <style lang="scss"> @import '../../_variables.scss'; -.timeline-menu { +.TimelineMenu { flex-shrink: 1; margin-right: auto; min-width: 0; width: 24rem; + .timeline-menu-popover-wrap { overflow: hidden; // Match panel heading padding to line up menu with bottom of heading margin-top: 0.6rem; padding: 0 15px 15px 15px; } + .timeline-menu-popover { width: 24rem; max-width: 100vw; @@ -77,10 +102,12 @@ transform: translateY(-100%); transition: transform 100ms; } + .panel::after { border-top-right-radius: 0; border-top-left-radius: 0; } + &.open .timeline-menu-popover { transform: translateY(0); } @@ -88,7 +115,6 @@ .timeline-menu-title { margin: 0; cursor: pointer; - display: flex; user-select: none; width: 100%; @@ -98,15 +124,13 @@ white-space: nowrap; } - i { + svg { margin-left: 0.6em; - flex-shrink: 0; - font-size: 1rem; transition: transform 100ms; } } - &.open .timeline-menu-title i { + &.open .timeline-menu-title svg { color: $fallback--text; color: var(--panelText, $fallback--text); transform: rotate(180deg); @@ -138,15 +162,11 @@ &:last-child { border: none; } - - i { - margin: 0 0.5em; - } } a { display: block; - padding: 0.6em 0; + padding: 0.6em 0.65em; &:hover { background-color: $fallback--lightBg; @@ -174,6 +194,11 @@ text-decoration: underline; } } + + svg { + margin-right: 0.4em; + margin-left: -0.2em; + } } } diff --git a/src/components/user_avatar/user_avatar.vue b/src/components/user_avatar/user_avatar.vue index e4e4127c..0f7c584b 100644 --- a/src/components/user_avatar/user_avatar.vue +++ b/src/components/user_avatar/user_avatar.vue @@ -1,5 +1,6 @@ <template> <StillImage + v-if="user" class="Avatar" :alt="user.screen_name" :title="user.screen_name" @@ -7,6 +8,11 @@ :class="{ 'avatar-compact': compact, 'better-shadow': betterShadow }" :image-load-error="imageLoadError" /> + <div + v-else + class="Avatar -placeholder" + :class="{ 'avatar-compact': compact }" + /> </template> <script src="./user_avatar.js"></script> @@ -14,11 +20,14 @@ @import '../../_variables.scss'; .Avatar { - --still-image-label-visibility: hidden; + --_avatarShadowBox: var(--avatarStatusShadow); + --_avatarShadowFilter: var(--avatarStatusShadowFilter); + --_avatarShadowInset: var(--avatarStatusShadowInset); + --_still-image-label-visibility: hidden; width: 48px; height: 48px; - box-shadow: var(--avatarStatusShadow); + box-shadow: var(--_avatarShadowBox); border-radius: $fallback--avatarRadius; border-radius: var(--avatarRadius, $fallback--avatarRadius); @@ -28,8 +37,8 @@ } &.better-shadow { - box-shadow: var(--avatarStatusShadowInset); - filter: var(--avatarStatusShadowFilter) + box-shadow: var(--_avatarShadowInset); + filter: var(--_avatarShadowFilter); } &.animated::before { @@ -42,5 +51,10 @@ border-radius: $fallback--avatarAltRadius; border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius); } + + &.-placeholder { + background-color: $fallback--fg; + background-color: var(--fg, $fallback--fg); + } } </style> diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js index 8e6b9d7f..3a8efafc 100644 --- a/src/components/user_card/user_card.js +++ b/src/components/user_card/user_card.js @@ -6,6 +6,22 @@ import ModerationTools from '../moderation_tools/moderation_tools.vue' import AccountActions from '../account_actions/account_actions.vue' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' import { mapGetters } from 'vuex' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faBell, + faRss, + faChevronDown, + faSearchPlus, + faExternalLinkAlt +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faRss, + faBell, + faChevronDown, + faSearchPlus, + faExternalLinkAlt +) export default { props: [ diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue index 041bb80f..f916af9d 100644 --- a/src/components/user_card/user_card.vue +++ b/src/components/user_card/user_card.vue @@ -21,7 +21,10 @@ :user="user" /> <div class="user-info-avatar-link-overlay"> - <i class="button-icon icon-zoom-in" /> + <FAIcon + class="fa-scale-110 fa-old-padding" + icon="search-plus" + /> </div> </a> <router-link @@ -54,8 +57,12 @@ v-if="isOtherUser && !user.is_local" :href="user.statusnet_profile_url" target="_blank" + class="external-link-button" > - <i class="icon-link-ext usersettings" /> + <FAIcon + class="icon" + icon="external-link-alt" + /> </a> <AccountActions v-if="isOtherUser && loggedIn" @@ -85,7 +92,13 @@ bot </span> </template> - <span v-if="user.locked"><i class="icon icon-lock" /></span> + <span v-if="user.locked"> + <FAIcon + class="lock-icon" + icon="lock" + size="sm" + /> + </span> <span v-if="!mergedConfig.hideUserStats && !hideBio" class="dailyAvg" @@ -133,7 +146,10 @@ <option value="striped">Striped bg</option> <option value="side">Side stripe</option> </select> - <i class="icon-down-open" /> + <FAIcon + class="select-down-icon" + icon="chevron-down" + /> </label> </div> </div> @@ -150,7 +166,7 @@ :click="subscribeUser" :title="$t('user_card.subscribe')" > - <i class="icon-bell-alt" /> + <FAIcon icon="bell" /> </ProgressButton> <ProgressButton v-else @@ -158,7 +174,18 @@ :click="unsubscribeUser" :title="$t('user_card.unsubscribe')" > - <i class="icon-bell-ringing-o" /> + <FALayers> + <FAIcon + icon="rss" + transform="left-5 shrink-6 up-3 rotate-20" + flip="horizontal" + /> + <FAIcon + icon="rss" + transform="right-5 shrink-6 up-3 rotate-20" + /> + <FAIcon icon="bell" /> + </FALayers> </ProgressButton> </template> </div> @@ -255,6 +282,11 @@ .user-card { position: relative; + &:hover .Avatar { + --_still-image-img-visibility: visible; + --_still-image-canvas-visibility: hidden; + } + .panel-heading { padding: .5em 0; text-align: center; @@ -283,7 +315,7 @@ mask: linear-gradient(to top, white, transparent) bottom no-repeat, linear-gradient(to top, white, white); // Autoprefixed seem to ignore this one, and also syntax is different - -webkit-mask-composite: xor; + -webkit-mask-composite: xor; mask-composite: exclude; background-size: cover; mask-size: 100% 60%; @@ -355,20 +387,17 @@ max-height: 56px; .Avatar { + --_avatarShadowBox: var(--avatarShadow); + --_avatarShadowFilter: var(--avatarShadowFilter); + --_avatarShadowInset: var(--avatarShadowInset); + flex: 1 0 100%; width: 56px; height: 56px; - box-shadow: 0px 1px 8px rgba(0,0,0,0.75); - box-shadow: var(--avatarShadow); object-fit: cover; } } - &:hover .Avatar { - --still-image-img: visible; - --still-image-canvas: hidden; - } - &-avatar-link { position: relative; cursor: pointer; @@ -388,7 +417,7 @@ opacity: 0; transition: opacity .2s ease; - i { + svg { color: #FFF; } } @@ -398,10 +427,17 @@ } } - .usersettings { - color: $fallback--lightText; - color: var(--lightText, $fallback--lightText); - opacity: .8; + .external-link-button { + cursor: pointer; + width: 2.5em; + text-align: center; + margin: -0.5em 0; + padding: 0.5em 0; + + &:not(:hover) .icon { + color: $fallback--lightText; + color: var(--lightText, $fallback--lightText); + } } .user-summary { @@ -447,6 +483,10 @@ font-weight: light; font-size: 15px; + .lock-icon { + margin-left: 0.5em; + } + .user-screen-name { min-width: 1px; flex: 0 1 auto; @@ -508,7 +548,7 @@ padding-bottom: 0; flex: 1 0 auto; } - .userHighlightSel.select i { + .userHighlightSel.select svg { line-height: 22px; } diff --git a/src/components/user_list_popover/user_list_popover.js b/src/components/user_list_popover/user_list_popover.js index b60f2c4c..32ca2b8d 100644 --- a/src/components/user_list_popover/user_list_popover.js +++ b/src/components/user_list_popover/user_list_popover.js @@ -1,3 +1,9 @@ +import { library } from '@fortawesome/fontawesome-svg-core' +import { faCircleNotch } from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const UserListPopover = { name: 'UserListPopover', diff --git a/src/components/user_list_popover/user_list_popover.vue b/src/components/user_list_popover/user_list_popover.vue index 185c73ca..95673733 100644 --- a/src/components/user_list_popover/user_list_popover.vue +++ b/src/components/user_list_popover/user_list_popover.vue @@ -31,7 +31,11 @@ </div> </div> <div v-else> - <i class="icon-spin4 animate-spin" /> + <FAIcon + icon="circle-notch" + spin + size="3x" + /> </div> </div> </Popover> diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 201727d4..c0b55a6c 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -6,6 +6,14 @@ import Conversation from '../conversation/conversation.vue' import TabSwitcher from 'src/components/tab_switcher/tab_switcher.js' import List from '../list/list.vue' import withLoadMore from '../../hocs/with_load_more/with_load_more' +import { library } from '@fortawesome/fontawesome-svg-core' +import { + faCircleNotch +} from '@fortawesome/free-solid-svg-icons' + +library.add( + faCircleNotch +) const FollowerList = withLoadMore({ fetch: (props, $store) => $store.dispatch('fetchFollowers', props.userId), diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index c7c67c0a..f1f51840 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -122,9 +122,10 @@ </div> <div class="panel-body"> <span v-if="error">{{ error }}</span> - <i + <FAIcon v-else - class="icon-spin3 animate-spin" + spin + icon="circle-notch" /> </div> </div> @@ -142,6 +143,7 @@ .user-profile-fields { margin: 0 0.5em; + img { object-fit: contain; vertical-align: middle; @@ -156,8 +158,7 @@ .user-profile-field { display: flex; - margin: 0.25em auto; - max-width: 32em; + margin: 0.25em; border: 1px solid var(--border, $fallback--border); border-radius: $fallback--inputRadius; border-radius: var(--inputRadius, $fallback--inputRadius); diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js index f0ca7e89..107b8985 100644 --- a/src/components/video_attachment/video_attachment.js +++ b/src/components/video_attachment/video_attachment.js @@ -3,27 +3,48 @@ const VideoAttachment = { props: ['attachment', 'controls'], data () { return { - loopVideo: this.$store.getters.mergedConfig.loopVideo + blocksSuspend: false, + // Start from true because removing "loop" property seems buggy in Vue + hasAudio: true + } + }, + computed: { + loopVideo () { + if (this.$store.getters.mergedConfig.loopVideoSilentOnly) { + return !this.hasAudio + } + return this.$store.getters.mergedConfig.loopVideo } }, methods: { - onVideoDataLoad (e) { + onPlaying (e) { + this.setHasAudio(e) + if (this.loopVideo) { + this.$emit('play', { looping: true }) + return + } + this.$emit('play') + }, + onPaused (e) { + this.$emit('pause') + }, + setHasAudio (e) { const target = e.srcElement || e.target + // If hasAudio is false, we've already marked this video to not have audio, + // a video can't gain audio out of nowhere so don't bother checking again. + if (!this.hasAudio) return if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { // non-zero if video has audio track - if (target.webkitAudioDecodedByteCount > 0) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } - } else if (typeof target.mozHasAudio !== 'undefined') { + if (target.webkitAudioDecodedByteCount > 0) return + } + if (typeof target.mozHasAudio !== 'undefined') { // true if video has audio track - if (target.mozHasAudio) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } - } else if (typeof target.audioTracks !== 'undefined') { - if (target.audioTracks.length > 0) { - this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly - } + if (target.mozHasAudio) return + } + if (typeof target.audioTracks !== 'undefined') { + if (target.audioTracks.length > 0) return } + this.hasAudio = false } } } diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue index 1ffed4e0..a4bf01e8 100644 --- a/src/components/video_attachment/video_attachment.vue +++ b/src/components/video_attachment/video_attachment.vue @@ -7,7 +7,8 @@ :alt="attachment.description" :title="attachment.description" playsinline - @loadeddata="onVideoDataLoad" + @playing="onPlaying" + @pause="onPaused" /> </template> |
