Merge remote-tracking branch 'upstream/develop' into settings-refactor

* upstream/develop: (89 commits)
  remove needless ref
  show preview popover when hover numbered replies
  refactor conditions
  do not make too many nested div
  add fetchStatus action
  refactor status loading logic
  split status preview popover into a separate component
  uninstall mobile-detect library
  listen both events
  minor css fix
  restrict distance at top side only
  set different trigger event in desktop and mobile by default
  fix eslint warnings
  fix popper go behind the top bar
  migrate Popper to v-popover
  fix popper go behind the top bar
  fix eslint warnings
  reset font-size to normal text size using rem
  use top placement by default
  hide status preview popper when hover popper content
  ...
This commit is contained in:
Henry Jameson 2019-10-29 09:36:16 +02:00
commit b66564a30d
67 changed files with 857 additions and 495 deletions

View file

@ -7,6 +7,7 @@ stages:
- lint - lint
- build - build
- test - test
- deploy
lint: lint:
stage: lint stage: lint
@ -33,3 +34,13 @@ build:
artifacts: artifacts:
paths: paths:
- dist/ - dist/
docs-deploy:
stage: deploy
image: alpine:latest
only:
- develop@pleroma/pleroma-fe
before_script:
- apk add curl
script:
- curl -X POST -F"token=$DOCS_PIPELINE_TRIGGER" -F'ref=master' https://git.pleroma.social/api/v4/projects/673/trigger/pipeline

View file

@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
## [Unreleased] ## [Unreleased]
### Added ### Added
- Ability to hide/show repeats from user
- User profile button clutter organized into a menu
- Emoji picker - Emoji picker
- Started changelog anew - Started changelog anew
### Changed ### Changed

View file

@ -1,7 +1,7 @@
# Pleroma-FE configuration and customization for instance administrators # Pleroma-FE configuration and customization for instance administrators
* *For user configuration, see USER_GUIDE.md* * *For user configuration, see [Pleroma-FE user guide](USER_GUIDE.md)*
* *For local development server configuration, see HACKING.md* * *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
## Where configuration is stored ## Where configuration is stored

View file

@ -1,3 +1,5 @@
# Pleroma-FE user guide
> Be prepared for breaking changes, unexpected behavior and this user guide becoming obsolete and wrong. > Be prepared for breaking changes, unexpected behavior and this user guide becoming obsolete and wrong.
> If there was no insanity > If there was no insanity
@ -6,8 +8,6 @@
> >
> --Catbag > --Catbag
# Pleroma-FE user guide
Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options. Pleroma-FE user interface is modeled after Qvitter which is modeled after older Twitter design. It provides a simple 2-column interface for microblogging. While being simple by default it also provides many powerful customization options.
## Posting, reading, basic functions. ## Posting, reading, basic functions.

View file

@ -39,10 +39,13 @@ h4 {
text-align: center; text-align: center;
} }
html {
font-size: 14px;
}
body { body {
font-family: sans-serif; font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif); font-family: var(--interfaceFont, sans-serif);
font-size: 14px;
margin: 0; margin: 0;
color: $fallback--text; color: $fallback--text;
color: var(--text, $fallback--text); color: var(--text, $fallback--text);
@ -705,31 +708,6 @@ nav {
} }
} }
@keyframes modal-background-fadein {
from {
background-color: rgba(0, 0, 0, 0);
}
to {
background-color: rgba(0, 0, 0, 0.5);
}
}
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
}
.button-icon { .button-icon {
font-size: 1.2em; font-size: 1.2em;
} }

View file

@ -53,6 +53,15 @@
:title="$t('nav.preferences')" :title="$t('nav.preferences')"
/> />
</router-link> </router-link>
<a
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
class="mobile-hidden"
target="_blank"
><i
class="button-icon icon-gauge nav-icon"
:title="$t('nav.administration')"
/></a>
<a <a
v-if="currentUser" v-if="currentUser"
href="#" href="#"

View file

@ -47,7 +47,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute }, { name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings }, { name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration }, { name: 'registration', path: '/registration', component: Registration },
{ name: 'password-reset', path: '/password-reset', component: PasswordReset }, { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute }, { name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },

View file

@ -0,0 +1,35 @@
import ProgressButton from '../progress_button/progress_button.vue'
const AccountActions = {
props: [
'user'
],
data () {
return { }
},
components: {
ProgressButton
},
methods: {
showRepeats () {
this.$store.dispatch('showReblogs', this.user.id)
},
hideRepeats () {
this.$store.dispatch('hideReblogs', this.user.id)
},
blockUser () {
this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}
export default AccountActions

View file

@ -0,0 +1,93 @@
<template>
<div class="account-actions">
<v-popover
trigger="click"
class="account-tools-popover"
:container="false"
placement="bottom-end"
:offset="5"
>
<div slot="popover">
<div class="dropdown-menu">
<button
class="btn btn-default btn-block dropdown-item"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
<template v-if="user.following">
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.showing_reblogs"
class="btn btn-default dropdown-item"
@click="hideRepeats"
>
{{ $t('user_card.hide_repeats') }}
</button>
<button
v-if="!user.showing_reblogs"
class="btn btn-default dropdown-item"
@click="showRepeats"
>
{{ $t('user_card.show_repeats') }}
</button>
</template>
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block dropdown-item"
@click="unblockUser"
>
{{ $t('user_card.unblock') }}
</button>
<button
v-else
class="btn btn-default btn-block dropdown-item"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
<button
class="btn btn-default btn-block dropdown-item"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
</div>
<div class="btn btn-default ellipsis-button">
<i class="icon-ellipsis trigger-button" />
</div>
</v-popover>
</div>
</template>
<script src="./account_actions.js"></script>
<style lang="scss">
@import '../../_variables.scss';
@import '../popper/popper.scss';
.account-actions {
margin: 0 .8em;
}
.account-actions button.dropdown-item {
margin-left: 0;
}
.account-actions .trigger-button {
color: $fallback--lightText;
color: var(--lightText, $fallback--lightText);
opacity: .8;
cursor: pointer;
&:hover {
color: $fallback--text;
color: var(--text, $fallback--text);
}
}
</style>

View file

@ -10,7 +10,8 @@ const Attachment = {
'statusId', 'statusId',
'size', 'size',
'allowPlay', 'allowPlay',
'setMedia' 'setMedia',
'naturalSizeLoad'
], ],
data () { data () {
return { return {
@ -88,6 +89,11 @@ const Attachment = {
} else { } else {
this.showHidden = !this.showHidden this.showHidden = !this.showHidden
} }
},
onImageLoad (image) {
const width = image.naturalWidth
const height = image.naturalHeight
this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
} }
} }
} }

View file

@ -58,6 +58,7 @@
:referrerpolicy="referrerpolicy" :referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype" :mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url" :src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
/> />
</a> </a>

View file

@ -4,8 +4,6 @@
trigger="click" trigger="click"
placement="top" placement="top"
class="extra-button-popover" class="extra-button-popover"
:offset="5"
:container="false"
> >
<div slot="popover"> <div slot="popover">
<div class="dropdown-menu"> <div class="dropdown-menu">

View file

