Merge remote-tracking branch 'origin/develop' into settings-changed

* origin/develop: (306 commits)
  fallback if shadows aren't defined
  Translated using Weblate (Chinese (Traditional))
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Italian)
  Translated using Weblate (Ukrainian)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Italian)
  Translated using Weblate (Russian)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Russian)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  Translated using Weblate (Portuguese)
  ...
This commit is contained in:
Henry Jameson 2021-02-01 19:39:57 +02:00
commit 8958f386be
222 changed files with 11463 additions and 4132 deletions

View file

@ -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: [
@ -27,7 +35,7 @@ const AccountActions = {
this.$store.dispatch('unblockUser', this.user.id)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
this.$store.dispatch('openUserReportingModal', { userId: this.user.id })
},
openChat () {
this.$router.push({

View file

@ -1,9 +1,10 @@
<template>
<div class="account-actions">
<div class="AccountActions">
<Popover
trigger="click"
placement="bottom"
:bound-to="{ x: 'container' }"
remove-padding
>
<div
slot="content"
@ -13,14 +14,14 @@
<template v-if="relationship.following">
<button
v-if="relationship.showing_reblogs"
class="btn btn-default dropdown-item"
class="btn button-default dropdown-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!relationship.showing_reblogs"
class="btn btn-default dropdown-item"
class="btn button-default dropdown-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
@ -32,27 +33,27 @@
</template>
<button
v-if="relationship.blocking"
class="btn btn-default btn-block dropdown-item"
class="btn button-default btn-block dropdown-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="btn btn-default btn-block dropdown-item"
class="btn button-default btn-block dropdown-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
class="btn btn-default btn-block dropdown-item"
class="btn button-default btn-block dropdown-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
<button
v-if="pleromaChatMessagesAvailable"
class="btn btn-default btn-block dropdown-item"
class="btn button-default btn-block dropdown-item"
@click="openChat"
>
{{ $t('user_card.message') }}
@ -61,9 +62,12 @@
</div>
<div
slot="trigger"
class="btn btn-default ellipsis-button"
class="ellipsis-button"
>
<i class="icon-ellipsis trigger-button" />
<FAIcon
class="icon"
icon="ellipsis-v"
/>
</div>
</Popover>
</div>
@ -73,22 +77,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>

View file

@ -8,7 +8,7 @@
{{ $t('general.error_retry') }}
</p>
<button
class="btn"
class="btn button-default"
@click="retry"
>
{{ $t('general.retry') }}

View file

@ -3,6 +3,24 @@ 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,
faPlayCircle,
faTimes
} from '@fortawesome/free-solid-svg-icons'
library.add(
faFile,
faMusic,
faImage,
faVideo,
faPlayCircle,
faTimes
)
const Attachment = {
props: [
@ -39,10 +57,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'

View file

@ -12,7 +12,7 @@
:alt="attachment.description"
:title="attachment.description"
>
<span :class="placeholderIconClass" />
<FAIcon :icon="placeholderIconClass" />
<b>{{ nsfw ? "NSFW / " : "" }}</b>{{ placeholderName }}
</a>
</div>
@ -36,20 +36,19 @@
: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
<button
v-if="nsfw && hideNsfwLocal && !hidden"
class="hider"
class="button-unstyled hider"
@click.prevent="toggleHidden"
>
<a
href="#"
@click.prevent="toggleHidden"
>Hide</a>
</div>
<FAIcon icon="times" />
</button>
<a
v-if="type === 'image' && (!hidden || preloadImage)"
@ -83,9 +82,10 @@
@play="$emit('play')"
@pause="$emit('pause')"
/>
<i
<FAIcon
v-if="!allowPlay"
class="play-icon icon-play-circled"
class="play-icon"
icon="play-circle"
/>
</a>
@ -142,6 +142,10 @@
white-space: nowrap;
text-overflow: ellipsis;
max-width: 100%;
svg {
color: inherit;
}
}
.nsfw-placeholder {
@ -228,15 +232,23 @@
.hider {
position: absolute;
right: 0;
white-space: nowrap;
margin: 10px;
padding: 5px;
background: rgba(230,230,230,0.6);
font-weight: bold;
padding: 0;
z-index: 4;
line-height: 1;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
text-align: center;
width: 2em;
height: 2em;
font-size: 1.25em;
// TODO: theming? hard to theme with unknown background image color
background: rgba(230, 230, 230, 0.7);
.svg-inline--fa {
color: rgba(0, 0, 0, 0.6);
}
&:hover .svg-inline--fa {
color: rgba(0, 0, 0, 0.9);
}
}
video {

View file

@ -3,7 +3,7 @@
<div class="block-card-content-container">
<button
v-if="blocked"
class="btn btn-default"
class="btn button-default"
:disabled="progress"
@click="unblockUser"
>
@ -16,7 +16,7 @@
</button>
<button
v-else
class="btn btn-default"
class="btn button-default"
:disabled="progress"
@click="blockUser"
>

View file

@ -6,11 +6,24 @@ 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 { promiseInterval } from '../../services/promise_interval/promise_interval.js'
import { getScrollPosition, getNewTopPosition, isBottomedOut, scrollableContainerHeight } from './chat_layout_utils.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: {
@ -24,7 +37,8 @@ const Chat = {
hoveredMessageChainId: undefined,
lastScrollPosition: {},
scrollableContainerHeight: '100%',
errorLoadingChat: false
errorLoadingChat: false,
messageRetriers: {}
}
},
created () {
@ -94,7 +108,7 @@ const Chat = {
const bottomedOutBeforeUpdate = this.bottomedOut(BOTTOMED_OUT_OFFSET)
this.$nextTick(() => {
if (bottomedOutBeforeUpdate) {
this.scrollDown({ forceRead: !document.hidden })
this.scrollDown()
}
})
},
@ -200,7 +214,7 @@ const Chat = {
this.$nextTick(() => {
scrollable.scrollTo({ top: scrollable.scrollHeight, left: 0, behavior })
})
if (forceRead || this.newMessageCount > 0) {
if (forceRead) {
this.readChat()
}
},
@ -208,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)
@ -225,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({
@ -264,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 })
}
})
})
})
@ -292,42 +323,74 @@ const Chat = {
})
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 } })

View file

@ -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 {

View file

@ -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"

View file

@ -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
}

View file

@ -10,7 +10,10 @@
<span class="title">
{{ $t("chats.chats") }}
</span>
<button @click="newChat">
<button
class="button-default"
@click="newChat"
>
{{ $t("chats.new") }}
</button>
</div>

View file

@ -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>

View file

@ -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',

View file

@ -24,16 +24,13 @@
}
}
.icon-ellipsis {
.menu-icon {
cursor: pointer;
&:hover, .extra-button-popover.open & {
color: $fallback--text;
color: var(--text, $fallback--text);
}
border-radius: $fallback--chatMessageRadius;
border-radius: var(--chatMessageRadius, $fallback--chatMessageRadius);
}
.popover {
@ -101,6 +98,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);

View file

@ -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"
@ -53,18 +53,19 @@
<div slot="content">
<div class="dropdown-menu">
<button
class="dropdown-item dropdown-item-icon"
class="button-default 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="button-default menu-icon"
:title="$t('chats.more')"
>
<i class="icon-ellipsis" />
<FAIcon icon="ellipsis-h" />
</button>
</Popover>
</div>

View file

@ -5,6 +5,8 @@
</template>
<script>
import localeService from 'src/services/locale/locale.service.js'
export default {
name: 'Timeago',
props: ['date'],
@ -16,7 +18,7 @@ export default {
if (this.date.getTime() === today.getTime()) {
return this.$t('display_date.today')
} else {
return this.date.toLocaleDateString('en', { day: 'numeric', month: 'long' })
return this.date.toLocaleDateString(localeService.internalToBrowserLocale(this.$i18n.locale), { day: 'numeric', month: 'long' })
}
}
}

View file

@ -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: {

View file

@ -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;
}
}

