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:
Henry Jameson 2022-06-15 04:08:49 +03:00
commit d4c17bab12
38 changed files with 527 additions and 239 deletions

View file

@ -23,7 +23,7 @@
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/vue-fontawesome": "3.0.0-5", "@fortawesome/vue-fontawesome": "3.0.0-5",
"@kazvmoe-infra/pinch-zoom-element": "1.2.0", "@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", "@vuelidate/validators": "2.0.0-alpha.27",
"body-scroll-lock": "2.7.1", "body-scroll-lock": "2.7.1",
"chromatism": "3.0.0", "chromatism": "3.0.0",
@ -31,6 +31,7 @@
"cropperjs": "1.5.12", "cropperjs": "1.5.12",
"diff": "3.5.0", "diff": "3.5.0",
"escape-html": "1.0.3", "escape-html": "1.0.3",
"js-cookie": "^3.0.1",
"localforage": "1.10.0", "localforage": "1.10.0",
"parse-link-header": "1.0.1", "parse-link-header": "1.0.1",
"phoenix": "1.6.2", "phoenix": "1.6.2",
@ -106,7 +107,7 @@
"sass": "1.20.1", "sass": "1.20.1",
"sass-loader": "7.2.0", "sass-loader": "7.2.0",
"selenium-server": "2.53.1", "selenium-server": "2.53.1",
"semver": "5.6.0", "semver": "5.7.1",
"serviceworker-webpack-plugin": "1.0.1", "serviceworker-webpack-plugin": "1.0.1",
"shelljs": "0.8.5", "shelljs": "0.8.5",
"sinon": "2.4.1", "sinon": "2.4.1",
@ -120,7 +121,7 @@
"webpack": "4.46.0", "webpack": "4.46.0",
"webpack-dev-middleware": "3.7.3", "webpack-dev-middleware": "3.7.3",
"webpack-hot-middleware": "2.24.3", "webpack-hot-middleware": "2.24.3",
"webpack-merge": "0.14.1" "webpack-merge": "0.20.0"
}, },
"engines": { "engines": {
"node": ">= 4.0.0", "node": ">= 4.0.0",

View file

@ -4,7 +4,6 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance
import FeaturesPanel from './components/features_panel/features_panel.vue' import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_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 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 MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue' import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue' import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
@ -32,7 +31,7 @@ export default {
MobilePostStatusButton, MobilePostStatusButton,
MobileNav, MobileNav,
DesktopNav, DesktopNav,
SettingsModal, SettingsModal: defineAsyncComponent(() => import('./components/settings_modal/settings_modal.vue')),
UserReportingModal, UserReportingModal,
PostStatusModal, PostStatusModal,
GlobalNoticeList GlobalNoticeList

View file

@ -301,6 +301,15 @@ nav {
margin-bottom: 0; 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, .underlay,
#sidebar, #sidebar,
#notifs-column { #notifs-column {
@ -820,7 +829,7 @@ option {
// Vue transitions // Vue transitions
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: opacity 0.2s; transition: opacity 0.3s;
} }
.fade-enter-from, .fade-enter-from,

View file

@ -55,6 +55,7 @@
<SettingsModal /> <SettingsModal />
<div id="modal" /> <div id="modal" />
<GlobalNoticeList /> <GlobalNoticeList />
<div id="popovers" />
</div> </div>
</template> </template>

View file

@ -40,7 +40,7 @@ const AccountActions = {
openChat () { openChat () {
this.$router.push({ this.$router.push({
name: 'chat', name: 'chat',
params: { recipient_id: this.user.id } params: { username: this.$store.state.users.currentUser.screen_name, recipient_id: this.user.id }
}) })
} }
}, },

View file

@ -44,7 +44,7 @@
<Popover <Popover
trigger="click" trigger="click"
placement="top" placement="top"
:bound-to-selector="isCurrentUser ? '' : '.scrollable-message-list'" bound-to-selector=".chat-view-inner"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
:margin="popoverMarginStyle" :margin="popoverMarginStyle"
@show="menuOpened = true" @show="menuOpened = true"

View file

@ -119,12 +119,12 @@
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
<button class="button-unstyled popover-trigger"> <span class="button-unstyled popover-trigger">
<FAIcon <FAIcon
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
icon="ellipsis-h" icon="ellipsis-h"
/> />
</button> </span>
</template> </template>
</Popover> </Popover>
</template> </template>

View file

@ -1,12 +1,12 @@
<template> <template>
<div> <div>
<label for="interface-language-switcher"> <label for="interface-language-switcher">
{{ $t('settings.interfaceLanguage') }} {{ promptText }}
</label> </label>
{{ ' ' }} {{ ' ' }}
<Select <Select
id="interface-language-switcher" id="interface-language-switcher"
v-model="language" v-model="controlledLanguage"
> >
<option <option
v-for="lang in languages" v-for="lang in languages"
@ -20,39 +20,43 @@
</template> </template>
<script> <script>
import languagesObject from '../../i18n/messages'
import localeService from '../../services/locale/locale.service.js' import localeService from '../../services/locale/locale.service.js'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
import Select from '../select/select.vue' import Select from '../select/select.vue'
export default { export default {
components: { components: {
Select Select
}, },
props: {
promptText: {
type: String,
required: true
},
language: {
type: String,
required: true
},
setLanguage: {
type: Function,
required: true
}
},
computed: { computed: {
languages () { languages () {
return _.map(languagesObject.languages, (code) => ({ code: code, name: this.getLanguageName(code) })).sort((a, b) => a.name.localeCompare(b.name)) return localeService.languages
}, },
language: { controlledLanguage: {
get: function () { return this.$store.getters.mergedConfig.interfaceLanguage }, get: function () { return this.language },
set: function (val) { set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val }) this.setLanguage(val)
} }
} }
}, },
methods: { methods: {
getLanguageName (code) { getLanguageName (code) {
const specialLanguageNames = { return localeService.getLanguageName(code)
'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)
} }
} }
} }

