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
- build
- test
- deploy
lint:
stage: lint
@ -33,3 +34,13 @@ build:
artifacts:
paths:
- 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]
### Added
- Ability to hide/show repeats from user
- User profile button clutter organized into a menu
- Emoji picker
- Started changelog anew
### Changed

View File

@ -1,7 +1,7 @@
# Pleroma-FE configuration and customization for instance administrators
* *For user configuration, see USER_GUIDE.md*
* *For local development server configuration, see HACKING.md*
* *For user configuration, see [Pleroma-FE user guide](USER_GUIDE.md)*
* *For local development server configuration, see [Hacking, tweaking, contributing](HACKING.md)*
## 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.
> If there was no insanity
@ -6,8 +8,6 @@
>
> --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.
## Posting, reading, basic functions.

View File

@ -39,10 +39,13 @@ h4 {
text-align: center;
}
html {
font-size: 14px;
}
body {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
font-size: 14px;
margin: 0;
color: $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 {
font-size: 1.2em;
}

View File

@ -53,6 +53,15 @@
:title="$t('nav.preferences')"
/>
</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
v-if="currentUser"
href="#"

View File

@ -47,7 +47,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ 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: 'friend-requests', path: '/friend-requests', component: FollowRequests, 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',
'size',
'allowPlay',
'setMedia'
'setMedia',
'naturalSizeLoad'
],
data () {
return {
@ -88,6 +89,11 @@ const Attachment = {
} else {
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"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
:image-load-handler="onImageLoad"
/>
</a>

View File

@ -4,8 +4,6 @@
trigger="click"
placement="top"
class="extra-button-popover"
:offset="5"
:container="false"
>
<div slot="popover">
<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 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 = {
props: [
'user',
'noFollowsYou'
],
data () {
return {
inProgress: false,
requestSent: false
}
},
components: {
BasicUserCard,
RemoteFollow
RemoteFollow,
FollowButton
},
computed: {
isMe () {
@ -24,21 +19,6 @@ const FollowCard = {
loggedIn () {
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>
</template>
<template v-else>
<button
v-if="!user.following"
class="btn btn-default follow-card-follow-button"
:disabled="inProgress"
: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>
<FollowButton
:user="user"
class="follow-card-follow-button"
:label-following="$t('user_card.follow_unfollow')"
/>
</template>
</div>
</basic-user-card>

View File

@ -1,23 +1,18 @@
import Attachment from '../attachment/attachment.vue'
import { chunk, last, dropRight } from 'lodash'
import { chunk, last, dropRight, sumBy } from 'lodash'
const Gallery = {
data: () => ({
width: 500
}),
props: [
'attachments',
'nsfw',
'setMedia'
],
data () {
return {
sizes: {}
}
},
components: { Attachment },
mounted () {
this.resize()
window.addEventListener('resize', this.resize)
},
destroyed () {
window.removeEventListener('resize', this.resize)
},
computed: {
rows () {
if (!this.attachments) {
@ -33,21 +28,24 @@ const Gallery = {
}
return rows
},
rowHeight () {
return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
},
useContainFit () {
return this.$store.getters.mergedConfig.useContainFit
}
},
methods: {
resize () {
// Quick optimization to make resizing not always trigger state change,
// only update attachment size in 10px steps
const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10
if (this.width !== width) {
this.width = width
}
onNaturalSizeLoad (id, size) {
this.$set(this.sizes, id, size)
},
rowStyle (itemsPerRow) {
return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
},
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"
:key="index"
class="gallery-row"
:style="rowHeight(row.length)"
:style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
>
<attachment
v-for="attachment in row"
:key="attachment.id"
:set-media="setMedia"
:nsfw="nsfw"
:attachment="attachment"
:allow-play="false"
/>
<div class="gallery-row-inner">
<attachment
v-for="attachment in row"
:key="attachment.id"
:set-media="setMedia"
:nsfw="nsfw"
:attachment="attachment"
:allow-play="false"
:natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
:style="itemStyle(attachment.id, row)"
/>
</div>
</div>
</div>
</template>
@ -28,15 +32,24 @@
@import '../../_variables.scss';
.gallery-row {
height: 200px;
position: relative;
height: 0;
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: stretch;
flex-grow: 1;
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
// we shouldn't have the need for .image here
.attachment.image {

View File

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

View File

@ -1,11 +1,13 @@
import StillImage from '../still-image/still-image.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'
const MediaModal = {
components: {
StillImage,
VideoAttachment
VideoAttachment,
Modal
},
computed: {
showing () {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,11 +10,12 @@ import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.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 fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.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'
const Status = {
@ -38,8 +39,6 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
preview: null,
showPreview: false,
showingTall: this.inConversation && this.focused,
showingLongSubject: false,
error: null,
@ -293,7 +292,8 @@ const Status = {
Gallery,
LinkPreview,
AvatarList,
Timeago
Timeago,
StatusPopover
},
methods: {
visibilityIcon (visibility) {
@ -368,27 +368,6 @@ const Status = {
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) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},

View File

@ -174,20 +174,26 @@
v-if="isReply"
class="reply-to-and-accountname"
>
<a
class="reply-to"
href="#"
: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()"
<StatusPopover
v-if="!isPreview"
:status-id="status.in_reply_to_status_id"
>
<i
v-if="!isPreview"
class="button-icon icon-reply"
/>
<span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
</a>
<a
class="reply-to"
href="#"
:aria-label="$t('tool_tip.reply')"
@click.prevent="gotoOriginal(status.in_reply_to_status_id)"
>
<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">
{{ replyToName }}
</router-link>
@ -199,50 +205,25 @@
</span>
</div>
<div
v-if="inConversation && !isPreview"
v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
>
<span
v-if="replies && replies.length"
class="faint"
>{{ $t('status.replies_list') }}</span>
<template v-if="replies">
<span
v-for="reply in replies"
:key="reply.id"
class="reply-link faint"
>
<a
href="#"
@click.prevent="gotoOriginal(reply.id)"
@mouseenter="replyEnter(reply.id, $event)"
@mouseout="replyLeave()"
>{{ reply.name }}</a>
</span>
</template>
<span class="faint">{{ $t('status.replies_list') }}</span>
<StatusPopover
v-for="reply in replies"
:key="reply.id"
:status-id="reply.id"
>
<a
href="#"
class="reply-link"
@click.prevent="gotoOriginal(reply.id)"
>{{ reply.name }}</a>
</StatusPopover>
</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
v-if="longSubject"
class="status-content-wrapper"
@ -439,18 +420,6 @@ $status-margin: 0.75em;
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 {
padding: $status-margin $status-margin 0;
display: flex;
@ -458,44 +427,6 @@ $status-margin: 0.75em;
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 {
margin-right: $status-margin;
}
@ -553,11 +484,6 @@ $status-margin: 0.75em;
flex-basis: 100%;
margin-bottom: 0.5em;
a {
display: inline-block;
word-break: break-all;
}
small {
font-weight: lighter;
}
@ -568,6 +494,11 @@ $status-margin: 0.75em;
justify-content: space-between;
line-height: 18px;
a {
display: inline-block;
word-break: break-all;
}
.name-and-account-name {
display: flex;
min-width: 0;
@ -600,6 +531,7 @@ $status-margin: 0.75em;
}
.heading-reply-row {
position: relative;
align-content: baseline;
font-size: 12px;
line-height: 18px;
@ -608,11 +540,13 @@ $status-margin: 0.75em;
flex-wrap: wrap;
align-items: stretch;
a {
> .reply-to-and-accountname > a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
word-break: break-all;
}
}
@ -639,6 +573,8 @@ $status-margin: 0.75em;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em;
color: $fallback--faint;
color: var(--faint, $fallback--faint);
}
.replies-separator {
@ -840,6 +776,11 @@ $status-margin: 0.75em;
&.button-icon-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
}
}
.button-icon.icon-reply {
&:not(.button-icon-disabled) {
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',
'referrerpolicy',
'mimetype',
'imageLoadError'
'imageLoadError',
'imageLoadHandler'
],
data () {
return {
@ -17,6 +18,7 @@ const StillImage = {
},
methods: {
onLoad () {
this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
const canvas = this.$refs.canvas
if (!canvas) return
const width = this.$refs.src.naturalWidth

View File

@ -1,14 +1,17 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.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 AccountActions from '../account_actions/account_actions.vue'
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 { mapGetters } from 'vuex'
export default {
props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ],
props: [
'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
],
data () {
return {
followRequestInProgress: false,
@ -96,30 +99,11 @@ export default {
UserAvatar,
RemoteFollow,
ModerationTools,
ProgressButton
AccountActions,
ProgressButton,
FollowButton
},
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 () {
this.$store.dispatch('muteUser', this.user.id)
},
@ -147,10 +131,10 @@ export default {
}
},
userProfileLink (user) {
return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
return generateProfileLink(
user.id, user.screen_name,
this.$store.state.instance.restrictedNicknames
)
},
zoomAvatar () {
const attachment = {
@ -159,9 +143,6 @@ export default {
}
this.$store.dispatch('setMedia', [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" />
</a>
<AccountActions
v-if="isOtherUser && loggedIn"
:user="user"
/>
</div>
<div class="bottom-line">
<router-link
class="user-screen-name"
@ -135,72 +138,27 @@
v-if="loggedIn && isOtherUser"
class="user-interactions"
>
<div v-if="!user.following">
<button
class="btn btn-default btn-block"
:disabled="followRequestInProgress"
:title="user.requested ? $t('user_card.follow_again') : ''"
@click="followUser"
>
<template v-if="followRequestInProgress">
{{ $t('user_card.follow_progress') }}
</template>
<template v-else-if="user.requested">
{{ $t('user_card.follow_sent') }}
</template>
<template v-else>
{{ $t('user_card.follow') }}
</template>
</button>
<div class="btn-group">
<FollowButton :user="user" />
<template v-if="user.following">
<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>
</template>
</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>
<button
v-if="user.muted"
@ -217,33 +175,6 @@
{{ $t('user_card.mute') }}
</button>
</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
v-if="loggedIn.role === &quot;admin&quot;"
:user="user"
@ -587,13 +518,12 @@
position: relative;
display: flex;
flex-flow: row wrap;
justify-content: space-between;
margin-right: -.75em;
> * {
flex: 1 0 0;
margin: 0 .75em .6em 0;
white-space: nowrap;
min-width: 95px;
}
button {

View File

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

View File

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

View File

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

View File

@ -68,6 +68,7 @@
},
"nav": {
"about": "About",
"administration": "Administration",
"back": "Back",
"chat": "Local Chat",
"friend_requests": "Follow Requests",
@ -554,6 +555,8 @@
"unmute": "Unmute",
"unmute_progress": "Unmuting...",
"mute_progress": "Muting...",
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
@ -629,6 +632,8 @@
"return_home": "Return to the home page",
"not_found": "We couldn't find that email or username.",
"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."
},
"login": {
"login": "Identificación",
"description": "Identificación con OAuth",
"login": "Identificarse",
"description": "Identificarse con OAuth",
"logout": "Cerrar sesión",
"password": "Contraseña",
"placeholder": "p.ej. lain",
@ -68,6 +68,7 @@
},
"nav": {
"about": "Acerca de",
"administration": "Administración",
"back": "Volver",
"chat": "Chat Local",
"friend_requests": "Solicitudes de seguimiento",
@ -106,6 +107,15 @@
"expired": "La encuesta terminó hace {0}",
"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": {
"add_sticker": "Añadir Pegatina"
},
@ -222,7 +232,9 @@
"data_import_export_tab": "Importar / Exportar Datos",
"default_vis": "Alcance de visibilidad por defecto",
"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.",
"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_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.",
@ -277,6 +289,8 @@
"no_mutes": "No hay usuarios sinlenciados",
"hide_follows_description": "No mostrar a quién sigo",
"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_moderator_badge": "Mostrar la insignia de Moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
@ -529,6 +543,7 @@
"follows_you": "¡Te sigue!",
"its_you": "¡Eres tú!",
"media": "Media",
"mention": "Mencionar",
"mute": "Silenciar",
"muted": "Silenciado",
"per_day": "por día",

View File

@ -68,6 +68,7 @@
},
"nav": {
"about": "Honi buruz",
"administration": "Administrazioa",
"back": "Atzera",
"chat": "Txat lokala",
"friend_requests": "Jarraitzeko eskaerak",
@ -106,6 +107,15 @@
"expired": "Inkesta {0} bukatu zen",
"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": {
"add_sticker": "Pegatina gehitu"
},
@ -199,12 +209,12 @@
"avatarRadius": "Avatarrak",
"background": "Atzeko planoa",
"bio": "Biografia",
"block_export": "Bloke esportatzea",
"block_export_button": "Esportatu zure blokeak csv fitxategi batera",
"block_import": "Bloke inportazioa",
"block_import_error": "Errorea blokeak inportatzen",
"blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.",
"blocks_tab": "Blokeak",
"block_export": "Blokeatu dituzunak esportatu",
"block_export_button": "Esportatu blokeatutakoak csv fitxategi batera",
"block_import": "Blokeatu dituzunak inportatu",
"block_import_error": "Errorea blokeatutakoak inportatzen",
"blocks_imported": "Blokeatutakoak inportaturik! Hauek prozesatzeak denbora hartuko du.",
"blocks_tab": "Blokeatutakoak",
"btnRadius": "Botoiak",
"cBlue": "Urdina (erantzun, jarraitu)",
"cGreen": "Berdea (Bertxiotu)",
@ -222,7 +232,9 @@
"data_import_export_tab": "Datuak Inportatu / Esportatu",
"default_vis": "Lehenetsitako ikusgaitasunak",
"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",
"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_instructions": "Idatzi zure pasahitza kontua ezabatzeko.",
"avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.",
@ -254,7 +266,7 @@
"instance_default": "(lehenetsia: {value})",
"instance_default_simple": "(lehenetsia)",
"interface": "Interfazea",
"interfaceLanguage": "Interfaze hizkuntza",
"interfaceLanguage": "Interfazearen hizkuntza",
"invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.",
"limited_availability": "Ez dago erabilgarri zure nabigatzailean",
"links": "Estekak",
@ -277,6 +289,8 @@
"no_mutes": "Ez daude erabiltzaile mututuak",
"hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen",
"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_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan",
"nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko",
@ -449,7 +463,7 @@
},
"version": {
"title": "Bertsioa",
"backend_version": "Backend Bertsio",
"backend_version": "Backend Bertsioa",
"frontend_version": "Frontend Bertsioa"
}
},
@ -529,6 +543,7 @@
"follows_you": "Jarraitzen dizu!",
"its_you": "Zu zara!",
"media": "Multimedia",
"mention": "Aipatu",
"mute": "Isilarazi",
"muted": "Isilduta",
"per_day": "eguneko",

View File

@ -106,6 +106,15 @@
"expired": "いれふだは {0} まえに、おわりました",
"not_enough_options": "ユニークなオプションが、たりません"
},
"emoji": {
"stickers": "ステッカー",
"emoji": "えもじ",
"keep_open": "ピッカーをあけたままにする",
"search_emoji": "えもじをさがす",
"add_emoji": "えもじをうちこむ",
"custom": "カスタムえもじ",
"unicode": "ユニコードえもじ"
},
"stickers": {
"add_sticker": "ステッカーをふやす"
},
@ -224,10 +233,11 @@
"default_vis": "デフォルトのこうかいはんい",
"delete_account": "アカウントをけす",
"delete_account_description": "あなたのアカウントとメッセージが、きえます。",
"delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。",
"delete_account_error": "アカウントをけすことが、できなかったかもしれません。インスタンスのアドミニストレーターに、おといあわせください。",
"delete_account_instructions": "ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。",
"discoverable": "けんさくなどのサービスで、このアカウントをみつけてもよい",
"avatar_size_instruction": "アバターのおおきさは、150×150ピクセルか、それよりもおおきくするといいです。",
"pad_emoji": "えもじをピッカーでえらんだとき、えもじのまわりにスペースをいれる",
"export_theme": "セーブ",
"filtering": "フィルタリング",
"filtering_explanation": "これらのことばをふくむすべてのものがミュートされます。1ぎょうに1つのことばをかいてください。",
@ -280,6 +290,8 @@
"no_mutes": "ミュートしていません",
"hide_follows_description": "フォローしているひとをみせない",
"hide_followers_description": "フォロワーをみせない",
"hide_follows_count_description": "フォローしているひとのかずをみせない",
"hide_followers_count_description": "フォロワーのかずをみせない",
"show_admin_badge": "アドミンのしるしをみせる",
"show_moderator_badge": "モデレーターのしるしをみせる",
"nsfw_clickthrough": "NSFWなファイルをかくす",
@ -532,6 +544,7 @@
"follows_you": "フォローされました!",
"its_you": "これはあなたです!",
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
"muted": "ミュートしています!",
"per_day": "/日",
@ -611,5 +624,16 @@
"person_talking": "{count} にんが、はなしています",
"people_talking": "{count} にんが、はなしています",
"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} 前に終了しました",
"not_enough_options": "相異なる選択肢が不足しています"
},
"emoji": {
"stickers": "ステッカー",
"emoji": "絵文字",
"keep_open": "ピッカーを開いたままにする",
"search_emoji": "絵文字を検索",
"add_emoji": "絵文字を挿入",
"custom": "カスタム絵文字",
"unicode": "Unicode絵文字"
},
"stickers": {
"add_sticker": "ステッカーを追加"
},
@ -228,6 +237,7 @@
"delete_account_instructions": "本当にアカウントを消してもいいなら、パスワードを入力してください。",
"discoverable": "検索などのサービスでこのアカウントを見つけることを許可する",
"avatar_size_instruction": "アバターの大きさは、150×150ピクセルか、それよりも大きくするといいです。",
"pad_emoji": "ピッカーから絵文字を挿入するとき、絵文字の両側にスペースを入れる",
"export_theme": "保存",
"filtering": "フィルタリング",
"filtering_explanation": "これらの言葉を含むすべてのものがミュートされます。1行に1つの言葉を書いてください。",
@ -280,6 +290,8 @@
"no_mutes": "ミュートはありません",
"hide_follows_description": "フォローしている人を見せない",
"hide_followers_description": "フォロワーを見せない",
"hide_follows_count_description": "フォローしている人の数を見せない",
"hide_followers_count_description": "フォロワーの数を見せない",
"show_admin_badge": "管理者のバッジを見せる",
"show_moderator_badge": "モデレーターのバッジを見せる",
"nsfw_clickthrough": "NSFWなファイルを隠す",
@ -532,6 +544,7 @@
"follows_you": "フォローされました!",
"its_you": "これはあなたです!",
"media": "メディア",
"mention": "メンション",
"mute": "ミュート",
"muted": "ミュートしています!",
"per_day": "/日",
@ -611,5 +624,16 @@
"person_talking": "{count} 人が話しています",
"people_talking": "{count} 人が話しています",
"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(PortalVue)
Vue.use(VBodyScrollLock)
Vue.use(VTooltip)
Vue.use(VTooltip, {
popover: {
defaultTrigger: 'hover click',
defaultContainer: false,
defaultOffset: 5
}
})
const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary

View File

@ -537,6 +537,10 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
fetchStatus ({ rootState, dispatch }, id) {
rootState.api.backendInteractor.fetchStatus({ id })
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
},
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
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]))
}
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 = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
@ -135,6 +147,7 @@ export const mutations = {
user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking
user.subscribed = relationship.subscribing
user.showing_reblogs = relationship.showing_reblogs
}
})
},
@ -272,6 +285,12 @@ const users = {
unmuteUser (store, id) {
return unmuteUser(store, id)
},
hideReblogs (store, id) {
return hideReblogs(store, id)
},
showReblogs (store, id) {
return showReblogs(store, id)
},
muteUsers (store, ids = []) {
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)
const form = {}
if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
return fetch(url, {
headers: authHeaders(credentials),
body: JSON.stringify(form),
headers: {
...authHeaders(credentials),
'Content-Type': 'application/json'
},
method: 'POST'
}).then((data) => data.json())
}

View File

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

View File

@ -69,6 +69,7 @@ export const parseUser = (data) => {
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing
}
@ -196,9 +197,11 @@ export const parseAttachment = (data) => {
return output
}
export const addEmojis = (string, emojis) => {
const matchOperatorsRegex = /[|\\{}()[\]^$+*?.-]/g
return emojis.reduce((acc, emoji) => {
const regexSafeShortCode = emoji.shortcode.replace(matchOperatorsRegex, '\\$&')
return acc.replace(
new RegExp(`:${emoji.shortcode}:`, 'g'),
new RegExp(`:${regexSafeShortCode}:`, 'g'),
`<img src='${emoji.url}' alt='${emoji.shortcode}' title='${emoji.shortcode}' class='emoji' />`
)
}, 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) => {
store.state.api.backendInteractor.followUser(user.id)
store.state.api.backendInteractor.followUser({ id: user.id })
.then((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",
"code": 59420,
"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-menu:before { content: '\f0c9'; } /* '' */
.icon-mail-alt:before { content: '\f0e0'; } /* '' */
.icon-gauge:before { content: '\f0e4'; } /* '' */
.icon-comment-empty:before { content: '\f0e5'; } /* '' */
.icon-bell-alt:before { content: '\f0f3'; } /* '' */
.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-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-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-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;'); }

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

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

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