Merge branch 'disjointed-popovers' into shigusegubu-vue3
* disjointed-popovers: (39 commits) limit width of popover to slightly lesser than screen width don't hide on clicks in popover's content use user popovers in notifications as well properly center user popovers user popovers WIP fix tests fix popovers in modals recalculate position on scrolls remove duplicate buttons on post buttons fix animations, replace ugly old mentionlink tooltips with new usercard ones fix popovers so that all of them have shadows turns out it is needed still + some code cleanup vPadding is no longer needed hide popovers on scroll more fixes to chat popovers popup offsets should be fixed now force panel headers to be square on mobile (for now?) fix gap between panel heading and timeline menu Fix Open Chat button fix? ...
This commit is contained in:
commit
d4c17bab12
38 changed files with 527 additions and 239 deletions
|
@ -23,7 +23,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/vue-fontawesome": "3.0.0-5",
|
||||
"@kazvmoe-infra/pinch-zoom-element": "1.2.0",
|
||||
"@vuelidate/core": "2.0.0-alpha.35",
|
||||
"@vuelidate/core": "2.0.0-alpha.41",
|
||||
"@vuelidate/validators": "2.0.0-alpha.27",
|
||||
"body-scroll-lock": "2.7.1",
|
||||
"chromatism": "3.0.0",
|
||||
|
@ -31,6 +31,7 @@
|
|||
"cropperjs": "1.5.12",
|
||||
"diff": "3.5.0",
|
||||
"escape-html": "1.0.3",
|
||||
"js-cookie": "^3.0.1",
|
||||
"localforage": "1.10.0",
|
||||
"parse-link-header": "1.0.1",
|
||||
"phoenix": "1.6.2",
|
||||
|
@ -106,7 +107,7 @@
|
|||
"sass": "1.20.1",
|
||||
"sass-loader": "7.2.0",
|
||||
"selenium-server": "2.53.1",
|
||||
"semver": "5.6.0",
|
||||
"semver": "5.7.1",
|
||||
"serviceworker-webpack-plugin": "1.0.1",
|
||||
"shelljs": "0.8.5",
|
||||
"sinon": "2.4.1",
|
||||
|
@ -120,7 +121,7 @@
|
|||
"webpack": "4.46.0",
|
||||
"webpack-dev-middleware": "3.7.3",
|
||||
"webpack-hot-middleware": "2.24.3",
|
||||
"webpack-merge": "0.14.1"
|
||||
"webpack-merge": "0.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4.0.0",
|
||||
|
|
|
@ -4,7 +4,6 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
|
|||
import FeaturesPanel from './components/features_panel/features_panel.vue'
|
||||
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
|
||||
import ShoutPanel from './components/shout_panel/shout_panel.vue'
|
||||
import SettingsModal from './components/settings_modal/settings_modal.vue'
|
||||
import MediaModal from './components/media_modal/media_modal.vue'
|
||||
import SideDrawer from './components/side_drawer/side_drawer.vue'
|
||||
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
|
||||
|
@ -32,7 +31,7 @@ export default {
|
|||
MobilePostStatusButton,
|
||||
MobileNav,
|
||||
DesktopNav,
|
||||
SettingsModal,
|
||||
SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
GlobalNoticeList
|
||||
|
|
11
src/App.scss
11
src/App.scss
|
@ -301,6 +301,15 @@ nav {
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.panel-heading,
|
||||
.panel-heading::after,
|
||||
.panel-heading::before,
|
||||
.panel,
|
||||
.panel::after {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.underlay,
|
||||
#sidebar,
|
||||
#notifs-column {
|
||||
|
@ -820,7 +829,7 @@ option {
|
|||
// Vue transitions
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
<SettingsModal />
|
||||
<div id="modal" />
|
||||
<GlobalNoticeList />
|
||||
<div id="popovers" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ const AccountActions = {
|
|||
openChat () {
|
||||
this.$router.push({
|
||||
name: 'chat',
|
||||
params: { recipient_id: this.user.id }
|
||||
params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
<Popover
|
||||
trigger="click"
|
||||
placement="top"
|
||||
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'"
|
||||
bound-to-selector=".chat-view-inner"
|
||||
:bound-to="{ x: 'container' }"
|
||||
:margin="popoverMarginStyle"
|
||||
@show="menuOpened = true"
|
||||
|
|
|
@ -119,12 +119,12 @@
|
|||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<button class="button-unstyled popover-trigger">
|
||||
<span class="button-unstyled popover-trigger">
|
||||
<FAIcon
|
||||
class="fa-scale-110 fa-old-padding"
|
||||
icon="ellipsis-h"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<label for="interface-language-switcher">
|
||||
{{ $t('settings.interfaceLanguage') }}
|
||||
{{ promptText }}
|
||||
</label>
|
||||
{{ ' ' }}
|
||||
<Select
|
||||
id="interface-language-switcher"
|
||||
v-model="language"
|
||||
v-model="controlledLanguage"
|
||||
>
|
||||
<option
|
||||
v-for="lang in languages"
|
||||
|
@ -20,39 +20,43 @@
|
|||
</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 Select from '../select/select.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Select
|
||||
},
|
||||
props: {
|
||||
promptText: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
setLanguage: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
languages () {
|
||||
return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
|
||||
return localeService.languages
|
||||
},
|
||||
|
||||
language: {
|
||||
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
||||
controlledLanguage: {
|
||||
get: function () { return this.language },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
this.setLanguage(val)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
getLanguageName (code) {
|
||||
const specialLanguageNames = {
|
||||
'ja_easy': 'やさしいにほんご',
|
||||
'zh': '简体中文',
|
||||
'zh_Hant': '繁體中文'
|
||||
}
|
||||
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
|
||||
const browserLocale = localeService.internalToBrowserLocale(code)
|
||||
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
|
||||
return localeService.getLanguageName(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
|
|||
import { mapGetters, mapState } from 'vuex'
|
||||
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
|
||||
import UserAvatar from '../user_avatar/user_avatar.vue'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faAt
|
||||
|
@ -14,7 +15,9 @@ library.add(
|
|||
const MentionLink = {
|
||||
name: 'MentionLink',
|
||||
components: {
|
||||
UserAvatar
|
||||
UserAvatar,
|
||||
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
|
||||
UserCard: defineAsyncComponent(() => import('../user_card/user_card.vue'))
|
||||
},
|
||||
props: {
|
||||
url: {
|
||||
|
@ -36,6 +39,7 @@ const MentionLink = {
|
|||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
if (this.shouldShowTooltip) return
|
||||
const link = generateProfileLink(
|
||||
this.userId || this.user.id,
|
||||
this.userScreenName || this.user.screen_name
|
||||
|
@ -110,7 +114,7 @@ const MentionLink = {
|
|||
}
|
||||
},
|
||||
shouldShowTooltip () {
|
||||
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote
|
||||
return this.mergedConfig.mentionLinkShowTooltip
|
||||
},
|
||||
shouldShowAvatar () {
|
||||
return this.mergedConfig.mentionLinkShowAvatar
|
||||
|
|
|
@ -101,7 +101,6 @@
|
|||
}
|
||||
|
||||
&:hover .new .full {
|
||||
opacity: 1;
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
|
@ -113,3 +112,9 @@
|
|||
color: var(--faint, $fallback--faint);
|
||||
}
|
||||
}
|
||||
|
||||
.mention-link-popover {
|
||||
max-width: 70ch;
|
||||
max-height: 20rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -9,66 +9,85 @@
|
|||
class="original"
|
||||
target="_blank"
|
||||
v-html="content"
|
||||
/><!-- eslint-enable vue/no-v-html --><span
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
:class="classnames"
|
||||
/><!-- eslint-enable vue/no-v-html -->
|
||||
<Popover
|
||||
trigger="click"
|
||||
:bound-to="{ x: 'container'}"
|
||||
bound-to-selector=".column"
|
||||
popover-class="popover-default mention-popover"
|
||||
:disabled="!shouldShowTooltip"
|
||||
>
|
||||
<a
|
||||
class="short button-unstyled"
|
||||
:class="{ '-with-tooltip': shouldShowTooltip }"
|
||||
:href="url"
|
||||
@click.prevent="onClick"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<UserAvatar
|
||||
v-if="shouldShowAvatar"
|
||||
class="mention-avatar"
|
||||
:user="user"
|
||||
/><span
|
||||
class="shortName"
|
||||
><FAIcon
|
||||
v-if="useAtIcon"
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/>{{ !useAtIcon ? '@' : '' }}<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
v-if="shouldShowFullUserName"
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/>
|
||||
</span>
|
||||
<template v-slot:trigger>
|
||||
<span
|
||||
v-if="isYou && shouldShowYous"
|
||||
:class="{ '-you': shouldBoldenYou }"
|
||||
> {{ ' ' + $t('status.you') }}</span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</a><span
|
||||
v-if="shouldShowTooltip"
|
||||
class="full popover-default"
|
||||
:class="[highlightType]"
|
||||
>
|
||||
<span
|
||||
class="userNameFull"
|
||||
v-if="user"
|
||||
class="new"
|
||||
:style="style"
|
||||
:class="classnames"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
@<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<a
|
||||
class="short button-unstyled"
|
||||
:class="{ '-with-tooltip': shouldShowTooltip }"
|
||||
:href="url"
|
||||
@click.prevent="onClick"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
<UserAvatar
|
||||
v-if="shouldShowAvatar"
|
||||
class="mention-avatar"
|
||||
:user="user"
|
||||
/><span
|
||||
class="shortName"
|
||||
><FAIcon
|
||||
v-if="useAtIcon"
|
||||
size="sm"
|
||||
icon="at"
|
||||
class="at"
|
||||
/>{{ !useAtIcon ? '@' : '' }}<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
v-if="shouldShowFullUserName"
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
v-if="isYou && shouldShowYous"
|
||||
:class="{ '-you': shouldBoldenYou }"
|
||||
> {{ ' ' + $t('status.you') }}</span>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</a><span
|
||||
v-if="shouldShowTooltip"
|
||||
class="full"
|
||||
>
|
||||
<span
|
||||
class="userNameFull"
|
||||
>
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
@<span
|
||||
class="userName"
|
||||
v-html="userName"
|
||||
/><span
|
||||
class="serverName"
|
||||
:class="{ '-faded': shouldFadeDomain }"
|
||||
v-html="'@' + serverName"
|
||||
/>
|
||||
<!-- eslint-enable vue/no-v-html -->
|
||||
</span>
|
||||
</span>
|
||||
</span></template>
|
||||
<template v-slot:content>
|
||||
<UserCard
|
||||
class="mention-link-popover"
|
||||
:user-id="user.id"
|
||||
:hide-bio="true"
|
||||
:bordered="false"
|
||||
:allow-zooming-avatar="true"
|
||||
:rounded="true"
|
||||
/>
|
||||
</template>
|
||||
</Popover>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
|
|||
import UserCard from '../user_card/user_card.vue'
|
||||
import Timeago from '../timeago/timeago.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import Popover from '../popover/popover.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'
|
||||
|
@ -46,7 +47,8 @@ const Notification = {
|
|||
UserCard,
|
||||
Timeago,
|
||||
Status,
|
||||
RichContent
|
||||
RichContent,
|
||||
Popover
|
||||
},
|
||||
methods: {
|
||||
toggleUserExpanded () {
|
||||
|
|
|
@ -34,21 +34,36 @@
|
|||
<a
|
||||
class="avatar-container"
|
||||
:href="$router.resolve(userProfileLink).href"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
@click.prevent
|
||||
>
|
||||
<UserAvatar
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
<Popover
|
||||
trigger="click"
|
||||
popover-class="popover-default user-popover"
|
||||
:overlay-centers="true"
|
||||
overlay-centers-selector=".user-info-avatar-link .Avatar"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
:bot="botIndicator"
|
||||
:compact="true"
|
||||
:better-shadow="betterShadow"
|
||||
:user="notification.from_profile"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<UserCard
|
||||
class="mention-link-popover"
|
||||
:user-id="getUser(notification).id"
|
||||
:hide-bio="true"
|
||||
:bordered="false"
|
||||
:allow-zooming-avatar="true"
|
||||
:rounded="true"
|
||||
/>
|
||||
</template>
|
||||
</Popover>
|
||||
</a>
|
||||
<div class="notification-right">
|
||||
<UserCard
|
||||
v-if="userExpanded"
|
||||
:user-id="getUser(notification).id"
|
||||
:rounded="true"
|
||||
:bordered="true"
|
||||
/>
|
||||
<span class="notification-details">
|
||||
<div class="name-and-action">
|
||||
<!-- eslint-disable vue/no-v-html -->
|
||||
|
@ -120,6 +135,14 @@
|
|||
</i18n-t>
|
||||
</small>
|
||||
</span>
|
||||
<span v-if="notification.type === 'poll'">
|
||||
<FAIcon
|
||||
class="type-icon"
|
||||
icon="poll-h"
|
||||
/>
|
||||
{{ ' ' }}
|
||||
<small>{{ $t('notifications.poll_ended') }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="isStatusNotification"
|
||||
|
|
|
@ -61,6 +61,15 @@
|
|||
:class="{ 'menu-checkbox-checked': filters.moves }"
|
||||
/>{{ $t('settings.notification_visibility_moves') }}
|
||||
</button>
|
||||
<button
|
||||
class="button-default dropdown-item"
|
||||
@click="toggleNotificationFilter('polls')"
|
||||
>
|
||||
<span
|
||||
class="menu-checkbox"
|
||||
:class="{ 'menu-checkbox-checked': filters.polls }"
|
||||
/>{{ $t('settings.notification_visibility_polls') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
v-for="notification in notificationsToDisplay"
|
||||
:key="notification.id"
|
||||
class="notification"
|
||||
:class="{"unseen": !minimalMode && !notification.seen}"
|
||||
:class="{unseen: !minimalMode && !notification.seen}"
|
||||
>
|
||||
<div class="notification-overlay" />
|
||||
<notification :notification="notification" />
|
||||
|
|
|
@ -31,13 +31,24 @@ const Popover = {
|
|||
|
||||
// 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
|
||||
removePadding: Boolean,
|
||||
|
||||
// self-explanatory (i hope)
|
||||
disabled: Boolean,
|
||||
|
||||
// Instead of putting popover next to anchor, overlay popover's center on top of anchor's center
|
||||
overlayCenters: Boolean,
|
||||
|
||||
// What selector (witin popover!) to use for determining center of popover
|
||||
overlayCentersSelector: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
hidden: true,
|
||||
styles: { opacity: 0 },
|
||||
oldSize: { width: 0, height: 0 }
|
||||
styles: {},
|
||||
oldSize: { width: 0, height: 0 },
|
||||
// used to avoid blinking if hovered onto popover
|
||||
graceTimeout: null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
@ -47,9 +58,7 @@ const Popover = {
|
|||
},
|
||||
updateStyles () {
|
||||
if (this.hidden) {
|
||||
this.styles = {
|
||||
opacity: 0
|
||||
}
|
||||
this.styles = {}
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -57,14 +66,26 @@ const Popover = {
|
|||
// its children are what are inside the slot. Expect only one v-slot:trigger.
|
||||
const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
|
||||
// SVGs don't have offsetWidth/Height, use fallback
|
||||
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
|
||||
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
|
||||
const screenBox = anchorEl.getBoundingClientRect()
|
||||
// Screen position of the origin point for popover
|
||||
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
|
||||
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
|
||||
const anchorScreenBox = anchorEl.getBoundingClientRect()
|
||||
|
||||
const anchorStyle = getComputedStyle(anchorEl)
|
||||
const topPadding = parseFloat(anchorStyle.paddingTop)
|
||||
const bottomPadding = parseFloat(anchorStyle.paddingBottom)
|
||||
|
||||
// Screen position of the origin point for popover = center of the anchor
|
||||
const origin = {
|
||||
x: anchorScreenBox.left + anchorWidth * 0.5,
|
||||
y: anchorScreenBox.top + anchorHeight * 0.5
|
||||
}
|
||||
const content = this.$refs.content
|
||||
const overlayCenter = this.overlayCentersSelector
|
||||
? this.$refs.content.querySelector(this.overlayCentersSelector)
|
||||
: null
|
||||
|
||||
// Minor optimization, don't call a slow reflow call if we don't have to
|
||||
const parentBounds = this.boundTo &&
|
||||
const parentScreenBox = this.boundTo &&
|
||||
(this.boundTo.x === 'container' || this.boundTo.y === 'container') &&
|
||||
this.containerBoundingClientRect()
|
||||
|
||||
|
@ -73,64 +94,85 @@ const Popover = {
|
|||
// What are the screen bounds for the popover? Viewport vs container
|
||||
// when using viewport, using default margin values to dodge the navbar
|
||||
const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
|
||||
min: parentBounds.left + (margin.left || 0),
|
||||
max: parentBounds.right - (margin.right || 0)
|
||||
min: parentScreenBox.left + (margin.left || 0),
|
||||
max: parentScreenBox.right - (margin.right || 0)
|
||||
} : {
|
||||
min: 0 + (margin.left || 10),
|
||||
max: window.innerWidth - (margin.right || 10)
|
||||
}
|
||||
|
||||
const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
|
||||
min: parentBounds.top + (margin.top || 0),
|
||||
max: parentBounds.bottom - (margin.bottom || 0)
|
||||
min: parentScreenBox.top + (margin.top || 0),
|
||||
max: parentScreenBox.bottom - (margin.bottom || 0)
|
||||
} : {
|
||||
min: 0 + (margin.top || 50),
|
||||
max: window.innerHeight - (margin.bottom || 5)
|
||||
}
|
||||
|
||||
let horizOffset = 0
|
||||
let vertOffset = 0
|
||||
|
||||
if (overlayCenter) {
|
||||
const box = content.getBoundingClientRect()
|
||||
const overlayCenterScreenBox = overlayCenter.getBoundingClientRect()
|
||||
const leftInnerOffset = overlayCenterScreenBox.left - box.left
|
||||
const topInnerOffset = overlayCenterScreenBox.top - box.top
|
||||
horizOffset = -leftInnerOffset - overlayCenter.offsetWidth * 0.5
|
||||
vertOffset = -topInnerOffset - overlayCenter.offsetWidth * 0.5
|
||||
} else {
|
||||
horizOffset = content.offsetWidth * -0.5
|
||||
vertOffset = content.offsetWidth * -0.5
|
||||
}
|
||||
const leftBorder = origin.x + horizOffset
|
||||
const rightBorder = origin.x - horizOffset
|
||||
// If overflowing from left, move it so that it doesn't
|
||||
if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) {
|
||||
horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min
|
||||
if (leftBorder < xBounds.min) {
|
||||
horizOffset += xBounds.min - leftBorder
|
||||
}
|
||||
|
||||
// If overflowing from right, move it so that it doesn't
|
||||
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) {
|
||||
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max
|
||||
if (rightBorder > xBounds.max) {
|
||||
horizOffset -= rightBorder - xBounds.max
|
||||
}
|
||||
|
||||
// Default to whatever user wished with placement prop
|
||||
let usingTop = this.placement !== 'bottom'
|
||||
let translateX = 0
|
||||
let translateY = 0
|
||||
|
||||
// Handle special cases, first force to displaying on top if there's not space on bottom,
|
||||
// regardless of what placement value was. Then check if there's not space on top, and
|
||||
// force to bottom, again regardless of what placement value was.
|
||||
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true
|
||||
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false
|
||||
if (overlayCenter) {
|
||||
translateX = origin.x + horizOffset
|
||||
translateY = origin.y + vertOffset
|
||||
} else {
|
||||
// Default to whatever user wished with placement prop
|
||||
let usingTop = this.placement !== 'bottom'
|
||||
|
||||
let vPadding = 0
|
||||
if (this.removePadding && usingTop) {
|
||||
const anchorStyle = getComputedStyle(anchorEl)
|
||||
vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom)
|
||||
// Handle special cases, first force to displaying on top if there's not space on bottom,
|
||||
// regardless of what placement value was. Then check if there's not space on top, and
|
||||
// force to bottom, again regardless of what placement value was.
|
||||
const topBoundary = origin.y - anchorHeight * 0.5 + (this.removePadding ? topPadding : 0)
|
||||
const bottomBoundary = origin.y + anchorHeight * 0.5 - (this.removePadding ? bottomPadding : 0)
|
||||
if (bottomBoundary + content.offsetHeight > yBounds.max) usingTop = true
|
||||
if (topBoundary - content.offsetHeight < yBounds.min) usingTop = false
|
||||
|
||||
const yOffset = (this.offset && this.offset.y) || 0
|
||||
translateY = usingTop
|
||||
? topBoundary - yOffset - content.offsetHeight
|
||||
: bottomBoundary + yOffset
|
||||
|
||||
const xOffset = (this.offset && this.offset.x) || 0
|
||||
translateX = origin.x + horizOffset + xOffset
|
||||
}
|
||||
|
||||
const yOffset = (this.offset && this.offset.y) || 0
|
||||
const translateY = usingTop
|
||||
? -anchorHeight + vPadding - yOffset - content.offsetHeight
|
||||
: yOffset
|
||||
|
||||
const xOffset = (this.offset && this.offset.x) || 0
|
||||
const translateX = anchorWidth * 0.5 - content.offsetWidth * 0.5 + horizOffset + xOffset
|
||||
|
||||
// Note, separate translateX and translateY avoids blurry text on chromium,
|
||||
// single translate or translate3d resulted in blurry text.
|
||||
this.styles = {
|
||||
opacity: 1,
|
||||
transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)`
|
||||
left: `${Math.round(translateX)}px`,
|
||||
top: `${Math.round(translateY)}px`
|
||||
}
|
||||
|
||||
if (parentScreenBox) {
|
||||
this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
|
||||
}
|
||||
},
|
||||
showPopover () {
|
||||
if (this.disabled) return
|
||||
const wasHidden = this.hidden
|
||||
this.hidden = false
|
||||
this.$nextTick(() => {
|
||||
|
@ -141,13 +183,30 @@ const Popover = {
|
|||
hidePopover () {
|
||||
if (!this.hidden) this.$emit('close')
|
||||
this.hidden = true
|
||||
this.styles = { opacity: 0 }
|
||||
},
|
||||
onMouseenter (e) {
|
||||
if (this.trigger === 'hover') this.showPopover()
|
||||
if (this.trigger === 'hover') {
|
||||
clearTimeout(this.graceTimeout)
|
||||
this.graceTimeout = null
|
||||
this.showPopover()
|
||||
}
|
||||
},
|
||||
onMouseleave (e) {
|
||||
if (this.trigger === 'hover') this.hidePopover()
|
||||
if (this.trigger === 'hover') {
|
||||
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
|
||||
}
|
||||
},
|
||||
onMouseenterContent (e) {
|
||||
if (this.trigger === 'hover') {
|
||||
clearTimeout(this.graceTimeout)
|
||||
this.graceTimeout = null
|
||||
this.showPopover()
|
||||
}
|
||||
},
|
||||
onMouseleaveContent (e) {
|
||||
if (this.trigger === 'hover') {
|
||||
this.graceTimeout = setTimeout(() => this.hidePopover(), 1)
|
||||
}
|
||||
},
|
||||
onClick (e) {
|
||||
if (this.trigger === 'click') {
|
||||
|
@ -160,8 +219,12 @@ const Popover = {
|
|||
},
|
||||
onClickOutside (e) {
|
||||
if (this.hidden) return
|
||||
if (this.$refs.content.contains(e.target)) return
|
||||
if (this.$el.contains(e.target)) return
|
||||
this.hidePopover()
|
||||
},
|
||||
onScroll (e) {
|
||||
this.updateStyles()
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
|
@ -175,11 +238,17 @@ const Popover = {
|
|||
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight }
|
||||
}
|
||||
},
|
||||
created () {
|
||||
mounted () {
|
||||
let scrollable = this.$refs.trigger.closest('.column.-scrollable')
|
||||
if (!scrollable) scrollable = window
|
||||
document.addEventListener('click', this.onClickOutside)
|
||||
scrollable.addEventListener('scroll', this.onScroll)
|
||||
},
|
||||
unmounted () {
|
||||
beforeUnmount () {
|
||||
let scrollable = this.$refs.trigger.closest('.column.-scrollable')
|
||||
if (!scrollable) scrollable = window
|
||||
document.removeEventListener('click', this.onClickOutside)
|
||||
scrollable.removeEventListener('scroll', this.onScroll)
|
||||
this.hidePopover()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div
|
||||
<span
|
||||
@mouseenter="onMouseenter"
|
||||
@mouseleave="onMouseleave"
|
||||
>
|
||||
|
@ -11,20 +11,26 @@
|
|||
>
|
||||
<slot name="trigger" />
|
||||
</button>
|
||||
<div
|
||||
v-if="!hidden"
|
||||
ref="content"
|
||||
:style="styles"
|
||||
class="popover"
|
||||
:class="popoverClass || 'popover-default'"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
class="popover-inner"
|
||||
:close="hidePopover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<teleport to="#popovers">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="!hidden"
|
||||
ref="content"
|
||||
:style="styles"
|
||||
class="popover"
|
||||
:class="popoverClass || 'popover-default'"
|
||||
@mouseenter="onMouseenterContent"
|
||||
@mouseleave="onMouseleaveContent"
|
||||
>
|
||||
<slot
|
||||
name="content"
|
||||
class="popover-inner"
|
||||
:close="hidePopover"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</teleport>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script src="./popover.js" />
|
||||
|
@ -37,14 +43,15 @@
|
|||
}
|
||||
|
||||
.popover {
|
||||
z-index: 500;
|
||||
position: absolute;
|
||||
z-index: 90000;
|
||||
position: fixed;
|
||||
min-width: 0;
|
||||
max-width: 90vw;
|
||||
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
}
|
||||
|
||||
.popover-default {
|
||||
transition: opacity 0.3s;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:offset="{ y: 5 }"
|
||||
:bound-to="{ x: 'container' }"
|
||||
remove-padding
|
||||
popover-class="ReactButton popover-default"
|
||||
@show="focusInput"
|
||||
>
|
||||
<template v-slot:content="{close}">
|
||||
|
@ -40,7 +41,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<template v-slot:trigger>
|
||||
<button
|
||||
<span
|
||||
class="button-unstyled popover-trigger"
|
||||
:title="$t('tool_tip.add_reaction')"
|
||||
>
|
||||
|
@ -48,7 +49,7 @@
|
|||
class="fa-scale-110 fa-old-padding"
|
||||
:icon="['far', 'smile-beam']"
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
</Popover>
|
||||
</template>
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import useVuelidate from '@vuelidate/core'
|
||||
import { required, requiredIf, sameAs } from '@vuelidate/validators'
|
||||
import { mapActions, mapState } from 'vuex'
|
||||
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
|
||||
import localeService from '../../services/locale/locale.service.js'
|
||||
|
||||
const registration = {
|
||||
setup () { return { v$: useVuelidate() } },
|
||||
|
@ -11,10 +13,14 @@ const registration = {
|
|||
username: '',
|
||||
password: '',
|
||||
confirm: '',
|
||||
reason: ''
|
||||
reason: '',
|
||||
language: ''
|
||||
},
|
||||
captcha: {}
|
||||
}),
|
||||
components: {
|
||||
InterfaceLanguageSwitcher
|
||||
},
|
||||
validations () {
|
||||
return {
|
||||
user: {
|
||||
|
@ -26,7 +32,8 @@ const registration = {
|
|||
required,
|
||||
sameAs: sameAs(this.user.password)
|
||||
},
|
||||
reason: { required: requiredIf(() => this.accountApprovalRequired) }
|
||||
reason: { required: requiredIf(() => this.accountApprovalRequired) },
|
||||
language: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -64,6 +71,9 @@ const registration = {
|
|||
this.user.captcha_solution = this.captcha.solution
|
||||
this.user.captcha_token = this.captcha.token
|
||||
this.user.captcha_answer_data = this.captcha.answer_data
|
||||
if (this.user.language) {
|
||||
this.user.language = localeService.internalToBackendLocale(this.user.language)
|
||||
}
|
||||
|
||||
this.v$.$touch()
|
||||
|
||||
|
|
|
@ -162,6 +162,18 @@
|
|||
</ul>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="form-group"
|
||||
:class="{ 'form-group--error': v$.user.language.$error }"
|
||||
>
|
||||
<interface-language-switcher
|
||||
for="email-language"
|
||||
:prompt-text="$t('registration.email_language')"
|
||||
:language="v$.user.language.$model"
|
||||
:set-language="val => v$.user.language.$model = val"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="accountApprovalRequired"
|
||||
class="form-group"
|
||||
|
|
|
@ -77,6 +77,12 @@ const GeneralTab = {
|
|||
!this.$store.state.users.currentUser.background_image
|
||||
},
|
||||
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable },
|
||||
language: {
|
||||
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
|
||||
set: function (val) {
|
||||
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
|
||||
}
|
||||
},
|
||||
...SharedComputedObject()
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -4,7 +4,11 @@
|
|||
<h2>{{ $t('settings.interface') }}</h2>
|
||||
<ul class="setting-list">
|
||||
<li>
|
||||
<interface-language-switcher />
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.interfaceLanguage')"
|
||||
:language="language"
|
||||
:set-language="val => language = val"
|
||||
/>
|
||||
</li>
|
||||
<li v-if="instanceSpecificPanelPresent">
|
||||
<BooleanSetting path="hideISP">
|
||||
|
@ -265,7 +269,7 @@
|
|||
path="mentionLinkShowTooltip"
|
||||
expert="1"
|
||||
>
|
||||
{{ $t('settings.mention_link_show_tooltip') }}
|
||||
{{ $t('settings.mention_link_use_tooltip') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -41,6 +41,11 @@
|
|||
{{ $t('settings.notification_visibility_emoji_reactions') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
<li>
|
||||
<BooleanSetting path="notificationVisibility.polls">
|
||||
{{ $t('settings.notification_visibility_polls') }}
|
||||
</BooleanSetting>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -8,8 +8,10 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
|
|||
import suggestor from 'src/components/emoji_input/suggestor.js'
|
||||
import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
|
||||
import Checkbox from 'src/components/checkbox/checkbox.vue'
|
||||
import InterfaceLanguageSwitcher from 'src/components/interface_language_switcher/interface_language_switcher.vue'
|
||||
import BooleanSetting from '../helpers/boolean_setting.vue'
|
||||
import SharedComputedObject from '../helpers/shared_computed_object.js'
|
||||
import localeService from 'src/services/locale/locale.service.js'
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
|
@ -40,7 +42,8 @@ const ProfileTab = {
|
|||
banner: null,
|
||||
bannerPreview: null,
|
||||
background: null,
|
||||
backgroundPreview: null
|
||||
backgroundPreview: null,
|
||||
emailLanguage: this.$store.state.users.currentUser.language || ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
|
@ -50,7 +53,8 @@ const ProfileTab = {
|
|||
Autosuggest,
|
||||
ProgressButton,
|
||||
Checkbox,
|
||||
BooleanSetting
|
||||
BooleanSetting,
|
||||
InterfaceLanguageSwitcher
|
||||
},
|
||||
computed: {
|
||||
user () {
|
||||
|
@ -111,19 +115,25 @@ const ProfileTab = {
|
|||
},
|
||||
methods: {
|
||||
updateProfile () {
|
||||
const params = {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
bot: this.bot,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
||||
if (this.emailLanguage) {
|
||||
params.language = localeService.internalToBackendLocale(this.emailLanguage)
|
||||
}
|
||||
|
||||
this.$store.state.api.backendInteractor
|
||||
.updateProfile({
|
||||
params: {
|
||||
note: this.newBio,
|
||||
locked: this.newLocked,
|
||||
// Backend notation.
|
||||
/* eslint-disable camelcase */
|
||||
display_name: this.newName,
|
||||
fields_attributes: this.newFields.filter(el => el != null),
|
||||
bot: this.bot,
|
||||
show_role: this.showRole
|
||||
/* eslint-enable camelcase */
|
||||
} }).then((user) => {
|
||||
.updateProfile({ params })
|
||||
.then((user) => {
|
||||
this.newFields.splice(user.fields.length)
|
||||
merge(this.newFields, user.fields)
|
||||
this.$store.commit('addNewUsers', [user])
|
||||
|
@ -193,8 +203,8 @@ const ProfileTab = {
|
|||
submitAvatar (cropper, file) {
|
||||
const that = this
|
||||
return new Promise((resolve, reject) => {
|
||||
function updateAvatar (avatar) {
|
||||
that.$store.state.api.backendInteractor.updateProfileImages({ avatar })
|
||||
function updateAvatar (avatar, avatarName) {
|
||||
that.$store.state.api.backendInteractor.updateProfileImages({ avatar, avatarName })
|
||||
.then((user) => {
|
||||
that.$store.commit('addNewUsers', [user])
|
||||
that.$store.commit('setCurrentUser', user)
|
||||
|
@ -207,9 +217,9 @@ const ProfileTab = {
|
|||
}
|
||||
|
||||
if (cropper) {
|
||||
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
|
||||
cropper.getCroppedCanvas().toBlob((data) => updateAvatar(data, file.name), file.type)
|
||||
} else {
|
||||
updateAvatar(file)
|
||||
updateAvatar(file, file.name)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -89,6 +89,13 @@
|
|||
{{ $t('settings.bot') }}
|
||||
</Checkbox>
|
||||
</p>
|
||||
<p>
|
||||
<interface-language-switcher
|
||||
:prompt-text="$t('settings.email_language')"
|
||||
:language="emailLanguage"
|
||||
:set-language="val => emailLanguage = val"
|
||||
/>
|
||||
</p>
|
||||
<button
|
||||
:disabled="newName && newName.length === 0"
|
||||
class="btn button-default"
|
||||
|
|
|
@ -11,6 +11,7 @@ import Timeago from '../timeago/timeago.vue'
|
|||
import StatusContent from '../status_content/status_content.vue'
|
||||
import RichContent from 'src/components/rich_content/rich_content.jsx'
|
||||
import StatusPopover from '../status_popover/status_popover.vue'
|
||||
import Popover from '../popover/popover.vue'
|
||||
import UserListPopover from '../user_list_popover/user_list_popover.vue'
|
||||
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
|
||||
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
|
||||
|
@ -115,7 +116,8 @@ const Status = {
|
|||
StatusContent,
|
||||
RichContent,
|
||||
MentionLink,
|
||||
MentionsLine
|
||||
MentionsLine,
|
||||
Popover
|
||||
},
|
||||
props: [
|
||||
'statusoid',
|
||||
|
|
|
@ -122,17 +122,33 @@
|
|||
v-if="!noHeading"
|
||||
class="left-side"
|
||||
>
|
||||
<a
|
||||
:href="$router.resolve(userProfileLink).href"
|
||||
@click.stop.prevent.capture="toggleUserExpanded"
|
||||
>
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
:bot="botIndicator"
|
||||
:compact="compact"
|
||||
:better-shadow="betterShadow"
|
||||
:user="status.user"
|
||||
/>
|
||||
<a :href="$router.resolve(userProfileLink).href" @click.prevent>
|
||||
<Popover
|
||||
trigger="click"
|
||||
popover-class="popover-default user-popover"
|
||||
:overlay-centers="true"
|
||||
overlay-centers-selector=".user-info-avatar-link .Avatar"
|
||||
>
|
||||
<template v-slot:trigger>
|
||||
<UserAvatar
|
||||
class="post-avatar"
|
||||
:bot="botIndicator"
|
||||
:compact="compact"
|
||||
:better-shadow="betterShadow"
|
||||
:user="status.user"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:content>
|
||||
<UserCard
|
||||
class="mention-link-popover"
|
||||
:user-id="status.user.id"
|
||||
:hide-bio="true"
|
||||
:bordered="false"
|
||||
:allow-zooming-avatar="true"
|
||||
:rounded="true"
|
||||
/>
|
||||
</template>
|
||||
</Popover>
|
||||
</a>
|
||||
</div>
|
||||
<div class="right-side">
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
height: 1.4;
|
||||
height: 1.4em;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,8 +52,6 @@
|
|||
border-width: 1px;
|
||||
border-radius: $fallback--tooltipRadius;
|
||||
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
|
||||
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: var(--popupShadow);
|
||||
|
||||
/* TODO cleanup this */
|
||||
.Status.Status {
|
||||
|
|
|
@ -93,7 +93,6 @@
|
|||
<style lang="scss">
|
||||
|
||||
.TimelineQuickSettings {
|
||||
align-self: stretch;
|
||||
|
||||
> button {
|
||||
line-height: 100%;
|
||||
|
|
|
@ -43,6 +43,10 @@
|
|||
min-width: 0;
|
||||
width: 24rem;
|
||||
|
||||
.popover-trigger-button {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.timeline-menu-popover-wrap {
|
||||
overflow: hidden;
|
||||
// Match panel heading padding to line up menu with bottom of heading
|
||||
|
|
|
@ -160,7 +160,8 @@
|
|||
"repeated_you": "repeated your status",
|
||||
"no_more_notifications": "No more notifications",
|
||||
"migrated_to": "migrated to",
|
||||
"reacted_with": "reacted with {0}"
|
||||
"reacted_with": "reacted with {0}",
|
||||
"poll_ended": "poll has ended"
|
||||
},
|
||||
"polls": {
|
||||
"add_poll": "Add poll",
|
||||
|
@ -254,7 +255,8 @@
|
|||
"password_required": "cannot be left blank",
|
||||
"password_confirmation_required": "cannot be left blank",
|
||||
"password_confirmation_match": "should be the same as password"
|
||||
}
|
||||
},
|
||||
"email_language": "In which language do you want to receive emails from the server?"
|
||||
},
|
||||
"remote_user_resolver": {
|
||||
"remote_user_resolver": "Remote user resolver",
|
||||
|
@ -303,6 +305,7 @@
|
|||
"avatarRadius": "Avatars",
|
||||
"background": "Background",
|
||||
"bio": "Bio",
|
||||
"email_language": "Language for receiving emails from the server",
|
||||
"block_export": "Block export",
|
||||
"block_export_button": "Export your blocks to a csv file",
|
||||
"block_import": "Block import",
|
||||
|
@ -427,6 +430,7 @@
|
|||
"notification_visibility_repeats": "Repeats",
|
||||
"notification_visibility_moves": "User Migrates",
|
||||
"notification_visibility_emoji_reactions": "Reactions",
|
||||
"notification_visibility_polls": "Ends of polls you voted in",
|
||||
"no_rich_text_description": "Strip rich text formatting from all posts",
|
||||
"no_blocks": "No blocks",
|
||||
"no_mutes": "No mutes",
|
||||
|
@ -519,7 +523,7 @@
|
|||
"mention_link_display_short": "always as short names (e.g. {'@'}foo)",
|
||||
"mention_link_display_full_for_remote": "as full names only for remote users (e.g. {'@'}foo{'@'}example.org)",
|
||||
"mention_link_display_full": "always as full names (e.g. {'@'}foo{'@'}example.org)",
|
||||
"mention_link_show_tooltip": "Show full user names as tooltip for remote users",
|
||||
"mention_link_use_tooltip": "Show user card when clicking mention links",
|
||||
"mention_link_show_avatar": "Show user avatar beside the link",
|
||||
"mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)",
|
||||
"mention_link_bolden_you": "Highlight mention of you when you are mentioned",
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import Cookies from 'js-cookie'
|
||||
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
|
||||
import messages from '../i18n/messages'
|
||||
import localeService from '../services/locale/locale.service.js'
|
||||
|
||||
const BACKEND_LANGUAGE_COOKIE_NAME = 'userLanguage'
|
||||
|
||||
const browserLocale = (window.navigator.language || 'en').split('-')[0]
|
||||
|
||||
|
@ -55,7 +59,8 @@ export const defaultState = {
|
|||
moves: true,
|
||||
emojiReactions: true,
|
||||
followRequest: true,
|
||||
chatMention: true
|
||||
chatMention: true,
|
||||
polls: true
|
||||
},
|
||||
webPushNotifications: false,
|
||||
muteWords: [],
|
||||
|
@ -165,6 +170,7 @@ const config = {
|
|||
break
|
||||
case 'interfaceLanguage':
|
||||
messages.setLanguage(this.getters.i18n, value)
|
||||
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
|
||||
break
|
||||
case 'thirdColumnMode':
|
||||
dispatch('setLayoutWidth', undefined)
|
||||
|
|
|
@ -151,9 +151,15 @@ const updateNotificationSettings = ({ credentials, settings }) => {
|
|||
}).then((data) => data.json())
|
||||
}
|
||||
|
||||
const updateProfileImages = ({ credentials, avatar = null, banner = null, background = null }) => {
|
||||
const updateProfileImages = ({ credentials, avatar = null, avatarName = null, banner = null, background = null }) => {
|
||||
const form = new FormData()
|
||||
if (avatar !== null) form.append('avatar', avatar)
|
||||
if (avatar !== null) {
|
||||
if (avatarName !== null) {
|
||||
form.append('avatar', avatar, avatarName)
|
||||
} else {
|
||||
form.append('avatar', avatar)
|
||||
}
|
||||
}
|
||||
if (banner !== null) form.append('header', banner)
|
||||
if (background !== null) form.append('pleroma_background_image', background)
|
||||
return fetch(MASTODON_PROFILE_UPDATE_URL, {
|
||||
|
@ -191,6 +197,7 @@ const updateProfile = ({ credentials, params }) => {
|
|||
// homepage
|
||||
// location
|
||||
// token
|
||||
// language
|
||||
const register = ({ params, credentials }) => {
|
||||
const { nickname, ...rest } = params
|
||||
return fetch(MASTODON_REGISTRATION_URL, {
|
||||
|
|
|
@ -1,12 +1,35 @@
|
|||
import languagesObject from '../../i18n/messages'
|
||||
import ISO6391 from 'iso-639-1'
|
||||
import _ from 'lodash'
|
||||
|
||||
const specialLanguageCodes = {
|
||||
'ja_easy': 'ja',
|
||||
'zh_Hant': 'zh-HANT'
|
||||
'zh_Hant': 'zh-HANT',
|
||||
'zh': 'zh-Hans'
|
||||
}
|
||||
|
||||
const internalToBrowserLocale = code => specialLanguageCodes[code] || code
|
||||
|
||||
const internalToBackendLocale = code => internalToBrowserLocale(code).replace('_', '-')
|
||||
|
||||
const getLanguageName = (code) => {
|
||||
const specialLanguageNames = {
|
||||
'ja_easy': 'やさしいにほんご',
|
||||
'zh': '简体中文',
|
||||
'zh_Hant': '繁體中文'
|
||||
}
|
||||
const languageName = specialLanguageNames[code] || ISO6391.getNativeName(code)
|
||||
const browserLocale = internalToBrowserLocale(code)
|
||||
return languageName.charAt(0).toLocaleUpperCase(browserLocale) + languageName.slice(1)
|
||||
}
|
||||
|
||||
const languages = _.map(languagesObject.languages, (code) => ({ code: code, name: getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name))
|
||||
|
||||
const localeService = {
|
||||
internalToBrowserLocale
|
||||
internalToBrowserLocale,
|
||||
internalToBackendLocale,
|
||||
languages,
|
||||
getLanguageName
|
||||
}
|
||||
|
||||
export default localeService
|
||||
|
|
|
@ -14,11 +14,12 @@ export const visibleTypes = store => {
|
|||
rootState.config.notificationVisibility.follows && 'follow',
|
||||
rootState.config.notificationVisibility.followRequest && 'follow_request',
|
||||
rootState.config.notificationVisibility.moves && 'move',
|
||||
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction'
|
||||
rootState.config.notificationVisibility.emojiReactions && 'pleroma:emoji_reaction',
|
||||
rootState.config.notificationVisibility.polls && 'poll'
|
||||
].filter(_ => _))
|
||||
}
|
||||
|
||||
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction']
|
||||
const statusNotifications = ['like', 'mention', 'repeat', 'pleroma:emoji_reaction', 'poll']
|
||||
|
||||
export const isStatusNotification = (type) => includes(statusNotifications, type)
|
||||
|
||||
|
@ -98,6 +99,9 @@ export const prepareNotificationObject = (notification, i18n) => {
|
|||
case 'follow_request':
|
||||
i18nString = 'follow_request'
|
||||
break
|
||||
case 'poll':
|
||||
i18nString = 'poll_ended'
|
||||
break
|
||||
}
|
||||
|
||||
if (notification.type === 'pleroma:emoji_reaction') {
|
||||
|
|
|
@ -4,7 +4,15 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
|
|||
const attentions = []
|
||||
const global = {
|
||||
mocks: {
|
||||
'$store': null
|
||||
'$store': {
|
||||
state: {},
|
||||
getters: {
|
||||
mergedConfig: () => ({
|
||||
mentionLinkShowTooltip: true
|
||||
}),
|
||||
findUserByUrl: () => null
|
||||
}
|
||||
}
|
||||
},
|
||||
stubs: {
|
||||
FAIcon: true
|
||||
|
@ -131,8 +139,7 @@ describe('RichContent', () => {
|
|||
].join(''),
|
||||
[
|
||||
makeMention('John'),
|
||||
makeMention('Josh'),
|
||||
makeMention('Jeremy')
|
||||
makeMention('Josh'), makeMention('Jeremy')
|
||||
].join('')
|
||||
].join('\n')
|
||||
|
||||
|
@ -359,9 +366,9 @@ describe('RichContent', () => {
|
|||
'</span>',
|
||||
'</a>',
|
||||
'<!-- eslint-enable vue/no-v-html -->',
|
||||
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'<!---->', // vue placeholder
|
||||
'</span>',
|
||||
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
|
||||
'<!--v-if-->', // vue placeholder, mentionsline's extra mentions and stuff
|
||||
'</span>'
|
||||
),
|
||||
p(
|
||||
|
@ -422,7 +429,7 @@ describe('RichContent', () => {
|
|||
'</span>',
|
||||
'</a>',
|
||||
'<!-- eslint-enable vue/no-v-html -->',
|
||||
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'<!---->', // vue placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'</span>',
|
||||
'<span class="MentionLink mention-link">',
|
||||
'<!-- eslint-disable vue/no-v-html -->',
|
||||
|
@ -435,9 +442,9 @@ describe('RichContent', () => {
|
|||
'</span>',
|
||||
'</a>',
|
||||
'<!-- eslint-enable vue/no-v-html -->',
|
||||
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'<!---->', // vue placeholder, mentionlink's "new" (i.e. rich) display
|
||||
'</span>',
|
||||
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff
|
||||
'<!--v-if-->', // vue placeholder, mentionsline's extra mentions and stuff
|
||||
'</span>',
|
||||
' ',
|
||||
'</span>',
|
||||
|
|
39
yarn.lock
39
yarn.lock
|
@ -1559,10 +1559,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-rc.17.tgz#e6dcf5b5bd3ae23595bdb154b9b578ebcdffd698"
|
||||
integrity sha512-7LHZKsFRV/HqDoMVY+cJamFzgHgsrmQFalROHC5FMWrzPzd+utG5e11krj1tVsnxYufGA2ABShX4nlcHXED+zQ==
|
||||
|
||||
"@vuelidate/core@2.0.0-alpha.35":
|
||||
version "2.0.0-alpha.35"
|
||||
resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.0-alpha.35.tgz#22d91787147b0883d31585fab44d0218622b7560"
|
||||
integrity sha512-BSGQElu5lyI0GzqehFzUWy7GXhEUC7Z8oEpdxgCyGGN5gOFlAQ5Zr4dDFzfIOhU4jik3CfPHK+i+Juqg2OCKNw==
|
||||
"@vuelidate/core@2.0.0-alpha.41":
|
||||
version "2.0.0-alpha.41"
|
||||
resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.0-alpha.41.tgz#eb4644aa45755c20901b4b8d20e1fecfd5389142"
|
||||
integrity sha512-fST7s5wiLW8ZNTexe8+7fDdBZYT7HjbuA43/XDtKTlHs1BMRDDaBoFLZbHSqmHisQvGXa7zLG9bvG8X5cHZaxg==
|
||||
dependencies:
|
||||
vue-demi "^0.12.0"
|
||||
|
||||
|
@ -5722,6 +5722,11 @@ js-base64@^2.1.9:
|
|||
version "2.5.0"
|
||||
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e"
|
||||
|
||||
js-cookie@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414"
|
||||
integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b"
|
||||
|
@ -6223,6 +6228,11 @@ lodash.isequal@^4.2.0:
|
|||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
|
||||
|
||||
lodash.isfunction@^3.0.8:
|
||||
version "3.0.9"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051"
|
||||
integrity sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==
|
||||
|
||||
lodash.isplainobject@^3.0.0, lodash.isplainobject@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5"
|
||||
|
@ -8671,20 +8681,20 @@ selenium-server@2.53.1:
|
|||
version "2.53.1"
|
||||
resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-2.53.1.tgz#d681528812f3c2e0531a6b7e613e23bb02cce8a6"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@5.6.0, semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.5.0, semver@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004"
|
||||
|
||||
semver@5.7.1, semver@^5.4.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
|
||||
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==
|
||||
|
||||
semver@^5.4.1:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
semver@^5.5.1:
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b"
|
||||
|
@ -10093,13 +10103,14 @@ webpack-log@^2.0.0:
|
|||
ansi-colors "^3.0.0"
|
||||
uuid "^3.3.2"
|
||||
|
||||
webpack-merge@0.14.1:
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-0.14.1.tgz#d6bfe6d9360a024e1e7f8e6383ae735f1737cd23"
|
||||
integrity sha1-1r/m2TYKAk4ef45jg65zXxc3zSM=
|
||||
webpack-merge@0.20.0:
|
||||
version "0.20.0"
|
||||
resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-0.20.0.tgz#e4b73429517181a287c59c8cafef5fc9eb1d9705"
|
||||
integrity sha1-5Lc0KVFxgaKHxZyMr+9fyesdlwU=
|
||||
dependencies:
|
||||
lodash.find "^3.2.1"
|
||||
lodash.isequal "^4.2.0"
|
||||
lodash.isfunction "^3.0.8"
|
||||
lodash.isplainobject "^3.2.0"
|
||||
lodash.merge "^3.3.2"
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue