aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.babelrc4
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--build/webpack.base.conf.js2
-rw-r--r--config/index.js4
-rw-r--r--package.json5
-rw-r--r--src/App.js43
-rw-r--r--src/App.scss124
-rw-r--r--src/App.vue9
-rw-r--r--src/components/attachment/attachment.js38
-rw-r--r--src/components/attachment/attachment.vue20
-rw-r--r--src/components/conversation/conversation.vue6
-rw-r--r--src/components/features_panel/features_panel.js14
-rw-r--r--src/components/features_panel/features_panel.vue29
-rw-r--r--src/components/follow_requests/follow_requests.js23
-rw-r--r--src/components/follow_requests/follow_requests.vue12
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue38
-rw-r--r--src/components/login_form/login_form.vue2
-rw-r--r--src/components/media_upload/media_upload.js6
-rw-r--r--src/components/media_upload/media_upload.vue2
-rw-r--r--src/components/nav_panel/nav_panel.vue5
-rw-r--r--src/components/notification/notification.js11
-rw-r--r--src/components/notification/notification.vue16
-rw-r--r--src/components/notifications/notifications.js38
-rw-r--r--src/components/notifications/notifications.scss81
-rw-r--r--src/components/notifications/notifications.vue15
-rw-r--r--src/components/post_status_form/post_status_form.js31
-rw-r--r--src/components/post_status_form/post_status_form.vue95
-rw-r--r--src/components/registration/registration.js10
-rw-r--r--src/components/registration/registration.vue4
-rw-r--r--src/components/retweet_button/retweet_button.js5
-rw-r--r--src/components/retweet_button/retweet_button.vue11
-rw-r--r--src/components/settings/settings.js67
-rw-r--r--src/components/settings/settings.vue196
-rw-r--r--src/components/status/status.js136
-rw-r--r--src/components/status/status.vue120
-rw-r--r--src/components/still-image/still-image.js6
-rw-r--r--src/components/still-image/still-image.vue2
-rw-r--r--src/components/style_switcher/style_switcher.js91
-rw-r--r--src/components/style_switcher/style_switcher.vue233
-rw-r--r--src/components/tab_switcher/tab_switcher.jsx44
-rw-r--r--src/components/tab_switcher/tab_switcher.scss43
-rw-r--r--src/components/timeline/timeline.js18
-rw-r--r--src/components/timeline/timeline.vue43
-rw-r--r--src/components/user_card/user_card.js11
-rw-r--r--src/components/user_card/user_card.vue25
-rw-r--r--src/components/user_card_content/user_card_content.js28
-rw-r--r--src/components/user_card_content/user_card_content.vue128
-rw-r--r--src/components/user_panel/user_panel.vue8
-rw-r--r--src/components/user_profile/user_profile.vue2
-rw-r--r--src/components/user_settings/user_settings.js32
-rw-r--r--src/components/user_settings/user_settings.vue222
-rw-r--r--src/components/who_to_follow_panel/who_to_follow_panel.js60
-rw-r--r--src/components/who_to_follow_panel/who_to_follow_panel.vue4
-rw-r--r--src/i18n/messages.js393
-rw-r--r--src/main.js156
-rw-r--r--src/modules/api.js13
-rw-r--r--src/modules/config.js31
-rw-r--r--src/modules/statuses.js191
-rw-r--r--src/modules/users.js8
-rw-r--r--src/services/api/api.service.js63
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js25
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js46
-rw-r--r--src/services/status_poster/status_poster.service.js4
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js4
-rw-r--r--src/services/user_highlighter/user_highlighter.js48
-rw-r--r--static/config.json11
-rw-r--r--static/font/LICENSE.txt9
-rw-r--r--static/font/config.json18
-rw-r--r--static/font/css/fontello-codes.css2
-rw-r--r--static/font/css/fontello-embedded.css14
-rw-r--r--static/font/css/fontello-ie7-codes.css2
-rw-r--r--static/font/css/fontello-ie7.css2
-rw-r--r--static/font/css/fontello.css16
-rw-r--r--static/font/demo.html22
-rw-r--r--static/font/font/fontello.eotbin15124 -> 15552 bytes
-rw-r--r--static/font/font/fontello.svg64
-rw-r--r--static/font/font/fontello.ttfbin14956 -> 15384 bytes
-rw-r--r--static/font/font/fontello.woffbin9176 -> 9432 bytes
-rw-r--r--static/font/font/fontello.woff2bin7812 -> 8020 bytes
-rw-r--r--test/unit/specs/modules/statuses.spec.js147
-rw-r--r--yarn.lock115
81 files changed, 2671 insertions, 956 deletions
diff --git a/.babelrc b/.babelrc
index 9fe4b349..bc2b0e31 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,5 +1,5 @@
{
- "presets": ["es2015", "stage-2"],
- "plugins": ["transform-runtime", "lodash"],
+ "presets": ["es2015", "stage-2", "env"],
+ "plugins": ["transform-runtime", "lodash", "transform-vue-jsx"],
"comments": false
}
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 3673b8b7..d7c217ce 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -8,3 +8,4 @@ Contributors of this project.
- hakui (hakui@freezepeach.xyz): CSS and styling
- shpuld (shpuld@shitposter.club): CSS and styling
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
+- hj (hj@shigusegubu.club): Code
diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js
index 7bba3a10..198532ca 100644
--- a/build/webpack.base.conf.js
+++ b/build/webpack.base.conf.js
@@ -54,7 +54,7 @@ module.exports = {
loader: 'vue'
},
{
- test: /\.js$/,
+ test: /\.jsx?$/,
loader: 'babel',
include: projectRoot,
exclude: /node_modules\/(?!tributejs)/
diff --git a/config/index.js b/config/index.js
index c48d91b8..5d2cb833 100644
--- a/config/index.js
+++ b/config/index.js
@@ -23,12 +23,12 @@ module.exports = {
assetsPublicPath: '/',
proxyTable: {
'/api': {
- target: 'htts://localhost:4000/',
+ target: 'http://localhost:4000/',
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/socket': {
- target: 'htts://localhost:4000/',
+ target: 'http://localhost:4000/',
changeOrigin: true,
cookieDomainRewrite: 'localhost',
ws: true
diff --git a/package.json b/package.json
index 5718d24d..b716e7c6 100644
--- a/package.json
+++ b/package.json
@@ -37,8 +37,12 @@
"autoprefixer": "^6.4.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.0.0",
+ "babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^6.0.0",
+ "babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.0.0",
+ "babel-plugin-transform-vue-jsx": "3",
+ "babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.0.0",
"babel-preset-stage-2": "^6.0.0",
"babel-register": "^6.0.0",
@@ -63,6 +67,7 @@
"html-webpack-plugin": "^2.8.1",
"http-proxy-middleware": "^0.17.2",
"inject-loader": "^2.0.1",
+ "iso-639-1": "^2.0.3",
"isparta-loader": "^2.0.0",
"json-loader": "^0.5.4",
"karma": "^1.3.0",
diff --git a/src/App.js b/src/App.js
index a052e058..be6548f3 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,8 +2,9 @@ import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
import UserFinder from './components/user_finder/user_finder.vue'
-import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
+import FeaturesPanel from './components/features_panel/features_panel.vue'
+import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue'
export default {
@@ -13,23 +14,55 @@ export default {
NavPanel,
Notifications,
UserFinder,
- WhoToFollowPanel,
InstanceSpecificPanel,
+ FeaturesPanel,
+ WhoToFollowPanel,
ChatPanel
},
data: () => ({
- mobileActivePanel: 'timeline'
+ mobileActivePanel: 'timeline',
+ supportsMask: window.CSS && window.CSS.supports && (
+ window.CSS.supports('mask-size', 'contain') ||
+ window.CSS.supports('-webkit-mask-size', 'contain') ||
+ window.CSS.supports('-moz-mask-size', 'contain') ||
+ window.CSS.supports('-ms-mask-size', 'contain') ||
+ window.CSS.supports('-o-mask-size', 'contain')
+ )
}),
+ created () {
+ // Load the locale from the storage
+ this.$i18n.locale = this.$store.state.config.interfaceLanguage
+ },
computed: {
currentUser () { return this.$store.state.users.currentUser },
background () {
return this.currentUser.background_image || this.$store.state.config.background
},
- logoStyle () { return { 'background-image': `url(${this.$store.state.config.logo})` } },
+ enableMask () { return this.supportsMask && this.$store.state.config.logoMask },
+ logoStyle () {
+ return {
+ 'visibility': this.enableMask ? 'hidden' : 'visible'
+ }
+ },
+ logoMaskStyle () {
+ return this.enableMask ? {
+ 'mask-image': `url(${this.$store.state.config.logo})`
+ } : {
+ 'background-color': this.enableMask ? '' : 'transparent'
+ }
+ },
+ logoBgStyle () {
+ return Object.assign({
+ 'margin': `${this.$store.state.config.logoMargin} 0`
+ }, this.enableMask ? {} : {
+ 'background-color': this.enableMask ? '' : 'transparent'
+ })
+ },
+ logo () { return this.$store.state.config.logo },
style () { return { 'background-image': `url(${this.background})` } },
sitename () { return this.$store.state.config.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
- showWhoToFollowPanel () { return this.$store.state.config.showWhoToFollowPanel },
+ suggestionsEnabled () { return this.$store.state.config.suggestionsEnabled },
showInstanceSpecificPanel () { return this.$store.state.config.showInstanceSpecificPanel }
},
methods: {
diff --git a/src/App.scss b/src/App.scss
index f830a33b..056a235e 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -48,7 +48,7 @@ a {
color: var(--link, $fallback--link);
}
-button{
+button {
user-select: none;
color: $fallback--fg;
color: var(--fg, $fallback--fg);
@@ -64,10 +64,19 @@ button{
font-size: 14px;
font-family: sans-serif;
+ &::-moz-focus-inner {
+ border: none;
+ }
+
&:hover {
box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
}
+ &:active {
+ border-bottom: 1px solid rgba(255, 255, 255, 0.2);
+ border-top: 1px solid rgba(0, 0, 0, 0.2);
+ }
+
&:disabled {
cursor: not-allowed;
opacity: 0.5;
@@ -105,6 +114,7 @@ input, textarea, .select {
position: relative;
height: 29px;
line-height: 16px;
+ hyphens: none;
.icon-down-open {
position: absolute;
@@ -142,6 +152,14 @@ input, textarea, .select {
color: $fallback--fg;
color: var(--fg, $fallback--fg);
}
+ &:disabled,
+ {
+ &,
+ & + label,
+ & + label::before {
+ opacity: .5;
+ }
+ }
+ label::before {
display: inline-block;
content: '✔';
@@ -168,6 +186,13 @@ input, textarea, .select {
}
}
+option {
+ color: $fallback--fg;
+ color: var(--fg, $fallback--fg);
+ background-color: $fallback--bg;
+ background-color: var(--bg, $fallback--bg);
+}
+
i[class*=icon-] {
color: $fallback--icon;
color: var(--icon, $fallback--icon)
@@ -211,6 +236,40 @@ nav {
position: fixed;
height: 50px;
+ .logo {
+ display: flex;
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ align-items: stretch;
+ justify-content: center;
+ flex: 0 0 auto;
+ z-index: -1;
+
+ .mask {
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ background-color: $fallback--fg;
+ background-color: var(--fg, $fallback--fg);
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ }
+
+ img {
+ height: 100%;
+ object-fit: contain;
+ display: block;
+ flex: 0;
+ }
+ }
+
.inner-nav {
padding-left: 20px;
padding-right: 20px;
@@ -219,9 +278,6 @@ nav {
flex-basis: 970px;
margin: auto;
height: 50px;
- background-repeat: no-repeat;
- background-position: center;
- background-size: auto 80%;
a i {
color: $fallback--link;
@@ -267,15 +323,42 @@ main-router {
}
.panel-heading {
+ display: flex;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
- padding: 0.6em 1.0em;
+ padding: .6em .6em;
text-align: left;
- font-size: 1.3em;
- line-height: 24px;
+ line-height: 28px;
background-color: $fallback--btn;
background-color: var(--btn, $fallback--btn);
+ align-items: baseline;
+
+ .title {
+ flex: 1 0 auto;
+ font-size: 1.3em;
+ }
+
+ .alert {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow-x: hidden;
+ }
+
+ button {
+ flex-shrink: 0;
+ }
+
+ button, .alert {
+ // height: 100%;
+ line-height: 21px;
+ min-height: 0;
+ box-sizing: border-box;
+ margin: 0;
+ margin-left: .25em;
+ min-width: 1px;
+ align-self: stretch;
+ }
}
.panel-heading.stub {
@@ -426,3 +509,30 @@ nav {
text-align: right;
padding-right: 20px;
}
+
+.visibility-tray {
+ font-size: 1.2em;
+ padding: 3px;
+ cursor: pointer;
+
+ .selected {
+ color: $fallback--lightFg;
+ color: var(--lightFg, $fallback--lightFg);
+ }
+
+ .text-format {
+ float: right;
+ }
+
+ div {
+ padding-top: 5px;
+ }
+}
+
+.visibility-notice {
+ padding: .5em;
+ border: 1px solid $fallback--faint;
+ border: 1px solid var(--faint, $fallback--faint);
+ border-radius: $fallback--inputRadius;
+ border-radius: var(--inputRadius, $fallback--inputRadius);
+}
diff --git a/src/App.vue b/src/App.vue
index 923d411b..059460f9 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,7 +1,11 @@
<template>
<div id="app" v-bind:style="style">
<nav class='container' @click="scrollToTop()" id="nav">
- <div class='inner-nav' :style="logoStyle">
+ <div class='logo' :style='logoBgStyle'>
+ <div class='mask' :style='logoMaskStyle'></div>
+ <img :src='logo' :style='logoStyle'>
+ </div>
+ <div class='inner-nav'>
<div class='item'>
<router-link :to="{ name: 'root'}">{{sitename}}</router-link>
</div>
@@ -24,7 +28,8 @@
<user-panel></user-panel>
<nav-panel></nav-panel>
<instance-specific-panel v-if="showInstanceSpecificPanel"></instance-specific-panel>
- <who-to-follow-panel v-if="currentUser && showWhoToFollowPanel"></who-to-follow-panel>
+ <features-panel v-if="!currentUser"></features-panel>
+ <who-to-follow-panel v-if="currentUser && suggestionsEnabled"></who-to-follow-panel>
<notifications v-if="currentUser"></notifications>
</div>
</div>
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index d9bc4477..41730720 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -13,9 +13,10 @@ const Attachment = {
return {
nsfwImage,
hideNsfwLocal: this.$store.state.config.hideNsfw,
+ loopVideo: this.$store.state.config.loopVideo,
showHidden: false,
loading: false,
- img: document.createElement('img')
+ img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img')
}
},
components: {
@@ -45,14 +46,35 @@ const Attachment = {
}
},
toggleHidden () {
- if (this.img.onload) {
- this.img.onload()
+ if (this.img) {
+ if (this.img.onload) {
+ this.img.onload()
+ } else {
+ this.loading = true
+ this.img.src = this.attachment.url
+ this.img.onload = () => {
+ this.loading = false
+ this.showHidden = !this.showHidden
+ }
+ }
} else {
- this.loading = true
- this.img.src = this.attachment.url
- this.img.onload = () => {
- this.loading = false
- this.showHidden = !this.showHidden
+ this.showHidden = !this.showHidden
+ }
+ },
+ onVideoDataLoad (e) {
+ if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') {
+ // non-zero if video has audio track
+ if (e.srcElement.webkitAudioDecodedByteCount > 0) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ }
+ } else if (typeof e.srcElement.mozHasAudio !== 'undefined') {
+ // true if video has audio track
+ if (e.srcElement.mozHasAudio) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ }
+ } else if (typeof e.srcElement.audioTracks !== 'undefined') {
+ if (e.srcElement.audioTracks.length > 0) {
+ this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
}
}
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index c48fb16b..8795b131 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -2,7 +2,7 @@
<div v-if="size==='hide'">
<a class="placeholder" v-if="type !== 'html'" target="_blank" :href="attachment.url">[{{nsfw ? "NSFW/" : ""}}{{type.toUpperCase()}}]</a>
</div>
- <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth}" v-show="!isEmpty">
+ <div v-else class="attachment" :class="{[type]: true, loading, 'small-attachment': isSmall, 'fullwidth': fullwidth, 'nsfw-placeholder': hidden}" v-show="!isEmpty">
<a class="image-attachment" v-if="hidden" @click.prevent="toggleHidden()">
<img :key="nsfwImage" :src="nsfwImage"/>
</a>
@@ -10,11 +10,11 @@
<a href="#" @click.prevent="toggleHidden()">Hide</a>
</div>
- <a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank">
+ <a v-if="type === 'image' && !hidden" class="image-attachment" :href="attachment.url" target="_blank" :title="attachment.description">
<StillImage :class="{'small': isSmall}" referrerpolicy="no-referrer" :mimetype="attachment.mimetype" :src="attachment.large_thumb_url || attachment.url"/>
</a>
- <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" :src="attachment.url" controls loop></video>
+ <video :class="{'small': isSmall}" v-if="type === 'video' && !hidden" @loadeddata="onVideoDataLoad" :src="attachment.url" controls :loop="loopVideo"></video>
<audio v-if="type === 'audio'" :src="attachment.url" controls></audio>
@@ -38,7 +38,6 @@
.attachments {
display: flex;
flex-wrap: wrap;
- margin-right: -0.7em;
.attachment.media-upload-container {
flex: 0 0 auto;
@@ -50,6 +49,14 @@
margin-right: 0.5em;
}
+ .nsfw-placeholder {
+ cursor: pointer;
+
+ &.loading {
+ cursor: progress;
+ }
+ }
+
.small-attachment {
&.image, &.video {
max-width: 35%;
@@ -58,6 +65,7 @@
}
.attachment {
+ position: relative;
flex: 1 0 30%;
margin: 0.5em 0.7em 0.6em 0.0em;
align-self: flex-start;
@@ -85,10 +93,6 @@
display: flex;
}
- &.loading {
- cursor: progress;
- }
-
.hider {
position: absolute;
margin: 10px;
diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue
index bfcd3fe7..5528fef6 100644
--- a/src/components/conversation/conversation.vue
+++ b/src/components/conversation/conversation.vue
@@ -1,9 +1,9 @@
<template>
<div class="timeline panel panel-default">
<div class="panel-heading conversation-heading">
- {{ $t('timeline.conversation') }}
- <span v-if="collapsable" style="float:right;">
- <small><a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a></small>
+ <span class="title"> {{ $t('timeline.conversation') }} </span>
+ <span v-if="collapsable">
+ <a href="#" @click.prevent="$emit('toggleExpanded')">{{ $t('timeline.collapse') }}</a>
</span>
</div>
<div class="panel-body">
diff --git a/src/components/features_panel/features_panel.js b/src/components/features_panel/features_panel.js
new file mode 100644
index 00000000..80f5c966
--- /dev/null
+++ b/src/components/features_panel/features_panel.js
@@ -0,0 +1,14 @@
+const FeaturesPanel = {
+ computed: {
+ chat: function () {
+ return this.$store.state.config.chatAvailable && (!this.$store.state.chatDisabled)
+ },
+ gopher: function () { return this.$store.state.config.gopherAvailable },
+ whoToFollow: function () { return this.$store.state.config.suggestionsEnabled },
+ mediaProxy: function () { return this.$store.state.config.mediaProxyAvailable },
+ scopeOptions: function () { return this.$store.state.config.scopeOptionsEnabled },
+ textlimit: function () { return this.$store.state.config.textlimit }
+ }
+}
+
+export default FeaturesPanel
diff --git a/src/components/features_panel/features_panel.vue b/src/components/features_panel/features_panel.vue
new file mode 100644
index 00000000..445143e9
--- /dev/null
+++ b/src/components/features_panel/features_panel.vue
@@ -0,0 +1,29 @@
+<template>
+ <div class="features-panel">
+ <div class="panel panel-default base01-background">
+ <div class="panel-heading timeline-heading base02-background base04">
+ <div class="title">
+ {{$t('features_panel.title')}}
+ </div>
+ </div>
+ <div class="panel-body features-panel">
+ <ul>
+ <li v-if="chat">{{$t('features_panel.chat')}}</li>
+ <li v-if="gopher">{{$t('features_panel.gopher')}}</li>
+ <li v-if="whoToFollow">{{$t('features_panel.who_to_follow')}}</li>
+ <li v-if="mediaProxy">{{$t('features_panel.media_proxy')}}</li>
+ <li v-if="scopeOptions">{{$t('features_panel.scope_options')}}</li>
+ <li>{{$t('features_panel.text_limit')}} = {{textlimit}}</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script src="./features_panel.js" ></script>
+
+<style lang="scss">
+ .features-panel li {
+ line-height: 24px;
+ }
+</style>
diff --git a/src/components/follow_requests/follow_requests.js b/src/components/follow_requests/follow_requests.js
new file mode 100644
index 00000000..11a228aa
--- /dev/null
+++ b/src/components/follow_requests/follow_requests.js
@@ -0,0 +1,23 @@
+import UserCard from '../user_card/user_card.vue'
+
+const FollowRequests = {
+ components: {
+ UserCard
+ },
+ created () {
+ this.updateRequests()
+ },
+ computed: {
+ requests () {
+ return this.$store.state.api.followRequests
+ }
+ },
+ methods: {
+ updateRequests () {
+ this.$store.state.api.backendInteractor.fetchFollowRequests()
+ .then((requests) => { this.$store.commit('setFollowRequests', requests) })
+ }
+ }
+}
+
+export default FollowRequests
diff --git a/src/components/follow_requests/follow_requests.vue b/src/components/follow_requests/follow_requests.vue
new file mode 100644
index 00000000..87dc4194
--- /dev/null
+++ b/src/components/follow_requests/follow_requests.vue
@@ -0,0 +1,12 @@
+<template>
+ <div class="settings panel panel-default">
+ <div class="panel-heading">
+ {{$t('nav.friend_requests')}}
+ </div>
+ <div class="panel-body">
+ <user-card v-for="request in requests" :key="request.id" :user="request" :showFollows="false" :showApproval="true"></user-card>
+ </div>
+ </div>
+</template>
+
+<script src="./follow_requests.js"></script>
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
new file mode 100644
index 00000000..4b541888
--- /dev/null
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -0,0 +1,38 @@
+<template>
+ <div>
+ <label for="interface-language-switcher" class='select'>
+ <select id="interface-language-switcher" v-model="language">
+ <option v-for="(langCode, i) in languageCodes" :value="langCode">
+ {{ languageNames[i] }}
+ </option>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+</template>
+
+<script>
+ import languagesObject from '../../i18n/messages'
+ import ISO6391 from 'iso-639-1'
+ import _ from 'lodash'
+
+ export default {
+ computed: {
+ languageCodes () {
+ return Object.keys(languagesObject)
+ },
+
+ languageNames () {
+ return _.map(this.languageCodes, ISO6391.getName)
+ },
+
+ language: {
+ get: function () { return this.$store.state.config.interfaceLanguage },
+ set: function (val) {
+ this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
+ this.$i18n.locale = val
+ }
+ }
+ }
+ }
+</script>
diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue
index 67fa95a8..b7fed48a 100644
--- a/src/components/login_form/login_form.vue
+++ b/src/components/login_form/login_form.vue
@@ -8,7 +8,7 @@
<form v-on:submit.prevent='submit(user)' class='login-form'>
<div class='form-group'>
<label for='username'>{{$t('login.username')}}</label>
- <input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' placeholder='e.g. lain'>
+ <input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')">
</div>
<div class='form-group'>
<label for='password'>{{$t('login.password')}}</label>
diff --git a/src/components/media_upload/media_upload.js b/src/components/media_upload/media_upload.js
index 8b4e7ad4..66337c3f 100644
--- a/src/components/media_upload/media_upload.js
+++ b/src/components/media_upload/media_upload.js
@@ -6,8 +6,10 @@ const mediaUpload = {
const input = this.$el.querySelector('input')
input.addEventListener('change', ({target}) => {
- const file = target.files[0]
- this.uploadFile(file)
+ for (var i = 0; i < target.files.length; i++) {
+ let file = target.files[i]
+ this.uploadFile(file)
+ }
})
},
data () {
diff --git a/src/components/media_upload/media_upload.vue b/src/components/media_upload/media_upload.vue
index 8b931d2d..88094ebb 100644
--- a/src/components/media_upload/media_upload.vue
+++ b/src/components/media_upload/media_upload.vue
@@ -3,7 +3,7 @@
<label class="btn btn-default">
<i class="icon-spin4 animate-spin" v-if="uploading"></i>
<i class="icon-upload" v-if="!uploading"></i>
- <input type=file style="position: fixed; top: -100em"></input>
+ <input type="file" style="position: fixed; top: -100em" multiple="true"></input>
</label>
</div>
</template>
diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue
index 2e1a6c7a..0b188f9a 100644
--- a/src/components/nav_panel/nav_panel.vue
+++ b/src/components/nav_panel/nav_panel.vue
@@ -12,6 +12,11 @@
{{ $t("nav.mentions") }}
</router-link>
</li>
+ <li v-if='currentUser && currentUser.locked'>
+ <router-link to='/friend-requests'>
+ {{ $t("nav.friend_requests") }}
+ </router-link>
+ </li>
<li>
<router-link to='/main/public'>
{{ $t("nav.public_tl") }}
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 3a274374..c786f2cc 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -1,6 +1,7 @@
import Status from '../status/status.vue'
import StillImage from '../still-image/still-image.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
+import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
const Notification = {
data () {
@@ -18,6 +19,16 @@ const Notification = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
}
+ },
+ computed: {
+ userClass () {
+ return highlightClass(this.notification.action.user)
+ },
+ userStyle () {
+ const highlight = this.$store.state.config.highlight
+ const user = this.notification.action.user
+ return highlightStyle(highlight[user.screen_name])
+ }
}
}
diff --git a/src/components/notification/notification.vue b/src/components/notification/notification.vue
index eed598a8..72c1ca69 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -1,6 +1,6 @@
<template>
<status v-if="notification.type === 'mention'" :compact="true" :statusoid="notification.status"></status>
- <div class="non-mention" v-else>
+ <div class="non-mention" :class="[userClass, { highlighted: userStyle }]" :style="[ userStyle ]"v-else>
<a class='avatar-container' :href="notification.action.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage class='avatar-compact' :src="notification.action.user.profile_image_url_original"/>
</a>
@@ -10,8 +10,9 @@
</div>
<span class="notification-details">
<div class="name-and-action">
- <span class="username" :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
- <span v-if="notification.type === 'favorite'">
+ <span class="username" v-if="!!notification.action.user.name_html" :title="'@'+notification.action.user.screen_name" v-html="notification.action.user.name_html"></span>
+ <span class="username" v-else :title="'@'+notification.action.user.screen_name">{{ notification.action.user.name }}</span>
+ <span v-if="notification.type === 'like'">
<i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small>
</span>
@@ -24,12 +25,17 @@
<small>{{$t('notifications.followed_you')}}</small>
</span>
</div>
- <small class="timeago"><router-link :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
+ <small class="timeago"><router-link v-if="notification.status" :to="{ name: 'conversation', params: { id: notification.status.id } }"><timeago :since="notification.action.created_at" :auto-update="240"></timeago></router-link></small>
</span>
<div class="follow-text" v-if="notification.type === 'follow'">
<router-link :to="{ name: 'user-profile', params: { id: notification.action.user.id } }">@{{notification.action.user.screen_name}}</router-link>
</div>
- <status v-else class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
+ <template v-else>
+ <status v-if="notification.status" class="faint" :compact="true" :statusoid="notification.status" :noHeading="true"></status>
+ <div class="broken-favorite" v-else>
+ {{$t('notifications.broken_favorite')}}
+ </div>
+ </template>
</div>
</div>
</template>
diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js
index f8314bfc..58956f98 100644
--- a/src/components/notifications/notifications.js
+++ b/src/components/notifications/notifications.js
@@ -1,25 +1,38 @@
import Notification from '../notification/notification.vue'
+import notificationsFetcher from '../../services/notifications_fetcher/notifications_fetcher.service.js'
-import { sortBy, take, filter } from 'lodash'
+import { sortBy, filter } from 'lodash'
const Notifications = {
- data () {
- return {
- visibleNotificationCount: 20
- }
+ created () {
+ const store = this.$store
+ const credentials = store.state.users.currentUser.credentials
+
+ notificationsFetcher.startFetching({ store, credentials })
},
computed: {
+ visibleTypes () {
+ return [
+ this.$store.state.config.notificationVisibility.likes && 'like',
+ this.$store.state.config.notificationVisibility.mentions && 'mention',
+ this.$store.state.config.notificationVisibility.repeats && 'repeat',
+ this.$store.state.config.notificationVisibility.follows && 'follow'
+ ].filter(_ => _)
+ },
notifications () {
- return this.$store.state.statuses.notifications
+ return this.$store.state.statuses.notifications.data
+ },
+ error () {
+ return this.$store.state.statuses.notifications.error
},
unseenNotifications () {
- return filter(this.notifications, ({seen}) => !seen)
+ return filter(this.visibleNotifications, ({seen}) => !seen)
},
visibleNotifications () {
// Don't know why, but sortBy([seen, -action.id]) doesn't work.
let sortedNotifications = sortBy(this.notifications, ({action}) => -action.id)
sortedNotifications = sortBy(sortedNotifications, 'seen')
- return take(sortedNotifications, this.visibleNotificationCount)
+ return sortedNotifications.filter((notification) => this.visibleTypes.includes(notification.type))
},
unseenCount () {
return this.unseenNotifications.length
@@ -40,6 +53,15 @@ const Notifications = {
methods: {
markAsSeen () {
this.$store.commit('markNotificationsAsSeen', this.visibleNotifications)
+ },
+ fetchOlderNotifications () {
+ const store = this.$store
+ const credentials = store.state.users.currentUser.credentials
+ notificationsFetcher.fetchAndUpdate({
+ store,
+ credentials,
+ older: true
+ })
}
}
}
diff --git a/src/components/notifications/notifications.scss b/src/components/notifications/notifications.scss
index 008530b4..a137ccd5 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -4,49 +4,30 @@
// a bit of a hack to allow scrolling below notifications
padding-bottom: 15em;
- .panel {
- background: $fallback--bg;
- background: var(--bg, $fallback--bg)
- }
-
- .panel-body {
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border)
- }
-
- .panel-heading {
- // force the text to stay centered, while keeping
- // the button in the right side of the panel heading
- position: relative;
- background: $fallback--btn;
- background: var(--btn, $fallback--btn);
- color: $fallback--fg;
- color: var(--fg, $fallback--fg);
- .read-button {
- position: absolute;
- right: 0.7em;
- height: 1.8em;
- line-height: 100%;
- }
- }
-
.unseen-count {
display: inline-block;
background-color: $fallback--cRed;
background-color: var(--cRed, $fallback--cRed);
text-shadow: 0px 0px 3px rgba(0, 0, 0, 0.5);
- min-width: 1.3em;
- border-radius: 1.3em;
- margin: 0 0.2em 0 -0.4em;
+ border-radius: 99px;
+ min-width: 22px;
+ max-width: 22px;
+ min-height: 22px;
+ max-height: 22px;
color: white;
- font-size: 0.9em;
+ font-size: 15px;
+ line-height: 22px;
text-align: center;
- line-height: 1.3em;
+ vertical-align: middle
+ }
+
+ .loadmore-error {
+ color: $fallback--fg;
+ color: var(--fg, $fallback--fg);
}
.unseen {
- border-left: 4px solid $fallback--cRed;
- border-left: 4px solid var(--cRed, $fallback--cRed);
+ box-shadow: inset 4px 0 0 var(--cRed, $fallback--cRed);
padding-left: 0;
}
}
@@ -55,8 +36,18 @@
box-sizing: border-box;
display: flex;
border-bottom: 1px solid;
- border-bottom-color: inherit;
- padding-left: 4px;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+
+ .broken-favorite {
+ border-radius: $fallback--tooltipRadius;
+ border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ background-color: $fallback--cAlertRed;
+ background-color: var(--cAlertRed, $fallback--cAlertRed);
+ padding: 2px .5em
+ }
.avatar-compact {
width: 32px;
@@ -71,7 +62,7 @@
}
}
- &:hover .animated.avatar {
+ &:hover .animated.avatar-compact {
canvas {
display: none;
}
@@ -147,6 +138,13 @@
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
}
.timeago {
float: right;
@@ -196,15 +194,4 @@
margin-bottom: 0.3em;
}
}
-
- // ugly as heck
- &:last-child {
- border-bottom: none;
- border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
- border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
- .status-el {
- border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
- border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
- }
- }
}
diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue
index 4fa6e925..7a4322f9 100644
--- a/src/components/notifications/notifications.vue
+++ b/src/components/notifications/notifications.vue
@@ -2,8 +2,13 @@
<div class="notifications">
<div class="panel panel-default">
<div class="panel-heading">
- <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
- {{$t('notifications.notifications')}}
+ <div class="title">
+ {{$t('notifications.notifications')}}
+ <span class="unseen-count" v-if="unseenCount">{{unseenCount}}</span>
+ </div>
+ <div @click.prevent class="loadmore-error alert error" v-if="error">
+ {{$t('timeline.error_fetching')}}
+ </div>
<button v-if="unseenCount" @click.prevent="markAsSeen" class="read-button">{{$t('notifications.read')}}</button>
</div>
<div class="panel-body">
@@ -11,6 +16,12 @@
<notification :notification="notification"></notification>
</div>
</div>
+ <div class="panel-footer">
+ <a href="#" v-on:click.prevent='fetchOlderNotifications()' v-if="!notifications.loading">
+ <div class="new-status-notification text-center panel-footer">{{$t('notifications.load_older')}}</div>
+ </a>
+ <div class="new-status-notification text-center panel-footer" v-else>...</div>
+ </div>
</div>
</div>
</template>
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 0597d652..d7f1ffb2 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -23,13 +23,19 @@ const PostStatusForm = {
props: [
'replyTo',
'repliedUser',
- 'attentions'
+ 'attentions',
+ 'messageScope',
+ 'subject'
],
components: {
MediaUpload
},
mounted () {
this.resize(this.$refs.textarea)
+
+ if (this.replyTo) {
+ this.$refs.textarea.focus()
+ }
},
data () {
const preset = this.$route.query.message
@@ -47,9 +53,12 @@ const PostStatusForm = {
posting: false,
highlighted: 0,
newStatus: {
+ spoilerText: this.subject,
status: statusText,
+ contentType: 'text/plain',
+ nsfw: false,
files: [],
- visibility: 'public'
+ visibility: this.messageScope || this.$store.state.users.currentUser.default_scope
},
caret: 0
}
@@ -67,7 +76,7 @@ const PostStatusForm = {
const firstchar = this.textAtCaret.charAt(0)
if (firstchar === '@') {
const matchedUsers = filter(this.users, (user) => (String(user.name + user.screen_name)).toUpperCase()
- .match(this.textAtCaret.slice(1).toUpperCase()))
+ .startsWith(this.textAtCaret.slice(1).toUpperCase()))
if (matchedUsers.length <= 0) {
return false
}
@@ -81,16 +90,16 @@ const PostStatusForm = {
}))
} else if (firstchar === ':') {
if (this.textAtCaret === ':') { return }
- const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.match(this.textAtCaret.slice(1)))
+ const matchedEmoji = filter(this.emoji.concat(this.customEmoji), (emoji) => emoji.shortcode.startsWith(this.textAtCaret.slice(1)))
if (matchedEmoji.length <= 0) {
return false
}
return map(take(matchedEmoji, 5), ({shortcode, image_url, utf}, index) => ({
- // eslint-disable-next-line camelcase
screen_name: `:${shortcode}:`,
name: '',
utf: utf || '',
- img: image_url,
+ // eslint-disable-next-line camelcase
+ img: utf ? '' : this.$store.state.config.server + image_url,
highlighted: index === this.highlighted
}))
} else {
@@ -130,6 +139,9 @@ const PostStatusForm = {
},
scopeOptionsEnabled () {
return this.$store.state.config.scopeOptionsEnabled
+ },
+ formattingOptionsEnabled () {
+ return this.$store.state.config.formattingOptionsEnabled
}
},
methods: {
@@ -199,15 +211,18 @@ const PostStatusForm = {
status: newStatus.status,
spoilerText: newStatus.spoilerText || null,
visibility: newStatus.visibility,
+ sensitive: newStatus.nsfw,
media: newStatus.files,
store: this.$store,
- inReplyToStatusId: this.replyTo
+ inReplyToStatusId: this.replyTo,
+ contentType: newStatus.contentType
}).then((data) => {
if (!data.error) {
this.newStatus = {
status: '',
files: [],
- visibility: newStatus.visibility
+ visibility: newStatus.visibility,
+ contentType: newStatus.contentType
}
this.$emit('posted')
let el = this.$el.querySelector('textarea')
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 802d51ed..42e9c65c 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -2,6 +2,14 @@
<div class="post-status-form">
<form @submit.prevent="postStatus(newStatus)">
<div class="form-group" >
+ <i18n
+ v-if="!this.$store.state.users.currentUser.locked && this.newStatus.visibility == 'private'"
+ path="post_status.account_not_locked_warning"
+ tag="p"
+ class="visibility-notice">
+ <router-link to="/user-settings">{{ $t('post_status.account_not_locked_warning_link') }}</router-link>
+ </i18n>
+ <p v-if="this.newStatus.visibility == 'direct'" class="visibility-notice">{{ $t('post_status.direct_warning') }}</p>
<input
v-if="scopeOptionsEnabled"
type="text"
@@ -24,11 +32,24 @@
@input="resize"
@paste="paste">
</textarea>
- <div v-if="scopeOptionsEnabled" class="visibility-tray">
- <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i>
- <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i>
- <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i>
- <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i>
+ <div class="visibility-tray">
+ <span class="text-format" v-if="formattingOptionsEnabled">
+ <label for="post-content-type" class="select">
+ <select id="post-content-type" v-model="newStatus.contentType" class="form-control">
+ <option value="text/plain">{{$t('post_status.content_type.plain_text')}}</option>
+ <option value="text/html">HTML</option>
+ <option value="text/markdown">Markdown</option>
+ </select>
+ <i class="icon-down-open"></i>
+ </label>
+ </span>
+
+ <div v-if="scopeOptionsEnabled">
+ <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct" :title="$t('post_status.scope.direct')"></i>
+ <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private" :title="$t('post_status.scope.private')"></i>
+ <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted" :title="$t('post_status.scope.unlisted')"></i>
+ <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public" :title="$t('post_status.scope.public')"></i>
+ </div>
</div>
</div>
<div style="position:relative;" v-if="candidates">
@@ -57,14 +78,20 @@
<i class="icon-cancel" @click="clearError"></i>
</div>
<div class="attachments">
- <div class="media-upload-container attachment" v-for="file in newStatus.files">
+ <div class="media-upload-wrapper" v-for="file in newStatus.files">
<i class="fa icon-cancel" @click="removeMediaFile(file)"></i>
- <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
- <video v-if="type(file) === 'video'" :src="file.image" controls></video>
- <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
- <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ <div class="media-upload-container attachment">
+ <img class="thumbnail media-upload" :src="file.image" v-if="type(file) === 'image'"></img>
+ <video v-if="type(file) === 'video'" :src="file.image" controls></video>
+ <audio v-if="type(file) === 'audio'" :src="file.image" controls></audio>
+ <a v-if="type(file) === 'unknown'" :href="file.image">{{file.url}}</a>
+ </div>
</div>
</div>
+ <div class="upload_settings" v-if="newStatus.files.length > 0">
+ <input type="checkbox" id="filesSensitive" v-model="newStatus.nsfw">
+ <label for="filesSensitive">{{$t('post_status.attachments_sensitive')}}</label>
+ </div>
</form>
</div>
</template>
@@ -91,17 +118,6 @@
}
}
-.post-status-form .visibility-tray {
- font-size: 1.2em;
- padding: 3px;
- cursor: pointer;
-
- .selected {
- color: $fallback--lightFg;
- color: var(--lightFg, $fallback--lightFg);
- }
-}
-
.post-status-form, .login {
.form-bottom {
display: flex;
@@ -123,14 +139,49 @@
text-align: center;
}
+ .media-upload-wrapper {
+ flex: 0 0 auto;
+ max-width: 100%;
+ min-width: 50px;
+ margin-right: .2em;
+ margin-bottom: .5em;
+
+ .icon-cancel {
+ display: inline-block;
+ position: static;
+ margin: 0;
+ padding-bottom: 0;
+ margin-left: $fallback--attachmentRadius;
+ margin-left: var(--attachmentRadius, $fallback--attachmentRadius);
+ background-color: $fallback--btn;
+ background-color: var(--btn, $fallback--btn);
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ }
+ }
+
.attachments {
padding: 0 0.5em;
.attachment {
+ margin: 0;
position: relative;
+ flex: 0 0 auto;
border: 1px solid $fallback--border;
border: 1px solid var(--border, $fallback--border);
- margin: 0.5em 0.8em 0.2em 0;
+ text-align: center;
+
+ audio {
+ min-width: 300px;
+ flex: 1 0 auto;
+ }
+
+ a {
+ display: block;
+ text-align: left;
+ line-height: 1.2;
+ padding: .5em;
+ }
}
i {
diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js
index 771b3b27..73840608 100644
--- a/src/components/registration/registration.js
+++ b/src/components/registration/registration.js
@@ -5,17 +5,23 @@ const registration = {
registering: false
}),
created () {
- if (!this.$store.state.config.registrationOpen || !!this.$store.state.users.currentUser) {
+ if ((!this.$store.state.config.registrationOpen && !this.token) || !!this.$store.state.users.currentUser) {
this.$router.push('/main/all')
}
+ // Seems like this doesn't work at first page open for some reason
+ if (this.$store.state.config.registrationOpen && this.token) {
+ this.$router.push('/registration')
+ }
},
computed: {
- termsofservice () { return this.$store.state.config.tos }
+ termsofservice () { return this.$store.state.config.tos },
+ token () { return this.$route.params.token }
},
methods: {
submit () {
this.registering = true
this.user.nickname = this.user.username
+ this.user.token = this.token
this.$store.state.api.backendInteractor.register(this.user).then(
(response) => {
if (response.ok) {
diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue
index 00f665af..087cab6b 100644
--- a/src/components/registration/registration.vue
+++ b/src/components/registration/registration.vue
@@ -38,6 +38,10 @@
<input :disabled="registering" v-model='user.captcha' placeholder='Enter captcha' type='test' class='form-control' id='captcha'>
</div>
-->
+ <div class='form-group' v-if='token' >
+ <label for='token'>{{$t('registration.token')}}</label>
+ <input disabled='true' v-model='token' class='form-control' id='token' type='text'>
+ </div>
<div class='form-group'>
<button :disabled="registering" type='submit' class='btn btn-default'>{{$t('general.submit')}}</button>
</div>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index 4a43542d..cafa9cbc 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,5 +1,5 @@
const RetweetButton = {
- props: ['status', 'loggedIn'],
+ props: ['status', 'loggedIn', 'visibility'],
data () {
return {
animated: false
@@ -9,6 +9,8 @@ const RetweetButton = {
retweet () {
if (!this.status.repeated) {
this.$store.dispatch('retweet', {id: this.status.id})
+ } else {
+ this.$store.dispatch('unretweet', {id: this.status.id})
}
this.animated = true
setTimeout(() => {
@@ -20,6 +22,7 @@ const RetweetButton = {
classes () {
return {
'retweeted': this.status.repeated,
+ 'retweeted-empty': !this.status.repeated,
'animate-spin': this.animated
}
}
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index 1bee3d08..ee5722bd 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,9 +1,14 @@
<template>
<div v-if="loggedIn">
- <i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i>
- <span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
+ <template v-if="visibility !== 'private' && visibility !== 'direct'">
+ <i :class='classes' class='icon-retweet rt-active' v-on:click.prevent='retweet()'></i>
+ <span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
+ </template>
+ <template v-else>
+ <i :class='classes' class='icon-lock' :title="$t('timeline.no_retweet_hint')"></i>
+ </template>
</div>
- <div v-else>
+ <div v-else-if="!loggedIn">
<i :class='classes' class='icon-retweet'></i>
<span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
</div>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index a26111d6..8ef84b2a 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -1,21 +1,43 @@
+/* eslint-env browser */
+import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
+import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import { filter, trim } from 'lodash'
const settings = {
data () {
+ const config = this.$store.state.config
+
return {
- hideAttachmentsLocal: this.$store.state.config.hideAttachments,
- hideAttachmentsInConvLocal: this.$store.state.config.hideAttachmentsInConv,
- hideNsfwLocal: this.$store.state.config.hideNsfw,
- muteWordsString: this.$store.state.config.muteWords.join('\n'),
- autoLoadLocal: this.$store.state.config.autoLoad,
- streamingLocal: this.$store.state.config.streaming,
- hoverPreviewLocal: this.$store.state.config.hoverPreview,
- stopGifs: this.$store.state.config.stopGifs
+ hideAttachmentsLocal: config.hideAttachments,
+ hideAttachmentsInConvLocal: config.hideAttachmentsInConv,
+ hideNsfwLocal: config.hideNsfw,
+ notificationVisibilityLocal: config.notificationVisibility,
+ replyVisibilityLocal: config.replyVisibility,
+ loopVideoLocal: config.loopVideo,
+ loopVideoSilentOnlyLocal: config.loopVideoSilentOnly,
+ muteWordsString: config.muteWords.join('\n'),
+ autoLoadLocal: config.autoLoad,
+ streamingLocal: config.streaming,
+ pauseOnUnfocusedLocal: config.pauseOnUnfocused,
+ hoverPreviewLocal: config.hoverPreview,
+ collapseMessageWithSubjectLocal: typeof config.collapseMessageWithSubject === 'undefined'
+ ? config.defaultCollapseMessageWithSubject
+ : config.collapseMessageWithSubject,
+ stopGifs: config.stopGifs,
+ loopSilentAvailable:
+ // Firefox
+ Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
+ // Chrome-likes
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
+ // Future spec, still not supported in Nightly 63 as of 08/2018
+ Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks')
}
},
components: {
- StyleSwitcher
+ TabSwitcher,
+ StyleSwitcher,
+ InterfaceLanguageSwitcher
},
computed: {
user () {
@@ -32,12 +54,36 @@ const settings = {
hideNsfwLocal (value) {
this.$store.dispatch('setOption', { name: 'hideNsfw', value })
},
+ 'notificationVisibilityLocal.likes' (value) {
+ this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
+ },
+ 'notificationVisibilityLocal.follows' (value) {
+ this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
+ },
+ 'notificationVisibilityLocal.repeats' (value) {
+ this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
+ },
+ 'notificationVisibilityLocal.mentions' (value) {
+ this.$store.dispatch('setOption', { name: 'notificationVisibility', value: this.$store.state.config.notificationVisibility })
+ },
+ replyVisibilityLocal (value) {
+ this.$store.dispatch('setOption', { name: 'replyVisibility', value })
+ },
+ loopVideoLocal (value) {
+ this.$store.dispatch('setOption', { name: 'loopVideo', value })
+ },
+ loopVideoSilentOnlyLocal (value) {
+ this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value })
+ },
autoLoadLocal (value) {
this.$store.dispatch('setOption', { name: 'autoLoad', value })
},
streamingLocal (value) {
this.$store.dispatch('setOption', { name: 'streaming', value })
},
+ pauseOnUnfocusedLocal (value) {
+ this.$store.dispatch('setOption', { name: 'pauseOnUnfocused', value })
+ },
hoverPreviewLocal (value) {
this.$store.dispatch('setOption', { name: 'hoverPreview', value })
},
@@ -45,6 +91,9 @@ const settings = {
value = filter(value.split('\n'), (word) => trim(word).length > 0)
this.$store.dispatch('setOption', { name: 'muteWords', value })
},
+ collapseMessageWithSubjectLocal (value) {
+ this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
+ },
stopGifs (value) {
this.$store.dispatch('setOption', { name: 'stopGifs', value })
}
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index b4514ba1..c106b79c 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -1,53 +1,137 @@
<template>
- <div class="settings panel panel-default">
- <div class="panel-heading">
- {{$t('settings.settings')}}
- </div>
- <div class="panel-body">
- <div class="setting-item">
- <h2>{{$t('settings.theme')}}</h2>
- <style-switcher></style-switcher>
- </div>
- <div class="setting-item">
- <h2>{{$t('settings.filtering')}}</h2>
- <p>{{$t('settings.filtering_explanation')}}</p>
- <textarea id="muteWords" v-model="muteWordsString"></textarea>
- </div>
- <div class="setting-item">
- <h2>{{$t('settings.attachments')}}</h2>
- <ul class="setting-list">
+<div class="settings panel panel-default">
+ <div class="panel-heading">
+ {{$t('settings.settings')}}
+ </div>
+ <div class="panel-body">
+ <tab-switcher>
+ <div :label="$t('settings.general')" >
+ <div class="setting-item">
+ <h2>{{ $t('settings.interfaceLanguage') }}</h2>
+ <interface-language-switcher />
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('nav.timeline')}}</h2>
+ <ul class="setting-list">
+ <li>
+ <input type="checkbox" id="collapseMessageWithSubject" v-model="collapseMessageWithSubjectLocal">
+ <label for="collapseMessageWithSubject">{{$t('settings.collapse_subject')}}</label>
+ </li>
+ <li>
+ <input type="checkbox" id="streaming" v-model="streamingLocal">
+ <label for="streaming">{{$t('settings.streaming')}}</label>
+ <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
+ <li>
+ <input :disabled="!streamingLocal" type="checkbox" id="pauseOnUnfocused" v-model="pauseOnUnfocusedLocal">
+ <label for="pauseOnUnfocused">{{$t('settings.pause_on_unfocused')}}</label>
+ </li>
+ </ul>
+ </li>
<li>
- <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
- <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
+ <input type="checkbox" id="autoload" v-model="autoLoadLocal">
+ <label for="autoload">{{$t('settings.autoload')}}</label>
</li>
<li>
- <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
- <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
+ <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
+ <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
</li>
+ </ul>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('settings.attachments')}}</h2>
+ <ul class="setting-list">
<li>
- <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
- <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
+ <input type="checkbox" id="hideAttachments" v-model="hideAttachmentsLocal">
+ <label for="hideAttachments">{{$t('settings.hide_attachments_in_tl')}}</label>
</li>
<li>
- <input type="checkbox" id="autoload" v-model="autoLoadLocal">
- <label for="autoload">{{$t('settings.autoload')}}</label>
+ <input type="checkbox" id="hideAttachmentsInConv" v-model="hideAttachmentsInConvLocal">
+ <label for="hideAttachmentsInConv">{{$t('settings.hide_attachments_in_convo')}}</label>
</li>
<li>
- <input type="checkbox" id="streaming" v-model="streamingLocal">
- <label for="streaming">{{$t('settings.streaming')}}</label>
+ <input type="checkbox" id="hideNsfw" v-model="hideNsfwLocal">
+ <label for="hideNsfw">{{$t('settings.nsfw_clickthrough')}}</label>
</li>
<li>
- <input type="checkbox" id="hoverPreview" v-model="hoverPreviewLocal">
- <label for="hoverPreview">{{$t('settings.reply_link_preview')}}</label>
+ <input type="checkbox" id="stopGifs" v-model="stopGifs">
+ <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
</li>
<li>
- <input type="checkbox" id="stopGifs" v-model="stopGifs">
- <label for="stopGifs">{{$t('settings.stop_gifs')}}</label>
+ <input type="checkbox" id="loopVideo" v-model="loopVideoLocal">
+ <label for="loopVideo">{{$t('settings.loop_video')}}</label>
+ <ul class="setting-list suboptions" :class="[{disabled: !streamingLocal}]">
+ <li>
+ <input :disabled="!loopVideoLocal || !loopSilentAvailable" type="checkbox" id="loopVideoSilentOnly" v-model="loopVideoSilentOnlyLocal">
+ <label for="loopVideoSilentOnly">{{$t('settings.loop_video_silent_only')}}</label>
+ <div v-if="!loopSilentAvailable" class="unavailable">
+ <i class="icon-globe"/>! {{$t('settings.limited_availability')}}
+ </div>
+ </li>
+ </ul>
</li>
- </ul>
+ </ul>
+ </div>
</div>
- </div>
+
+ <div :label="$t('settings.theme')" >
+ <div class="setting-item">
+ <style-switcher></style-switcher>
+ </div>
+ </div>
+
+ <div :label="$t('settings.filtering')" >
+ <div class="setting-item">
+ <div class="select-multiple">
+ <span class="label">{{$t('settings.notification_visibility')}}</span>
+ <ul class="option-list">
+ <li>
+ <input type="checkbox" id="notification-visibility-likes" v-model="notificationVisibilityLocal.likes">
+ <label for="notification-visibility-likes">
+ {{$t('settings.notification_visibility_likes')}}
+ </label>
+ </li>
+ <li>
+ <input type="checkbox" id="notification-visibility-repeats" v-model="notificationVisibilityLocal.repeats">
+ <label for="notification-visibility-repeats">
+ {{$t('settings.notification_visibility_repeats')}}
+ </label>
+ </li>
+ <li>
+ <input type="checkbox" id="notification-visibility-follows" v-model="notificationVisibilityLocal.follows">
+ <label for="notification-visibility-follows">
+ {{$t('settings.notification_visibility_follows')}}
+ </label>
+ </li>
+ <li>
+ <input type="checkbox" id="notification-visibility-mentions" v-model="notificationVisibilityLocal.mentions">
+ <label for="notification-visibility-mentions">
+ {{$t('settings.notification_visibility_mentions')}}
+ </label>
+ </li>
+ </ul>
+ </label>
+ </div>
+ <div>
+ {{$t('settings.replies_in_timeline')}}
+ <label for="replyVisibility" class="select">
+ <select id="replyVisibility" v-model="replyVisibilityLocal">
+ <option value="all" selected>{{$t('settings.reply_visibility_all')}}</option>
+ <option value="following">{{$t('settings.reply_visibility_following')}}</option>
+ <option value="self">{{$t('settings.reply_visibility_self')}}</option>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+ </div>
+ <div class="setting-item">
+ <p>{{$t('settings.filtering_explanation')}}</p>
+ <textarea id="muteWords" v-model="muteWordsString"></textarea>
+ </div>
+ </div>
+
+ </tab-switcher>
</div>
+</div>
</template>
<script src="./settings.js">
@@ -57,13 +141,39 @@
@import '../../_variables.scss';
.setting-item {
+ border-bottom: 2px solid var(--btn, $fallback--btn);
margin: 1em 1em 1.4em;
+ padding-bottom: 1.4em;
+
+ > div {
+ margin-bottom: .5em;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ margin-bottom: 1em;
+ }
+
+ select {
+ min-width: 10em;
+ }
+
textarea {
width: 100%;
height: 100px;
}
+ .unavailable,
+ .unavailable i {
+ color: var(--cRed, $fallback--cRed);
+ color: $fallback--cRed;
+ }
+
.old-avatar {
width: 128px;
border-radius: $fallback--avatarRadius;
@@ -79,15 +189,31 @@
}
.btn {
- margin-top: 1em;
min-height: 28px;
+ }
+
+ .submit {
+ margin-top: 1em;
+ min-height: 30px;
width: 10em;
}
}
-.setting-list {
+.select-multiple {
+ display: flex;
+ .option-list {
+ margin: 0;
+ padding-left: .5em;
+ }
+}
+.setting-list,
+.option-list{
list-style-type: none;
+ padding-left: 2em;
li {
margin-bottom: 0.5em;
}
+ .suboptions {
+ margin-top: 0.3em
+ }
}
</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 87ef90d8..45f5ccac 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -6,6 +6,7 @@ import PostStatusForm from '../post_status_form/post_status_form.vue'
import UserCardContent from '../user_card_content/user_card_content.vue'
import StillImage from '../still-image/still-image.vue'
import { filter, find } from 'lodash'
+import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
const Status = {
name: 'Status',
@@ -21,25 +22,48 @@ const Status = {
'noHeading',
'inlineExpanded'
],
- data: () => ({
- replying: false,
- expanded: false,
- unmuted: false,
- userExpanded: false,
- preview: null,
- showPreview: false,
- showingTall: false
- }),
+ data () {
+ return {
+ replying: false,
+ expanded: false,
+ unmuted: false,
+ userExpanded: false,
+ preview: null,
+ showPreview: false,
+ showingTall: false,
+ expandingSubject: !this.$store.state.config.collapseMessageWithSubject
+ }
+ },
computed: {
muteWords () {
return this.$store.state.config.muteWords
},
+ repeaterClass () {
+ const user = this.statusoid.user
+ return highlightClass(user)
+ },
+ userClass () {
+ const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
+ return highlightClass(user)
+ },
+ repeaterStyle () {
+ const user = this.statusoid.user
+ const highlight = this.$store.state.config.highlight
+ return highlightStyle(highlight[user.screen_name])
+ },
+ userStyle () {
+ if (this.noHeading) return
+ const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
+ const highlight = this.$store.state.config.highlight
+ return highlightStyle(highlight[user.screen_name])
+ },
hideAttachments () {
return (this.$store.state.config.hideAttachments && !this.inConversation) ||
(this.$store.state.config.hideAttachmentsInConv && this.inConversation)
},
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name },
+ retweeterHtml () { return this.statusoid.user.name_html },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
@@ -59,7 +83,6 @@ const Status = {
return hits
},
muted () { return !this.unmuted && (this.status.user.muted || this.muteWordHits.length > 0) },
- isReply () { return !!this.status.in_reply_to_status_id },
isFocused () {
// retweet or root of an expanded conversation
if (this.focused) {
@@ -77,12 +100,84 @@ const Status = {
//
// Using max-height + overflow: auto for status components resulted in false positives
// very often with japanese characters, and it was very annoying.
+ tallStatus () {
+ const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
+ return lengthScore > 20
+ },
+ isReply () {
+ if (this.status.in_reply_to_status_id) {
+ return true
+ }
+ // For private replies where we can't see the OP, in_reply_to_status_id will be null.
+ // So instead, check that the post starts with a @mention.
+ if (this.status.visibility === 'private') {
+ var textBody = this.status.text
+ if (this.status.summary !== null) {
+ textBody = textBody.substring(this.status.summary.length, textBody.length)
+ }
+ return textBody.startsWith('@')
+ }
+ return false
+ },
+ hideReply () {
+ if (this.$store.state.config.replyVisibility === 'all') {
+ return false
+ }
+ if (this.inlineExpanded || this.expanded || this.inConversation || !this.isReply) {
+ return false
+ }
+ if (this.status.user.id === this.$store.state.users.currentUser.id) {
+ return false
+ }
+ if (this.status.activity_type === 'repeat') {
+ return false
+ }
+ var checkFollowing = this.$store.state.config.replyVisibility === 'following'
+ for (var i = 0; i < this.status.attentions.length; ++i) {
+ if (this.status.user.id === this.status.attentions[i].id) {
+ continue
+ }
+ if (checkFollowing && this.status.attentions[i].following) {
+ return false
+ }
+ if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
+ return false
+ }
+ }
+ return this.status.attentions.length > 0
+ },
+ hideSubjectStatus () {
+ if (this.tallStatus && !this.$store.state.config.collapseMessageWithSubject) {
+ return false
+ }
+ return !this.expandingSubject && this.status.summary
+ },
hideTallStatus () {
+ if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
+ return false
+ }
if (this.showingTall) {
return false
}
- const lengthScore = this.status.statusnet_html.split(/<p|<br/).length + this.status.text.length / 80
- return lengthScore > 20
+ return this.tallStatus
+ },
+ showingMore () {
+ return this.showingTall || (this.status.summary && this.expandingSubject)
+ },
+ nsfwClickthrough () {
+ if (!this.status.nsfw) {
+ return false
+ }
+ if (this.status.summary && this.$store.state.config.collapseMessageWithSubject) {
+ return false
+ }
+ return true
+ },
+ replySubject () {
+ if (this.status.summary && !this.status.summary.match(/^re[: ]/i)) {
+ return 're: '.concat(this.status.summary)
+ }
+ return this.status.summary
},
attachmentSize () {
if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
@@ -142,8 +237,16 @@ const Status = {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
},
- toggleShowTall () {
- this.showingTall = !this.showingTall
+ toggleShowMore () {
+ if (this.showingTall) {
+ this.showingTall = false
+ } else if (this.expandingSubject) {
+ this.expandingSubject = false
+ } else if (this.hideTallStatus) {
+ this.showingTall = true
+ } else if (this.hideSubjectStatus) {
+ this.expandingSubject = true
+ }
},
replyEnter (id, event) {
this.showPreview = true
@@ -179,6 +282,11 @@ const Status = {
}
}
}
+ },
+ filters: {
+ capitalize: function (str) {
+ return str.charAt(0).toUpperCase() + str.slice(1)
+ }
}
}
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index ace141cd..eb521280 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,5 +1,5 @@
<template>
- <div class="status-el" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
+ <div class="status-el" v-if="!hideReply" :class="[{ 'status-el_focused': isFocused }, { 'status-conversation': inlineExpanded }]">
<template v-if="muted && !noReplyLinks">
<div class="media status container muted">
<small><router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link></small>
@@ -8,16 +8,17 @@
</div>
</template>
<template v-else>
- <div v-if="retweet && !noHeading" class="media container retweet-info">
+ <div v-if="retweet && !noHeading" :class="[repeaterClass, { highlighted: repeaterStyle }]" :style="[repeaterStyle]" class="media container retweet-info">
<StillImage v-if="retweet" class='avatar' :src="statusoid.user.profile_image_url_original"/>
<div class="media-body faint">
- <a :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
+ <a v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
+ <a v-else :href="statusoid.user.statusnet_profile_url" class="user-name" :title="'@'+statusoid.user.screen_name">{{retweeter}}</a>
<i class='fa icon-retweet retweeted'></i>
{{$t('timeline.repeated')}}
</div>
</div>
- <div class="media status">
+ <div :class="[userClass, { highlighted: userStyle, 'is-retweet': retweet }]" :style="[ userStyle ]" class="media status">
<div v-if="!noHeading" class="media-left">
<a :href="status.user.statusnet_profile_url" @click.stop.prevent.capture="toggleUserExpanded">
<StillImage class='avatar' :class="{'avatar-compact': compact}" :src="status.user.profile_image_url_original"/>
@@ -30,7 +31,8 @@
<div v-if="!noHeading" class="media-body container media-heading">
<div class="media-heading-left">
<div class="name-and-links">
- <h4 class="user-name">{{status.user.name}}</h4>
+ <h4 class="user-name" v-if="status.user.name_html" v-html="status.user.name_html"></h4>
+ <h4 class="user-name" v-else>{{status.user.name}}</h4>
<span class="links">
<router-link :to="{ name: 'user-profile', params: { id: status.user.id } }">{{status.user.screen_name}}</router-link>
<span v-if="status.in_reply_to_screen_name" class="faint reply-info">
@@ -55,10 +57,16 @@
<router-link class="timeago" :to="{ name: 'conversation', params: { id: status.id } }">
<timeago :since="status.created_at" :auto-update="60"></timeago>
</router-link>
- <span v-if="status.visibility"><i :class="visibilityIcon(status.visibility)"></i> </span>
- <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url"><i class="icon-link-ext"></i></a>
+ <div class="visibility-icon" v-if="status.visibility">
+ <i :class="visibilityIcon(status.visibility)" :title="status.visibility | capitalize"></i>
+ </div>
+ <a :href="status.external_url" target="_blank" v-if="!status.is_local" class="source_url" title="Source">
+ <i class="icon-link-ext-alt"></i>
+ </a>
<template v-if="expandable">
- <a href="#" @click.prevent="toggleExpanded"><i class="icon-plus-squared"></i></a>
+ <a href="#" @click.prevent="toggleExpanded" title="Expand">
+ <i class="icon-plus-squared"></i>
+ </a>
</template>
<a href="#" @click.prevent="toggleMute" v-if="unmuted"><i class="icon-eye-off"></i></a>
</div>
@@ -72,13 +80,15 @@
</div>
<div :class="{'tall-status': hideTallStatus}" class="status-content-wrapper">
- <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowTall">Show more</a>
- <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html"></div>
- <a v-if="showingTall" href="#" class="tall-status-unhider" @click.prevent="toggleShowTall">Show less</a>
+ <a class="tall-status-hider" :class="{ 'tall-status-hider_focused': isFocused }" v-if="hideTallStatus" href="#" @click.prevent="toggleShowMore">Show more</a>
+ <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.statusnet_html" v-if="!hideSubjectStatus"></div>
+ <div @click.prevent="linkClicked" class="status-content media-body" v-html="status.summary" v-else></div>
+ <a v-if="hideSubjectStatus" href="#" class="cw-status-hider" @click.prevent="toggleShowMore">Show more</a>
+ <a v-if="showingMore" href="#" class="status-unhider" @click.prevent="toggleShowMore">Show less</a>
</div>
- <div v-if='status.attachments' class='attachments media-body'>
- <attachment :size="attachmentSize" :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id">
+ <div v-if='status.attachments && !hideSubjectStatus' class='attachments media-body'>
+ <attachment :size="attachmentSize" :status-id="status.id" :nsfw="nsfwClickthrough" :attachment="attachment" v-for="attachment in status.attachments" :key="attachment.id">
</attachment>
</div>
@@ -88,7 +98,7 @@
<i class="icon-reply" :class="{'icon-reply-active': replying}"></i>
</a>
</div>
- <retweet-button :loggedIn='loggedIn' :status='status'></retweet-button>
+ <retweet-button :visibility='status.visibility' :loggedIn='loggedIn' :status='status'></retweet-button>
<favorite-button :loggedIn='loggedIn' :status='status'></favorite-button>
<delete-button :status='status'></delete-button>
</div>
@@ -96,7 +106,7 @@
</div>
<div class="container" v-if="replying">
<div class="reply-left"/>
- <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"/>
+ <post-status-form class="reply-body" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" :message-scope="status.visibility" :subject="replySubject" v-on:posted="toggleReplying"/>
</div>
</template>
</div>
@@ -139,6 +149,7 @@
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
+
.status {
flex: 1;
border: 0;
@@ -153,6 +164,7 @@
text-align: center;
border-width: 1px;
border-style: solid;
+
i {
font-size: 2em;
}
@@ -194,6 +206,7 @@
.media-heading {
flex-wrap: nowrap;
+ line-height: 18px;
}
.media-heading-left {
@@ -216,12 +229,22 @@
flex: 1 0;
display: flex;
flex-wrap: wrap;
- align-content: center;
+ align-items: baseline;
+
+ .user-name {
+ margin-right: .45em;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
}
+
.links {
display: flex;
- padding-top: 1px;
- margin-left: 0.2em;
font-size: 12px;
color: $fallback--link;
color: var(--link, $fallback--link);
@@ -245,19 +268,25 @@
}
.media-heading-right {
+ display: inline-flex;
flex-shrink: 0;
- display: flex;
flex-wrap: nowrap;
- max-height: 1.5em;
- margin-left: 0.25em;
+ margin-left: .25em;
+ align-self: baseline;
+
.timeago {
margin-right: 0.2em;
font-size: 12px;
- padding-top: 1px;
+ align-self: last baseline;
}
- i {
+
+ > * {
margin-left: 0.2em;
}
+ a:hover i {
+ color: $fallback--fg;
+ color: var(--fg, $fallback--fg);
+ }
}
a {
@@ -287,7 +316,7 @@
}
}
- .tall-status-unhider {
+ .status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
}
@@ -306,16 +335,41 @@
font-style: italic;
}
+ pre {
+ overflow: auto;
+ }
+
p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
+
+ h1 {
+ font-size: 1.1em;
+ line-height: 1.2em;
+ margin: 1.4em 0;
+ }
+
+ h2 {
+ font-size: 1.1em;
+ margin: 1.0em 0;
+ }
+
+ h3 {
+ font-size: 1em;
+ margin: 1.2em 0;
+ }
+
+ h4 {
+ margin: 1.1em 0;
+ }
}
.retweet-info {
padding: 0.4em 0.6em 0 0.6em;
- margin: 0 0 -0.5em 0;
+ margin: 0;
+
.avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
@@ -331,9 +385,22 @@
display: flex;
align-content: center;
flex-wrap: wrap;
+
+ .user-name {
+ font-weight: bold;
+
+ img {
+ width: 14px;
+ height: 14px;
+ vertical-align: middle;
+ object-fit: contain
+ }
+ }
+
i {
padding: 0 0.2em;
}
+
a {
max-width: 100%;
overflow: hidden;
@@ -427,6 +494,9 @@
.status {
display: flex;
padding: 0.6em;
+ &.is-retweet {
+ padding-top: 0.1em;
+ }
}
.status-conversation:last-child {
diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index 0839aca5..5ad06dc2 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -18,7 +18,11 @@ const StillImage = {
onLoad () {
const canvas = this.$refs.canvas
if (!canvas) return
- canvas.getContext('2d').drawImage(this.$refs.src, 1, 1, canvas.width, canvas.height)
+ const width = this.$refs.src.naturalWidth
+ const height = this.$refs.src.naturalHeight
+ canvas.width = width
+ canvas.height = height
+ canvas.getContext('2d').drawImage(this.$refs.src, 0, 0, width, height)
}
}
}
diff --git a/src/components/still-image/still-image.vue b/src/components/still-image/still-image.vue
index a37c678d..1dcb7ce6 100644
--- a/src/components/still-image/still-image.vue
+++ b/src/components/still-image/still-image.vue
@@ -23,6 +23,7 @@
img {
width: 100%;
height: 100%;
+ object-fit: contain;
}
&.animated {
@@ -60,6 +61,7 @@
right: 0;
width: 100%;
height: 100%;
+ object-fit: contain;
}
}
</style>
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index 6f4845c4..95c15b49 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -5,6 +5,7 @@ export default {
return {
availableStyles: [],
selected: this.$store.state.config.theme,
+ invalidThemeImported: false,
bgColorLocal: '',
btnColorLocal: '',
textColorLocal: '',
@@ -32,25 +33,61 @@ export default {
})
},
mounted () {
- this.bgColorLocal = rgbstr2hex(this.$store.state.config.colors.bg)
- this.btnColorLocal = rgbstr2hex(this.$store.state.config.colors.btn)
- this.textColorLocal = rgbstr2hex(this.$store.state.config.colors.fg)
- this.linkColorLocal = rgbstr2hex(this.$store.state.config.colors.link)
-
- this.redColorLocal = rgbstr2hex(this.$store.state.config.colors.cRed)
- this.blueColorLocal = rgbstr2hex(this.$store.state.config.colors.cBlue)
- this.greenColorLocal = rgbstr2hex(this.$store.state.config.colors.cGreen)
- this.orangeColorLocal = rgbstr2hex(this.$store.state.config.colors.cOrange)
-
- this.btnRadiusLocal = this.$store.state.config.radii.btnRadius || 4
- this.inputRadiusLocal = this.$store.state.config.radii.inputRadius || 4
- this.panelRadiusLocal = this.$store.state.config.radii.panelRadius || 10
- this.avatarRadiusLocal = this.$store.state.config.radii.avatarRadius || 5
- this.avatarAltRadiusLocal = this.$store.state.config.radii.avatarAltRadius || 50
- this.tooltipRadiusLocal = this.$store.state.config.radii.tooltipRadius || 2
- this.attachmentRadiusLocal = this.$store.state.config.radii.attachmentRadius || 5
+ this.normalizeLocalState(this.$store.state.config.colors, this.$store.state.config.radii)
},
methods: {
+ exportCurrentTheme () {
+ const stringified = JSON.stringify({
+ // To separate from other random JSON files and possible future theme formats
+ _pleroma_theme_version: 1,
+ colors: this.$store.state.config.colors,
+ radii: this.$store.state.config.radii
+ }, null, 2) // Pretty-print and indent with 2 spaces
+
+ // Create an invisible link with a data url and simulate a click
+ const e = document.createElement('a')
+ e.setAttribute('download', 'pleroma_theme.json')
+ e.setAttribute('href', 'data:application/json;base64,' + window.btoa(stringified))
+ e.style.display = 'none'
+
+ document.body.appendChild(e)
+ e.click()
+ document.body.removeChild(e)
+ },
+
+ importTheme () {
+ this.invalidThemeImported = false
+ const filePicker = document.createElement('input')
+ filePicker.setAttribute('type', 'file')
+ filePicker.setAttribute('accept', '.json')
+
+ filePicker.addEventListener('change', event => {
+ if (event.target.files[0]) {
+ // eslint-disable-next-line no-undef
+ const reader = new FileReader()
+ reader.onload = ({target}) => {
+ try {
+ const parsed = JSON.parse(target.result)
+ if (parsed._pleroma_theme_version === 1) {
+ this.normalizeLocalState(parsed.colors, parsed.radii)
+ } else {
+ // A theme from the future, spooky
+ this.invalidThemeImported = true
+ }
+ } catch (e) {
+ // This will happen both if there is a JSON syntax error or the theme is missing components
+ this.invalidThemeImported = true
+ }
+ }
+ reader.readAsText(event.target.files[0])
+ }
+ })
+
+ document.body.appendChild(filePicker)
+ filePicker.click()
+ document.body.removeChild(filePicker)
+ },
+
setCustomTheme () {
if (!this.bgColorLocal && !this.btnColorLocal && !this.linkColorLocal) {
// reset to picked themes
@@ -95,6 +132,26 @@ export default {
attachmentRadius: this.attachmentRadiusLocal
}})
}
+ },
+
+ normalizeLocalState (colors, radii) {
+ this.bgColorLocal = rgbstr2hex(colors.bg)
+ this.btnColorLocal = rgbstr2hex(colors.btn)
+ this.textColorLocal = rgbstr2hex(colors.fg)
+ this.linkColorLocal = rgbstr2hex(colors.link)
+
+ this.redColorLocal = rgbstr2hex(colors.cRed)
+ this.blueColorLocal = rgbstr2hex(colors.cBlue)
+ this.greenColorLocal = rgbstr2hex(colors.cGreen)
+ this.orangeColorLocal = rgbstr2hex(colors.cOrange)
+
+ this.btnRadiusLocal = radii.btnRadius || 4
+ this.inputRadiusLocal = radii.inputRadius || 4
+ this.panelRadiusLocal = radii.panelRadius || 10
+ this.avatarRadiusLocal = radii.avatarRadius || 5
+ this.avatarAltRadiusLocal = radii.avatarAltRadius || 50
+ this.tooltipRadiusLocal = radii.tooltipRadius || 2
+ this.attachmentRadiusLocal = radii.attachmentRadius || 5
}
},
watch: {
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index 7acba1dc..72a338bd 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -1,94 +1,30 @@
<template>
- <div>
- <div>{{$t('settings.presets')}}
+<div>
+ <div class="presets-container">
+ <div>
+ {{$t('settings.presets')}}
<label for="style-switcher" class='select'>
<select id="style-switcher" v-model="selected" class="style-switcher">
- <option v-for="style in availableStyles" :value="style">{{style[0]}}</option>
+ <option v-for="style in availableStyles"
+ :value="style"
+ :style="{
+ backgroundColor: style[1],
+ color: style[3]
+ }">
+ {{style[0]}}
+ </option>
</select>
<i class="icon-down-open"/>
</label>
</div>
- <div class="color-container">
- <p>{{$t('settings.theme_help')}}</p>
- <div class="color-item">
- <label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label>
- <input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal">
- <input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal">
- </div>
- <div class="color-item">
- <label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label>
- <input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal">
- <input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal">
- </div>
- <div class="color-item">
- <label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label>
- <input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal">
- <input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal">
- </div>
- <div class="color-item">
- <label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label>
- <input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal">
- <input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal">
- </div>
- <div class="color-item">
- <label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label>
- <input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal">
- <input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal">
- </div>
- <div class="color-item">
- <label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label>
- <input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal">
- <input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal">
- </div>
- <div class="color-item">
- <label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label>
- <input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal">
- <input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal">
- </div>
- <div class="color-item">
- <label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label>
- <input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal">
- <input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal">
- </div>
- </div>
- <div class="radius-container">
- <p>{{$t('settings.radii_help')}}</p>
- <div class="radius-item">
- <label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label>
- <input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16">
- <input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal">
- </div>
- <div class="radius-item">
- <label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label>
- <input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16">
- <input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal">
- </div>
- <div class="radius-item">
- <label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label>
- <input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50">
- <input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal">
- </div>
- <div class="radius-item">
- <label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label>
- <input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28">
- <input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal">
- </div>
- <div class="radius-item">
- <label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label>
- <input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28">
- <input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal">
- </div>
- <div class="radius-item">
- <label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label>
- <input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50">
- <input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal">
- </div>
- <div class="radius-item">
- <label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label>
- <input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20">
- <input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal">
- </div>
+ <div class="import-export">
+ <button class="btn" @click="exportCurrentTheme">{{ $t('settings.export_theme') }}</button>
+ <button class="btn" @click="importTheme">{{ $t('settings.import_theme') }}</button>
+ <p v-if="invalidThemeImported" class="import-warning">{{ $t('settings.invalid_theme_imported') }}</p>
</div>
+ </div>
+
+ <div class="preview-container">
<div :style="{
'--btnRadius': btnRadiusLocal + 'px',
'--inputRadius': inputRadiusLocal + 'px',
@@ -119,8 +55,95 @@
</div>
</div>
</div>
- <button class="btn" @click="setCustomTheme">{{$t('general.apply')}}</button>
</div>
+
+ <div class="color-container">
+ <p>{{$t('settings.theme_help')}}</p>
+ <div class="color-item">
+ <label for="bgcolor" class="theme-color-lb">{{$t('settings.background')}}</label>
+ <input id="bgcolor" class="theme-color-cl" type="color" v-model="bgColorLocal">
+ <input id="bgcolor-t" class="theme-color-in" type="text" v-model="bgColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="fgcolor" class="theme-color-lb">{{$t('settings.foreground')}}</label>
+ <input id="fgcolor" class="theme-color-cl" type="color" v-model="btnColorLocal">
+ <input id="fgcolor-t" class="theme-color-in" type="text" v-model="btnColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="textcolor" class="theme-color-lb">{{$t('settings.text')}}</label>
+ <input id="textcolor" class="theme-color-cl" type="color" v-model="textColorLocal">
+ <input id="textcolor-t" class="theme-color-in" type="text" v-model="textColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="linkcolor" class="theme-color-lb">{{$t('settings.links')}}</label>
+ <input id="linkcolor" class="theme-color-cl" type="color" v-model="linkColorLocal">
+ <input id="linkcolor-t" class="theme-color-in" type="text" v-model="linkColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="redcolor" class="theme-color-lb">{{$t('settings.cRed')}}</label>
+ <input id="redcolor" class="theme-color-cl" type="color" v-model="redColorLocal">
+ <input id="redcolor-t" class="theme-color-in" type="text" v-model="redColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="bluecolor" class="theme-color-lb">{{$t('settings.cBlue')}}</label>
+ <input id="bluecolor" class="theme-color-cl" type="color" v-model="blueColorLocal">
+ <input id="bluecolor-t" class="theme-color-in" type="text" v-model="blueColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="greencolor" class="theme-color-lb">{{$t('settings.cGreen')}}</label>
+ <input id="greencolor" class="theme-color-cl" type="color" v-model="greenColorLocal">
+ <input id="greencolor-t" class="theme-color-in" type="green" v-model="greenColorLocal">
+ </div>
+ <div class="color-item">
+ <label for="orangecolor" class="theme-color-lb">{{$t('settings.cOrange')}}</label>
+ <input id="orangecolor" class="theme-color-cl" type="color" v-model="orangeColorLocal">
+ <input id="orangecolor-t" class="theme-color-in" type="text" v-model="orangeColorLocal">
+ </div>
+ </div>
+
+ <div class="radius-container">
+ <p>{{$t('settings.radii_help')}}</p>
+ <div class="radius-item">
+ <label for="btnradius" class="theme-radius-lb">{{$t('settings.btnRadius')}}</label>
+ <input id="btnradius" class="theme-radius-rn" type="range" v-model="btnRadiusLocal" max="16">
+ <input id="btnradius-t" class="theme-radius-in" type="text" v-model="btnRadiusLocal">
+ </div>
+ <div class="radius-item">
+ <label for="inputradius" class="theme-radius-lb">{{$t('settings.inputRadius')}}</label>
+ <input id="inputradius" class="theme-radius-rn" type="range" v-model="inputRadiusLocal" max="16">
+ <input id="inputradius-t" class="theme-radius-in" type="text" v-model="inputRadiusLocal">
+ </div>
+ <div class="radius-item">
+ <label for="panelradius" class="theme-radius-lb">{{$t('settings.panelRadius')}}</label>
+ <input id="panelradius" class="theme-radius-rn" type="range" v-model="panelRadiusLocal" max="50">
+ <input id="panelradius-t" class="theme-radius-in" type="text" v-model="panelRadiusLocal">
+ </div>
+ <div class="radius-item">
+ <label for="avatarradius" class="theme-radius-lb">{{$t('settings.avatarRadius')}}</label>
+ <input id="avatarradius" class="theme-radius-rn" type="range" v-model="avatarRadiusLocal" max="28">
+ <input id="avatarradius-t" class="theme-radius-in" type="green" v-model="avatarRadiusLocal">
+ </div>
+ <div class="radius-item">
+ <label for="avataraltradius" class="theme-radius-lb">{{$t('settings.avatarAltRadius')}}</label>
+ <input id="avataraltradius" class="theme-radius-rn" type="range" v-model="avatarAltRadiusLocal" max="28">
+ <input id="avataraltradius-t" class="theme-radius-in" type="text" v-model="avatarAltRadiusLocal">
+ </div>
+ <div class="radius-item">
+ <label for="attachmentradius" class="theme-radius-lb">{{$t('settings.attachmentRadius')}}</label>
+ <input id="attachmentrradius" class="theme-radius-rn" type="range" v-model="attachmentRadiusLocal" max="50">
+ <input id="attachmentradius-t" class="theme-radius-in" type="text" v-model="attachmentRadiusLocal">
+ </div>
+ <div class="radius-item">
+ <label for="tooltipradius" class="theme-radius-lb">{{$t('settings.tooltipRadius')}}</label>
+ <input id="tooltipradius" class="theme-radius-rn" type="range" v-model="tooltipRadiusLocal" max="20">
+ <input id="tooltipradius-t" class="theme-radius-in" type="text" v-model="tooltipRadiusLocal">
+ </div>
+ </div>
+
+ <div class="apply-container">
+ <button class="btn submit" @click="setCustomTheme">{{$t('general.apply')}}</button>
+ </div>
+</div>
</template>
<script src="./style_switcher.js"></script>
@@ -131,15 +154,24 @@
margin-right: 1em;
}
+.import-warning {
+ color: $fallback--cRed;
+ color: var(--cRed, $fallback--cRed);
+}
+
+.apply-container,
.radius-container,
-.color-container {
+.color-container,
+.presets-container {
display: flex;
p {
+ flex: 2 0 100%;
margin-top: 2em;
margin-bottom: .5em;
}
}
+
.radius-container {
flex-direction: column;
}
@@ -149,6 +181,36 @@
justify-content: space-between;
}
+.presets-container {
+ justify-content: center;
+ .import-export {
+ display: flex;
+
+ .btn {
+ margin-left: .5em;
+ }
+ }
+}
+
+.preview-container {
+ border-top: 1px dashed;
+ border-bottom: 1px dashed;
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ margin: 1em -1em 0;
+ padding: 1em;
+
+ .btn {
+ margin-top: 1em;
+ min-height: 30px;
+ width: 10em;
+ }
+}
+
+.apply-container {
+ justify-content: center;
+}
+
.radius-item,
.color-item {
min-width: 20em;
@@ -216,6 +278,7 @@
flex: 0;
min-width: 2em;
cursor: pointer;
+ max-height: 29px;
}
.theme-preview-content {
diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.jsx
new file mode 100644
index 00000000..3fff38f6
--- /dev/null
+++ b/src/components/tab_switcher/tab_switcher.jsx
@@ -0,0 +1,44 @@
+import Vue from 'vue'
+
+import './tab_switcher.scss'
+
+export default Vue.component('tab-switcher', {
+ name: 'TabSwitcher',
+ data () {
+ return {
+ active: 0
+ }
+ },
+ methods: {
+ activateTab(index) {
+ return () => this.active = index;
+ }
+ },
+ render(h) {
+ const tabs = this.$slots.default
+ .filter(slot => slot.data)
+ .map((slot, index) => {
+ const classes = ['tab']
+
+ if (index === this.active) {
+ classes.push('active')
+ }
+ return (<button onClick={this.activateTab(index)} class={ classes.join(' ') }>{slot.data.attrs.label}</button>)
+ });
+ const contents = (
+ <div>
+ {this.$slots.default.filter(slot => slot.data)[this.active]}
+ </div>
+ );
+ return (
+ <div class="tab-switcher">
+ <div class="tabs">
+ {tabs}
+ </div>
+ <div class="contents">
+ {contents}
+ </div>
+ </div>
+ )
+ }
+})
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
new file mode 100644
index 00000000..374a19c5
--- /dev/null
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -0,0 +1,43 @@
+@import '../../_variables.scss';
+
+.tab-switcher {
+ .tabs {
+ display: flex;
+ position: relative;
+ justify-content: center;
+ width: 100%;
+ overflow: hidden;
+ padding-top: 5px;
+
+ &::after, &::before {
+ display: block;
+ content: '';
+ flex: 1 1 auto;
+ }
+
+ .tab, &::after, &::before {
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--btn;
+ border-bottom-color: var(--btn, $fallback--btn);
+ }
+
+ .tab {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ padding: .3em 1em;
+
+ &:not(.active) {
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--btn;
+ border-bottom-color: var(--btn, $fallback--btn);
+ z-index: 4;
+ }
+
+ &.active {
+ background: transparent;
+ border-bottom: none;
+ z-index: 5;
+ }
+ }
+ }
+}
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index f24626f9..a651f619 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -13,7 +13,8 @@ const Timeline = {
],
data () {
return {
- paused: false
+ paused: false,
+ unfocused: false
}
},
computed: {
@@ -65,8 +66,15 @@ const Timeline = {
this.fetchFollowers()
}
},
+ mounted () {
+ if (typeof document.hidden !== 'undefined') {
+ document.addEventListener('visibilitychange', this.handleVisibilityChange, false)
+ this.unfocused = document.hidden
+ }
+ },
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
+ if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
},
methods: {
@@ -113,6 +121,9 @@ const Timeline = {
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses()
}
+ },
+ handleVisibilityChange () {
+ this.unfocused = document.hidden
}
},
watch: {
@@ -122,7 +133,10 @@ const Timeline = {
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
- if (window.pageYOffset < 15 && !this.paused) {
+ if (window.pageYOffset < 15 &&
+ !this.paused &&
+ !(this.unfocused && this.$store.state.config.pauseOnUnfocused)
+ ) {
this.showNewStatuses()
} else {
this.paused = true
diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue
index c4e0fbce..2dd4376a 100644
--- a/src/components/timeline/timeline.vue
+++ b/src/components/timeline/timeline.vue
@@ -4,12 +4,12 @@
<div class="title">
{{title}}
</div>
- <button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
- {{$t('timeline.show_new')}}{{newStatusCountStr}}
- </button>
<div @click.prevent class="loadmore-error alert error" v-if="timelineError">
{{$t('timeline.error_fetching')}}
</div>
+ <button @click.prevent="showNewStatuses" class="loadmore-button" v-if="timeline.newStatusCount > 0 && !timelineError">
+ {{$t('timeline.show_new')}}{{newStatusCountStr}}
+ </button>
<div @click.prevent class="loadmore-text" v-if="!timeline.newStatusCount > 0 && !timelineError">
{{$t('timeline.up_to_date')}}
</div>
@@ -57,36 +57,7 @@
@import '../../_variables.scss';
.timeline {
- .timeline-heading {
- position: relative;
- display: flex;
- }
-
- .title {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- max-width: 70%;
- }
-
- .loadmore-button {
- position: absolute;
- right: 0.6em;
- font-size: 14px;
-
- min-width: 6em;
- height: 1.8em;
- line-height: 100%;
- }
-
.loadmore-text {
- position: absolute;
- right: 0.6em;
- font-size: 14px;
- min-width: 6em;
- font-family: sans-serif;
- text-align: center;
- padding: 0 0.5em 0 0.5em;
opacity: 0.8;
background-color: transparent;
color: $fallback--faint;
@@ -94,14 +65,6 @@
}
.loadmore-error {
- position: absolute;
- right: 0.6em;
- font-size: 14px;
- min-width: 6em;
- font-family: sans-serif;
- text-align: center;
- padding: 0 0.25em 0 0.25em;
- margin: 0;
color: $fallback--fg;
color: var(--fg, $fallback--fg);
}
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index a7a871c3..a019627a 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -3,7 +3,8 @@ import UserCardContent from '../user_card_content/user_card_content.vue'
const UserCard = {
props: [
'user',
- 'showFollows'
+ 'showFollows',
+ 'showApproval'
],
data () {
return {
@@ -16,6 +17,14 @@ const UserCard = {
methods: {
toggleUserExpanded () {
this.userExpanded = !this.userExpanded
+ },
+ approveUser () {
+ this.$store.state.api.backendInteractor.approveUser(this.user.id)
+ this.$store.dispatch('removeFollowRequest', this.user)
+ },
+ denyUser () {
+ this.$store.state.api.backendInteractor.denyUser(this.user.id)
+ this.$store.dispatch('removeFollowRequest', this.user)
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 51d6965f..48f272ca 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -7,14 +7,24 @@
<user-card-content :user="user" :switcher="false"></user-card-content>
</div>
<div class="name-and-screen-name" v-else>
- <div :title="user.name" class="user-name">
+ <div :title="user.name" v-if="user.name_html" class="user-name">
+ <span v-html="user.name_html"></span>
+ <span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
+ {{ $t('user_card.follows_you') }}
+ </span>
+ </div>
+ <div :title="user.name" v-else class="user-name">
{{ user.name }}
<span class="follows-you" v-if="!userExpanded && showFollows && user.follows_you">
- {{ $t('user_card.follows_you') }}
+ {{ $t('user_card.follows_you') }}
</span>
</div>
<a :href="user.statusnet_profile_url" target="blank"><div class="user-screen-name">@{{ user.screen_name }}</div></a>
</div>
+ <div class="approval" v-if="showApproval">
+ <button class="btn btn-default" @click="approveUser">{{ $t('user_card.approve') }}</button>
+ <button class="btn btn-default" @click="denyUser">{{ $t('user_card.deny') }}</button>
+ </div>
</div>
</template>
@@ -63,16 +73,25 @@
border-radius: var(--panelRadius, $fallback--panelRadius);
border-style: solid;
border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
+ border-color: var(--border, $fallback--border);
border-width: 1px;
overflow: hidden;
.panel-heading {
background: transparent;
+ flex-direction: column;
+ align-items: stretch;
}
p {
margin-bottom: 0;
}
}
+
+.approval {
+ button {
+ width: 100%;
+ margin-bottom: 0.5em;
+ }
+}
</style>
diff --git a/src/components/user_card_content/user_card_content.js b/src/components/user_card_content/user_card_content.js
index 4d4266cb..76a5577e 100644
--- a/src/components/user_card_content/user_card_content.js
+++ b/src/components/user_card_content/user_card_content.js
@@ -9,11 +9,6 @@ export default {
if (color) {
const rgb = hex2rgb(color)
const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .5)`
- console.log(rgb)
- console.log([
- `url(${this.user.cover_photo})`,
- `linear-gradient(to bottom, ${tintColor}, ${tintColor})`
- ].join(', '))
return {
backgroundColor: `rgb(${Math.floor(rgb.r * 0.53)}, ${Math.floor(rgb.g * 0.56)}, ${Math.floor(rgb.b * 0.59)})`,
backgroundImage: [
@@ -37,6 +32,29 @@ export default {
dailyAvg () {
const days = Math.ceil((new Date() - new Date(this.user.created_at)) / (60 * 60 * 24 * 1000))
return Math.round(this.user.statuses_count / days)
+ },
+ userHighlightType: {
+ get () {
+ const data = this.$store.state.config.highlight[this.user.screen_name]
+ return data && data.type || 'disabled'
+ },
+ set (type) {
+ const data = this.$store.state.config.highlight[this.user.screen_name]
+ if (type !== 'disabled') {
+ this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: data && data.color || '#FFFFFF', type })
+ } else {
+ this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })
+ }
+ }
+ },
+ userHighlightColor: {
+ get () {
+ const data = this.$store.state.config.highlight[this.user.screen_name]
+ return data && data.color
+ },
+ set (color) {
+ this.$store.dispatch('setHighlight', { user: this.user.screen_name, color })
+ }
}
},
components: {
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index c120df9a..59358040 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -1,29 +1,46 @@
<template>
- <div id="heading" class="profile-panel-background" :style="headingStyle">
- <div class="panel-heading text-center">
- <div class='user-info'>
- <router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
- <i class="icon-cog usersettings"></i>
+<div id="heading" class="profile-panel-background" :style="headingStyle">
+ <div class="panel-heading text-center">
+ <div class='user-info'>
+ <router-link to='/user-settings' style="float: right; margin-top:16px;" v-if="!isOtherUser">
+ <i class="icon-cog usersettings"></i>
+ </router-link>
+ <a :href="user.statusnet_profile_url" target="_blank" class="floater" v-if="isOtherUser">
+ <i class="icon-link-ext usersettings"></i>
+ </a>
+ <div class='container'>
+ <router-link :to="{ name: 'user-profile', params: { id: user.id } }">
+ <StillImage class="avatar" :src="user.profile_image_url_original"/>
</router-link>
- <a :href="user.statusnet_profile_url" target="_blank" style="float: right; margin-top:16px;" v-if="isOtherUser">
- <i class="icon-link-ext usersettings"></i>
- </a>
- <div class='container'>
- <router-link :to="{ name: 'user-profile', params: { id: user.id } }">
- <StillImage class="avatar" :src="user.profile_image_url_original"/>
+ <div class="name-and-screen-name">
+ <div :title="user.name" class='user-name' v-if="user.name_html" v-html="user.name_html"></div>
+ <div :title="user.name" class='user-name' v-else>{{user.name}}</div>
+ <router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
+ <span>@{{user.screen_name}}</span><span v-if="user.locked"><i class="icon icon-lock"></i></span>
+ <span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
</router-link>
- <div class="name-and-screen-name">
- <div :title="user.name" class='user-name'>{{user.name}}</div>
- <router-link class='user-screen-name':to="{ name: 'user-profile', params: { id: user.id } }">
- <span>@{{user.screen_name}}</span>
- <span class="dailyAvg">{{dailyAvg}} {{ $t('user_card.per_day') }}</span>
- </router-link>
- </div>
</div>
- <div v-if="isOtherUser" class="user-interactions">
- <div v-if="user.follows_you && loggedIn" class="following">
- {{ $t('user_card.follows_you') }}
- </div>
+ </div>
+ <div class="user-meta">
+ <div v-if="user.follows_you && loggedIn && isOtherUser" class="following">
+ {{ $t('user_card.follows_you') }}
+ </div>
+ <div class="floater" v-if="switcher || isOtherUser">
+ <!-- id's need to be unique, otherwise vue confuses which user-card checkbox belongs to -->
+ <input class="userHighlightText" type="text" :id="'userHighlightColorTx'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
+ <input class="userHighlightCl" type="color" :id="'userHighlightColor'+user.id" v-if="userHighlightType !== 'disabled'" v-model="userHighlightColor"/>
+ <label for="style-switcher" class='userHighlightSel select'>
+ <select class="userHighlightSel" :id="'userHighlightSel'+user.id" v-model="userHighlightType">
+ <option value="disabled">No highlight</option>
+ <option value="solid">Solid bg</option>
+ <option value="striped">Striped bg</option>
+ <option value="side">Side stripe</option>
+ </select>
+ <i class="icon-down-open"/>
+ </label>
+ </div>
+ </div>
+ <div v-if="isOtherUser" class="user-interactions">
<div class="follow" v-if="loggedIn">
<span v-if="user.following">
<!--Following them!-->
@@ -88,7 +105,8 @@
<span>{{user.followers_count}}</span>
</div>
</div>
- <p v-if="!hideBio">{{user.description}}</p>
+ <p v-if="!hideBio && user.description_html" class="profile-bio" v-html="user.description_html"></p>
+ <p v-else-if="!hideBio" class="profile-bio">{{ user.description }}</p>
</div>
</div>
</template>
@@ -112,7 +130,11 @@
.profile-panel-body {
word-wrap: break-word;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0), $fallback--bg 80%);
- background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%)
+ background: linear-gradient(to bottom, rgba(0, 0, 0, 0), var(--bg, $fallback--bg) 80%);
+
+ .profile-bio {
+ text-align: center;
+ }
}
.user-info {
@@ -179,6 +201,27 @@
padding-right: 0.1em;
}
+ .user-meta {
+ margin-bottom: .4em;
+
+ .following {
+ font-size: 14px;
+ flex: 0 0 100%;
+ margin: 0;
+ padding-left: 16px;
+ text-align: left;
+ float: left;
+ }
+ .floater {
+ margin: 0;
+ }
+
+ &::after {
+ display: block;
+ content: '';
+ clear: both;
+ }
+ }
.user-interactions {
display: flex;
flex-flow: row wrap;
@@ -188,14 +231,6 @@
flex: 1;
}
- .following {
- font-size: 14px;
- flex: 0 0 100%;
- margin: 0 0 .4em 0;
- padding-left: 16px;
- text-align: left;
- }
-
.mute {
max-width: 220px;
min-height: 28px;
@@ -278,4 +313,33 @@
font-size: 0.7em;
color: #CCC;
}
+.floater {
+ float: right;
+ margin-top: 16px;
+
+ .userHighlightCl {
+ padding: 2px 10px;
+ }
+ .userHighlightSel,
+ .userHighlightSel.select {
+ padding-top: 0;
+ padding-bottom: 0;
+ }
+ .userHighlightSel.select i {
+ line-height: 22px;
+ }
+
+ .userHighlightText {
+ width: 70px;
+ }
+
+ .userHighlightCl,
+ .userHighlightText,
+ .userHighlightSel,
+ .userHighlightSel.select {
+ height: 22px;
+ vertical-align: top;
+ margin-right: 0
+ }
+}
</style>
diff --git a/src/components/user_panel/user_panel.vue b/src/components/user_panel/user_panel.vue
index 3d4f873d..2d5cb500 100644
--- a/src/components/user_panel/user_panel.vue
+++ b/src/components/user_panel/user_panel.vue
@@ -14,8 +14,10 @@
<style lang="scss">
.user-panel {
- .profile-panel-background .panel-heading {
- background: transparent;
- }
+ .profile-panel-background .panel-heading {
+ background: transparent;
+ flex-direction: column;
+ align-items: stretch;
+ }
}
</style>
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index f8502907..91d4acd2 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -17,6 +17,8 @@
padding-bottom: 10px;
.panel-heading {
background: transparent;
+ flex-direction: column;
+ align-items: stretch;
}
}
</style>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index b6026e18..0b13a668 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,3 +1,4 @@
+import TabSwitcher from '../tab_switcher/tab_switcher.jsx'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
const UserSettings = {
@@ -5,6 +6,8 @@ const UserSettings = {
return {
newname: this.$store.state.users.currentUser.name,
newbio: this.$store.state.users.currentUser.description,
+ newlocked: this.$store.state.users.currentUser.locked,
+ newdefaultScope: this.$store.state.users.currentUser.default_scope,
followList: null,
followImportError: false,
followsImported: false,
@@ -16,11 +19,13 @@ const UserSettings = {
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
- changePasswordError: false
+ changePasswordError: false,
+ activeTab: 'profile'
}
},
components: {
- StyleSwitcher
+ StyleSwitcher,
+ TabSwitcher
},
computed: {
user () {
@@ -28,18 +33,36 @@ const UserSettings = {
},
pleromaBackend () {
return this.$store.state.config.pleromaBackend
+ },
+ scopeOptionsEnabled () {
+ return this.$store.state.config.scopeOptionsEnabled
+ },
+ vis () {
+ return {
+ public: { selected: this.newdefaultScope === 'public' },
+ unlisted: { selected: this.newdefaultScope === 'unlisted' },
+ private: { selected: this.newdefaultScope === 'private' },
+ direct: { selected: this.newdefaultScope === 'direct' }
+ }
}
},
methods: {
updateProfile () {
const name = this.newname
const description = this.newbio
- this.$store.state.api.backendInteractor.updateProfile({params: {name, description}}).then((user) => {
+ const locked = this.newlocked
+ /* eslint-disable camelcase */
+ const default_scope = this.newdefaultScope
+ this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked, default_scope}}).then((user) => {
if (!user.error) {
this.$store.commit('addNewUsers', [user])
this.$store.commit('setCurrentUser', user)
}
})
+ /* eslint-enable camelcase */
+ },
+ changeVis (visibility) {
+ this.newdefaultScope = visibility
},
uploadFile (slot, e) {
const file = e.target.files[0]
@@ -215,6 +238,9 @@ const UserSettings = {
this.changePasswordError = res.error
}
})
+ },
+ activateTab (tabName) {
+ this.activeTab = tabName
}
}
}
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index fbf3f651..9daafdce 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -4,108 +4,131 @@
{{$t('settings.user_settings')}}
</div>
<div class="panel-body profile-edit">
- <div class="setting-item">
- <h3>{{$t('settings.name_bio')}}</h3>
- <p>{{$t('settings.name')}}</p>
- <input class='name-changer' id='username' v-model="newname"></input>
- <p>{{$t('settings.bio')}}</p>
- <textarea class="bio" v-model="newbio"></textarea>
- <button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
- </div>
- <div class="setting-item">
- <h3>{{$t('settings.avatar')}}</h3>
- <p>{{$t('settings.current_avatar')}}</p>
- <img :src="user.profile_image_url_original" class="old-avatar"></img>
- <p>{{$t('settings.set_new_avatar')}}</p>
- <img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
- </img>
- <div>
- <input type="file" @change="uploadFile(0, $event)" ></input>
+ <tab-switcher>
+ <div :label="$t('settings.profile_tab')">
+ <div class="setting-item" >
+ <h2>{{$t('settings.name_bio')}}</h2>
+ <p>{{$t('settings.name')}}</p>
+ <input class='name-changer' id='username' v-model="newname"></input>
+ <p>{{$t('settings.bio')}}</p>
+ <textarea class="bio" v-model="newbio"></textarea>
+ <p>
+ <input type="checkbox" v-model="newlocked" id="account-locked">
+ <label for="account-locked">{{$t('settings.lock_account_description')}}</label>
+ </p>
+ <div v-if="scopeOptionsEnabled">
+ <label for="default-vis">{{$t('settings.default_vis')}}</label>
+ <div id="default-vis" class="visibility-tray">
+ <i v-on:click="changeVis('direct')" class="icon-mail-alt" :class="vis.direct"></i>
+ <i v-on:click="changeVis('private')" class="icon-lock" :class="vis.private"></i>
+ <i v-on:click="changeVis('unlisted')" class="icon-lock-open-alt" :class="vis.unlisted"></i>
+ <i v-on:click="changeVis('public')" class="icon-globe" :class="vis.public"></i>
+ </div>
+ </div>
+ <button :disabled='newname.length <= 0' class="btn btn-default" @click="updateProfile">{{$t('general.submit')}}</button>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('settings.avatar')}}</h2>
+ <p>{{$t('settings.current_avatar')}}</p>
+ <img :src="user.profile_image_url_original" class="old-avatar"></img>
+ <p>{{$t('settings.set_new_avatar')}}</p>
+ <img class="new-avatar" v-bind:src="previews[0]" v-if="previews[0]">
+ </img>
+ <div>
+ <input type="file" @change="uploadFile(0, $event)" ></input>
+ </div>
+ <i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
+ <button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('settings.profile_banner')}}</h2>
+ <p>{{$t('settings.current_profile_banner')}}</p>
+ <img :src="user.cover_photo" class="banner"></img>
+ <p>{{$t('settings.set_new_profile_banner')}}</p>
+ <img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
+ </img>
+ <div>
+ <input type="file" @change="uploadFile(1, $event)" ></input>
+ </div>
+ <i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
+ <button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
+ </div>
+ <div class="setting-item">
+ <h2>{{$t('settings.profile_background')}}</h2>
+ <p>{{$t('settings.set_new_profile_background')}}</p>
+ <img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
+ </img>
+ <div>
+ <input type="file" @change="uploadFile(2, $event)" ></input>
+ </div>
+ <i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
+ <button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
+ </div>
</div>
- <i class="icon-spin4 animate-spin" v-if="uploading[0]"></i>
- <button class="btn btn-default" v-else-if="previews[0]" @click="submitAvatar">{{$t('general.submit')}}</button>
- </div>
- <div class="setting-item">
- <h3>{{$t('settings.profile_banner')}}</h3>
- <p>{{$t('settings.current_profile_banner')}}</p>
- <img :src="user.cover_photo" class="banner"></img>
- <p>{{$t('settings.set_new_profile_banner')}}</p>
- <img class="banner" v-bind:src="previews[1]" v-if="previews[1]">
- </img>
- <div>
- <input type="file" @change="uploadFile(1, $event)" ></input>
- </div>
- <i class=" icon-spin4 animate-spin uploading" v-if="uploading[1]"></i>
- <button class="btn btn-default" v-else-if="previews[1]" @click="submitBanner">{{$t('general.submit')}}</button>
- </div>
- <div class="setting-item">
- <h3>{{$t('settings.profile_background')}}</h3>
- <p>{{$t('settings.set_new_profile_background')}}</p>
- <img class="bg" v-bind:src="previews[2]" v-if="previews[2]">
- </img>
- <div>
- <input type="file" @change="uploadFile(2, $event)" ></input>
- </div>
- <i class=" icon-spin4 animate-spin uploading" v-if="uploading[2]"></i>
- <button class="btn btn-default" v-else-if="previews[2]" @click="submitBg">{{$t('general.submit')}}</button>
- </div>
- <div class="setting-item">
- <h3>{{$t('settings.change_password')}}</h3>
- <div>
- <p>{{$t('settings.current_password')}}</p>
- <input type="password" v-model="changePasswordInputs[0]">
- </div>
- <div>
- <p>{{$t('settings.new_password')}}</p>
- <input type="password" v-model="changePasswordInputs[1]">
- </div>
- <div>
- <p>{{$t('settings.confirm_new_password')}}</p>
- <input type="password" v-model="changePasswordInputs[2]">
- </div>
- <button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
- <p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
- <p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
- <p v-if="changePasswordError">{{changePasswordError}}</p>
- </div>
- <div class="setting-item" v-if="pleromaBackend">
- <h3>{{$t('settings.follow_import')}}</h3>
- <p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
- <form v-model="followImportForm">
- <input type="file" ref="followlist" v-on:change="followListChange"></input>
- </form>
- <i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
- <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
- <div v-if="followsImported">
- <i class="icon-cross" @click="dismissImported"></i>
- <p>{{$t('settings.follows_imported')}}</p>
- </div>
- <div v-else-if="followImportError">
- <i class="icon-cross" @click="dismissImported"></i>
- <p>{{$t('settings.follow_import_error')}}</p>
+
+ <div :label="$t('settings.security_tab')">
+ <div class="setting-item">
+ <h2>{{$t('settings.change_password')}}</h2>
+ <div>
+ <p>{{$t('settings.current_password')}}</p>
+ <input type="password" v-model="changePasswordInputs[0]">
+ </div>
+ <div>
+ <p>{{$t('settings.new_password')}}</p>
+ <input type="password" v-model="changePasswordInputs[1]">
+ </div>
+ <div>
+ <p>{{$t('settings.confirm_new_password')}}</p>
+ <input type="password" v-model="changePasswordInputs[2]">
+ </div>
+ <button class="btn btn-default" @click="changePassword">{{$t('general.submit')}}</button>
+ <p v-if="changedPassword">{{$t('settings.changed_password')}}</p>
+ <p v-else-if="changePasswordError !== false">{{$t('settings.change_password_error')}}</p>
+ <p v-if="changePasswordError">{{changePasswordError}}</p>
+ </div>
+
+ <div class="setting-item">
+ <h2>{{$t('settings.delete_account')}}</h2>
+ <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
+ <div v-if="deletingAccount">
+ <p>{{$t('settings.delete_account_instructions')}}</p>
+ <p>{{$t('login.password')}}</p>
+ <input type="password" v-model="deleteAccountConfirmPasswordInput">
+ <button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
+ </div>
+ <p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
+ <p v-if="deleteAccountError">{{deleteAccountError}}</p>
+ <button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
+ </div>
</div>
- </div>
- <div class="setting-item" v-if="enableFollowsExport">
- <h3>{{$t('settings.follow_export')}}</h3>
- <button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
- </div>
- <div class="setting-item" v-else>
- <h3>{{$t('settings.follow_export_processing')}}</h3>
- </div>
- <hr>
- <div class="setting-item">
- <h3>{{$t('settings.delete_account')}}</h3>
- <p v-if="!deletingAccount">{{$t('settings.delete_account_description')}}</p>
- <div v-if="deletingAccount">
- <p>{{$t('settings.delete_account_instructions')}}</p>
- <p>{{$t('login.password')}}</p>
- <input type="password" v-model="deleteAccountConfirmPasswordInput">
- <button class="btn btn-default" @click="deleteAccount">{{$t('settings.delete_account')}}</button>
+
+ <div :label="$t('settings.data_import_export_tab')" v-if="pleromaBackend">
+ <div class="setting-item">
+ <h2>{{$t('settings.follow_import')}}</h2>
+ <p>{{$t('settings.import_followers_from_a_csv_file')}}</p>
+ <form v-model="followImportForm">
+ <input type="file" ref="followlist" v-on:change="followListChange"></input>
+ </form>
+ <i class=" icon-spin4 animate-spin uploading" v-if="uploading[3]"></i>
+ <button class="btn btn-default" v-else @click="importFollows">{{$t('general.submit')}}</button>
+ <div v-if="followsImported">
+ <i class="icon-cross" @click="dismissImported"></i>
+ <p>{{$t('settings.follows_imported')}}</p>
+ </div>
+ <div v-else-if="followImportError">
+ <i class="icon-cross" @click="dismissImported"></i>
+ <p>{{$t('settings.follow_import_error')}}</p>
+ </div>
+ </div>
+ <div class="setting-item" v-if="enableFollowsExport">
+ <h2>{{$t('settings.follow_export')}}</h2>
+ <button class="btn btn-default" @click="exportFollows">{{$t('settings.follow_export_button')}}</button>
+ </div>
+ <div class="setting-item" v-else>
+ <h2>{{$t('settings.follow_export_processing')}}</h2>
+ </div>
</div>
- <p v-if="deleteAccountError !== false">{{$t('settings.delete_account_error')}}</p>
- <p v-if="deleteAccountError">{{deleteAccountError}}</p>
- <button class="btn btn-default" v-if="!deletingAccount" @click="confirmDelete">{{$t('general.submit')}}</button>
- </div>
+ </tab-switcher>
</div>
</div>
</template>
@@ -121,6 +144,7 @@
input[type=file] {
padding: 5px;
+ height: auto;
}
.banner {
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.js b/src/components/who_to_follow_panel/who_to_follow_panel.js
index 51b9f469..ce60308f 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.js
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.js
@@ -1,18 +1,21 @@
-function showWhoToFollow (panel, reply, aHost, aUser) {
- var users = reply.ids
+import apiService from '../../services/api/api.service.js'
+
+function showWhoToFollow (panel, reply) {
+ var users = reply
var cn
- var index = 0
- var random = Math.floor(Math.random() * 10)
- for (cn = random; cn < users.length; cn = cn + 10) {
+ var index
+ var step = 7
+ cn = Math.floor(Math.random() * step)
+ for (index = 0; index < 3; index++) {
var user
user = users[cn]
var img
- if (user.icon) {
- img = user.icon
+ if (user.avatar) {
+ img = user.avatar
} else {
img = '/images/avi.png'
}
- var name = user.to_id
+ var name = user.acct
if (index === 0) {
panel.img1 = img
panel.name1 = name
@@ -44,35 +47,20 @@ function showWhoToFollow (panel, reply, aHost, aUser) {
}
})
}
- index = index + 1
- if (index > 2) {
- break
- }
}
+ cn = (cn + step) % users.length
}
function getWhoToFollow (panel) {
- var user = panel.$store.state.users.currentUser.screen_name
- if (user) {
+ var credentials = panel.$store.state.users.currentUser.credentials
+ if (credentials) {
panel.name1 = 'Loading...'
panel.name2 = 'Loading...'
panel.name3 = 'Loading...'
- var host = window.location.hostname
- var whoToFollowProvider = panel.$store.state.config.whoToFollowProvider
- var url
- url = whoToFollowProvider.replace(/{{host}}/g, encodeURIComponent(host))
- url = url.replace(/{{user}}/g, encodeURIComponent(user))
- window.fetch(url, {mode: 'cors'}).then(function (response) {
- if (response.ok) {
- return response.json()
- } else {
- panel.name1 = ''
- panel.name2 = ''
- panel.name3 = ''
- }
- }).then(function (reply) {
- showWhoToFollow(panel, reply, host, user)
- })
+ apiService.suggestions({credentials: credentials})
+ .then((reply) => {
+ showWhoToFollow(panel, reply)
+ })
}
}
@@ -95,26 +83,26 @@ const WhoToFollowPanel = {
moreUrl: function () {
var host = window.location.hostname
var user = this.user
- var whoToFollowLink = this.$store.state.config.whoToFollowLink
+ var suggestionsWeb = this.$store.state.config.suggestionsWeb
var url
- url = whoToFollowLink.replace(/{{host}}/g, encodeURIComponent(host))
+ url = suggestionsWeb.replace(/{{host}}/g, encodeURIComponent(host))
url = url.replace(/{{user}}/g, encodeURIComponent(user))
return url
},
- showWhoToFollowPanel () {
- return this.$store.state.config.showWhoToFollowPanel
+ suggestionsEnabled () {
+ return this.$store.state.config.suggestionsEnabled
}
},
watch: {
user: function (user, oldUser) {
- if (this.showWhoToFollowPanel) {
+ if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
}
},
mounted:
function () {
- if (this.showWhoToFollowPanel) {
+ if (this.suggestionsEnabled) {
getWhoToFollow(this)
}
}
diff --git a/src/components/who_to_follow_panel/who_to_follow_panel.vue b/src/components/who_to_follow_panel/who_to_follow_panel.vue
index 5af6d0d5..8b3abe70 100644
--- a/src/components/who_to_follow_panel/who_to_follow_panel.vue
+++ b/src/components/who_to_follow_panel/who_to_follow_panel.vue
@@ -3,7 +3,7 @@
<div class="panel panel-default base01-background">
<div class="panel-heading timeline-heading base02-background base04">
<div class="title">
- Who to follow
+ {{$t('who_to_follow.who_to_follow')}}
</div>
</div>
<div class="panel-body who-to-follow">
@@ -11,7 +11,7 @@
<img v-bind:src="img1"/> <router-link :to="{ name: 'user-profile', params: { id: id1 } }">{{ name1 }}</router-link><br>
<img v-bind:src="img2"/> <router-link :to="{ name: 'user-profile', params: { id: id2 } }">{{ name2 }}</router-link><br>
<img v-bind:src="img3"/> <router-link :to="{ name: 'user-profile', params: { id: id3 } }">{{ name3 }}</router-link><br>
- <img v-bind:src="$store.state.config.logo"> <a v-bind:href="moreUrl" target="_blank">More</a>
+ <img v-bind:src="$store.state.config.logo"> <a v-bind:href="moreUrl" target="_blank">{{$t('who_to_follow.more')}}</a>
</p>
</div>
</div>
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index ecf86771..5c5ed447 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -48,7 +48,11 @@ const de = {
settings: 'Einstellungen',
theme: 'Farbschema',
presets: 'Voreinstellungen',
- theme_help: 'Benutze HTML Farbcodes (#rrggbb) um dein Farbschema anzupassen.',
+ export_theme: 'Farbschema speichern',
+ import_theme: 'Farbschema laden',
+ invalid_theme_imported: 'Die ausgewählte Datei ist kein unterstütztes Pleroma-Theme. Keine Änderungen wurden vorgenommen.',
+ theme_help: 'Benutze HTML Farbcodes (#rrggbb) um dein Farbschema anzupassen',
+ radii_help: 'Kantenrundung (in Pixel) der Oberfläche anpassen',
background: 'Hintergrund',
foreground: 'Vordergrund',
text: 'Text',
@@ -58,6 +62,7 @@ const de = {
cOrange: 'Orange (Favorisieren)',
cGreen: 'Grün (Retweet)',
btnRadius: 'Buttons',
+ inputRadius: 'Eingabefelder',
panelRadius: 'Panel',
avatarRadius: 'Avatare',
avatarAltRadius: 'Avatare (Benachrichtigungen)',
@@ -76,7 +81,20 @@ const de = {
follow_import: 'Folgeliste importieren',
import_followers_from_a_csv_file: 'Importiere Kontakte, denen du folgen möchtest, aus einer CSV-Datei',
follows_imported: 'Folgeliste importiert! Die Bearbeitung kann eine Zeit lang dauern.',
- follow_import_error: 'Fehler beim importieren der Folgeliste'
+ follow_import_error: 'Fehler beim importieren der Folgeliste',
+ delete_account: 'Account löschen',
+ delete_account_description: 'Lösche deinen Account und alle deine Nachrichten dauerhaft.',
+ delete_account_instructions: 'Tippe dein Passwort unten in das Feld ein um die Löschung deines Accounts zu bestätigen.',
+ delete_account_error: 'Es ist ein Fehler beim löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.',
+ follow_export: 'Folgeliste exportieren',
+ follow_export_processing: 'In Bearbeitung. Die Liste steht gleich zum herunterladen bereit.',
+ follow_export_button: 'Liste (.csv) erstellen',
+ change_password: 'Passwort ändern',
+ current_password: 'Aktuelles Passwort',
+ new_password: 'Neues Passwort',
+ confirm_new_password: 'Neues Passwort bestätigen',
+ changed_password: 'Passwort erfolgreich geändert!',
+ change_password_error: 'Es gab ein Problem bei der Änderung des Passworts.'
},
notifications: {
notifications: 'Benachrichtigungen',
@@ -88,6 +106,7 @@ const de = {
login: {
login: 'Anmelden',
username: 'Benutzername',
+ placeholder: 'z.B. lain',
password: 'Passwort',
register: 'Registrieren',
logout: 'Abmelden'
@@ -101,7 +120,16 @@ const de = {
},
post_status: {
posting: 'Veröffentlichen',
- default: 'Sitze gerade im Hofbräuhaus.'
+ default: 'Sitze gerade im Hofbräuhaus.',
+ account_not_locked_warning: 'Dein Profil ist nicht {0}. Wer dir folgen will, kann das jederzeit tun und dann auch deine privaten Beiträge sehen.',
+ account_not_locked_warning_link: 'gesperrt',
+ direct_warning: 'Dieser Beitrag wird nur für die erwähnten Nutzer sichtbar sein.',
+ scope: {
+ public: 'Öffentlich - Beitrag an öffentliche Zeitleisten',
+ unlisted: 'Nicht gelistet - Nicht in öffentlichen Zeitleisten anzeigen',
+ private: 'Nur Folgende - Beitrag nur an Folgende',
+ direct: 'Direkt - Beitrag nur an erwähnte Profile'
+ }
},
finder: {
find_user: 'Finde Benutzer',
@@ -184,6 +212,7 @@ const fi = {
login: {
login: 'Kirjaudu sisään',
username: 'Käyttäjänimi',
+ placeholder: 'esim. lain',
password: 'Salasana',
register: 'Rekisteröidy',
logout: 'Kirjaudu ulos'
@@ -218,7 +247,8 @@ const en = {
timeline: 'Timeline',
mentions: 'Mentions',
public_tl: 'Public Timeline',
- twkn: 'The Whole Known Network'
+ twkn: 'The Whole Known Network',
+ friend_requests: 'Follow Requests'
},
user_card: {
follows_you: 'Follows you!',
@@ -232,7 +262,9 @@ const en = {
followers: 'Followers',
followees: 'Following',
per_day: 'per day',
- remote_follow: 'Remote follow'
+ remote_follow: 'Remote follow',
+ approve: 'Approve',
+ deny: 'Deny'
},
timeline: {
show_new: 'Show new',
@@ -241,9 +273,11 @@ const en = {
load_older: 'Load older statuses',
conversation: 'Conversation',
collapse: 'Collapse',
- repeated: 'repeated'
+ repeated: 'repeated',
+ no_retweet_hint: 'Post is marked as followers-only or direct and cannot be repeated'
},
settings: {
+ general: 'General',
user_settings: 'User Settings',
name_bio: 'Name & Bio',
name: 'Name',
@@ -259,7 +293,10 @@ const en = {
settings: 'Settings',
theme: 'Theme',
presets: 'Presets',
+ export_theme: 'Save preset',
+ import_theme: 'Load preset',
theme_help: 'Use hex color codes (#rrggbb) to customize your color theme.',
+ invalid_theme_imported: 'The selected file is not a supported Pleroma theme. No changes to your theme were made.',
radii_help: 'Set up interface edge rounding (in pixels)',
background: 'Background',
foreground: 'Foreground',
@@ -282,10 +319,23 @@ const en = {
hide_attachments_in_tl: 'Hide attachments in timeline',
hide_attachments_in_convo: 'Hide attachments in conversations',
nsfw_clickthrough: 'Enable clickthrough NSFW attachment hiding',
+ collapse_subject: 'Collapse posts with subjects',
stop_gifs: 'Play-on-hover GIFs',
autoload: 'Enable automatic loading when scrolled to the bottom',
streaming: 'Enable automatic streaming of new posts when scrolled to the top',
+ pause_on_unfocused: 'Pause streaming when tab is not focused',
+ loop_video: 'Loop videos',
+ loop_video_silent_only: 'Loop only videos without sound (i.e. Mastodon\'s "gifs")',
reply_link_preview: 'Enable reply-link preview on mouse hover',
+ replies_in_timeline: 'Replies in timeline',
+ reply_visibility_all: 'Show all replies',
+ reply_visibility_following: 'Only show replies directed at me or users I\'m following',
+ reply_visibility_self: 'Only show replies directed at me',
+ notification_visibility: 'Types of notifications to show',
+ notification_visibility_likes: 'Likes',
+ notification_visibility_mentions: 'Mentions',
+ notification_visibility_repeats: 'Repeats',
+ notification_visibility_follows: 'Follows',
follow_import: 'Follow import',
import_followers_from_a_csv_file: 'Import follows from a csv file',
follows_imported: 'Follows imported! Processing them will take a while.',
@@ -302,18 +352,28 @@ const en = {
new_password: 'New password',
confirm_new_password: 'Confirm new password',
changed_password: 'Password changed successfully!',
- change_password_error: 'There was an issue changing your password.'
+ change_password_error: 'There was an issue changing your password.',
+ lock_account_description: 'Restrict your account to approved followers only',
+ limited_availability: 'Unavailable in your browser',
+ default_vis: 'Default visibility scope',
+ profile_tab: 'Profile',
+ security_tab: 'Security',
+ data_import_export_tab: 'Data Import / Export',
+ interfaceLanguage: 'Interface language'
},
notifications: {
notifications: 'Notifications',
read: 'Read!',
followed_you: 'followed you',
favorited_you: 'favorited your status',
- repeated_you: 'repeated your status'
+ repeated_you: 'repeated your status',
+ broken_favorite: 'Unknown status, searching for it...',
+ load_older: 'Load older notifications'
},
login: {
login: 'Log in',
username: 'Username',
+ placeholder: 'e.g. lain',
password: 'Password',
register: 'Register',
logout: 'Log out'
@@ -323,12 +383,26 @@ const en = {
fullname: 'Display name',
email: 'Email',
bio: 'Bio',
- password_confirm: 'Password confirmation'
+ password_confirm: 'Password confirmation',
+ token: 'Invite token'
},
post_status: {
posting: 'Posting',
- content_warning: 'Content warning (optional)',
- default: 'Just landed in L.A.'
+ content_warning: 'Subject (optional)',
+ default: 'Just landed in L.A.',
+ account_not_locked_warning: 'Your account is not {0}. Anyone can follow you to view your follower-only posts.',
+ account_not_locked_warning_link: 'locked',
+ direct_warning: 'This post will only be visible to all the mentioned users.',
+ attachments_sensitive: 'Mark attachments as sensitive',
+ scope: {
+ public: 'Public - Post to public timelines',
+ unlisted: 'Unlisted - Do not post to public timelines',
+ private: 'Followers-only - Post to followers only',
+ direct: 'Direct - Post to mentioned users only'
+ },
+ content_type: {
+ plain_text: 'Plain text'
+ }
},
finder: {
find_user: 'Find user',
@@ -340,6 +414,19 @@ const en = {
},
user_profile: {
timeline_title: 'User Timeline'
+ },
+ who_to_follow: {
+ who_to_follow: 'Who to follow',
+ more: 'More'
+ },
+ features_panel: {
+ title: 'Features',
+ chat: 'Chat',
+ gopher: 'Gopher',
+ who_to_follow: 'Who to follow',
+ media_proxy: 'Media proxy',
+ scope_options: 'Scope options',
+ text_limit: 'Text limit'
}
}
@@ -434,6 +521,7 @@ const eo = {
login: {
login: 'Ensaluti',
username: 'Salutnomo',
+ placeholder: 'ekz. lain',
password: 'Pasvorto',
register: 'Registriĝi',
logout: 'Elsaluti'
@@ -521,6 +609,7 @@ const et = {
login: {
login: 'Logi sisse',
username: 'Kasutajanimi',
+ placeholder: 'nt lain',
password: 'Parool',
register: 'Registreeru',
logout: 'Logi välja'
@@ -604,6 +693,7 @@ const hu = {
login: {
login: 'Bejelentkezés',
username: 'Felhasználó név',
+ placeholder: 'e.g. lain',
password: 'Jelszó',
register: 'Feliratkozás',
logout: 'Kijelentkezés'
@@ -687,6 +777,7 @@ const ro = {
login: {
login: 'Loghează',
username: 'Nume utilizator',
+ placeholder: 'd.e. lain',
password: 'Parolă',
register: 'Înregistrare',
logout: 'Deloghează'
@@ -719,114 +810,156 @@ const ja = {
chat: 'ローカルチャット',
timeline: 'タイムライン',
mentions: 'メンション',
- public_tl: '公開タイムライン',
- twkn: '接続しているすべてのネットワーク'
+ public_tl: 'パブリックタイムライン',
+ twkn: 'つながっているすべてのネットワーク',
+ friend_requests: 'Follow Requests'
},
user_card: {
follows_you: 'フォローされました!',
- following: 'フォロー中!',
+ following: 'フォローしています!',
follow: 'フォロー',
- blocked: 'ブロック済み!',
+ blocked: 'ブロックしています!',
block: 'ブロック',
- statuses: '投稿',
+ statuses: 'ステータス',
mute: 'ミュート',
- muted: 'ミュート済み',
+ muted: 'ミュートしています!',
followers: 'フォロワー',
followees: 'フォロー',
per_day: '/日',
- remote_follow: 'リモートフォロー'
+ remote_follow: 'リモートフォロー',
+ approve: 'Approve',
+ deny: 'Deny'
},
timeline: {
- show_new: '更新',
- error_fetching: '更新の取得中にエラーが発生しました。',
- up_to_date: '最新',
- load_older: '古い投稿を読み込む',
- conversation: '会話',
- collapse: '折り畳む',
+ show_new: 'よみこみ',
+ error_fetching: 'よみこみがエラーになりました。',
+ up_to_date: 'さいしん',
+ load_older: 'ふるいステータス',
+ conversation: 'スレッド',
+ collapse: 'たたむ',
repeated: 'リピート'
},
settings: {
- user_settings: 'ユーザー設定',
- name_bio: '名前とプロフィール',
- name: '名前',
+ user_settings: 'ユーザーせってい',
+ name_bio: 'なまえとプロフィール',
+ name: 'なまえ',
bio: 'プロフィール',
avatar: 'アバター',
- current_avatar: 'あなたの現在のアバター',
- set_new_avatar: '新しいアバターを設定する',
+ current_avatar: 'いまのアバター',
+ set_new_avatar: 'あたらしいアバターをせっていする',
profile_banner: 'プロフィールバナー',
- current_profile_banner: '現在のプロフィールバナー',
- set_new_profile_banner: '新しいプロフィールバナーを設定する',
- profile_background: 'プロフィールの背景',
- set_new_profile_background: '新しいプロフィールの背景を設定する',
- settings: '設定',
+ current_profile_banner: 'いまのプロフィールバナー',
+ set_new_profile_banner: 'あたらしいプロフィールバナーを設定する',
+ profile_background: 'プロフィールのバックグラウンド',
+ set_new_profile_background: 'あたらしいプロフィールのバックグラウンドをせっていする',
+ settings: 'せってい',
theme: 'テーマ',
presets: 'プリセット',
- theme_help: '16進数カラーコード (#aabbcc) を使用してカラーテーマをカスタマイズ出来ます。',
- radii_help: 'インターフェースの縁の丸さを設定する。',
- background: '背景',
- foreground: '前景',
- text: '文字',
+ theme_help: 'カラーテーマをカスタマイズできます。',
+ radii_help: 'インターフェースのまるさをせっていする。',
+ background: 'バックグラウンド',
+ foreground: 'フォアグラウンド',
+ text: 'もじ',
links: 'リンク',
- cBlue: '青 (返信, フォロー)',
- cRed: '赤 (キャンセル)',
- cOrange: 'オレンジ (お気に入り)',
- cGreen: '緑 (リツイート)',
+ cBlue: 'あお (リプライ, フォロー)',
+ cRed: 'あか (キャンセル)',
+ cOrange: 'オレンジ (おきにいり)',
+ cGreen: 'みどり (リピート)',
btnRadius: 'ボタン',
+ inputRadius: 'Input fields',
panelRadius: 'パネル',
avatarRadius: 'アバター',
- avatarAltRadius: 'アバター (通知)',
+ avatarAltRadius: 'アバター (つうち)',
tooltipRadius: 'ツールチップ/アラート',
attachmentRadius: 'ファイル',
filtering: 'フィルタリング',
- filtering_explanation: 'これらの単語を含むすべてのものがミュートされます。1行に1つの単語を入力してください。',
+ filtering_explanation: 'これらのことばをふくむすべてのものがミュートされます。1行に1つのことばをかいてください。',
attachments: 'ファイル',
- hide_attachments_in_tl: 'タイムラインのファイルを隠す。',
- hide_attachments_in_convo: '会話の中のファイルを隠す。',
- nsfw_clickthrough: 'NSFWファイルの非表示を有効にする。',
- stop_gifs: 'カーソルを重ねた時にGIFを再生する。',
- autoload: '下にスクロールした時に自動で読み込むようにする。',
- streaming: '上までスクロールした時に自動でストリーミングされるようにする。',
- reply_link_preview: 'マウスカーソルを重ねた時に返信のプレビューを表示するようにする。',
+ hide_attachments_in_tl: 'タイムラインのファイルをかくす。',
+ hide_attachments_in_convo: 'スレッドのファイルをかくす。',
+ nsfw_clickthrough: 'NSFWなファイルをかくす。',
+ stop_gifs: 'カーソルをかさねたとき、GIFをうごかす。',
+ autoload: 'したにスクロールしたとき、じどうてきによみこむ。',
+ streaming: 'うえまでスクロールしたとき、じどうてきにストリーミングする。',
+ reply_link_preview: 'カーソルをかさねたとき、リプライのプレビューをみる。',
follow_import: 'フォローインポート',
import_followers_from_a_csv_file: 'CSVファイルからフォローをインポートする。',
- follows_imported: 'フォローがインポートされました!処理に少し時間がかかるかもしれません。',
- follow_import_error: 'フォロワーのインポート中にエラーが発生しました。'
+ follows_imported: 'フォローがインポートされました! すこしじかんがかかるかもしれません。',
+ follow_import_error: 'フォローのインポートがエラーになりました。',
+ delete_account: 'アカウントをけす',
+ delete_account_description: 'あなたのアカウントとメッセージが、きえます。',
+ delete_account_instructions: 'ほんとうにアカウントをけしてもいいなら、パスワードをかいてください。',
+ delete_account_error: 'アカウントをけすことが、できなかったかもしれません。インスタンスのかんりしゃに、れんらくしてください。',
+ follow_export: 'フォローのエクスポート',
+ follow_export_processing: 'おまちください。まもなくファイルをダウンロードできます。',
+ follow_export_button: 'エクスポート',
+ change_password: 'パスワードをかえる',
+ current_password: 'いまのパスワード',
+ new_password: 'あたらしいパスワード',
+ confirm_new_password: 'あたらしいパスワードのかくにん',
+ changed_password: 'パスワードが、かわりました!',
+ change_password_error: 'パスワードをかえることが、できなかったかもしれません。',
+ lock_account_description: 'あなたがみとめたひとだけ、あなたのアカウントをフォローできます。'
},
notifications: {
- notifications: '通知',
- read: '読んだ!',
+ notifications: 'つうち',
+ read: 'よんだ!',
followed_you: 'フォローされました',
- favorited_you: 'あなたの投稿がお気に入りされました',
- repeated_you: 'あなたの投稿がリピートされました'
+ favorited_you: 'あなたのステータスがおきにいりされました',
+ repeated_you: 'あなたのステータスがリピートされました'
},
login: {
login: 'ログイン',
- username: 'ユーザー名',
+ username: 'ユーザーめい',
+ placeholder: 'れい: lain',
password: 'パスワード',
- register: '登録',
+ register: 'はじめる',
logout: 'ログアウト'
},
registration: {
- registration: '登録',
- fullname: '表示名',
+ registration: 'はじめる',
+ fullname: 'スクリーンネーム',
email: 'Eメール',
bio: 'プロフィール',
- password_confirm: 'パスワードの確認'
+ password_confirm: 'パスワードのかくにん'
},
post_status: {
- posting: '投稿',
- default: 'ちょうどL.A.に着陸しました。'
+ posting: 'とうこう',
+ content_warning: 'せつめい (かかなくてもよい)',
+ default: 'はねだくうこうに、つきました。',
+ account_not_locked_warning: 'あなたのアカウントは {0} ではありません。あなたをフォローすれば、だれでも、フォロワーげんていのステータスをよむことができます。',
+ account_not_locked_warning_link: 'ロックされたアカウント',
+ direct_warning: 'このステータスは、メンションされたユーザーだけが、よむことができます。',
+ scope: {
+ public: 'パブリック - パブリックタイムラインにとどきます。',
+ unlisted: 'アンリステッド - パブリックタイムラインにとどきません。',
+ private: 'フォロワーげんてい - フォロワーのみにとどきます。',
+ direct: 'ダイレクト - メンションされたユーザーのみにとどきます。'
+ }
},
finder: {
- find_user: 'ユーザー検索',
- error_fetching_user: 'ユーザー検索でエラーが発生しました'
+ find_user: 'ユーザーをさがす',
+ error_fetching_user: 'ユーザーけんさくがエラーになりました。'
},
general: {
- submit: '送信',
- apply: '適用'
+ submit: 'そうしん',
+ apply: 'てきよう'
},
user_profile: {
timeline_title: 'ユーザータイムライン'
+ },
+ who_to_follow: {
+ who_to_follow: 'おすすめユーザー',
+ more: 'くわしく'
+ },
+ features_panel: {
+ title: 'ゆうこうなきのう',
+ chat: 'チャット',
+ gopher: 'Gopher',
+ who_to_follow: 'おすすめユーザー',
+ media_proxy: 'メディアプロクシ',
+ scope_options: 'こうかいはんい',
+ text_limit: 'もじのかず'
}
}
@@ -877,7 +1010,7 @@ const fr = {
settings: 'Paramètres',
theme: 'Thème',
filtering: 'Filtre',
- filtering_explanation: 'Tout les statuts contenant ces mots seront masqués. Un mot par ligne.',
+ filtering_explanation: 'Tous les statuts contenant ces mots seront masqués. Un mot par ligne.',
attachments: 'Pièces jointes',
hide_attachments_in_tl: 'Masquer les pièces jointes dans le journal',
hide_attachments_in_convo: 'Masquer les pièces jointes dans les conversations',
@@ -886,15 +1019,18 @@ const fr = {
reply_link_preview: 'Afficher un aperçu lors du survol de liens vers une réponse',
presets: 'Thèmes prédéfinis',
theme_help: 'Spécifiez des codes couleur hexadécimaux (#aabbcc) pour personnaliser les couleurs du thème',
- background: 'Arrière plan',
+ background: 'Arrière-plan',
foreground: 'Premier plan',
text: 'Texte',
links: 'Liens',
streaming: 'Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page',
- follow_import: 'Importer ses abonnements',
- import_followers_from_a_csv_file: 'Importer ses abonnements depuis un fichier csv',
+ follow_import: 'Importer des abonnements',
+ import_followers_from_a_csv_file: 'Importer des abonnements depuis un fichier csv',
follows_imported: 'Abonnements importés ! Le traitement peut prendre un moment.',
follow_import_error: 'Erreur lors de l\'importation des abonnements.',
+ follow_export: 'Exporter les abonnements',
+ follow_export_button: 'Exporter les abonnements en csv',
+ follow_export_processing: 'Exportation en cours…',
cBlue: 'Bleu (Répondre, suivre)',
cRed: 'Rouge (Annuler)',
cOrange: 'Orange (Aimer)',
@@ -907,7 +1043,15 @@ const fr = {
tooltipRadius: 'Info-bulles/alertes ',
attachmentRadius: 'Pièces jointes',
radii_help: 'Vous pouvez ici choisir le niveau d\'arrondi des angles de l\'interface (en pixels)',
- stop_gifs: 'N\'animer les GIFS que lors du survol du curseur de la souris'
+ stop_gifs: 'N\'animer les GIFS que lors du survol du curseur de la souris',
+ change_password: 'Modifier son mot de passe',
+ current_password: 'Mot de passe actuel',
+ new_password: 'Nouveau mot de passe',
+ confirm_new_password: 'Confirmation du nouveau mot de passe',
+ delete_account: 'Supprimer le compte',
+ delete_account_description: 'Supprimer définitivement votre compte et tous vos statuts.',
+ delete_account_instructions: 'Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.',
+ delete_account_error: 'Il y a eu un problème lors de la tentative de suppression de votre compte. Si le problème persiste, contactez l\'administrateur de cette instance.'
},
notifications: {
notifications: 'Notifications',
@@ -919,6 +1063,7 @@ const fr = {
login: {
login: 'Connexion',
username: 'Identifiant',
+ placeholder: 'p.e. lain',
password: 'Mot de passe',
register: 'S\'inscrire',
logout: 'Déconnexion'
@@ -932,7 +1077,16 @@ const fr = {
},
post_status: {
posting: 'Envoi en cours',
- default: 'Écrivez ici votre prochain statut.'
+ default: 'Écrivez ici votre prochain statut.',
+ account_not_locked_warning: 'Votre compte n’est pas {0}. N’importe qui peut vous suivre pour voir vos billets en Abonné·e·s uniquement.',
+ account_not_locked_warning_link: 'verrouillé',
+ direct_warning: 'Ce message sera visible à toutes les personnes mentionnées.',
+ scope: {
+ public: 'Publique - Afficher dans les fils publics',
+ unlisted: 'Non-Listé - Ne pas afficher dans les fils publics',
+ private: 'Abonné·e·s uniquement - Seul·e·s vos abonné·e·s verront vos billets',
+ direct: 'Direct - N’envoyer qu’aux personnes mentionnées'
+ }
},
finder: {
find_user: 'Chercher un utilisateur',
@@ -1063,10 +1217,10 @@ const oc = {
links: 'Ligams',
cBlue: 'Blau (Respondre, seguir)',
cRed: 'Roge (Anullar)',
- cOrange: 'Irange (Metre en favorit)',
+ cOrange: 'Irange (Aimar)',
cGreen: 'Verd (Repartajar)',
- inputRadius: 'Camps tèxte',
btnRadius: 'Botons',
+ inputRadius: 'Camps tèxte',
panelRadius: 'Panèls',
avatarRadius: 'Avatars',
avatarAltRadius: 'Avatars (Notificacions)',
@@ -1085,7 +1239,20 @@ const oc = {
follow_import: 'Importar los abonaments',
import_followers_from_a_csv_file: 'Importar los seguidors d’un fichièr csv',
follows_imported: 'Seguidors importats. Lo tractament pòt trigar una estona.',
- follow_import_error: 'Error en important los seguidors'
+ follow_import_error: 'Error en important los seguidors',
+ delete_account: 'Suprimir lo compte',
+ delete_account_description: 'Suprimir vòstre compte e los messatges per sempre.',
+ delete_account_instructions: 'Picatz vòstre senhal dins lo camp tèxte çai-jos per confirmar la supression del compte.',
+ delete_account_error: 'Una error s’es producha en suprimir lo compte. S’aquò ten d’arribar mercés de contactar vòstre administrador d’instància.',
+ follow_export: 'Exportar los abonaments',
+ follow_export_processing: 'Tractament, vos demandarem lèu de telecargar lo fichièr',
+ follow_export_button: 'Exportar vòstres abonaments dins un fichièr csv',
+ change_password: 'Cambiar lo senhal',
+ current_password: 'Senhal actual',
+ new_password: 'Nòu senhal',
+ confirm_new_password: 'Confirmatz lo nòu senhal',
+ changed_password: 'Senhal corrèctament cambiat',
+ change_password_error: 'Una error s’es producha en cambiant lo senhal.'
},
notifications: {
notifications: 'Notficacions',
@@ -1097,6 +1264,7 @@ const oc = {
login: {
login: 'Connexion',
username: 'Nom d’utilizaire',
+ placeholder: 'e.g. lain',
password: 'Senhal',
register: 'Se marcar',
logout: 'Desconnexion'
@@ -1110,6 +1278,7 @@ const oc = {
},
post_status: {
posting: 'Mandadís',
+ content_warning: 'Avís de contengut (opcional)',
default: 'Escrivètz aquí vòstre estatut.'
},
finder: {
@@ -1230,6 +1399,7 @@ const pl = {
login: {
login: 'Zaloguj',
username: 'Użytkownik',
+ placeholder: 'n.p. lain',
password: 'Hasło',
register: 'Zarejestruj',
logout: 'Wyloguj'
@@ -1333,6 +1503,7 @@ const es = {
login: {
login: 'Identificación',
username: 'Usuario',
+ placeholder: 'p.ej. lain',
password: 'Contraseña',
register: 'Registrar',
logout: 'Salir'
@@ -1447,6 +1618,7 @@ const pt = {
login: {
login: 'Entrar',
username: 'Usuário',
+ placeholder: 'p.e. lain',
password: 'Senha',
register: 'Registrar',
logout: 'Sair'
@@ -1507,9 +1679,11 @@ const ru = {
load_older: 'Загрузить старые статусы',
conversation: 'Разговор',
collapse: 'Свернуть',
- repeated: 'повторил(а)'
+ repeated: 'повторил(а)',
+ no_retweet_hint: 'Пост помечен как "только для подписчиков" или "личное" и поэтому не может быть повторён'
},
settings: {
+ general: 'Общие',
user_settings: 'Настройки пользователя',
name_bio: 'Имя и описание',
name: 'Имя',
@@ -1524,9 +1698,11 @@ const ru = {
set_new_profile_background: 'Загрузить новый фон профиля',
settings: 'Настройки',
theme: 'Тема',
+ export_theme: 'Сохранить Тему',
+ import_theme: 'Загрузить Тему',
presets: 'Пресеты',
theme_help: 'Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.',
- radii_help: 'Округление краёв элементов интерфейса (в пикселях)',
+ radii_help: 'Скругление углов элементов интерфейса (в пикселях)',
background: 'Фон',
foreground: 'Передний план',
text: 'Текст',
@@ -1551,22 +1727,57 @@ const ru = {
nsfw_clickthrough: 'Включить скрытие NSFW вложений',
autoload: 'Включить автоматическую загрузку при прокрутке вниз',
streaming: 'Включить автоматическую загрузку новых сообщений при прокрутке вверх',
+ pause_on_unfocused: 'Приостановить загрузку когда вкладка не в фокусе',
+ loop_video: 'Зациливать видео',
+ loop_video_silent_only: 'Зацикливать только беззвучные видео (т.е. "гифки" с Mastodon)',
reply_link_preview: 'Включить предварительный просмотр ответа при наведении мыши',
+ replies_in_timeline: 'Ответы в ленте',
+ reply_visibility_all: 'Показывать все ответы',
+ reply_visibility_following: 'Показывать только ответы мне и тех на кого я подписан',
+ reply_visibility_self: 'Показывать только ответы мне',
+ notification_visibility: 'Показывать уведомления',
+ notification_visibility_likes: 'Лайки',
+ notification_visibility_mentions: 'Упоминания',
+ notification_visibility_repeats: 'Повторы',
+ notification_visibility_follows: 'Подписки',
follow_import: 'Импортировать читаемых',
import_followers_from_a_csv_file: 'Импортировать читаемых из файла .csv',
follows_imported: 'Список читаемых импортирован. Обработка займёт некоторое время..',
- follow_import_error: 'Ошибка при импортировании читаемых.'
+ follow_import_error: 'Ошибка при импортировании читаемых.',
+ delete_account: 'Удалить аккаунт',
+ delete_account_description: 'Удалить ваш аккаунт и все ваши сообщения.',
+ delete_account_instructions: 'Введите ваш пароль в поле ниже для подтверждения удаления.',
+ delete_account_error: 'Возникла ошибка в процессе удаления вашего аккаунта. Если это повторяется, свяжитесь с администратором вашего сервера.',
+ follow_export: 'Экспортировать читаемых',
+ follow_export_processing: 'Ведётся обработка, скоро вам будет предложено загрузить файл',
+ follow_export_button: 'Экспортировать читаемых в файл .csv',
+ change_password: 'Сменить пароль',
+ current_password: 'Текущий пароль',
+ new_password: 'Новый пароль',
+ confirm_new_password: 'Подтверждение нового пароля',
+ changed_password: 'Пароль изменён успешно.',
+ change_password_error: 'Произошла ошибка при попытке изменить пароль.',
+ lock_account_description: 'Аккаунт доступен только подтверждённым подписчикам',
+ limited_availability: 'Не доступно в вашем браузере',
+ profile_tab: 'Профиль',
+ security_tab: 'Безопасность',
+ data_import_export_tab: 'Импорт / Экспорт данных',
+ collapse_subject: 'Сворачивать посты с темой',
+ interfaceLanguage: 'Язык интерфейса'
},
notifications: {
notifications: 'Уведомления',
read: 'Прочесть',
followed_you: 'начал(а) читать вас',
favorited_you: 'нравится ваш статус',
- repeated_you: 'повторил(а) ваш статус'
+ repeated_you: 'повторил(а) ваш статус',
+ broken_favorite: 'Неизвестный статус, ищем...',
+ load_older: 'Загрузить старые уведомления'
},
login: {
login: 'Войти',
username: 'Имя пользователя',
+ placeholder: 'e.c. lain',
password: 'Пароль',
register: 'Зарегистрироваться',
logout: 'Выйти'
@@ -1576,11 +1787,23 @@ const ru = {
fullname: 'Отображаемое имя',
email: 'Email',
bio: 'Описание',
- password_confirm: 'Подтверждение пароля'
+ password_confirm: 'Подтверждение пароля',
+ token: 'Код приглашения'
},
post_status: {
posting: 'Отправляется',
- default: 'Что нового?'
+ content_warning: 'Тема (не обязательно)',
+ default: 'Что нового?',
+ account_not_locked_warning: 'Ваш аккаунт не {0}. Кто угодно может зафоловить вас чтобы прочитать посты только для подписчиков',
+ account_not_locked_warning_link: 'залочен',
+ direct_warning: 'Этот пост будет видет только упомянутым пользователям',
+ attachments_sensitive: 'Вложения содержат чувствительный контент',
+ scope: {
+ public: 'Публичный - этот пост виден всем',
+ unlisted: 'Непубличный - этот пост не виден на публичных лентах',
+ private: 'Для подписчиков - этот пост видят только подписчики',
+ direct: 'Личное - этот пост видят только те кто в нём упомянут'
+ }
},
finder: {
find_user: 'Найти пользователя',
@@ -1685,6 +1908,7 @@ const nb = {
login: {
login: 'Logg inn',
username: 'Brukernavn',
+ placeholder: 'f. eks lain',
password: 'Passord',
register: 'Registrer',
logout: 'Logg ut'
@@ -1818,6 +2042,7 @@ const he = {
login: {
login: 'התחבר',
username: 'שם המשתמש',
+ placeholder: 'למשל lain',
password: 'סיסמה',
register: 'הירשם',
logout: 'התנתק'
diff --git a/src/main.js b/src/main.js
index 0c964dcc..75c2bab2 100644
--- a/src/main.js
+++ b/src/main.js
@@ -12,6 +12,7 @@ import UserProfile from './components/user_profile/user_profile.vue'
import Settings from './components/settings/settings.vue'
import Registration from './components/registration/registration.vue'
import UserSettings from './components/user_settings/user_settings.vue'
+import FollowRequests from './components/follow_requests/follow_requests.vue'
import statusesModule from './modules/statuses.js'
import usersModule from './modules/users.js'
@@ -44,15 +45,25 @@ Vue.use(VueChatScroll)
const persistedStateOptions = {
paths: [
+ 'config.collapseMessageWithSubject',
'config.hideAttachments',
'config.hideAttachmentsInConv',
'config.hideNsfw',
+ 'config.replyVisibility',
+ 'config.notificationVisibility',
'config.autoLoad',
'config.hoverPreview',
'config.streaming',
'config.muteWords',
'config.customTheme',
- 'users.lastLoginName'
+ 'config.highlight',
+ 'config.loopVideo',
+ 'config.loopVideoSilentOnly',
+ 'config.pauseOnUnfocused',
+ 'config.stopGifs',
+ 'config.interfaceLanguage',
+ 'users.lastLoginName',
+ 'statuses.notifications.maxSavedId'
]
}
@@ -70,6 +81,7 @@ const store = new Vuex.Store({
})
const i18n = new VueI18n({
+ // By default, use the browser locale, we will update it if neccessary
locale: currentLocale,
fallbackLocale: 'en',
messages
@@ -78,67 +90,87 @@ const i18n = new VueI18n({
window.fetch('/api/statusnet/config.json')
.then((res) => res.json())
.then((data) => {
- const {name, closed: registrationClosed, textlimit} = data.site
+ const {name, closed: registrationClosed, textlimit, server} = data.site
store.dispatch('setOption', { name: 'name', value: name })
store.dispatch('setOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setOption', { name: 'textlimit', value: parseInt(textlimit) })
- })
-
-window.fetch('/static/config.json')
- .then((res) => res.json())
- .then((data) => {
- const {theme, background, logo, showWhoToFollowPanel, whoToFollowProvider, whoToFollowLink, showInstanceSpecificPanel, scopeOptionsEnabled} = data
- store.dispatch('setOption', { name: 'theme', value: theme })
- store.dispatch('setOption', { name: 'background', value: background })
- store.dispatch('setOption', { name: 'logo', value: logo })
- store.dispatch('setOption', { name: 'showWhoToFollowPanel', value: showWhoToFollowPanel })
- store.dispatch('setOption', { name: 'whoToFollowProvider', value: whoToFollowProvider })
- store.dispatch('setOption', { name: 'whoToFollowLink', value: whoToFollowLink })
- store.dispatch('setOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
- store.dispatch('setOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
- if (data['chatDisabled']) {
- store.dispatch('disableChat')
- }
-
- const routes = [
- { name: 'root',
- path: '/',
- redirect: to => {
- var redirectRootLogin = data['redirectRootLogin']
- var redirectRootNoLogin = data['redirectRootNoLogin']
- return (store.state.users.currentUser ? redirectRootLogin : redirectRootNoLogin) || '/main/all'
- }},
- { path: '/main/all', component: PublicAndExternalTimeline },
- { path: '/main/public', component: PublicTimeline },
- { path: '/main/friends', component: FriendsTimeline },
- { path: '/tag/:tag', component: TagTimeline },
- { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
- { name: 'user-profile', path: '/users/:id', component: UserProfile },
- { name: 'mentions', path: '/:username/mentions', component: Mentions },
- { name: 'settings', path: '/settings', component: Settings },
- { name: 'registration', path: '/registration', component: Registration },
- { name: 'user-settings', path: '/user-settings', component: UserSettings }
- ]
-
- const router = new VueRouter({
- mode: 'history',
- routes,
- scrollBehavior: (to, from, savedPosition) => {
- if (to.matched.some(m => m.meta.dontScroll)) {
- return false
- }
- return savedPosition || { x: 0, y: 0 }
+ store.dispatch('setOption', { name: 'server', value: server })
+
+ var apiConfig = data.site.pleromafe
+
+ window.fetch('/static/config.json')
+ .then((res) => res.json())
+ .then((data) => {
+ var staticConfig = data
+ // This takes static config and overrides properties that are present in apiConfig
+ var config = Object.assign({}, staticConfig, apiConfig)
+
+ var theme = (config.theme)
+ var background = (config.background)
+ var logo = (config.logo)
+ var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask)
+ var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin)
+ var redirectRootNoLogin = (config.redirectRootNoLogin)
+ var redirectRootLogin = (config.redirectRootLogin)
+ var chatDisabled = (config.chatDisabled)
+ var showInstanceSpecificPanel = (config.showInstanceSpecificPanel)
+ var scopeOptionsEnabled = (config.scopeOptionsEnabled)
+ var formattingOptionsEnabled = (config.formattingOptionsEnabled)
+ var defaultCollapseMessageWithSubject = (config.collapseMessageWithSubject)
+
+ store.dispatch('setOption', { name: 'theme', value: theme })
+ store.dispatch('setOption', { name: 'background', value: background })
+ store.dispatch('setOption', { name: 'logo', value: logo })
+ store.dispatch('setOption', { name: 'logoMask', value: logoMask })
+ store.dispatch('setOption', { name: 'logoMargin', value: logoMargin })
+ store.dispatch('setOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel })
+ store.dispatch('setOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled })
+ store.dispatch('setOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled })
+ store.dispatch('setOption', { name: 'defaultCollapseMessageWithSubject', value: defaultCollapseMessageWithSubject })
+ if (chatDisabled) {
+ store.dispatch('disableChat')
}
- })
- /* eslint-disable no-new */
- new Vue({
- router,
- store,
- i18n,
- el: '#app',
- render: h => h(App)
+ const routes = [
+ { name: 'root',
+ path: '/',
+ redirect: to => {
+ return (store.state.users.currentUser ? redirectRootLogin : redirectRootNoLogin) || '/main/all'
+ }},
+ { path: '/main/all', component: PublicAndExternalTimeline },
+ { path: '/main/public', component: PublicTimeline },
+ { path: '/main/friends', component: FriendsTimeline },
+ { path: '/tag/:tag', component: TagTimeline },
+ { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } },
+ { name: 'user-profile', path: '/users/:id', component: UserProfile },
+ { name: 'mentions', path: '/:username/mentions', component: Mentions },
+ { name: 'settings', path: '/settings', component: Settings },
+ { name: 'registration', path: '/registration', component: Registration },
+ { name: 'registration', path: '/registration/:token', component: Registration },
+ { name: 'friend-requests', path: '/friend-requests', component: FollowRequests },
+ { name: 'user-settings', path: '/user-settings', component: UserSettings }
+ ]
+
+ const router = new VueRouter({
+ mode: 'history',
+ routes,
+ scrollBehavior: (to, from, savedPosition) => {
+ if (to.matched.some(m => m.meta.dontScroll)) {
+ return false
+ }
+ return savedPosition || { x: 0, y: 0 }
+ }
+ })
+
+ /* eslint-disable no-new */
+ new Vue({
+ router,
+ store,
+ i18n,
+ el: '#app',
+ render: h => h(App)
+ })
})
})
@@ -181,3 +213,15 @@ window.fetch('/instance/panel.html')
store.dispatch('setOption', { name: 'instanceSpecificPanelContent', value: html })
})
+window.fetch('/nodeinfo/2.0.json')
+ .then((res) => res.json())
+ .then((data) => {
+ const metadata = data.metadata
+ store.dispatch('setOption', { name: 'mediaProxyAvailable', value: data.metadata.mediaProxy })
+ store.dispatch('setOption', { name: 'chatAvailable', value: data.metadata.chat })
+ store.dispatch('setOption', { name: 'gopherAvailable', value: data.metadata.gopher })
+
+ const suggestions = metadata.suggestions
+ store.dispatch('setOption', { name: 'suggestionsEnabled', value: suggestions.enabled })
+ store.dispatch('setOption', { name: 'suggestionsWeb', value: suggestions.web })
+ })
diff --git a/src/modules/api.js b/src/modules/api.js
index c91fb97b..2f07a91e 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -7,7 +7,8 @@ const api = {
backendInteractor: backendInteractorService(),
fetchers: {},
socket: null,
- chatDisabled: false
+ chatDisabled: false,
+ followRequests: []
},
mutations: {
setBackendInteractor (state, backendInteractor) {
@@ -24,6 +25,9 @@ const api = {
},
setChatDisabled (state, value) {
state.chatDisabled = value
+ },
+ setFollowRequests (state, value) {
+ state.followRequests = value
}
},
actions: {
@@ -42,6 +46,9 @@ const api = {
store.commit('addFetcher', {timeline, fetcher})
}
},
+ fetchOldPost (store, { postId }) {
+ store.state.backendInteractor.fetchOldPost({ store, postId })
+ },
stopFetching (store, timeline) {
const fetcher = store.state.fetchers[timeline]
window.clearInterval(fetcher)
@@ -57,6 +64,10 @@ const api = {
},
disableChat (store) {
store.commit('setChatDisabled', true)
+ },
+ removeFollowRequest (store, request) {
+ let requests = store.state.followRequests.filter((it) => it !== request)
+ store.commit('setFollowRequests', requests)
}
}
}
diff --git a/src/modules/config.js b/src/modules/config.js
index 9a62905e..60a34bc1 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,16 +1,32 @@
-import { set } from 'vue'
+import { set, delete as del } from 'vue'
import StyleSetter from '../services/style_setter/style_setter.js'
+const browserLocale = (window.navigator.language || 'en').split('-')[0]
+
const defaultState = {
name: 'Pleroma FE',
colors: {},
+ collapseMessageWithSubject: false,
hideAttachments: false,
hideAttachmentsInConv: false,
hideNsfw: true,
+ loopVideo: true,
+ loopVideoSilentOnly: true,
autoLoad: true,
streaming: false,
hoverPreview: true,
- muteWords: []
+ pauseOnUnfocused: true,
+ stopGifs: false,
+ replyVisibility: 'all',
+ notificationVisibility: {
+ follows: true,
+ mentions: true,
+ likes: true,
+ repeats: true
+ },
+ muteWords: [],
+ highlight: {},
+ interfaceLanguage: browserLocale
}
const config = {
@@ -18,12 +34,23 @@ const config = {
mutations: {
setOption (state, { name, value }) {
set(state, name, value)
+ },
+ setHighlight (state, { user, color, type }) {
+ const data = this.state.config.highlight[user]
+ if (color || type) {
+ set(state.highlight, user, { color: color || data.color, type: type || data.type })
+ } else {
+ del(state.highlight, user)
+ }
}
},
actions: {
setPageTitle ({state}, option = '') {
document.title = `${option} ${state.name}`
},
+ setHighlight ({ commit, dispatch }, { user, color, type }) {
+ commit('setHighlight', {user, color, type})
+ },
setOption ({ commit, dispatch }, { name, value }) {
commit('setOption', {name, value})
switch (name) {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index b493c212..f980f53d 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,4 +1,5 @@
import { includes, remove, slice, sortBy, toInteger, each, find, flatten, maxBy, minBy, merge, last, isArray } from 'lodash'
+import { set } from 'vue'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
@@ -22,13 +23,22 @@ export const defaultState = {
allStatuses: [],
allStatusesObject: {},
maxId: 0,
- notifications: [],
+ notifications: {
+ desktopNotificationSilence: true,
+ maxId: 0,
+ maxSavedId: 0,
+ minId: Number.POSITIVE_INFINITY,
+ data: [],
+ error: false,
+ brokenFavorites: {}
+ },
favorites: new Set(),
error: false,
timelines: {
mentions: emptyTl(),
public: emptyTl(),
user: emptyTl(),
+ own: emptyTl(),
publicAndExternal: emptyTl(),
friends: emptyTl(),
tag: emptyTl()
@@ -58,6 +68,15 @@ export const prepareStatus = (status) => {
return status
}
+const visibleNotificationTypes = (rootState) => {
+ return [
+ rootState.config.notificationVisibility.likes && 'like',
+ rootState.config.notificationVisibility.mentions && 'mention',
+ rootState.config.notificationVisibility.repeats && 'repeat',
+ rootState.config.notificationVisibility.follows && 'follow'
+ ].filter(_ => _)
+}
+
export const statusType = (status) => {
if (status.is_post_verb) {
return 'status'
@@ -76,8 +95,7 @@ export const statusType = (status) => {
return 'deletion'
}
- // TODO change to status.activity_type === 'follow' when gs supports it
- if (status.text.match(/started following/)) {
+ if (status.text.match(/started following/) || status.activity_type === 'follow') {
return 'follow'
}
@@ -134,11 +152,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const result = mergeOrAdd(allStatuses, allStatusesObject, status)
status = result.item
- if (result.new) {
- if (statusType(status) === 'retweet' && status.retweeted_status.user.id === user.id) {
- addNotification({ type: 'repeat', status: status, action: status })
- }
+ const brokenFavorites = state.notifications.brokenFavorites[status.id] || []
+ brokenFavorites.forEach((fav) => {
+ fav.status = status
+ })
+ delete state.notifications.brokenFavorites[status.id]
+ if (result.new) {
// We are mentioned in a post
if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) {
const mentions = state.timelines.mentions
@@ -150,10 +170,6 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
sortTimeline(mentions)
}
- // Don't add notification for self-mention
- if (status.user.id !== user.id) {
- addNotification({ type: 'mention', status, action: status })
- }
}
}
@@ -176,45 +192,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
return status
}
- const addNotification = ({type, status, action}) => {
- // Only add a new notification if we don't have one for the same action
- if (!find(state.notifications, (oldNotification) => oldNotification.action.id === action.id)) {
- state.notifications.push({ type, status, action, seen: false })
-
- if ('Notification' in window && window.Notification.permission === 'granted') {
- const title = action.user.name
- const result = {}
- result.icon = action.user.profile_image_url
- result.body = action.text // there's a problem that it doesn't put a space before links tho
-
- // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
- if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
- action.attachments[0].mimetype.startsWith('image/')) {
- result.image = action.attachments[0].url
- }
-
- let notification = new window.Notification(title, result)
-
- // Chrome is known for not closing notifications automatically
- // according to MDN, anyway.
- setTimeout(notification.close.bind(notification), 5000)
- }
- }
- }
-
- const favoriteStatus = (favorite) => {
+ const favoriteStatus = (favorite, counter) => {
const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) })
if (status) {
- status.fave_num += 1
-
// This is our favorite, so the relevant bit.
if (favorite.user.id === user.id) {
status.favorited = true
- }
-
- // Add a notification if the user's status is favorited
- if (status.user.id === user.id) {
- addNotification({type: 'favorite', status, action: favorite})
+ } else {
+ status.fave_num += 1
}
}
return status
@@ -248,18 +233,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
},
'favorite': (favorite) => {
// Only update if this is a new favorite.
+ // Ignore our own favorites because we get info about likes as response to like request
if (!state.favorites.has(favorite.id)) {
state.favorites.add(favorite.id)
favoriteStatus(favorite)
}
},
- 'follow': (status) => {
- let re = new RegExp(`started following ${user.name} \\(${user.statusnet_profile_url}\\)`)
- let repleroma = new RegExp(`started following ${user.screen_name}$`)
- if (status.text.match(re) || status.text.match(repleroma)) {
- addNotification({ type: 'follow', status: status, action: status })
- }
- },
'deletion': (deletion) => {
const uri = deletion.uri
@@ -269,7 +248,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
return
}
- remove(state.notifications, ({action: {id}}) => id === status.id)
+ remove(state.notifications.data, ({action: {id}}) => id === status.id)
remove(allStatuses, { uri })
if (timeline) {
@@ -298,8 +277,69 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
}
+const addNewNotifications = (state, { dispatch, notifications, older, visibleNotificationTypes }) => {
+ const allStatuses = state.allStatuses
+ const allStatusesObject = state.allStatusesObject
+ each(notifications, (notification) => {
+ const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice)
+ const action = result.item
+ // Only add a new notification if we don't have one for the same action
+ if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) {
+ state.notifications.maxId = Math.max(notification.id, state.notifications.maxId)
+ state.notifications.minId = Math.min(notification.id, state.notifications.minId)
+
+ const fresh = !older && !notification.is_seen && notification.id > state.notifications.maxSavedId
+ const status = notification.ntype === 'like'
+ ? find(allStatuses, { id: action.in_reply_to_status_id })
+ : action
+
+ const result = {
+ type: notification.ntype,
+ status,
+ action,
+ // Always assume older notifications as seen
+ seen: !fresh
+ }
+
+ if (notification.ntype === 'like' && !status) {
+ let broken = state.notifications.brokenFavorites[action.in_reply_to_status_id]
+ if (broken) {
+ broken.push(result)
+ } else {
+ dispatch('fetchOldPost', { postId: action.in_reply_to_status_id })
+ broken = [ result ]
+ state.notifications.brokenFavorites[action.in_reply_to_status_id] = broken
+ }
+ }
+
+ state.notifications.data.push(result)
+
+ if ('Notification' in window && window.Notification.permission === 'granted') {
+ const title = action.user.name
+ const result = {}
+ result.icon = action.user.profile_image_url
+ result.body = action.text // there's a problem that it doesn't put a space before links tho
+
+ // Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
+ if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
+ action.attachments[0].mimetype.startsWith('image/')) {
+ result.image = action.attachments[0].url
+ }
+
+ if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
+ let notification = new window.Notification(title, result)
+ // Chrome is known for not closing notifications automatically
+ // according to MDN, anyway.
+ setTimeout(notification.close.bind(notification), 5000)
+ }
+ }
+ }
+ })
+}
+
export const mutations = {
addNewStatuses,
+ addNewNotifications,
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
@@ -316,6 +356,11 @@ export const mutations = {
const newStatus = state.allStatusesObject[status.id]
newStatus.favorited = value
},
+ setFavoritedConfirm (state, { status }) {
+ const newStatus = state.allStatusesObject[status.id]
+ newStatus.favorited = status.favorited
+ newStatus.fave_num = status.fave_num
+ },
setRetweeted (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
newStatus.repeated = value
@@ -334,6 +379,12 @@ export const mutations = {
setError (state, { value }) {
state.error = value
},
+ setNotificationsError (state, { value }) {
+ state.notifications.error = value
+ },
+ setNotificationsSilence (state, { value }) {
+ state.notifications.desktopNotificationSilence = value
+ },
setProfileView (state, { v }) {
// load followers / friends only when needed
state.timelines['user'].viewing = v
@@ -345,6 +396,7 @@ export const mutations = {
state.timelines['user'].followers = followers
},
markNotificationsAsSeen (state, notifications) {
+ set(state.notifications, 'maxSavedId', state.notifications.maxId)
each(notifications, (notification) => {
notification.seen = true
})
@@ -360,9 +412,18 @@ const statuses = {
addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false, noIdUpdate = false }) {
commit('addNewStatuses', { statuses, showImmediately, timeline, noIdUpdate, user: rootState.users.currentUser })
},
+ addNewNotifications ({ rootState, commit, dispatch }, { notifications, older }) {
+ commit('addNewNotifications', { visibleNotificationTypes: visibleNotificationTypes(rootState), dispatch, notifications, older })
+ },
setError ({ rootState, commit }, { value }) {
commit('setError', { value })
},
+ setNotificationsError ({ rootState, commit }, { value }) {
+ commit('setNotificationsError', { value })
+ },
+ setNotificationsSilence ({ rootState, commit }, { value }) {
+ commit('setNotificationsSilence', { value })
+ },
addFriends ({ rootState, commit }, { friends }) {
commit('addFriends', { friends })
},
@@ -377,17 +438,41 @@ const statuses = {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ .then(response => {
+ if (response.ok) {
+ return response.json()
+ } else {
+ return {}
+ }
+ })
+ .then(status => {
+ commit('setFavoritedConfirm', { status })
+ })
},
unfavorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: false })
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ .then(response => {
+ if (response.ok) {
+ return response.json()
+ } else {
+ return {}
+ }
+ })
+ .then(status => {
+ commit('setFavoritedConfirm', { status })
+ })
},
retweet ({ rootState, commit }, status) {
// Optimistic retweeting...
commit('setRetweeted', { status, value: true })
apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
+ unretweet ({ rootState, commit }, status) {
+ commit('setRetweeted', { status, value: false })
+ apiService.unretweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ },
queueFlush ({ rootState, commit }, { timeline, id }) {
commit('queueFlush', { timeline, id })
}
diff --git a/src/modules/users.js b/src/modules/users.js
index 8303ecc1..e90d6bb9 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -42,6 +42,10 @@ export const mutations = {
},
setUserForStatus (state, status) {
status.user = state.usersObject[status.user.id]
+ },
+ setColor (state, { user: {id}, highlighted }) {
+ const user = state.usersObject[id]
+ set(user, 'highlight', highlighted)
}
}
@@ -103,6 +107,8 @@ const users = {
// Start getting fresh tweets.
store.dispatch('startFetching', 'friends')
+ // Start getting our own posts, only really needed for mitigating broken favorites
+ store.dispatch('startFetching', ['own', user.id])
// Get user mutes and follower info
store.rootState.api.backendInteractor.fetchMutes().then((mutedUsers) => {
@@ -115,7 +121,7 @@ const users = {
}
// Fetch our friends
- store.rootState.api.backendInteractor.fetchFriends()
+ store.rootState.api.backendInteractor.fetchFriends({id: user.id})
.then((friends) => commit('addNewUsers', friends))
})
} else {
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 0d91851b..87315657 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -8,6 +8,7 @@ const TAG_TIMELINE_URL = '/api/statusnet/tags/timeline'
const FAVORITE_URL = '/api/favorites/create'
const UNFAVORITE_URL = '/api/favorites/destroy'
const RETWEET_URL = '/api/statuses/retweet'
+const UNRETWEET_URL = '/api/statuses/unretweet'
const STATUS_UPDATE_URL = '/api/statuses/update.json'
const STATUS_DELETE_URL = '/api/statuses/destroy'
const STATUS_URL = '/api/statuses/show'
@@ -26,12 +27,17 @@ const BANNER_UPDATE_URL = '/api/account/update_profile_banner.json'
const PROFILE_UPDATE_URL = '/api/account/update_profile.json'
const EXTERNAL_PROFILE_URL = '/api/externalprofile/show.json'
const QVITTER_USER_TIMELINE_URL = '/api/qvitter/statuses/user_timeline.json'
+const QVITTER_USER_NOTIFICATIONS_URL = '/api/qvitter/statuses/notifications.json'
const BLOCKING_URL = '/api/blocks/create.json'
const UNBLOCKING_URL = '/api/blocks/destroy.json'
const USER_URL = '/api/users/show.json'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
+const FOLLOW_REQUESTS_URL = '/api/pleroma/friend_requests'
+const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
+const DENY_USER_URL = '/api/pleroma/friendships/deny'
+const SUGGESTIONS_URL = '/api/v1/suggestions'
import { each, map } from 'lodash'
import 'whatwg-fetch'
@@ -127,11 +133,13 @@ const updateBanner = ({credentials, params}) => {
const updateProfile = ({credentials, params}) => {
let url = PROFILE_UPDATE_URL
+ console.log(params)
+
const form = new FormData()
each(params, (value, key) => {
- if (key === 'description' || /* Always include description, because it might be empty */
- value) {
+ /* Always include description and locked, because it might be empty or false */
+ if (key === 'description' || key === 'locked' || value) {
form.append(key, value)
}
})
@@ -153,6 +161,7 @@ const updateProfile = ({credentials, params}) => {
// bio
// homepage
// location
+// token
const register = (params) => {
const form = new FormData()
@@ -216,6 +225,22 @@ const unblockUser = ({id, credentials}) => {
}).then((data) => data.json())
}
+const approveUser = ({id, credentials}) => {
+ let url = `${APPROVE_USER_URL}?user_id=${id}`
+ return fetch(url, {
+ headers: authHeaders(credentials),
+ method: 'POST'
+ }).then((data) => data.json())
+}
+
+const denyUser = ({id, credentials}) => {
+ let url = `${DENY_USER_URL}?user_id=${id}`
+ return fetch(url, {
+ headers: authHeaders(credentials),
+ method: 'POST'
+ }).then((data) => data.json())
+}
+
const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
@@ -240,6 +265,12 @@ const fetchAllFollowing = ({username, credentials}) => {
.then((data) => data.json())
}
+const fetchFollowRequests = ({credentials}) => {
+ const url = FOLLOW_REQUESTS_URL
+ return fetch(url, { headers: authHeaders(credentials) })
+ .then((data) => data.json())
+}
+
const fetchConversation = ({id, credentials}) => {
let url = `${CONVERSATION_URL}/${id}.json?count=100`
return fetch(url, { headers: authHeaders(credentials) })
@@ -273,8 +304,12 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL,
mentions: MENTIONS_URL,
+ notifications: QVITTER_USER_NOTIFICATIONS_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
user: QVITTER_USER_TIMELINE_URL,
+ // separate timeline for own posts, so it won't break due to user timeline bugs
+ // really needed only for broken favorites
+ own: QVITTER_USER_TIMELINE_URL,
tag: TAG_TIMELINE_URL
}
@@ -331,7 +366,14 @@ const retweet = ({ id, credentials }) => {
})
}
-const postStatus = ({credentials, status, spoilerText, visibility, mediaIds, inReplyToStatusId}) => {
+const unretweet = ({ id, credentials }) => {
+ return fetch(`${UNRETWEET_URL}/${id}.json`, {
+ headers: authHeaders(credentials),
+ method: 'POST'
+ })
+}
+
+const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) => {
const idsText = mediaIds.join(',')
const form = new FormData()
@@ -339,6 +381,8 @@ const postStatus = ({credentials, status, spoilerText, visibility, mediaIds, inR
form.append('source', 'Pleroma FE')
if (spoilerText) form.append('spoiler_text', spoilerText)
if (visibility) form.append('visibility', visibility)
+ if (sensitive) form.append('sensitive', sensitive)
+ if (contentType) form.append('content_type', contentType)
form.append('media_ids', idsText)
if (inReplyToStatusId) {
form.append('in_reply_to_status_id', inReplyToStatusId)
@@ -413,6 +457,12 @@ const fetchMutes = ({credentials}) => {
}).then((data) => data.json())
}
+const suggestions = ({credentials}) => {
+ return fetch(SUGGESTIONS_URL, {
+ headers: authHeaders(credentials)
+ }).then((data) => data.json())
+}
+
const apiService = {
verifyCredentials,
fetchTimeline,
@@ -428,6 +478,7 @@ const apiService = {
favorite,
unfavorite,
retweet,
+ unretweet,
postStatus,
deleteStatus,
uploadMedia,
@@ -442,7 +493,11 @@ const apiService = {
externalProfile,
followImport,
deleteAccount,
- changePassword
+ changePassword,
+ fetchFollowRequests,
+ approveUser,
+ denyUser,
+ suggestions
}
export default apiService
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 14173558..c84373ac 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -42,15 +42,34 @@ const backendInteractorService = (credentials) => {
return apiService.unblockUser({credentials, id})
}
+ const approveUser = (id) => {
+ return apiService.approveUser({credentials, id})
+ }
+
+ const denyUser = (id) => {
+ return apiService.denyUser({credentials, id})
+ }
+
const startFetching = ({timeline, store, userId = false}) => {
return timelineFetcherService.startFetching({timeline, store, credentials, userId})
}
+ const fetchOldPost = ({store, postId}) => {
+ return timelineFetcherService.fetchAndUpdate({
+ store,
+ credentials,
+ timeline: 'own',
+ older: true,
+ until: postId + 1
+ })
+ }
+
const setUserMute = ({id, muted = true}) => {
return apiService.setUserMute({id, muted, credentials})
}
const fetchMutes = () => apiService.fetchMutes({credentials})
+ const fetchFollowRequests = () => apiService.fetchFollowRequests({credentials})
const register = (params) => apiService.register(params)
const updateAvatar = ({params}) => apiService.updateAvatar({credentials, params})
@@ -77,6 +96,7 @@ const backendInteractorService = (credentials) => {
fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials,
startFetching,
+ fetchOldPost,
setUserMute,
fetchMutes,
register,
@@ -87,7 +107,10 @@ const backendInteractorService = (credentials) => {
externalProfile,
followImport,
deleteAccount,
- changePassword
+ changePassword,
+ fetchFollowRequests,
+ approveUser,
+ denyUser
}
return backendInteractorServiceInstance
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
new file mode 100644
index 00000000..1480cded
--- /dev/null
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -0,0 +1,46 @@
+import apiService from '../api/api.service.js'
+
+const update = ({store, notifications, older}) => {
+ store.dispatch('setNotificationsError', { value: false })
+
+ store.dispatch('addNewNotifications', { notifications, older })
+}
+
+const fetchAndUpdate = ({store, credentials, older = false}) => {
+ const args = { credentials }
+ const rootState = store.rootState || store.state
+ const timelineData = rootState.statuses.notifications
+
+ if (older) {
+ if (timelineData.minId !== Number.POSITIVE_INFINITY) {
+ args['until'] = timelineData.minId
+ }
+ } else {
+ args['since'] = timelineData.maxId
+ }
+
+ args['timeline'] = 'notifications'
+
+ return apiService.fetchTimeline(args)
+ .then((notifications) => {
+ update({store, notifications, older})
+ }, () => store.dispatch('setNotificationsError', { value: true }))
+ .catch(() => store.dispatch('setNotificationsError', { value: true }))
+}
+
+const startFetching = ({credentials, store}) => {
+ fetchAndUpdate({ credentials, store })
+ const boundFetchAndUpdate = () => fetchAndUpdate({ credentials, store })
+ // Initially there's set flag to silence all desktop notifications so
+ // that there won't spam of them when user just opened up the FE we
+ // reset that flag after a while to show new notifications once again.
+ setTimeout(() => store.dispatch('setNotificationsSilence', false), 10000)
+ return setInterval(boundFetchAndUpdate, 10000)
+}
+
+const notificationsFetcher = {
+ fetchAndUpdate,
+ startFetching
+}
+
+export default notificationsFetcher
diff --git a/src/services/status_poster/status_poster.service.js b/src/services/status_poster/status_poster.service.js
index 3381e9e2..7f8b0fc0 100644
--- a/src/services/status_poster/status_poster.service.js
+++ b/src/services/status_poster/status_poster.service.js
@@ -1,10 +1,10 @@
import { map } from 'lodash'
import apiService from '../api/api.service.js'
-const postStatus = ({ store, status, spoilerText, visibility, media = [], inReplyToStatusId = undefined }) => {
+const postStatus = ({ store, status, spoilerText, visibility, sensitive, media = [], inReplyToStatusId = undefined, contentType = 'text/plain' }) => {
const mediaIds = map(media, 'id')
- return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, mediaIds, inReplyToStatusId})
+ return apiService.postStatus({credentials: store.state.users.currentUser.credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType})
.then((data) => data.json())
.then((data) => {
if (!data.error) {
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index bb5fdc2e..0e3e32d2 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -14,13 +14,13 @@ const update = ({store, statuses, timeline, showImmediately}) => {
})
}
-const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false}) => {
+const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
if (older) {
- args['until'] = timelineData.minVisibleId
+ args['until'] = until || timelineData.minVisibleId
} else {
args['since'] = timelineData.maxId
}
diff --git a/src/services/user_highlighter/user_highlighter.js b/src/services/user_highlighter/user_highlighter.js
new file mode 100644
index 00000000..ebb25eca
--- /dev/null
+++ b/src/services/user_highlighter/user_highlighter.js
@@ -0,0 +1,48 @@
+import { hex2rgb } from '../color_convert/color_convert.js'
+const highlightStyle = (prefs) => {
+ if (prefs === undefined) return
+ const {color, type} = prefs
+ if (typeof color !== 'string') return
+ const rgb = hex2rgb(color)
+ if (rgb == null) return
+ const solidColor = `rgb(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)})`
+ const tintColor = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .1)`
+ const tintColor2 = `rgba(${Math.floor(rgb.r)}, ${Math.floor(rgb.g)}, ${Math.floor(rgb.b)}, .2)`
+ if (type === 'striped') {
+ return {
+ backgroundImage: [
+ 'repeating-linear-gradient(-45deg,',
+ `${tintColor} ,`,
+ `${tintColor} 20px,`,
+ `${tintColor2} 20px,`,
+ `${tintColor2} 40px`
+ ].join(' '),
+ backgroundPosition: '0 0'
+ }
+ } else if (type === 'solid') {
+ return {
+ backgroundColor: tintColor2
+ }
+ } else if (type === 'side') {
+ return {
+ backgroundImage: [
+ 'linear-gradient(to right,',
+ `${solidColor} ,`,
+ `${solidColor} 2px,`,
+ `transparent 6px`
+ ].join(' '),
+ backgroundPosition: '0 0'
+ }
+ }
+}
+
+const highlightClass = (user) => {
+ return 'USER____' + user.screen_name
+ .replace(/\./g, '_')
+ .replace(/@/g, '_AT_')
+}
+
+export {
+ highlightClass,
+ highlightStyle
+}
diff --git a/static/config.json b/static/config.json
index 4dacfebe..144fe951 100644
--- a/static/config.json
+++ b/static/config.json
@@ -2,14 +2,13 @@
"theme": "pleroma-dark",
"background": "/static/aurora_borealis.jpg",
"logo": "/static/logo.png",
+ "logoMask": true,
+ "logoMargin": ".1em",
"redirectRootNoLogin": "/main/all",
"redirectRootLogin": "/main/friends",
"chatDisabled": false,
- "showWhoToFollowPanel": false,
- "whoToFollowProvider": "https://vinayaka.distsn.org/cgi-bin/vinayaka-user-match-osa-api.cgi?{{host}}+{{user}}",
- "whoToFollowProviderDummy2": "https://followlink.osa-p.net/api/get_recommend.json?acct=@{{user}}@{{host}}",
- "whoToFollowLink": "https://vinayaka.distsn.org/?{{host}}+{{user}}",
- "whoToFollowLinkDummy2": "https://followlink.osa-p.net/recommend.html",
"showInstanceSpecificPanel": false,
- "scopeOptionsEnabled": false
+ "scopeOptionsEnabled": false,
+ "formattingOptionsEnabled": false,
+ "collapseMessageWithSubject": false
}
diff --git a/static/font/LICENSE.txt b/static/font/LICENSE.txt
index c26be384..95966f00 100644
--- a/static/font/LICENSE.txt
+++ b/static/font/LICENSE.txt
@@ -19,6 +19,15 @@ Font license info
Homepage: http://www.entypo.com
+## Iconic
+
+ Copyright (C) 2012 by P.J. Onori
+
+ Author: P.J. Onori
+ License: SIL (http://scripts.sil.org/OFL)
+ Homepage: http://somerandomdude.com/work/iconic/
+
+
## Fontelico
Copyright (C) 2012 by Fontello project
diff --git a/static/font/config.json b/static/font/config.json
index fec1f988..20cb3254 100644
--- a/static/font/config.json
+++ b/static/font/config.json
@@ -4,7 +4,7 @@
"css_use_suffix": false,
"hinting": true,
"units_per_em": 1000,
- "ascent": 850,
+ "ascent": 857,
"glyphs": [
{
"uid": "9bd60140934a1eb9236fd7a8ab1ff6ba",
@@ -67,10 +67,10 @@
"src": "fontawesome"
},
{
- "uid": "9e1c33b6849ceb08db8acfaf02188b7d",
+ "uid": "1a5cfa186647e8c929c2b17b9fc4dac1",
"css": "plus-squared",
"code": 59398,
- "src": "entypo"
+ "src": "font-awesome"
},
{
"uid": "e99461abfef3923546da8d745372c995",
@@ -115,6 +115,12 @@
"src": "fontawesome"
},
{
+ "uid": "e35de5ea31cd56970498e33efbcb8e36",
+ "css": "link-ext-alt",
+ "code": 61583,
+ "src": "fontawesome"
+ },
+ {
"uid": "381da2c2f7fd51f8de877c044d7f439d",
"css": "picture",
"code": 59403,
@@ -179,6 +185,12 @@
"css": "globe",
"code": 59410,
"src": "fontawesome"
+ },
+ {
+ "uid": "b3a9e2dab4d19ea3b2f628242c926bfe",
+ "css": "brush",
+ "code": 59411,
+ "src": "iconic"
}
]
} \ No newline at end of file
diff --git a/static/font/css/fontello-codes.css b/static/font/css/fontello-codes.css
index b9447091..b1c76c3f 100644
--- a/static/font/css/fontello-codes.css
+++ b/static/font/css/fontello-codes.css
@@ -18,9 +18,11 @@
.icon-bell:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
+.icon-brush:before { content: '\e813'; } /* '' */
.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-comment-empty:before { content: '\f0e5'; } /* '' */
diff --git a/static/font/css/fontello-embedded.css b/static/font/css/fontello-embedded.css
index deee4990..bea63f38 100644
--- a/static/font/css/fontello-embedded.css
+++ b/static/font/css/fontello-embedded.css
@@ -1,15 +1,15 @@
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.eot?88606112');
- src: url('../font/fontello.eot?88606112#iefix') format('embedded-opentype'),
- url('../font/fontello.svg?88606112#fontello') format('svg');
+ src: url('../font/fontello.eot?99189355');
+ src: url('../font/fontello.eot?99189355#iefix') format('embedded-opentype'),
+ url('../font/fontello.svg?99189355#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'fontello';
- src: url('data:application/octet-stream;base64,d09GRgABAAAAACPYAA8AAAAAOmwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+L1NyY21hcAAAAdgAAAEIAAADSEWEciVjdnQgAAAC4AAAABMAAAAgBvH+5mZwZ20AAAL0AAAFkAAAC3CKkZBZZ2FzcAAACIQAAAAIAAAACAAAABBnbHlmAAAIjAAAF2wAACQGwkzh6GhlYWQAAB/4AAAAMgAAADYR4uGyaGhlYQAAICwAAAAgAAAAJAfKA/JobXR4AAAgTAAAAEwAAAB4bOn/9GxvY2EAACCYAAAAPgAAAD6Ae3UwbWF4cAAAINgAAAAgAAAAIAFzDaZuYW1lAAAg+AAAAXcAAALNzJ0eIHBvc3QAACJwAAAA6wAAAU29notZcHJlcAAAI1wAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZF7IOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGBxeMHwyYQ76n8UQxZzAsBQozAiSAwD5HgwpAHic5ZJLTsMwFEVPaCgFyi/8P9kBI5Qh6opYSxfCTroBRgw6iXSHdjdQrus3bFfAi04kPyv2U+4BjoGJeTctNL80lPpxt9n1J5zt+i3fXj9x7c5MnT40pGVapXUac5cXedwM2y2IQzt7q/GJn3uesnPkm1pPOOWEGaee45w5F1xy5Slu6LjljnseePQpz7zwyhu9P50euO0/1by8mq9Y9SWZSslVgf8xCooHCooLCoojCpwFCpwKCpwPCpwUCoo7CpweCsp0CpwoCpwtCpwyCpw3Cpw8CuwACmyD3avYCzRUbAhpWbErpFXF1pDWFftDGis2idxV7BR5UbFd5LFiz9gMFfo/D/FtAHicY2BAAxIQyJzwPxqEARKeA9sAeJytVml300YUHXlJnIQsJQstamHExGmwRiZswYAJQbJjIF2crZWgixQ76b7xid/gX/Nk2nPoN35a7xsvJJC053Cak6N3583VzNtlElqS2AvrkZSbL8XU1iaN7DwJ6YZNy1F8KDt7IWWKyd8FURCtltq3HYdERCJQta6wRBD7HlmaZHzoUUbLtqRXTcotPekuW+NBvVXffho6yrE7oaRmM3RoPbIlVRhVokimPVLSpmWo+itJK7y/wsxXzVDCiE4iabwZxtBI3htntMpoNbbjKIpsstwoUiSa4UEUeZTVEufkigkMygfNkPLKpxHlw/yIrNijnFawS7bT/L4vead3OT+xX29RtuRAH8iO7ODsdCVfhFtbYdy0k+0oVBF213dCbNnsVP9mj/KaRgO3KzK90IxgqXyFECs/ocz+IVktnE/5kkejWrKRE0HrZU7sSz6B1uOIKXHNGFnQ3dEJEdT9kjMM9pg+Hvzx3imWCxMCeBzLekclnAgTKWFzNEnaMHJgJWWLKqn1rpg45XVaxFvCfu3a0ZfOaONQd2I8Ww8dWzlRyfFoUqeZTJ3aSc2jKQ2ilHQmeMyvAyg/oklebWM1iZVH0zhmxoREIgIt3EtTQSw7saQpBM2jGb25G6a5di1apMkD9dyj9/TmVri501PaDvSzRn9Wp2I62AvT6WnkL/Fp2uUiRen66Rl+TOJB1gIykS02w5SDB2/9DtLL15YchdcG2O7t8yuofdZE8KQB+xvQHk/VKQlMhZhViFZAYq1rWZbJ1awWqcjUd0OaVr6s0wSKchwXx76Mcf1fMzOWmBK+34nTsyMuPXPtSwjTHHybdT2a16nFcgFxZnlOp1mW7+s0x/IDneZZntfpCEtbp6MsP9RpgeVHOh1jeUELmnTfwZCLMOQCDpAwhKUDQ1hegiEsFQxhuQhDWBZhCMslGMLyYxjCchmGsLysZdXUU0nj2plYBmxCYGKOHrnMReVqKrlUQrtoVGpDnhJulVQUz6p/ZaBePPKGObAWSJfIml8xzpWPRuX41hUtbxo7V8Cx6m8fjvY58VLWi4U/Bf/V1lQlvWLNw5Or8BuGnmwnqjapeHRNl89VPbr+X1RUWAv0G0iFWCjKsmxwZyKEjzqdhmqglUPMbMw8tOt1y5qfw/03MUIWUP34NxQaC9yDTllJWe3grNXX27LcO4NyOBMsSTE38/pW+CIjs9J+kVnKno98HnAFjEpl2GoDrRW82ScxD5neJM8EcVtRNkja2M4EiQ0c84B5850EJmHqqg3kTuGGDfgFYW7BeSdconqjLIfuRezzKKT8W6fiRPaoaIzAs9kbYa/vQspvcQwkNPmlfgxUFaGpGDUV0DRSbqgGX8bZum1Cxg70Iyp2w7Ks4sPHFveVkm0ZhHykiNWjo5/WXqJOqtx+ZhSX752+BcEgNTF/e990cZDKu1rJMkdtA1O3GpVT15pD41WH6uZR9b3j7BM5a5puuiceel/TqtvBxVwssPZtDtJSJhfU9WGFDaLLxaVQ6mU0Se+4BxgWGNDvUIqN/6v62HyeK1WF0XEk307Ut9HnYAz8D9h/R/UD0Pdj6HINLs/3mhOfbvThbJmuohfrp+g3MGutuVm6BtzQdAPiIUetjrjKDXynBnF6pLkc6SHgY90V4gHAJoDF4BPdtYzmUwCj+Yw5PsDnzGHQZA6DLeYw2GbOGsAOcxjsMofBHnMYfMGcdYAvmcMgZA6DiDkMnjAnAHjKHAZfMYfB18xh8A1z7gN8yxwGMXMYJMxhsK/p1jDMLV7QXaC2QVWgA1NPWNzD4lBTZcj+jheG/b1BzP7BIKb+qOn2kPoTLwz1Z4OY+otBTP1V050h9TdeGOrvBjH1D4OY+ky/GMtlBr+MfJcKB5RdbD7n74n3D9vFQLkAAQAB//8AD3icxVoLjFxXeT7/Oee+79x53bl3H7OzszM7M/vKej0zO+PYznq8fiebeGMvzq7jmI1jO8Qb2zwCMRDTlLhWImicQkqRKhJFDRGFQvOgQMVTTULTUFVQwETQSuUhmkBrUIUEuPGk37mzazuEFLVS1d3ZO/fc8/rP//j+x11GjL3yM/4tfpoNslyrt9CdMCTjtE0QZ/w4ofuwn/V9qXWNlvw46YVVZKhLub6BKurSqPZTU10CdIcB/1Z8JjGWePRRXGYS6jtxqR2PP/po/M2BuvnoR+OvHRgfVwOYBE1PiEfEODNZkg2zFtva2jSJfS3GQdU2ZunWcZN0Qz/ODGEcxwQu5zQSIJcLtsik5PN4xGeuWl+sFQvV0pVdKVvrGy3Vyx7PUaO58p3x9eJAoVyZrDfCWo7WUbXRrFUDoY8Suoyi6sKlc8qAP+/nfN7V0/WHfj7Fg2zX1nzw8tfCHOWDc26jeKbQiJ0L8p+xus748TNxn86E6eR5O2efT/V5AU/lU7LHXbm576kgnw9wof6hof4c7QrOY0bgnR/DFPt8kuFHyeZ58KHF+llfqycVt6XQlHDYRdn0+aHQwlEC79MZ36NIOuXJejNdUddSJBktEI/En59wM+5/nncDlya+5vVT13ucvHuSuvL0kht/tv2S6yTIOHXKSNnSpPDZuJvRhtph2B7CjhfpsCCNSmsw2+17Mcs0dE2Q+2qCSoNhkEoIzR+l5iqCRhjNMN2hrlh4Her4uz/+0yO3/dsnhr/5zTboDO3fTmfhscK3v1147KfHj9OTHZKzr0MwfhTNv5IT/C42wDax6daGAkldqTVIMEg/apEuDV0umdBzg7ixqLROzkF12LxGaMxMt4KBUtdAkBlKR7rj6xWoyioap1qyWBinZaVQapIZUHcr9lGuN9bR5EDnrjlQDfopR5kk9Io/b5sXXtJ0DuuiJcjbfAqHe9IKvCXaaGkLkg6ZT7p55ykTT9pfUk9sk3fJaMKSFziGS1xIcmln0Gufdd2zdtans/rt2g9i9tlY7KzdG5w1lrSYjWEaN0X7yQC8AENeeVq8yJ+A/HrYFNvMbmA3tObqvZzJ3TpMatcmTnx2ergCo9JJbmOa1I6DhTAnOspIx2eJ6QKfJSbEsctYxRSnZq5Jj3cP+n2G1jNaao5Ts97UjYDqZaOgZ/yg2oB51WBZfkbnYFGxEEl/XOFHc4pq1bCJbnApMII02JkOQh9C8qiI3ma50swBV6gxOrGGCne/YT8dSThbDyaCxOYJJ/H8up+sy2q2sdnqnj1ddZw9L/9Jtdqv2cJzBh2yMvM7PiLPO0Fl7p/uGr7zuS0bbypOHsg7t+0sHrlq09qNpx6gW6H2B7c4iYQzsTnxdkm3tffeVrUqum2MDJ64NjmSuufDdsPSdV8nrX3hurt7qat7fzo9eMXikavtU7cdbG0YPNBId/TtH0Sen2Nx1st2tLZKJQLBtaOWwTVdaDpUjTHBmdjPdCJ9FwOr58FZAvoRS/Qmenu6u8Ig46dTSUtncfJsZUXVIJPsoNRkEmykyeJkMVPM1DK1Sf6FkbVrRy58bGj9+iFefu7gc88d5OcuPpgfWdueUs+ee27ZHp4Qe4UDTD3CtrSmDy3MbJRMrrM5sfpQb0IKEh3BQwOYPK7g5zgDph4H0QLawA/vu3H39Tu2j44U8umUoQWjkHDBI8i3BLCEYI0gDHzIrQLVBzgbQFFQXSlXYPW4RtJuRhakABf20yyvqEA/GvgFACs9gEJUw+XFjMh8+LpdJ3bxPXfsoaxpvMl20kO6Fp+NGca13T2WIRN3mW6iN9ypJ/StgdTMITtuHjZMsrU3mV5Y6ow1r+3qsUyRvAtWFM+GO7W4sd2X0uoMtmn/urm5d8zNnVD9iVymt6p7emaWtPUxcyabsI1bLXe9prdymqe71Xi2N06uEY3t7slfYbiGP3vZUGedpm3KLg/tSQAmOzL4tVjiT7MKfBowyQcMADx1rnFdO8qUusDCpGBCsqPK+nTii6oh5mB3ytoEmwmK3aWRUsXQeoFJAdSElJ9SyqG4lwmjR5ViQTeSfhDWqjlOPhCvUL6KiuoCTKqB/UFIAR2C3ZNpPrlp//5NT5o2UadZqlNj8NM6BzboTvsbTjY47wXwTUHWoVXOOE9pniVp/ya6d9N+23QsHcyFMrRPYKLkJo15Tvvrth9/JPDOAuUegdOz8GDFp/2r+AwfZj7rbgUxYuAAuMCgZyrWCFPKnwGdCxXqhBehtQy04hPtA/Cm7QOOcxO+aYiGnKy7z6Ez7Vschz7i5Ox9jtN+AY+dfU4We73SfuUO8TmxyFaz/lZW7U1z2I/NQ+eJzRAbGWarabXyVmGhAuCiRqjAxlAcLDfQxK0eBk11C11FI8eXn/7i8Obtcg/9fHb/2Fa3Z7ZdHlrM5/Qxmumq97Q/Pdblul0BfbeaX99otFPT8uDpq+nnqiux6/3bt35lPyb2uFvHFtVEO991cISu66l3YWKPyaWa+I6EV22nZk4fkC061zWuJir+yVdeeeUJuSqy4zjiglXsDa1dfT4nEceZvJhrSc6yGQQ/UnllFRkdZfB4gnTwARzA0eHrNC0CcG1eWfxMMjE+Wi52h4n+ZH86nTKjiMJT7itHlBmYbIZUGugoFPxZo5Ksl0OAkgFQaiY7Po4OTe2dwoevf/nck3upj3Iv3wObcnVxEiZiX18vvXzPYIPqJXGyVOfJK6b49J5pubZ9/vzSUwvU9wgc41410OSPmXbqwt5IBflj6os5OPOR6MydE29gO9kbAU6/x86wh9lfsmda3Q+0uGXee/diXmryxBou+OwEE3Dj2655fGB2vtVkmZTLTStjLqbJSpDULLmYjHHYJ1eecjFOwgb/EDw6BrTRn2e+H/N39Lam/mczfZ/mLq5A/sxCq/ypTzz2Zw/96YcevP/9p0+95653vO3Y0uGD+2/cM3fdNZOTk2X8TtYCxBfhJPwlrLaP/EDFoYDIMvAzaiMmjdqV5X5YdYMgBHgFHYIIahAK/cb8lbaR6bQFxhvL40OMD5fXV/1q/eby+qodLrcvn99c9kIrAj/rx7crUMCFfustXxd47d3RI/pk3L/w7KUukQy8bVG4i+u3XzXsO5f1vN51+6uUafTStj+6RMaPL5vTvplyqqP9A1z5H22Loz+xDfcX3ndpLn2e+qKO9g/VnL/77Uv96NLkWy+kSvV6iZ+LdFTh2lf4HWIzcC1s+VaEa2wF1rIpDn9pLYeHTWsF2oBq/AgALevcBAQbar+wDG0P2QhEbrbtm9BDwwrn1AA1cAVDv8I/vLIXvXqvMIz24kEUjioUbS4DKL+//R0a7qyqUBTb5OybbP7x9gvt70S3Nj0cbR+RofaBx/kM39rBao1eHeqHfoTVJeXdLx5t+VTiE/sAx1j3heWzPaRO8pCztA97DGM3W/WDAHv5UAJnOiKeEbuZixh1mr3UchlCc9o21Ivgd+s1j3sw41GDQARphyIYuwWTCBmIyvfMeWaa7nZEKfo8AquYvqO3Y/ljr5rCl37XnEHMGX69ORyuWt93caoEPqz6zbEm/LcpFy/N0XU+t7wH12cWFhZabn8hNRQki+mUBcPX6nDLzXpB2WG1NFBO1sd5weOZhOYjgPJVwqT8+5RswvAQOE8RYkQDbsrPCbpgDUxQau2Q1X4/P/vHPfVdt++q9/DHRvrOI5Q53zeSHZ8YTPFTt2n5sbx25L0UFCYmFs2JAcsaXkt//jEazq5fUyisWZ9tf+djfSMIgNaN9HVV5/bfe+3cBxK2E+Z4IePYiQ/MXXd6cVd9JYbh9wCLDWDxSKuCoAWCwtGP4pBIDYgiz6JCW0kzxXSpkU7oSAzSAwAUj0Jt2XHAiSDnRiBSDYwMwv+nqA8kE309H1x4Kcqpkw/+/Yd4CrcfvX3dHJ+96pH2lwI8z9A0subbDz/44OHbc0y8cgFx7QLocemL9Et+5zWPW7PzG9ezL7LPs8/BPXyI3ct0pV5wFqASd99l30R0tcCuh5pNsRrLs25m4zicHqIP04fo/fQ+ehe9nQ7RLYD177N/gUrqSBJ307U0hPkm4vdf0PfoG/Q1+gp9idZQDc9IPWfboEI29t+0vPu98MAqH/0i+gXu/u9pMNg2nJmwF7Gtvf9/jFhYiCTRmmRcGIIbR5mhC0PlkabQTdgJCZOWgFzHgJUIbucslR7Na5Ij/J3psLG1VhJ8rCYOMW5o3FjCGlpnDa2zhnZpDU3rrKHtwdm1q3v/lzsvLGzsjiLFF+gs/TV9lm6gPexv2bPsr9in2RPsU+yd7B3gkQ4+AqHwZ2M7laflVMhUGOdTpELy6hRNIttphGWV6GwgvTzpG/WyPjkuFU6qSog/Qn5BLxiNSrmI6LI2zhGC4jGgWs/hBvCtciC9gJuyyp8M9VctG1NUVItWApVCwX5qQb1SjQbooRqMDSpYFqtWyqqdI2RTyMULemAg9wqUm0dC1qyHFd2oqqXCZojJRmCAAkzVjRz3m4ERJWFGpawHNbVOPwhq6v0ix0NdrTeJUYiNK+N8UmVwiI1roLuak/0iqGJVTG4WouIH0KoxiVVwUacvN8JqA8fFsXw9U2woZ4jnRsHwRBkkqHZF0YXAo45zBA2sBIKDZo6DO41mAFSYIuSWk+Oqmhdxo4oRBVCDdDJQ12bQKE9RptkoKhoVg6uTYIgAiMJVNZCHqk+ccLIM+DUOqcWp3Cgrvjf0jEcZJARRNoBMNvT1gD55x1ff+tavvvjcMf1dX6A0NwVxKZKZNMJcbuoCIpPS1nRJJgBRCIkfnXQEj5rUMZJMl7SsFBw5FjbjhoUhyJcw0eZSiwnhe2lpIucjrlmc0pYuuabbcCdQfqFbWA3RpyaQJkryDCcuEwKrSpNM9YWFBcL+lCZcF9tzt7tX6JqW1oQjYw420qUpLXl9Vap0U1CXDRo0qehUeShx2zBS0rCU/+Ie2txDGsHjpsDSQiMJ140VNNfgwhSWEei6ZpoJ6WMdLC48IZFwm0mb44c0jhYXrkA+qFgFQ3SwDzd9gUSTq3Nr4BI+JLuEJUCAiHFPsUOiRwcN4JOUhqkZrkQDybAWEeJKnsJ0hOAW57YJVum6oVmufdvbZsmlGOZnFGwoRmsubB4/pCi3ISEOVmMQCJFOnLhlk3DueOYnz9wRXdr/TCZXJTBTaA6GYQnkJEbEV+K6q+ngK1yciB7gnpuKrYSTQ9aGMA3bkJquuUo1cDTXAlM0HEEkufBM9VxYEKvQyZM2ltRwLFsahkGWZhommCQUL6EOthCe6tYk0gnbjHOhwMwDA6SOXxBxxU6ppC71uA0akMd5lu9w0ns4vKzUkdUKkQCPpamZkpyumObi1NI1PemR7fjI2TWwHLJICVtKS9Ul7YjBPGGmlP6CDtvwIlGC3wktrrCYOzg0mrLLszzNUmVVsBpMh5loPA4dIVWuFMgiJTfBSI/btqbqlo6lKdWADHBmCYMAC3TC8TBRyR2XdizzBnVmVShUdgBWc1sg1dLAXaRcaozSJ7WOljWTlme5XCbgsxAHHBE/FbtYhc2yx1veYACL5DPTdaW9y2lgmUWkiqPIh7GhXFLvE+Z1oD3CwE42rMW0lTCw+JrRUmXL+y5N4ggAh39zlN5JsvlrsmxEfuHwELFtW1avGpodnvVTrs0qVDFVmU+Brm74QT8pmFK1MMREoaGrctgUqZoZwKlSpkImqqypGmqlGYGYRwpiNxDQtlZVE5t1PKZz97zl6KYtoEDOpbXJ2u4bbtl5f32txd1fOr4t1/KUtXHz3n1Uizr33DK7fcvkOpM7v1rutVub9950+L1vOTYdrSF2taaWjv2+CVhKHdh9/arVU2uutNKiKqwg8UPT0ddvLQ+1Zacrn3ttn5r9XtPkUUrBkc/fKH4CWfWzjWxHSxkCIv3VRJs7nE9fqj3TMaE4zcDpuFBhy3E858fRcXihFSM2kPfTrJ/65QoTV4MDym2FgYLtHKmaY0M5FcW8iMl60OkHp1ajVYl8a0MNKtN/3HD93OY9tx+59cjO6YEBveT1JGpJYfMilcoP7L+xrXXFFagP8sHy9hvffec7T96sBi9hcF4rmbqXErv6clduyfi5/M7pPbufun64N0FJEdf3Pr1w0wPlUvtcQupm1Np+42Chq/v6y8ZmBrwUu1gnfDHS5Q3sZCs9RKQnYQzNcYDDAGxA1TUUowYZPATA4mLtEBySUc1QZVT7kNW4yGpaYwjU9OP/3djL6osLLXttb2myUaqpEiNlLhWfVdrfR1G7U1wcqDZUPVHVHdPJ6O3TxTSwUq43ahgvUvs3tSdUfZHuM2zbaN8ZVXbo66W6bQ6a9tkg6xxoP6glZAu4e/sBJ/CQh/s0E9Uk6evT+ykaVy+1J6KZT5pwQ99VSTp3MVHXW5oXTcwGwXIe+Wuxlz+zXCtqtupDJDVTaRTfhlANziXKzpTvWbysJKjS6hlVjinXVLqiStrG8qtBdW6R6bz/iV4VLreLl7cX/MTLP4sKBSIZ1Qhet3XwsnoCJS5WIMgnT9UVvKjEsGInT4iX+NOIy9ewK1oj6v2nQAreKchHZvBq+mH6g2trUtXmL9bnlK7neLNWVYaAXzxHiONxRIFRLV5Fi7COZXG9WC+9/Pxgg7r7n5/Jlzf38uz0UP8bP5vvagz/Y33SLeRi3M0lc7GC/sHFVHE9jY+KBoZ/tb2lI9fP9wb3Nbt6stSTDTffFXx5bLbvA8WKlYIbtVNmVhya9sJdg6NrV3LJI+JFnC9kV7EDLbeuAKLsKM+9jNghQBTptWRKVMqtQHQiJqDTOcWFN1/WDW+vOKHe/AL3F1pJYuvWDhb6elMJFlKoRwCByFxhLFAAMaMCzCk+TipwB6AqbIje+CIIjGL2Kb5BvatAxp3PEf3irV+9g2Z3TMRjPW/Y0pUvF9DmJ/6G7j7943srI8c+2DsoTA9uETGPjPmGnzDi8wfp9I8p8ePT/K7rTs1MvXU4O1kbH1yfEdp1pz586rr2D25+dFHeXDala8GnSRHXvMDMZtMj1Q/MoWvx0U6t5wlgwTgbZcOtchl+VimAhALAKR9XqffxqCSjSj/FMH1VqGndoyXf8HjnXStEPlkf1yJDvlgNUil3PoS0t5GN+AQhGwm/sGZqz57mST9vtX/oONTnZLv4Sbp/b+77Nz0sUwlpu/BUoty/Zm9rIpfSz3iBQzlVLsrZfvzM96+JaFW1gd2syA53ACoPAcH5saPw+oyQk0Z5F9ysmIcgXfjaVum3DxEYo1BJOU8B59lKE8v393b56YRn6axIRaPzeiz8zYICchC/8+qpXi7oYYZfG5UTLq8q+PFBL4jKDR/NhZeXFZ669UH+wG3KClUt5KnOu9MjERYnmM9KQJQr2fdaYW2YGyaQmPdlYi7CArFNkqbKVEppr9BdhNwIbAxSVRHN4NoSFjI0tmSRYZjzNqnCkwS/Ymwl2hh7/Ulq4LHLZhrgWvV3DMdAjJ9T4409mGsaVyP+yCeTjDUnq6uvGB2qDBb6c73dST/pp1M4XbwZ0zId3LjI0TRAIVlM0sUH6q9WDUuZ4mQH97WLd3Rf4C1XT98nY/Tw/VEBWDXx+feYbE89a5uPIPa/s/PNH27Poaf95QgNqY9edNsn6N622ynAerQRf3/hfvLkSSC+GV2X8f0JeUK4zGRjbCe7s3XHWAmZS77fQzheTXOE7tsYGbAKxOLHPWJ2zGaxo8yJ8ZjDjwKHWMyxY4uIWjUSJheLDOGsOcdMU85bKk2YIXbtNVdv37J544ZGbfWq4aHBQrY3zKQSNhI5hmQrHkEJkssc8qiaMib/0j+MRG/ZIq9Ri/6rBAoaVfAzHTdan9LCKhSxGkV4ITLxDN238B7+rs+c0E/R089E70CecfUl0342en8CZi3hpn1opO9M+cp216Zd0k3lymsHHGds7uDcmOPsmDjZN0L73/P43fyuT79rx2vndhZtf7lvjP4ge92m3JrpxppCD7cL+LEbI33svwDcxj83eJxjYGRgYADiOwmhRfH8Nl8ZuJlfAEUYrqv7c8Lo/3//R7NUMCcAuRwMTCBRAEPOC4YAAHicY2BkYGAO+p/FwMBS9v/v/88sFQxAERQgBwCiSgbDeJxjfsHAwCwIxAsQmEUfSIPEFYA4EioO4q/+/49F//9/EGY6xcAAwmBxIGZqAqn9/5f5BRC//P8fLA7S+wLKhqkB0ixlDAwA5fkhKwAAAAAASgDOARIBbAHyAqQC9AO2BDgEbgTYBVIGpAbaBw4HRAgYCGAMZA06DcoOaA7ODz4P0BAmEJARWBIDAAAAAQAAAB4B+AALAAAAAAACACwAPABzAAAAqgtwAAAAAHicdZDdasIwGIbfzJ9tCtvYYKfL0VDG6g8MRBAEh55sJzI8HbXWtlIbSaPgbewedjG7iV3LXts4hrKWNM/35MuXrwFwjW8I5M8TR84CZ4xyPsEpepYL9M+Wi+QXyyVU8Wa5TP9uuYIHBJaruMEHK4jiOaMFPi0LXIlLyye4EHeWC/SPlovknuUSbsWr5TK9Z7mCiUgtV3EvvgZqtdVREBpZG9Rlu9nqyOlWKqoocWPprk2odCr7cq4S48excjy13PPYD9axq/fhfp74Oo1UIltOc69GfuJr1/izXfV0E7SNmcu5Vks5tBlypdXC94wTGrPqNhp/z8MACitsoRHxqkIYSNRo65zbaKKFDmnKDMnMPCtCAhcxjYs1d4TZSsq4zzFnlND6zIjJDjx+l0d+TAq4P2YVfbR6GE9IuzOizEv25bC7w6wRKcky3czOfntPseFpbVrDXbsuddaVxPCghuR97NYWNB69k92Koe2iwfef//sB6XOEUwB4nG1O2XLCMBCzIHeh933QL/BT+0OOswQPG9t17FL+vk14rR60GmlHI7EQJzTif2ywwBIZchQoUaFGgzOssMY5LnCJK1zjBre4wz0e8IgnPOMFr3jDBu+i0Mpq4iJ5dqrLxqhCM5GkwcdjGSgeiGJJR5Juu115TqMcv5IK1C216wt2vUux7tzBSufJFipGpXelNzqmQPm36cg1wfS7OOc10/akyuTnm7XEnLHT+7xn11I+emM/Zv6s2Ni9pJ+YDWRTNSjDUnFcazf8GfE0Mg/k+bieGubC6aNpjXU6sQpjnUYKchouxC8ABVcBAHicY/DewXAiKGIjI2Nf5AbGnRwMHAzJBRsZWJ02MTAyaIEYm7mYGDkgLD4GMIvNaRfTAaA0J5DN7rSLwQHCZmZw2ajC2BEYscGhI2Ijc4rLRjUQbxdHAwMji0NHckgESEkkEGzmYWLk0drB+L91A0vvRiYGFwAMdiP0AAA=') format('woff'),
- url('data:application/octet-stream;base64,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') format('truetype');
+ src: url('data:application/octet-stream;base64,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') format('woff'),
+ url('data:application/octet-stream;base64,AAEAAAAPAIAAAwBwR1NVQiCLJXoAAAD8AAAAVE9TLzI+L1OAAAABUAAAAFZjbWFwRboj/wAAAagAAANkY3Z0IAb//vQAADAAAAAAIGZwZ22KkZBZAAAwIAAAC3BnYXNwAAAAEAAAL/gAAAAIZ2x5ZniVdesAAAUMAAAldmhlYWQSx7ccAAAqhAAAADZoaGVhB9ED+wAAKrwAAAAkaG10eHQb//QAACrgAAAAgGxvY2GSq4ZgAAArYAAAAEJtYXhwAXUNpgAAK6QAAAAgbmFtZcydHiAAACvEAAACzXBvc3TmYbBhAAAulAAAAWRwcmVw5UErvAAAO5AAAACGAAEAAAAKADAAPgACREZMVAAObGF0bgAaAAQAAAAAAAAAAQAAAAQAAAAAAAAAAQAAAAFsaWdhAAgAAAABAAAAAQAEAAQAAAABAAgAAQAGAAAAAQAAAAEDoQGQAAUAAAJ6ArwAAACMAnoCvAAAAeAAMQECAAACAAUDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFBmRWQAQOgA8jQDWf9xAFoDZwCeAAAAAQAAAAAAAAAAAAUAAAADAAAALAAAAAQAAAHgAAEAAAAAANoAAwABAAAALAADAAoAAAHgAAQArgAAABgAEAADAAjoE+gy6DTwj/DJ8ODw5fES8T7x5fI0//8AAOgA6DLoNPCO8Mnw4PDl8RLxPvHl8jT//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAYAD4APgA+AEAAQABAAEAAQABAAEAAAAABAAIAAwAEAAUABgAHAAgACQAKAAsADAANAA4ADwAQABEAEgATABQAFQAWABcAGAAZABoAGwAcAB0AHgAfAAABBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAGEAAAAAAAAAB8AAOgAAADoAAAAAAEAAOgBAADoAQAAAAIAAOgCAADoAgAAAAMAAOgDAADoAwAAAAQAAOgEAADoBAAAAAUAAOgFAADoBQAAAAYAAOgGAADoBgAAAAcAAOgHAADoBwAAAAgAAOgIAADoCAAAAAkAAOgJAADoCQAAAAoAAOgKAADoCgAAAAsAAOgLAADoCwAAAAwAAOgMAADoDAAAAA0AAOgNAADoDQAAAA4AAOgOAADoDgAAAA8AAOgPAADoDwAAABAAAOgQAADoEAAAABEAAOgRAADoEQAAABIAAOgSAADoEgAAABMAAOgTAADoEwAAABQAAOgyAADoMgAAABUAAOg0AADoNAAAABYAAPCOAADwjgAAABcAAPCPAADwjwAAABgAAPDJAADwyQAAABkAAPDgAADw4AAAABoAAPDlAADw5QAAABsAAPESAADxEgAAABwAAPE+AADxPgAAAB0AAPHlAADx5QAAAB4AAPI0AADyNAAAAB8AAQAA//YC1AKNACQAHkAbIhkQBwQAAgFHAwECAAJvAQEAAGYUHBQUBAUYKyUUDwEGIi8BBwYiLwEmND8BJyY0PwE2Mh8BNzYyHwEWFA8BFxYC1A9MECwQpKQQLBBMEBCkpBAQTBAsEKSkECwQTA8PpKQPdxYQTA8PpaUPD0wQLBCkpBAsEEwQEKSkEBBMDy4PpKQPAAQAAP+4A6EDNQAIABEAKQBAAEZAQzUBBwYJAAICAAJHAAkGCW8IAQYHBm8ABwMHbwAEAAIEVAUBAwEBAAIDAGAABAQCWAACBAJMPTwjMyMiMiU5GBIKBR0rJTQmDgIeATY3NCYOAh4BNjcVFAYjISImJzU0NhczHgE7ATI2NzMyFgMGKwEVFAYHIyImJzUjIiY/ATYyHwEWAsoUHhQCGBoYjRQgEgIWHBhGIBb8yxceASAW7gw2I48iNg3uFiC2CRiPFA+PDxQBjxcTEfoKHgr6EiQOFgISIBIEGgwOFgISIBIEGomzFiAgFrMWIAEfKCgfHgFSFvoPFAEWDvosEfoKCvoRAAAAAAEAAP/RA6EDRwAfAB1AGhIPCgQDBQACAUcAAgACbwEBAABmHRQXAwUXKwEUDwETFRQOAS8BBwYiJjU0NxMnJjU0NyU3NjIfAQUWA6EPyjAMFQz7+gwWDAEwyw4fARh+CyAMfQEYIAHwDA/F/ukMCxABB4SEBxIKBAgBF8UPDBUFKP4XF/4oBQACAAD/0QOhA0cACQApACdAJBwZFA4NCQgHBgUDAQwAAgFHAAIAAm8BAQAAZiUkFxYSEAMFFCsBNy8BDwEXBzcXExQPARMVFCMiLwEHBiImNTQ3EycmNTQ3JTc2Mh8BBRYCe6rramnsqynT0/4PyjAXCgz7+gwWDAEwyw4fARh+CyAMfQEYIAEppiLV1SKm629vAbIMD8X+6QwcB4SEBxIKBAgBF8UPDBUFKP4XF/4oBQAAAAACAAD//wQwAoMAIQBDAEJAPyIBBAYBRwMBAQcGBwEGbQkBBgQHBgRrCAECAAcBAgdgAAQAAARUAAQEAFgFAQAEAExCQBYhJRghFhUoEwoFHSslFAYnISImLwEuATMRIyIuAT8BNjIfARYUBgcjFSEyHwEWJRQPAQYiLwEmNDY7ATUhIi8BJjQ2NyEyFh8BHgEVETMyFgLKCgj96QUGAgMBAgFrDxQBCLMLIAyyCRYOawFBCQVZBAFlCLIMIAuzCBYOa/6+CQVZBAoIAhgEBgIDAQJrDhYSBwwBAgMEAQwBTxYbCtYMDNYKHBQB1gZsBeINCtYNDdYKGxbWB2sFDQoBAgMFAggD/rIWAAAABQAA/8oD6AK4AAkAGgA+AEQAVwBXQFQ0GwIABFMGAgIAUkMCAQJQQiknCAEGBgEERwAFBAVvAAIAAQACAW0AAQYAAQZrAAYDAAYDawADA24ABAAABFQABAQAWAAABABMTEsTLhkkFB0HBRorJTcuATc0NwYHFgE0JgciBhUUFjI2NTQ2MzI2NxQVBgIPAQYjIicmNTQ3LgEnJjQ3PgEzMhc3NjMyFh8BFgcWExQGBxMWFxQHBgcOASM3PgE3Jic3HgEXFgE2KzA4ASKAVV4BahALRmQQFhBEMAsQyjvqOxwFCgdECRlQhjILC1b8lzIyHwUKAw4LJAsBCRVYSZ0E+gsWJ1TcfCl3yEVBXSM1YiALcE8jaj1DOkGEkAFnCxABZEULEBALMEQQdQQBaf5aaTIJJwYKByokeE0RKhKDmAo2CQYGFAYBBf79ToAbARgZXhMTJC1gakoKhGlkQD8kYjYTAAACAAD/uANZAxIAIwAzAEFAPg0BAAEfAQQDAkcCAQABAwEAA20FAQMEAQMEawAHAAEABwFgAAQGBgRUAAQEBlgABgQGTDU1IzMWIyQjCAUcKwE1NCYHIzU0JicjIgYHFSMiBgcVFBY3MxUUFjsBMjY3NTMyNhMRFAYHISImNRE0NjchMhYCyhQPsxYORw8UAbIPFAEWDrIWDkcPFAGzDhaOXkP96UNeXkMCF0NeAUFIDhYBsw8UARYOsxQPSA4WAbMOFhYOsxQBP/3oQl4BYEECGEJeAWAAAAACAAD/uANaAxIACABqAEVAQmVZTEEEAAQ7CgIBADQoGxAEAwEDRwAFBAVvBgEEAARvAAABAG8AAQMBbwADAgNvAAICZlxbU1FJSCsqIiATEgcFFisBNCYiDgEWMjYlFRQGDwEGBxYXFhQHDgEnIi8BBgcGBwYrASImNScmJwcGIicmJyY0Nz4BNyYvAS4BJzU0Nj8BNjcmJyY0Nz4BMzIfATY3Njc2OwEyFh8BFhc3NjIXFhcWFAcOAQcWHwEeAQI7UnhSAlZ0VgEcCAdoCgsTKAYFD1ANBwdNGRoJBwQQfAgMEBsXTwYQBkYWBAUIKAoPCGYHCAEKBWgIDhclBgUPUA0HCE0YGgkIAxF8BwwBDxwXTwUPB0gUBAQJKAoPCGYHCgFlO1RUdlRUeHwHDAEQHhUbMgYOBhVQAQU8DQhMHBAKB2cJDDwFBkAeBQ4GDDIPHBsPAQwHfAcMARAZGiAtBwwHFFAFPA0ITBwQCgdnCQs7BQVDHAUOBgwyDxwaEAEMAAAAAgAAAAADawLKACcAQABCQD8UAQIBAUcABgIFAgYFbQAFAwIFA2sABAMAAwQAbQABAAIGAQJgAAMEAANUAAMDAFgAAAMATBYjGSUqJScHBRsrJRQWDwEOAQcjIiY1ETQ2OwEyFhUXFg8BDgEnIyIGBxEUFhczMh4CARQHAQYiJj0BIyImPQE0NjczNTQ2FhcBFgFlAgECAQgIskNeXkOyCAoBAQECAQgIsiU0ATYktAYCBgICBgv+0QscFvoOFhYO+hYcCwEvCzUCEgUOCQIDXkMBiENeCggLCQYNBwgBNCb+eCU0AQQCCAEsDgv+0AoUD6EWDtYPFAGhDhYCCf7QCgAAAAABAAD/7gO2AjAAFAAZQBYNAQABAUcCAQEAAW8AAABmFBcSAwUXKwkBBiInASY0PwE2MhcJATYyHwEWFAOr/mIKHgr+YgsLXQoeCgEoASgLHAxcCwGW/mMLCwGdCx4KXAsL/tgBKAsLXAscAAAB//7/ewO4A2cAMQAfQBwAAQAAAVQAAQEAWAIBAAEATAEAKikAMQExAwUUKxciJy4BNwE2Fx4BFxYHAQ4BJyY2NwE2FgcBBhcWNzY3ATYmJyYHAQYeAjcBNhYHAQb0ZkRIBFYB8FBeLEYMGlD+JihgIB4GLAFMGDQa/rQsGAwMGBYB2jIgPDY2/hJCBGSGSgHwGDQa/hBShUhGwF4B8FAaDEYsYFD+JigKIBhkKgFOGjQY/rQsGggCBBYB2jJ2EA4y/hJMhmIEQAHuGC4a/hBSAAAAAAT///+4BC8DEgAIAA8AHwAvAFVAUh0UAgEDDwEAAQ4NDAkEAgAcFQIEAgRHAAIABAACBG0ABgcBAwEGA2AAAQAAAgEAYAAEBQUEVAAEBAVYAAUEBUwREC4rJiMZFxAfER8TExIIBRcrARQOASY0Nh4BARUhNTcXASUhIgYHERQWNyEyNicRNCYXERQGByEiJjcRNDY3ITIWAWU+Wj4+Wj4CPPzusloBHQEe/IMHCgEMBgN9BwwBClE0JfyDJDYBNCUDfSU0AhgtPgJCVkIEOv76+muzWQEdoQoI/VoHDAEKCAKmCAoS/VolNAE2JAKmJTQBNgAL////cQQvAxIADwAfAC8APwBPAF8AbwB/AI8AnwCvAMRAGZBAAgkIiIBgIAQFBHg4AgMCUDAAAwEABEdLsCFQWEA3ABUSDAIICRUIYBMBCRABBAUJBGARDQIFDgYCAgMFAmAPAQMKAQABAwBgCwcCAQEUWAAUFA0USRtAPgAVEgwCCAkVCGATAQkQAQQFCQRgEQ0CBQ4GAgIDBQJgDwEDCgEAAQMAYAsHAgEUFAFUCwcCAQEUWAAUARRMWUAmrqumo56blpSOjIaEfnx2c25rZmReW1ZUTks1NTUmNSY1NTMWBR0rFzU0JgcjIgYdARQWOwEyNic1NCYrASIGHQEUFjczMjYnNTQmJyMiBh0BFBYXMzI2ARE0JiMhIgYXERQWMyEyNgE1NCYHIyIGHQEUFjsBMjYBNTQmByMiBgcVFBY7ATI2AxE0JgchIgYXERQWFyEyNhc1NCYrASIGBxUUFjczMjY3NTQmJyMiBgcVFBYXMzI2NzU0JgcjIgYHFRQWOwEyNjcRFAYjISImNxE0NjchMhbWFA9IDhYWDkgOFgEUD0gOFhYOSA4WARQPSA4WFg5IDhYCOxYO/lMOFgEUDwGtDxT9xRQPSA4WFg5IDhYDERYORw8UARYORw8U1RYO/lMOFgEUDwGtDxTXFg5HDxQBFg5HDxQBFg5HDxQBFg5HDxQBFg5HDxQBFg5HDxRINCX8gyQ2ATQlA30lNCRIDhYBFA9IDhYW5EgOFhYOSA4WARTmRw8UARYORw8UARb+YQEeDhYWDv7iDhYWApFHDxYBFBBHDhYW/YtIDhYBFA9IDhYWAbsBHQ8WARQQ/uMPFAEWyUgOFhYOSA4WARTmRw8UARYORw8UARbkRw8WARQQRw4WFmf9EiU0NCUC7iU0ATYAAQAA/8cCdANLABQAF0AUCQEAAQFHAAEAAW8AAABmHBICBRYrCQEGIi8BJjQ3CQEmND8BNjIXARYUAmr+YgscC10LCwEo/tgLC10KHgoBngoBcP5hCgpdCxwLASkBKAscC10LC/5iCxwAAAAAAQAA/8cCmANLABQAF0AUAQEAAQFHAAEAAW8AAABmFxcCBRYrCQIWFA8BBiInASY0NwE2Mh8BFhQCjv7XASkKCl0LHAv+YgsLAZ4KHgpdCgKx/tj+1woeCl0KCgGfCh4KAZ4LC10KHgABAAAAAAO2Ak0AFAAZQBYFAQACAUcAAgACbwEBAABmFxQSAwUXKyUHBiInCQEGIi8BJjQ3ATYyFwEWFAOrXAseCv7Y/tgLHAtdCwsBngscCwGeC3JcCgoBKf7XCgpcCx4KAZ4KCv5iCxwAAAADAAD/cQPEA1oADAAaAEIA6UAMAAECAAFHKBsCAwFGS7AOUFhAKwcBBQEAAQVlAAACAQBjAAMAAQUDAWAABAQIWAAICAxIAAICBlgABgYNBkkbS7AhUFhALAcBBQEAAQVlAAACAQACawADAAEFAwFgAAQECFgACAgMSAACAgZYAAYGDQZJG0uwJFBYQCkHAQUBAAEFZQAAAgEAAmsAAwABBQMBYAACAAYCBlwABAQIWAAICAwESRtALwcBBQEAAQVlAAACAQACawAIAAQDCARgAAMAAQUDAWAAAgYGAlQAAgIGWAAGAgZMWVlZQAwfIhIoFhEjExIJBR0rBTQjIiY3NCIVFBY3MiUhJhE0LgIiDgIVEAUUBisBFAYiJjUjIiY1PgQ3NDY3JjU0PgEWFRQHHgEXFB4DAf0JITABEjooCf6MAtaVGjRSbFI0GgKmKh36VHZU+h0qHC4wJBIChGkFICwgBWqCARYiMDBZCDAhCQkpOgGpqAEpHDw4IiI4PBz+16gdKjtUVDsqHRgyVF6ITVSSEAoLFx4CIhULChCSVE6GYFI0AAAAAgAAAAACgwMSAAcAHwAqQCcFAwIAAQIBAAJtAAICbgAEAQEEVAAEBAFYAAEEAUwjEyU2ExAGBRorEyE1NCYOARcFERQGByEiJicRNDYXMzU0NjIWBxUzMhazAR1UdlQBAdAgFv3pFx4BIBYRlMyWAhIXHgGsbDtUAlA9of6+Fh4BIBUBQhYgAWxmlJRmbB4AA//9/7gDWQMSAAwBvQH3AndLsAlQWEE8AL0AuwC4AJ8AlgCIAAYAAwAAAI8AAQACAAMA2gDTAG0AWQBRAEIAPgAzACAAGQAKAAcAAgGeAZgBlgGMAYsBegF1AWUBYwEDAOEA4AAMAAYABwFTAU0BKAADAAgABgH0AdsB0QHLAcABvgE4ATMACAABAAgABgBHG0uwClBYQUMAuwC4AJ8AiAAEAAUAAAC9AAEAAwAFAI8AAQACAAMA2gDTAG0AWQBRAEIAPgAzACAAGQAKAAcAAgGeAZgBlgGMAYsBegF1AWUBYwEDAOEA4AAMAAYABwFTAU0BKAADAAgABgH0AdsB0QHLAcABvgE4ATMACAABAAgABwBHAJYAAQAFAAEARhtBPAC9ALsAuACfAJYAiAAGAAMAAACPAAEAAgADANoA0wBtAFkAUQBCAD4AMwAgABkACgAHAAIBngGYAZYBjAGLAXoBdQFlAWMBAwDhAOAADAAGAAcBUwFNASgAAwAIAAYB9AHbAdEBywHAAb4BOAEzAAgAAQAIAAYAR1lZS7AJUFhANQACAwcDAgdtAAcGAwcGawAGCAMGCGsACAEDCAFrAAEBbgkBAAMDAFQJAQAAA1gFBAIDAANMG0uwClBYQDoEAQMFAgUDZQACBwUCB2sABwYFBwZrAAYIBQYIawAIAQUIAWsAAQFuCQEABQUAVAkBAAAFVgAFAAVKG0A1AAIDBwMCB20ABwYDBwZrAAYIAwYIawAIAQMIAWsAAQFuCQEAAwMAVAkBAAADWAUEAgMAA0xZWUEZAAEAAAHYAdYBuQG3AVcBVgDHAMUAtQC0ALEArgB5AHYABwAGAAAADAABAAwACgAFABQrATIeARQOASIuAj4BAQ4BBzI+ATU+ATc2FyY2PwE2PwEGJjUUBzQmBjUuBC8BJjQvAQcGFCoBFCIGIgc2JyYjNiYnMy4CJy4BBwYUHwEWBh4BBwYPAQYWFxYUBiIPAQYmJyYnJgcmJyYHMiYHPgEjNj8BNicWPwE2NzYyFjMWNCcyJyYnJgcGFyIPAQYvASYnIgc2JiM2JyYiDwEGHgEyFxYHIgYiBhYHLgEnFicjIgYiJyY3NBcnBgcyNj8BNhc3FyYHBgcWBycuASciBwYHHgIUNxYHMhcWFxYHJyYGFjMiDwEGHwEGFjcGHwMeAhcGFgciBjUeAhQWNzYnLgI1MzIfAQYeAjMeAQcyHgQfAxYyPwE2FhcWNyIfAR4BFR4BFzY1BhYzNjUGLwEmNCY2FzI2LgInBiYnFAYVIzY0PwE2LwEmByIHDgMmJy4BND8BNic2PwE2OwEyNDYmIxY2FxY3JyY3FjceAh8BFjY3FhceAT4BJjUnNS4BNjc0Nj8BNicyNycmIjc2Jz4BMxY2Jz4BNxY2Jj4BFTc2IxY3Nic2JiczMjU2JyYDNjcmIi8BNiYvASYvASYPASIPARUmJyIuAQ4BDwEmNiYGDwEGNgYVDgEVLgE3HgEXFgcGBwYXFAYWAa10xnJyxujIbgZ6vAETAggDAQIEAxEVEwoBDAIIBgMBBwYEBAoFBgQBCAECAQMDBAQEBAYBBgIICQUEBgIEAwEIDAEFHAQDAgIBCAEOAQIHCQMEBAEEAgMBBwoCBAUNAwMUDhMECAYBAgECBQkCARMJBgQCBQYKAwgEBwUCAwYJBAYBBQkEBQMDAgUEAQ4HCw8EEAMDAQgECAEIAwEIBAMCAgMEAgQSBQMMDAEDAwIMGRsDBgUFEwUDCwQNCwEEAgYECAQJBFEyBAUCBgUDARgKAQIHBQQDBAQEAQIBAQECCgcHEgQHCQQDCAQCDgEBAgIOAgQCAg8IAwQDAgMFAQQKCgEECAQFDAcCAwgDCQcWBgYFCAgQBBQKAQIEAgYDDgMEAQoFCBEKAgICAgEFAgQBCgIDDAMCCAECCAMBAwIHCwQBAgIIFAMICgECAQQCAwUCAQMCAQMBBBgDCQMBAQEDDQIOBAIDAQQDBQIGCAQCAgEIBAQHCAUHDAQEAgICBgEFBAMCAwUMBAISAQQCAgUOCQICCggFCQIGBgcFCQwKaXNQAQwBDQEEAxUBAwUCAwICAQUMCAMGBgYGAQEECAQKAQcGAgoCBAEMAQECAgQLDwECCQoBAxJ0xOrEdHTE6sR0/t0BCAIGBgEECAMFCwEMAQMCAgwBCgcCAwQCBAECBgwFBgMDAgQBAQMDBAIEAQMDAgIIBAIGBAEDBAEEBAYHAwgHCgcEBQYFDAMBAgQCAQMMCQ4DBAUHCAUDEQIDDggFDAMBAwkJBgQDBgEOBAoEAQIFAgIGCgQHBwcBCQUIBwgDAgcDAgQCBgIEBQoDAw4CBQICBQQHAgEKCA8CAwMHAwIOAwIDBAYEBgQEAQEtTwQBCAQDBAYPCgIGBAUEBQ4JFAsCAQYaAgEXBQQGAwUUAwMQBQIBBAgFCAQBCxgNBQwCAgQEDAgOBA4BCgsUBwgBBQMNAgECARIDCgQECQUGAgMKAwIDBQwCEAgSAwMEBAYCBAoHDgEFAgQBBAICEAUPBQIFAwILAggEBAICBBgOCQ4FCQEEBgECAwIBBAMGBwYFAg8KAQQBAgMBAgMIBRcEAggIAwUOAgoKBQECAwQLCQUCAgICBgIKBgoEBAQDAQQKBAYBBwIBBwYFBAIDAQUEAv4NFVUCAgUEBgIPAQECAQIBAQMCCgMGAgIFBgcDDgYCAQUEAggBAggCAgICBRwIEQkOCQwCBBAHAAIAAP+lA48DJAAMABcAIkAfFAEBAhEFAgABAkcAAgECbwABAAFvAAAAZhsWIgMFFyslFAYnIic+ASc0NjIWARYUBwEuAScBNjIB0K57UUREUgFYelgBniAh/sIUUjgBPiBe0XywASgnilI9WFgB9SBeIP7CN1QUAT4gAAAC//3/cQPrA1kAJwBQALBADiQWBgMBAkxCNAMEAwJHS7AhUFhAJgABAgMCAQNtBwEDBAIDBGsAAgIAWAYBAAAMSAAEBAVYAAUFDQVJG0uwJFBYQCMAAQIDAgEDbQcBAwQCAwRrAAQABQQFXAACAgBYBgEAAAwCSRtAKQABAgMCAQNtBwEDBAIDBGsGAQAAAgEAAmAABAUFBFQABAQFWAAFBAVMWVlAFykoAQBHRTEvKFApUBQSDAoAJwEnCAUUKwEiBwYHBgcUFh8BMzI1Njc2NzYzMhYXBwYWHwEWPgEvAS4BDwEmJyYBIhUGBwYHBiMiJyYnNzYmLwEmDgEfAR4BPwEWFxYzMjc2NzY3NCYvAQHug3FtQ0UFBQQEVBMFNTNTV2NPjjQ6CQIM9wsUCgQ6AhIJQURaXAEzEwU1M1NWY1BIRTU7CAIL+AsUCgQ6AhIKQERaXWaCcW5CRQUFBAQDWUA+a26BCAkCARJiU1EvMT44OQkTAzIDCRYQ4wgLBjxGJij+BBJiU1EvMSAeODkJEwMyAwkWEOMICwY8RiYoQD5rboIICAIBAAAAAAL///9iA+oDWQAfAEEASUAKBAECAAFHMQEBREuwJFBYQBMAAgABAAIBbQABAW4DAQAADABJG0APAwEAAgBvAAIBAm8AAQFmWUANAQAhIBQTAB8BHwQFFCsBIgcGBzE2NzYXFhcWFxYGBwYXHgE3PgE3NiYnLgEnJgEiBwYHBgcGFhcWFxYXFjc2NzEGBwYnJicmJyY2NzYmJyYB8ldRVERWbGpnak9CISEGJQ4aEDMRAwoCIwElJpBeW/4FGA8EBAYBJAIkJkhbe3d5fWFWbGpna09CISAFJQgGDhIDWR0eOUUVFB4gT0JWU7NRKRsQAREDDwZaw1ldkCYl/u4QBAYIBlrDWV1IWyQiGBlRRRUUHiBPQlZTs1EVIQ4SAAAAAAIAAAAAA+gDWQAnAD8AfUATKAEBBhEBAgE3LgIEAiEBBQQER0uwJFBYQCQABAIFAgQFbQAFAwIFA2sAAQACBAECYAADAAADAFwABgYMBkkbQCwABgEGbwAEAgUCBAVtAAUDAgUDawABAAIEAQJgAAMAAANUAAMDAFgAAAMATFlACjobJTU2JTMHBRsrARUUBiMhIiY1ETQ2NyEyFh0BFAYjISIGBxEUFhchMjY9ATQ2OwEyFhMRFA4BLwEBBiIvASY0NwEnJjQ2MyEyFgMSXkP+MENeXkMBiQcKCgf+dyU0ATYkAdAlNAoIJAgK1hYcC2L+lAUQBEAGBgFsYgsWDgEdDxQBU7JDXl5DAdBCXgEKCCQICjQl/jAlNAE2JLIICgoB2v7jDxQCDGL+lAYGQAUOBgFsYgscFhYAAAACAAD/uANZAxIAGAAoADJALxIJAgIAAUcAAgABAAIBbQAEAAACBABgAAEDAwFUAAEBA1gAAwEDTDU3FBkzBQUZKwERNCYnISIGHwEBBhQfARYyNwEXFjMyNzYTERQGByEiJjURNDY3ITIWAsoUD/70GBMSUP7WCws5CxwLASpRCg8GCBWPXkP96UNeXkMCF0NeAVMBDA8UAS0QUP7WCx4KOQoKASpQCwMKATX96EJeAWBBAhhCXgFgAAAAAAMAAAAAA1oCywAPAB8ALwA3QDQoAQQFCAACAAECRwAFAAQDBQRgAAMAAgEDAmAAAQAAAVQAAQEAWAAAAQBMJjUmNSYzBgUaKyUVFAYHISImJzU0NjchMhYDFRQGJyEiJic1NDYXITIWAxUUBiMhIiYnNTQ2FyEyFgNZFBD87w8UARYOAxEPFgEUEPzvDxQBFg4DEQ8WARQQ/O8PFAEWDgMRDxZrRw8UARYORw8UARYBEEgOFgEUD0gOFgEUAQ5HDhYWDkcPFgEUAAAAAAL///+4A+kCygAZADgALUAqCQACAgMBRwADAgNvAAIBAm8AAQAAAVQAAQEAWAAAAQBMNzQmJDozBAUWKwERFAYHISImNxEWFxYXHgI3MzI+ATc2NzY3FAYHBg8BDgInIyImLwEuAS8BJicuASc0NjMhMhYD6DQl/MokNgEZH8pMICZEGwIcQigfX7cgGDYp0jQ1DCIeDQIMHhEeDSIGk2ASIzwBLisDNiQ2Ac3+RSU0ATYkAbsbFok3GBocARocF0R8Fr8sUB2SIycJEgwBCgoSCBwDZUIOF1IkKzo0AAAAAgAA/3ED6ALKABcAPQBiQAw0CAIBACYLAgMCAkdLsCFQWEAXAAQFAQABBABgAAEAAgMBAmAAAwMNA0kbQB4AAwIDcAAEBQEAAQQAYAABAgIBVAABAQJYAAIBAkxZQBEBADs6JCIdGxIQABcBFwYFFCsBIg4BBxQWHwEHBgc2PwEXFjMyPgIuAQEUDgEjIicGBwYHIyImJzUmNiY/ATY/AT4CPwEuASc0PgEgHgEB9HLGdAFQSTAPDRpVRRggJiJyxnQCeMIBgIbmiCcqbpMbJAMIDgICBAIDDAQNFAcUEAcPWGQBhuYBEOaGAoNOhEw+cikcNTMuJDwVAwVOhJiETv7iYaRgBGEmCAQMCQECCAQDDwUOFggcHBMqMpJUYaRgYKQAAAEAAP+4A+gDNQArAClAJiYBBAMBRwADBANvAAQBBG8AAQIBbwACAAJvAAAAZiMXEz0XBQUZKyUUBw4CBwYiJjU0Njc2NTQuBSsBFRQGIicBJjQ3ATYyFgcVMyAXFgPoRwEKBAUHEQoCAQMUIjg+VlY3fRQgCf7jCwsBHQscGAJ9AY5aHuhdnwQSEAQKDAgFFAMmHzhaQDAeEgaPDhYLAR4KHgoBHgoUD4/hSwABAAAAAAKDA1oAIwBmS7AkUFhAIAAEBQAFBABtAgYCAAEFAAFrAAEBbgAFBQNYAAMDDAVJG0AlAAQFAAUEAG0CBgIAAQUAAWsAAQFuAAMFBQNUAAMDBVgABQMFTFlAEwEAIB8bGBQTEA4JBgAjASMHBRQrATIWFxEUBgchIiYnETQ2FzM1NDYeAQcUBisBIiY1NCYiBhcVAk0XHgEgFv3pFx4BIBYRlMyWAhQPJA4WVHZUAQGsHhf+vhYeASAVAUIWIAGzZ5QCkGkOFhYOO1RUO7MAAAUAAP9xA+gDWQAQABQAJQAvADkA20AXMykCBwghAQUCHRUNDAQABQNHBAEFAUZLsCFQWEAtBgwDCwQBBwIHAQJtAAIFBwIFawAFAAcFAGsJAQcHCFgKAQgIDEgEAQAADQBJG0uwJFBYQCwGDAMLBAEHAgcBAm0AAgUHAgVrAAUABwUAawQBAABuCQEHBwhYCgEICAwHSRtAMgYMAwsEAQcCBwECbQACBQcCBWsABQAHBQBrBAEAAG4KAQgHBwhUCgEICAdWCQEHCAdKWVlAIBERAAA3NTIxLSsoJyQiHx4bGREUERQTEgAQAA83DQUVKwERFAYHERQGByEiJicREzYzIREjEQERFAYHISImJxEiJicRMzIXJRUjNTQ2OwEyFgUVIzU0NjsBMhYBiRYOFBD+4w8UAYsEDQGfjgI7Fg7+4w8UAQ8UAe0NBP4+xQoIoQgKAXfFCgihCAoCpv5UDxQB/r8PFAEWDgEdAegM/ngBiP4M/uMPFAEWDgFBFg4BrAytfX0ICgoIfX0ICgoAAAADAAD/uAR4AxMACAAsAE8Ad0B0LCUCCgcgHw4DAwIyEwIECANHAAEHAW8ABwoHbw4BAAoNCgANbQALDQINCwJtDAEKAA0LCg1gBgECBQEDCAIDYAAIBAQIVAAICARYCQEECARMAQBNS0pIRURBPzYzMS8pKCQiHBsXFRIQCgkFBAAIAQgPBRQrASImPgEeAgYFMzIWBxUUBisBFRQGByMiJj0BIyImJzU0NjczNTQ2FzMyFhcBFBY3MxUGIyEiJjU0PgUXMhceATI2NzYzMhcjIgYVAYlZfgJ6tngGhAHDxAcMAQoIxAwGawgKxQcKAQwGxQoIawcKAf5lKh2PJjn+GENSBAwSHiY6IQsLLFRkVCwLC0kwfR0qAWV+sIACfLR6SQwGawgKxQcKAQwGxQoIawcKAcQHDAEKCP6/HSwBhRxOQx44QjY4IhoCCiIiIiIKNiodAAAAAAEAAAABAADpIaNfXw889QALA+gAAAAA15m5twAAAADXmbm3//3/YgR4A2cAAAAIAAIAAAAAAAAAAQAAA1n/cQAABHb//f/zBHgAAQAAAAAAAAAAAAAAAAAAACAD6AAAAxEAAAOgAAADoAAAA6AAAAQvAAAD6AAAA1kAAANZAAADoAAAA+gAAAOr//4EL///BC///wLKAAACygAAA+gAAAPoAAACggAAA1n//QOgAAAD6P/9A+n//wPoAAADWQAAA1kAAAPo//8D6AAAA+gAAAKCAAAD6AAABHYAAAAAAAAASgDOARIBbAHyAqQDEAPSBFQEigT0BW4GwAb2ByoHYAg0CHwMgAy+DZQOJA7CDyAPhg/2EIgQ3hFIEhASuwAAAAEAAAAgAfgACwAAAAAAAgAsADwAcwAAAKoLcAAAAAAAAAASAN4AAQAAAAAAAAA1AAAAAQAAAAAAAQAIADUAAQAAAAAAAgAHAD0AAQAAAAAAAwAIAEQAAQAAAAAABAAIAEwAAQAAAAAABQALAFQAAQAAAAAABgAIAF8AAQAAAAAACgArAGcAAQAAAAAACwATAJIAAwABBAkAAABqAKUAAwABBAkAAQAQAQ8AAwABBAkAAgAOAR8AAwABBAkAAwAQAS0AAwABBAkABAAQAT0AAwABBAkABQAWAU0AAwABBAkABgAQAWMAAwABBAkACgBWAXMAAwABBAkACwAmAclDb3B5cmlnaHQgKEMpIDIwMTggYnkgb3JpZ2luYWwgYXV0aG9ycyBAIGZvbnRlbGxvLmNvbWZvbnRlbGxvUmVndWxhcmZvbnRlbGxvZm9udGVsbG9WZXJzaW9uIDEuMGZvbnRlbGxvR2VuZXJhdGVkIGJ5IHN2ZzJ0dGYgZnJvbSBGb250ZWxsbyBwcm9qZWN0Lmh0dHA6Ly9mb250ZWxsby5jb20AQwBvAHAAeQByAGkAZwBoAHQAIAAoAEMAKQAgADIAMAAxADgAIABiAHkAIABvAHIAaQBnAGkAbgBhAGwAIABhAHUAdABoAG8AcgBzACAAQAAgAGYAbwBuAHQAZQBsAGwAbwAuAGMAbwBtAGYAbwBuAHQAZQBsAGwAbwBSAGUAZwB1AGwAYQByAGYAbwBuAHQAZQBsAGwAbwBmAG8AbgB0AGUAbABsAG8AVgBlAHIAcwBpAG8AbgAgADEALgAwAGYAbwBuAHQAZQBsAGwAbwBHAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAHMAdgBnADIAdAB0AGYAIABmAHIAbwBtACAARgBvAG4AdABlAGwAbABvACAAcAByAG8AagBlAGMAdAAuAGgAdAB0AHAAOgAvAC8AZgBvAG4AdABlAGwAbABvAC4AYwBvAG0AAAAAAgAAAAAAAAAKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAQIBAwEEAQUBBgEHAQgBCQEKAQsBDAENAQ4BDwEQAREBEgETARQBFQEWARcBGAEZARoBGwEcAR0BHgEfASABIQAGY2FuY2VsBnVwbG9hZARzdGFyCnN0YXItZW1wdHkHcmV0d2VldAdleWUtb2ZmDHBsdXMtc3F1YXJlZANjb2cGbG9nb3V0CWRvd24tb3BlbgZhdHRhY2gHcGljdHVyZQV2aWRlbwpyaWdodC1vcGVuCWxlZnQtb3Blbgd1cC1vcGVuBGJlbGwEbG9jawVnbG9iZQVicnVzaAVzcGluMwVzcGluNAhsaW5rLWV4dAxsaW5rLWV4dC1hbHQEbWVudQhtYWlsLWFsdA1jb21tZW50LWVtcHR5BXJlcGx5DWxvY2stb3Blbi1hbHQKYmlub2N1bGFycwl1c2VyLXBsdXMAAAABAAH//wAPAAAAAAAAAAAAAAAAAAAAAAAYABgAGAAYA2f/YgNn/2KwACwgsABVWEVZICBLuAAOUUuwBlNaWLA0G7AoWWBmIIpVWLACJWG5CAAIAGNjI2IbISGwAFmwAEMjRLIAAQBDYEItsAEssCBgZi2wAiwgZCCwwFCwBCZasigBCkNFY0VSW1ghIyEbilggsFBQWCGwQFkbILA4UFghsDhZWSCxAQpDRWNFYWSwKFBYIbEBCkNFY0UgsDBQWCGwMFkbILDAUFggZiCKimEgsApQWGAbILAgUFghsApgGyCwNlBYIbA2YBtgWVlZG7ABK1lZI7AAUFhlWVktsAMsIEUgsAQlYWQgsAVDUFiwBSNCsAYjQhshIVmwAWAtsAQsIyEjISBksQViQiCwBiNCsQEKQ0VjsQEKQ7ABYEVjsAMqISCwBkMgiiCKsAErsTAFJbAEJlFYYFAbYVJZWCNZISCwQFNYsAErGyGwQFkjsABQWGVZLbAFLLAHQyuyAAIAQ2BCLbAGLLAHI0IjILAAI0JhsAJiZrABY7ABYLAFKi2wBywgIEUgsAtDY7gEAGIgsABQWLBAYFlmsAFjYESwAWAtsAgssgcLAENFQiohsgABAENgQi2wCSywAEMjRLIAAQBDYEItsAosICBFILABKyOwAEOwBCVgIEWKI2EgZCCwIFBYIbAAG7AwUFiwIBuwQFlZI7AAUFhlWbADJSNhRESwAWAtsAssICBFILABKyOwAEOwBCVgIEWKI2EgZLAkUFiwABuwQFkjsABQWGVZsAMlI2FERLABYC2wDCwgsAAjQrILCgNFWCEbIyFZKiEtsA0ssQICRbBkYUQtsA4ssAFgICCwDENKsABQWCCwDCNCWbANQ0qwAFJYILANI0JZLbAPLCCwEGJmsAFjILgEAGOKI2GwDkNgIIpgILAOI0IjLbAQLEtUWLEEZERZJLANZSN4LbARLEtRWEtTWLEEZERZGyFZJLATZSN4LbASLLEAD0NVWLEPD0OwAWFCsA8rWbAAQ7ACJUKxDAIlQrENAiVCsAEWIyCwAyVQWLEBAENgsAQlQoqKIIojYbAOKiEjsAFhIIojYbAOKiEbsQEAQ2CwAiVCsAIlYbAOKiFZsAxDR7ANQ0dgsAJiILAAUFiwQGBZZrABYyCwC0NjuAQAYiCwAFBYsEBgWWawAWNgsQAAEyNEsAFDsAA+sgEBAUNgQi2wEywAsQACRVRYsA8jQiBFsAsjQrAKI7ABYEIgYLABYbUQEAEADgBCQopgsRIGK7ByKxsiWS2wFCyxABMrLbAVLLEBEystsBYssQITKy2wFyyxAxMrLbAYLLEEEystsBkssQUTKy2wGiyxBhMrLbAbLLEHEystsBwssQgTKy2wHSyxCRMrLbAeLACwDSuxAAJFVFiwDyNCIEWwCyNCsAojsAFgQiBgsAFhtRAQAQAOAEJCimCxEgYrsHIrGyJZLbAfLLEAHistsCAssQEeKy2wISyxAh4rLbAiLLEDHistsCMssQQeKy2wJCyxBR4rLbAlLLEGHistsCYssQceKy2wJyyxCB4rLbAoLLEJHistsCksIDywAWAtsCosIGCwEGAgQyOwAWBDsAIlYbABYLApKiEtsCsssCorsCoqLbAsLCAgRyAgsAtDY7gEAGIgsABQWLBAYFlmsAFjYCNhOCMgilVYIEcgILALQ2O4BABiILAAUFiwQGBZZrABY2AjYTgbIVktsC0sALEAAkVUWLABFrAsKrABFTAbIlktsC4sALANK7EAAkVUWLABFrAsKrABFTAbIlktsC8sIDWwAWAtsDAsALABRWO4BABiILAAUFiwQGBZZrABY7ABK7ALQ2O4BABiILAAUFiwQGBZZrABY7ABK7AAFrQAAAAAAEQ+IzixLwEVKi2wMSwgPCBHILALQ2O4BABiILAAUFiwQGBZZrABY2CwAENhOC2wMiwuFzwtsDMsIDwgRyCwC0NjuAQAYiCwAFBYsEBgWWawAWNgsABDYbABQ2M4LbA0LLECABYlIC4gR7AAI0KwAiVJiopHI0cjYSBYYhshWbABI0KyMwEBFRQqLbA1LLAAFrAEJbAEJUcjRyNhsAlDK2WKLiMgIDyKOC2wNiywABawBCWwBCUgLkcjRyNhILAEI0KwCUMrILBgUFggsEBRWLMCIAMgG7MCJgMaWUJCIyCwCEMgiiNHI0cjYSNGYLAEQ7ACYiCwAFBYsEBgWWawAWNgILABKyCKimEgsAJDYGQjsANDYWRQWLACQ2EbsANDYFmwAyWwAmIgsABQWLBAYFlmsAFjYSMgILAEJiNGYTgbI7AIQ0awAiWwCENHI0cjYWAgsARDsAJiILAAUFiwQGBZZrABY2AjILABKyOwBENgsAErsAUlYbAFJbACYiCwAFBYsEBgWWawAWOwBCZhILAEJWBkI7ADJWBkUFghGyMhWSMgILAEJiNGYThZLbA3LLAAFiAgILAFJiAuRyNHI2EjPDgtsDgssAAWILAII0IgICBGI0ewASsjYTgtsDkssAAWsAMlsAIlRyNHI2GwAFRYLiA8IyEbsAIlsAIlRyNHI2EgsAUlsAQlRyNHI2GwBiWwBSVJsAIlYbkIAAgAY2MjIFhiGyFZY7gEAGIgsABQWLBAYFlmsAFjYCMuIyAgPIo4IyFZLbA6LLAAFiCwCEMgLkcjRyNhIGCwIGBmsAJiILAAUFiwQGBZZrABYyMgIDyKOC2wOywjIC5GsAIlRlJYIDxZLrErARQrLbA8LCMgLkawAiVGUFggPFkusSsBFCstsD0sIyAuRrACJUZSWCA8WSMgLkawAiVGUFggPFkusSsBFCstsD4ssDUrIyAuRrACJUZSWCA8WS6xKwEUKy2wPyywNiuKICA8sAQjQoo4IyAuRrACJUZSWCA8WS6xKwEUK7AEQy6wKystsEAssAAWsAQlsAQmIC5HI0cjYbAJQysjIDwgLiM4sSsBFCstsEEssQgEJUKwABawBCWwBCUgLkcjRyNhILAEI0KwCUMrILBgUFggsEBRWLMCIAMgG7MCJgMaWUJCIyBHsARDsAJiILAAUFiwQGBZZrABY2AgsAErIIqKYSCwAkNgZCOwA0NhZFBYsAJDYRuwA0NgWbADJbACYiCwAFBYsEBgWWawAWNhsAIlRmE4IyA8IzgbISAgRiNHsAErI2E4IVmxKwEUKy2wQiywNSsusSsBFCstsEMssDYrISMgIDywBCNCIzixKwEUK7AEQy6wKystsEQssAAVIEewACNCsgABARUUEy6wMSotsEUssAAVIEewACNCsgABARUUEy6wMSotsEYssQABFBOwMiotsEcssDQqLbBILLAAFkUjIC4gRoojYTixKwEUKy2wSSywCCNCsEgrLbBKLLIAAEErLbBLLLIAAUErLbBMLLIBAEErLbBNLLIBAUErLbBOLLIAAEIrLbBPLLIAAUIrLbBQLLIBAEIrLbBRLLIBAUIrLbBSLLIAAD4rLbBTLLIAAT4rLbBULLIBAD4rLbBVLLIBAT4rLbBWLLIAAEArLbBXLLIAAUArLbBYLLIBAEArLbBZLLIBAUArLbBaLLIAAEMrLbBbLLIAAUMrLbBcLLIBAEMrLbBdLLIBAUMrLbBeLLIAAD8rLbBfLLIAAT8rLbBgLLIBAD8rLbBhLLIBAT8rLbBiLLA3Ky6xKwEUKy2wYyywNyuwOystsGQssDcrsDwrLbBlLLAAFrA3K7A9Ky2wZiywOCsusSsBFCstsGcssDgrsDsrLbBoLLA4K7A8Ky2waSywOCuwPSstsGossDkrLrErARQrLbBrLLA5K7A7Ky2wbCywOSuwPCstsG0ssDkrsD0rLbBuLLA6Ky6xKwEUKy2wbyywOiuwOystsHAssDorsDwrLbBxLLA6K7A9Ky2wciyzCQQCA0VYIRsjIVlCK7AIZbADJFB4sAEVMC0AS7gAyFJYsQEBjlmwAbkIAAgAY3CxAAVCsgABACqxAAVCswoCAQgqsQAFQrMOAAEIKrEABkK6AsAAAQAJKrEAB0K6AEAAAQAJKrEDAESxJAGIUViwQIhYsQNkRLEmAYhRWLoIgAABBECIY1RYsQMARFlZWVmzDAIBDCq4Af+FsASNsQIARAAA') format('truetype');
}
/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
@@ -17,7 +17,7 @@
@media screen and (-webkit-min-device-pixel-ratio:0) {
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.svg?88606112#fontello') format('svg');
+ src: url('../font/fontello.svg?99189355#fontello') format('svg');
}
}
*/
@@ -71,9 +71,11 @@
.icon-bell:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
+.icon-brush:before { content: '\e813'; } /* '' */
.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-comment-empty:before { content: '\f0e5'; } /* '' */
diff --git a/static/font/css/fontello-ie7-codes.css b/static/font/css/fontello-ie7-codes.css
index ad83b705..5ba45f75 100644
--- a/static/font/css/fontello-ie7-codes.css
+++ b/static/font/css/fontello-ie7-codes.css
@@ -18,9 +18,11 @@
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
+.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&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-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
diff --git a/static/font/css/fontello-ie7.css b/static/font/css/fontello-ie7.css
index 1aa9f5c2..6ab5e0ca 100644
--- a/static/font/css/fontello-ie7.css
+++ b/static/font/css/fontello-ie7.css
@@ -29,9 +29,11 @@
.icon-bell { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe810;&nbsp;'); }
.icon-lock { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe811;&nbsp;'); }
.icon-globe { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe812;&nbsp;'); }
+.icon-brush { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe813;&nbsp;'); }
.icon-spin3 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe832;&nbsp;'); }
.icon-spin4 { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xe834;&nbsp;'); }
.icon-link-ext { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf08e;&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-mail-alt { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e0;&nbsp;'); }
.icon-comment-empty { *zoom: expression( this.runtimeStyle['zoom'] = '1', this.innerHTML = '&#xf0e5;&nbsp;'); }
diff --git a/static/font/css/fontello.css b/static/font/css/fontello.css
index 0bb5eb92..2a3d708d 100644
--- a/static/font/css/fontello.css
+++ b/static/font/css/fontello.css
@@ -1,11 +1,11 @@
@font-face {
font-family: 'fontello';
- src: url('../font/fontello.eot?92801760');
- src: url('../font/fontello.eot?92801760#iefix') format('embedded-opentype'),
- url('../font/fontello.woff2?92801760') format('woff2'),
- url('../font/fontello.woff?92801760') format('woff'),
- url('../font/fontello.ttf?92801760') format('truetype'),
- url('../font/fontello.svg?92801760#fontello') format('svg');
+ src: url('../font/fontello.eot?60305294');
+ src: url('../font/fontello.eot?60305294#iefix') format('embedded-opentype'),
+ url('../font/fontello.woff2?60305294') format('woff2'),
+ url('../font/fontello.woff?60305294') format('woff'),
+ url('../font/fontello.ttf?60305294') format('truetype'),
+ url('../font/fontello.svg?60305294#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?92801760#fontello') format('svg');
+ src: url('../font/fontello.svg?60305294#fontello') format('svg');
}
}
*/
@@ -74,9 +74,11 @@
.icon-bell:before { content: '\e810'; } /* '' */
.icon-lock:before { content: '\e811'; } /* '' */
.icon-globe:before { content: '\e812'; } /* '' */
+.icon-brush:before { content: '\e813'; } /* '' */
.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-comment-empty:before { content: '\f0e5'; } /* '' */
diff --git a/static/font/demo.html b/static/font/demo.html
index 80148022..26a1875e 100644
--- a/static/font/demo.html
+++ b/static/font/demo.html
@@ -229,11 +229,11 @@ body {
}
@font-face {
font-family: 'fontello';
- src: url('./font/fontello.eot?48963108');
- src: url('./font/fontello.eot?48963108#iefix') format('embedded-opentype'),
- url('./font/fontello.woff?48963108') format('woff'),
- url('./font/fontello.ttf?48963108') format('truetype'),
- url('./font/fontello.svg?48963108#fontello') format('svg');
+ src: url('./font/fontello.eot?32487936');
+ src: url('./font/fontello.eot?32487936#iefix') format('embedded-opentype'),
+ url('./font/fontello.woff?32487936') format('woff'),
+ url('./font/fontello.ttf?32487936') format('truetype'),
+ url('./font/fontello.svg?32487936#fontello') format('svg');
font-weight: normal;
font-style: normal;
}
@@ -325,21 +325,23 @@ body {
<div class="the-icons span3" title="Code: 0xe810"><i class="demo-icon icon-bell">&#xe810;</i> <span class="i-name">icon-bell</span><span class="i-code">0xe810</span></div>
<div class="the-icons span3" title="Code: 0xe811"><i class="demo-icon icon-lock">&#xe811;</i> <span class="i-name">icon-lock</span><span class="i-code">0xe811</span></div>
<div class="the-icons span3" title="Code: 0xe812"><i class="demo-icon icon-globe">&#xe812;</i> <span class="i-name">icon-globe</span><span class="i-code">0xe812</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: 0xe813"><i class="demo-icon icon-brush">&#xe813;</i> <span class="i-name">icon-brush</span><span class="i-code">0xe813</span></div>
</div>
<div class="row">
+ <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: 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: 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: 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: 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: 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: 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: 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: 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: 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>
diff --git a/static/font/font/fontello.eot b/static/font/font/fontello.eot
index aa6bb542..0533282b 100644
--- a/static/font/font/fontello.eot
+++ b/static/font/font/fontello.eot
Binary files differ
diff --git a/static/font/font/fontello.svg b/static/font/font/fontello.svg
index 0862d277..ec7c464d 100644
--- a/static/font/font/fontello.svg
+++ b/static/font/font/fontello.svg
@@ -4,65 +4,69 @@
<metadata>Copyright (C) 2018 by original authors @ fontello.com</metadata>
<defs>
<font id="fontello" horiz-adv-x="1000" >
-<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
+<font-face font-family="fontello" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="857" descent="-143" />
<missing-glyph horiz-adv-x="1000" />
-<glyph glyph-name="cancel" unicode="&#xe800;" d="M724 112q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
+<glyph glyph-name="cancel" unicode="&#xe800;" d="M724 119q0-22-15-38l-76-76q-16-15-38-15t-38 15l-164 165-164-165q-16-15-38-15t-38 15l-76 76q-16 16-16 38t16 38l164 164-164 164q-16 16-16 38t16 38l76 76q16 16 38 16t38-16l164-164 164 164q16 16 38 16t38-16l76-76q15-15 15-38t-15-38l-164-164 164-164q15-15 15-38z" horiz-adv-x="785.7" />
-<glyph glyph-name="upload" unicode="&#xe801;" d="M714 29q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
+<glyph glyph-name="upload" unicode="&#xe801;" d="M714 36q0 14-10 25t-25 10-25-10-11-25 11-25 25-11 25 11 10 25z m143 0q0 14-10 25t-26 10-25-10-10-25 10-25 25-11 26 11 10 25z m72 125v-179q0-22-16-38t-38-16h-821q-23 0-38 16t-16 38v179q0 22 16 38t38 15h238q12-31 39-51t62-20h143q34 0 61 20t40 51h238q22 0 38-15t16-38z m-182 361q-9-22-33-22h-143v-250q0-15-10-25t-25-11h-143q-15 0-25 11t-11 25v250h-143q-23 0-33 22-9 22 8 39l250 250q10 10 25 10t25-10l250-250q18-17 8-39z" horiz-adv-x="928.6" />
-<glyph glyph-name="star" unicode="&#xe802;" d="M929 489q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
+<glyph glyph-name="star" unicode="&#xe802;" d="M929 496q0-12-15-27l-202-197 48-279q0-4 0-12 0-11-6-19t-17-9q-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
-<glyph glyph-name="star-empty" unicode="&#xe803;" d="M635 290l170 166-235 34-106 213-105-213-236-34 171-166-41-235 211 111 211-111z m294 199q0-12-15-27l-202-197 48-279q0-4 0-12 0-28-23-28-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
+<glyph glyph-name="star-empty" unicode="&#xe803;" d="M635 297l170 166-235 34-106 213-105-213-236-34 171-166-41-235 211 111 211-111z m294 199q0-12-15-27l-202-197 48-279q0-4 0-12 0-28-23-28-10 0-22 7l-251 132-250-132q-12-7-23-7-11 0-17 9t-6 19q0 4 1 12l48 279-203 197q-14 15-14 27 0 21 31 26l280 40 126 254q11 23 27 23t28-23l125-254 280-40q32-5 32-26z" horiz-adv-x="928.6" />
-<glyph glyph-name="retweet" unicode="&#xe804;" d="M714 11q0-7-5-13t-13-5h-535q-5 0-8 1t-5 4-3 4-2 7 0 6v335h-107q-15 0-25 11t-11 25q0 13 8 23l179 214q11 12 27 12t28-12l178-214q9-10 9-23 0-15-11-25t-25-11h-107v-214h321q9 0 14-6l89-108q4-5 4-11z m357 232q0-13-8-23l-178-214q-12-13-28-13t-27 13l-179 214q-8 10-8 23 0 14 11 25t25 11h107v214h-322q-9 0-14 7l-89 107q-4 5-4 11 0 7 5 12t13 6h536q4 0 7-1t5-4 3-5 2-6 1-7v-334h107q14 0 25-11t10-25z" horiz-adv-x="1071.4" />
+<glyph glyph-name="retweet" unicode="&#xe804;" d="M714 18q0-7-5-13t-13-5h-535q-5 0-8 1t-5 4-3 4-2 7 0 6v335h-107q-15 0-25 11t-11 25q0 13 8 23l179 214q11 12 27 12t28-12l178-214q9-10 9-23 0-15-11-25t-25-11h-107v-214h321q9 0 14-6l89-108q4-5 4-11z m357 232q0-13-8-23l-178-214q-12-13-28-13t-27 13l-179 214q-8 10-8 23 0 14 11 25t25 11h107v214h-322q-9 0-14 7l-89 107q-4 5-4 11 0 7 5 12t13 6h536q4 0 7-1t5-4 3-5 2-6 1-7v-334h107q14 0 25-11t10-25z" horiz-adv-x="1071.4" />
-<glyph glyph-name="eye-off" unicode="&#xe805;" d="M310 105l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
+<glyph glyph-name="eye-off" unicode="&#xe805;" d="M310 112l43 79q-48 35-76 88t-27 114q0 67 34 125-128-65-213-197 94-144 239-209z m217 424q0 11-8 19t-19 7q-70 0-120-50t-50-119q0-11 8-19t19-8 19 8 8 19q0 48 34 82t82 34q11 0 19 8t8 19z m202 106q0-4 0-5-59-105-176-316t-176-316l-28-50q-5-9-15-9-7 0-75 39-9 6-9 16 0 7 25 49-80 36-147 96t-117 137q-11 17-11 38t11 39q86 131 212 207t277 76q50 0 100-10l31 54q5 9 15 9 3 0 10-3t18-9 18-10 18-10 10-7q9-5 9-15z m21-249q0-78-44-142t-117-91l157 280q4-25 4-47z m250-72q0-19-11-38-22-36-61-81-84-96-194-149t-234-53l41 74q119 10 219 76t169 171q-65 100-158 164l35 63q53-36 102-85t81-103q11-19 11-39z" horiz-adv-x="1000" />
-<glyph glyph-name="plus-squared" unicode="&#xe806;" d="M700 750q42 0 71-29t29-71l0-600q0-40-29-70t-71-30l-600 0q-40 0-70 30t-30 70l0 600q0 42 30 71t70 29l600 0z m-50-450l0 100-200 0 0 200-100 0 0-200-200 0 0-100 200 0 0-200 100 0 0 200 200 0z" horiz-adv-x="800" />
+<glyph glyph-name="plus-squared" unicode="&#xe806;" d="M714 321v72q0 14-10 25t-25 10h-179v179q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-179h-178q-15 0-25-10t-11-25v-72q0-14 11-25t25-10h178v-179q0-14 11-25t25-11h71q15 0 25 11t11 25v179h179q14 0 25 10t10 25z m143 304v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
-<glyph glyph-name="cog" unicode="&#xe807;" d="M571 350q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
+<glyph glyph-name="cog" unicode="&#xe807;" d="M571 357q0 59-41 101t-101 42-101-42-42-101 42-101 101-42 101 42 41 101z m286 61v-124q0-7-4-13t-11-7l-104-16q-10-30-21-51 19-27 59-77 6-6 6-13t-5-13q-15-21-55-61t-53-39q-7 0-14 5l-77 60q-25-13-51-21-9-76-16-104-4-16-20-16h-124q-8 0-14 5t-6 12l-16 103q-27 9-50 21l-79-60q-6-5-14-5-8 0-14 6-70 64-92 94-4 5-4 13 0 6 5 12 8 12 28 37t30 40q-15 28-23 55l-102 15q-7 1-11 7t-5 13v124q0 7 5 13t10 7l104 16q8 25 22 51-23 32-60 77-6 7-6 14 0 5 5 12 15 20 55 60t53 40q7 0 15-5l77-60q24 13 50 21 9 76 17 104 3 16 20 16h124q7 0 13-5t7-12l15-103q28-9 51-20l79 59q5 5 13 5 7 0 14-5 72-67 92-95 4-5 4-12 0-7-4-13-9-12-29-37t-30-40q15-28 23-54l102-16q7-1 12-7t4-13z" horiz-adv-x="857.1" />
-<glyph glyph-name="logout" unicode="&#xe808;" d="M357 46q0-2 1-11t0-14-2-14-5-11-12-3h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
+<glyph glyph-name="logout" unicode="&#xe808;" d="M357 53q0-2 1-11t0-14-2-14-5-10-12-4h-178q-67 0-114 47t-47 114v392q0 67 47 114t114 47h178q8 0 13-5t5-13q0-2 1-11t0-15-2-13-5-11-12-3h-178q-37 0-63-26t-27-64v-392q0-37 27-63t63-27h174t6 0 7-2 4-3 4-5 1-8z m518 304q0-14-11-25l-303-304q-11-10-25-10t-25 10-11 25v161h-250q-14 0-25 11t-11 25v214q0 15 11 25t25 11h250v161q0 14 11 25t25 10 25-10l303-304q11-10 11-25z" horiz-adv-x="928.6" />
-<glyph glyph-name="down-open" unicode="&#xe809;" d="M939 399l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+<glyph glyph-name="down-open" unicode="&#xe809;" d="M939 406l-414-413q-10-11-25-11t-25 11l-414 413q-11 11-11 26t11 25l93 92q10 11 25 11t25-11l296-296 296 296q11 11 25 11t26-11l92-92q11-11 11-25t-11-26z" horiz-adv-x="1000" />
-<glyph glyph-name="attach" unicode="&#xe80a;" d="M244-140q-102 0-170 72-72 70-74 166t84 190l496 496q80 80 174 54 44-12 79-47t47-79q26-96-54-176l-474-474q-40-40-88-46-48-4-80 28-30 24-27 74t47 92l332 334q24 26 50 0t0-50l-332-332q-44-44-20-70 12-8 24-6 24 4 46 26l474 474q50 50 34 108-16 60-76 76-54 14-108-36l-494-494q-66-76-64-143t52-117q50-48 117-50t141 62l496 494q24 24 50 0 26-22 0-48l-496-496q-82-82-186-82z" horiz-adv-x="939" />
+<glyph glyph-name="attach" unicode="&#xe80a;" d="M244-133q-102 0-170 72-72 70-74 166t84 190l496 496q80 80 174 54 44-12 79-47t47-79q26-96-54-176l-474-474q-40-40-88-46-48-4-80 28-30 24-27 74t47 92l332 334q24 26 50 0t0-50l-332-332q-44-44-20-70 12-8 24-6 24 4 46 26l474 474q50 50 34 108-16 60-76 76-54 14-108-36l-494-494q-66-76-64-143t52-117q50-48 117-50t141 62l496 494q24 24 50 0 26-22 0-48l-496-496q-82-82-186-82z" horiz-adv-x="939" />
-<glyph glyph-name="picture" unicode="&#xe80b;" d="M357 529q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
+<glyph glyph-name="picture" unicode="&#xe80b;" d="M357 536q0-45-31-76t-76-32-76 32-31 76 31 76 76 31 76-31 31-76z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
-<glyph glyph-name="video" unicode="&#xe80c;" d="M214-43v72q0 14-10 25t-25 10h-72q-14 0-25-10t-11-25v-72q0-14 11-25t25-11h72q14 0 25 11t10 25z m0 214v72q0 14-10 25t-25 11h-72q-14 0-25-11t-11-25v-72q0-14 11-25t25-10h72q14 0 25 10t10 25z m0 215v71q0 15-10 25t-25 11h-72q-14 0-25-11t-11-25v-71q0-15 11-25t25-11h72q14 0 25 11t10 25z m572-429v286q0 14-11 25t-25 11h-429q-14 0-25-11t-10-25v-286q0-14 10-25t25-11h429q15 0 25 11t11 25z m-572 643v71q0 15-10 26t-25 10h-72q-14 0-25-10t-11-26v-71q0-14 11-25t25-11h72q14 0 25 11t10 25z m786-643v72q0 14-11 25t-25 10h-71q-15 0-25-10t-11-25v-72q0-14 11-25t25-11h71q15 0 25 11t11 25z m-214 429v285q0 15-11 26t-25 10h-429q-14 0-25-10t-10-26v-285q0-15 10-25t25-11h429q15 0 25 11t11 25z m214-215v72q0 14-11 25t-25 11h-71q-15 0-25-11t-11-25v-72q0-14 11-25t25-10h71q15 0 25 10t11 25z m0 215v71q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-71q0-15 11-25t25-11h71q15 0 25 11t11 25z m0 214v71q0 15-11 26t-25 10h-71q-15 0-25-10t-11-26v-71q0-14 11-25t25-11h71q15 0 25 11t11 25z m71 89v-750q0-37-26-63t-63-26h-893q-36 0-63 26t-26 63v750q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
+<glyph glyph-name="video" unicode="&#xe80c;" d="M214-36v72q0 14-10 25t-25 10h-72q-14 0-25-10t-11-25v-72q0-14 11-25t25-11h72q14 0 25 11t10 25z m0 214v72q0 14-10 25t-25 11h-72q-14 0-25-11t-11-25v-72q0-14 11-25t25-10h72q14 0 25 10t10 25z m0 215v71q0 15-10 25t-25 11h-72q-14 0-25-11t-11-25v-71q0-15 11-25t25-11h72q14 0 25 11t10 25z m572-429v286q0 14-11 25t-25 11h-429q-14 0-25-11t-10-25v-286q0-14 10-25t25-11h429q15 0 25 11t11 25z m-572 643v71q0 15-10 26t-25 10h-72q-14 0-25-10t-11-26v-71q0-14 11-25t25-11h72q14 0 25 11t10 25z m786-643v72q0 14-11 25t-25 10h-71q-15 0-25-10t-11-25v-72q0-14 11-25t25-11h71q15 0 25 11t11 25z m-214 429v285q0 15-11 26t-25 10h-429q-14 0-25-10t-10-26v-285q0-15 10-25t25-11h429q15 0 25 11t11 25z m214-215v72q0 14-11 25t-25 11h-71q-15 0-25-11t-11-25v-72q0-14 11-25t25-10h71q15 0 25 10t11 25z m0 215v71q0 15-11 25t-25 11h-71q-15 0-25-11t-11-25v-71q0-15 11-25t25-11h71q15 0 25 11t11 25z m0 214v71q0 15-11 26t-25 10h-71q-15 0-25-10t-11-26v-71q0-14 11-25t25-11h71q15 0 25 11t11 25z m71 89v-750q0-37-26-63t-63-26h-893q-36 0-63 26t-26 63v750q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
-<glyph glyph-name="right-open" unicode="&#xe80d;" d="M618 361l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
+<glyph glyph-name="right-open" unicode="&#xe80d;" d="M618 368l-414-415q-11-10-25-10t-25 10l-93 93q-11 11-11 25t11 25l296 297-296 296q-11 11-11 25t11 25l93 93q10 11 25 11t25-11l414-414q10-11 10-25t-10-25z" horiz-adv-x="714.3" />
-<glyph glyph-name="left-open" unicode="&#xe80e;" d="M654 682l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
+<glyph glyph-name="left-open" unicode="&#xe80e;" d="M654 689l-297-296 297-297q10-10 10-25t-10-25l-93-93q-11-10-25-10t-25 10l-414 415q-11 10-11 25t11 25l414 414q10 11 25 11t25-11l93-93q10-10 10-25t-10-25z" horiz-adv-x="714.3" />
-<glyph glyph-name="up-open" unicode="&#xe80f;" d="M939 107l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
+<glyph glyph-name="up-open" unicode="&#xe80f;" d="M939 114l-92-92q-11-10-26-10t-25 10l-296 297-296-297q-11-10-25-10t-25 10l-93 92q-11 11-11 26t11 25l414 414q11 10 25 10t25-10l414-414q11-11 11-25t-11-26z" horiz-adv-x="1000" />
-<glyph glyph-name="bell" unicode="&#xe810;" d="M509-96q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-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" unicode="&#xe810;" d="M509-89q0 8-9 8-33 0-57 24t-23 57q0 9-9 9t-9-9q0-41 29-70t69-28q9 0 9 9z m-372 160h726q-149 168-149 465 0 28-13 58t-39 58-67 45-95 17-95-17-67-45-39-58-13-58q0-297-149-465z m827 0q0-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="lock" unicode="&#xe811;" d="M179 421h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
+<glyph glyph-name="lock" unicode="&#xe811;" d="M179 428h285v108q0 59-42 101t-101 41-101-41-41-101v-108z m464-53v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v108q0 102 74 176t176 74 177-74 73-176v-108h18q23 0 38-15t16-38z" horiz-adv-x="642.9" />
-<glyph glyph-name="globe" unicode="&#xe812;" d="M429 779q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
+<glyph glyph-name="globe" unicode="&#xe812;" d="M429 786q116 0 215-58t156-156 57-215-57-215-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58z m153-291q-2-1-6-5t-7-6q1 0 2 3t3 6 2 4q3 4 12 8 8 4 29 7 19 5 29-6-1 1 5 7t8 7q2 1 8 3t9 4l1 12q-7-1-10 4t-3 12q0-2-4-5 0 4-2 5t-7-1-5-1q-5 2-8 5t-5 9-2 8q-1 3-5 6t-5 6q-1 1-2 3t-1 4-3 3-3 1-4-3-4-5-2-3q-2 1-4 1t-2-1-3-1-3-2q-1-2-4-2t-5-1q8 3-1 6-5 2-9 2 6 2 5 6t-5 8h3q-1 2-5 5t-10 5-7 3q-5 3-19 5t-18 1q-3-4-3-6t2-8 2-7q1-3-3-7t-3-7q0-4 7-9t6-12q-2-4-9-9t-9-6q-3-5-1-11t6-9q1-1 1-2t-2-3-3-2-4-2l-1-1q-7-3-12 3t-7 15q-4 14-9 17-13 4-16-1-3 7-23 15-14 5-33 2 4 0 0 8-4 9-10 7 1 3 2 10t0 7q2 8 7 13 1 1 4 5t5 7 1 4q19-3 28 6 2 3 6 9t6 10q5 3 8 3t8-3 8-3q8-1 8 6t-4 11q7 0 2 10-2 4-5 5-6 2-15-3-4-2 2-4-1 0-6-6t-9-10-9 3q0 0-3 7t-5 8q-5 0-9-9 1 5-6 9t-14 4q11 7-4 15-4 3-12 3t-11-2q-2-4-3-7t3-4 6-3 6-2 5-2q8-6 5-8-1 0-5-2t-6-2-4-2q-1-3 0-8t-1-8q-3 3-5 10t-4 9q4-5-14-3l-5 0q-3 0-9-1t-12-1-7 5q-3 4 0 11 0 2 2 1-2 2-6 5t-6 5q-25-8-52-23 3 0 6 1 3 1 8 4t5 3q19 7 24 4l3 2q7-9 11-14-4 3-17 1-11-3-12-7 4-6 2-10-2 2-6 6t-8 6-8 3q-9 0-13-1-81-45-131-124 4-4 7-4 2-1 3-5t1-6 6 1q5-4 2-10 1 0 25-15 10-10 11-12 2-6-5-10-1 1-5 5t-5 2q-2-3 0-10t6-7q-4 0-5-9t-2-20 0-13l1-1q-2-6 3-19t12-11q-7-1 11-24 3-4 4-5 2-1 7-4t9-6 5-5q2-3 6-13t8-13q-2-3 5-11t6-13q-1 0-2-1t-1 0q2-4 9-8t8-7q1-2 1-6t2-6 4-1q2 11-13 35-8 13-9 16-2 2-4 8t-2 8q1 0 3 0t5-2 4-3 1-1q-1-4 1-10t7-10 10-11 6-7q4-4 8-11t0-8q5 0 11-5t10-11q3-5 4-15t3-13q1-4 5-8t7-5l9-5t7-3q3-2 10-6t12-7q6-2 9-2t8 1 8 2q8 1 16-8t12-12q20-10 30-6-1 0 1-4t4-9 5-8 3-5q3-3 10-8t10-8q4 2 4 5-1-5 4-11t10-6q8 2 8 18-17-8-27 10 0 0-2 3t-2 5-1 4 0 5 2 1q5 0 6 2t-1 7-2 8q-1 4-6 11t-7 8q-3-5-9-4t-9 5q0-1-1-3t-1-4q-7 0-8 0 1 2 1 10t2 13q1 2 3 6t5 9 2 7-3 5-9 1q-11 0-15-11-1-2-2-6t-2-6-5-4q-4-2-14-1t-13 3q-8 4-13 16t-5 20q0 6 1 15t2 14-3 14q2 1 5 5t5 6q2 1 3 1t3 0 2 1 1 3q0 1-2 2-1 1-2 1 4-1 16 1t15-1q9-6 12 1 0 1-1 6t0 7q3-15 16-5 2-1 9-3t9-2q2-1 4-3t3-3 3 0 5 4q5-8 7-13 6-23 10-25 4-2 6-1t3 5 0 8-1 7l-1 5v10l0 4q-8 2-10 7t0 10 9 10q0 1 4 2t9 4 7 4q12 11 8 20 4 0 6 5 0 0-2 2t-5 2-2 2q5 2 1 8 3 2 4 7t4 5q5-6 12-1 5 5 1 9 2 4 11 6t10 5q4-1 5 1t0 7 2 7q2 2 9 5t7 2l9 7q2 2 0 2 10-1 18 6 5 6-4 11 2 4-1 5t-9 4q2 0 7 0t5 1q9 5-3 9-10 2-24-7z m-91-490q115 21 195 106-1 2-7 2t-7 2q-10 4-13 5 1 4-1 7t-5 5-7 5-6 4q-1 1-4 3t-4 3-4 2-5 2-5-1l-2-1q-2 0-3-1t-3-2-2-1 0-2q-12 10-20 13-3 0-6 3t-6 4-6 0-6-3q-3-3-4-9t-1-7q-4 3 0 10t1 10q-1 3-6 2t-6-2-7-5-5-3-4-3-5-5q-2-2-4-6t-2-6q-1 2-7 3t-5 3q1-5 2-19t3-22q4-17-7-26-15-14-16-23-2-12 7-14 0-4-5-12t-4-12q0-3 2-9z" horiz-adv-x="857.1" />
-<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 850c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
+<glyph glyph-name="brush" unicode="&#xe813;" d="M464 209q0-124-87-212t-210-87q-81 0-149 40 68 39 109 108t40 151q0 61 44 105t105 44 105-44 43-105z m415 562q32-32 32-79t-33-79l-318-318q-20 55-61 97t-97 62l318 318q32 32 79 32t80-33z" horiz-adv-x="928" />
-<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 850c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
+<glyph glyph-name="spin3" unicode="&#xe832;" d="M494 857c-266 0-483-210-494-472-1-19 13-20 13-20l84 0c16 0 19 10 19 18 10 199 176 358 378 358 107 0 205-45 273-118l-58-57c-11-12-11-27 5-31l247-50c21-5 46 11 37 44l-58 227c-2 9-16 22-29 13l-65-60c-89 91-214 148-352 148z m409-508c-16 0-19-10-19-18-10-199-176-358-377-358-108 0-205 45-274 118l59 57c10 12 10 27-5 31l-248 50c-21 5-46-11-37-44l58-227c2-9 16-22 30-13l64 60c89-91 214-148 353-148 265 0 482 210 493 473 1 18-13 19-13 19l-84 0z" horiz-adv-x="1000" />
-<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 332v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
+<glyph glyph-name="spin4" unicode="&#xe834;" d="M498 857c-114 0-228-39-320-116l0 0c173 140 428 130 588-31 134-134 164-332 89-495-10-29-5-50 12-68 21-20 61-23 84 0 3 3 12 15 15 24 71 180 33 393-112 539-99 98-228 147-356 147z m-409-274c-14 0-29-5-39-16-3-3-13-15-15-24-71-180-34-393 112-539 185-185 479-195 676-31l0 0c-173-140-428-130-589 31-134 134-163 333-89 495 11 29 6 50-12 68-11 11-27 17-44 16z" horiz-adv-x="1001" />
-<glyph glyph-name="menu" unicode="&#xf0c9;" d="M857 100v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
+<glyph glyph-name="link-ext" unicode="&#xf08e;" d="M786 339v-178q0-67-47-114t-114-47h-464q-67 0-114 47t-47 114v464q0 66 47 113t114 48h393q7 0 12-5t5-13v-36q0-8-5-13t-12-5h-393q-37 0-63-26t-27-63v-464q0-37 27-63t63-27h464q37 0 63 27t26 63v178q0 8 5 13t13 5h36q8 0 13-5t5-13z m214 482v-285q0-15-11-25t-25-11-25 11l-98 98-364-364q-5-6-13-6t-12 6l-64 64q-6 5-6 12t6 13l364 364-98 98q-11 11-11 25t11 25 25 11h285q15 0 25-11t11-25z" horiz-adv-x="1000" />
-<glyph glyph-name="mail-alt" unicode="&#xf0e0;" d="M1000 454v-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="link-ext-alt" unicode="&#xf08f;" d="M714 339v268q0 15-10 25t-25 11h-268q-24 0-33-22-10-23 8-39l80-80-298-298q-11-11-11-26t11-25l57-57q11-10 25-10t25 10l298 298 81-80q10-11 25-11 6 0 14 3 21 10 21 33z m143 286v-536q0-66-47-113t-114-48h-535q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535q67 0 114-48t47-113z" horiz-adv-x="857.1" />
-<glyph glyph-name="comment-empty" unicode="&#xf0e5;" d="M500 636q-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="menu" unicode="&#xf0c9;" d="M857 107v-71q0-15-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 25t25 11h785q15 0 26-11t10-25z m0 286v-72q0-14-10-25t-26-10h-785q-15 0-25 10t-11 25v72q0 14 11 25t25 10h785q15 0 26-10t10-25z m0 285v-71q0-14-10-25t-26-11h-785q-15 0-25 11t-11 25v71q0 15 11 26t25 10h785q15 0 26-10t10-26z" horiz-adv-x="857.1" />
-<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 225q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" 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="lock-open-alt" unicode="&#xf13e;" d="M589 421q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
+<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="binoculars" unicode="&#xf1e5;" d="M393 671v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+<glyph glyph-name="reply" unicode="&#xf112;" d="M1000 232q0-93-71-252-1-4-6-13t-7-17-7-12q-7-10-16-10-8 0-13 6t-5 14q0 5 1 15t2 13q3 38 3 69 0 56-10 101t-27 77-45 56-59 39-74 24-86 12-98 3h-125v-143q0-14-10-25t-26-11-25 11l-285 286q-11 10-11 25t11 25l285 286q11 10 25 10t26-10 10-25v-143h125q398 0 488-225 30-75 30-186z" horiz-adv-x="1000" />
-<glyph glyph-name="user-plus" unicode="&#xf234;" d="M393 350q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
+<glyph glyph-name="lock-open-alt" unicode="&#xf13e;" d="M589 428q23 0 38-15t16-38v-322q0-22-16-37t-38-16h-535q-23 0-38 16t-16 37v322q0 22 16 38t38 15h17v179q0 103 74 177t176 73 177-73 73-177q0-14-10-25t-25-11h-36q-14 0-25 11t-11 25q0 59-42 101t-101 42-101-42-41-101v-179h410z" horiz-adv-x="642.9" />
+
+<glyph glyph-name="binoculars" unicode="&#xf1e5;" d="M393 678v-428q0-15-11-25t-25-11v-321q0-15-10-25t-26-11h-285q-15 0-25 11t-11 25v285l139 488q4 12 17 12h237z m178 0v-392h-142v392h142z m429-500v-285q0-15-11-25t-25-11h-285q-15 0-25 11t-11 25v321q-15 0-25 11t-11 25v428h237q13 0 17-12z m-589 661v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z m375 0v-125h-197v125q0 8 5 13t13 5h161q8 0 13-5t5-13z" horiz-adv-x="1000" />
+
+<glyph glyph-name="user-plus" unicode="&#xf234;" d="M393 357q-89 0-152 63t-62 151 62 152 152 63 151-63 63-152-63-151-151-63z m536-71h196q7 0 13-6t5-12v-107q0-8-5-13t-13-5h-196v-197q0-7-6-12t-12-6h-107q-8 0-13 6t-5 12v197h-197q-7 0-12 5t-6 13v107q0 7 6 12t12 6h197v196q0 7 5 13t13 5h107q7 0 12-5t6-13v-196z m-411-125q0-29 21-51t50-21h143v-133q-38-28-95-28h-488q-67 0-108 39t-41 106q0 30 2 58t8 61 15 60 24 55 34 45 48 30 62 11q11 0 22-10 44-34 86-51t92-17 92 17 86 51q11 10 22 10 73 0 121-54h-125q-29 0-50-21t-21-50v-107z" horiz-adv-x="1142.9" />
</font>
</defs>
</svg> \ No newline at end of file
diff --git a/static/font/font/fontello.ttf b/static/font/font/fontello.ttf
index c9a50e1a..3f91b643 100644
--- a/static/font/font/fontello.ttf
+++ b/static/font/font/fontello.ttf
Binary files differ
diff --git a/static/font/font/fontello.woff b/static/font/font/fontello.woff
index 5accf907..ec5cf4aa 100644
--- a/static/font/font/fontello.woff
+++ b/static/font/font/fontello.woff
Binary files differ
diff --git a/static/font/font/fontello.woff2 b/static/font/font/fontello.woff2
index 1cdb8a64..8eed845a 100644
--- a/static/font/font/fontello.woff2
+++ b/static/font/font/fontello.woff2
Binary files differ
diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js
index f929192b..7d403312 100644
--- a/test/unit/specs/modules/statuses.spec.js
+++ b/test/unit/specs/modules/statuses.spec.js
@@ -247,7 +247,7 @@ describe('The Statuses module', () => {
in_reply_to_status_id: '1', // The API uses strings here...
uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
text: 'a favorited something by b',
- user: {}
+ user: { id: 99 }
}
mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' })
@@ -264,7 +264,7 @@ describe('The Statuses module', () => {
expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.maxId).to.eq(favorite.id)
- // If something is favorited by the current user, it also sets the 'favorited' property
+ // If something is favorited by the current user, it also sets the 'favorited' property but does not increment counter to avoid over-counting. Counter is incremented (updated, really) via response to the favorite request.
const user = {
id: 1
}
@@ -281,45 +281,11 @@ describe('The Statuses module', () => {
mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user })
expect(state.timelines.public.visibleStatuses.length).to.eql(1)
- expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(2)
+ expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1)
expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true)
})
describe('notifications', () => {
- it('adds a notfications for retweets if you are the retweetet', () => {
- const user = { id: 1 }
- const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- status.user = user
- const retweet = makeMockStatus({id: 2, is_post_verb: false})
- retweet.retweeted_status = status
-
- mutations.addNewStatuses(state, { statuses: [retweet], user })
-
- expect(state.notifications.length).to.eql(1)
- expect(state.notifications[0].status).to.eql(retweet)
- expect(state.notifications[0].action).to.eql(retweet)
- expect(state.notifications[0].type).to.eql('repeat')
- })
-
- it('adds a notification when you are mentioned', () => {
- const user = { id: 1 }
- const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const mentionedStatus = makeMockStatus({id: 2})
- mentionedStatus.attentions = [user]
-
- mutations.addNewStatuses(state, { statuses: [status], user })
-
- expect(state.notifications.length).to.eql(0)
-
- mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
- expect(state.notifications.length).to.eql(1)
- expect(state.notifications[0].status).to.eql(mentionedStatus)
- expect(state.notifications[0].action).to.eql(mentionedStatus)
- expect(state.notifications[0].type).to.eql('mention')
- })
-
it('removes a notification when the notice gets removed', () => {
const user = { id: 1 }
const state = cloneDeep(defaultState)
@@ -335,92 +301,39 @@ describe('The Statuses module', () => {
deletion.uri = 'xxx'
mutations.addNewStatuses(state, { statuses: [status, otherStatus], user })
-
- expect(state.notifications.length).to.eql(1)
+ mutations.addNewNotifications(
+ state,
+ {
+ notifications: [{
+ ntype: 'mention',
+ status: otherStatus,
+ notice: otherStatus,
+ is_seen: false
+ }]
+ })
+
+ expect(state.notifications.data.length).to.eql(1)
+ mutations.addNewNotifications(
+ state,
+ {
+ notifications: [{
+ ntype: 'mention',
+ status: mentionedStatus,
+ notice: mentionedStatus,
+ is_seen: false
+ }]
+ })
mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
expect(state.allStatuses.length).to.eql(3)
- expect(state.notifications.length).to.eql(2)
- expect(state.notifications[1].status).to.eql(mentionedStatus)
- expect(state.notifications[1].action).to.eql(mentionedStatus)
- expect(state.notifications[1].type).to.eql('mention')
+ expect(state.notifications.data.length).to.eql(2)
+ expect(state.notifications.data[1].status).to.eql(mentionedStatus)
+ expect(state.notifications.data[1].action).to.eql(mentionedStatus)
+ expect(state.notifications.data[1].type).to.eql('mention')
mutations.addNewStatuses(state, { statuses: [deletion], user })
expect(state.allStatuses.length).to.eql(2)
- expect(state.notifications.length).to.eql(1)
- })
-
- it('adds the message to mentions when you are mentioned', () => {
- const user = { id: 1 }
- const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const mentionedStatus = makeMockStatus({id: 2})
- mentionedStatus.attentions = [user]
-
- mutations.addNewStatuses(state, { statuses: [status], user })
-
- expect(state.timelines.mentions.statuses).to.have.length(0)
-
- mutations.addNewStatuses(state, { statuses: [mentionedStatus], user })
- expect(state.timelines.mentions.statuses).to.have.length(1)
- expect(state.timelines.mentions.statuses).to.eql([mentionedStatus])
- })
-
- it('adds a notfication when one of the user\'s status is favorited', () => {
- const state = cloneDeep(defaultState)
- const status = makeMockStatus({id: 1})
- const user = {id: 1}
- status.user = user
-
- const favorite = {
- id: 2,
- is_post_verb: false,
- in_reply_to_status_id: '1', // The API uses strings here...
- uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00',
- text: 'a favorited something by b',
- user: {}
- }
-
- mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public', user })
- mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public', user })
-
- expect(state.notifications).to.have.length(1)
- })
-
- it('adds a notification when the user is followed', () => {
- const state = cloneDeep(defaultState)
- const user = {id: 1, screen_name: 'b'}
- const follower = {id: 2, screen_name: 'a'}
-
- const follow = {
- id: 3,
- is_post_verb: false,
- activity_type: 'follow',
- text: 'a started following b',
- user: follower
- }
-
- mutations.addNewStatuses(state, { statuses: [follow], showImmediately: true, timeline: 'public', user })
-
- expect(state.notifications).to.have.length(1)
- })
-
- it('does not add a notification when an other user is followed', () => {
- const state = cloneDeep(defaultState)
- const user = {id: 1, screen_name: 'b'}
- const follower = {id: 2, screen_name: 'a'}
-
- const follow = {
- id: 3,
- is_post_verb: false,
- activity_type: 'follow',
- text: 'a started following b@shitposter.club',
- user: follower
- }
-
- mutations.addNewStatuses(state, { statuses: [follow], showImmediately: true, timeline: 'public', user })
-
- expect(state.notifications).to.have.length(0)
+ expect(state.notifications.data.length).to.eql(1)
})
})
})
diff --git a/yarn.lock b/yarn.lock
index 390ad70b..fdad8b49 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -434,6 +434,10 @@ babel-helper-replace-supers@^6.24.1:
babel-traverse "^6.24.1"
babel-types "^6.24.1"
+babel-helper-vue-jsx-merge-props@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz#22aebd3b33902328e513293a8e4992b384f9f1b6"
+
babel-helpers@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2"
@@ -500,6 +504,10 @@ babel-plugin-syntax-exponentiation-operator@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de"
+babel-plugin-syntax-jsx@^6.18.0:
+ version "6.18.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz#0af32a9a6e13ca7a3fd5069e62d7b0f58d0d8946"
+
babel-plugin-syntax-object-rest-spread@^6.8.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
@@ -516,7 +524,7 @@ babel-plugin-transform-async-generator-functions@^6.24.1:
babel-plugin-syntax-async-generators "^6.5.0"
babel-runtime "^6.22.0"
-babel-plugin-transform-async-to-generator@^6.24.1:
+babel-plugin-transform-async-to-generator@^6.22.0, babel-plugin-transform-async-to-generator@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761"
dependencies:
@@ -555,7 +563,7 @@ babel-plugin-transform-es2015-block-scoped-functions@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-block-scoping@^6.24.1:
+babel-plugin-transform-es2015-block-scoping@^6.23.0, babel-plugin-transform-es2015-block-scoping@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f"
dependencies:
@@ -565,7 +573,7 @@ babel-plugin-transform-es2015-block-scoping@^6.24.1:
babel-types "^6.26.0"
lodash "^4.17.4"
-babel-plugin-transform-es2015-classes@^6.24.1:
+babel-plugin-transform-es2015-classes@^6.23.0, babel-plugin-transform-es2015-classes@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db"
dependencies:
@@ -579,33 +587,33 @@ babel-plugin-transform-es2015-classes@^6.24.1:
babel-traverse "^6.24.1"
babel-types "^6.24.1"
-babel-plugin-transform-es2015-computed-properties@^6.24.1:
+babel-plugin-transform-es2015-computed-properties@^6.22.0, babel-plugin-transform-es2015-computed-properties@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3"
dependencies:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
-babel-plugin-transform-es2015-destructuring@^6.22.0:
+babel-plugin-transform-es2015-destructuring@^6.22.0, babel-plugin-transform-es2015-destructuring@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d"
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
+babel-plugin-transform-es2015-duplicate-keys@^6.22.0, babel-plugin-transform-es2015-duplicate-keys@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e"
dependencies:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
-babel-plugin-transform-es2015-for-of@^6.22.0:
+babel-plugin-transform-es2015-for-of@^6.22.0, babel-plugin-transform-es2015-for-of@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691"
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-function-name@^6.24.1:
+babel-plugin-transform-es2015-function-name@^6.22.0, babel-plugin-transform-es2015-function-name@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b"
dependencies:
@@ -619,7 +627,7 @@ babel-plugin-transform-es2015-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-modules-amd@^6.24.1:
+babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154"
dependencies:
@@ -627,6 +635,15 @@ babel-plugin-transform-es2015-modules-amd@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
+babel-plugin-transform-es2015-modules-commonjs@^6.23.0:
+ version "6.26.2"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3"
+ dependencies:
+ babel-plugin-transform-strict-mode "^6.24.1"
+ babel-runtime "^6.26.0"
+ babel-template "^6.26.0"
+ babel-types "^6.26.0"
+
babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.0.tgz#0d8394029b7dc6abe1a97ef181e00758dd2e5d8a"
@@ -636,7 +653,7 @@ babel-plugin-transform-es2015-modules-commonjs@^6.24.1:
babel-template "^6.26.0"
babel-types "^6.26.0"
-babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
+babel-plugin-transform-es2015-modules-systemjs@^6.23.0, babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23"
dependencies:
@@ -644,7 +661,7 @@ babel-plugin-transform-es2015-modules-systemjs@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
-babel-plugin-transform-es2015-modules-umd@^6.24.1:
+babel-plugin-transform-es2015-modules-umd@^6.23.0, babel-plugin-transform-es2015-modules-umd@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468"
dependencies:
@@ -652,14 +669,14 @@ babel-plugin-transform-es2015-modules-umd@^6.24.1:
babel-runtime "^6.22.0"
babel-template "^6.24.1"
-babel-plugin-transform-es2015-object-super@^6.24.1:
+babel-plugin-transform-es2015-object-super@^6.22.0, babel-plugin-transform-es2015-object-super@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d"
dependencies:
babel-helper-replace-supers "^6.24.1"
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-parameters@^6.24.1:
+babel-plugin-transform-es2015-parameters@^6.23.0, babel-plugin-transform-es2015-parameters@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b"
dependencies:
@@ -670,7 +687,7 @@ babel-plugin-transform-es2015-parameters@^6.24.1:
babel-traverse "^6.24.1"
babel-types "^6.24.1"
-babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
+babel-plugin-transform-es2015-shorthand-properties@^6.22.0, babel-plugin-transform-es2015-shorthand-properties@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0"
dependencies:
@@ -683,7 +700,7 @@ babel-plugin-transform-es2015-spread@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-sticky-regex@^6.24.1:
+babel-plugin-transform-es2015-sticky-regex@^6.22.0, babel-plugin-transform-es2015-sticky-regex@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc"
dependencies:
@@ -697,13 +714,13 @@ babel-plugin-transform-es2015-template-literals@^6.22.0:
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-typeof-symbol@^6.22.0:
+babel-plugin-transform-es2015-typeof-symbol@^6.22.0, babel-plugin-transform-es2015-typeof-symbol@^6.23.0:
version "6.23.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372"
dependencies:
babel-runtime "^6.22.0"
-babel-plugin-transform-es2015-unicode-regex@^6.24.1:
+babel-plugin-transform-es2015-unicode-regex@^6.22.0, babel-plugin-transform-es2015-unicode-regex@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9"
dependencies:
@@ -711,7 +728,7 @@ babel-plugin-transform-es2015-unicode-regex@^6.24.1:
babel-runtime "^6.22.0"
regexpu-core "^2.0.0"
-babel-plugin-transform-exponentiation-operator@^6.24.1:
+babel-plugin-transform-exponentiation-operator@^6.22.0, babel-plugin-transform-exponentiation-operator@^6.24.1:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e"
dependencies:
@@ -726,7 +743,7 @@ babel-plugin-transform-object-rest-spread@^6.22.0:
babel-plugin-syntax-object-rest-spread "^6.8.0"
babel-runtime "^6.26.0"
-babel-plugin-transform-regenerator@^6.24.1:
+babel-plugin-transform-regenerator@^6.22.0, babel-plugin-transform-regenerator@^6.24.1:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
dependencies:
@@ -745,6 +762,47 @@ babel-plugin-transform-strict-mode@^6.24.1:
babel-runtime "^6.22.0"
babel-types "^6.24.1"
+babel-plugin-transform-vue-jsx@3:
+ version "3.7.0"
+ resolved "https://registry.yarnpkg.com/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-3.7.0.tgz#d40492e6692a36b594f7e9a1928f43e969740960"
+ dependencies:
+ esutils "^2.0.2"
+
+babel-preset-env@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a"
+ dependencies:
+ babel-plugin-check-es2015-constants "^6.22.0"
+ babel-plugin-syntax-trailing-function-commas "^6.22.0"
+ babel-plugin-transform-async-to-generator "^6.22.0"
+ babel-plugin-transform-es2015-arrow-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoped-functions "^6.22.0"
+ babel-plugin-transform-es2015-block-scoping "^6.23.0"
+ babel-plugin-transform-es2015-classes "^6.23.0"
+ babel-plugin-transform-es2015-computed-properties "^6.22.0"
+ babel-plugin-transform-es2015-destructuring "^6.23.0"
+ babel-plugin-transform-es2015-duplicate-keys "^6.22.0"
+ babel-plugin-transform-es2015-for-of "^6.23.0"
+ babel-plugin-transform-es2015-function-name "^6.22.0"
+ babel-plugin-transform-es2015-literals "^6.22.0"
+ babel-plugin-transform-es2015-modules-amd "^6.22.0"
+ babel-plugin-transform-es2015-modules-commonjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-systemjs "^6.23.0"
+ babel-plugin-transform-es2015-modules-umd "^6.23.0"
+ babel-plugin-transform-es2015-object-super "^6.22.0"
+ babel-plugin-transform-es2015-parameters "^6.23.0"
+ babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
+ babel-plugin-transform-es2015-spread "^6.22.0"
+ babel-plugin-transform-es2015-sticky-regex "^6.22.0"
+ babel-plugin-transform-es2015-template-literals "^6.22.0"
+ babel-plugin-transform-es2015-typeof-symbol "^6.23.0"
+ babel-plugin-transform-es2015-unicode-regex "^6.22.0"
+ babel-plugin-transform-exponentiation-operator "^6.22.0"
+ babel-plugin-transform-regenerator "^6.22.0"
+ browserslist "^3.2.6"
+ invariant "^2.2.2"
+ semver "^5.3.0"
+
babel-preset-es2015@^6.0.0:
version "6.24.1"
resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.1.tgz#d44050d6bc2c9feea702aaf38d727a0210538939"
@@ -996,6 +1054,13 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6:
caniuse-db "^1.0.30000639"
electron-to-chromium "^1.2.7"
+browserslist@^3.2.6:
+ version "3.2.8"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6"
+ dependencies:
+ caniuse-lite "^1.0.30000844"
+ electron-to-chromium "^1.3.47"
+
buffer@^4.9.0:
version "4.9.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298"
@@ -1069,6 +1134,10 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639:
version "1.0.30000801"
resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000801.tgz#a1d49def94c4e5aca5ccf1d58812e4668fac19d4"
+caniuse-lite@^1.0.30000844:
+ version "1.0.30000878"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000878.tgz#c644c39588dd42d3498e952234c372e5a40a4123"
+
caseless@~0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -1789,6 +1858,10 @@ electron-to-chromium@^1.2.7:
version "1.3.32"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.32.tgz#11d0684c0840e003c4be8928f8ac5f35dbc2b4e6"
+electron-to-chromium@^1.3.47:
+ version "1.3.61"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.61.tgz#a8ac295b28d0f03d85e37326fd16b6b6b17a1795"
+
emojis-list@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
@@ -3081,6 +3154,10 @@ isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
+iso-639-1@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/iso-639-1/-/iso-639-1-2.0.3.tgz#72dd3448ac5629c271628c5ac566369428d6ccd0"
+
isobject@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89"