View file

@ -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"

View file

@ -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' ],

View file

@ -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);
}

View file

@ -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>

View file

@ -10,12 +10,13 @@
class="panel-heading conversation-heading"
>
<span class="title"> {{ $t('timeline.conversation') }} </span>
<span v-if="collapsable">
<a
href="#"
@click.prevent="toggleExpanded"
>{{ $t('timeline.collapse') }}</a>
</span>
<button
v-if="collapsable"
class="button-unstyled -link"
@click.prevent="toggleExpanded"
>
{{ $t('timeline.collapse') }}
</button>
</div>
<status
v-for="status in conversation"
@ -57,13 +58,6 @@
}
&.-expanded {
.conversation-status {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-left-color: $fallback--cRed;
border-left-color: var(--cRed, $fallback--cRed);
}
.conversation-status:last-child {
border-bottom: none;
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;

View file

@ -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')
}
}
}

View file

@ -0,0 +1,117 @@
@import '../../_variables.scss';
.DesktopNav {
height: 50px;
width: 100%;
position: fixed;
a {
color: var(--topBarLink, $fallback--link);
}
.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-default {
&, 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;
height: 100%;
text-align: center;
.svg-inline--fa {
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;
}
}
}

View file

@ -0,0 +1,81 @@
<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
/>
<button
class="button-unstyled nav-icon"
@click.stop="openSettingsModal"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="cog"
:title="$t('nav.preferences')"
/>
</button>
<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>
<button
v-if="currentUser"
class="button-unstyled nav-icon"
@click.prevent="logout"
>
<FAIcon
fixed-width
class="fa-scale-110 fa-old-padding"
icon="sign-out-alt"
:title="$t('login.logout')"
/>
</button>
</div>
</div>
</nav>
</template>
<script src="./desktop_nav.js"></script>
<style src="./desktop_nav.scss" lang="scss"></style>

View file

@ -6,7 +6,7 @@
<ProgressButton
v-if="muted"
:click="unmuteDomain"
class="btn btn-default"
class="btn button-default"
>
{{ $t('domain_mute_card.unmute') }}
<template slot="progress">
@ -16,7 +16,7 @@
<ProgressButton
v-else
:click="muteDomain"
class="btn btn-default"
class="btn button-default"
>
{{ $t('domain_mute_card.mute') }}
<template slot="progress">

View file

@ -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
@ -105,7 +114,8 @@ const EmojiInput = {
showPicker: false,
temporarilyHideSuggestions: false,
keepOpen: false,
disableClickOutside: false
disableClickOutside: false,
suggestions: []
}
},
components: {
@ -115,21 +125,6 @@ const EmojiInput = {
padEmoji () {
return this.$store.getters.mergedConfig.padEmoji
},
suggestions () {
const firstchar = this.textAtCaret.charAt(0)
if (this.textAtCaret === firstchar) { return [] }
const matchedSuggestions = this.suggest(this.textAtCaret)
if (matchedSuggestions.length <= 0) {
return []
}
return take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }, index) => ({
...rest,
// eslint-disable-next-line camelcase
img: imageUrl || '',
highlighted: index === this.highlighted
}))
},
showSuggestions () {
return this.focused &&
this.suggestions &&
@ -179,6 +174,23 @@ const EmojiInput = {
watch: {
showSuggestions: function (newValue) {
this.$emit('shown', newValue)
},
textAtCaret: async function (newWord) {
const firstchar = newWord.charAt(0)
this.suggestions = []
if (newWord === firstchar) return
const matchedSuggestions = await this.suggest(newWord)
// Async: cancel if textAtCaret has changed during wait
if (this.textAtCaret !== newWord) return
if (matchedSuggestions.length <= 0) return
this.suggestions = take(matchedSuggestions, 5)
.map(({ imageUrl, ...rest }) => ({
...rest,
img: imageUrl || ''
}))
},
suggestions (newValue) {
this.$nextTick(this.resize)
}
},
methods: {

View file

@ -6,13 +6,13 @@
>
<slot />
<template v-if="enableEmojiPicker">
<div
<button
v-if="!hideEmojiButton"
class="emoji-picker-icon"
class="button-unstyled emoji-picker-icon"
@click.prevent="togglePicker"
>
<i class="icon-smile" />
</div>
<FAIcon :icon="['far', 'smile-beam']" />
</button>
<EmojiPicker
v-if="enableEmojiPicker"
ref="picker"
@ -37,7 +37,7 @@
v-for="(suggestion, index) in suggestions"
:key="index"
class="autocomplete-item"
:class="{ highlighted: suggestion.highlighted }"
:class="{ highlighted: index === highlighted }"
@click.stop.prevent="onClick($event, suggestion)"
>
<span class="image">

View file

@ -1,4 +1,3 @@
import { debounce } from 'lodash'
/**
* suggest - generates a suggestor function to be used by emoji-input
* data: object providing source information for specific types of suggestions:
@ -11,19 +10,19 @@ import { debounce } from 'lodash'
* doesn't support user linking you can just provide only emoji.
*/
const debounceUserSearch = debounce((data, input) => {
data.updateUsersList(input)
}, 500)
export default data => input => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {
return suggestEmoji(data.emoji)(input)
export default data => {
const emojiCurry = suggestEmoji(data.emoji)
const usersCurry = data.store && suggestUsers(data.store)
return input => {
const firstChar = input[0]
if (firstChar === ':' && data.emoji) {
return emojiCurry(input)
}
if (firstChar === '@' && usersCurry) {
return usersCurry(input)
}
return []
}
if (firstChar === '@' && data.users) {
return suggestUsers(data)(input)
}
return []
}
export const suggestEmoji = emojis => input => {
@ -57,50 +56,75 @@ export const suggestEmoji = emojis => input => {
})
}
export const suggestUsers = data => input => {
const noPrefix = input.toLowerCase().substr(1)
const users = data.users
export const suggestUsers = ({ dispatch, state }) => {
// Keep some persistent values in closure, most importantly for the
// custom debounce to work. Lodash debounce does not return a promise.
let suggestions = []
let previousQuery = ''
let timeout = null
let cancelUserSearch = null
const newUsers = users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
/* taking only 20 results so that sorting is a bit cheaper, we display
* only 5 anyway. could be inaccurate, but we ideally we should query
* backend anyway
*/
).slice(0, 20).sort((a, b) => {
let aScore = 0
let bScore = 0
// Matches on screen name (i.e. user@instance) makes a priority
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
// Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
const diff = (bScore - aScore) * 10
// Then sort alphabetically
const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
}).map(({ screen_name, name, profile_image_url_original }) => ({
displayText: screen_name,
detailText: name,
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
}))
// BE search users to get more comprehensive results
if (data.updateUsersList) {
debounceUserSearch(data, noPrefix)
const userSearch = (query) => dispatch('searchUsers', { query })
const debounceUserSearch = (query) => {
cancelUserSearch && cancelUserSearch()
return new Promise((resolve, reject) => {
timeout = setTimeout(() => {
userSearch(query).then(resolve).catch(reject)
}, 300)
cancelUserSearch = () => {
clearTimeout(timeout)
resolve([])
}
})
}
return async input => {
const noPrefix = input.toLowerCase().substr(1)
if (previousQuery === noPrefix) return suggestions
suggestions = []
previousQuery = noPrefix
// Fetch more and wait, don't fetch if there's the 2nd @ because
// the backend user search can't deal with it.
// Reference semantics make it so that we get the updated data after
// the await.
if (!noPrefix.includes('@')) {
await debounceUserSearch(noPrefix)
}
const newSuggestions = state.users.users.filter(
user =>
user.screen_name.toLowerCase().startsWith(noPrefix) ||
user.name.toLowerCase().startsWith(noPrefix)
).slice(0, 20).sort((a, b) => {
let aScore = 0
let bScore = 0
// Matches on screen name (i.e. user@instance) makes a priority
aScore += a.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
bScore += b.screen_name.toLowerCase().startsWith(noPrefix) ? 2 : 0
// Matches on name takes second priority
aScore += a.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
bScore += b.name.toLowerCase().startsWith(noPrefix) ? 1 : 0
const diff = (bScore - aScore) * 10
// Then sort alphabetically
const nameAlphabetically = a.name > b.name ? 1 : -1
const screenNameAlphabetically = a.screen_name > b.screen_name ? 1 : -1
return diff + nameAlphabetically + screenNameAlphabetically
/* eslint-disable camelcase */
}).map(({ screen_name, name, profile_image_url_original }) => ({
displayText: screen_name,
detailText: name,
imageUrl: profile_image_url_original,
replacement: '@' + screen_name + ' '
}))
/* eslint-enable camelcase */
suggestions = newSuggestions || []
return suggestions
}
return newUsers
/* eslint-enable camelcase */
}

View file

@ -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
@ -177,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)
}
]

View file

@ -82,7 +82,7 @@
&.active {
border-bottom: 4px solid;
i {
svg {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}

View file

@ -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>

View file

@ -6,7 +6,7 @@
:users="accountsForEmoji[reaction.name]"
>
<button
class="emoji-reaction btn btn-default"
class="emoji-reaction btn button-default"
:class="{ 'picked-reaction': reactedWith(reaction.name), 'not-clickable': !loggedIn }"
@click="emojiOnClick(reaction.name, $event)"
@mouseenter="fetchEmojiReactionsByIfMissing()"

View file

@ -2,13 +2,13 @@
<div class="import-export-container">
<slot name="before" />
<button
class="btn"
class="btn button-default"
@click="exportData"
>
{{ exportLabel }}
</button>
<button
class="btn"
class="btn button-default"
@click="importData"
>
{{ importLabel }}

View file

@ -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: {

View file

@ -1,12 +1,17 @@
<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
v-else
class="btn btn-default"
class="btn button-default"
@click="process"
>
{{ exportButtonLabel }}
@ -19,7 +24,6 @@
<style lang="scss">
.exporter {
&-processing {
font-size: 1.5em;
margin: 0.25em;
}
}

View file

@ -1,4 +1,28 @@
import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faEllipsisH,
faBookmark,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt
} from '@fortawesome/free-solid-svg-icons'
import {
faBookmark as faBookmarkReg,
faFlag
} from '@fortawesome/free-regular-svg-icons'
library.add(
faEllipsisH,
faBookmark,
faBookmarkReg,
faEyeSlash,
faThumbtack,
faShareAlt,
faExternalLinkAlt,
faFlag
)
const ExtraButtons = {
props: [ 'status' ],
@ -44,6 +68,9 @@ const ExtraButtons = {
this.$store.dispatch('unbookmark', { id: this.status.id })
.then(() => this.$emit('onSuccess'))
.catch(err => this.$emit('onError', err.error.error))
},
reportStatus () {
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
}
},
computed: {

View file

@ -1,9 +1,11 @@
<template>
<Popover
class="ExtraButtons"
trigger="click"
placement="top"
class="extra-button-popover"
:offset="{ y: 5 }"
:bound-to="{ x: 'container' }"
remove-padding
>
<div
slot="content"
@ -12,71 +14,122 @@
<div class="dropdown-menu">
<button
v-if="canMute && !status.thread_muted"
class="dropdown-item dropdown-item-icon"
class="button-default 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"
class="button-default 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"
class="dropdown-item dropdown-item-icon"
class="button-default dropdown-item dropdown-item-icon"
@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"
class="dropdown-item dropdown-item-icon"
class="button-default dropdown-item dropdown-item-icon"
@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"
class="dropdown-item dropdown-item-icon"
class="button-default dropdown-item dropdown-item-icon"
@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"
class="dropdown-item dropdown-item-icon"
class="button-default dropdown-item dropdown-item-icon"
@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"
class="dropdown-item dropdown-item-icon"
class="button-default dropdown-item dropdown-item-icon"
@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"
class="button-default 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>
<a
v-if="!status.is_local"
class="button-default dropdown-item dropdown-item-icon"
title="Source"
:href="status.external_url"
target="_blank"
>
<FAIcon
fixed-width
icon="external-link-alt"
/><span>{{ $t("status.external_source") }}</span>
</a>
<button
class="button-default dropdown-item dropdown-item-icon"
@click.prevent="reportStatus"
@click="close"
>
<FAIcon
fixed-width
:icon="['far', 'flag']"
/><span>{{ $t("user_card.report") }}</span>
</button>
</div>
</div>
<i
<span
slot="trigger"
class="icon-ellipsis button-icon"
/>
class="popover-trigger"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="ellipsis-h"
/>
</span>
</Popover>
</template>
@ -85,13 +138,16 @@
<style lang="scss">
@import '../../_variables.scss';
.icon-ellipsis {
cursor: pointer;
.ExtraButtons {
.popover-trigger {
position: static;
padding: 10px;
margin: -10px;
&:hover,
.extra-button-popover.open & {
color: $fallback--text;
color: var(--text, $fallback--text);
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
}
</style>

View file

@ -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'],
@ -21,13 +31,6 @@ const FavoriteButton = {
}
},
computed: {
classes () {
return {
'icon-star-empty': !this.status.favorited,
'icon-star': this.status.favorited,
'animate-spin': this.animated
}
},
...mapGetters(['mergedConfig'])
}
}

View file

@ -1,20 +1,31 @@
<template>
<div v-if="loggedIn">
<i
:class="classes"
class="button-icon favorite-button fav-active"
<div class="FavoriteButton">
<button
v-if="loggedIn"
class="button-unstyled interactive"
:class="status.favorited && '-favorited'"
:title="$t('tool_tip.favorite')"
@click.prevent="favorite()"
/>
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
<div v-else>
<i
:class="classes"
class="button-icon favorite-button"
:title="$t('tool_tip.favorite')"
/>
<span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
>
<FAIcon
class="fa-scale-110 fa-old-padding"
:icon="[status.favorited ? 'fas' : 'far', 'star']"
:spin="animated"
/>
</button>
<span v-else>
<FAIcon
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.favorite')"
:icon="['far', 'star']"
/>
</span>
<span
v-if="!mergedConfig.hidePostStats && status.fave_num > 0"
class="action-counter"
>
{{ status.fave_num }}
</span>
</div>
</template>
@ -23,18 +34,29 @@
<style lang="scss">
@import '../../_variables.scss';
.fav-active {
cursor: pointer;
animation-duration: 0.6s;
.FavoriteButton {
display: flex;
&:hover {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
> :first-child {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
.svg-inline--fa {
animation-duration: 0.6s;
}
&:hover .svg-inline--fa,
&.-favorited .svg-inline--fa {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
}
}
.favorite-button.icon-star {
color: $fallback--cOrange;
color: var(--cOrange, $fallback--cOrange);
}
</style>

View file

@ -1,3 +1,5 @@
import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
const FeaturesPanel = {
computed: {
chat: function () { return this.$store.state.instance.chatAvailable },
@ -6,7 +8,8 @@ const FeaturesPanel = {
whoToFollow: function () { return this.$store.state.instance.suggestionsEnabled },
mediaProxy: function () { return this.$store.state.instance.mediaProxyAvailable },
minimalScopesMode: function () { return this.$store.state.instance.minimalScopesMode },
textlimit: function () { return this.$store.state.instance.textlimit }
textlimit: function () { return this.$store.state.instance.textlimit },
uploadlimit: function () { return fileSizeFormatService.fileSizeFormat(this.$store.state.instance.uploadlimit) }
}
}

View file

@ -25,6 +25,7 @@
</li>
<li>{{ $t('features_panel.scope_options') }}</li>
<li>{{ $t('features_panel.text_limit') }} = {{ textlimit }}</li>
<li>{{ $t('features_panel.upload_limit') }} = {{ uploadlimit.num }} {{ $t('upload.file_size_units.' + uploadlimit.unit) }}</li>
</ul>
</div>
</div>

View file

@ -1,6 +1,6 @@
<template>
<button
class="btn btn-default follow-button"
class="btn button-default follow-button"
:class="{ toggled: isPressed }"
:disabled="inProgress"
:title="title"

View file

@ -2,13 +2,13 @@
<basic-user-card :user="user">
<div class="follow-request-card-content-container">
<button
class="btn btn-default"
class="btn button-default"
@click="approveUser"
>
{{ $t('user_card.approve') }}
</button>
<button
class="btn btn-default"
class="btn button-default"
@click="denyUser"
>
{{ $t('user_card.deny') }}

View file

@ -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: [

View file

@ -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"

View file

@ -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: {

View file

@ -9,10 +9,15 @@
<div class="notice-message">
{{ $t(notice.messageKey, notice.messageArgs) }}
</div>
<i
class="button-icon icon-cancel"
<button
class="button-unstyled close-notice"
@click="closeNotice(notice)"
/>
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="times"
/>
</button>
</div>
</div>
</template>
@ -53,7 +58,7 @@
.global-error {
background-color: var(--alertPopupError, $fallback--cRed);
color: var(--alertPopupErrorText, $fallback--text);
i {
.svg-inline--fa {
color: var(--alertPopupErrorText, $fallback--text);
}
}
@ -61,7 +66,7 @@
.global-warning {
background-color: var(--alertPopupWarning, $fallback--cOrange);
color: var(--alertPopupWarningText, $fallback--text);
i {
.svg-inline--fa {
color: var(--alertPopupWarningText, $fallback--text);
}
}
@ -69,9 +74,16 @@
.global-info {
background-color: var(--alertPopupNeutral, $fallback--fg);
color: var(--alertPopupNeutralText, $fallback--text);
i {
.svg-inline--fa {
color: var(--alertPopupNeutralText, $fallback--text);
}
}
.close-notice {
padding-right: 0.2em;
.svg-inline--fa:hover {
opacity: 0.6;
}
}
}
</style>

View file

@ -1,5 +1,13 @@
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faCircleNotch
} from '@fortawesome/free-solid-svg-icons'
library.add(
faCircleNotch
)
const ImageCropper = {
props: {
@ -43,8 +51,7 @@ const ImageCropper = {
cropper: undefined,
dataUrl: undefined,
filename: undefined,
submitting: false,
submitError: null
submitting: false
}
},
computed: {
@ -56,9 +63,6 @@ const ImageCropper = {
},
cancelText () {
return this.cancelButtonLabel || this.$t('image_cropper.cancel')
},
submitErrorMsg () {
return this.submitError && this.submitError instanceof Error ? this.submitError.toString() : this.submitError
}
},
methods: {
@ -72,12 +76,8 @@ const ImageCropper = {
},
submit (cropping = true) {
this.submitting = true
this.avatarUploadError = null
this.submitHandler(cropping && this.cropper, this.file)
.then(() => this.destroy())
.catch((err) => {
this.submitError = err
})
.finally(() => {
this.submitting = false
})
@ -103,9 +103,6 @@ const ImageCropper = {
reader.readAsDataURL(this.file)
this.$emit('changed', this.file, reader)
}
},
clearError () {
this.submitError = null
}
},
mounted () {

View file

@ -11,39 +11,30 @@
</div>
<div class="image-cropper-buttons-wrapper">
<button
class="btn"
class="button-default btn"
type="button"
:disabled="submitting"
@click="submit()"
v-text="saveText"
/>
<button
class="btn"
class="button-default btn"
type="button"
:disabled="submitting"
@click="destroy"
v-text="cancelText"
/>
<button
class="btn"
class="button-default btn"
type="button"
:disabled="submitting"
@click="submit(false)"
v-text="saveWithoutCroppingText"
/>
<i
<FAIcon
v-if="submitting"
class="icon-spin4 animate-spin"
/>
</div>
<div
v-if="submitError"
class="alert error"
>
{{ submitErrorMsg }}
<i
class="button-icon icon-cancel"
@click="clearError"
spin
icon="circle-notch"
/>
</div>
</div>

View file

@ -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: {

View file

@ -7,27 +7,29 @@
@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
class="btn btn-default"
class="btn button-default"
@click="submit"
>
{{ 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>

View file

@ -12,31 +12,39 @@
v-model="language"
>
<option
v-for="(langCode, i) in languageCodes"
:key="langCode"
:value="langCode"
v-for="lang in languages"
:key="lang.code"
:value="lang.code"
>
{{ languageNames[i] }}
{{ lang.name }}
</option>
</select>
<i class="icon-down-open" />
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div>
</template>
<script>
import languagesObject from '../../i18n/messages'
import localeService from '../../services/locale/locale.service.js'
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: {
languageCodes () {
return languagesObject.languages
},
languageNames () {
return _.map(this.languageCodes, this.getLanguageName)
languages () {
return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
},
language: {
@ -50,11 +58,13 @@ export default {
methods: {
getLanguageName (code) {
const specialLanguageNames = {
'ja': 'Japanese (日本語)',
'ja_easy': 'Japanese (やさしいにほんご)',
'zh': 'Chinese (简体中文)'
'ja_easy': 'やさしいにほんご',
'zh': '简体中文',
'zh_Hant': '繁體中文'
}
return specialLanguageNames[code] || ISO6391.getName(code)
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
const browserLocale = localeService.internalToBrowserLocale(code)
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
}
}
}

View file

@ -1,3 +1,5 @@
import { mapGetters } from 'vuex'
const LinkPreview = {
name: 'LinkPreview',
props: [
@ -15,11 +17,20 @@ const LinkPreview = {
// Currently BE shoudn't give cards if tagged NSFW, this is a bit paranoid
// as it makes sure to hide the image if somehow NSFW tagged preview can
// exist.
return this.card.image && !this.nsfw && this.size !== 'hide'
return this.card.image && !this.censored && this.size !== 'hide'
},
censored () {
return this.nsfw && this.hideNsfwConfig
},
useDescription () {
return this.card.description && /\S/.test(this.card.description)
}
},
hideNsfwConfig () {
return this.mergedConfig.hideNsfw
},
...mapGetters([
'mergedConfig'
])
},
created () {
if (this.useImage) {

View file

@ -9,12 +9,17 @@
<div
v-if="useImage && imageLoaded"
class="card-image"
:class="{ 'small-image': size === 'small' }"
>
<img :src="card.image">
</div>
<div class="card-content">
<span class="card-host faint">{{ card.provider_name }}</span>
<span class="card-host faint">
<span
v-if="censored"
class="nsfw-alert alert warning"
>{{ $t('status.nsfw') }}</span>
{{ card.provider_name }}
</span>
<h4 class="card-title">{{ card.title }}</h4>
<p
v-if="useDescription"
@ -50,10 +55,6 @@
}
}
.small-image {
width: 80px;
}
.card-content {
max-height: 100%;
margin: 0.5em;
@ -76,6 +77,10 @@
max-height: calc(1.2em * 3 - 1px);
}
.nsfw-alert {
margin: 2em 0;
}
color: $fallback--text;
color: var(--text, $fallback--text);
border-style: solid;

View file

@ -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: () => ({

View file

@ -61,7 +61,7 @@
<button
:disabled="loggingIn"
type="submit"
class="btn btn-default"
class="btn button-default"
>
{{ $t('login.login') }}
</button>
@ -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>

View file

@ -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: {

View file

@ -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>

View file

@ -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 {

View file

@ -1,30 +1,29 @@
<template>
<div
<label
class="media-upload"
:class="{ disabled: disabled }"
:title="$t('tool_tip.media_upload')"
>
<label
class="label"
:title="$t('tool_tip.media_upload')"
<FAIcon
v-if="uploading"
class="progress-icon"
icon="circle-notch"
spin
/>
<FAIcon
v-if="!uploading"
class="new-icon"
icon="upload"
/>
<input
v-if="uploadReady"
:disabled="disabled"
type="file"
style="position: fixed; top: -100em"
multiple="true"
@change="change"
>
<i
v-if="uploading"
class="progress-icon icon-spin4 animate-spin"
/>
<i
v-if="!uploading"
class="new-icon icon-upload"
/>
<input
v-if="uploadReady"
:disabled="disabled"
type="file"
style="position: fixed; top: -100em"
multiple="true"
@change="change"
>
</label>
</div>
</label>
</template>
<script src="./media_upload.js" ></script>
@ -33,22 +32,6 @@
@import '../../_variables.scss';
.media-upload {
.label {
display: inline-block;
}
.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;
}
}
cursor: pointer;
}
</style>

View file

@ -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: () => ({

View file

@ -23,23 +23,23 @@
<div class="form-group">
<div class="login-bottom">
<div>
<a
href="#"
<button
class="button-unstyled -link"
@click.prevent="requireTOTP"
>
{{ $t('login.enter_two_factor_code') }}
</a>
</button>
<br>
<a
href="#"
<button
class="button-unstyled -link"
@click.prevent="abortMFA"
>
{{ $t('general.cancel') }}
</a>
</button>
</div>
<button
type="submit"
class="btn btn-default"
class="btn button-default"
>
{{ $t('general.verify') }}
</button>
@ -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>

View file

@ -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,

View file

@ -25,23 +25,23 @@
<div class="form-group">
<div class="login-bottom">
<div>
<a
href="#"
<button
class="button-unstyled -link"
@click.prevent="requireRecovery"
>
{{ $t('login.enter_recovery_code') }}
</a>
</button>
<br>
<a
href="#"
<button
class="button-unstyled -link"
@click.prevent="abortMFA"
>
{{ $t('general.cancel') }}
</a>
</button>
</div>
<button
type="submit"
class="btn btn-default"
class="btn button-default"
>
{{ $t('general.verify') }}
</button>
@ -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>

View file

@ -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: {

View file

@ -1,49 +1,51 @@
<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">
<button
class="button-unstyled mobile-nav-button"
@click.stop.prevent="toggleMobileSidebar()"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="bars"
/>
<div
v-if="unreadChatCount"
class="alert-dot"
/>
</button>
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
>
{{ sitename }}
</router-link>
</div>
<div class="item right">
<button
v-if="currentUser"
class="button-unstyled mobile-nav-button"
@click.stop.prevent="openMobileNotifications()"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="bell"
/>
<div
v-if="unseenNotificationsCount"
class="alert-dot"
/>
</button>
</div>
</nav>
<div
@ -59,7 +61,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 +89,124 @@
<style lang="scss">
@import '../../_variables.scss';
.mobile-inner-nav {
width: 100%;
display: flex;
align-items: center;
}
.mobile-nav-button {
display: flex;
justify-content: center;
width: 50px;
position: relative;
cursor: pointer;
}
.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%);
.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-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;
.mobile-inner-nav {
width: 100%;
display: flex;
align-items: center;
}
}
.mobile-notifications {
margin-top: 50px;
width: 100vw;
height: calc(100vh - 50px);
overflow-x: hidden;
overflow-y: scroll;
.mobile-nav-button {
display: inline-block;
text-align: center;
padding: 0 1em;
position: relative;
cursor: pointer;
}
color: $fallback--text;
color: var(--text, $fallback--text);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
.site-name {
padding: 0 .3em;
display: inline-block;
}
.notifications {
padding: 0;
border-radius: 0;
box-shadow: none;
.panel {
border-radius: 0;
margin: 0;
box-shadow: none;
.item {
/* moslty just to get rid of extra whitespaces */
display: flex;
}
.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;
}
}
}
}

View file

@ -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',

View file

@ -1,11 +1,11 @@
<template>
<div v-if="isLoggedIn">
<button
class="new-status-button"
class="button-default new-status-button"
: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);

View file

@ -12,13 +12,13 @@
<div class="dropdown-menu">
<span v-if="user.is_local">
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleRight(&quot;admin&quot;)"
>
{{ $t(!!user.rights.admin ? 'user_card.admin_menu.revoke_admin' : 'user_card.admin_menu.grant_admin') }}
</button>
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleRight(&quot;moderator&quot;)"
>
{{ $t(!!user.rights.moderator ? 'user_card.admin_menu.revoke_moderator' : 'user_card.admin_menu.grant_moderator') }}
@ -29,13 +29,13 @@
/>
</span>
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleActivationStatus()"
>
{{ $t(!!user.deactivated ? 'user_card.admin_menu.activate_account' : 'user_card.admin_menu.deactivate_account') }}
</button>
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="deleteUserDialog(true)"
>
{{ $t('user_card.admin_menu.delete_account') }}
@ -47,7 +47,7 @@
/>
<span v-if="hasTagPolicy">
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_NSFW)"
>
{{ $t('user_card.admin_menu.force_nsfw') }}
@ -57,7 +57,7 @@
/>
</button>
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.STRIP_MEDIA)"
>
{{ $t('user_card.admin_menu.strip_media') }}
@ -67,7 +67,7 @@
/>
</button>
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.FORCE_UNLISTED)"
>
{{ $t('user_card.admin_menu.force_unlisted') }}
@ -77,7 +77,7 @@
/>
</button>
<button
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.SANDBOX)"
>
{{ $t('user_card.admin_menu.sandbox') }}
@ -88,7 +88,7 @@
</button>
<button
v-if="user.is_local"
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_REMOTE_SUBSCRIPTION)"
>
{{ $t('user_card.admin_menu.disable_remote_subscription') }}
@ -99,7 +99,7 @@
</button>
<button
v-if="user.is_local"
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.DISABLE_ANY_SUBSCRIPTION)"
>
{{ $t('user_card.admin_menu.disable_any_subscription') }}
@ -110,7 +110,7 @@
</button>
<button
v-if="user.is_local"
class="dropdown-item"
class="button-default dropdown-item"
@click="toggleTag(tags.QUARANTINE)"
>
{{ $t('user_card.admin_menu.quarantine') }}
@ -124,7 +124,7 @@
</div>
<button
slot="trigger"
class="btn btn-default btn-block"
class="btn button-default btn-block"
:class="{ toggled }"
>
{{ $t('user_card.admin_menu.moderation') }}
@ -141,13 +141,13 @@
<p>{{ $t('user_card.admin_menu.delete_user_confirmation') }}</p>
<template slot="footer">
<button
class="btn btn-default"
class="btn button-default"
@click="deleteUserDialog(false)"
>
{{ $t('general.cancel') }}
</button>
<button
class="btn btn-default danger"
class="btn button-default danger"
@click="deleteUser()"
>
{{ $t('user_card.admin_menu.delete_user') }}

View file

@ -3,7 +3,7 @@
<div class="mute-card-content-container">
<button
v-if="muted"
class="btn btn-default"
class="btn button-default"
:disabled="progress"
@click="unmuteUser"
>
@ -16,7 +16,7 @@
</button>
<button
v-else
class="btn btn-default"
class="btn button-default"
:disabled="progress"
@click="muteUser"
>

View file

@ -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) {

View file

@ -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,84 +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);
}
}
.nav-panel li:last-child {
border: none;
}
.nav-panel a {
display: block;
padding: 0.8em 0.85em;
&: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);
ul {
list-style: none;
margin: 0;
padding: 0;
}
&.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 {
position: relative;
border-bottom: 1px solid;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
padding: 0;
&:hover {
text-decoration: underline;
&: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);
}
&: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);
}
}
}
.nav-panel .button-icon {
margin-right: 0.5em;
}
li:last-child {
border: none;
}
.nav-panel .button-icon:before {
width: 1.1em;
a {
display: block;
box-sizing: border-box;
align-items: stretch;
height: 3.5em;
line-height: 3.5em;
padding: 0 1em;
&:hover {
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--link;
color: var(--selectedMenuText, $fallback--link);
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
}
&.router-link-active {
font-weight: bolder;
background-color: $fallback--lightBg;
background-color: var(--selectedMenu, $fallback--lightBg);
color: $fallback--text;
color: var(--selectedMenuText, $fallback--text);
--faint: var(--selectedMenuFaintText, $fallback--faint);
--faintLink: var(--selectedMenuFaintLink, $fallback--faint);
--lightText: var(--selectedMenuLightText, $fallback--lightText);
--icon: var(--selectedMenuIcon, $fallback--icon);
&:hover {
text-decoration: underline;
}
}
}
.fa-scale-110 {
margin-right: 0.8em;
}
.badge {
position: absolute;
right: 0.6rem;
top: 1.25em;
}
}
</style>

View file

@ -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 () {

View file

@ -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);
}
}

View file

@ -1,5 +1,5 @@
<template>
<status
<Status
v-if="notification.type === 'mention'"
:compact="true"
:statusoid="notification.status"
@ -14,16 +14,20 @@
{{ notification.from_profile.screen_name }}
</router-link>
</small>
<a
href="#"
class="unmute"
<button
class="button-unstyled unmute"
@click.prevent="toggleMute"
><i class="button-icon icon-eye-off" /></a>
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</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 +64,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'">
@ -116,11 +133,16 @@
/>
</span>
</div>
<a
<button
v-if="needMute"
href="#"
class="button-unstyled"
@click.prevent="toggleMute"
><i class="button-icon icon-eye-off" /></a>
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="eye-slash"
/>
</button>
</span>
<div
v-if="notification.type === 'follow' || notification.type === 'follow_request'"
@ -136,13 +158,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()"
/>

View file

@ -6,6 +6,13 @@ import {
filteredNotificationsFromStore,
unseenNotificationsFromStore
} from '../../services/notification_utils/notification_utils.js'
import FaviconService from '../../services/favicon_service/favicon_service.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
@ -69,8 +76,10 @@ const Notifications = {
watch: {
unseenCountTitle (count) {
if (count > 0) {
FaviconService.drawFaviconBadge()
this.$store.dispatch('setPageTitle', `(${count})`)
} else {
FaviconService.clearFaviconBadge()
this.$store.dispatch('setPageTitle', '')
}
}

View file

@ -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;

View file

@ -15,16 +15,9 @@
class="badge badge-notification unseen-count"
>{{ unseenCount }}</span>
</div>
<div
v-if="error"
class="loadmore-error alert error"
@click.prevent
>
{{ $t('timeline.error_fetching') }}
</div>
<button
v-if="unseenCount"
class="read-button"
class="button-default read-button"
@click.prevent="markAsSeen"
>
{{ $t('notifications.read') }}
@ -48,20 +41,24 @@
>
{{ $t('notifications.no_more_notifications') }}
</div>
<a
<button
v-else-if="!loading"
href="#"
class="button-unstyled -link -fullwidth"
@click.prevent="fetchOlderNotifications()"
>
<div class="new-status-notification text-center panel-footer">
{{ minimalMode ? $t('interactions.load_older') : $t('notifications.load_older') }}
</div>
</a>
</button>
<div
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>

View file

@ -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;

View file

@ -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: () => ({

View file

@ -51,7 +51,7 @@
<button
:disabled="isPending"
type="submit"
class="btn btn-default btn-block"
class="btn button-default btn-block"
>
{{ $t('general.submit') }}
</button>
@ -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;
}
}

View file

@ -42,14 +42,15 @@
:value="index"
>
<label class="option-vote">
<div>{{ option.title }}</div>
<!-- eslint-disable-next-line vue/no-v-html -->
<div v-html="option.title_html" />
</label>
</div>
</div>
<div class="footer faint">
<button
v-if="!showResults"
class="btn btn-default poll-vote-button"
class="btn button-default poll-vote-button"
type="button"
:disabled="isDisabled"
@click="vote"

View file

@ -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',

View file

@ -12,6 +12,7 @@
<input
:id="`poll-${index}`"
v-model="options[index]"
size="1"
class="poll-option-input"
type="text"
:placeholder="$t('polls.option')"
@ -20,24 +21,26 @@
@keydown.enter.stop.prevent="nextOption(index)"
>
</div>
<div
<button
v-if="options.length > 2"
class="icon-container"
class="delete-option button-unstyled -hover-highlight"
@click="deleteOption(index)"
>
<i
class="icon-cancel"
@click="deleteOption(index)"
/>
</div>
<FAIcon icon="times" />
</button>
</div>
<a
<button
v-if="options.length < maxOptions"
class="add-option faint"
class="add-option faint button-unstyled -hover-highlight"
@click="addOption"
>
<i class="icon-plus" />
<FAIcon
icon="plus"
size="sm"
/>
{{ $t("polls.add_option") }}
</a>
</button>
<div class="poll-type-expiry">
<div
class="poll-type"
@ -55,7 +58,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 +89,10 @@
{{ $t(`time.${unit}_short`, ['']) }}
</option>
</select>
<i class="icon-down-open" />
<FAIcon
class="select-down-icon"
icon="chevron-down"
/>
</label>
</div>
</div>
@ -103,7 +112,7 @@
.add-option {
align-self: flex-start;
padding-top: 0.25em;
cursor: pointer;
padding-left: 0.1em;
}
.poll-option {
@ -122,10 +131,10 @@
}
}
.icon-container {
.delete-option {
// Hack: Move the icon over the input box
width: 2em;
margin-left: -2em;
width: 1.5em;
margin-left: -1.5em;
z-index: 1;
}

View file

@ -21,7 +21,10 @@ const Popover = {
// Replaces the classes you may want for the popover container.
// Use 'popover-default' in addition to get the default popover
// styles with your custom class.
popoverClass: String
popoverClass: String,
// If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom.
removePadding: Boolean
},
data () {
return {
@ -96,9 +99,15 @@ const Popover = {
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
let vPadding = 0
if (this.removePadding && usingTop) {
const anchorStyle = getComputedStyle(anchorEl)
vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
}
const yOffset = (this.offset && this.offset.y) || 0
const translateY = usingTop
? -anchorEl.offsetHeight - yOffset - content.offsetHeight
? -anchorEl.offsetHeight + vPadding - yOffset - content.offsetHeight
: yOffset
const xOffset = (this.offset && this.offset.x) || 0

View file

@ -3,12 +3,13 @@
@mouseenter="onMouseenter"
@mouseleave="onMouseleave"
>
<div
<button
ref="trigger"
class="button-unstyled -fullwidth popover-trigger-button"
@click="onClick"
>
<slot name="trigger" />
</div>
</button>
<div
v-if="!hidden"
ref="content"
@ -27,9 +28,13 @@
<script src="./popover.js" />
<style lang=scss>
<style lang="scss">
@import '../../_variables.scss';
.popover-trigger-button {
display: block;
}
.popover {
z-index: 8;
position: absolute;
@ -90,13 +95,14 @@
box-shadow: none;
width: 100%;
height: 100%;
box-sizing: border-box;
--btnText: var(--popoverText, $fallback--text);
&-icon {
padding-left: 0.5rem;
i {
svg {
margin-right: 0.25rem;
color: var(--menuPopoverIcon, $fallback--icon)
}
@ -111,7 +117,7 @@
--faintLink: var(--selectedMenuPopoverFaintLink, $fallback--faint);
--lightText: var(--selectedMenuPopoverLightText, $fallback--lightText);
--icon: var(--selectedMenuPopoverIcon, $fallback--icon);
i {
svg {
color: var(--selectedMenuPopoverIcon, $fallback--icon);
}
}

View file

@ -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,
@ -137,8 +159,7 @@ const PostStatusForm = {
...this.$store.state.instance.emoji,
...this.$store.state.instance.customEmoji
],
users: this.$store.state.users.users,
updateUsersList: (query) => this.$store.dispatch('searchUsers', { query })
store: this.$store
})
},
emojiSuggestor () {
@ -251,7 +272,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 +280,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
@ -507,7 +530,7 @@ const PostStatusForm = {
!(isFormBiggerThanScroller &&
this.$refs.textarea.selectionStart !== this.$refs.textarea.value.length)
const totalDelta = shouldScrollToBottom ? bottomChangeDelta : 0
const targetScroll = currentScroll + totalDelta
const targetScroll = Math.round(currentScroll + totalDelta)
if (scrollerRef === window) {
scrollerRef.scroll(0, targetScroll)

View file

@ -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"
@ -23,12 +24,12 @@
tag="p"
class="visibility-notice"
>
<a
href="#"
<button
class="button-unstyled -link"
@click="openProfileTab"
>
{{ $t('post_status.account_not_locked_warning_link') }}
</a>
</button>
</i18n>
<p
v-if="!hideScopeNotice && newStatus.visibility === 'public'"
@ -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
@ -232,38 +243,34 @@
@upload-failed="uploadFailed"
@all-uploaded="finishedUploadingFiles"
/>
<div
class="emoji-icon"
<button
class="emoji-icon button-unstyled"
:title="$t('emoji.add_emoji')"
@click="showEmojiPicker"
>
<i
:title="$t('emoji.add_emoji')"
class="icon-smile btn btn-default"
@click="showEmojiPicker"
/>
</div>
<div
<FAIcon icon="smile-beam" />
</button>
<button
v-if="pollsAvailable"
class="poll-icon"
class="poll-icon button-unstyled"
: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"
/>
</div>
<FAIcon icon="poll-h" />
</button>
</div>
<button
v-if="posting"
disabled
class="btn btn-default"
class="btn button-default"
>
{{ $t('post_status.posting') }}
</button>
<button
v-else-if="isOverLengthLimit"
disabled
class="btn btn-default"
class="btn button-default"
>
{{ $t('general.submit') }}
</button>
@ -271,7 +278,7 @@
<button
v-else
:disabled="uploadingFiles || disableSubmit"
class="btn btn-default"
class="btn button-default"
@touchstart.stop.prevent="postStatus($event, newStatus)"
@click.stop.prevent="postStatus($event, newStatus)"
>
@ -283,8 +290,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,10 +302,12 @@
:key="file.url"
class="media-upload-wrapper"
>
<i
class="fa button-icon icon-cancel"
<button
class="button-unstyled hider"
@click="removeMediaFile(file)"
/>
>
<FAIcon icon="times" />
</button>
<attachment
:attachment="file"
:set-media="() => $store.dispatch('setMedia', newStatus.files)"
@ -375,24 +385,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 +439,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 +481,7 @@
text-align: right;
}
.icon-chart-bar {
.poll-icon {
cursor: pointer;
}
@ -487,19 +494,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,23 +516,12 @@
flex-direction: column;
}
.media-upload-wrapper .attachments {
padding: 0 0.5em;
.attachments .media-upload-wrapper {
position: relative;
.attachment {
margin: 0;
padding: 0;
position: relative;
}
i {
position: absolute;
margin: 10px;
padding: 5px;
background: rgba(230,230,230,0.6);
border-radius: $fallback--attachmentRadius;
border-radius: var(--attachmentRadius, $fallback--attachmentRadius);
font-weight: bold;
}
}
@ -612,11 +595,6 @@
cursor: not-allowed;
}
.icon-cancel {
cursor: pointer;
z-index: 4;
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 0.6; }

View file

@ -1,4 +1,8 @@
import Popover from '../popover/popover.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { faSmileBeam } from '@fortawesome/free-regular-svg-icons'
library.add(faSmileBeam)
const ReactButton = {
props: ['status'],
@ -23,13 +27,21 @@ const ReactButton = {
},
computed: {
commonEmojis () {
return ['👍', '😠', '👀', '😂', '🔥']
return [
{ displayText: 'thumbsup', replacement: '👍' },
{ displayText: 'angry', replacement: '😠' },
{ displayText: 'eyes', replacement: '👀' },
{ displayText: 'joy', replacement: '😂' },
{ displayText: 'fire', replacement: '🔥' }
]
},
emojis () {
if (this.filterWord !== '') {
const filterWordLowercase = this.filterWord.toLowerCase()
let orderedEmojiList = []
for (const emoji of this.$store.state.instance.emoji) {
if (emoji.replacement === this.filterWord) return [emoji]
const indexOfFilterWord = emoji.displayText.toLowerCase().indexOf(filterWordLowercase)
if (indexOfFilterWord > -1) {
if (!Array.isArray(orderedEmojiList[indexOfFilterWord])) {

View file

@ -3,7 +3,8 @@
trigger="click"
placement="top"
:offset="{ y: 5 }"
class="react-button-popover"
:bound-to="{ x: 'container' }"
remove-padding
>
<div
slot="content"
@ -12,23 +13,26 @@
<div class="reaction-picker-filter">
<input
v-model="filterWord"
size="1"
:placeholder="$t('emoji.search_emoji')"
>
</div>
<div class="reaction-picker">
<span
v-for="emoji in commonEmojis"
:key="emoji"
:key="emoji.replacement"
class="emoji-button"
@click="addReaction($event, emoji, close)"
:title="emoji.displayText"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji }}
{{ emoji.replacement }}
</span>
<div class="reaction-picker-divider" />
<span
v-for="(emoji, key) in emojis"
:key="key"
class="emoji-button"
:title="emoji.displayText"
@click="addReaction($event, emoji.replacement, close)"
>
{{ emoji.replacement }}
@ -36,11 +40,16 @@
<div class="reaction-bottom-fader" />
</div>
</div>
<i
<span
slot="trigger"
class="icon-smile button-icon add-reaction-button"
class="ReactButton"
:title="$t('tool_tip.add_reaction')"
/>
>
<FAIcon
class="fa-scale-110 fa-old-padding"
:icon="['far', 'smile-beam']"
/>
</span>
</Popover>
</template>
@ -98,10 +107,11 @@
}
}
.add-reaction-button {
cursor: pointer;
.ReactButton {
padding: 10px;
margin: -10px;
&:hover {
&:hover .svg-inline--fa {
color: $fallback--text;
color: var(--text, $fallback--text);
}

View file

@ -211,7 +211,7 @@
<button
:disabled="isPending"
type="submit"
class="btn btn-default"
class="btn button-default"
>
{{ $t('general.submit') }}
</button>

View file

@ -16,7 +16,7 @@
>
<button
click="submit"
class="remote-button"
class="button-default remote-button"
>
{{ $t('user_card.remote_follow') }}
</button>

View file

@ -1,3 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faReply } from '@fortawesome/free-solid-svg-icons'
library.add(faReply)
const ReplyButton = {
name: 'ReplyButton',

View file

@ -1,21 +1,58 @@
<template>
<div>
<i
<div class="ReplyButton">
<button
v-if="loggedIn"
class="button-icon button-reply icon-reply"
:title="$t('tool_tip.reply')"
class="button-unstyled interactive"
:class="{'-active': replying}"
@click.prevent="$emit('toggle')"
/>
<i
v-else
class="button-icon button-reply -disabled icon-reply"
:title="$t('tool_tip.reply')"
/>
<span v-if="status.replies_count > 0">
@click.prevent="$emit('toggle')"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="reply"
/>
</button>
<span v-else>
<FAIcon
icon="reply"
class="fa-scale-110 fa-old-padding"
:title="$t('tool_tip.reply')"
/>
</span>
<span
v-if="status.replies_count > 0"
class="action-counter"
>
{{ status.replies_count }}
</span>
</div>
</template>
<script src="./reply_button.js"></script>
<style lang="scss">
@import '../../_variables.scss';
.ReplyButton {
display: flex;
> :first-child {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
&:hover .svg-inline--fa,
&.-active .svg-inline--fa {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
}
</style>

View file

@ -1,3 +1,7 @@
import { library } from '@fortawesome/fontawesome-svg-core'
import { faRetweet } from '@fortawesome/free-solid-svg-icons'
library.add(faRetweet)
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
@ -20,13 +24,6 @@ const RetweetButton = {
}
},
computed: {
classes () {
return {
'retweeted': this.status.repeated,
'retweeted-empty': !this.status.repeated,
'animate-spin': this.animated
}
},
mergedConfig () {
return this.$store.getters.mergedConfig
}

View file

@ -1,29 +1,38 @@
<template>
<div v-if="loggedIn">
<template v-if="visibility !== 'private' && visibility !== 'direct'">
<i
:class="classes"
class="button-icon retweet-button icon-retweet rt-active"
:title="$t('tool_tip.repeat')"
@click.prevent="retweet()"
<div class="RetweetButton">
<button
v-if="visibility !== 'private' && visibility !== 'direct' && loggedIn"
class="button-unstyled interactive"
:class="status.repeated && '-repeated'"
:title="$t('tool_tip.repeat')"
@click.prevent="retweet()"
>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="retweet"
:spin="animated"
/>
<span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</template>
<template v-else>
<i
:class="classes"
class="button-icon icon-lock"
</button>
<span v-else-if="loggedIn">
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="lock"
:title="$t('timeline.no_retweet_hint')"
/>
</template>
</div>
<div v-else-if="!loggedIn">
<i
:class="classes"
class="button-icon icon-retweet"
:title="$t('tool_tip.repeat')"
/>
<span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</span>
<span v-else>
<FAIcon
class="fa-scale-110 fa-old-padding"
icon="retweet"
:title="$t('tool_tip.repeat')"
/>
</span>
<span
v-if="!mergedConfig.hidePostStats && status.repeat_num > 0"
class="no-event"
>
{{ status.repeat_num }}
</span>
</div>
</template>
@ -31,16 +40,30 @@
<style lang="scss">
@import '../../_variables.scss';
.rt-active {
cursor: pointer;
animation-duration: 0.6s;
&:hover {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
.RetweetButton {
display: flex;
> :first-child {
padding: 10px;
margin: -10px -8px -10px -10px;
}
.action-counter {
pointer-events: none;
user-select: none;
}
.interactive {
.svg-inline--fa {
animation-duration: 0.6s;
}
&:hover .svg-inline--fa,
&.-repeated .svg-inline--fa {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
}
}
.icon-retweet.retweeted {
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
</style>

View file

@ -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',

View file

@ -1,36 +1,56 @@
<template>
<div
v-if="!showNothing"
class="scope-selector"
class="ScopeSelector"
>
<i
<button
v-if="showDirect"
class="icon-mail-alt"
class="button-unstyled scope"
:class="css.direct"
:title="$t('post_status.scope.direct')"
@click="changeVis('direct')"
/>
<i
>
<FAIcon
icon="envelope"
class="fa-scale-110 fa-old-padding"
/>
</button>
<button
v-if="showPrivate"
class="icon-lock"
class="button-unstyled scope"
:class="css.private"
:title="$t('post_status.scope.private')"
@click="changeVis('private')"
/>
<i
>
<FAIcon
icon="lock"
class="fa-scale-110 fa-old-padding"
/>
</button>
<button
v-if="showUnlisted"
class="icon-lock-open-alt"
class="button-unstyled 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"
/>
</button>
<button
v-if="showPublic"
class="icon-globe"
class="button-unstyled scope"
:class="css.public"
:title="$t('post_status.scope.public')"
@click="changeVis('public')"
/>
>
<FAIcon
icon="globe"
class="fa-scale-110 fa-old-padding"
/>
</button>
</div>
</template>
@ -39,12 +59,16 @@
<style lang="scss">
@import '../../_variables.scss';
.scope-selector {
i {
font-size: 1.2em;
cursor: pointer;
.ScopeSelector {
&.selected {
.scope {
display: inline-block;
cursor: pointer;
min-width: 1.3em;
min-height: 1.3em;
text-align: center;
&.selected svg {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
}

Some files were not shown because too many files have changed in this diff Show more