View file

@ -2,6 +2,7 @@ import generateProfileLink from 'src/services/user_profile_link_generator/user_p
import { mapGetters, mapState } from 'vuex' import { mapGetters, mapState } from 'vuex'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import { defineAsyncComponent } from 'vue'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faAt faAt
@ -14,7 +15,9 @@ library.add(
const MentionLink = { const MentionLink = {
name: 'MentionLink', name: 'MentionLink',
components: { components: {
UserAvatar UserAvatar,
Popover: defineAsyncComponent(() => import('../popover/popover.vue')),
UserCard: defineAsyncComponent(() => import('../user_card/user_card.vue'))
}, },
props: { props: {
url: { url: {
@ -36,6 +39,7 @@ const MentionLink = {
}, },
methods: { methods: {
onClick () { onClick () {
if (this.shouldShowTooltip) return
const link = generateProfileLink( const link = generateProfileLink(
this.userId || this.user.id, this.userId || this.user.id,
this.userScreenName || this.user.screen_name this.userScreenName || this.user.screen_name
@ -110,7 +114,7 @@ const MentionLink = {
} }
}, },
shouldShowTooltip () { shouldShowTooltip () {
return this.mergedConfig.mentionLinkShowTooltip && this.mergedConfig.mentionLinkDisplay === 'short' && this.isRemote return this.mergedConfig.mentionLinkShowTooltip
}, },
shouldShowAvatar () { shouldShowAvatar () {
return this.mergedConfig.mentionLinkShowAvatar return this.mergedConfig.mentionLinkShowAvatar

View file

@ -101,7 +101,6 @@
} }
&:hover .new .full { &:hover .new .full {
opacity: 1;
pointer-events: initial; pointer-events: initial;
} }
@ -113,3 +112,9 @@
color: var(--faint, $fallback--faint); color: var(--faint, $fallback--faint);
} }
} }
.mention-link-popover {
max-width: 70ch;
max-height: 20rem;
overflow: hidden;
}

View file

@ -9,66 +9,85 @@
class="original" class="original"
target="_blank" target="_blank"
v-html="content" v-html="content"
/><!-- eslint-enable vue/no-v-html --><span /><!-- eslint-enable vue/no-v-html -->
v-if="user" <Popover
class="new" trigger="click"
:style="style" :bound-to="{ x: 'container'}"
:class="classnames" bound-to-selector=".column"
popover-class="popover-default mention-popover"
:disabled="!shouldShowTooltip"
> >
<a <template v-slot:trigger>
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 <span
v-if="isYou && shouldShowYous" v-if="user"
:class="{ '-you': shouldBoldenYou }" class="new"
> {{ ' ' + $t('status.you') }}</span> :style="style"
<!-- eslint-enable vue/no-v-html --> :class="classnames"
</a><span
v-if="shouldShowTooltip"
class="full popover-default"
:class="[highlightType]"
>
<span
class="userNameFull"
> >
<!-- eslint-disable vue/no-v-html --> <a
@<span class="short button-unstyled"
class="userName" :class="{ '-with-tooltip': shouldShowTooltip }"
v-html="userName" :href="url"
/><span @click.prevent="onClick"
class="serverName" >
:class="{ '-faded': shouldFadeDomain }" <!-- eslint-disable vue/no-v-html -->
v-html="'@' + serverName" <UserAvatar
/> v-if="shouldShowAvatar"
<!-- eslint-enable vue/no-v-html --> class="mention-avatar"
</span> :user="user"
</span> /><span
</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> </span>
</template> </template>

View file

@ -5,6 +5,7 @@ import UserAvatar from '../user_avatar/user_avatar.vue'
import UserCard from '../user_card/user_card.vue' import UserCard from '../user_card/user_card.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' 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 { isStatusNotification } from '../../services/notification_utils/notification_utils.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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 generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
@ -46,7 +47,8 @@ const Notification = {
UserCard, UserCard,
Timeago, Timeago,
Status, Status,
RichContent RichContent,
Popover
}, },
methods: { methods: {
toggleUserExpanded () { toggleUserExpanded () {

View file

@ -34,21 +34,36 @@
<a <a
class="avatar-container" class="avatar-container"
:href="$router.resolve(userProfileLink).href" :href="$router.resolve(userProfileLink).href"
@click.stop.prevent.capture="toggleUserExpanded" @click.prevent
> >
<UserAvatar <Popover
:compact="true" trigger="click"
:better-shadow="betterShadow" popover-class="popover-default user-popover"
:user="notification.from_profile" :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> </a>
<div class="notification-right"> <div class="notification-right">
<UserCard
v-if="userExpanded"
:user-id="getUser(notification).id"
:rounded="true"
:bordered="true"
/>
<span class="notification-details"> <span class="notification-details">
<div class="name-and-action"> <div class="name-and-action">
<!-- eslint-disable vue/no-v-html --> <!-- eslint-disable vue/no-v-html -->
@ -120,6 +135,14 @@
</i18n-t> </i18n-t>
</small> </small>
</span> </span>
<span v-if="notification.type === 'poll'">
<FAIcon
class="type-icon"
icon="poll-h"
/>
{{ ' ' }}
<small>{{ $t('notifications.poll_ended') }}</small>
</span>
</div> </div>
<div <div
v-if="isStatusNotification" v-if="isStatusNotification"

View file

@ -61,6 +61,15 @@
:class="{ 'menu-checkbox-checked': filters.moves }" :class="{ 'menu-checkbox-checked': filters.moves }"
/>{{ $t('settings.notification_visibility_moves') }} />{{ $t('settings.notification_visibility_moves') }}
</button> </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> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>

View file

@ -30,7 +30,7 @@
v-for="notification in notificationsToDisplay" v-for="notification in notificationsToDisplay"
:key="notification.id" :key="notification.id"
class="notification" class="notification"
:class="{&quot;unseen&quot;: !minimalMode && !notification.seen}" :class="{unseen: !minimalMode && !notification.seen}"
> >
<div class="notification-overlay" /> <div class="notification-overlay" />
<notification :notification="notification" /> <notification :notification="notification" />

View file

@ -31,13 +31,24 @@ const Popover = {
// If true, subtract padding when calculating position for the popover, // If true, subtract padding when calculating position for the popover,
// use it when popover offset looks to be different on top vs bottom. // 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 () { data () {
return { return {
hidden: true, hidden: true,
styles: { opacity: 0 }, styles: {},
oldSize: { width: 0, height: 0 } oldSize: { width: 0, height: 0 },
// used to avoid blinking if hovered onto popover
graceTimeout: null
} }
}, },
methods: { methods: {
@ -47,9 +58,7 @@ const Popover = {
}, },
updateStyles () { updateStyles () {
if (this.hidden) { if (this.hidden) {
this.styles = { this.styles = {}
opacity: 0
}
return return
} }
@ -57,14 +66,26 @@ const Popover = {
// its children are what are inside the slot. Expect only one v-slot:trigger. // 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 const anchorEl = (this.$refs.trigger && this.$refs.trigger.children[0]) || this.$el
// SVGs don't have offsetWidth/Height, use fallback // SVGs don't have offsetWidth/Height, use fallback
const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight const anchorHeight = anchorEl.offsetHeight || anchorEl.clientHeight
const screenBox = anchorEl.getBoundingClientRect() const anchorWidth = anchorEl.offsetWidth || anchorEl.clientWidth
// Screen position of the origin point for popover const anchorScreenBox = anchorEl.getBoundingClientRect()
const origin = { x: screenBox.left + screenBox.width * 0.5, y: screenBox.top }
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 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 // 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.boundTo.x === 'container' || this.boundTo.y === 'container') &&
this.containerBoundingClientRect() this.containerBoundingClientRect()
@ -73,64 +94,85 @@ const Popover = {
// What are the screen bounds for the popover? Viewport vs container // What are the screen bounds for the popover? Viewport vs container
// when using viewport, using default margin values to dodge the navbar // when using viewport, using default margin values to dodge the navbar
const xBounds = this.boundTo && this.boundTo.x === 'container' ? { const xBounds = this.boundTo && this.boundTo.x === 'container' ? {
min: parentBounds.left + (margin.left || 0), min: parentScreenBox.left + (margin.left || 0),
max: parentBounds.right - (margin.right || 0) max: parentScreenBox.right - (margin.right || 0)
} : { } : {
min: 0 + (margin.left || 10), min: 0 + (margin.left || 10),
max: window.innerWidth - (margin.right || 10) max: window.innerWidth - (margin.right || 10)
} }
const yBounds = this.boundTo && this.boundTo.y === 'container' ? { const yBounds = this.boundTo && this.boundTo.y === 'container' ? {
min: parentBounds.top + (margin.top || 0), min: parentScreenBox.top + (margin.top || 0),
max: parentBounds.bottom - (margin.bottom || 0) max: parentScreenBox.bottom - (margin.bottom || 0)
} : { } : {
min: 0 + (margin.top || 50), min: 0 + (margin.top || 50),
max: window.innerHeight - (margin.bottom || 5) max: window.innerHeight - (margin.bottom || 5)
} }
let horizOffset = 0 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 overflowing from left, move it so that it doesn't
if ((origin.x - content.offsetWidth * 0.5) < xBounds.min) { if (leftBorder < xBounds.min) {
horizOffset += -(origin.x - content.offsetWidth * 0.5) + xBounds.min horizOffset += xBounds.min - leftBorder
} }
// If overflowing from right, move it so that it doesn't // If overflowing from right, move it so that it doesn't
if ((origin.x + horizOffset + content.offsetWidth * 0.5) > xBounds.max) { if (rightBorder > xBounds.max) {
horizOffset -= (origin.x + horizOffset + content.offsetWidth * 0.5) - xBounds.max horizOffset -= rightBorder - xBounds.max
} }
// Default to whatever user wished with placement prop let translateX = 0
let usingTop = this.placement !== 'bottom' let translateY = 0
// Handle special cases, first force to displaying on top if there's not space on bottom, if (overlayCenter) {
// regardless of what placement value was. Then check if there's not space on top, and translateX = origin.x + horizOffset
// force to bottom, again regardless of what placement value was. translateY = origin.y + vertOffset
if (origin.y + content.offsetHeight > yBounds.max) usingTop = true } else {
if (origin.y - content.offsetHeight < yBounds.min) usingTop = false // Default to whatever user wished with placement prop
let usingTop = this.placement !== 'bottom'
let vPadding = 0 // Handle special cases, first force to displaying on top if there's not space on bottom,
if (this.removePadding && usingTop) { // regardless of what placement value was. Then check if there's not space on top, and
const anchorStyle = getComputedStyle(anchorEl) // force to bottom, again regardless of what placement value was.
vPadding = parseFloat(anchorStyle.paddingTop) + parseFloat(anchorStyle.paddingBottom) 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 = { this.styles = {
opacity: 1, left: `${Math.round(translateX)}px`,
transform: `translateX(${Math.round(translateX)}px) translateY(${Math.round(translateY)}px)` top: `${Math.round(translateY)}px`
}
if (parentScreenBox) {
this.styles.maxWidth = `${Math.round(parentScreenBox.width)}px`
} }
}, },
showPopover () { showPopover () {
if (this.disabled) return
const wasHidden = this.hidden const wasHidden = this.hidden
this.hidden = false this.hidden = false
this.$nextTick(() => { this.$nextTick(() => {
@ -141,13 +183,30 @@ const Popover = {
hidePopover () { hidePopover () {
if (!this.hidden) this.$emit('close') if (!this.hidden) this.$emit('close')
this.hidden = true this.hidden = true
this.styles = { opacity: 0 }
}, },
onMouseenter (e) { onMouseenter (e) {
if (this.trigger === 'hover') this.showPopover() if (this.trigger === 'hover') {
clearTimeout(this.graceTimeout)
this.graceTimeout = null
this.showPopover()
}
}, },
onMouseleave (e) { 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) { onClick (e) {
if (this.trigger === 'click') { if (this.trigger === 'click') {
@ -160,8 +219,12 @@ const Popover = {
}, },
onClickOutside (e) { onClickOutside (e) {
if (this.hidden) return if (this.hidden) return
if (this.$refs.content.contains(e.target)) return
if (this.$el.contains(e.target)) return if (this.$el.contains(e.target)) return
this.hidePopover() this.hidePopover()
},
onScroll (e) {
this.updateStyles()
} }
}, },
updated () { updated () {
@ -175,11 +238,17 @@ const Popover = {
this.oldSize = { width: content.offsetWidth, height: content.offsetHeight } 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) 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) document.removeEventListener('click', this.onClickOutside)
scrollable.removeEventListener('scroll', this.onScroll)
this.hidePopover() this.hidePopover()
} }
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<div <span
@mouseenter="onMouseenter" @mouseenter="onMouseenter"
@mouseleave="onMouseleave" @mouseleave="onMouseleave"
> >
@ -11,20 +11,26 @@
> >
<slot name="trigger" /> <slot name="trigger" />
</button> </button>
<div <teleport to="#popovers">
v-if="!hidden" <transition name="fade">
ref="content" <div
:style="styles" v-if="!hidden"
class="popover" ref="content"
:class="popoverClass || 'popover-default'" :style="styles"
> class="popover"
<slot :class="popoverClass || 'popover-default'"
name="content" @mouseenter="onMouseenterContent"
class="popover-inner" @mouseleave="onMouseleaveContent"
:close="hidePopover" >
/> <slot
</div> name="content"
</div> class="popover-inner"
:close="hidePopover"
/>
</div>
</transition>
</teleport>
</span>
</template> </template>
<script src="./popover.js" /> <script src="./popover.js" />
@ -37,14 +43,15 @@
} }
.popover { .popover {
z-index: 500; z-index: 90000;
position: absolute; position: fixed;
min-width: 0; min-width: 0;
max-width: 90vw;
box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
box-shadow: var(--popupShadow);
} }
.popover-default { .popover-default {
transition: opacity 0.3s;
&:after { &:after {
content: ''; content: '';
position: absolute; position: absolute;

View file

@ -6,6 +6,7 @@
:offset="{ y: 5 }" :offset="{ y: 5 }"
:bound-to="{ x: 'container' }" :bound-to="{ x: 'container' }"
remove-padding remove-padding
popover-class="ReactButton popover-default"
@show="focusInput" @show="focusInput"
> >
<template v-slot:content="{close}"> <template v-slot:content="{close}">
@ -40,7 +41,7 @@
</div> </div>
</template> </template>
<template v-slot:trigger> <template v-slot:trigger>
<button <span
class="button-unstyled popover-trigger" class="button-unstyled popover-trigger"
:title="$t('tool_tip.add_reaction')" :title="$t('tool_tip.add_reaction')"
> >
@ -48,7 +49,7 @@
class="fa-scale-110 fa-old-padding" class="fa-scale-110 fa-old-padding"
:icon="['far', 'smile-beam']" :icon="['far', 'smile-beam']"
/> />
</button> </span>
</template> </template>
</Popover> </Popover>
</template> </template>

View file

@ -1,6 +1,8 @@
import useVuelidate from '@vuelidate/core' import useVuelidate from '@vuelidate/core'
import { required, requiredIf, sameAs } from '@vuelidate/validators' import { required, requiredIf, sameAs } from '@vuelidate/validators'
import { mapActions, mapState } from 'vuex' 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 = { const registration = {
setup () { return { v$: useVuelidate() } }, setup () { return { v$: useVuelidate() } },
@ -11,10 +13,14 @@ const registration = {
username: '', username: '',
password: '', password: '',
confirm: '', confirm: '',
reason: '' reason: '',
language: ''
}, },
captcha: {} captcha: {}
}), }),
components: {
InterfaceLanguageSwitcher
},
validations () { validations () {
return { return {
user: { user: {
@ -26,7 +32,8 @@ const registration = {
required, required,
sameAs: sameAs(this.user.password) 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_solution = this.captcha.solution
this.user.captcha_token = this.captcha.token this.user.captcha_token = this.captcha.token
this.user.captcha_answer_data = this.captcha.answer_data this.user.captcha_answer_data = this.captcha.answer_data
if (this.user.language) {
this.user.language = localeService.internalToBackendLocale(this.user.language)
}
this.v$.$touch() this.v$.$touch()

View file

@ -162,6 +162,18 @@
</ul> </ul>
</div> </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 <div
v-if="accountApprovalRequired" v-if="accountApprovalRequired"
class="form-group" class="form-group"

View file

@ -77,6 +77,12 @@ const GeneralTab = {
!this.$store.state.users.currentUser.background_image !this.$store.state.users.currentUser.background_image
}, },
instanceShoutboxPresent () { return this.$store.state.instance.shoutAvailable }, 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() ...SharedComputedObject()
}, },
methods: { methods: {

View file

@ -4,7 +4,11 @@
<h2>{{ $t('settings.interface') }}</h2> <h2>{{ $t('settings.interface') }}</h2>
<ul class="setting-list"> <ul class="setting-list">
<li> <li>
<interface-language-switcher /> <interface-language-switcher
:prompt-text="$t('settings.interfaceLanguage')"
:language="language"
:set-language="val => language = val"
/>
</li> </li>
<li v-if="instanceSpecificPanelPresent"> <li v-if="instanceSpecificPanelPresent">
<BooleanSetting path="hideISP"> <BooleanSetting path="hideISP">
@ -265,7 +269,7 @@
path="mentionLinkShowTooltip" path="mentionLinkShowTooltip"
expert="1" expert="1"
> >
{{ $t('settings.mention_link_show_tooltip') }} {{ $t('settings.mention_link_use_tooltip') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
</ul> </ul>

View file

@ -41,6 +41,11 @@
{{ $t('settings.notification_visibility_emoji_reactions') }} {{ $t('settings.notification_visibility_emoji_reactions') }}
</BooleanSetting> </BooleanSetting>
</li> </li>
<li>
<BooleanSetting path="notificationVisibility.polls">
{{ $t('settings.notification_visibility_polls') }}
</BooleanSetting>
</li>
</ul> </ul>
</li> </li>
</ul> </ul>

View file

@ -8,8 +8,10 @@ import EmojiInput from 'src/components/emoji_input/emoji_input.vue'
import suggestor from 'src/components/emoji_input/suggestor.js' import suggestor from 'src/components/emoji_input/suggestor.js'
import Autosuggest from 'src/components/autosuggest/autosuggest.vue' import Autosuggest from 'src/components/autosuggest/autosuggest.vue'
import Checkbox from 'src/components/checkbox/checkbox.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 BooleanSetting from '../helpers/boolean_setting.vue'
import SharedComputedObject from '../helpers/shared_computed_object.js' 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 { library } from '@fortawesome/fontawesome-svg-core'
import { import {
@ -40,7 +42,8 @@ const ProfileTab = {
banner: null, banner: null,
bannerPreview: null, bannerPreview: null,
background: null, background: null,
backgroundPreview: null backgroundPreview: null,
emailLanguage: this.$store.state.users.currentUser.language || ''
} }
}, },
components: { components: {
@ -50,7 +53,8 @@ const ProfileTab = {
Autosuggest, Autosuggest,
ProgressButton, ProgressButton,
Checkbox, Checkbox,
BooleanSetting BooleanSetting,
InterfaceLanguageSwitcher
}, },
computed: { computed: {
user () { user () {
@ -111,19 +115,25 @@ const ProfileTab = {
}, },
methods: { methods: {
updateProfile () { 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 this.$store.state.api.backendInteractor
.updateProfile({ .updateProfile({ params })
params: { .then((user) => {
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) => {
this.newFields.splice(user.fields.length) this.newFields.splice(user.fields.length)
merge(this.newFields, user.fields) merge(this.newFields, user.fields)
this.$store.commit('addNewUsers', [user]) this.$store.commit('addNewUsers', [user])
@ -193,8 +203,8 @@ const ProfileTab = {
submitAvatar (cropper, file) { submitAvatar (cropper, file) {
const that = this const that = this
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
function updateAvatar (avatar) { function updateAvatar (avatar, avatarName) {
that.$store.state.api.backendInteractor.updateProfileImages({ avatar }) that.$store.state.api.backendInteractor.updateProfileImages({ avatar, avatarName })
.then((user) => { .then((user) => {
that.$store.commit('addNewUsers', [user]) that.$store.commit('addNewUsers', [user])
that.$store.commit('setCurrentUser', user) that.$store.commit('setCurrentUser', user)
@ -207,9 +217,9 @@ const ProfileTab = {
} }
if (cropper) { if (cropper) {
cropper.getCroppedCanvas().toBlob(updateAvatar, file.type) cropper.getCroppedCanvas().toBlob((data) => updateAvatar(data, file.name), file.type)
} else { } else {
updateAvatar(file) updateAvatar(file, file.name)
} }
}) })
}, },

View file

@ -89,6 +89,13 @@
{{ $t('settings.bot') }} {{ $t('settings.bot') }}
</Checkbox> </Checkbox>
</p> </p>
<p>
<interface-language-switcher
:prompt-text="$t('settings.email_language')"
:language="emailLanguage"
:set-language="val => emailLanguage = val"
/>
</p>
<button <button
:disabled="newName && newName.length === 0" :disabled="newName && newName.length === 0"
class="btn button-default" class="btn button-default"

View file

@ -11,6 +11,7 @@ import Timeago from '../timeago/timeago.vue'
import StatusContent from '../status_content/status_content.vue' import StatusContent from '../status_content/status_content.vue'
import RichContent from 'src/components/rich_content/rich_content.jsx' import RichContent from 'src/components/rich_content/rich_content.jsx'
import StatusPopover from '../status_popover/status_popover.vue' 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 UserListPopover from '../user_list_popover/user_list_popover.vue'
import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' import EmojiReactions from '../emoji_reactions/emoji_reactions.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue' import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
@ -115,7 +116,8 @@ const Status = {
StatusContent, StatusContent,
RichContent, RichContent,
MentionLink, MentionLink,
MentionsLine MentionsLine,
Popover
}, },
props: [ props: [
'statusoid', 'statusoid',

View file

@ -122,17 +122,33 @@
v-if="!noHeading" v-if="!noHeading"
class="left-side" class="left-side"
> >
<a <a :href="$router.resolve(userProfileLink).href" @click.prevent>
:href="$router.resolve(userProfileLink).href" <Popover
@click.stop.prevent.capture="toggleUserExpanded" trigger="click"
> popover-class="popover-default user-popover"
<UserAvatar :overlay-centers="true"
class="post-avatar" overlay-centers-selector=".user-info-avatar-link .Avatar"
:bot="botIndicator" >
:compact="compact" <template v-slot:trigger>
:better-shadow="betterShadow" <UserAvatar
:user="status.user" 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> </a>
</div> </div>
<div class="right-side"> <div class="right-side">

View file

@ -33,7 +33,7 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
height: 1.4; height: 1.4em;
} }
} }

View file

@ -52,8 +52,6 @@
border-width: 1px; border-width: 1px;
border-radius: $fallback--tooltipRadius; border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $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 */ /* TODO cleanup this */
.Status.Status { .Status.Status {

View file

@ -93,7 +93,6 @@
<style lang="scss"> <style lang="scss">
.TimelineQuickSettings { .TimelineQuickSettings {
align-self: stretch;
> button { > button {
line-height: 100%; line-height: 100%;

View file

@ -43,6 +43,10 @@
min-width: 0; min-width: 0;
width: 24rem; width: 24rem;
.popover-trigger-button {
vertical-align: bottom;
}
.timeline-menu-popover-wrap { .timeline-menu-popover-wrap {
overflow: hidden; overflow: hidden;
// Match panel heading padding to line up menu with bottom of heading // Match panel heading padding to line up menu with bottom of heading

View file

@ -160,7 +160,8 @@
"repeated_you": "repeated your status", "repeated_you": "repeated your status",
"no_more_notifications": "No more notifications", "no_more_notifications": "No more notifications",
"migrated_to": "migrated to", "migrated_to": "migrated to",
"reacted_with": "reacted with {0}" "reacted_with": "reacted with {0}",
"poll_ended": "poll has ended"
}, },
"polls": { "polls": {
"add_poll": "Add poll", "add_poll": "Add poll",
@ -254,7 +255,8 @@
"password_required": "cannot be left blank", "password_required": "cannot be left blank",
"password_confirmation_required": "cannot be left blank", "password_confirmation_required": "cannot be left blank",
"password_confirmation_match": "should be the same as password" "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": "Remote user resolver", "remote_user_resolver": "Remote user resolver",
@ -303,6 +305,7 @@
"avatarRadius": "Avatars", "avatarRadius": "Avatars",
"background": "Background", "background": "Background",
"bio": "Bio", "bio": "Bio",
"email_language": "Language for receiving emails from the server",
"block_export": "Block export", "block_export": "Block export",
"block_export_button": "Export your blocks to a csv file", "block_export_button": "Export your blocks to a csv file",
"block_import": "Block import", "block_import": "Block import",
@ -427,6 +430,7 @@
"notification_visibility_repeats": "Repeats", "notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates", "notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions", "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_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks", "no_blocks": "No blocks",
"no_mutes": "No mutes", "no_mutes": "No mutes",
@ -519,7 +523,7 @@
"mention_link_display_short": "always as short names (e.g. {'@'}foo)", "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_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_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_show_avatar": "Show user avatar beside the link",
"mention_link_fade_domain": "Fade domains (e.g. {'@'}example.org in {'@'}foo{'@'}example.org)", "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", "mention_link_bolden_you": "Highlight mention of you when you are mentioned",

View file

@ -1,5 +1,9 @@
import Cookies from 'js-cookie'
import { setPreset, applyTheme } from '../services/style_setter/style_setter.js' import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
import messages from '../i18n/messages' 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] const browserLocale = (window.navigator.language || 'en').split('-')[0]
@ -55,7 +59,8 @@ export const defaultState = {
moves: true, moves: true,
emojiReactions: true, emojiReactions: true,
followRequest: true, followRequest: true,
chatMention: true chatMention: true,
polls: true
}, },
webPushNotifications: false, webPushNotifications: false,
muteWords: [], muteWords: [],
@ -165,6 +170,7 @@ const config = {
break break
case 'interfaceLanguage': case 'interfaceLanguage':
messages.setLanguage(this.getters.i18n, value) messages.setLanguage(this.getters.i18n, value)
Cookies.set(BACKEND_LANGUAGE_COOKIE_NAME, localeService.internalToBackendLocale(value))
break break
case 'thirdColumnMode': case 'thirdColumnMode':
dispatch('setLayoutWidth', undefined) dispatch('setLayoutWidth', undefined)

View file

@ -151,9 +151,15 @@ const updateNotificationSettings = ({ credentials, settings }) => {
}).then((data) => data.json()) }).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() 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 (banner !== null) form.append('header', banner)
if (background !== null) form.append('pleroma_background_image', background) if (background !== null) form.append('pleroma_background_image', background)
return fetch(MASTODON_PROFILE_UPDATE_URL, { return fetch(MASTODON_PROFILE_UPDATE_URL, {
@ -191,6 +197,7 @@ const updateProfile = ({ credentials, params }) => {
// homepage // homepage
// location // location
// token // token
// language
const register = ({ params, credentials }) => { const register = ({ params, credentials }) => {
const { nickname, ...rest } = params const { nickname, ...rest } = params
return fetch(MASTODON_REGISTRATION_URL, { return fetch(MASTODON_REGISTRATION_URL, {

View file

@ -1,12 +1,35 @@
import languagesObject from '../../i18n/messages'
import ISO6391 from 'iso-639-1'
import _ from 'lodash'
const specialLanguageCodes = { const specialLanguageCodes = {
'ja_easy': 'ja', 'ja_easy': 'ja',
'zh_Hant': 'zh-HANT' 'zh_Hant': 'zh-HANT',
'zh': 'zh-Hans'
} }
const internalToBrowserLocale = code => specialLanguageCodes[code] || code 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 = { const localeService = {
internalToBrowserLocale internalToBrowserLocale,
internalToBackendLocale,
languages,
getLanguageName
} }
export default localeService export default localeService

View file

@ -14,11 +14,12 @@ export const visibleTypes = store => {
rootState.config.notificationVisibility.follows && 'follow', rootState.config.notificationVisibility.follows && 'follow',
rootState.config.notificationVisibility.followRequest && 'follow_request', rootState.config.notificationVisibility.followRequest && 'follow_request',
rootState.config.notificationVisibility.moves && 'move', 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(_ => _)) ].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) export const isStatusNotification = (type) => includes(statusNotifications, type)
@ -98,6 +99,9 @@ export const prepareNotificationObject = (notification, i18n) => {
case 'follow_request': case 'follow_request':
i18nString = 'follow_request' i18nString = 'follow_request'
break break
case 'poll':
i18nString = 'poll_ended'
break
} }
if (notification.type === 'pleroma:emoji_reaction') { if (notification.type === 'pleroma:emoji_reaction') {

View file

@ -4,7 +4,15 @@ import RichContent from 'src/components/rich_content/rich_content.jsx'
const attentions = [] const attentions = []
const global = { const global = {
mocks: { mocks: {
'$store': null '$store': {
state: {},
getters: {
mergedConfig: () => ({
mentionLinkShowTooltip: true
}),
findUserByUrl: () => null
}
}
}, },
stubs: { stubs: {
FAIcon: true FAIcon: true
@ -131,8 +139,7 @@ describe('RichContent', () => {
].join(''), ].join(''),
[ [
makeMention('John'), makeMention('John'),
makeMention('Josh'), makeMention('Josh'), makeMention('Jeremy')
makeMention('Jeremy')
].join('') ].join('')
].join('\n') ].join('\n')
@ -359,9 +366,9 @@ describe('RichContent', () => {
'</span>', '</span>',
'</a>', '</a>',
'<!-- eslint-enable vue/no-v-html -->', '<!-- eslint-enable vue/no-v-html -->',
'<!--v-if-->', // v-if placeholder, mentionlink's "new" (i.e. rich) display '<!---->', // vue placeholder
'</span>', '</span>',
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff '<!--v-if-->', // vue placeholder, mentionsline's extra mentions and stuff
'</span>' '</span>'
), ),
p( p(
@ -422,7 +429,7 @@ describe('RichContent', () => {
'</span>', '</span>',
'</a>', '</a>',
'<!-- eslint-enable vue/no-v-html -->', '<!-- 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>',
'<span class="MentionLink mention-link">', '<span class="MentionLink mention-link">',
'<!-- eslint-disable vue/no-v-html -->', '<!-- eslint-disable vue/no-v-html -->',
@ -435,9 +442,9 @@ describe('RichContent', () => {
'</span>', '</span>',
'</a>', '</a>',
'<!-- eslint-enable vue/no-v-html -->', '<!-- 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>',
'<!--v-if-->', // v-if placeholder, mentionsline's extra mentions and stuff '<!--v-if-->', // vue placeholder, mentionsline's extra mentions and stuff
'</span>', '</span>',
' ', ' ',
'</span>', '</span>',

View file

@ -1559,10 +1559,10 @@
resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-rc.17.tgz#e6dcf5b5bd3ae23595bdb154b9b578ebcdffd698" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-2.0.0-rc.17.tgz#e6dcf5b5bd3ae23595bdb154b9b578ebcdffd698"
integrity sha512-7LHZKsFRV/HqDoMVY+cJamFzgHgsrmQFalROHC5FMWrzPzd+utG5e11krj1tVsnxYufGA2ABShX4nlcHXED+zQ== integrity sha512-7LHZKsFRV/HqDoMVY+cJamFzgHgsrmQFalROHC5FMWrzPzd+utG5e11krj1tVsnxYufGA2ABShX4nlcHXED+zQ==
"@vuelidate/core@2.0.0-alpha.35": "@vuelidate/core@2.0.0-alpha.41":
version "2.0.0-alpha.35" version "2.0.0-alpha.41"
resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.0-alpha.35.tgz#22d91787147b0883d31585fab44d0218622b7560" resolved "https://registry.yarnpkg.com/@vuelidate/core/-/core-2.0.0-alpha.41.tgz#eb4644aa45755c20901b4b8d20e1fecfd5389142"
integrity sha512-BSGQElu5lyI0GzqehFzUWy7GXhEUC7Z8oEpdxgCyGGN5gOFlAQ5Zr4dDFzfIOhU4jik3CfPHK+i+Juqg2OCKNw== integrity sha512-fST7s5wiLW8ZNTexe8+7fDdBZYT7HjbuA43/XDtKTlHs1BMRDDaBoFLZbHSqmHisQvGXa7zLG9bvG8X5cHZaxg==
dependencies: dependencies:
vue-demi "^0.12.0" vue-demi "^0.12.0"
@ -5722,6 +5722,11 @@ js-base64@^2.1.9:
version "2.5.0" version "2.5.0"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.5.0.tgz#42255ba183ab67ce59a0dee640afdc00ab5ae93e" 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: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 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" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" 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: lodash.isplainobject@^3.0.0, lodash.isplainobject@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-3.2.0.tgz#9a8238ae16b200432960cd7346512d0123fbf4c5" 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" version "2.53.1"
resolved "https://registry.yarnpkg.com/selenium-server/-/selenium-server-2.53.1.tgz#d681528812f3c2e0531a6b7e613e23bb02cce8a6" 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" version "5.6.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" 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: semver@7.0.0:
version "7.0.0" version "7.0.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e"
integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== 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: semver@^5.5.1:
version "5.7.0" version "5.7.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" 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" ansi-colors "^3.0.0"
uuid "^3.3.2" uuid "^3.3.2"
webpack-merge@0.14.1: webpack-merge@0.20.0:
version "0.14.1" version "0.20.0"
resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-0.14.1.tgz#d6bfe6d9360a024e1e7f8e6383ae735f1737cd23" resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-0.20.0.tgz#e4b73429517181a287c59c8cafef5fc9eb1d9705"
integrity sha1-1r/m2TYKAk4ef45jg65zXxc3zSM= integrity sha1-5Lc0KVFxgaKHxZyMr+9fyesdlwU=
dependencies: dependencies:
lodash.find "^3.2.1" lodash.find "^3.2.1"
lodash.isequal "^4.2.0" lodash.isequal "^4.2.0"
lodash.isfunction "^3.0.8"
lodash.isplainobject "^3.2.0" lodash.isplainobject "^3.2.0"
lodash.merge "^3.3.2" lodash.merge "^3.3.2"