aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.js43
-rw-r--r--src/App.scss117
-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/interface_language_switcher/interface_language_switcher.vue38
-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/notification/notification.vue11
-rw-r--r--src/components/notifications/notifications.js38
-rw-r--r--src/components/notifications/notifications.scss77
-rw-r--r--src/components/notifications/notifications.vue15
-rw-r--r--src/components/post_status_form/post_status_form.js22
-rw-r--r--src/components/post_status_form/post_status_form.vue95
-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.vue193
-rw-r--r--src/components/status/status.js115
-rw-r--r--src/components/status/status.vue107
-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.vue236
-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.js5
-rw-r--r--src/components/timeline/timeline.vue43
-rw-r--r--src/components/user_card/user_card.vue4
-rw-r--r--src/components/user_card_content/user_card_content.vue10
-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.js30
-rw-r--r--src/components/user_settings/user_settings.vue225
-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.js370
-rw-r--r--src/main.js151
-rw-r--r--src/modules/api.js3
-rw-r--r--src/modules/config.js17
-rw-r--r--src/modules/statuses.js187
-rw-r--r--src/modules/users.js4
-rw-r--r--src/services/api/api.service.js19
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js11
-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
49 files changed, 1968 insertions, 734 deletions
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 2426b998..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: '✔';
@@ -218,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;
@@ -226,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;
@@ -274,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 {
@@ -433,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/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/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/notification/notification.vue b/src/components/notification/notification.vue
index bb76ddf8..72c1ca69 100644
--- a/src/components/notification/notification.vue
+++ b/src/components/notification/notification.vue
@@ -12,7 +12,7 @@
<div class="name-and-action">
<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 === 'favorite'">
+ <span v-if="notification.type === 'like'">
<i class="fa icon-star lit"></i>
<small>{{$t('notifications.favorited_you')}}</small>
</span>
@@ -25,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 5853c68e..a137ccd5 100644
--- a/src/components/notifications/notifications.scss
+++ b/src/components/notifications/notifications.scss
@@ -4,44 +4,26 @@
// 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 {
@@ -54,7 +36,18 @@
box-sizing: border-box;
display: flex;
border-bottom: 1px solid;
- border-bottom-color: inherit;
+ 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;
@@ -69,7 +62,7 @@
}
}
- &:hover .animated.avatar {
+ &:hover .animated.avatar-compact {
canvas {
display: none;
}
@@ -145,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;
@@ -194,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 61f2ac0a..d7f1ffb2 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -24,7 +24,8 @@ const PostStatusForm = {
'replyTo',
'repliedUser',
'attentions',
- 'messageScope'
+ 'messageScope',
+ 'subject'
],
components: {
MediaUpload
@@ -52,9 +53,12 @@ const PostStatusForm = {
posting: false,
highlighted: 0,
newStatus: {
+ spoilerText: this.subject,
status: statusText,
+ contentType: 'text/plain',
+ nsfw: false,
files: [],
- visibility: this.messageScope || 'public'
+ visibility: this.messageScope || this.$store.state.users.currentUser.default_scope
},
caret: 0
}
@@ -72,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
}
@@ -86,7 +90,7 @@ 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
}
@@ -135,6 +139,9 @@ const PostStatusForm = {
},
scopeOptionsEnabled () {
return this.$store.state.config.scopeOptionsEnabled
+ },
+ formattingOptionsEnabled () {
+ return this.$store.state.config.formattingOptionsEnabled
}
},
methods: {
@@ -204,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 7aa0e7c4..42e9c65c 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -32,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" :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 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">
@@ -65,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>
@@ -99,25 +118,6 @@
}
}
-.post-status-form .visibility-tray {
- font-size: 1.2em;
- padding: 3px;
- cursor: pointer;
-
- .selected {
- color: $fallback--lightFg;
- color: var(--lightFg, $fallback--lightFg);
- }
-}
-
-.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);
-}
-
.post-status-form, .login {
.form-bottom {
display: flex;
@@ -139,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/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index f5b00599..ee5722bd 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -1,7 +1,12 @@
<template>
- <div v-if="loggedIn && 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>
+ <div v-if="loggedIn">
+ <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-if="!loggedIn">
<i :class='classes' class='icon-retweet'></i>
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 6245e758..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">
@@ -61,12 +145,35 @@
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;
@@ -82,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 a2d6f41f..45f5ccac 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -22,15 +22,18 @@ 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
@@ -80,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) {
@@ -98,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) ||
@@ -163,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
@@ -200,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 e2fb5d36..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>
@@ -11,8 +11,8 @@
<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 v-if="retweeterHtml" :href="statusoid.user.statusnet_profile_url" style="font-weight: bold;" :title="'@'+statusoid.user.screen_name" v-html="retweeterHtml"></a>
- <a v-else :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>
@@ -57,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>
@@ -74,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>
@@ -98,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" :message-scope="status.visibility" 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>
@@ -141,6 +149,7 @@
margin-top: 0.25em;
margin-left: 0.5em;
z-index: 50;
+
.status {
flex: 1;
border: 0;
@@ -155,6 +164,7 @@
text-align: center;
border-width: 1px;
border-style: solid;
+
i {
font-size: 2em;
}
@@ -196,6 +206,7 @@
.media-heading {
flex-wrap: nowrap;
+ line-height: 18px;
}
.media-heading-left {
@@ -218,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);
@@ -247,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 {
@@ -289,7 +316,7 @@
}
}
- .tall-status-unhider {
+ .status-unhider, .cw-status-hider {
width: 100%;
text-align: center;
}
@@ -308,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;
+
.avatar {
border-radius: $fallback--avatarAltRadius;
border-radius: var(--avatarAltRadius, $fallback--avatarAltRadius);
@@ -333,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;
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 112bbc1e..72a338bd 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -1,97 +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="{
- backgroundColor: style[1],
- color: style[3]
- }">{{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',
@@ -122,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>
@@ -134,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;
}
@@ -152,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;
@@ -219,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 5c179567..a651f619 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -133,7 +133,10 @@ const Timeline = {
}
if (count > 0) {
// only 'stream' them when you're scrolled to the top
- if (window.pageYOffset < 15 && !this.paused && !this.unfocused) {
+ 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.vue b/src/components/user_card/user_card.vue
index 7e3e0afe..48f272ca 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -73,12 +73,14 @@
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 {
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 71222d15..59358040 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -105,8 +105,8 @@
<span>{{user.followers_count}}</span>
</div>
</div>
- <p v-if="!hideBio && user.description_html" v-html="user.description_html"></p>
- <p v-else-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>
@@ -130,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 {
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 443e63dd..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 = {
@@ -6,6 +7,7 @@ const UserSettings = {
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,
@@ -17,11 +19,13 @@ const UserSettings = {
deleteAccountError: false,
changePasswordInputs: [ '', '', '' ],
changedPassword: false,
- changePasswordError: false
+ changePasswordError: false,
+ activeTab: 'profile'
}
},
components: {
- StyleSwitcher
+ StyleSwitcher,
+ TabSwitcher
},
computed: {
user () {
@@ -29,6 +33,17 @@ 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: {
@@ -36,12 +51,18 @@ const UserSettings = {
const name = this.newname
const description = this.newbio
const locked = this.newlocked
- this.$store.state.api.backendInteractor.updateProfile({params: {name, description, locked}}).then((user) => {
+ /* 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]
@@ -217,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 881b0fa1..9daafdce 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -4,112 +4,131 @@
{{$t('settings.user_settings')}}
</div>
<div class="panel-body profile-edit">
- <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>
- <div class="setting-item">
- <input type="checkbox" v-model="newlocked" id="account-locked">
- <label for="account-locked">{{$t('settings.lock_account_description')}}</label>
+ <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>
- <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 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" v-if="pleromaBackend">
- <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 :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">
- <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>
- <hr>
- <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 :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>
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 e0b961a2..d0fc46a2 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -51,6 +51,9 @@ const de = {
settings: 'Einstellungen',
theme: 'Farbschema',
presets: 'Voreinstellungen',
+ 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',
@@ -275,9 +278,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',
@@ -293,7 +298,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',
@@ -316,10 +324,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.',
@@ -337,14 +358,22 @@ const en = {
confirm_new_password: 'Confirm new password',
changed_password: 'Password changed successfully!',
change_password_error: 'There was an issue changing your password.',
- lock_account_description: 'Restrict your account to approved followers only'
+ 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',
@@ -369,11 +398,15 @@ const en = {
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: {
@@ -386,19 +419,32 @@ 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'
}
}
const eo = {
chat: {
- title: 'Babilo'
+ title: 'Babilejo'
},
nav: {
- chat: 'Loka babilo',
- timeline: 'Tempovido',
+ chat: 'Loka babilejo',
+ timeline: 'Tempolinio',
mentions: 'Mencioj',
- public_tl: 'Publika tempovido',
- twkn: 'Tuta konata reto'
+ public_tl: 'Publika tempolinio',
+ twkn: 'La tuta konata reto'
},
user_card: {
follows_you: 'Abonas vin!',
@@ -408,26 +454,26 @@ const eo = {
block: 'Bari',
statuses: 'Statoj',
mute: 'Silentigi',
- muted: 'Silentigita',
+ muted: 'Silentigitaj',
followers: 'Abonantoj',
followees: 'Abonatoj',
per_day: 'tage',
- remote_follow: 'Fora abono'
+ remote_follow: 'Fore aboni'
},
timeline: {
show_new: 'Montri novajn',
- error_fetching: 'Eraro ĝisdatigante',
+ error_fetching: 'Eraro dum ĝisdatigo',
up_to_date: 'Ĝisdata',
- load_older: 'Enlegi pli malnovajn statojn',
+ load_older: 'Montri pli malnovajn statojn',
conversation: 'Interparolo',
collapse: 'Maletendi',
repeated: 'ripetata'
},
settings: {
- user_settings: 'Uzulaj agordoj',
- name_bio: 'Nomo kaj prio',
+ user_settings: 'Uzantaj agordoj',
+ name_bio: 'Nomo kaj priskribo',
name: 'Nomo',
- bio: 'Prio',
+ bio: 'Priskribo',
avatar: 'Profilbildo',
current_avatar: 'Via nuna profilbildo',
set_new_avatar: 'Agordi novan profilbildon',
@@ -437,9 +483,9 @@ const eo = {
profile_background: 'Profila fono',
set_new_profile_background: 'Agordi novan profilan fonon',
settings: 'Agordoj',
- theme: 'Haŭto',
- presets: 'Antaŭmetaĵoj',
- theme_help: 'Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran haŭton.',
+ theme: 'Etoso',
+ presets: 'Antaŭagordoj',
+ theme_help: 'Uzu deksesumajn kolorkodojn (#rrvvbb) por adapti vian koloran etoson.',
radii_help: 'Agordi fasadan rondigon de randoj (rastrumere)',
background: 'Fono',
foreground: 'Malfono',
@@ -447,65 +493,65 @@ const eo = {
links: 'Ligiloj',
cBlue: 'Blua (Respondo, abono)',
cRed: 'Ruĝa (Nuligo)',
- cOrange: 'Orange (Ŝato)',
+ cOrange: 'Oranĝa (Ŝato)',
cGreen: 'Verda (Kunhavigo)',
btnRadius: 'Butonoj',
panelRadius: 'Paneloj',
avatarRadius: 'Profilbildoj',
- avatarAltRadius: 'Profilbildoj (Sciigoj)',
+ avatarAltRadius: 'Profilbildoj (sciigoj)',
tooltipRadius: 'Ŝpruchelpiloj/avertoj',
attachmentRadius: 'Kunsendaĵoj',
filtering: 'Filtrado',
filtering_explanation: 'Ĉiuj statoj kun tiuj ĉi vortoj silentiĝos, po unu linie',
attachments: 'Kunsendaĵoj',
- hide_attachments_in_tl: 'Kaŝi kunsendaĵojn en tempovido',
+ hide_attachments_in_tl: 'Kaŝi kunsendaĵojn en tempolinio',
hide_attachments_in_convo: 'Kaŝi kunsendaĵojn en interparoloj',
nsfw_clickthrough: 'Ŝalti traklakan kaŝon de konsternaj kunsendaĵoj',
stop_gifs: 'Movi GIF-bildojn dum ŝvebo',
- autoload: 'Ŝalti memfaran enlegadon ĉe subo de paĝo',
- streaming: 'Ŝalti memfaran fluigon de novaj afiŝoj ĉe supro de paĝo',
+ autoload: 'Ŝalti memfaran ŝarĝadon ĉe subo de paĝo',
+ streaming: 'Ŝalti memfaran fluigon de novaj afiŝoj ĉe la supro de la paĝo',
reply_link_preview: 'Ŝalti respond-ligilan antaŭvidon dum ŝvebo',
follow_import: 'Abona enporto',
- import_followers_from_a_csv_file: 'Enporti abonojn de CSV-dosiero',
+ import_followers_from_a_csv_file: 'Enporti abonojn el CSV-dosiero',
follows_imported: 'Abonoj enportiĝis! Traktado daŭros iom.',
follow_import_error: 'Eraro enportante abonojn'
},
notifications: {
notifications: 'Sciigoj',
- read: 'Legita!',
+ read: 'Legite!',
followed_you: 'ekabonis vin',
favorited_you: 'ŝatis vian staton',
repeated_you: 'ripetis vian staton'
},
login: {
- login: 'Saluti',
+ login: 'Ensaluti',
username: 'Salutnomo',
placeholder: 'ekz. lain',
password: 'Pasvorto',
register: 'Registriĝi',
- logout: 'Adiaŭi'
+ logout: 'Elsaluti'
},
registration: {
registration: 'Registriĝo',
fullname: 'Vidiga nomo',
email: 'Retpoŝtadreso',
- bio: 'Prio',
+ bio: 'Priskribo',
password_confirm: 'Konfirmo de pasvorto'
},
post_status: {
- posting: 'Afiŝanta',
- default: 'Ĵus alvenis la universalan kongreson!'
+ posting: 'Afiŝante',
+ default: 'Ĵus alvenis al la Universala Kongreso!'
},
finder: {
- find_user: 'Trovi uzulon',
- error_fetching_user: 'Eraro alportante uzulon'
+ find_user: 'Trovi uzanton',
+ error_fetching_user: 'Eraro alportante uzanton'
},
general: {
submit: 'Sendi',
apply: 'Apliki'
},
user_profile: {
- timeline_title: 'Uzula tempovido'
+ timeline_title: 'Uzanta tempolinio'
}
}
@@ -769,115 +815,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: 'ユーザー名',
- placeholder: '例えば lain',
+ 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: 'もじのかず'
}
}
@@ -1089,8 +1176,8 @@ const oc = {
twkn: 'Lo malhum conegut'
},
user_card: {
- follows_you: 'Vos sèc !',
- following: 'Seguit !',
+ follows_you: 'Vos sèc!',
+ following: 'Seguit!',
follow: 'Seguir',
blocked: 'Blocat',
block: 'Blocar',
@@ -1135,10 +1222,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)',
@@ -1157,12 +1244,25 @@ 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',
- read: 'Legit !',
- followed_you: 'vos sèc',
+ read: 'Legit!',
+ followed_you: 'vos a seguit',
favorited_you: 'a aimat vòstre estatut',
repeated_you: 'a repetit your vòstre estatut'
},
@@ -1183,6 +1283,7 @@ const oc = {
},
post_status: {
posting: 'Mandadís',
+ content_warning: 'Avís de contengut (opcional)',
default: 'Escrivètz aquí vòstre estatut.'
},
finder: {
@@ -1438,7 +1539,7 @@ const pt = {
title: 'Chat'
},
nav: {
- chat: 'Chat Local',
+ chat: 'Chat local',
timeline: 'Linha do tempo',
mentions: 'Menções',
public_tl: 'Linha do tempo pública',
@@ -1482,16 +1583,28 @@ const pt = {
theme: 'Tema',
presets: 'Predefinições',
theme_help: 'Use cores em código hexadecimal (#rrggbb) para personalizar seu esquema de cores.',
+ radii_help: 'Arredondar arestas da interface (em píxeis)',
background: 'Plano de Fundo',
foreground: 'Primeiro Plano',
text: 'Texto',
links: 'Links',
+ cBlue: 'Azul (Responder, seguir)',
+ cRed: 'Vermelho (Cancelar)',
+ cOrange: 'Laranja (Favoritar)',
+ cGreen: 'Verde (Repetir)',
+ btnRadius: 'Botões',
+ panelRadius: 'Paineis',
+ avatarRadius: 'Avatares',
+ avatarAltRadius: 'Avatares (Notificações)',
+ tooltipRadius: 'Dicass/alertas',
+ attachmentRadius: 'Anexos',
filtering: 'Filtragem',
filtering_explanation: 'Todas as postagens contendo estas palavras serão silenciadas, uma por linha.',
attachments: 'Anexos',
hide_attachments_in_tl: 'Ocultar anexos na linha do tempo.',
hide_attachments_in_convo: 'Ocultar anexos em conversas',
nsfw_clickthrough: 'Habilitar clique para ocultar anexos NSFW',
+ stop_gifs: 'Reproduzir GIFs ao passar o cursor em cima',
autoload: 'Habilitar carregamento automático quando a rolagem chegar ao fim.',
streaming: 'Habilitar o fluxo automático de postagens quando ao topo da página',
reply_link_preview: 'Habilitar a pré-visualização de link de respostas ao passar o mouse.',
@@ -1502,8 +1615,10 @@ const pt = {
},
notifications: {
notifications: 'Notificações',
- read: 'Ler!',
- followed_you: 'seguiu você'
+ read: 'Lido!',
+ followed_you: 'seguiu você',
+ favorited_you: 'favoritou sua postagem',
+ repeated_you: 'repetiu sua postagem'
},
login: {
login: 'Entrar',
@@ -1522,7 +1637,7 @@ const pt = {
},
post_status: {
posting: 'Publicando',
- default: 'Acabo de aterrizar em L.A.'
+ default: 'Acabei de chegar no Rio!'
},
finder: {
find_user: 'Buscar usuário',
@@ -1531,6 +1646,9 @@ const pt = {
general: {
submit: 'Enviar',
apply: 'Aplicar'
+ },
+ user_profile: {
+ timeline_title: 'Linha do tempo do usuário'
}
}
@@ -1566,9 +1684,11 @@ const ru = {
load_older: 'Загрузить старые статусы',
conversation: 'Разговор',
collapse: 'Свернуть',
- repeated: 'повторил(а)'
+ repeated: 'повторил(а)',
+ no_retweet_hint: 'Пост помечен как "только для подписчиков" или "личное" и поэтому не может быть повторён'
},
settings: {
+ general: 'Общие',
user_settings: 'Настройки пользователя',
name_bio: 'Имя и описание',
name: 'Имя',
@@ -1583,9 +1703,11 @@ const ru = {
set_new_profile_background: 'Загрузить новый фон профиля',
settings: 'Настройки',
theme: 'Тема',
+ export_theme: 'Сохранить Тему',
+ import_theme: 'Загрузить Тему',
presets: 'Пресеты',
theme_help: 'Используйте шестнадцатеричные коды цветов (#rrggbb) для настройки темы.',
- radii_help: 'Округление краёв элементов интерфейса (в пикселях)',
+ radii_help: 'Скругление углов элементов интерфейса (в пикселях)',
background: 'Фон',
foreground: 'Передний план',
text: 'Текст',
@@ -1610,7 +1732,19 @@ 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: 'Список читаемых импортирован. Обработка займёт некоторое время..',
@@ -1627,14 +1761,23 @@ const ru = {
new_password: 'Новый пароль',
confirm_new_password: 'Подтверждение нового пароля',
changed_password: 'Пароль изменён успешно.',
- change_password_error: 'Произошла ошибка при попытке изменить пароль.'
+ 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: 'Войти',
@@ -1654,7 +1797,18 @@ const ru = {
},
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: 'Найти пользователя',
diff --git a/src/main.js b/src/main.js
index cb53edd3..75c2bab2 100644
--- a/src/main.js
+++ b/src/main.js
@@ -45,16 +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',
'config.highlight',
- 'users.lastLoginName'
+ 'config.loopVideo',
+ 'config.loopVideoSilentOnly',
+ 'config.pauseOnUnfocused',
+ 'config.stopGifs',
+ 'config.interfaceLanguage',
+ 'users.lastLoginName',
+ 'statuses.notifications.maxSavedId'
]
}
@@ -72,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
@@ -86,64 +96,81 @@ window.fetch('/api/statusnet/config.json')
store.dispatch('setOption', { name: 'registrationOpen', value: (registrationClosed === '0') })
store.dispatch('setOption', { name: 'textlimit', value: parseInt(textlimit) })
store.dispatch('setOption', { name: 'server', value: server })
- })
-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: '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 }
+ 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)
+ })
})
})
@@ -186,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 a61340c2..2f07a91e 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -46,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)
diff --git a/src/modules/config.js b/src/modules/config.js
index 2b50655b..60a34bc1 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -1,17 +1,32 @@
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,
+ pauseOnUnfocused: true,
+ stopGifs: false,
+ replyVisibility: 'all',
+ notificationVisibility: {
+ follows: true,
+ mentions: true,
+ likes: true,
+ repeats: true
+ },
muteWords: [],
- highlight: {}
+ highlight: {},
+ interfaceLanguage: browserLocale
}
const config = {
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 291ab53c..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,11 +438,31 @@ 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...
diff --git a/src/modules/users.js b/src/modules/users.js
index ba548765..e90d6bb9 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -107,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) => {
@@ -119,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 13cc4796..87315657 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -27,6 +27,7 @@ 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'
@@ -36,6 +37,7 @@ 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'
@@ -302,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
}
@@ -367,7 +373,7 @@ const unretweet = ({ id, credentials }) => {
})
}
-const postStatus = ({credentials, status, spoilerText, visibility, mediaIds, inReplyToStatusId}) => {
+const postStatus = ({credentials, status, spoilerText, visibility, sensitive, mediaIds, inReplyToStatusId, contentType}) => {
const idsText = mediaIds.join(',')
const form = new FormData()
@@ -375,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)
@@ -449,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,
@@ -482,7 +496,8 @@ const apiService = {
changePassword,
fetchFollowRequests,
approveUser,
- denyUser
+ 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 dbfb54f9..c84373ac 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -54,6 +54,16 @@ const backendInteractorService = (credentials) => {
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})
}
@@ -86,6 +96,7 @@ const backendInteractorService = (credentials) => {
fetchAllFollowing,
verifyCredentials: apiService.verifyCredentials,
startFetching,
+ fetchOldPost,
setUserMute,
fetchMutes,
register,
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
}