@ -0,0 +1,53 @@
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
export default {
props: ['user', 'labelFollowing', 'buttonClass'],
data () {
return {
inProgress: false
}
},
computed: {
isPressed () {
return this.inProgress || this.user.following
},
title () {
if (this.inProgress || this.user.following) {
return this.$t('user_card.follow_unfollow')
} else if (this.user.requested) {
return this.$t('user_card.follow_again')
} else {
return this.$t('user_card.follow')
}
},
label () {
if (this.inProgress) {
return this.$t('user_card.follow_progress')
} else if (this.user.following) {
return this.labelFollowing || this.$t('user_card.following')
} else if (this.user.requested) {
return this.$t('user_card.follow_sent')
} else {
return this.$t('user_card.follow')
}
}
},
methods: {
onClick () {
this.user.following ? this.unfollow() : this.follow()
},
follow () {
this.inProgress = true
requestFollow(this.user, this.$store).then(() => {
this.inProgress = false
})
},
unfollow () {
const store = this.$store
this.inProgress = true
requestUnfollow(this.user, store).then(() => {
this.inProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
}
}
}

View file

@ -0,0 +1,13 @@
<template>
<button
class="btn btn-default follow-button"
:class="{ pressed: isPressed }"
:disabled="inProgress"
:title="title"
@click="onClick"
>
{{ label }}
</button>
</template>
<script src="./follow_button.js"></script>

View file

@ -1,21 +1,16 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue' import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate' import FollowButton from '../follow_button/follow_button.vue'
const FollowCard = { const FollowCard = {
props: [ props: [
'user', 'user',
'noFollowsYou' 'noFollowsYou'
], ],
data () {
return {
inProgress: false,
requestSent: false
}
},
components: { components: {
BasicUserCard, BasicUserCard,
RemoteFollow RemoteFollow,
FollowButton
}, },
computed: { computed: {
isMe () { isMe () {
@ -24,21 +19,6 @@ const FollowCard = {
loggedIn () { loggedIn () {
return this.$store.state.users.currentUser return this.$store.state.users.currentUser
} }
},
methods: {
followUser () {
this.inProgress = true
requestFollow(this.user, this.$store).then(({ sent }) => {
this.inProgress = false
this.requestSent = sent
})
},
unfollowUser () {
this.inProgress = true
requestUnfollow(this.user, this.$store).then(() => {
this.inProgress = false
})
}
} }
} }

View file

@ -16,36 +16,11 @@
</div> </div>
</template> </template>
<template v-else> <template v-else>
<button <FollowButton
v-if="!user.following" :user="user"
class="btn btn-default follow-card-follow-button" class="follow-card-follow-button"
:disabled="inProgress" :label-following="$t('user_card.follow_unfollow')"
:title="requestSent ? $t('user_card.follow_again') : ''" />
@click="followUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="requestSent">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<button
v-else
class="btn btn-default follow-card-follow-button pressed"
:disabled="inProgress"
@click="unfollowUser"
>
<template v-if="inProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else>
{{ $t('user_card.follow_unfollow') }}
</template>
</button>
</template> </template>
</div> </div>
</basic-user-card> </basic-user-card>

View file

@ -1,23 +1,18 @@
import Attachment from '../attachment/attachment.vue' import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight } from 'lodash' import { chunk, last, dropRight, sumBy } from 'lodash'
const Gallery = { const Gallery = {
data: () => ({
width: 500
}),
props: [ props: [
'attachments', 'attachments',
'nsfw', 'nsfw',
'setMedia' 'setMedia'
], ],
data () {
return {
sizes: {}
}
},
components: { Attachment }, components: { Attachment },
mounted () {
this.resize()
window.addEventListener('resize', this.resize)
},
destroyed () {
window.removeEventListener('resize', this.resize)
},
computed: { computed: {
rows () { rows () {
if (!this.attachments) { if (!this.attachments) {
@ -33,21 +28,24 @@ const Gallery = {
} }
return rows return rows
}, },
rowHeight () {
return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
},
useContainFit () { useContainFit () {
return this.$store.getters.mergedConfig.useContainFit return this.$store.getters.mergedConfig.useContainFit
} }
}, },
methods: { methods: {
resize () { onNaturalSizeLoad (id, size) {
// Quick optimization to make resizing not always trigger state change, this.$set(this.sizes, id, size)
// only update attachment size in 10px steps },
const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10 rowStyle (itemsPerRow) {
if (this.width !== width) { return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
this.width = width },
} itemStyle (id, row) {
const total = sumBy(row, item => this.getAspectRatio(item.id))
return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
},
getAspectRatio (id) {
const size = this.sizes[id]
return size ? size.width / size.height : 1
} }
} }
} }

View file

@ -7,17 +7,21 @@
v-for="(row, index) in rows" v-for="(row, index) in rows"
:key="index" :key="index"
class="gallery-row" class="gallery-row"
:style="rowHeight(row.length)" :style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }" :class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
> >
<attachment <div class="gallery-row-inner">
v-for="attachment in row" <attachment
:key="attachment.id" v-for="attachment in row"
:set-media="setMedia" :key="attachment.id"
:nsfw="nsfw" :set-media="setMedia"
:attachment="attachment" :nsfw="nsfw"
:allow-play="false" :attachment="attachment"
/> :allow-play="false"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
/>
</div>
</div> </div>
</div> </div>
</template> </template>
@ -28,15 +32,24 @@
@import '../../_variables.scss'; @import '../../_variables.scss';
.gallery-row { .gallery-row {
height: 200px; position: relative;
height: 0;
width: 100%; width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
flex-grow: 1; flex-grow: 1;
margin-top: 0.5em; margin-top: 0.5em;
.gallery-row-inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
}
// FIXME: specificity problem with this and .attachments.attachment // FIXME: specificity problem with this and .attachments.attachment
// we shouldn't have the need for .image here // we shouldn't have the need for .image here
.attachment.image { .attachment.image {

View file

@ -59,6 +59,8 @@ const LoginForm = {
if (result.error) { if (result.error) {
if (result.error === 'mfa_required') { if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result }) this.requireMFA({ app: app, settings: result })
} else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else { } else {
this.error = result.error this.error = result.error
this.focusOnPasswordInput() this.focusOnPasswordInput()

View file

@ -1,11 +1,13 @@
import StillImage from '../still-image/still-image.vue' import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import fileTypeService from '../../services/file_type/file_type.service.js'
const MediaModal = { const MediaModal = {
components: { components: {
StillImage, StillImage,
VideoAttachment VideoAttachment,
Modal
}, },
computed: { computed: {
showing () { showing () {

View file

@ -1,9 +1,8 @@
<template> <template>
<div <Modal
v-if="showing" v-if="showing"
v-body-scroll-lock="showing" class="media-modal-view"
class="modal-view media-modal-view" @backdropClicked="hide"
@click.prevent="hide"
> >
<img <img
v-if="type === 'image'" v-if="type === 'image'"
@ -33,21 +32,15 @@
> >
<i class="icon-right-open arrow-icon" /> <i class="icon-right-open arrow-icon" />
</button> </button>
</div> </Modal>
</template> </template>
<script src="./media_modal.js"></script> <script src="./media_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .modal-view.media-modal-view {
.media-modal-view {
z-index: 1001; z-index: 1001;
body:not(.scroll-locked) & {
display: none;
}
&:hover { &:hover {
.modal-view-button-arrow { .modal-view-button-arrow {
opacity: 0.75; opacity: 0.75;
@ -114,5 +107,4 @@
} }
} }
} }
</style> </style>

View file

@ -0,0 +1,52 @@
<template>
<div
v-show="isOpen"
v-body-scroll-lock="isOpen"
class="modal-view"
@click.self="$emit('backdropClicked')"
>
<slot />
</div>
</template>
<script>
export default {
props: {
isOpen: {
type: Boolean,
default: true
}
}
}
</script>
<style lang="scss">
.modal-view {
z-index: 1000;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
overflow: auto;
animation-duration: 0.2s;
background-color: rgba(0, 0, 0, 0.5);
animation-name: modal-background-fadein;
body:not(.scroll-locked) & {
opacity: 0;
}
}
@keyframes modal-background-fadein {
from {
background-color: rgba(0, 0, 0, 0);
}
to {
background-color: rgba(0, 0, 0, 0.5);
}
}
</style>

View file

@ -3,9 +3,7 @@
<v-popover <v-popover
trigger="click" trigger="click"
class="moderation-tools-popover" class="moderation-tools-popover"
:container="false"
placement="bottom-end" placement="bottom-end"
:offset="5"
@show="showDropDown = true" @show="showDropDown = true"
@hide="showDropDown = false" @hide="showDropDown = false"
> >

View file

@ -25,6 +25,12 @@ const passwordReset = {
this.$router.push({ name: 'root' }) this.$router.push({ name: 'root' })
} }
}, },
props: {
passwordResetRequested: {
default: false,
type: Boolean
}
},
methods: { methods: {
dismissError () { dismissError () {
this.error = null this.error = null

View file

@ -10,7 +10,10 @@
> >
<div class="container"> <div class="container">
<div v-if="!mailerEnabled"> <div v-if="!mailerEnabled">
<p> <p v-if="passwordResetRequested">
{{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }}
</p>
<p v-else>
{{ $t('password_reset.password_reset_disabled') }} {{ $t('password_reset.password_reset_disabled') }}
</p> </p>
</div> </div>
@ -25,6 +28,12 @@
</div> </div>
</div> </div>
<div v-else> <div v-else>
<p
v-if="passwordResetRequested"
class="password-reset-required error"
>
{{ $t('password_reset.password_reset_required') }}
</p>
<p> <p>
{{ $t('password_reset.instruction') }} {{ $t('password_reset.instruction') }}
</p> </p>
@ -104,6 +113,11 @@
margin: 0.3em 0.0em 1em; margin: 0.3em 0.0em 1em;
} }
.password-reset-required {
background-color: var(--alertError, $fallback--alertError);
padding: 10px 0;
}
.notice-dismissible { .notice-dismissible {
padding-right: 2rem; padding-right: 2rem;
} }

View file

@ -20,7 +20,6 @@
margin: 5px; margin: 5px;
border-color: $fallback--bg; border-color: $fallback--bg;
border-color: var(--bg, $fallback--bg); border-color: var(--bg, $fallback--bg);
z-index: 1;
} }
&[x-placement^="top"] { &[x-placement^="top"] {
@ -31,7 +30,7 @@
border-left-color: transparent !important; border-left-color: transparent !important;
border-right-color: transparent !important; border-right-color: transparent !important;
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
bottom: -5px; bottom: -4px;
left: calc(50% - 5px); left: calc(50% - 5px);
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@ -46,7 +45,7 @@
border-left-color: transparent !important; border-left-color: transparent !important;
border-right-color: transparent !important; border-right-color: transparent !important;
border-top-color: transparent !important; border-top-color: transparent !important;
top: -5px; top: -4px;
left: calc(50% - 5px); left: calc(50% - 5px);
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
@ -61,7 +60,7 @@
border-left-color: transparent !important; border-left-color: transparent !important;
border-top-color: transparent !important; border-top-color: transparent !important;
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
left: -5px; left: -4px;
top: calc(50% - 5px); top: calc(50% - 5px);
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
@ -76,7 +75,7 @@
border-top-color: transparent !important; border-top-color: transparent !important;
border-right-color: transparent !important; border-right-color: transparent !important;
border-bottom-color: transparent !important; border-bottom-color: transparent !important;
right: -5px; right: -4px;
top: calc(50% - 5px); top: calc(50% - 5px);
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;

View file

@ -1,24 +1,43 @@
import PostStatusForm from '../post_status_form/post_status_form.vue' import PostStatusForm from '../post_status_form/post_status_form.vue'
import Modal from '../modal/modal.vue'
import get from 'lodash/get'
const PostStatusModal = { const PostStatusModal = {
components: { components: {
PostStatusForm PostStatusForm,
Modal
},
data () {
return {
resettingForm: false
}
}, },
computed: { computed: {
isLoggedIn () { isLoggedIn () {
return !!this.$store.state.users.currentUser return !!this.$store.state.users.currentUser
}, },
isOpen () { modalActivated () {
return this.isLoggedIn && this.$store.state.postStatus.modalActivated return this.$store.state.postStatus.modalActivated
},
isFormVisible () {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
}, },
params () { params () {
return this.$store.state.postStatus.params || {} return this.$store.state.postStatus.params || {}
} }
}, },
watch: { watch: {
isOpen (val) { params (newVal, oldVal) {
if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) {
this.resettingForm = true
this.$nextTick(() => {
this.resettingForm = false
})
}
},
isFormVisible (val) {
if (val) { if (val) {
this.$nextTick(() => this.$el.querySelector('textarea').focus()) this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
} }
} }
}, },

View file

@ -1,13 +1,11 @@
<template> <template>
<div <Modal
v-if="isOpen" v-if="isLoggedIn && !resettingForm"
class="post-form-modal-view modal-view" :is-open="modalActivated"
@click="closeModal" class="post-form-modal-view"
@backdropClicked="closeModal"
> >
<div <div class="post-form-modal-panel panel">
class="post-form-modal-panel panel"
@click.stop=""
>
<div class="panel-heading"> <div class="panel-heading">
{{ $t('post_status.new_status') }} {{ $t('post_status.new_status') }}
</div> </div>
@ -17,15 +15,13 @@
@posted="closeModal" @posted="closeModal"
/> />
</div> </div>
</div> </Modal>
</template> </template>
<script src="./post_status_modal.js"></script> <script src="./post_status_modal.js"></script>
<style lang="scss"> <style lang="scss">
@import '../../_variables.scss'; .modal-view.post-form-modal-view {
.post-form-modal-view {
align-items: flex-start; align-items: flex-start;
} }

View file

@ -122,6 +122,17 @@
{{ $t("nav.about") }} {{ $t("nav.about") }}
</router-link> </router-link>
</li> </li>
<li
v-if="currentUser && currentUser.role === 'admin'"
@click="toggleDrawer"
>
<a
href="/pleroma/admin/#/login-pleroma"
target="_blank"
>
{{ $t("nav.administration") }}
</a>
</li>
<li <li
v-if="currentUser" v-if="currentUser"
@click="toggleDrawer" @click="toggleDrawer"

View file

@ -10,11 +10,12 @@ import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue' import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue' import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue' import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
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'
import fileType from 'src/services/file_type/file_type.service' import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js' import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, find, unescape, uniqBy } from 'lodash' import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
const Status = { const Status = {
@ -38,8 +39,6 @@ const Status = {
replying: false, replying: false,
unmuted: false, unmuted: false,
userExpanded: false, userExpanded: false,
preview: null,
showPreview: false,
showingTall: this.inConversation && this.focused, showingTall: this.inConversation && this.focused,
showingLongSubject: false, showingLongSubject: false,
error: null, error: null,
@ -293,7 +292,8 @@ const Status = {
Gallery, Gallery,
LinkPreview, LinkPreview,
AvatarList, AvatarList,
Timeago Timeago,
StatusPopover
}, },
methods: { methods: {
visibilityIcon (visibility) { visibilityIcon (visibility) {
@ -368,27 +368,6 @@ const Status = {
this.expandingSubject = true this.expandingSubject = true
} }
}, },
replyEnter (id, event) {
this.showPreview = true
const targetId = id
const statuses = this.$store.state.statuses.allStatuses
if (!this.preview) {
// if we have the status somewhere already
this.preview = find(statuses, { 'id': targetId })
// or if we have to fetch it
if (!this.preview) {
this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => {
this.preview = status
})
}
} else if (this.preview.id !== targetId) {
this.preview = find(statuses, { 'id': targetId })
}
},
replyLeave () {
this.showPreview = false
},
generateUserProfileLink (id, name) { generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
}, },

View file

@ -174,20 +174,26 @@
v-if="isReply" v-if="isReply"
class="reply-to-and-accountname" class="reply-to-and-accountname"
> >
<a <StatusPopover
class="reply-to" v-if="!isPreview"
href="#" :status-id="status.in_reply_to_status_id"
:aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
@mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
@mouseleave.prevent.stop="replyLeave()"
> >
<i <a
v-if="!isPreview" class="reply-to"
class="button-icon icon-reply" href="#"
/> :aria-label="$t('tool_tip.reply')"
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span> @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
</a> >
<i class="button-icon icon-reply" />
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
</a>
</StatusPopover>
<span
v-else
class="reply-to"
>
<span class="reply-to-text">{{ $t('status.reply_to') }}</span>
</span>
<router-link :to="replyProfileLink"> <router-link :to="replyProfileLink">
{{ replyToName }} {{ replyToName }}
</router-link> </router-link>
@ -199,50 +205,25 @@
</span> </span>
</div> </div>
<div <div
v-if="inConversation && !isPreview" v-if="inConversation && !isPreview && replies && replies.length"
class="replies" class="replies"
> >
<span <span class="faint">{{ $t('status.replies_list') }}</span>
v-if="replies && replies.length" <StatusPopover
class="faint" v-for="reply in replies"
>{{ $t('status.replies_list') }}</span> :key="reply.id"
<template v-if="replies"> :status-id="reply.id"
<span >
v-for="reply in replies" <a
:key="reply.id" href="#"
class="reply-link faint" class="reply-link"
> @click.prevent="gotoOriginal(reply.id)"
<a >{{ reply.name }}</a>
href="#" </StatusPopover>
@click.prevent="gotoOriginal(reply.id)"
@mouseenter="replyEnter(reply.id, $event)"
@mouseout="replyLeave()"
>{{ reply.name }}</a>
</span>
</template>
</div> </div>
</div> </div>
</div> </div>
<div
v-if="showPreview"
class="status-preview-container"
>
<status
v-if="preview"
class="status-preview"
:is-preview="true"
:statusoid="preview"
:compact="true"
/>
<div
v-else
class="status-preview status-preview-loading"
>
<i class="icon-spin4 animate-spin" />
</div>
</div>
<div <div
v-if="longSubject" v-if="longSubject"
class="status-content-wrapper" class="status-content-wrapper"
@ -439,18 +420,6 @@ $status-margin: 0.75em;
min-width: 0; min-width: 0;
} }
.status-preview.status-el {
border-style: solid;
border-width: 1px;
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
}
.status-preview-container {
position: relative;
max-width: 100%;
}
.status-pin { .status-pin {
padding: $status-margin $status-margin 0; padding: $status-margin $status-margin 0;
display: flex; display: flex;
@ -458,44 +427,6 @@ $status-margin: 0.75em;
justify-content: flex-end; justify-content: flex-end;
} }
.status-preview {
position: absolute;
max-width: 95%;
display: flex;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-style: solid;
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);
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
.status {
flex: 1;
border: 0;
min-width: 15em;
}
}
.status-preview-loading {
display: block;
min-width: 15em;
padding: 1em;
text-align: center;
border-width: 1px;
border-style: solid;
i {
font-size: 2em;
}
}
.media-left { .media-left {
margin-right: $status-margin; margin-right: $status-margin;
} }
@ -553,11 +484,6 @@ $status-margin: 0.75em;
flex-basis: 100%; flex-basis: 100%;
margin-bottom: 0.5em; margin-bottom: 0.5em;
a {
display: inline-block;
word-break: break-all;
}
small { small {
font-weight: lighter; font-weight: lighter;
} }
@ -568,6 +494,11 @@ $status-margin: 0.75em;
justify-content: space-between; justify-content: space-between;
line-height: 18px; line-height: 18px;
a {
display: inline-block;
word-break: break-all;
}
.name-and-account-name { .name-and-account-name {
display: flex; display: flex;
min-width: 0; min-width: 0;
@ -600,6 +531,7 @@ $status-margin: 0.75em;
} }
.heading-reply-row { .heading-reply-row {
position: relative;
align-content: baseline; align-content: baseline;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
@ -608,11 +540,13 @@ $status-margin: 0.75em;
flex-wrap: wrap; flex-wrap: wrap;
align-items: stretch; align-items: stretch;
a { > .reply-to-and-accountname > a {
max-width: 100%; max-width: 100%;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
display: inline-block;
word-break: break-all;
} }
} }
@ -639,6 +573,8 @@ $status-margin: 0.75em;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em; margin: 0 0.4em 0 0.2em;
color: $fallback--faint;
color: var(--faint, $fallback--faint);
} }
.replies-separator { .replies-separator {
@ -840,6 +776,11 @@ $status-margin: 0.75em;
&.button-icon-active { &.button-icon-active {
color: $fallback--cBlue; color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue); color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
cursor: pointer; cursor: pointer;
} }
} }

View file

@ -0,0 +1,34 @@
import { find } from 'lodash'
const StatusPopover = {
name: 'StatusPopover',
props: [
'statusId'
],
data () {
return {
popperOptions: {
modifiers: {
preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
}
}
}
},
computed: {
status () {
return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
}
},
components: {
Status: () => import('../status/status.vue')
},
methods: {
enter () {
if (!this.status) {
this.$store.dispatch('fetchStatus', this.statusId)
}
}
}
}
export default StatusPopover

View file

@ -0,0 +1,85 @@
<template>
<v-popover
popover-class="status-popover"
placement="top-start"
:popper-options="popperOptions"
@show="enter()"
>
<template slot="popover">
<Status
v-if="status"
:is-preview="true"
:statusoid="status"
:compact="true"
/>
<div
v-else
class="status-preview-loading"
>
<i class="icon-spin4 animate-spin" />
</div>
</template>
<slot />
</v-popover>
</template>
<script src="./status_popover.js" ></script>
<style lang="scss">
@import '../../_variables.scss';
.tooltip.popover.status-popover {
font-size: 1rem;
min-width: 15em;
max-width: 95%;
margin-left: 0.5em;
.popover-inner {
border-color: $fallback--border;
border-color: var(--border, $fallback--border);
border-style: solid;
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);
}
.popover-arrow::before {
position: absolute;
content: '';
left: -7px;
border: solid 7px transparent;
z-index: -1;
}
&[x-placement^="bottom-start"] .popover-arrow::before {
top: -2px;
border-top-width: 0;
border-bottom-color: $fallback--border;
border-bottom-color: var(--border, $fallback--border);
}
&[x-placement^="top-start"] .popover-arrow::before {
bottom: -2px;
border-bottom-width: 0;
border-top-color: $fallback--border;
border-top-color: var(--border, $fallback--border);
}
.status-el.status-el {
border: none;
}
.status-preview-loading {
padding: 1em;
text-align: center;
i {
font-size: 2em;
}
}
}
</style>

View file

@ -3,7 +3,8 @@ const StillImage = {
'src', 'src',
'referrerpolicy', 'referrerpolicy',
'mimetype', 'mimetype',
'imageLoadError' 'imageLoadError',
'imageLoadHandler'
], ],
data () { data () {
return { return {
@ -17,6 +18,7 @@ const StillImage = {
}, },
methods: { methods: {
onLoad () { onLoad () {
this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
const canvas = this.$refs.canvas const canvas = this.$refs.canvas
if (!canvas) return if (!canvas) return
const width = this.$refs.src.naturalWidth const width = this.$refs.src.naturalWidth

View file

@ -1,14 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue' import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue' import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue' import ProgressButton from '../progress_button/progress_button.vue'
import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue' import ModerationTools from '../moderation_tools/moderation_tools.vue'
import AccountActions from '../account_actions/account_actions.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js' import { hex2rgb } from '../../services/color_convert/color_convert.js'
import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
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'
import { mapGetters } from 'vuex' import { mapGetters } from 'vuex'
export default { export default {
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ], props: [
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
],
data () { data () {
return { return {
followRequestInProgress: false, followRequestInProgress: false,
@ -96,30 +99,11 @@ export default {
UserAvatar, UserAvatar,
RemoteFollow, RemoteFollow,
ModerationTools, ModerationTools,
ProgressButton AccountActions,
ProgressButton,
FollowButton
}, },
methods: { methods: {
followUser () {
const store = this.$store
this.followRequestInProgress = true
requestFollow(this.user, store).then(() => {
this.followRequestInProgress = false
})
},
unfollowUser () {
const store = this.$store
this.followRequestInProgress = true
requestUnfollow(this.user, store).then(() => {
this.followRequestInProgress = false
store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
})
},
blockUser () {
this.$store.dispatch('blockUser', this.user.id)
},
unblockUser () {
this.$store.dispatch('unblockUser', this.user.id)
},
muteUser () { muteUser () {
this.$store.dispatch('muteUser', this.user.id) this.$store.dispatch('muteUser', this.user.id)
}, },
@ -147,10 +131,10 @@ export default {
} }
}, },
userProfileLink (user) { userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames) return generateProfileLink(
}, user.id, user.screen_name,
reportUser () { this.$store.state.instance.restrictedNicknames
this.$store.dispatch('openUserReportingModal', this.user.id) )
}, },
zoomAvatar () { zoomAvatar () {
const attachment = { const attachment = {
@ -159,9 +143,6 @@ export default {
} }
this.$store.dispatch('setMedia', [attachment]) this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment) this.$store.dispatch('setCurrent', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
} }
} }
} }

View file

@ -66,8 +66,11 @@
> >
<i class="icon-link-ext usersettings" /> <i class="icon-link-ext usersettings" />
</a> </a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
/>
</div> </div>
<div class="bottom-line"> <div class="bottom-line">
<router-link <router-link
class="user-screen-name" class="user-screen-name"
@ -135,72 +138,27 @@
v-if="loggedIn && isOtherUser" v-if="loggedIn && isOtherUser"
class="user-interactions" class="user-interactions"
> >
<div v-if="!user.following"> <div class="btn-group">
<button <FollowButton :user="user" />
class="btn btn-default btn-block" <template v-if="user.following">
:disabled="followRequestInProgress" <ProgressButton
:title="user.requested ? $t('user_card.follow_again') : ''" v-if="!user.subscribed"
@click="followUser" class="btn btn-default"
> :click="subscribeUser"
<template v-if="followRequestInProgress"> :title="$t('user_card.subscribe')"
{{ $t('user_card.follow_progress') }} >
</template> <i class="icon-bell-alt" />
<template v-else-if="user.requested"> </ProgressButton>
{{ $t('user_card.follow_sent') }} <ProgressButton
</template> v-else
<template v-else> class="btn btn-default pressed"
{{ $t('user_card.follow') }} :click="unsubscribeUser"
</template> :title="$t('user_card.unsubscribe')"
</button> >
<i class="icon-bell-ringing-o" />
</ProgressButton>
</template>
</div> </div>
<div v-else-if="followRequestInProgress">
<button
class="btn btn-default btn-block pressed"
disabled
:title="$t('user_card.follow_unfollow')"
@click="unfollowUser"
>
{{ $t('user_card.follow_progress') }}
</button>
</div>
<div
v-else
class="btn-group"
>
<button
class="btn btn-default pressed"
:title="$t('user_card.follow_unfollow')"
@click="unfollowUser"
>
{{ $t('user_card.following') }}
</button>
<ProgressButton
v-if="!user.subscribed"
class="btn btn-default"
:click="subscribeUser"
:title="$t('user_card.subscribe')"
>
<i class="icon-bell-alt" />
</ProgressButton>
<ProgressButton
v-else
class="btn btn-default pressed"
:click="unsubscribeUser"
:title="$t('user_card.unsubscribe')"
>
<i class="icon-bell-ringing-o" />
</ProgressButton>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</div>
<div> <div>
<button <button
v-if="user.muted" v-if="user.muted"
@ -217,33 +175,6 @@
{{ $t('user_card.mute') }} {{ $t('user_card.mute') }}
</button> </button>
</div> </div>
<div>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block pressed"
@click="unblockUser"
>
{{ $t('user_card.blocked') }}
</button>
<button
v-else
class="btn btn-default btn-block"
@click="blockUser"
>
{{ $t('user_card.block') }}
</button>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="reportUser"
>
{{ $t('user_card.report') }}
</button>
</div>
<ModerationTools <ModerationTools
v-if="loggedIn.role === &quot;admin&quot;" v-if="loggedIn.role === &quot;admin&quot;"
:user="user" :user="user"
@ -587,13 +518,12 @@
position: relative; position: relative;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
justify-content: space-between;
margin-right: -.75em; margin-right: -.75em;
> * { > * {
flex: 1 0 0;
margin: 0 .75em .6em 0; margin: 0 .75em .6em 0;
white-space: nowrap; white-space: nowrap;
min-width: 95px;
} }
button { button {

View file

@ -2,12 +2,14 @@
import Status from '../status/status.vue' import Status from '../status/status.vue'
import List from '../list/list.vue' import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue' import Checkbox from '../checkbox/checkbox.vue'
import Modal from '../modal/modal.vue'
const UserReportingModal = { const UserReportingModal = {
components: { components: {
Status, Status,
List, List,
Checkbox Checkbox,
Modal
}, },
data () { data () {
return { return {

View file

@ -1,13 +1,9 @@
<template> <template>
<div <Modal
v-if="isOpen" v-if="isOpen"
class="modal-view" @backdropClicked="closeModal"
@click="closeModal"
> >
<div <div class="user-reporting-panel panel">
class="user-reporting-panel panel"
@click.stop=""
>
<div class="panel-heading"> <div class="panel-heading">
<div class="title"> <div class="title">
{{ $t('user_reporting.title', [user.screen_name]) }} {{ $t('user_reporting.title', [user.screen_name]) }}
@ -69,7 +65,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </Modal>
</template> </template>
<script src="./user_reporting_modal.js"></script> <script src="./user_reporting_modal.js"></script>

View file

@ -2,42 +2,49 @@ import * as bodyScrollLock from 'body-scroll-lock'
let previousNavPaddingRight let previousNavPaddingRight
let previousAppBgWrapperRight let previousAppBgWrapperRight
const lockerEls = new Set([])
const disableBodyScroll = (el) => { const disableBodyScroll = (el) => {
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
bodyScrollLock.disableBodyScroll(el, { bodyScrollLock.disableBodyScroll(el, {
reserveScrollBarGap: true reserveScrollBarGap: true
}) })
lockerEls.add(el)
setTimeout(() => { setTimeout(() => {
// If previousNavPaddingRight is already set, don't set it again. if (lockerEls.size <= 1) {
if (previousNavPaddingRight === undefined) { // If previousNavPaddingRight is already set, don't set it again.
const navEl = document.getElementById('nav') if (previousNavPaddingRight === undefined) {
previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right') const navEl = document.getElementById('nav')
navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px` previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right')
navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
}
// If previousAppBgWrapeprRight is already set, don't set it again.
if (previousAppBgWrapperRight === undefined) {
const appBgWrapperEl = document.getElementById('app_bg_wrapper')
previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
}
document.body.classList.add('scroll-locked')
} }
// If previousAppBgWrapeprRight is already set, don't set it again.
if (previousAppBgWrapperRight === undefined) {
const appBgWrapperEl = document.getElementById('app_bg_wrapper')
previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
}
document.body.classList.add('scroll-locked')
}) })
} }
const enableBodyScroll = (el) => { const enableBodyScroll = (el) => {
lockerEls.delete(el)
setTimeout(() => { setTimeout(() => {
if (previousNavPaddingRight !== undefined) { if (lockerEls.size === 0) {
document.getElementById('nav').style.paddingRight = previousNavPaddingRight if (previousNavPaddingRight !== undefined) {
// Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again. document.getElementById('nav').style.paddingRight = previousNavPaddingRight
previousNavPaddingRight = undefined // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
previousNavPaddingRight = undefined
}
if (previousAppBgWrapperRight !== undefined) {
document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
// Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
previousAppBgWrapperRight = undefined
}
document.body.classList.remove('scroll-locked')
} }
if (previousAppBgWrapperRight !== undefined) {
document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
// Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
previousAppBgWrapperRight = undefined
}
document.body.classList.remove('scroll-locked')
}) })
bodyScrollLock.enableBodyScroll(el) bodyScrollLock.enableBodyScroll(el)
} }

View file

@ -68,6 +68,7 @@
}, },
"nav": { "nav": {
"about": "About", "about": "About",
"administration": "Administration",
"back": "Back", "back": "Back",
"chat": "Local Chat", "chat": "Local Chat",
"friend_requests": "Follow Requests", "friend_requests": "Follow Requests",
@ -554,6 +555,8 @@
"unmute": "Unmute", "unmute": "Unmute",
"unmute_progress": "Unmuting...", "unmute_progress": "Unmuting...",
"mute_progress": "Muting...", "mute_progress": "Muting...",
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"admin_menu": { "admin_menu": {
"moderation": "Moderation", "moderation": "Moderation",
"grant_admin": "Grant Admin", "grant_admin": "Grant Admin",
@ -629,6 +632,8 @@
"return_home": "Return to the home page", "return_home": "Return to the home page",
"not_found": "We couldn't find that email or username.", "not_found": "We couldn't find that email or username.",
"too_many_requests": "You have reached the limit of attempts, try again later.", "too_many_requests": "You have reached the limit of attempts, try again later.",
"password_reset_disabled": "Password reset is disabled. Please contact your instance administrator." "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
"password_reset_required": "You must reset your password to log in.",
"password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
} }
} }

View file

@ -45,8 +45,8 @@
"error": "Se ha producido un error al importar el archivo." "error": "Se ha producido un error al importar el archivo."
}, },
"login": { "login": {
"login": "Identificación", "login": "Identificarse",
"description": "Identificación con OAuth", "description": "Identificarse con OAuth",
"logout": "Cerrar sesión", "logout": "Cerrar sesión",
"password": "Contraseña", "password": "Contraseña",
"placeholder": "p.ej. lain", "placeholder": "p.ej. lain",
@ -68,6 +68,7 @@
}, },
"nav": { "nav": {
"about": "Acerca de", "about": "Acerca de",
"administration": "Administración",
"back": "Volver", "back": "Volver",
"chat": "Chat Local", "chat": "Chat Local",
"friend_requests": "Solicitudes de seguimiento", "friend_requests": "Solicitudes de seguimiento",
@ -106,6 +107,15 @@
"expired": "La encuesta terminó hace {0}", "expired": "La encuesta terminó hace {0}",
"not_enough_options": "Muy pocas opciones únicas en la encuesta" "not_enough_options": "Muy pocas opciones únicas en la encuesta"
}, },
"emoji": {
"stickers": "Pegatinas",
"emoji": "Emoji",
"keep_open": "Mantener el selector abierto",
"search_emoji": "Buscar un emoji",
"add_emoji": "Insertar un emoji",
"custom": "Emojis personalizados",
"unicode": "Emojis unicode"
},
"stickers": { "stickers": {
"add_sticker": "Añadir Pegatina" "add_sticker": "Añadir Pegatina"
}, },
@ -222,7 +232,9 @@
"data_import_export_tab": "Importar / Exportar Datos", "data_import_export_tab": "Importar / Exportar Datos",
"default_vis": "Alcance de visibilidad por defecto", "default_vis": "Alcance de visibilidad por defecto",
"delete_account": "Eliminar la cuenta", "delete_account": "Eliminar la cuenta",
"discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios",
"delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.", "delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.",
"pad_emoji": "Rellenar con espacios al agregar emojis desde el selector",
"delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.", "delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.",
"delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.", "delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.",
"avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.", "avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.",
@ -277,6 +289,8 @@
"no_mutes": "No hay usuarios sinlenciados", "no_mutes": "No hay usuarios sinlenciados",
"hide_follows_description": "No mostrar a quién sigo", "hide_follows_description": "No mostrar a quién sigo",
"hide_followers_description": "No mostrar quién me sigue", "hide_followers_description": "No mostrar quién me sigue",
"hide_follows_count_description": "No mostrar el número de cuentas que sigo",
"hide_followers_count_description": "No mostrar el número de cuentas que me siguen",
"show_admin_badge": "Mostrar la insignia de Administrador en mi perfil", "show_admin_badge": "Mostrar la insignia de Administrador en mi perfil",
"show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil", "show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW", "nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
@ -529,6 +543,7 @@
"follows_you": "¡Te sigue!", "follows_you": "¡Te sigue!",
"its_you": "¡Eres tú!", "its_you": "¡Eres tú!",
"media": "Media", "media": "Media",
"mention": "Mencionar",
"mute": "Silenciar", "mute": "Silenciar",
"muted": "Silenciado", "muted": "Silenciado",
"per_day": "por día", "per_day": "por día",

View file

@ -68,6 +68,7 @@
}, },
"nav": { "nav": {
"about": "Honi buruz", "about": "Honi buruz",
"administration": "Administrazioa",
"back": "Atzera", "back": "Atzera",
"chat": "Txat lokala", "chat": "Txat lokala",
"friend_requests": "Jarraitzeko eskaerak", "friend_requests": "Jarraitzeko eskaerak",
@ -106,6 +107,15 @@
"expired": "Inkesta {0} bukatu zen", "expired": "Inkesta {0} bukatu zen",
"not_enough_options": "Aukera gutxiegi inkestan" "not_enough_options": "Aukera gutxiegi inkestan"
}, },
"emoji": {
"stickers": "Pegatinak",
"emoji": "Emoji",
"keep_open": "Mantendu hautatzailea zabalik",
"search_emoji": "Bilatu emoji bat",
"add_emoji": "Emoji bat gehitu",
"custom": "Ohiko emojiak",
"unicode": "Unicode emojiak"
},
"stickers": { "stickers": {
"add_sticker": "Pegatina gehitu" "add_sticker": "Pegatina gehitu"
}, },
@ -199,12 +209,12 @@
"avatarRadius": "Avatarrak", "avatarRadius": "Avatarrak",
"background": "Atzeko planoa", "background": "Atzeko planoa",
"bio": "Biografia", "bio": "Biografia",
"block_export": "Bloke esportatzea", "block_export": "Blokeatu dituzunak esportatu",
"block_export_button": "Esportatu zure blokeak csv fitxategi batera", "block_export_button": "Esportatu blokeatutakoak csv fitxategi batera",
"block_import": "Bloke inportazioa", "block_import": "Blokeatu dituzunak inportatu",
"block_import_error": "Errorea blokeak inportatzen", "block_import_error": "Errorea blokeatutakoak inportatzen",
"blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.", "blocks_imported": "Blokeatutakoak inportaturik! Hauek prozesatzeak denbora hartuko du.",
"blocks_tab": "Blokeak", "blocks_tab": "Blokeatutakoak",
"btnRadius": "Botoiak", "btnRadius": "Botoiak",
"cBlue": "Urdina (erantzun, jarraitu)", "cBlue": "Urdina (erantzun, jarraitu)",
"cGreen": "Berdea (Bertxiotu)", "cGreen": "Berdea (Bertxiotu)",
@ -222,7 +232,9 @@
"data_import_export_tab": "Datuak Inportatu / Esportatu", "data_import_export_tab": "Datuak Inportatu / Esportatu",
"default_vis": "Lehenetsitako ikusgaitasunak", "default_vis": "Lehenetsitako ikusgaitasunak",
"delete_account": "Ezabatu kontua", "delete_account": "Ezabatu kontua",
"discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea",
"delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak", "delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak",
"pad_emoji": "Zuriuneak gehitu emoji bat aukeratzen denean",
"delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.", "delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.",
"delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.", "delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.",
"avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.", "avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.",
@ -254,7 +266,7 @@
"instance_default": "(lehenetsia: {value})", "instance_default": "(lehenetsia: {value})",
"instance_default_simple": "(lehenetsia)", "instance_default_simple": "(lehenetsia)",
"interface": "Interfazea", "interface": "Interfazea",
"interfaceLanguage": "Interfaze hizkuntza", "interfaceLanguage": "Interfazearen hizkuntza",
"invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.", "invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.",
"limited_availability": "Ez dago erabilgarri zure nabigatzailean", "limited_availability": "Ez dago erabilgarri zure nabigatzailean",
"links": "Estekak", "links": "Estekak",
@ -277,6 +289,8 @@
"no_mutes": "Ez daude erabiltzaile mututuak", "no_mutes": "Ez daude erabiltzaile mututuak",
"hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen", "hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen",
"hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen", "hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen",
"hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua",
"hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua",
"show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan", "show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan",
"show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan", "show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan",
"nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko", "nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko",
@ -449,7 +463,7 @@
}, },
"version": { "version": {
"title": "Bertsioa", "title": "Bertsioa",
"backend_version": "Backend Bertsio", "backend_version": "Backend Bertsioa",
"frontend_version": "Frontend Bertsioa" "frontend_version": "Frontend Bertsioa"
} }
}, },
@ -529,6 +543,7 @@
"follows_you": "Jarraitzen dizu!", "follows_you": "Jarraitzen dizu!",
"its_you": "Zu zara!", "its_you": "Zu zara!",
"media": "Multimedia", "media": "Multimedia",
"mention": "Aipatu",
"mute": "Isilarazi", "mute": "Isilarazi",
"muted": "Isilduta", "muted": "Isilduta",
"per_day": "eguneko", "per_day": "eguneko",

View file

@ -106,6 +106,15 @@
"expired": "いれふだは {0} まえに、おわりました", "expired": "いれふだは {0} まえに、おわりました",
"not_enough_options": "ユニークなオプションが、たりません" "not_enough_options": "ユニークなオプションが、たりません"
}, },
"emoji": {
"stickers": "ステッカー",
"emoji": "えもじ",
"keep_open": "ピッカーをあけたままにする",
"search_emoji": "えもじをさがす",
"add_emoji": "えもじをうちこむ",
"custom": "カスタムえもじ",
"unicode": "ユニコードえもじ"
},
"stickers": { "stickers": {
"add_sticker": "ステッカーをふやす" "add_sticker": "ステッカーをふやす"
}, },
@ -224,10 +233,11 @@
"default_vis": "デフォルトのこうかいはんい", "default_vis": "デフォルトのこうかいはんい",
"delete_account": "アカウントをけす", "delete_account": "アカウントをけす",
"delete_account_description": "あなたのアカウントとメッセージが、きえます。", "delete_account_description": "あなたのアカウントとメッセージが、きえます。",
"delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。", "delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのアドミニストレーターに、おといあわせください。",
"delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。", "delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。",
"discoverable": "けんさくなどのサービスで、このアカウントをみつけてもよい", "discoverable": "けんさくなどのサービスで、このアカウントをみつけてもよい",
"avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。", "avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。",
"pad_emoji": "えもじをピッカーでえらんだとき、えもじのまわりにスペースをいれる",
"export_theme": "セーブ", "export_theme": "セーブ",
"filtering": "フィルタリング", "filtering": "フィルタリング",
"filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。", "filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。",
@ -280,6 +290,8 @@
"no_mutes": "ミュートしていません", "no_mutes": "ミュートしていません",
"hide_follows_description": "フォローしているひとをみせない", "hide_follows_description": "フォローしているひとをみせない",
"hide_followers_description": "フォロワーをみせない", "hide_followers_description": "フォロワーをみせない",
"hide_follows_count_description": "フォローしているひとのかずをみせない",
"hide_followers_count_description": "フォロワーのかずをみせない",
"show_admin_badge": "アドミンのしるしをみせる", "show_admin_badge": "アドミンのしるしをみせる",
"show_moderator_badge": "モデレーターのしるしをみせる", "show_moderator_badge": "モデレーターのしるしをみせる",
"nsfw_clickthrough": "NSFWなファイルをかくす", "nsfw_clickthrough": "NSFWなファイルをかくす",
@ -532,6 +544,7 @@
"follows_you": "フォローされました!", "follows_you": "フォローされました!",
"its_you": "これはあなたです!", "its_you": "これはあなたです!",
"media": "メディア", "media": "メディア",
"mention": "メンション",
"mute": "ミュート", "mute": "ミュート",
"muted": "ミュートしています!", "muted": "ミュートしています!",
"per_day": "/日", "per_day": "/日",
@ -611,5 +624,16 @@
"person_talking": "{count} にんが、はなしています", "person_talking": "{count} にんが、はなしています",
"people_talking": "{count} にんが、はなしています", "people_talking": "{count} にんが、はなしています",
"no_results": "みつかりませんでした" "no_results": "みつかりませんでした"
},
"password_reset": {
"forgot_password": "パスワードを、わすれましたか?",
"password_reset": "パスワードリセット",
"instruction": "あなたのメールアドレスかユーザーめいをいれてください。パスワードをリセットするためのリンクをおくります。",
"placeholder": "あなたのメールアドレスかユーザーめい",
"check_email": "パスワードをリセットするためのリンクがかかれたメールが、とどいているかどうか、みてください。",
"return_home": "ホームページにもどる",
"not_found": "そのメールアドレスまたはユーザーめいを、みつけることができませんでした。",
"too_many_requests": "パスワードリセットを、ためすことが、おおすぎます。しばらくしてから、ためしてください。",
"password_reset_disabled": "このインスタンスでは、パスワードリセットは、できません。インスタンスのアドミニストレーターに、おといあわせください。"
} }
} }

View file

@ -106,6 +106,15 @@
"expired": "投票は {0} 前に終了しました", "expired": "投票は {0} 前に終了しました",
"not_enough_options": "相異なる選択肢が不足しています" "not_enough_options": "相異なる選択肢が不足しています"
}, },
"emoji": {
"stickers": "ステッカー",
"emoji": "絵文字",
"keep_open": "ピッカーを開いたままにする",
"search_emoji": "絵文字を検索",
"add_emoji": "絵文字を挿入",
"custom": "カスタム絵文字",
"unicode": "Unicode絵文字"
},
"stickers": { "stickers": {
"add_sticker": "ステッカーを追加" "add_sticker": "ステッカーを追加"
}, },
@ -228,6 +237,7 @@
"delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。", "delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。",
"discoverable": "検索などのサービスでこのアカウントを見つけることを許可する", "discoverable": "検索などのサービスでこのアカウントを見つけることを許可する",
"avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。", "avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。",
"pad_emoji": "ピッカーから絵文字を挿入するとき、絵文字の両側にスペースを入れる",
"export_theme": "保存", "export_theme": "保存",
"filtering": "フィルタリング", "filtering": "フィルタリング",
"filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください。", "filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください。",
@ -280,6 +290,8 @@
"no_mutes": "ミュートはありません", "no_mutes": "ミュートはありません",
"hide_follows_description": "フォローしている人を見せない", "hide_follows_description": "フォローしている人を見せない",
"hide_followers_description": "フォロワーを見せない", "hide_followers_description": "フォロワーを見せない",
"hide_follows_count_description": "フォローしている人の数を見せない",
"hide_followers_count_description": "フォロワーの数を見せない",
"show_admin_badge": "管理者のバッジを見せる", "show_admin_badge": "管理者のバッジを見せる",
"show_moderator_badge": "モデレーターのバッジを見せる", "show_moderator_badge": "モデレーターのバッジを見せる",
"nsfw_clickthrough": "NSFWなファイルを隠す", "nsfw_clickthrough": "NSFWなファイルを隠す",
@ -532,6 +544,7 @@
"follows_you": "フォローされました!", "follows_you": "フォローされました!",
"its_you": "これはあなたです!", "its_you": "これはあなたです!",
"media": "メディア", "media": "メディア",
"mention": "メンション",
"mute": "ミュート", "mute": "ミュート",
"muted": "ミュートしています!", "muted": "ミュートしています!",
"per_day": "/日", "per_day": "/日",
@ -611,5 +624,16 @@
"person_talking": "{count} 人が話しています", "person_talking": "{count} 人が話しています",
"people_talking": "{count} 人が話しています", "people_talking": "{count} 人が話しています",
"no_results": "見つかりませんでした" "no_results": "見つかりませんでした"
},
"password_reset": {
"forgot_password": "パスワードを忘れましたか?",
"password_reset": "パスワードリセット",
"instruction": "メールアドレスまたはユーザー名を入力してください。パスワードをリセットするためのリンクを送信します。",
"placeholder": "メールアドレスまたはユーザー名",
"check_email": "パスワードをリセットするためのリンクが記載されたメールが届いているか確認してください。",
"return_home": "ホームページに戻る",
"not_found": "メールアドレスまたはユーザー名が見つかりませんでした。",
"too_many_requests": "試行回数の制限に達しました。しばらく時間を置いてから再試行してください。",
"password_reset_disabled": "このインスタンスではパスワードリセットは無効になっています。インスタンスの管理者に連絡してください。"
} }
} }

View file

@ -41,7 +41,13 @@ Vue.use(VueChatScroll)
Vue.use(VueClickOutside) Vue.use(VueClickOutside)
Vue.use(PortalVue) Vue.use(PortalVue)
Vue.use(VBodyScrollLock) Vue.use(VBodyScrollLock)
Vue.use(VTooltip) Vue.use(VTooltip, {
popover: {
defaultTrigger: 'hover click',
defaultContainer: false,
defaultOffset: 5
}
})
const i18n = new VueI18n({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary

View file

@ -537,6 +537,10 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) { setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value }) commit('setNotificationsSilence', { value })
}, },
fetchStatus ({ rootState, dispatch }, id) {
rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
deleteStatus ({ rootState, commit }, status) { deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status }) commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials }) apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })

View file

@ -60,6 +60,18 @@ const unmuteUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship])) .then((relationship) => store.commit('updateUserRelationship', [relationship]))
} }
const hideReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
.then((relationship) => {
store.commit('updateUserRelationship', [relationship])
})
}
const showReblogs = (store, userId) => {
return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
export const mutations = { export const mutations = {
setMuted (state, { user: { id }, muted }) { setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id] const user = state.usersObject[id]
@ -135,6 +147,7 @@ export const mutations = {
user.muted = relationship.muting user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking user.statusnet_blocking = relationship.blocking
user.subscribed = relationship.subscribing user.subscribed = relationship.subscribing
user.showing_reblogs = relationship.showing_reblogs
} }
}) })
}, },
@ -272,6 +285,12 @@ const users = {
unmuteUser (store, id) { unmuteUser (store, id) {
return unmuteUser(store, id) return unmuteUser(store, id)
}, },
hideReblogs (store, id) {
return hideReblogs(store, id)
},
showReblogs (store, id) {
return showReblogs(store, id)
},
muteUsers (store, ids = []) { muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id))) return Promise.all(ids.map(id => muteUser(store, id)))
}, },

View file

@ -219,10 +219,16 @@ const authHeaders = (accessToken) => {
} }
} }
const followUser = ({ id, credentials }) => { const followUser = ({ id, credentials, ...options }) => {
let url = MASTODON_FOLLOW_URL(id) let url = MASTODON_FOLLOW_URL(id)
const form = {}
if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
return fetch(url, { return fetch(url, {
headers: authHeaders(credentials), body: JSON.stringify(form),
headers: {
...authHeaders(credentials),
'Content-Type': 'application/json'
},
method: 'POST' method: 'POST'
}).then((data) => data.json()) }).then((data) => data.json())
} }

View file

@ -31,8 +31,8 @@ const backendInteractorService = credentials => {
return apiService.fetchUserRelationship({ id, credentials }) return apiService.fetchUserRelationship({ id, credentials })
} }
const followUser = (id) => { const followUser = ({ id, reblogs }) => {
return apiService.followUser({ credentials, id }) return apiService.followUser({ credentials, id, reblogs })
} }
const unfollowUser = (id) => { const unfollowUser = (id) => {

View file

@ -69,6 +69,7 @@ export const parseUser = (data) => {
output.following = relationship.following output.following = relationship.following
output.statusnet_blocking = relationship.blocking output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting output.muted = relationship.muting
output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing output.subscribed = relationship.subscribing
} }
@ -196,9 +197,11 @@ export const parseAttachment = (data) => {
return output return output
} }
export const addEmojis = (string, emojis) => { export const addEmojis = (string, emojis) => {
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
return emojis.reduce((acc, emoji) => { return emojis.reduce((acc, emoji) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace( return acc.replace(
new RegExp(`:${emoji.shortcode}:`, 'g'), new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />` `<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />`
) )
}, string) }, string)

View file

@ -14,7 +14,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
}) })
export const requestFollow = (user, store) => new Promise((resolve, reject) => { export const requestFollow = (user, store) => new Promise((resolve, reject) => {
store.state.api.backendInteractor.followUser(user.id) store.state.api.backendInteractor.followUser({ id: user.id })
.then((updated) => { .then((updated) => {
store.commit('updateUserRelationship', [updated]) store.commit('updateUserRelationship', [updated])

0
static/font/LICENSE.txt Normal file → Executable file
View file

0
static/font/README.txt Normal file → Executable file
View file

6
static/font/config.json Normal file → Executable file
View file

@ -297,6 +297,12 @@
"css": "zoom-in", "css": "zoom-in",
"code": 59420, "code": 59420,
"src": "fontawesome" "src": "fontawesome"
},
{
"uid": "0bda4bc779d4c32623dec2e43bd67ee8",
"css": "gauge",
"code": 61668,
"src": "fontawesome"
} }
] ]
} }

0
static/font/css/animation.css Normal file → Executable file
View file

1
static/font/css/fontello-codes.css vendored Normal file → Executable file
View file

@ -34,6 +34,7 @@
.icon-link-ext-alt:before { content: '\f08f'; } /* '' */ .icon-link-ext-alt:before { content: '\f08f'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-menu:before { content: '\f0c9'; } /* '' */
.icon-mail-alt:before { content: '\f0e0'; } /* '' */ .icon-mail-alt:before { content: '\f0e0'; } /* '' */
.icon-gauge:before { content: '\f0e4'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */ .icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-bell-alt:before { content: '\f0f3'; } /* '' */ .icon-bell-alt:before { content: '\f0f3'; } /* '' */
.icon-plus-squared:before { content: '\f0fe'; } /* '' */ .icon-plus-squared:before { content: '\f0fe'; } /* '' */

14
static/font/css/fontello-embedded.css vendored Normal file → Executable file

File diff suppressed because one or more lines are too long

1
static/font/css/fontello-ie7-codes.css vendored Normal file → Executable file
View file

@ -34,6 +34,7 @@
.icon-link-ext-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08f;&nbsp;'); } .icon-link-ext-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08f;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); } .icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); } .icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); }
.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); } .icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f3;&nbsp;'); } .icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f3;&nbsp;'); }
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); } .icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); }

1
static/font/css/fontello-ie7.css vendored Normal file → Executable file
View file

@ -45,6 +45,7 @@
.icon-link-ext-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08f;&nbsp;'); } .icon-link-ext-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08f;&nbsp;'); }
.icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); } .icon-menu { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0c9;&nbsp;'); }
.icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); } .icon-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); }
.icon-gauge { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e4;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); } .icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
.icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f3;&nbsp;'); } .icon-bell-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0f3;&nbsp;'); }
.icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); } .icon-plus-squared { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0fe;&nbsp;'); }

16
static/font/css/fontello.css vendored Normal file → Executable file
View file

@ -1,11 +1,11 @@
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.eot?94788965'); src: url('../font/fontello.eot?70867224');
src: url('../font/fontello.eot?94788965#iefix') format('embedded-opentype'), src: url('../font/fontello.eot?70867224#iefix') format('embedded-opentype'),
url('../font/fontello.woff2?94788965') format('woff2'), url('../font/fontello.woff2?70867224') format('woff2'),
url('../font/fontello.woff?94788965') format('woff'), url('../font/fontello.woff?70867224') format('woff'),
url('../font/fontello.ttf?94788965') format('truetype'), url('../font/fontello.ttf?70867224') format('truetype'),
url('../font/fontello.svg?94788965#fontello') format('svg'); url('../font/fontello.svg?70867224#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -15,7 +15,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) { @media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('../font/fontello.svg?94788965#fontello') format('svg'); src: url('../font/fontello.svg?70867224#fontello') format('svg');
} }
} }
*/ */
@ -83,12 +83,14 @@
.icon-pin:before { content: '\e819'; } /* '' */ .icon-pin:before { content: '\e819'; } /* '' */
.icon-wrench:before { content: '\e81a'; } /* '' */ .icon-wrench:before { content: '\e81a'; } /* '' */
.icon-chart-bar:before { content: '\e81b'; } /* '' */ .icon-chart-bar:before { content: '\e81b'; } /* '' */
.icon-zoom-in:before { content: '\e81c'; } /* '' */
.icon-spin3:before { content: '\e832'; } /* '' */ .icon-spin3:before { content: '\e832'; } /* '' */
.icon-spin4:before { content: '\e834'; } /* '' */ .icon-spin4:before { content: '\e834'; } /* '' */
.icon-link-ext:before { content: '\f08e'; } /* '' */ .icon-link-ext:before { content: '\f08e'; } /* '' */
.icon-link-ext-alt:before { content: '\f08f'; } /* '' */ .icon-link-ext-alt:before { content: '\f08f'; } /* '' */
.icon-menu:before { content: '\f0c9'; } /* '' */ .icon-menu:before { content: '\f0c9'; } /* '' */
.icon-mail-alt:before { content: '\f0e0'; } /* '' */ .icon-mail-alt:before { content: '\f0e0'; } /* '' */
.icon-gauge:before { content: '\f0e4'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */ .icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-bell-alt:before { content: '\f0f3'; } /* '' */ .icon-bell-alt:before { content: '\f0f3'; } /* '' */
.icon-plus-squared:before { content: '\f0fe'; } /* '' */ .icon-plus-squared:before { content: '\f0fe'; } /* '' */

26
static/font/demo.html Normal file → Executable file
View file

@ -229,11 +229,11 @@ body {
} }
@font-face { @font-face {
font-family: 'fontello'; font-family: 'fontello';
src: url('./font/fontello.eot?31206390'); src: url('./font/fontello.eot?56851497');
src: url('./font/fontello.eot?31206390#iefix') format('embedded-opentype'), src: url('./font/fontello.eot?56851497#iefix') format('embedded-opentype'),
url('./font/fontello.woff?31206390') format('woff'), url('./font/fontello.woff?56851497') format('woff'),
url('./font/fontello.ttf?31206390') format('truetype'), url('./font/fontello.ttf?56851497') format('truetype'),
url('./font/fontello.svg?31206390#fontello') format('svg'); url('./font/fontello.svg?56851497#fontello') format('svg');
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
} }
@ -340,30 +340,32 @@ body {
<div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-chart-bar">&#xe81b;</i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe81b</span></div> <div class="the-icons span3" title="Code: 0xe81b"><i class="demo-icon icon-chart-bar">&#xe81b;</i> <span class="i-name">icon-chart-bar</span><span class="i-code">0xe81b</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xe81c"><i class="demo-icon icon-zoom-in">&#xe81c;</i> <span class="i-name">icon-zoom-in</span><span class="i-code">0xe81c</span></div>
<div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div> <div class="the-icons span3" title="Code: 0xe832"><i class="demo-icon icon-spin3 animate-spin">&#xe832;</i> <span class="i-name">icon-spin3</span><span class="i-code">0xe832</span></div>
<div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div> <div class="the-icons span3" title="Code: 0xe834"><i class="demo-icon icon-spin4 animate-spin">&#xe834;</i> <span class="i-name">icon-spin4</span><span class="i-code">0xe834</span></div>
<div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div> <div class="the-icons span3" title="Code: 0xf08e"><i class="demo-icon icon-link-ext">&#xf08e;</i> <span class="i-name">icon-link-ext</span><span class="i-code">0xf08e</span></div>
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf08f"><i class="demo-icon icon-link-ext-alt">&#xf08f;</i> <span class="i-name">icon-link-ext-alt</span><span class="i-code">0xf08f</span></div>
<div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div> <div class="the-icons span3" title="Code: 0xf0c9"><i class="demo-icon icon-menu">&#xf0c9;</i> <span class="i-name">icon-menu</span><span class="i-code">0xf0c9</span></div>
<div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div> <div class="the-icons span3" title="Code: 0xf0e0"><i class="demo-icon icon-mail-alt">&#xf0e0;</i> <span class="i-name">icon-mail-alt</span><span class="i-code">0xf0e0</span></div>
<div class="the-icons span3" title="Code: 0xf0e4"><i class="demo-icon icon-gauge">&#xf0e4;</i> <span class="i-name">icon-gauge</span><span class="i-code">0xf0e4</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div> <div class="the-icons span3" title="Code: 0xf0e5"><i class="demo-icon icon-comment-empty">&#xf0e5;</i> <span class="i-name">icon-comment-empty</span><span class="i-code">0xf0e5</span></div>
<div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt">&#xf0f3;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div> <div class="the-icons span3" title="Code: 0xf0f3"><i class="demo-icon icon-bell-alt">&#xf0f3;</i> <span class="i-name">icon-bell-alt</span><span class="i-code">0xf0f3</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div> <div class="the-icons span3" title="Code: 0xf0fe"><i class="demo-icon icon-plus-squared">&#xf0fe;</i> <span class="i-name">icon-plus-squared</span><span class="i-code">0xf0fe</span></div>
<div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div> <div class="the-icons span3" title="Code: 0xf112"><i class="demo-icon icon-reply">&#xf112;</i> <span class="i-name">icon-reply</span><span class="i-code">0xf112</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf118"><i class="demo-icon icon-smile">&#xf118;</i> <span class="i-name">icon-smile</span><span class="i-code">0xf118</span></div> <div class="the-icons span3" title="Code: 0xf118"><i class="demo-icon icon-smile">&#xf118;</i> <span class="i-name">icon-smile</span><span class="i-code">0xf118</span></div>
<div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div> <div class="the-icons span3" title="Code: 0xf13e"><i class="demo-icon icon-lock-open-alt">&#xf13e;</i> <span class="i-name">icon-lock-open-alt</span><span class="i-code">0xf13e</span></div>
</div>
<div class="row">
<div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis">&#xf141;</i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div> <div class="the-icons span3" title="Code: 0xf141"><i class="demo-icon icon-ellipsis">&#xf141;</i> <span class="i-name">icon-ellipsis</span><span class="i-code">0xf141</span></div>
<div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div> <div class="the-icons span3" title="Code: 0xf144"><i class="demo-icon icon-play-circled">&#xf144;</i> <span class="i-name">icon-play-circled</span><span class="i-code">0xf144</span></div>
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
</div> </div>
<div class="row"> <div class="row">
<div class="the-icons span3" title="Code: 0xf164"><i class="demo-icon icon-thumbs-up-alt">&#xf164;</i> <span class="i-name">icon-thumbs-up-alt</span><span class="i-code">0xf164</span></div>
<div class="the-icons span3" title="Code: 0xf1e5"><i class="demo-icon icon-binoculars">&#xf1e5;</i> <span class="i-name">icon-binoculars</span><span class="i-code">0xf1e5</span></div>
<div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div> <div class="the-icons span3" title="Code: 0xf234"><i class="demo-icon icon-user-plus">&#xf234;</i> <span class="i-name">icon-user-plus</span><span class="i-code">0xf234</span></div>
</div> </div>
</div> </div>

BIN
static/font/font/fontello.eot Normal file → Executable file

Binary file not shown.

2
static/font/font/fontello.svg Normal file → Executable file
View file

@ -76,6 +76,8 @@
<glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 461v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-27 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-32-15-28-5h-2q-12 0-27 5t-32 15-30 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 63-26t26-63z" horiz-adv-x="1000" /> <glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 461v-443q0-37-26-63t-63-27h-822q-36 0-63 27t-26 63v443q25-27 56-49 202-137 278-192 32-24 51-37t53-27 61-13h2q28 0 61 13t53 27 51 37q95 68 278 192 32 22 56 49z m0 164q0-44-27-84t-68-69q-210-146-262-181-5-4-23-17t-30-22-29-18-32-15-28-5h-2q-12 0-27 5t-32 15-30 18-30 22-23 17q-51 35-147 101t-114 80q-35 23-65 64t-31 77q0 43 23 72t66 29h822q36 0 63-26t26-63z" horiz-adv-x="1000" />
<glyph glyph-name="gauge" unicode="&#xf0e4;" d="M214 214q0 30-21 51t-50 21-51-21-21-51 21-50 51-21 50 21 21 50z m107 250q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m239-268l57 213q3 14-5 27t-21 16-27-3-17-22l-56-213q-33-3-60-25t-35-55q-11-43 11-81t66-50 81 11 50 66q9 33-4 65t-40 51z m369 18q0 30-21 51t-51 21-50-21-21-51 21-50 50-21 51 21 21 50z m-358 357q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m250-107q0 30-20 51t-51 21-50-21-21-51 21-50 50-21 51 21 20 50z m179-250q0-145-79-269-10-17-30-17h-782q-20 0-30 17-79 123-79 269 0 102 40 194t106 160 160 107 194 39 194-39 160-107 106-160 40-194z" horiz-adv-x="1000" />
<glyph glyph-name="comment-empty" unicode="&#xf0e5;" d="M500 643q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" /> <glyph glyph-name="comment-empty" unicode="&#xf0e5;" d="M500 643q-114 0-213-39t-157-105-59-142q0-62 40-119t113-98l48-28-15-53q-13-51-39-97 85 36 154 96l24 21 32-3q38-5 72-5 114 0 213 39t157 105 59 142-59 142-157 105-213 39z m500-286q0-97-67-179t-182-130-251-48q-39 0-81 4-110-97-257-135-27-8-63-12h-3q-8 0-15 6t-9 15v1q-2 2 0 6t1 6 2 5l4 5t4 5 4 5q4 5 17 19t20 22 17 22 18 28 15 33 15 42q-88 50-138 123t-51 157q0 97 67 179t182 130 251 48 251-48 182-130 67-179z" horiz-adv-x="1000" />
<glyph glyph-name="bell-alt" unicode="&#xf0f3;" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" /> <glyph glyph-name="bell-alt" unicode="&#xf0f3;" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m455 160q0-29-21-50t-50-21h-250q0-59-42-101t-101-42-101 42-42 101h-250q-29 0-50 21t-21 50q28 24 51 49t47 67 42 89 27 115 11 145q0 84 66 157t171 89q-5 10-5 21 0 23 16 38t38 16 38-16 16-38q0-11-5-21 106-16 171-89t66-157q0-78 11-145t28-115 41-89 48-67 50-49z" horiz-adv-x="1000" />

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
static/font/font/fontello.ttf Normal file → Executable file

Binary file not shown.

BIN
static/font/font/fontello.woff Normal file → Executable file

Binary file not shown.

BIN
static/font/font/fontello.woff2 Normal file → Executable file

Binary file not shown.

View file

@ -345,5 +345,16 @@ describe('API Entities normalizer', () => {
const result = addEmojis('Admin add the :tenshi: emoji', emojis) const result = addEmojis('Admin add the :tenshi: emoji', emojis)
expect(result).to.equal('Admin add the :tenshi: emoji') expect(result).to.equal('Admin add the :tenshi: emoji')
}) })
it('Doesn\'t blow up on regex special characters', () => {
const emojis = makeMockEmojiMasto([{
shortcode: 'c++'
}, {
shortcode: '[a-z] {|}*'
}])
const result = addEmojis('This post has :c++: emoji and :[a-z] {|}*: emoji', emojis)
expect(result).to.include('title=\'c++\'')
expect(result).to.include('title=\'[a-z] {|}*\'')
})
}) })
}) })