aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorHenry Jameson <me@hjkos.com>2019-11-08 19:48:31 +0200
committerHenry Jameson <me@hjkos.com>2019-11-08 19:48:31 +0200
commit0dcb696e26dfcf97ad46e533ed4797bc9364fc01 (patch)
tree2d7b5e7d43b92eb36a9b170e9c5e32dda5d2b7b4 /src
parent6ade3be5b40816f8765e657442aa2945e51ce7d4 (diff)
parent632773ba91ef021dd589ab4d2037f5e0ed7ca5b2 (diff)
Merge remote-tracking branch 'upstream/develop' into emoji-optimizations
* upstream/develop: (95 commits) Lightbox/modal multi image improvements - #381 '/api/pleroma/profile/mfa' -> '/api/pleroma/accounts/mfa' Add ability to change user's email translations-de-batch-1 eu-translate update profile-banner rounding css, fixes #690 fix indentation remove needless ref show preview popover when hover numbered replies refactor conditions do not make too many nested div add fetchStatus action refactor status loading logic split status preview popover into a separate component uninstall mobile-detect library listen both events minor css fix restrict distance at top side only set different trigger event in desktop and mobile by default fix eslint warnings ...
Diffstat (limited to 'src')
-rw-r--r--src/App.js4
-rw-r--r--src/App.scss30
-rw-r--r--src/boot/routes.js2
-rw-r--r--src/components/account_actions/account_actions.js35
-rw-r--r--src/components/account_actions/account_actions.vue93
-rw-r--r--src/components/attachment/attachment.js16
-rw-r--r--src/components/attachment/attachment.vue1
-rw-r--r--src/components/checkbox/checkbox.vue41
-rw-r--r--src/components/emoji_input/emoji_input.js2
-rw-r--r--src/components/emoji_picker/emoji_picker.js5
-rw-r--r--src/components/emoji_picker/emoji_picker.vue20
-rw-r--r--src/components/extra_buttons/extra_buttons.vue2
-rw-r--r--src/components/favorite_button/favorite_button.js8
-rw-r--r--src/components/favorite_button/favorite_button.vue4
-rw-r--r--src/components/follow_button/follow_button.js53
-rw-r--r--src/components/follow_button/follow_button.vue13
-rw-r--r--src/components/follow_card/follow_card.js26
-rw-r--r--src/components/follow_card/follow_card.vue35
-rw-r--r--src/components/gallery/gallery.js42
-rw-r--r--src/components/gallery/gallery.vue41
-rw-r--r--src/components/interface_language_switcher/interface_language_switcher.vue2
-rw-r--r--src/components/login_form/login_form.js2
-rw-r--r--src/components/media_modal/media_modal.js25
-rw-r--r--src/components/media_modal/media_modal.vue40
-rw-r--r--src/components/mobile_nav/mobile_nav.js2
-rw-r--r--src/components/mobile_post_status_button/mobile_post_status_button.js2
-rw-r--r--src/components/modal/modal.vue52
-rw-r--r--src/components/moderation_tools/moderation_tools.vue2
-rw-r--r--src/components/notification/notification.js2
-rw-r--r--src/components/password_reset/password_reset.js6
-rw-r--r--src/components/password_reset/password_reset.vue16
-rw-r--r--src/components/popper/popper.scss9
-rw-r--r--src/components/post_status_form/post_status_form.js31
-rw-r--r--src/components/post_status_form/post_status_form.vue9
-rw-r--r--src/components/post_status_modal/post_status_modal.js4
-rw-r--r--src/components/post_status_modal/post_status_modal.vue19
-rw-r--r--src/components/retweet_button/retweet_button.js8
-rw-r--r--src/components/retweet_button/retweet_button.vue4
-rw-r--r--src/components/settings/settings.js239
-rw-r--r--src/components/settings/settings.vue308
-rw-r--r--src/components/status/status.js75
-rw-r--r--src/components/status/status.vue153
-rw-r--r--src/components/status_popover/status_popover.js34
-rw-r--r--src/components/status_popover/status_popover.vue85
-rw-r--r--src/components/still-image/still-image.js6
-rw-r--r--src/components/style_switcher/style_switcher.js12
-rw-r--r--src/components/style_switcher/style_switcher.vue45
-rw-r--r--src/components/timeline/timeline.js6
-rw-r--r--src/components/user_card/user_card.js65
-rw-r--r--src/components/user_card/user_card.vue126
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.js4
-rw-r--r--src/components/user_reporting_modal/user_reporting_modal.vue12
-rw-r--r--src/components/user_settings/user_settings.js24
-rw-r--r--src/components/user_settings/user_settings.vue147
-rw-r--r--src/components/video_attachment/video_attachment.js8
-rw-r--r--src/directives/body_scroll_lock.js51
-rw-r--r--src/i18n/de.json43
-rw-r--r--src/i18n/en.json10
-rw-r--r--src/i18n/es.json19
-rw-r--r--src/i18n/eu.json37
-rw-r--r--src/i18n/ru.json4
-rw-r--r--src/main.js8
-rw-r--r--src/modules/config.js31
-rw-r--r--src/modules/instance.js8
-rw-r--r--src/modules/statuses.js4
-rw-r--r--src/modules/users.js19
-rw-r--r--src/services/api/api.service.js36
-rw-r--r--src/services/backend_interactor_service/backend_interactor_service.js6
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js1
-rw-r--r--src/services/follow_manipulate/follow_manipulate.js2
-rw-r--r--src/services/notifications_fetcher/notifications_fetcher.service.js5
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js16
72 files changed, 1244 insertions, 1113 deletions
diff --git a/src/App.js b/src/App.js
index fe63b54c..04a40e30 100644
--- a/src/App.js
+++ b/src/App.js
@@ -45,7 +45,7 @@ export default {
}),
created () {
// Load the locale from the storage
- this.$i18n.locale = this.$store.state.config.interfaceLanguage
+ this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage
window.addEventListener('resize', this.updateMobileState)
},
destroyed () {
@@ -93,7 +93,7 @@ export default {
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
- !this.$store.state.config.hideISP &&
+ !this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
diff --git a/src/App.scss b/src/App.scss
index fe271ce1..310962b8 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -39,10 +39,13 @@ h4 {
text-align: center;
}
+html {
+ font-size: 14px;
+}
+
body {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
- font-size: 14px;
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
@@ -717,31 +720,6 @@ nav {
}
}
-@keyframes modal-background-fadein {
- from {
- background-color: rgba(0, 0, 0, 0);
- }
- to {
- background-color: rgba(0, 0, 0, 0.5);
- }
-}
-
-.modal-view {
- z-index: 1000;
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- display: flex;
- justify-content: center;
- align-items: center;
- overflow: auto;
- animation-duration: 0.2s;
- background-color: rgba(0, 0, 0, 0.5);
- animation-name: modal-background-fadein;
-}
-
.button-icon {
font-size: 1.2em;
}
diff --git a/src/boot/routes.js b/src/boot/routes.js
index cd02711c..5670236c 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -47,7 +47,7 @@ export default (store) => {
{ name: 'dms', path: '/users/:username/dms', component: DMs, beforeEnter: validateAuthenticatedRoute },
{ name: 'settings', path: '/settings', component: Settings },
{ name: 'registration', path: '/registration', component: Registration },
- { name: 'password-reset', path: '/password-reset', component: PasswordReset },
+ { name: 'password-reset', path: '/password-reset', component: PasswordReset, props: true },
{ name: 'registration-token', path: '/registration/:token', component: Registration },
{ name: 'friend-requests', path: '/friend-requests', component: FollowRequests, beforeEnter: validateAuthenticatedRoute },
{ name: 'user-settings', path: '/user-settings', component: UserSettings, beforeEnter: validateAuthenticatedRoute },
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
new file mode 100644
index 00000000..204d506a
--- /dev/null
+++ b/src/components/account_actions/account_actions.js
@@ -0,0 +1,35 @@
+import ProgressButton from '../progress_button/progress_button.vue'
+
+const AccountActions = {
+ props: [
+ 'user'
+ ],
+ data () {
+ return { }
+ },
+ components: {
+ ProgressButton
+ },
+ methods: {
+ showRepeats () {
+ this.$store.dispatch('showReblogs', this.user.id)
+ },
+ hideRepeats () {
+ this.$store.dispatch('hideReblogs', this.user.id)
+ },
+ blockUser () {
+ this.$store.dispatch('blockUser', this.user.id)
+ },
+ unblockUser () {
+ this.$store.dispatch('unblockUser', this.user.id)
+ },
+ reportUser () {
+ this.$store.dispatch('openUserReportingModal', this.user.id)
+ },
+ mentionUser () {
+ this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
+ }
+ }
+}
+
+export default AccountActions
diff --git a/src/components/account_actions/account_actions.vue b/src/components/account_actions/account_actions.vue
new file mode 100644
index 00000000..046cba93
--- /dev/null
+++ b/src/components/account_actions/account_actions.vue
@@ -0,0 +1,93 @@
+<template>
+ <div class="account-actions">
+ <v-popover
+ trigger="click"
+ class="account-tools-popover"
+ :container="false"
+ placement="bottom-end"
+ :offset="5"
+ >
+ <div slot="popover">
+ <div class="dropdown-menu">
+ <button
+ class="btn btn-default btn-block dropdown-item"
+ @click="mentionUser"
+ >
+ {{ $t('user_card.mention') }}
+ </button>
+ <template v-if="user.following">
+ <div
+ role="separator"
+ class="dropdown-divider"
+ />
+ <button
+ v-if="user.showing_reblogs"
+ class="btn btn-default dropdown-item"
+ @click="hideRepeats"
+ >
+ {{ $t('user_card.hide_repeats') }}
+ </button>
+ <button
+ v-if="!user.showing_reblogs"
+ class="btn btn-default dropdown-item"
+ @click="showRepeats"
+ >
+ {{ $t('user_card.show_repeats') }}
+ </button>
+ </template>
+ <div
+ role="separator"
+ class="dropdown-divider"
+ />
+ <button
+ v-if="user.statusnet_blocking"
+ class="btn btn-default btn-block dropdown-item"
+ @click="unblockUser"
+ >
+ {{ $t('user_card.unblock') }}
+ </button>
+ <button
+ v-else
+ class="btn btn-default btn-block dropdown-item"
+ @click="blockUser"
+ >
+ {{ $t('user_card.block') }}
+ </button>
+ <button
+ class="btn btn-default btn-block dropdown-item"
+ @click="reportUser"
+ >
+ {{ $t('user_card.report') }}
+ </button>
+ </div>
+ </div>
+ <div class="btn btn-default ellipsis-button">
+ <i class="icon-ellipsis trigger-button" />
+ </div>
+ </v-popover>
+ </div>
+</template>
+
+<script src="./account_actions.js"></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+@import '../popper/popper.scss';
+.account-actions {
+ margin: 0 .8em;
+}
+
+.account-actions button.dropdown-item {
+ margin-left: 0;
+}
+.account-actions .trigger-button {
+ color: $fallback--lightText;
+ color: var(--lightText, $fallback--lightText);
+ opacity: .8;
+ cursor: pointer;
+ &:hover {
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
+ }
+}
+</style>
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index e93921fe..06b496b0 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -10,13 +10,14 @@ const Attachment = {
'statusId',
'size',
'allowPlay',
- 'setMedia'
+ 'setMedia',
+ 'naturalSizeLoad'
],
data () {
return {
nsfwImage: this.$store.state.instance.nsfwCensorImage || nsfwImage,
- hideNsfwLocal: this.$store.state.config.hideNsfw,
- preloadImage: this.$store.state.config.preloadImage,
+ hideNsfwLocal: this.$store.getters.mergedConfig.hideNsfw,
+ preloadImage: this.$store.getters.mergedConfig.preloadImage,
loading: false,
img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'),
modalOpen: false,
@@ -57,7 +58,7 @@ const Attachment = {
}
},
openModal (event) {
- const modalTypes = this.$store.state.config.playVideosInModal
+ const modalTypes = this.$store.getters.mergedConfig.playVideosInModal
? ['image', 'video']
: ['image']
if (fileTypeService.fileMatchesSomeType(modalTypes, this.attachment) ||
@@ -70,7 +71,7 @@ const Attachment = {
}
},
toggleHidden (event) {
- if (this.$store.state.config.useOneClickNsfw && !this.showHidden) {
+ if (this.$store.getters.mergedConfig.useOneClickNsfw && !this.showHidden) {
this.openModal(event)
return
}
@@ -88,6 +89,11 @@ const Attachment = {
} else {
this.showHidden = !this.showHidden
}
+ },
+ onImageLoad (image) {
+ const width = image.naturalWidth
+ const height = image.naturalHeight
+ this.naturalSizeLoad && this.naturalSizeLoad({ width, height })
}
}
}
diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue
index af16e302..0748b2f0 100644
--- a/src/components/attachment/attachment.vue
+++ b/src/components/attachment/attachment.vue
@@ -58,6 +58,7 @@
:referrerpolicy="referrerpolicy"
:mimetype="attachment.mimetype"
:src="attachment.large_thumb_url || attachment.url"
+ :image-load-handler="onImageLoad"
/>
</a>
diff --git a/src/components/checkbox/checkbox.vue b/src/components/checkbox/checkbox.vue
index 2b822ec3..5917598a 100644
--- a/src/components/checkbox/checkbox.vue
+++ b/src/components/checkbox/checkbox.vue
@@ -1,13 +1,22 @@
<template>
- <label class="checkbox">
+ <label
+ class="checkbox"
+ :class="{ disabled, indeterminate }"
+ >
<input
type="checkbox"
+ :disabled="disabled"
:checked="checked"
:indeterminate.prop="indeterminate"
@change="$emit('change', $event.target.checked)"
>
<i class="checkbox-indicator" />
- <span v-if="!!$slots.default"><slot /></span>
+ <span
+ class="label"
+ v-if="!!$slots.default"
+ >
+ <slot />
+ </span>
</label>
</template>
@@ -17,7 +26,11 @@ export default {
prop: 'checked',
event: 'change'
},
- props: ['checked', 'indeterminate']
+ props: [
+ 'checked',
+ 'indeterminate',
+ 'disabled'
+ ]
}
</script>
@@ -27,12 +40,16 @@ export default {
.checkbox {
position: relative;
display: inline-block;
- padding-left: 1.2em;
min-height: 1.2em;
+ &-indicator {
+ position: relative;
+ padding-left: 1.2em;
+ }
+
&-indicator::before {
position: absolute;
- left: 0;
+ right: 0;
top: 0;
display: block;
content: '✔';
@@ -54,6 +71,17 @@ export default {
box-sizing: border-box;
}
+ &.disabled {
+ .checkbox-indicator::before,
+ .label {
+ opacity: .5;
+ }
+ .label {
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
+ }
+ }
+
input[type=checkbox] {
display: none;
@@ -68,9 +96,6 @@ export default {
color: var(--text, $fallback--text);
}
- &:disabled + .checkbox-indicator::before {
- opacity: .5;
- }
}
& > span {
diff --git a/src/components/emoji_input/emoji_input.js b/src/components/emoji_input/emoji_input.js
index 2c73faa7..001a22e9 100644
--- a/src/components/emoji_input/emoji_input.js
+++ b/src/components/emoji_input/emoji_input.js
@@ -99,7 +99,7 @@ const EmojiInput = {
},
computed: {
padEmoji () {
- return this.$store.state.config.padEmoji
+ return this.$store.getters.mergedConfig.padEmoji
},
suggestions () {
const firstchar = this.textAtCaret.charAt(0)
diff --git a/src/components/emoji_picker/emoji_picker.js b/src/components/emoji_picker/emoji_picker.js
index 0d5ffc9b..76d1f26b 100644
--- a/src/components/emoji_picker/emoji_picker.js
+++ b/src/components/emoji_picker/emoji_picker.js
@@ -1,4 +1,5 @@
import { set } from 'vue'
+import Checkbox from '../checkbox/checkbox.vue'
const LOAD_EMOJI_BY = 50
const LOAD_EMOJI_INTERVAL = 100
@@ -18,7 +19,6 @@ const EmojiPicker = {
},
data () {
return {
- labelKey: String(Math.random() * 100000),
keyword: '',
activeGroup: 'custom',
showingStickers: false,
@@ -32,7 +32,8 @@ const EmojiPicker = {
}
},
components: {
- StickerPicker: () => import('../sticker_picker/sticker_picker.vue')
+ StickerPicker: () => import('../sticker_picker/sticker_picker.vue'),
+ Checkbox
},
methods: {
onEmoji (emoji) {
diff --git a/src/components/emoji_picker/emoji_picker.vue b/src/components/emoji_picker/emoji_picker.vue
index e46a5ec1..43da6aa2 100644
--- a/src/components/emoji_picker/emoji_picker.vue
+++ b/src/components/emoji_picker/emoji_picker.vue
@@ -75,22 +75,10 @@
</span>
</div>
</div>
- <div
- class="keep-open"
- >
- <input
- :id="labelKey + 'keep-open'"
- v-model="keepOpen"
- type="checkbox"
- >
- <label
- class="keep-open-label"
- :for="labelKey + 'keep-open'"
- >
- <div class="keep-open-label-text">
- {{ $t('emoji.keep_open') }}
- </div>
- </label>
+ <div class="keep-open">
+ <Checkbox v-model="keepOpen">
+ {{ $t('emoji.keep_open') }}
+ </Checkbox>
</div>
<div
v-if="askForSanity"
diff --git a/src/components/extra_buttons/extra_buttons.vue b/src/components/extra_buttons/extra_buttons.vue
index 6781a4f8..746f1c91 100644
--- a/src/components/extra_buttons/extra_buttons.vue
+++ b/src/components/extra_buttons/extra_buttons.vue
@@ -4,8 +4,6 @@
trigger="click"
placement="top"
class="extra-button-popover"
- :offset="5"
- :container="false"
>
<div slot="popover">
<div class="dropdown-menu">
diff --git a/src/components/favorite_button/favorite_button.js b/src/components/favorite_button/favorite_button.js
index a24eacbf..5014d84f 100644
--- a/src/components/favorite_button/favorite_button.js
+++ b/src/components/favorite_button/favorite_button.js
@@ -1,10 +1,9 @@
+import { mapGetters } from 'vuex'
+
const FavoriteButton = {
props: ['status', 'loggedIn'],
data () {
return {
- hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined'
- ? this.$store.state.instance.hidePostStats
- : this.$store.state.config.hidePostStats,
animated: false
}
},
@@ -28,7 +27,8 @@ const FavoriteButton = {
'icon-star': this.status.favorited,
'animate-spin': this.animated
}
- }
+ },
+ ...mapGetters(['mergedConfig'])
}
}
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index 06ce9983..fbc90f84 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -6,7 +6,7 @@
:title="$t('tool_tip.favorite')"
@click.prevent="favorite()"
/>
- <span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span>
+ <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
<div v-else>
<i
@@ -14,7 +14,7 @@
class="button-icon favorite-button"
:title="$t('tool_tip.favorite')"
/>
- <span v-if="!hidePostStatsLocal && status.fave_num > 0">{{ status.fave_num }}</span>
+ <span v-if="!mergedConfig.hidePostStats && status.fave_num > 0">{{ status.fave_num }}</span>
</div>
</template>
diff --git a/src/components/follow_button/follow_button.js b/src/components/follow_button/follow_button.js
new file mode 100644
index 00000000..12da2645
--- /dev/null
+++ b/src/components/follow_button/follow_button.js
@@ -0,0 +1,53 @@
+import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
+export default {
+ props: ['user', 'labelFollowing', 'buttonClass'],
+ data () {
+ return {
+ inProgress: false
+ }
+ },
+ computed: {
+ isPressed () {
+ return this.inProgress || this.user.following
+ },
+ title () {
+ if (this.inProgress || this.user.following) {
+ return this.$t('user_card.follow_unfollow')
+ } else if (this.user.requested) {
+ return this.$t('user_card.follow_again')
+ } else {
+ return this.$t('user_card.follow')
+ }
+ },
+ label () {
+ if (this.inProgress) {
+ return this.$t('user_card.follow_progress')
+ } else if (this.user.following) {
+ return this.labelFollowing || this.$t('user_card.following')
+ } else if (this.user.requested) {
+ return this.$t('user_card.follow_sent')
+ } else {
+ return this.$t('user_card.follow')
+ }
+ }
+ },
+ methods: {
+ onClick () {
+ this.user.following ? this.unfollow() : this.follow()
+ },
+ follow () {
+ this.inProgress = true
+ requestFollow(this.user, this.$store).then(() => {
+ this.inProgress = false
+ })
+ },
+ unfollow () {
+ const store = this.$store
+ this.inProgress = true
+ requestUnfollow(this.user, store).then(() => {
+ this.inProgress = false
+ store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
+ })
+ }
+ }
+}
diff --git a/src/components/follow_button/follow_button.vue b/src/components/follow_button/follow_button.vue
new file mode 100644
index 00000000..f0cbb94b
--- /dev/null
+++ b/src/components/follow_button/follow_button.vue
@@ -0,0 +1,13 @@
+<template>
+ <button
+ class="btn btn-default follow-button"
+ :class="{ pressed: isPressed }"
+ :disabled="inProgress"
+ :title="title"
+ @click="onClick"
+ >
+ {{ label }}
+ </button>
+</template>
+
+<script src="./follow_button.js"></script>
diff --git a/src/components/follow_card/follow_card.js b/src/components/follow_card/follow_card.js
index dc4a0d41..aefd609e 100644
--- a/src/components/follow_card/follow_card.js
+++ b/src/components/follow_card/follow_card.js
@@ -1,21 +1,16 @@
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
-import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
+import FollowButton from '../follow_button/follow_button.vue'
const FollowCard = {
props: [
'user',
'noFollowsYou'
],
- data () {
- return {
- inProgress: false,
- requestSent: false
- }
- },
components: {
BasicUserCard,
- RemoteFollow
+ RemoteFollow,
+ FollowButton
},
computed: {
isMe () {
@@ -24,21 +19,6 @@ const FollowCard = {
loggedIn () {
return this.$store.state.users.currentUser
}
- },
- methods: {
- followUser () {
- this.inProgress = true
- requestFollow(this.user, this.$store).then(({ sent }) => {
- this.inProgress = false
- this.requestSent = sent
- })
- },
- unfollowUser () {
- this.inProgress = true
- requestUnfollow(this.user, this.$store).then(() => {
- this.inProgress = false
- })
- }
}
}
diff --git a/src/components/follow_card/follow_card.vue b/src/components/follow_card/follow_card.vue
index 310fe843..81e6e6dc 100644
--- a/src/components/follow_card/follow_card.vue
+++ b/src/components/follow_card/follow_card.vue
@@ -16,36 +16,11 @@
</div>
</template>
<template v-else>
- <button
- v-if="!user.following"
- class="btn btn-default follow-card-follow-button"
- :disabled="inProgress"
- :title="requestSent ? $t('user_card.follow_again') : ''"
- @click="followUser"
- >
- <template v-if="inProgress">
- {{ $t('user_card.follow_progress') }}
- </template>
- <template v-else-if="requestSent">
- {{ $t('user_card.follow_sent') }}
- </template>
- <template v-else>
- {{ $t('user_card.follow') }}
- </template>
- </button>
- <button
- v-else
- class="btn btn-default follow-card-follow-button pressed"
- :disabled="inProgress"
- @click="unfollowUser"
- >
- <template v-if="inProgress">
- {{ $t('user_card.follow_progress') }}
- </template>
- <template v-else>
- {{ $t('user_card.follow_unfollow') }}
- </template>
- </button>
+ <FollowButton
+ :user="user"
+ class="follow-card-follow-button"
+ :label-following="$t('user_card.follow_unfollow')"
+ />
</template>
</div>
</basic-user-card>
diff --git a/src/components/gallery/gallery.js b/src/components/gallery/gallery.js
index 7f33a81b..f856fd0a 100644
--- a/src/components/gallery/gallery.js
+++ b/src/components/gallery/gallery.js
@@ -1,23 +1,18 @@
import Attachment from '../attachment/attachment.vue'
-import { chunk, last, dropRight } from 'lodash'
+import { chunk, last, dropRight, sumBy } from 'lodash'
const Gallery = {
- data: () => ({
- width: 500
- }),
props: [
'attachments',
'nsfw',
'setMedia'
],
- components: { Attachment },
- mounted () {
- this.resize()
- window.addEventListener('resize', this.resize)
- },
- destroyed () {
- window.removeEventListener('resize', this.resize)
+ data () {
+ return {
+ sizes: {}
+ }
},
+ components: { Attachment },
computed: {
rows () {
if (!this.attachments) {
@@ -33,21 +28,24 @@ const Gallery = {
}
return rows
},
- rowHeight () {
- return itemsPerRow => ({ 'height': `${(this.width / (itemsPerRow + 0.6))}px` })
- },
useContainFit () {
- return this.$store.state.config.useContainFit
+ return this.$store.getters.mergedConfig.useContainFit
}
},
methods: {
- resize () {
- // Quick optimization to make resizing not always trigger state change,
- // only update attachment size in 10px steps
- const width = Math.floor(this.$el.getBoundingClientRect().width / 10) * 10
- if (this.width !== width) {
- this.width = width
- }
+ onNaturalSizeLoad (id, size) {
+ this.$set(this.sizes, id, size)
+ },
+ rowStyle (itemsPerRow) {
+ return { 'padding-bottom': `${(100 / (itemsPerRow + 0.6))}%` }
+ },
+ itemStyle (id, row) {
+ const total = sumBy(row, item => this.getAspectRatio(item.id))
+ return { flex: `${this.getAspectRatio(id) / total} 1 0%` }
+ },
+ getAspectRatio (id) {
+ const size = this.sizes[id]
+ return size ? size.width / size.height : 1
}
}
}
diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue
index 6169d294..7abc2161 100644
--- a/src/components/gallery/gallery.vue
+++ b/src/components/gallery/gallery.vue
@@ -7,17 +7,21 @@
v-for="(row, index) in rows"
:key="index"
class="gallery-row"
- :style="rowHeight(row.length)"
+ :style="rowStyle(row.length)"
:class="{ 'contain-fit': useContainFit, 'cover-fit': !useContainFit }"
>
- <attachment
- v-for="attachment in row"
- :key="attachment.id"
- :set-media="setMedia"
- :nsfw="nsfw"
- :attachment="attachment"
- :allow-play="false"
- />
+ <div class="gallery-row-inner">
+ <attachment
+ v-for="attachment in row"
+ :key="attachment.id"
+ :set-media="setMedia"
+ :nsfw="nsfw"
+ :attachment="attachment"
+ :allow-play="false"
+ :natural-size-load="onNaturalSizeLoad.bind(null, attachment.id)"
+ :style="itemStyle(attachment.id, row)"
+ />
+ </div>
</div>
</div>
</template>
@@ -28,15 +32,24 @@
@import '../../_variables.scss';
.gallery-row {
- height: 200px;
+ position: relative;
+ height: 0;
width: 100%;
- display: flex;
- flex-direction: row;
- flex-wrap: nowrap;
- align-content: stretch;
flex-grow: 1;
margin-top: 0.5em;
+ .gallery-row-inner {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ align-content: stretch;
+ }
+
// FIXME: specificity problem with this and .attachments.attachment
// we shouldn't have the need for .image here
.attachment.image {
diff --git a/src/components/interface_language_switcher/interface_language_switcher.vue b/src/components/interface_language_switcher/interface_language_switcher.vue
index 83df9a0b..1ca22001 100644
--- a/src/components/interface_language_switcher/interface_language_switcher.vue
+++ b/src/components/interface_language_switcher/interface_language_switcher.vue
@@ -40,7 +40,7 @@ export default {
},
language: {
- get: function () { return this.$store.state.config.interfaceLanguage },
+ get: function () { return this.$store.getters.mergedConfig.interfaceLanguage },
set: function (val) {
this.$store.dispatch('setOption', { name: 'interfaceLanguage', value: val })
this.$i18n.locale = val
diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js
index 10f52fe2..0b574a04 100644
--- a/src/components/login_form/login_form.js
+++ b/src/components/login_form/login_form.js
@@ -59,6 +59,8 @@ const LoginForm = {
if (result.error) {
if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result })
+ } else if (result.identifier === 'password_reset_required') {
+ this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else {
this.error = result.error
this.focusOnPasswordInput()
diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js
index 992d7129..abb18c7d 100644
--- a/src/components/media_modal/media_modal.js
+++ b/src/components/media_modal/media_modal.js
@@ -1,11 +1,14 @@
import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue'
+import Modal from '../modal/modal.vue'
import fileTypeService from '../../services/file_type/file_type.service.js'
+import GestureService from '../../services/gesture_service/gesture_service'
const MediaModal = {
components: {
StillImage,
- VideoAttachment
+ VideoAttachment,
+ Modal
},
computed: {
showing () {
@@ -27,7 +30,27 @@ const MediaModal = {
return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null
}
},
+ created () {
+ this.mediaSwipeGestureRight = GestureService.swipeGesture(
+ GestureService.DIRECTION_RIGHT,
+ this.goPrev,
+ 50
+ )
+ this.mediaSwipeGestureLeft = GestureService.swipeGesture(
+ GestureService.DIRECTION_LEFT,
+ this.goNext,
+ 50
+ )
+ },
methods: {
+ mediaTouchStart (e) {
+ GestureService.beginSwipe(e, this.mediaSwipeGestureRight)
+ GestureService.beginSwipe(e, this.mediaSwipeGestureLeft)
+ },
+ mediaTouchMove (e) {
+ GestureService.updateSwipe(e, this.mediaSwipeGestureRight)
+ GestureService.updateSwipe(e, this.mediaSwipeGestureLeft)
+ },
hide () {
this.$store.dispatch('closeMediaViewer')
},
diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue
index 06ced5a1..49e3143e 100644
--- a/src/components/media_modal/media_modal.vue
+++ b/src/components/media_modal/media_modal.vue
@@ -1,14 +1,15 @@
<template>
- <div
+ <Modal
v-if="showing"
- v-body-scroll-lock="showing"
- class="modal-view media-modal-view"
- @click.prevent="hide"
+ class="media-modal-view"
+ @backdropClicked="hide"
>
<img
v-if="type === 'image'"
class="modal-image"
:src="currentMedia.url"
+ @touchstart.stop="mediaTouchStart"
+ @touchmove.stop="mediaTouchMove"
>
<VideoAttachment
v-if="type === 'video'"
@@ -33,33 +34,25 @@
>
<i class="icon-right-open arrow-icon" />
</button>
- </div>
+ </Modal>
</template>
<script src="./media_modal.js"></script>
<style lang="scss">
-@import '../../_variables.scss';
-
-.media-modal-view {
+.modal-view.media-modal-view {
z-index: 1001;
- body:not(.scroll-locked) & {
- display: none;
- }
-
- &:hover {
- .modal-view-button-arrow {
- opacity: 0.75;
+ .modal-view-button-arrow {
+ opacity: 0.75;
- &:focus,
- &:hover {
- outline: none;
- box-shadow: none;
- }
- &:hover {
- opacity: 1;
- }
+ &:focus,
+ &:hover {
+ outline: none;
+ box-shadow: none;
+ }
+ &:hover {
+ opacity: 1;
}
}
}
@@ -114,5 +107,4 @@
}
}
}
-
</style>
diff --git a/src/components/mobile_nav/mobile_nav.js b/src/components/mobile_nav/mobile_nav.js
index c2bb76ee..5a90c31f 100644
--- a/src/components/mobile_nav/mobile_nav.js
+++ b/src/components/mobile_nav/mobile_nav.js
@@ -63,7 +63,7 @@ const MobileNav = {
this.$refs.notifications.markAsSeen()
},
onScroll ({ target: { scrollTop, clientHeight, scrollHeight } }) {
- if (this.$store.state.config.autoLoad && scrollTop + clientHeight >= scrollHeight) {
+ if (this.$store.getters.mergedConfig.autoLoad && scrollTop + clientHeight >= scrollHeight) {
this.$refs.notifications.fetchOlderNotifications()
}
}
diff --git a/src/components/mobile_post_status_button/mobile_post_status_button.js b/src/components/mobile_post_status_button/mobile_post_status_button.js
index 3e77148a..0ad12bb1 100644
--- a/src/components/mobile_post_status_button/mobile_post_status_button.js
+++ b/src/components/mobile_post_status_button/mobile_post_status_button.js
@@ -30,7 +30,7 @@ const MobilePostStatusButton = {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
},
autohideFloatingPostButton () {
- return !!this.$store.state.config.autohideFloatingPostButton
+ return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
}
},
watch: {
diff --git a/src/components/modal/modal.vue b/src/components/modal/modal.vue
new file mode 100644
index 00000000..cee24241
--- /dev/null
+++ b/src/components/modal/modal.vue
@@ -0,0 +1,52 @@
+<template>
+ <div
+ v-show="isOpen"
+ v-body-scroll-lock="isOpen"
+ class="modal-view"
+ @click.self="$emit('backdropClicked')"
+ >
+ <slot />
+ </div>
+</template>
+
+<script>
+export default {
+ props: {
+ isOpen: {
+ type: Boolean,
+ default: true
+ }
+ }
+}
+</script>
+
+<style lang="scss">
+.modal-view {
+ z-index: 1000;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ overflow: auto;
+ animation-duration: 0.2s;
+ background-color: rgba(0, 0, 0, 0.5);
+ animation-name: modal-background-fadein;
+
+ body:not(.scroll-locked) & {
+ opacity: 0;
+ }
+}
+
+@keyframes modal-background-fadein {
+ from {
+ background-color: rgba(0, 0, 0, 0);
+ }
+ to {
+ background-color: rgba(0, 0, 0, 0.5);
+ }
+}
+</style>
diff --git a/src/components/moderation_tools/moderation_tools.vue b/src/components/moderation_tools/moderation_tools.vue
index d97ca3aa..006d6373 100644
--- a/src/components/moderation_tools/moderation_tools.vue
+++ b/src/components/moderation_tools/moderation_tools.vue
@@ -3,9 +3,7 @@
<v-popover
trigger="click"
class="moderation-tools-popover"
- :container="false"
placement="bottom-end"
- :offset="5"
@show="showDropDown = true"
@hide="showDropDown = false"
>
diff --git a/src/components/notification/notification.js b/src/components/notification/notification.js
index 8e817f3b..7d46eb5a 100644
--- a/src/components/notification/notification.js
+++ b/src/components/notification/notification.js
@@ -39,7 +39,7 @@ const Notification = {
return highlightClass(this.notification.from_profile)
},
userStyle () {
- const highlight = this.$store.state.config.highlight
+ const highlight = this.$store.getters.mergedConfig.highlight
const user = this.notification.from_profile
return highlightStyle(highlight[user.screen_name])
},
diff --git a/src/components/password_reset/password_reset.js b/src/components/password_reset/password_reset.js
index fa71e07a..62e74e30 100644
--- a/src/components/password_reset/password_reset.js
+++ b/src/components/password_reset/password_reset.js
@@ -25,6 +25,12 @@ const passwordReset = {
this.$router.push({ name: 'root' })
}
},
+ props: {
+ passwordResetRequested: {
+ default: false,
+ type: Boolean
+ }
+ },
methods: {
dismissError () {
this.error = null
diff --git a/src/components/password_reset/password_reset.vue b/src/components/password_reset/password_reset.vue
index 00474e95..713c9dce 100644
--- a/src/components/password_reset/password_reset.vue
+++ b/src/components/password_reset/password_reset.vue
@@ -10,7 +10,10 @@
>
<div class="container">
<div v-if="!mailerEnabled">
- <p>
+ <p v-if="passwordResetRequested">
+ {{ $t('password_reset.password_reset_required_but_mailer_is_disabled') }}
+ </p>
+ <p v-else>
{{ $t('password_reset.password_reset_disabled') }}
</p>
</div>
@@ -25,6 +28,12 @@
</div>
</div>
<div v-else>
+ <p
+ v-if="passwordResetRequested"
+ class="password-reset-required error"
+ >
+ {{ $t('password_reset.password_reset_required') }}
+ </p>
<p>
{{ $t('password_reset.instruction') }}
</p>
@@ -104,6 +113,11 @@
margin: 0.3em 0.0em 1em;
}
+ .password-reset-required {
+ background-color: var(--alertError, $fallback--alertError);
+ padding: 10px 0;
+ }
+
.notice-dismissible {
padding-right: 2rem;
}
diff --git a/src/components/popper/popper.scss b/src/components/popper/popper.scss
index 279b01be..06daa871 100644
--- a/src/components/popper/popper.scss
+++ b/src/components/popper/popper.scss
@@ -20,7 +20,6 @@
margin: 5px;
border-color: $fallback--bg;
border-color: var(--bg, $fallback--bg);
- z-index: 1;
}
&[x-placement^="top"] {
@@ -31,7 +30,7 @@
border-left-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
- bottom: -5px;
+ bottom: -4px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
@@ -46,7 +45,7 @@
border-left-color: transparent !important;
border-right-color: transparent !important;
border-top-color: transparent !important;
- top: -5px;
+ top: -4px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
@@ -61,7 +60,7 @@
border-left-color: transparent !important;
border-top-color: transparent !important;
border-bottom-color: transparent !important;
- left: -5px;
+ left: -4px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
@@ -76,7 +75,7 @@
border-top-color: transparent !important;
border-right-color: transparent !important;
border-bottom-color: transparent !important;
- right: -5px;
+ right: -4px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js
index 483c395e..af6299e4 100644
--- a/src/components/post_status_form/post_status_form.js
+++ b/src/components/post_status_form/post_status_form.js
@@ -7,6 +7,8 @@ import fileTypeService from '../../services/file_type/file_type.service.js'
import { findOffset } from '../../services/offset_finder/offset_finder.service.js'
import { reject, map, uniqBy } from 'lodash'
import suggestor from '../emoji_input/suggestor.js'
+import { mapGetters } from 'vuex'
+import Checkbox from '../checkbox/checkbox.vue'
const buildMentionsString = ({ user, attentions = [] }, currentUser) => {
let allAttentions = [...attentions]
@@ -35,7 +37,8 @@ const PostStatusForm = {
MediaUpload,
EmojiInput,
PollForm,
- ScopeSelector
+ ScopeSelector,
+ Checkbox
},
mounted () {
this.resize(this.$refs.textarea)
@@ -50,9 +53,7 @@ const PostStatusForm = {
const preset = this.$route.query.message
let statusText = preset || ''
- const scopeCopy = typeof this.$store.state.config.scopeCopy === 'undefined'
- ? this.$store.state.instance.scopeCopy
- : this.$store.state.config.scopeCopy
+ const { scopeCopy } = this.$store.getters.mergedConfig
if (this.replyTo) {
const currentUser = this.$store.state.users.currentUser
@@ -63,9 +64,7 @@ const PostStatusForm = {
? this.copyMessageScope
: this.$store.state.users.currentUser.default_scope
- const contentType = typeof this.$store.state.config.postContentType === 'undefined'
- ? this.$store.state.instance.postContentType
- : this.$store.state.config.postContentType
+ const { postContentType: contentType } = this.$store.getters.mergedConfig
return {
dropFiles: [],
@@ -94,10 +93,7 @@ const PostStatusForm = {
return this.$store.state.users.currentUser.default_scope
},
showAllScopes () {
- const minimalScopesMode = typeof this.$store.state.config.minimalScopesMode === 'undefined'
- ? this.$store.state.instance.minimalScopesMode
- : this.$store.state.config.minimalScopesMode
- return !minimalScopesMode
+ return !this.mergedConfig.minimalScopesMode
},
emojiUserSuggestor () {
return suggestor({
@@ -145,13 +141,7 @@ const PostStatusForm = {
return this.$store.state.instance.minimalScopesMode
},
alwaysShowSubject () {
- if (typeof this.$store.state.config.alwaysShowSubjectInput !== 'undefined') {
- return this.$store.state.config.alwaysShowSubjectInput
- } else if (typeof this.$store.state.instance.alwaysShowSubjectInput !== 'undefined') {
- return this.$store.state.instance.alwaysShowSubjectInput
- } else {
- return true
- }
+ return this.mergedConfig.alwaysShowSubjectInput
},
postFormats () {
return this.$store.state.instance.postFormats || []
@@ -164,13 +154,14 @@ const PostStatusForm = {
this.$store.state.instance.pollLimits.max_options >= 2
},
hideScopeNotice () {
- return this.$store.state.config.hideScopeNotice
+ return this.$store.getters.mergedConfig.hideScopeNotice
},
pollContentError () {
return this.pollFormVisible &&
this.newStatus.poll &&
this.newStatus.poll.error
- }
+ },
+ ...mapGetters(['mergedConfig'])
},
methods: {
postStatus (newStatus) {
diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue
index 60dcf90c..13e8b0aa 100644
--- a/src/components/post_status_form/post_status_form.vue
+++ b/src/components/post_status_form/post_status_form.vue
@@ -264,12 +264,9 @@
v-if="newStatus.files.length > 0"
class="upload_settings"
>
- <input
- id="filesSensitive"
- v-model="newStatus.nsfw"
- type="checkbox"
- >
- <label for="filesSensitive">{{ $t('post_status.attachments_sensitive') }}</label>
+ <Checkbox v-model="newStatus.nsfw">
+ {{ $t('post_status.attachments_sensitive') }}
+ </Checkbox>
</div>
</form>
</div>
diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js
index 38258296..b44354db 100644
--- a/src/components/post_status_modal/post_status_modal.js
+++ b/src/components/post_status_modal/post_status_modal.js
@@ -1,9 +1,11 @@
import PostStatusForm from '../post_status_form/post_status_form.vue'
+import Modal from '../modal/modal.vue'
import get from 'lodash/get'
const PostStatusModal = {
components: {
- PostStatusForm
+ PostStatusForm,
+ Modal
},
data () {
return {
diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue
index d3a82389..dbcd321e 100644
--- a/src/components/post_status_modal/post_status_modal.vue
+++ b/src/components/post_status_modal/post_status_modal.vue
@@ -1,14 +1,11 @@
<template>
- <div
+ <Modal
v-if="isLoggedIn && !resettingForm"
- v-show="modalActivated"
- class="post-form-modal-view modal-view"
- @click="closeModal"
+ :is-open="modalActivated"
+ class="post-form-modal-view"
+ @backdropClicked="closeModal"
>
- <div
- class="post-form-modal-panel panel"
- @click.stop=""
- >
+ <div class="post-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.new_status') }}
</div>
@@ -18,15 +15,13 @@
@posted="closeModal"
/>
</div>
- </div>
+ </Modal>
</template>
<script src="./post_status_modal.js"></script>
<style lang="scss">
-@import '../../_variables.scss';
-
-.post-form-modal-view {
+.modal-view.post-form-modal-view {
align-items: flex-start;
}
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
index fb543a9c..d9a0f92e 100644
--- a/src/components/retweet_button/retweet_button.js
+++ b/src/components/retweet_button/retweet_button.js
@@ -1,10 +1,9 @@
+import { mapGetters } from 'vuex'
+
const RetweetButton = {
props: ['status', 'loggedIn', 'visibility'],
data () {
return {
- hidePostStatsLocal: typeof this.$store.state.config.hidePostStats === 'undefined'
- ? this.$store.state.instance.hidePostStats
- : this.$store.state.config.hidePostStats,
animated: false
}
},
@@ -28,7 +27,8 @@ const RetweetButton = {
'retweeted-empty': !this.status.repeated,
'animate-spin': this.animated
}
- }
+ },
+ ...mapGetters(['mergedConfig'])
}
}
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
index d58a7f8c..074f7747 100644
--- a/src/components/retweet_button/retweet_button.vue
+++ b/src/components/retweet_button/retweet_button.vue
@@ -7,7 +7,7 @@
:title="$t('tool_tip.repeat')"
@click.prevent="retweet()"
/>
- <span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span>
+ <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</template>
<template v-else>
<i
@@ -23,7 +23,7 @@
class="button-icon icon-retweet"
:title="$t('tool_tip.repeat')"
/>
- <span v-if="!hidePostStatsLocal && status.repeat_num > 0">{{ status.repeat_num }}</span>
+ <span v-if="!mergedConfig.hidePostStats && status.repeat_num > 0">{{ status.repeat_num }}</span>
</div>
</template>
diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js
index b6540d7e..c49083f9 100644
--- a/src/components/settings/settings.js
+++ b/src/components/settings/settings.js
@@ -5,88 +5,22 @@ import TabSwitcher from '../tab_switcher/tab_switcher.js'
import StyleSwitcher from '../style_switcher/style_switcher.vue'
import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue'
import { extractCommit } from '../../services/version/version.service'
+import { instanceDefaultProperties, defaultState as configDefaultState } from '../../modules/config.js'
+import Checkbox from '../checkbox/checkbox.vue'
const pleromaFeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma-fe/commit/'
const pleromaBeCommitUrl = 'https://git.pleroma.social/pleroma/pleroma/commit/'
+const multiChoiceProperties = [
+ 'postContentType',
+ 'subjectLineBehavior'
+]
+
const settings = {
data () {
- const user = this.$store.state.config
const instance = this.$store.state.instance
return {
- hideAttachmentsLocal: user.hideAttachments,
- padEmojiLocal: user.padEmoji,
- hideAttachmentsInConvLocal: user.hideAttachmentsInConv,
- maxThumbnails: user.maxThumbnails,
- hideNsfwLocal: user.hideNsfw,
- useOneClickNsfw: user.useOneClickNsfw,
- hideISPLocal: user.hideISP,
- preloadImage: user.preloadImage,
-
- hidePostStatsLocal: typeof user.hidePostStats === 'undefined'
- ? instance.hidePostStats
- : user.hidePostStats,
- hidePostStatsDefault: this.$t('settings.values.' + instance.hidePostStats),
-
- hideUserStatsLocal: typeof user.hideUserStats === 'undefined'
- ? instance.hideUserStats
- : user.hideUserStats,
- hideUserStatsDefault: this.$t('settings.values.' + instance.hideUserStats),
-
- hideFilteredStatusesLocal: typeof user.hideFilteredStatuses === 'undefined'
- ? instance.hideFilteredStatuses
- : user.hideFilteredStatuses,
- hideFilteredStatusesDefault: this.$t('settings.values.' + instance.hideFilteredStatuses),
-
- notificationVisibilityLocal: user.notificationVisibility,
- replyVisibilityLocal: user.replyVisibility,
- loopVideoLocal: user.loopVideo,
- muteWordsString: user.muteWords.join('\n'),
- autoLoadLocal: user.autoLoad,
- streamingLocal: user.streaming,
- pauseOnUnfocusedLocal: user.pauseOnUnfocused,
- hoverPreviewLocal: user.hoverPreview,
- autohideFloatingPostButtonLocal: user.autohideFloatingPostButton,
-
- hideMutedPostsLocal: typeof user.hideMutedPosts === 'undefined'
- ? instance.hideMutedPosts
- : user.hideMutedPosts,
- hideMutedPostsDefault: this.$t('settings.values.' + instance.hideMutedPosts),
-
- collapseMessageWithSubjectLocal: typeof user.collapseMessageWithSubject === 'undefined'
- ? instance.collapseMessageWithSubject
- : user.collapseMessageWithSubject,
- collapseMessageWithSubjectDefault: this.$t('settings.values.' + instance.collapseMessageWithSubject),
-
- subjectLineBehaviorLocal: typeof user.subjectLineBehavior === 'undefined'
- ? instance.subjectLineBehavior
- : user.subjectLineBehavior,
- subjectLineBehaviorDefault: instance.subjectLineBehavior,
-
- postContentTypeLocal: typeof user.postContentType === 'undefined'
- ? instance.postContentType
- : user.postContentType,
- postContentTypeDefault: instance.postContentType,
-
- alwaysShowSubjectInputLocal: typeof user.alwaysShowSubjectInput === 'undefined'
- ? instance.alwaysShowSubjectInput
- : user.alwaysShowSubjectInput,
- alwaysShowSubjectInputDefault: this.$t('settings.values.' + instance.alwaysShowSubjectInput),
-
- scopeCopyLocal: typeof user.scopeCopy === 'undefined'
- ? instance.scopeCopy
- : user.scopeCopy,
- scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy),
-
- minimalScopesModeLocal: typeof user.minimalScopesMode === 'undefined'
- ? instance.minimalScopesMode
- : user.minimalScopesMode,
- minimalScopesModeDefault: this.$t('settings.values.' + instance.minimalScopesMode),
-
- stopGifs: user.stopGifs,
- webPushNotificationsLocal: user.webPushNotifications,
- loopVideoSilentOnlyLocal: user.loopVideosSilentOnly,
loopSilentAvailable:
// Firefox
Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') ||
@@ -94,8 +28,6 @@ const settings = {
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') ||
// Future spec, still not supported in Nightly 63 as of 08/2018
Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'),
- playVideosInModal: user.playVideosInModal,
- useContainFit: user.useContainFit,
backendVersion: instance.backendVersion,
frontendVersion: instance.frontendVersion
@@ -104,7 +36,8 @@ const settings = {
components: {
TabSwitcher,
StyleSwitcher,
- InterfaceLanguageSwitcher
+ InterfaceLanguageSwitcher,
+ Checkbox
},
computed: {
user () {
@@ -122,116 +55,56 @@ const settings = {
},
backendVersionLink () {
return pleromaBeCommitUrl + extractCommit(this.backendVersion)
+ },
+ // Getting localized values for instance-default properties
+ ...instanceDefaultProperties
+ .filter(key => multiChoiceProperties.includes(key))
+ .map(key => [
+ key + 'DefaultValue',
+ function () {
+ return this.$store.getters.instanceDefaultConfig[key]
+ }
+ ])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+ ...instanceDefaultProperties
+ .filter(key => !multiChoiceProperties.includes(key))
+ .map(key => [
+ key + 'LocalizedValue',
+ function () {
+ return this.$t('settings.values.' + this.$store.getters.instanceDefaultConfig[key])
+ }
+ ])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+ // Generating computed values for vuex properties
+ ...Object.keys(configDefaultState)
+ .map(key => [key, {
+ get () { return this.$store.getters.mergedConfig[key] },
+ set (value) {
+ this.$store.dispatch('setOption', { name: key, value })
+ }
+ }])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
+ // Special cases (need to transform values)
+ muteWordsString: {
+ get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
+ set (value) {
+ this.$store.dispatch('setOption', {
+ name: 'muteWords',
+ value: filter(value.split('\n'), (word) => trim(word).length > 0)
+ })
+ }
}
},
+ // Updating nested properties
watch: {
- hideAttachmentsLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideAttachments', value })
- },
- padEmojiLocal (value) {
- this.$store.dispatch('setOption', { name: 'padEmoji', value })
- },
- hideAttachmentsInConvLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideAttachmentsInConv', value })
- },
- hidePostStatsLocal (value) {
- this.$store.dispatch('setOption', { name: 'hidePostStats', value })
- },
- hideUserStatsLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideUserStats', value })
- },
- hideFilteredStatusesLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideFilteredStatuses', value })
- },
- hideNsfwLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideNsfw', value })
- },
- useOneClickNsfw (value) {
- this.$store.dispatch('setOption', { name: 'useOneClickNsfw', value })
- },
- preloadImage (value) {
- this.$store.dispatch('setOption', { name: 'preloadImage', value })
- },
- hideISPLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideISP', 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 })
- },
- autohideFloatingPostButtonLocal (value) {
- this.$store.dispatch('setOption', { name: 'autohideFloatingPostButton', value })
- },
- muteWordsString (value) {
- value = filter(value.split('\n'), (word) => trim(word).length > 0)
- this.$store.dispatch('setOption', { name: 'muteWords', value })
- },
- hideMutedPostsLocal (value) {
- this.$store.dispatch('setOption', { name: 'hideMutedPosts', value })
- },
- collapseMessageWithSubjectLocal (value) {
- this.$store.dispatch('setOption', { name: 'collapseMessageWithSubject', value })
- },
- scopeCopyLocal (value) {
- this.$store.dispatch('setOption', { name: 'scopeCopy', value })
- },
- alwaysShowSubjectInputLocal (value) {
- this.$store.dispatch('setOption', { name: 'alwaysShowSubjectInput', value })
- },
- subjectLineBehaviorLocal (value) {
- this.$store.dispatch('setOption', { name: 'subjectLineBehavior', value })
- },
- postContentTypeLocal (value) {
- this.$store.dispatch('setOption', { name: 'postContentType', value })
- },
- minimalScopesModeLocal (value) {
- this.$store.dispatch('setOption', { name: 'minimalScopesMode', value })
- },
- stopGifs (value) {
- this.$store.dispatch('setOption', { name: 'stopGifs', value })
- },
- webPushNotificationsLocal (value) {
- this.$store.dispatch('setOption', { name: 'webPushNotifications', value })
- if (value) this.$store.dispatch('registerPushNotifications')
- },
- playVideosInModal (value) {
- this.$store.dispatch('setOption', { name: 'playVideosInModal', value })
- },
- useContainFit (value) {
- this.$store.dispatch('setOption', { name: 'useContainFit', value })
- },
- maxThumbnails (value) {
- value = this.maxThumbnails = Math.floor(Math.max(value, 0))
- this.$store.dispatch('setOption', { name: 'maxThumbnails', value })
+ notificationVisibility: {
+ handler (value) {
+ this.$store.dispatch('setOption', {
+ name: 'notificationVisibility',
+ value: this.$store.getters.mergedConfig.notificationVisibility
+ })
+ },
+ deep: true
}
}
}
diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue
index 6d87a060..a83489d2 100644
--- a/src/components/settings/settings.vue
+++ b/src/components/settings/settings.vue
@@ -36,12 +36,9 @@
<interface-language-switcher />
</li>
<li v-if="instanceSpecificPanelPresent">
- <input
- id="hideISP"
- v-model="hideISPLocal"
- type="checkbox"
- >
- <label for="hideISP">{{ $t('settings.hide_isp') }}</label>
+ <Checkbox v-model="hideISP">
+ {{ $t('settings.hide_isp') }}
+ </Checkbox>
</li>
</ul>
</div>
@@ -49,58 +46,42 @@
<h2>{{ $t('nav.timeline') }}</h2>
<ul class="setting-list">
<li>
- <input
- id="hideMutedPosts"
- v-model="hideMutedPostsLocal"
- type="checkbox"
- >
- <label for="hideMutedPosts">{{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsDefault }) }}</label>
+ <Checkbox v-model="hideMutedPosts">
+ {{ $t('settings.hide_muted_posts') }} {{ $t('settings.instance_default', { value: hideMutedPostsLocalizedValue }) }}
+ </Checkbox>
</li>
<li>
- <input
- id="collapseMessageWithSubject"
- v-model="collapseMessageWithSubjectLocal"
- type="checkbox"
- >
- <label for="collapseMessageWithSubject">{{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectDefault }) }}</label>
+ <Checkbox v-model="collapseMessageWithSubject">
+ {{ $t('settings.collapse_subject') }} {{ $t('settings.instance_default', { value: collapseMessageWithSubjectLocalizedValue }) }}
+ </Checkbox>
</li>
<li>
- <input
- id="streaming"
- v-model="streamingLocal"
- type="checkbox"
- >
- <label for="streaming">{{ $t('settings.streaming') }}</label>
+ <Checkbox v-model="streaming">
+ {{ $t('settings.streaming') }}
+ </Checkbox>
<ul
class="setting-list suboptions"
- :class="[{disabled: !streamingLocal}]"
+ :class="[{disabled: !streaming}]"
>
<li>
- <input
- id="pauseOnUnfocused"
- v-model="pauseOnUnfocusedLocal"
- :disabled="!streamingLocal"
- type="checkbox"
+ <Checkbox
+ v-model="pauseOnUnfocused"
+ :disabled="!streaming"
>
- <label for="pauseOnUnfocused">{{ $t('settings.pause_on_unfocused') }}</label>
+ {{ $t('settings.pause_on_unfocused') }}
+ </Checkbox>
</li>
</ul>
</li>
<li>
- <input
- id="autoload"
- v-model="autoLoadLocal"
- type="checkbox"
- >
- <label for="autoload">{{ $t('settings.autoload') }}</label>
+ <Checkbox v-model="autoLoad">
+ {{ $t('settings.autoload') }}
+ </Checkbox>
</li>
<li>
- <input
- id="hoverPreview"
- v-model="hoverPreviewLocal"
- type="checkbox"
- >
- <label for="hoverPreview">{{ $t('settings.reply_link_preview') }}</label>
+ <Checkbox v-model="hoverPreview">
+ {{ $t('settings.reply_link_preview') }}
+ </Checkbox>
</li>
</ul>
</div>
@@ -109,24 +90,14 @@
<h2>{{ $t('settings.composing') }}</h2>
<ul class="setting-list">
<li>
- <input
- id="scopeCopy"
- v-model="scopeCopyLocal"
- type="checkbox"
- >
- <label for="scopeCopy">
- {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyDefault }) }}
- </label>
+ <Checkbox v-model="scopeCopy">
+ {{ $t('settings.scope_copy') }} {{ $t('settings.instance_default', { value: scopeCopyLocalizedValue }) }}
+ </Checkbox>
</li>
<li>
- <input
- id="subjectHide"
- v-model="alwaysShowSubjectInputLocal"
- type="checkbox"
- >
- <label for="subjectHide">
- {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputDefault }) }}
- </label>
+ <Checkbox v-model="alwaysShowSubjectInput">
+ {{ $t('settings.subject_input_always_show') }} {{ $t('settings.instance_default', { value: alwaysShowSubjectInputLocalizedValue }) }}
+ </Checkbox>
</li>
<li>
<div>
@@ -137,19 +108,19 @@
>
<select
id="subjectLineBehavior"
- v-model="subjectLineBehaviorLocal"
+ v-model="subjectLineBehavior"
>
<option value="email">
{{ $t('settings.subject_line_email') }}
- {{ subjectLineBehaviorDefault == 'email' ? $t('settings.instance_default_simple') : '' }}
+ {{ subjectLineBehaviorDefaultValue == 'email' ? $t('settings.instance_default_simple') : '' }}
</option>
<option value="masto">
{{ $t('settings.subject_line_mastodon') }}
- {{ subjectLineBehaviorDefault == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
+ {{ subjectLineBehaviorDefaultValue == 'mastodon' ? $t('settings.instance_default_simple') : '' }}
</option>
<option value="noop">
{{ $t('settings.subject_line_noop') }}
- {{ subjectLineBehaviorDefault == 'noop' ? $t('settings.instance_default_simple') : '' }}
+ {{ subjectLineBehaviorDefaultValue == 'noop' ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
<i class="icon-down-open" />
@@ -165,7 +136,7 @@
>
<select
id="postContentType"
- v-model="postContentTypeLocal"
+ v-model="postContentType"
>
<option
v-for="postFormat in postFormats"
@@ -173,7 +144,7 @@
:value="postFormat"
>
{{ $t(`post_status.content_type["${postFormat}"]`) }}
- {{ postContentTypeDefault === postFormat ? $t('settings.instance_default_simple') : '' }}
+ {{ postContentTypeDefaultValue === postFormat ? $t('settings.instance_default_simple') : '' }}
</option>
</select>
<i class="icon-down-open" />
@@ -181,30 +152,19 @@
</div>
</li>
<li>
- <input
- id="minimalScopesMode"
- v-model="minimalScopesModeLocal"
- type="checkbox"
- >
- <label for="minimalScopesMode">
- {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeDefault }) }}
- </label>
+ <Checkbox v-model="minimalScopesMode">
+ {{ $t('settings.minimal_scopes_mode') }} {{ $t('settings.instance_default', { value: minimalScopesModeLocalizedValue }) }}
+ </Checkbox>
</li>
<li>
- <input
- id="autohideFloatingPostButton"
- v-model="autohideFloatingPostButtonLocal"
- type="checkbox"
- >
- <label for="autohideFloatingPostButton">{{ $t('settings.autohide_floating_post_button') }}</label>
+ <Checkbox v-model="autohideFloatingPostButton">
+ {{ $t('settings.autohide_floating_post_button') }}
+ </Checkbox>
</li>
<li>
- <input
- id="padEmoji"
- v-model="padEmojiLocal"
- type="checkbox"
- >
- <label for="padEmoji">{{ $t('settings.pad_emoji') }}</label>
+ <Checkbox v-model="padEmoji">
+ {{ $t('settings.pad_emoji') }}
+ </Checkbox>
</li>
</ul>
</div>
@@ -213,23 +173,19 @@
<h2>{{ $t('settings.attachments') }}</h2>
<ul class="setting-list">
<li>
- <input
- id="hideAttachments"
- v-model="hideAttachmentsLocal"
- type="checkbox"
- >
- <label for="hideAttachments">{{ $t('settings.hide_attachments_in_tl') }}</label>
+ <Checkbox v-model="hideAttachments">
+ {{ $t('settings.hide_attachments_in_tl') }}
+ </Checkbox>
</li>
<li>
- <input
- id="hideAttachmentsInConv"
- v-model="hideAttachmentsInConvLocal"
- type="checkbox"
- >
- <label for="hideAttachmentsInConv">{{ $t('settings.hide_attachments_in_convo') }}</label>
+ <Checkbox v-model="hideAttachmentsInConv">
+ {{ $t('settings.hide_attachments_in_convo') }}
+ </Checkbox>
</li>
<li>
- <label for="maxThumbnails">{{ $t('settings.max_thumbnails') }}</label>
+ <label for="maxThumbnails">
+ {{ $t('settings.max_thumbnails') }}
+ </label>
<input
id="maxThumbnails"
v-model.number="maxThumbnails"
@@ -240,60 +196,48 @@
>
</li>
<li>
- <input
- id="hideNsfw"
- v-model="hideNsfwLocal"
- type="checkbox"
- >
- <label for="hideNsfw">{{ $t('settings.nsfw_clickthrough') }}</label>
+ <Checkbox v-model="hideNsfw">
+ {{ $t('settings.nsfw_clickthrough') }}
+ </Checkbox>
</li>
<ul class="setting-list suboptions">
<li>
- <input
- id="preloadImage"
+ <Checkbox
v-model="preloadImage"
- :disabled="!hideNsfwLocal"
- type="checkbox"
+ :disabled="!hideNsfw"
>
- <label for="preloadImage">{{ $t('settings.preload_images') }}</label>
+ {{ $t('settings.preload_images') }}
+ </Checkbox>
</li>
<li>
- <input
- id="useOneClickNsfw"
+ <Checkbox
v-model="useOneClickNsfw"
- :disabled="!hideNsfwLocal"
- type="checkbox"
+ :disabled="!hideNsfw"
>
- <label for="useOneClickNsfw">{{ $t('settings.use_one_click_nsfw') }}</label>
+ {{ $t('settings.use_one_click_nsfw') }}
+ </Checkbox>
</li>
</ul>
<li>
- <input
- id="stopGifs"
- v-model="stopGifs"
- type="checkbox"
- >
- <label for="stopGifs">{{ $t('settings.stop_gifs') }}</label>
+ <Checkbox v-model="stopGifs">
+ {{ $t('settings.stop_gifs') }}
+ </Checkbox>
</li>
<li>
- <input
- id="loopVideo"
- v-model="loopVideoLocal"
- type="checkbox"
- >
- <label for="loopVideo">{{ $t('settings.loop_video') }}</label>
+ <Checkbox v-model="loopVideo">
+ {{ $t('settings.loop_video') }}
+ </Checkbox>
<ul
class="setting-list suboptions"
- :class="[{disabled: !streamingLocal}]"
+ :class="[{disabled: !streaming}]"
>
<li>
- <input
- id="loopVideoSilentOnly"
- v-model="loopVideoSilentOnlyLocal"
- :disabled="!loopVideoLocal || !loopSilentAvailable"
- type="checkbox"
+ <Checkbox
+ v-model="loopVideoSilentOnly"
+ :disabled="!loopVideo || !loopSilentAvailable"
>
- <label for="loopVideoSilentOnly">{{ $t('settings.loop_video_silent_only') }}</label>
+ {{ $t('settings.loop_video_silent_only') }}
+ </Checkbox>
<div
v-if="!loopSilentAvailable"
class="unavailable"
@@ -304,20 +248,14 @@
</ul>
</li>
<li>
- <input
- id="playVideosInModal"
- v-model="playVideosInModal"
- type="checkbox"
- >
- <label for="playVideosInModal">{{ $t('settings.play_videos_in_modal') }}</label>
+ <Checkbox v-model="playVideosInModal">
+ {{ $t('settings.play_videos_in_modal') }}
+ </Checkbox>
</li>
<li>
- <input
- id="useContainFit"
- v-model="useContainFit"
- type="checkbox"
- >
- <label for="useContainFit">{{ $t('settings.use_contain_fit') }}</label>
+ <Checkbox v-model="useContainFit">
+ {{ $t('settings.use_contain_fit') }}
+ </Checkbox>
</li>
</ul>
</div>
@@ -326,14 +264,9 @@
<h2>{{ $t('settings.notifications') }}</h2>
<ul class="setting-list">
<li>
- <input
- id="webPushNotifications"
- v-model="webPushNotificationsLocal"
- type="checkbox"
- >
- <label for="webPushNotifications">
+ <Checkbox v-model="webPushNotifications">
{{ $t('settings.enable_web_push_notifications') }}
- </label>
+ </Checkbox>
</li>
</ul>
</div>
@@ -351,44 +284,24 @@
<span class="label">{{ $t('settings.notification_visibility') }}</span>
<ul class="option-list">
<li>
- <input
- id="notification-visibility-likes"
- v-model="notificationVisibilityLocal.likes"
- type="checkbox"
- >
- <label for="notification-visibility-likes">
+ <Checkbox v-model="notificationVisibility.likes">
{{ $t('settings.notification_visibility_likes') }}
- </label>
+ </Checkbox>
</li>
<li>
- <input
- id="notification-visibility-repeats"
- v-model="notificationVisibilityLocal.repeats"
- type="checkbox"
- >
- <label for="notification-visibility-repeats">
+ <Checkbox v-model="notificationVisibility.repeats">
{{ $t('settings.notification_visibility_repeats') }}
- </label>
+ </Checkbox>
</li>
<li>
- <input
- id="notification-visibility-follows"
- v-model="notificationVisibilityLocal.follows"
- type="checkbox"
- >
- <label for="notification-visibility-follows">
+ <Checkbox v-model="notificationVisibility.follows">
{{ $t('settings.notification_visibility_follows') }}
- </label>
+ </Checkbox>
</li>
<li>
- <input
- id="notification-visibility-mentions"
- v-model="notificationVisibilityLocal.mentions"
- type="checkbox"
- >
- <label for="notification-visibility-mentions">
+ <Checkbox v-model="notificationVisibility.mentions">
{{ $t('settings.notification_visibility_mentions') }}
- </label>
+ </Checkbox>
</li>
</ul>
</div>
@@ -400,7 +313,7 @@
>
<select
id="replyVisibility"
- v-model="replyVisibilityLocal"
+ v-model="replyVisibility"
>
<option
value="all"
@@ -413,24 +326,14 @@
</label>
</div>
<div>
- <input
- id="hidePostStats"
- v-model="hidePostStatsLocal"
- type="checkbox"
- >
- <label for="hidePostStats">
- {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsDefault }) }}
- </label>
+ <Checkbox v-model="hidePostStats">
+ {{ $t('settings.hide_post_stats') }} {{ $t('settings.instance_default', { value: hidePostStatsLocalizedValue }) }}
+ </Checkbox>
</div>
<div>
- <input
- id="hideUserStats"
- v-model="hideUserStatsLocal"
- type="checkbox"
- >
- <label for="hideUserStats">
- {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsDefault }) }}
- </label>
+ <Checkbox v-model="hideUserStats">
+ {{ $t('settings.hide_user_stats') }} {{ $t('settings.instance_default', { value: hideUserStatsLocalizedValue }) }}
+ </Checkbox>
</div>
</div>
<div class="setting-item">
@@ -442,14 +345,9 @@
/>
</div>
<div>
- <input
- id="hideFilteredStatuses"
- v-model="hideFilteredStatusesLocal"
- type="checkbox"
- >
- <label for="hideFilteredStatuses">
- {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesDefault }) }}
- </label>
+ <Checkbox v-model="hideFilteredStatuses">
+ {{ $t('settings.hide_filtered_statuses') }} {{ $t('settings.instance_default', { value: hideFilteredStatusesLocalizedValue }) }}
+ </Checkbox>
</div>
</div>
</div>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index d17ba318..4fbd5ac3 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -10,11 +10,13 @@ import Gallery from '../gallery/gallery.vue'
import LinkPreview from '../link-preview/link-preview.vue'
import AvatarList from '../avatar_list/avatar_list.vue'
import Timeago from '../timeago/timeago.vue'
+import StatusPopover from '../status_popover/status_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
-import { filter, find, unescape, uniqBy } from 'lodash'
+import { filter, unescape, uniqBy } from 'lodash'
+import { mapGetters } from 'vuex'
const Status = {
name: 'Status',
@@ -37,25 +39,19 @@ const Status = {
replying: false,
unmuted: false,
userExpanded: false,
- preview: null,
- showPreview: false,
showingTall: this.inConversation && this.focused,
showingLongSubject: false,
error: null,
- expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
- ? !this.$store.state.instance.collapseMessageWithSubject
- : !this.$store.state.config.collapseMessageWithSubject,
+ expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
computed: {
localCollapseSubjectDefault () {
- return typeof this.$store.state.config.collapseMessageWithSubject === 'undefined'
- ? this.$store.state.instance.collapseMessageWithSubject
- : this.$store.state.config.collapseMessageWithSubject
+ return this.mergedConfig.collapseMessageWithSubject
},
muteWords () {
- return this.$store.state.config.muteWords
+ return this.mergedConfig.muteWords
},
repeaterClass () {
const user = this.statusoid.user
@@ -70,18 +66,18 @@ const Status = {
},
repeaterStyle () {
const user = this.statusoid.user
- const highlight = this.$store.state.config.highlight
+ const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
userStyle () {
if (this.noHeading) return
const user = this.retweet ? (this.statusoid.retweeted_status.user) : this.statusoid.user
- const highlight = this.$store.state.config.highlight
+ const highlight = this.mergedConfig.highlight
return highlightStyle(highlight[user.screen_name])
},
hideAttachments () {
- return (this.$store.state.config.hideAttachments && !this.inConversation) ||
- (this.$store.state.config.hideAttachmentsInConv && this.inConversation)
+ return (this.mergedConfig.hideAttachments && !this.inConversation) ||
+ (this.mergedConfig.hideAttachmentsInConv && this.inConversation)
},
userProfileLink () {
return this.generateUserProfileLink(this.status.user.id, this.status.user.screen_name)
@@ -120,9 +116,7 @@ const Status = {
},
muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
hideFilteredStatuses () {
- return typeof this.$store.state.config.hideFilteredStatuses === 'undefined'
- ? this.$store.state.instance.hideFilteredStatuses
- : this.$store.state.config.hideFilteredStatuses
+ return this.mergedConfig.hideFilteredStatuses
},
hideStatus () {
return (this.hideReply || this.deleted) || (this.muted && this.hideFilteredStatuses)
@@ -163,7 +157,7 @@ const Status = {
}
},
hideReply () {
- if (this.$store.state.config.replyVisibility === 'all') {
+ if (this.mergedConfig.replyVisibility === 'all') {
return false
}
if (this.inConversation || !this.isReply) {
@@ -175,7 +169,7 @@ const Status = {
if (this.status.type === 'retweet') {
return false
}
- const checkFollowing = this.$store.state.config.replyVisibility === 'following'
+ const checkFollowing = this.mergedConfig.replyVisibility === 'following'
for (var i = 0; i < this.status.attentions.length; ++i) {
if (this.status.user.id === this.status.attentions[i].id) {
continue
@@ -220,9 +214,7 @@ const Status = {
replySubject () {
if (!this.status.summary) return ''
const decodedSummary = unescape(this.status.summary)
- const behavior = typeof this.$store.state.config.subjectLineBehavior === 'undefined'
- ? this.$store.state.instance.subjectLineBehavior
- : this.$store.state.config.subjectLineBehavior
+ const behavior = this.mergedConfig.subjectLineBehavior
const startsWithRe = decodedSummary.match(/^re[: ]/i)
if ((behavior !== 'noop' && startsWithRe) || behavior === 'masto') {
return decodedSummary
@@ -233,8 +225,8 @@ const Status = {
}
},
attachmentSize () {
- if ((this.$store.state.config.hideAttachments && !this.inConversation) ||
- (this.$store.state.config.hideAttachmentsInConv && this.inConversation) ||
+ if ((this.mergedConfig.hideAttachments && !this.inConversation) ||
+ (this.mergedConfig.hideAttachmentsInConv && this.inConversation) ||
(this.status.attachments.length > this.maxThumbnails)) {
return 'hide'
} else if (this.compact) {
@@ -246,7 +238,7 @@ const Status = {
if (this.attachmentSize === 'hide') {
return []
}
- return this.$store.state.config.playVideosInModal
+ return this.mergedConfig.playVideosInModal
? ['image', 'video']
: ['image']
},
@@ -261,7 +253,7 @@ const Status = {
)
},
maxThumbnails () {
- return this.$store.state.config.maxThumbnails
+ return this.mergedConfig.maxThumbnails
},
contentHtml () {
if (!this.status.summary_html) {
@@ -284,10 +276,9 @@ const Status = {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
},
hidePostStats () {
- return typeof this.$store.state.config.hidePostStats === 'undefined'
- ? this.$store.state.instance.hidePostStats
- : this.$store.state.config.hidePostStats
- }
+ return this.mergedConfig.hidePostStats
+ },
+ ...mapGetters(['mergedConfig'])
},
components: {
Attachment,
@@ -301,7 +292,8 @@ const Status = {
Gallery,
LinkPreview,
AvatarList,
- Timeago
+ Timeago,
+ StatusPopover
},
methods: {
visibilityIcon (visibility) {
@@ -376,27 +368,6 @@ const Status = {
this.expandingSubject = true
}
},
- replyEnter (id, event) {
- this.showPreview = true
- const targetId = id
- const statuses = this.$store.state.statuses.allStatuses
-
- if (!this.preview) {
- // if we have the status somewhere already
- this.preview = find(statuses, { 'id': targetId })
- // or if we have to fetch it
- if (!this.preview) {
- this.$store.state.api.backendInteractor.fetchStatus({ id }).then((status) => {
- this.preview = status
- })
- }
- } else if (this.preview.id !== targetId) {
- this.preview = find(statuses, { 'id': targetId })
- }
- },
- replyLeave () {
- this.showPreview = false
- },
generateUserProfileLink (id, name) {
return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames)
},
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 93f37a49..65778b2e 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -174,20 +174,26 @@
v-if="isReply"
class="reply-to-and-accountname"
>
- <a
+ <StatusPopover
+ v-if="!isPreview"
+ :status-id="status.in_reply_to_status_id"
+ >
+ <a
+ class="reply-to"
+ href="#"
+ :aria-label="$t('tool_tip.reply')"
+ @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
+ >
+ <i class="button-icon icon-reply" />
+ <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
+ </a>
+ </StatusPopover>
+ <span
+ v-else
class="reply-to"
- href="#"
- :aria-label="$t('tool_tip.reply')"
- @click.prevent="gotoOriginal(status.in_reply_to_status_id)"
- @mouseenter.prevent.stop="replyEnter(status.in_reply_to_status_id, $event)"
- @mouseleave.prevent.stop="replyLeave()"
>
- <i
- v-if="!isPreview"
- class="button-icon icon-reply"
- />
- <span class="faint-link reply-to-text">{{ $t('status.reply_to') }}</span>
- </a>
+ <span class="reply-to-text">{{ $t('status.reply_to') }}</span>
+ </span>
<router-link :to="replyProfileLink">
{{ replyToName }}
</router-link>
@@ -199,51 +205,26 @@
</span>
</div>
<div
- v-if="inConversation && !isPreview"
+ v-if="inConversation && !isPreview && replies && replies.length"
class="replies"
>
- <span
- v-if="replies && replies.length"
- class="faint"
- >{{ $t('status.replies_list') }}</span>
- <template v-if="replies">
- <span
- v-for="reply in replies"
- :key="reply.id"
- class="reply-link faint"
- >
- <a
- href="#"
- @click.prevent="gotoOriginal(reply.id)"
- @mouseenter="replyEnter(reply.id, $event)"
- @mouseout="replyLeave()"
- >{{ reply.name }}</a>
- </span>
- </template>
+ <span class="faint">{{ $t('status.replies_list') }}</span>
+ <StatusPopover
+ v-for="reply in replies"
+ :key="reply.id"
+ :status-id="reply.id"
+ >
+ <a
+ href="#"
+ class="reply-link"
+ @click.prevent="gotoOriginal(reply.id)"
+ >{{ reply.name }}</a>
+ </StatusPopover>
</div>
</div>
</div>
<div
- v-if="showPreview"
- class="status-preview-container"
- >
- <status
- v-if="preview"
- class="status-preview"
- :is-preview="true"
- :statusoid="preview"
- :compact="true"
- />
- <div
- v-else
- class="status-preview status-preview-loading"
- >
- <i class="icon-spin4 animate-spin" />
- </div>
- </div>
-
- <div
v-if="longSubject"
class="status-content-wrapper"
:class="{ 'tall-status': !showingLongSubject }"
@@ -439,18 +420,6 @@ $status-margin: 0.75em;
min-width: 0;
}
-.status-preview.status-el {
- border-style: solid;
- border-width: 1px;
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
-}
-
-.status-preview-container {
- position: relative;
- max-width: 100%;
-}
-
.status-pin {
padding: $status-margin $status-margin 0;
display: flex;
@@ -458,44 +427,6 @@ $status-margin: 0.75em;
justify-content: flex-end;
}
-.status-preview {
- position: absolute;
- max-width: 95%;
- display: flex;
- background-color: $fallback--bg;
- background-color: var(--bg, $fallback--bg);
- border-color: $fallback--border;
- border-color: var(--border, $fallback--border);
- border-style: solid;
- border-width: 1px;
- border-radius: $fallback--tooltipRadius;
- border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
- box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
- box-shadow: var(--popupShadow);
- margin-top: 0.25em;
- margin-left: 0.5em;
- z-index: 50;
-
- .status {
- flex: 1;
- border: 0;
- min-width: 15em;
- }
-}
-
-.status-preview-loading {
- display: block;
- min-width: 15em;
- padding: 1em;
- text-align: center;
- border-width: 1px;
- border-style: solid;
-
- i {
- font-size: 2em;
- }
-}
-
.media-left {
margin-right: $status-margin;
}
@@ -553,11 +484,6 @@ $status-margin: 0.75em;
flex-basis: 100%;
margin-bottom: 0.5em;
- a {
- display: inline-block;
- word-break: break-all;
- }
-
small {
font-weight: lighter;
}
@@ -568,6 +494,11 @@ $status-margin: 0.75em;
justify-content: space-between;
line-height: 18px;
+ a {
+ display: inline-block;
+ word-break: break-all;
+ }
+
.name-and-account-name {
display: flex;
min-width: 0;
@@ -600,6 +531,7 @@ $status-margin: 0.75em;
}
.heading-reply-row {
+ position: relative;
align-content: baseline;
font-size: 12px;
line-height: 18px;
@@ -608,11 +540,13 @@ $status-margin: 0.75em;
flex-wrap: wrap;
align-items: stretch;
- a {
+ > .reply-to-and-accountname > a {
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
+ display: inline-block;
+ word-break: break-all;
}
}
@@ -639,6 +573,8 @@ $status-margin: 0.75em;
overflow: hidden;
text-overflow: ellipsis;
margin: 0 0.4em 0 0.2em;
+ color: $fallback--faint;
+ color: var(--faint, $fallback--faint);
}
.replies-separator {
@@ -840,6 +776,11 @@ $status-margin: 0.75em;
&.button-icon-active {
color: $fallback--cBlue;
color: var(--cBlue, $fallback--cBlue);
+ }
+}
+
+.button-icon.icon-reply {
+ &:not(.button-icon-disabled) {
cursor: pointer;
}
}
diff --git a/src/components/status_popover/status_popover.js b/src/components/status_popover/status_popover.js
new file mode 100644
index 00000000..19f16bd9
--- /dev/null
+++ b/src/components/status_popover/status_popover.js
@@ -0,0 +1,34 @@
+import { find } from 'lodash'
+
+const StatusPopover = {
+ name: 'StatusPopover',
+ props: [
+ 'statusId'
+ ],
+ data () {
+ return {
+ popperOptions: {
+ modifiers: {
+ preventOverflow: { padding: { top: 50 }, boundariesElement: 'viewport' }
+ }
+ }
+ }
+ },
+ computed: {
+ status () {
+ return find(this.$store.state.statuses.allStatuses, { id: this.statusId })
+ }
+ },
+ components: {
+ Status: () => import('../status/status.vue')
+ },
+ methods: {
+ enter () {
+ if (!this.status) {
+ this.$store.dispatch('fetchStatus', this.statusId)
+ }
+ }
+ }
+}
+
+export default StatusPopover
diff --git a/src/components/status_popover/status_popover.vue b/src/components/status_popover/status_popover.vue
new file mode 100644
index 00000000..eacf4c06
--- /dev/null
+++ b/src/components/status_popover/status_popover.vue
@@ -0,0 +1,85 @@
+<template>
+ <v-popover
+ popover-class="status-popover"
+ placement="top-start"
+ :popper-options="popperOptions"
+ @show="enter()"
+ >
+ <template slot="popover">
+ <Status
+ v-if="status"
+ :is-preview="true"
+ :statusoid="status"
+ :compact="true"
+ />
+ <div
+ v-else
+ class="status-preview-loading"
+ >
+ <i class="icon-spin4 animate-spin" />
+ </div>
+ </template>
+
+ <slot />
+ </v-popover>
+</template>
+
+<script src="./status_popover.js" ></script>
+
+<style lang="scss">
+@import '../../_variables.scss';
+
+.tooltip.popover.status-popover {
+ font-size: 1rem;
+ min-width: 15em;
+ max-width: 95%;
+ margin-left: 0.5em;
+
+ .popover-inner {
+ border-color: $fallback--border;
+ border-color: var(--border, $fallback--border);
+ border-style: solid;
+ border-width: 1px;
+ border-radius: $fallback--tooltipRadius;
+ border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
+ box-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5);
+ box-shadow: var(--popupShadow);
+ }
+
+ .popover-arrow::before {
+ position: absolute;
+ content: '';
+ left: -7px;
+ border: solid 7px transparent;
+ z-index: -1;
+ }
+
+ &[x-placement^="bottom-start"] .popover-arrow::before {
+ top: -2px;
+ border-top-width: 0;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+ }
+
+ &[x-placement^="top-start"] .popover-arrow::before {
+ bottom: -2px;
+ border-bottom-width: 0;
+ border-top-color: $fallback--border;
+ border-top-color: var(--border, $fallback--border);
+ }
+
+ .status-el.status-el {
+ border: none;
+ }
+
+ .status-preview-loading {
+ padding: 1em;
+ text-align: center;
+
+ i {
+ font-size: 2em;
+ }
+ }
+}
+
+</style>
diff --git a/src/components/still-image/still-image.js b/src/components/still-image/still-image.js
index 02e98f19..e48fef47 100644
--- a/src/components/still-image/still-image.js
+++ b/src/components/still-image/still-image.js
@@ -3,11 +3,12 @@ const StillImage = {
'src',
'referrerpolicy',
'mimetype',
- 'imageLoadError'
+ 'imageLoadError',
+ 'imageLoadHandler'
],
data () {
return {
- stopGifs: this.$store.state.config.stopGifs
+ stopGifs: this.$store.getters.mergedConfig.stopGifs
}
},
computed: {
@@ -17,6 +18,7 @@ const StillImage = {
},
methods: {
onLoad () {
+ this.imageLoadHandler && this.imageLoadHandler(this.$refs.src)
const canvas = this.$refs.canvas
if (!canvas) return
const width = this.$refs.src.naturalWidth
diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js
index ef5704c1..ebde4475 100644
--- a/src/components/style_switcher/style_switcher.js
+++ b/src/components/style_switcher/style_switcher.js
@@ -10,6 +10,7 @@ import ContrastRatio from '../contrast_ratio/contrast_ratio.vue'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
import Preview from './preview.vue'
import ExportImport from '../export_import/export_import.vue'
+import Checkbox from '../checkbox/checkbox.vue'
// List of color values used in v1
const v1OnlyNames = [
@@ -27,7 +28,7 @@ export default {
data () {
return {
availableStyles: [],
- selected: this.$store.state.config.theme,
+ selected: this.$store.getters.mergedConfig.theme,
previewShadows: {},
previewColors: {},
@@ -112,7 +113,7 @@ export default {
})
},
mounted () {
- this.normalizeLocalState(this.$store.state.config.customTheme)
+ this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme)
if (typeof this.shadowSelected === 'undefined') {
this.shadowSelected = this.shadowsAvailable[0]
}
@@ -341,7 +342,8 @@ export default {
FontControl,
TabSwitcher,
Preview,
- ExportImport
+ ExportImport,
+ Checkbox
},
methods: {
setCustomTheme () {
@@ -368,9 +370,9 @@ export default {
return version >= 1 || version <= 2
},
clearAll () {
- const state = this.$store.state.config.customTheme
+ const state = this.$store.getters.mergedConfig.customTheme
const version = state.colors ? 2 : 'l1'
- this.normalizeLocalState(this.$store.state.config.customTheme, version)
+ this.normalizeLocalState(this.$store.getters.mergedConfig.customTheme, version)
},
// Clears all the extra stuff when loading V1 theme
diff --git a/src/components/style_switcher/style_switcher.vue b/src/components/style_switcher/style_switcher.vue
index d32641c2..ad032041 100644
--- a/src/components/style_switcher/style_switcher.vue
+++ b/src/components/style_switcher/style_switcher.vue
@@ -42,44 +42,29 @@
</div>
<div class="save-load-options">
<span class="keep-option">
- <input
- id="keep-color"
- v-model="keepColor"
- type="checkbox"
- >
- <label for="keep-color">{{ $t('settings.style.switcher.keep_color') }}</label>
+ <Checkbox v-model="keepColor">
+ {{ $t('settings.style.switcher.keep_color') }}
+ </Checkbox>
</span>
<span class="keep-option">
- <input
- id="keep-shadows"
- v-model="keepShadows"
- type="checkbox"
- >
- <label for="keep-shadows">{{ $t('settings.style.switcher.keep_shadows') }}</label>
+ <Checkbox v-model="keepShadows">
+ {{ $t('settings.style.switcher.keep_shadows') }}
+ </Checkbox>
</span>
<span class="keep-option">
- <input
- id="keep-opacity"
- v-model="keepOpacity"
- type="checkbox"
- >
- <label for="keep-opacity">{{ $t('settings.style.switcher.keep_opacity') }}</label>
+ <Checkbox v-model="keepOpacity">
+ {{ $t('settings.style.switcher.keep_opacity') }}
+ </Checkbox>
</span>
<span class="keep-option">
- <input
- id="keep-roundness"
- v-model="keepRoundness"
- type="checkbox"
- >
- <label for="keep-roundness">{{ $t('settings.style.switcher.keep_roundness') }}</label>
+ <Checkbox v-model="keepRoundness">
+ {{ $t('settings.style.switcher.keep_roundness') }}
+ </Checkbox>
</span>
<span class="keep-option">
- <input
- id="keep-fonts"
- v-model="keepFonts"
- type="checkbox"
- >
- <label for="keep-fonts">{{ $t('settings.style.switcher.keep_fonts') }}</label>
+ <Checkbox v-model="keepFonts">
+ {{ $t('settings.style.switcher.keep_fonts') }}
+ </Checkbox>
</span>
<p>{{ $t('settings.style.switcher.save_load_hint') }}</p>
</div>
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 0594576c..27a9a55e 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -141,7 +141,7 @@ const Timeline = {
const bodyBRect = document.body.getBoundingClientRect()
const height = Math.max(bodyBRect.height, -(bodyBRect.y))
if (this.timeline.loading === false &&
- this.$store.state.config.autoLoad &&
+ this.$store.getters.mergedConfig.autoLoad &&
this.$el.offsetHeight > 0 &&
(window.innerHeight + window.pageYOffset) >= (height - 750)) {
this.fetchOlderStatuses()
@@ -153,7 +153,7 @@ const Timeline = {
},
watch: {
newStatusCount (count) {
- if (!this.$store.state.config.streaming) {
+ if (!this.$store.getters.mergedConfig.streaming) {
return
}
if (count > 0) {
@@ -162,7 +162,7 @@ const Timeline = {
const top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
if (top < 15 &&
!this.paused &&
- !(this.unfocused && this.$store.state.config.pauseOnUnfocused)
+ !(this.unfocused && this.$store.getters.mergedConfig.pauseOnUnfocused)
) {
this.showNewStatuses()
} else {
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 9c931c01..cc8a1ed6 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -1,19 +1,20 @@
import UserAvatar from '../user_avatar/user_avatar.vue'
import RemoteFollow from '../remote_follow/remote_follow.vue'
import ProgressButton from '../progress_button/progress_button.vue'
+import FollowButton from '../follow_button/follow_button.vue'
import ModerationTools from '../moderation_tools/moderation_tools.vue'
+import AccountActions from '../account_actions/account_actions.vue'
import { hex2rgb } from '../../services/color_convert/color_convert.js'
-import { requestFollow, requestUnfollow } from '../../services/follow_manipulate/follow_manipulate'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
+import { mapGetters } from 'vuex'
export default {
- props: [ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar' ],
+ props: [
+ 'user', 'switcher', 'selected', 'hideBio', 'rounded', 'bordered', 'allowZoomingAvatar'
+ ],
data () {
return {
followRequestInProgress: false,
- hideUserStatsLocal: typeof this.$store.state.config.hideUserStats === 'undefined'
- ? this.$store.state.instance.hideUserStats
- : this.$store.state.config.hideUserStats,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
}
},
@@ -29,9 +30,9 @@ export default {
}]
},
style () {
- const color = this.$store.state.config.customTheme.colors
- ? this.$store.state.config.customTheme.colors.bg // v2
- : this.$store.state.config.colors.bg // v1
+ const color = this.$store.getters.mergedConfig.customTheme.colors
+ ? this.$store.getters.mergedConfig.customTheme.colors.bg // v2
+ : this.$store.getters.mergedConfig.colors.bg // v1
if (color) {
const rgb = (typeof color === 'string') ? hex2rgb(color) : color
@@ -63,21 +64,22 @@ export default {
},
userHighlightType: {
get () {
- const data = this.$store.state.config.highlight[this.user.screen_name]
+ const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
return (data && data.type) || 'disabled'
},
set (type) {
- const data = this.$store.state.config.highlight[this.user.screen_name]
+ const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
if (type !== 'disabled') {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: (data && data.color) || '#FFFFFF', type })
} else {
this.$store.dispatch('setHighlight', { user: this.user.screen_name, color: undefined })
}
- }
+ },
+ ...mapGetters(['mergedConfig'])
},
userHighlightColor: {
get () {
- const data = this.$store.state.config.highlight[this.user.screen_name]
+ const data = this.$store.getters.mergedConfig.highlight[this.user.screen_name]
return data && data.color
},
set (color) {
@@ -90,36 +92,18 @@ export default {
const validRole = rights.admin || rights.moderator
const roleTitle = rights.admin ? 'admin' : 'moderator'
return validRole && roleTitle
- }
+ },
+ ...mapGetters(['mergedConfig'])
},
components: {
UserAvatar,
RemoteFollow,
ModerationTools,
- ProgressButton
+ AccountActions,
+ ProgressButton,
+ FollowButton
},
methods: {
- followUser () {
- const store = this.$store
- this.followRequestInProgress = true
- requestFollow(this.user, store).then(() => {
- this.followRequestInProgress = false
- })
- },
- unfollowUser () {
- const store = this.$store
- this.followRequestInProgress = true
- requestUnfollow(this.user, store).then(() => {
- this.followRequestInProgress = false
- store.commit('removeStatus', { timeline: 'friends', userId: this.user.id })
- })
- },
- blockUser () {
- this.$store.dispatch('blockUser', this.user.id)
- },
- unblockUser () {
- this.$store.dispatch('unblockUser', this.user.id)
- },
muteUser () {
this.$store.dispatch('muteUser', this.user.id)
},
@@ -147,10 +131,10 @@ export default {
}
},
userProfileLink (user) {
- return generateProfileLink(user.id, user.screen_name, this.$store.state.instance.restrictedNicknames)
- },
- reportUser () {
- this.$store.dispatch('openUserReportingModal', this.user.id)
+ return generateProfileLink(
+ user.id, user.screen_name,
+ this.$store.state.instance.restrictedNicknames
+ )
},
zoomAvatar () {
const attachment = {
@@ -159,9 +143,6 @@ export default {
}
this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment)
- },
- mentionUser () {
- this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index 5b6f66e7..6f3c958e 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -66,8 +66,11 @@
>
<i class="icon-link-ext usersettings" />
</a>
+ <AccountActions
+ v-if="isOtherUser && loggedIn"
+ :user="user"
+ />
</div>
-
<div class="bottom-line">
<router-link
class="user-screen-name"
@@ -81,7 +84,7 @@
>{{ visibleRole }}</span>
<span v-if="user.locked"><i class="icon icon-lock" /></span>
<span
- v-if="!hideUserStatsLocal && !hideBio"
+ v-if="!mergedConfig.hideUserStats && !hideBio"
class="dailyAvg"
>{{ dailyAvg }} {{ $t('user_card.per_day') }}</span>
</div>
@@ -135,72 +138,27 @@
v-if="loggedIn && isOtherUser"
class="user-interactions"
>
- <div v-if="!user.following">
- <button
- class="btn btn-default btn-block"
- :disabled="followRequestInProgress"
- :title="user.requested ? $t('user_card.follow_again') : ''"
- @click="followUser"
- >
- <template v-if="followRequestInProgress">
- {{ $t('user_card.follow_progress') }}
- </template>
- <template v-else-if="user.requested">
- {{ $t('user_card.follow_sent') }}
- </template>
- <template v-else>
- {{ $t('user_card.follow') }}
- </template>
- </button>
- </div>
- <div v-else-if="followRequestInProgress">
- <button
- class="btn btn-default btn-block pressed"
- disabled
- :title="$t('user_card.follow_unfollow')"
- @click="unfollowUser"
- >
- {{ $t('user_card.follow_progress') }}
- </button>
- </div>
- <div
- v-else
- class="btn-group"
- >
- <button
- class="btn btn-default pressed"
- :title="$t('user_card.follow_unfollow')"
- @click="unfollowUser"
- >
- {{ $t('user_card.following') }}
- </button>
- <ProgressButton
- v-if="!user.subscribed"
- class="btn btn-default"
- :click="subscribeUser"
- :title="$t('user_card.subscribe')"
- >
- <i class="icon-bell-alt" />
- </ProgressButton>
- <ProgressButton
- v-else
- class="btn btn-default pressed"
- :click="unsubscribeUser"
- :title="$t('user_card.unsubscribe')"
- >
- <i class="icon-bell-ringing-o" />
- </ProgressButton>
- </div>
-
- <div>
- <button
- class="btn btn-default btn-block"
- @click="mentionUser"
- >
- {{ $t('user_card.mention') }}
- </button>
+ <div class="btn-group">
+ <FollowButton :user="user" />
+ <template v-if="user.following">
+ <ProgressButton
+ v-if="!user.subscribed"
+ class="btn btn-default"
+ :click="subscribeUser"
+ :title="$t('user_card.subscribe')"
+ >
+ <i class="icon-bell-alt" />
+ </ProgressButton>
+ <ProgressButton
+ v-else
+ class="btn btn-default pressed"
+ :click="unsubscribeUser"
+ :title="$t('user_card.unsubscribe')"
+ >
+ <i class="icon-bell-ringing-o" />
+ </ProgressButton>
+ </template>
</div>
-
<div>
<button
v-if="user.muted"
@@ -217,33 +175,6 @@
{{ $t('user_card.mute') }}
</button>
</div>
-
- <div>
- <button
- v-if="user.statusnet_blocking"
- class="btn btn-default btn-block pressed"
- @click="unblockUser"
- >
- {{ $t('user_card.blocked') }}
- </button>
- <button
- v-else
- class="btn btn-default btn-block"
- @click="blockUser"
- >
- {{ $t('user_card.block') }}
- </button>
- </div>
-
- <div>
- <button
- class="btn btn-default btn-block"
- @click="reportUser"
- >
- {{ $t('user_card.report') }}
- </button>
- </div>
-
<ModerationTools
v-if="loggedIn.role === &quot;admin&quot;"
:user="user"
@@ -262,7 +193,7 @@
class="panel-body"
>
<div
- v-if="!hideUserStatsLocal && switcher"
+ v-if="!mergedConfig.hideUserStats && switcher"
class="user-counts"
>
<div
@@ -345,6 +276,8 @@
mask-composite: exclude;
background-size: cover;
mask-size: 100% 60%;
+ border-top-left-radius: calc(var(--panelRadius) - 1px);
+ border-top-right-radius: calc(var(--panelRadius) - 1px);
&.hide-bio {
mask-size: 100% 40px;
@@ -587,13 +520,12 @@
position: relative;
display: flex;
flex-flow: row wrap;
- justify-content: space-between;
margin-right: -.75em;
> * {
- flex: 1 0 0;
margin: 0 .75em .6em 0;
white-space: nowrap;
+ min-width: 95px;
}
button {
diff --git a/src/components/user_reporting_modal/user_reporting_modal.js b/src/components/user_reporting_modal/user_reporting_modal.js
index 7c6ea409..833fa98a 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.js
+++ b/src/components/user_reporting_modal/user_reporting_modal.js
@@ -2,12 +2,14 @@
import Status from '../status/status.vue'
import List from '../list/list.vue'
import Checkbox from '../checkbox/checkbox.vue'
+import Modal from '../modal/modal.vue'
const UserReportingModal = {
components: {
Status,
List,
- Checkbox
+ Checkbox,
+ Modal
},
data () {
return {
diff --git a/src/components/user_reporting_modal/user_reporting_modal.vue b/src/components/user_reporting_modal/user_reporting_modal.vue
index c79a3707..6ee53461 100644
--- a/src/components/user_reporting_modal/user_reporting_modal.vue
+++ b/src/components/user_reporting_modal/user_reporting_modal.vue
@@ -1,13 +1,9 @@
<template>
- <div
+ <Modal
v-if="isOpen"
- class="modal-view"
- @click="closeModal"
+ @backdropClicked="closeModal"
>
- <div
- class="user-reporting-panel panel"
- @click.stop=""
- >
+ <div class="user-reporting-panel panel">
<div class="panel-heading">
<div class="title">
{{ $t('user_reporting.title', [user.screen_name]) }}
@@ -69,7 +65,7 @@
</div>
</div>
</div>
- </div>
+ </Modal>
</template>
<script src="./user_reporting_modal.js"></script>
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index f12cccae..3fdc5340 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -17,6 +17,7 @@ import Autosuggest from '../autosuggest/autosuggest.vue'
import Importer from '../importer/importer.vue'
import Exporter from '../exporter/exporter.vue'
import withSubscription from '../../hocs/with_subscription/with_subscription'
+import Checkbox from '../checkbox/checkbox.vue'
import Mfa from './mfa.vue'
const BlockList = withSubscription({
@@ -34,6 +35,7 @@ const MuteList = withSubscription({
const UserSettings = {
data () {
return {
+ newEmail: '',
newName: this.$store.state.users.currentUser.name,
newBio: unescape(this.$store.state.users.currentUser.description),
newLocked: this.$store.state.users.currentUser.locked,
@@ -55,6 +57,9 @@ const UserSettings = {
backgroundPreview: null,
bannerUploadError: null,
backgroundUploadError: null,
+ changeEmailError: false,
+ changeEmailPassword: '',
+ changedEmail: false,
deletingAccount: false,
deleteAccountConfirmPasswordInput: '',
deleteAccountError: false,
@@ -82,7 +87,8 @@ const UserSettings = {
ProgressButton,
Importer,
Exporter,
- Mfa
+ Mfa,
+ Checkbox
},
computed: {
user () {
@@ -303,6 +309,22 @@ const UserSettings = {
}
})
},
+ changeEmail () {
+ const params = {
+ email: this.newEmail,
+ password: this.changeEmailPassword
+ }
+ this.$store.state.api.backendInteractor.changeEmail(params)
+ .then((res) => {
+ if (res.status === 'success') {
+ this.changedEmail = true
+ this.changeEmailError = false
+ } else {
+ this.changedEmail = false
+ this.changeEmailError = 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 ef75ac52..8c18cf49 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -53,12 +53,9 @@
/>
</EmojiInput>
<p>
- <input
- id="account-locked"
- v-model="newLocked"
- type="checkbox"
- >
- <label for="account-locked">{{ $t('settings.lock_account_description') }}</label>
+ <Checkbox v-model="newLocked">
+ {{ $t('settings.lock_account_description') }}
+ </Checkbox>
</p>
<div>
<label for="default-vis">{{ $t('settings.default_vis') }}</label>
@@ -75,69 +72,52 @@
</div>
</div>
<p>
- <input
- id="account-no-rich-text"
- v-model="newNoRichText"
- type="checkbox"
- >
- <label for="account-no-rich-text">{{ $t('settings.no_rich_text_description') }}</label>
+ <Checkbox v-model="newNoRichText">
+ {{ $t('settings.no_rich_text_description') }}
+ </Checkbox>
</p>
<p>
- <input
- id="account-hide-follows"
- v-model="hideFollows"
- type="checkbox"
- >
- <label for="account-hide-follows">{{ $t('settings.hide_follows_description') }}</label>
+ <Checkbox v-model="hideFollows">
+ {{ $t('settings.hide_follows_description') }}
+ </Checkbox>
</p>
<p class="setting-subitem">
- <input
- id="account-hide-follows-count"
+ <Checkbox
v-model="hideFollowsCount"
- type="checkbox"
:disabled="!hideFollows"
>
- <label for="account-hide-follows-count">{{ $t('settings.hide_follows_count_description') }}</label>
+ {{ $t('settings.hide_follows_count_description') }}
+ </Checkbox>
</p>
<p>
- <input
- id="account-hide-followers"
+ <Checkbox
v-model="hideFollowers"
- type="checkbox"
>
- <label for="account-hide-followers">{{ $t('settings.hide_followers_description') }}</label>
+ {{ $t('settings.hide_followers_description') }}
+ </Checkbox>
</p>
<p class="setting-subitem">
- <input
- id="account-hide-followers-count"
+ <Checkbox
v-model="hideFollowersCount"
- type="checkbox"
:disabled="!hideFollowers"
>
- <label for="account-hide-followers-count">{{ $t('settings.hide_followers_count_description') }}</label>
+ {{ $t('settings.hide_followers_count_description') }}
+ </Checkbox>
</p>
<p>
- <input
- id="account-show-role"
- v-model="showRole"
- type="checkbox"
- >
- <label
- v-if="role === 'admin'"
- for="account-show-role"
- >{{ $t('settings.show_admin_badge') }}</label>
- <label
- v-if="role === 'moderator'"
- for="account-show-role"
- >{{ $t('settings.show_moderator_badge') }}</label>
+ <Checkbox v-model="showRole">
+ <template v-if="role === 'admin'">
+ {{ $t('settings.show_admin_badge') }}
+ </template>
+ <template v-if="role === 'moderator'">
+ {{ $t('settings.show_moderator_badge') }}
+ </template>
+ </Checkbox>
</p>
<p>
- <input
- id="discoverable"
- v-model="discoverable"
- type="checkbox"
- >
- <label for="discoverable">{{ $t('settings.discoverable') }}</label>
+ <Checkbox v-model="discoverable">
+ {{ $t('settings.discoverable') }}
+ </Checkbox>
</p>
<button
:disabled="newName && newName.length === 0"
@@ -254,6 +234,39 @@
<div :label="$t('settings.security_tab')">
<div class="setting-item">
+ <h2>{{ $t('settings.change_email') }}</h2>
+ <div>
+ <p>{{ $t('settings.new_email') }}</p>
+ <input
+ v-model="newEmail"
+ type="email"
+ autocomplete="email"
+ >
+ </div>
+ <div>
+ <p>{{ $t('settings.current_password') }}</p>
+ <input
+ v-model="changeEmailPassword"
+ type="password"
+ autocomplete="current-password"
+ >
+ </div>
+ <button
+ class="btn btn-default"
+ @click="changeEmail"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <p v-if="changedEmail">
+ {{ $t('settings.changed_email') }}
+ </p>
+ <template v-if="changeEmailError !== false">
+ <p>{{ $t('settings.change_email_error') }}</p>
+ <p>{{ changeEmailError }}</p>
+ </template>
+ </div>
+
+ <div class="setting-item">
<h2>{{ $t('settings.change_password') }}</h2>
<div>
<p>{{ $t('settings.current_password') }}</p>
@@ -367,44 +380,24 @@
<span class="label">{{ $t('settings.notification_setting') }}</span>
<ul class="option-list">
<li>
- <input
- id="notification-setting-follows"
- v-model="notificationSettings.follows"
- type="checkbox"
- >
- <label for="notification-setting-follows">
+ <Checkbox v-model="notificationSettings.follows">
{{ $t('settings.notification_setting_follows') }}
- </label>
+ </Checkbox>
</li>
<li>
- <input
- id="notification-setting-followers"
- v-model="notificationSettings.followers"
- type="checkbox"
- >
- <label for="notification-setting-followers">
+ <Checkbox v-model="notificationSettings.followers">
{{ $t('settings.notification_setting_followers') }}
- </label>
+ </Checkbox>
</li>
<li>
- <input
- id="notification-setting-non-follows"
- v-model="notificationSettings.non_follows"
- type="checkbox"
- >
- <label for="notification-setting-non-follows">
+ <Checkbox v-model="notificationSettings.non_follows">
{{ $t('settings.notification_setting_non_follows') }}
- </label>
+ </Checkbox>
</li>
<li>
- <input
- id="notification-setting-non-followers"
- v-model="notificationSettings.non_followers"
- type="checkbox"
- >
- <label for="notification-setting-non-followers">
+ <Checkbox v-model="notificationSettings.non_followers">
{{ $t('settings.notification_setting_non_followers') }}
- </label>
+ </Checkbox>
</li>
</ul>
</div>
diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js
index 76b19a02..f0ca7e89 100644
--- a/src/components/video_attachment/video_attachment.js
+++ b/src/components/video_attachment/video_attachment.js
@@ -3,7 +3,7 @@ const VideoAttachment = {
props: ['attachment', 'controls'],
data () {
return {
- loopVideo: this.$store.state.config.loopVideo
+ loopVideo: this.$store.getters.mergedConfig.loopVideo
}
},
methods: {
@@ -12,16 +12,16 @@ const VideoAttachment = {
if (typeof target.webkitAudioDecodedByteCount !== 'undefined') {
// non-zero if video has audio track
if (target.webkitAudioDecodedByteCount > 0) {
- this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} else if (typeof target.mozHasAudio !== 'undefined') {
// true if video has audio track
if (target.mozHasAudio) {
- this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
} else if (typeof target.audioTracks !== 'undefined') {
if (target.audioTracks.length > 0) {
- this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly
+ this.loopVideo = this.loopVideo && !this.$store.getters.mergedConfig.loopVideoSilentOnly
}
}
}
diff --git a/src/directives/body_scroll_lock.js b/src/directives/body_scroll_lock.js
index 6ab20c3f..13a6de1c 100644
--- a/src/directives/body_scroll_lock.js
+++ b/src/directives/body_scroll_lock.js
@@ -2,42 +2,49 @@ import * as bodyScrollLock from 'body-scroll-lock'
let previousNavPaddingRight
let previousAppBgWrapperRight
+const lockerEls = new Set([])
const disableBodyScroll = (el) => {
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth
bodyScrollLock.disableBodyScroll(el, {
reserveScrollBarGap: true
})
+ lockerEls.add(el)
setTimeout(() => {
- // If previousNavPaddingRight is already set, don't set it again.
- if (previousNavPaddingRight === undefined) {
- const navEl = document.getElementById('nav')
- previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right')
- navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
+ if (lockerEls.size <= 1) {
+ // If previousNavPaddingRight is already set, don't set it again.
+ if (previousNavPaddingRight === undefined) {
+ const navEl = document.getElementById('nav')
+ previousNavPaddingRight = window.getComputedStyle(navEl).getPropertyValue('padding-right')
+ navEl.style.paddingRight = previousNavPaddingRight ? `calc(${previousNavPaddingRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
+ }
+ // If previousAppBgWrapeprRight is already set, don't set it again.
+ if (previousAppBgWrapperRight === undefined) {
+ const appBgWrapperEl = document.getElementById('app_bg_wrapper')
+ previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
+ appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
+ }
+ document.body.classList.add('scroll-locked')
}
- // If previousAppBgWrapeprRight is already set, don't set it again.
- if (previousAppBgWrapperRight === undefined) {
- const appBgWrapperEl = document.getElementById('app_bg_wrapper')
- previousAppBgWrapperRight = window.getComputedStyle(appBgWrapperEl).getPropertyValue('right')
- appBgWrapperEl.style.right = previousAppBgWrapperRight ? `calc(${previousAppBgWrapperRight} + ${scrollBarGap}px)` : `${scrollBarGap}px`
- }
- document.body.classList.add('scroll-locked')
})
}
const enableBodyScroll = (el) => {
+ lockerEls.delete(el)
setTimeout(() => {
- if (previousNavPaddingRight !== undefined) {
- document.getElementById('nav').style.paddingRight = previousNavPaddingRight
- // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
- previousNavPaddingRight = undefined
- }
- if (previousAppBgWrapperRight !== undefined) {
- document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
- // Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
- previousAppBgWrapperRight = undefined
+ if (lockerEls.size === 0) {
+ if (previousNavPaddingRight !== undefined) {
+ document.getElementById('nav').style.paddingRight = previousNavPaddingRight
+ // Restore previousNavPaddingRight to undefined so disableBodyScroll knows it can be set again.
+ previousNavPaddingRight = undefined
+ }
+ if (previousAppBgWrapperRight !== undefined) {
+ document.getElementById('app_bg_wrapper').style.right = previousAppBgWrapperRight
+ // Restore previousAppBgWrapperRight to undefined so disableBodyScroll knows it can be set again.
+ previousAppBgWrapperRight = undefined
+ }
+ document.body.classList.remove('scroll-locked')
}
- document.body.classList.remove('scroll-locked')
})
bodyScrollLock.enableBodyScroll(el)
}
diff --git a/src/i18n/de.json b/src/i18n/de.json
index fa9db16c..a4b4c16f 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -5,11 +5,11 @@
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
- "media_proxy": "Media Proxy",
+ "media_proxy": "Medienproxy",
"scope_options": "Reichweitenoptionen",
"text_limit": "Textlimit",
"title": "Features",
- "who_to_follow": "Who to follow"
+ "who_to_follow": "Wem folgen?"
},
"finder": {
"error_fetching_user": "Fehler beim Suchen des Benutzers",
@@ -29,15 +29,18 @@
"username": "Benutzername"
},
"nav": {
+ "about": "Über",
"back": "Zurück",
"chat": "Lokaler Chat",
"friend_requests": "Followanfragen",
"mentions": "Erwähnungen",
+ "interactions": "Interaktionen",
"dms": "Direktnachrichten",
"public_tl": "Öffentliche Zeitleiste",
"timeline": "Zeitleiste",
"twkn": "Das gesamte bekannte Netzwerk",
"user_search": "Benutzersuche",
+ "search": "Suche",
"preferences": "Voreinstellungen"
},
"notifications": {
@@ -115,6 +118,9 @@
"delete_account_description": "Lösche deinen Account und alle deine Nachrichten unwiderruflich.",
"delete_account_error": "Es ist ein Fehler beim Löschen deines Accounts aufgetreten. Tritt dies weiterhin auf, wende dich an den Administrator der Instanz.",
"delete_account_instructions": "Tippe dein Passwort unten in das Feld ein, um die Löschung deines Accounts zu bestätigen.",
+ "discoverable": "Erlaubnis für automatisches Suchen nach diesem Account",
+ "avatar_size_instruction": "Die empfohlene minimale Größe für Avatare ist 150x150 Pixel.",
+ "pad_emoji": "Emojis mit Leerzeichen umrahmen",
"export_theme": "Farbschema speichern",
"filtering": "Filtern",
"filtering_explanation": "Alle Beiträge die diese Wörter enthalten werden ausgeblendet. Ein Wort pro Zeile.",
@@ -128,8 +134,11 @@
"general": "Allgemein",
"hide_attachments_in_convo": "Anhänge in Unterhaltungen ausblenden",
"hide_attachments_in_tl": "Anhänge in der Zeitleiste ausblenden",
+ "hide_muted_posts": "Verberge Beiträge stummgeschalteter Nutzer",
+ "max_thumbnails": "Maximale Anzahl von Vorschaubildern pro Beitrag",
"hide_isp": "Instanz-spezifisches Panel ausblenden",
"preload_images": "Bilder vorausladen",
+ "use_one_click_nsfw": "Heikle Anhänge mit nur einem Klick öffnen",
"hide_post_stats": "Beitragsstatistiken verbergen (z.B. die Anzahl der Favoriten)",
"hide_user_stats": "Benutzerstatistiken verbergen (z.B. die Anzahl der Follower)",
"hide_filtered_statuses": "Gefilterte Beiträge verbergen",
@@ -147,6 +156,9 @@
"lock_account_description": "Sperre deinen Account, um neue Follower zu genehmigen oder abzulehnen",
"loop_video": "Videos wiederholen",
"loop_video_silent_only": "Nur Videos ohne Ton wiederholen (z.B. Mastodons \"gifs\")",
+ "mutes_tab": "Mutes",
+ "play_videos_in_modal": "Videos in größerem Medienfenster abspielen",
+ "use_contain_fit": "Vorschaubilder nicht zuschneiden",
"name": "Name",
"name_bio": "Name & Bio",
"new_password": "Neues Passwort",
@@ -158,6 +170,8 @@
"no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen",
"hide_follows_description": "Zeige nicht, wem ich folge",
"hide_followers_description": "Zeige nicht, wer mir folgt",
+ "hide_follows_count_description": "Verberge die Anzahl deiner Gefolgten",
+ "hide_followers_count_description": "Verberge die Anzahl deiner Folgenden",
"nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind",
"oauth_tokens": "OAuth-Token",
"token": "Zeichen",
@@ -176,10 +190,12 @@
"reply_visibility_all": "Alle Antworten zeigen",
"reply_visibility_following": "Zeige nur Antworten an mich oder an Benutzer, denen ich folge",
"reply_visibility_self": "Nur Antworten an mich anzeigen",
+ "autohide_floating_post_button": "Automatisches Verbergen des Knopfs für neue Beiträge (mobil)",
"saving_err": "Fehler beim Speichern der Einstellungen",
"saving_ok": "Einstellungen gespeichert",
"security_tab": "Sicherheit",
"scope_copy": "Reichweite beim Antworten übernehmen (Direktnachrichten werden immer kopiert)",
+ "minimal_scopes_mode": "Minimiere Reichweitenoptionen",
"set_new_avatar": "Setze einen neuen Avatar",
"set_new_profile_background": "Setze einen neuen Hintergrund für dein Profil",
"set_new_profile_banner": "Setze einen neuen Banner für dein Profil",
@@ -189,7 +205,8 @@
"subject_line_email": "Wie Email: \"re: Betreff\"",
"subject_line_mastodon": "Wie Mastodon: unverändert kopieren",
"subject_line_noop": "Nicht kopieren",
- "stop_gifs": "Play-on-hover GIFs",
+ "post_status_content_type": "Beitragsart",
+ "stop_gifs": "Animationen nur beim Darüberfahren abspielen",
"streaming": "Aktiviere automatisches Laden (Streaming) von neuen Beiträgen",
"text": "Text",
"theme": "Farbschema",
@@ -372,5 +389,25 @@
"GiB": "GiB",
"TiB": "TiB"
}
+ },
+ "search": {
+ "people": "Leute",
+ "hashtags": "Hashtags",
+ "person_talking": "{count} Person spricht darüber",
+ "people_talking": "{count} Leute sprechen darüber",
+ "no_results": "Keine Ergebnisse"
+ },
+ "password_reset": {
+ "forgot_password": "Passwort vergessen?",
+ "password_reset": "Password zurücksetzen",
+ "instruction": "Wenn du hier deinen Benutznamen oder die zugehörige E-Mail-Adresse eingibst, kann dir der Server einen Link zum Passwortzurücksetzen zuschicken.",
+ "placeholder": "Dein Benutzername oder die zugehörige E-Mail-Adresse",
+ "check_email": "Im E-Mail-Posteingang des angebenen Kontos müsste sich jetzt (oder zumindest in Kürze) die E-Mail mit dem Link zum Passwortzurücksetzen befinden.",
+ "return_home": "Zurück zur Heimseite",
+ "not_found": "Benutzername/E-Mail-Adresse nicht gefunden. Vertippt?",
+ "too_many_requests": "Kurze Pause. Zu viele Versuche. Bitte, später nochmal probieren.",
+ "password_reset_disabled": "Passwortzurücksetzen deaktiviert. Bitte Administrator kontaktieren.",
+ "password_reset_required": "Passwortzurücksetzen erforderlich",
+ "password_reset_required_but_mailer_is_disabled": "Passwortzurücksetzen wäre erforderlich, ist aber deaktiviert. Bitte Administrator kontaktieren."
}
}
diff --git a/src/i18n/en.json b/src/i18n/en.json
index c6c98c48..8ecb3f3d 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -219,6 +219,9 @@
"cGreen": "Green (Retweet)",
"cOrange": "Orange (Favorite)",
"cRed": "Red (Cancel)",
+ "change_email": "Change Email",
+ "change_email_error": "There was an issue changing your email.",
+ "changed_email": "Email changed successfully!",
"change_password": "Change Password",
"change_password_error": "There was an issue changing your password.",
"changed_password": "Password changed successfully!",
@@ -277,6 +280,7 @@
"use_contain_fit": "Don't crop the attachment in thumbnails",
"name": "Name",
"name_bio": "Name & Bio",
+ "new_email": "New Email",
"new_password": "New password",
"notification_visibility": "Types of notifications to show",
"notification_visibility_follows": "Follows",
@@ -558,6 +562,8 @@
"unmute": "Unmute",
"unmute_progress": "Unmuting...",
"mute_progress": "Muting...",
+ "hide_repeats": "Hide repeats",
+ "show_repeats": "Show repeats",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
@@ -633,6 +639,8 @@
"return_home": "Return to the home page",
"not_found": "We couldn't find that email or username.",
"too_many_requests": "You have reached the limit of attempts, try again later.",
- "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator."
+ "password_reset_disabled": "Password reset is disabled. Please contact your instance administrator.",
+ "password_reset_required": "You must reset your password to log in.",
+ "password_reset_required_but_mailer_is_disabled": "You must reset your password, but password reset is disabled. Please contact your instance administrator."
}
}
diff --git a/src/i18n/es.json b/src/i18n/es.json
index 91c7f383..163eb707 100644
--- a/src/i18n/es.json
+++ b/src/i18n/es.json
@@ -45,8 +45,8 @@
"error": "Se ha producido un error al importar el archivo."
},
"login": {
- "login": "Identificación",
- "description": "Identificación con OAuth",
+ "login": "Identificarse",
+ "description": "Identificarse con OAuth",
"logout": "Cerrar sesión",
"password": "Contraseña",
"placeholder": "p.ej. lain",
@@ -68,6 +68,7 @@
},
"nav": {
"about": "Acerca de",
+ "administration": "Administración",
"back": "Volver",
"chat": "Chat Local",
"friend_requests": "Solicitudes de seguimiento",
@@ -106,6 +107,15 @@
"expired": "La encuesta terminó hace {0}",
"not_enough_options": "Muy pocas opciones únicas en la encuesta"
},
+ "emoji": {
+ "stickers": "Pegatinas",
+ "emoji": "Emoji",
+ "keep_open": "Mantener el selector abierto",
+ "search_emoji": "Buscar un emoji",
+ "add_emoji": "Insertar un emoji",
+ "custom": "Emojis personalizados",
+ "unicode": "Emojis unicode"
+ },
"stickers": {
"add_sticker": "Añadir Pegatina"
},
@@ -222,7 +232,9 @@
"data_import_export_tab": "Importar / Exportar Datos",
"default_vis": "Alcance de visibilidad por defecto",
"delete_account": "Eliminar la cuenta",
+ "discoverable": "Permitir la aparición de esta cuenta en los resultados de búsqueda y otros servicios",
"delete_account_description": "Eliminar para siempre la cuenta y todos los mensajes.",
+ "pad_emoji": "Rellenar con espacios al agregar emojis desde el selector",
"delete_account_error": "Hubo un error al eliminar tu cuenta. Si el fallo persiste, ponte en contacto con el administrador de tu instancia.",
"delete_account_instructions": "Escribe tu contraseña para confirmar la eliminación de tu cuenta.",
"avatar_size_instruction": "El tamaño mínimo recomendado para el avatar es de 150X150 píxeles.",
@@ -277,6 +289,8 @@
"no_mutes": "No hay usuarios sinlenciados",
"hide_follows_description": "No mostrar a quién sigo",
"hide_followers_description": "No mostrar quién me sigue",
+ "hide_follows_count_description": "No mostrar el número de cuentas que sigo",
+ "hide_followers_count_description": "No mostrar el número de cuentas que me siguen",
"show_admin_badge": "Mostrar la insignia de Administrador en mi perfil",
"show_moderator_badge": "Mostrar la insignia de Moderador en mi perfil",
"nsfw_clickthrough": "Activar el clic para ocultar los adjuntos NSFW",
@@ -529,6 +543,7 @@
"follows_you": "¡Te sigue!",
"its_you": "¡Eres tú!",
"media": "Media",
+ "mention": "Mencionar",
"mute": "Silenciar",
"muted": "Silenciado",
"per_day": "por día",
diff --git a/src/i18n/eu.json b/src/i18n/eu.json
index ad8f4c05..1c75bf75 100644
--- a/src/i18n/eu.json
+++ b/src/i18n/eu.json
@@ -68,6 +68,7 @@
},
"nav": {
"about": "Honi buruz",
+ "administration": "Administrazioa",
"back": "Atzera",
"chat": "Txat lokala",
"friend_requests": "Jarraitzeko eskaerak",
@@ -106,6 +107,15 @@
"expired": "Inkesta {0} bukatu zen",
"not_enough_options": "Aukera gutxiegi inkestan"
},
+ "emoji": {
+ "stickers": "Pegatinak",
+ "emoji": "Emoji",
+ "keep_open": "Mantendu hautatzailea zabalik",
+ "search_emoji": "Bilatu emoji bat",
+ "add_emoji": "Emoji bat gehitu",
+ "custom": "Ohiko emojiak",
+ "unicode": "Unicode emojiak"
+ },
"stickers": {
"add_sticker": "Pegatina gehitu"
},
@@ -199,12 +209,12 @@
"avatarRadius": "Avatarrak",
"background": "Atzeko planoa",
"bio": "Biografia",
- "block_export": "Bloke esportatzea",
- "block_export_button": "Esportatu zure blokeak csv fitxategi batera",
- "block_import": "Bloke inportazioa",
- "block_import_error": "Errorea blokeak inportatzen",
- "blocks_imported": "Blokeak inportaturik! Hauek prozesatzeak denbora hartuko du.",
- "blocks_tab": "Blokeak",
+ "block_export": "Blokeatu dituzunak esportatu",
+ "block_export_button": "Esportatu blokeatutakoak csv fitxategi batera",
+ "block_import": "Blokeatu dituzunak inportatu",
+ "block_import_error": "Errorea blokeatutakoak inportatzen",
+ "blocks_imported": "Blokeatutakoak inportaturik! Hauek prozesatzeak denbora hartuko du.",
+ "blocks_tab": "Blokeatutakoak",
"btnRadius": "Botoiak",
"cBlue": "Urdina (erantzun, jarraitu)",
"cGreen": "Berdea (Bertxiotu)",
@@ -222,7 +232,9 @@
"data_import_export_tab": "Datuak Inportatu / Esportatu",
"default_vis": "Lehenetsitako ikusgaitasunak",
"delete_account": "Ezabatu kontua",
+ "discoverable": "Baimendu zure kontua kanpo bilaketa-emaitzetan eta bestelako zerbitzuetan agertzea",
"delete_account_description": "Betirako ezabatu zure kontua eta zure mezu guztiak",
+ "pad_emoji": "Zuriuneak gehitu emoji bat aukeratzen denean",
"delete_account_error": "Arazo bat gertatu da zure kontua ezabatzerakoan. Arazoa jarraitu eskero, administratzailearekin harremanetan jarri.",
"delete_account_instructions": "Idatzi zure pasahitza kontua ezabatzeko.",
"avatar_size_instruction": "Avatar irudien gomendatutako gutxieneko tamaina 150x150 pixel dira.",
@@ -254,7 +266,7 @@
"instance_default": "(lehenetsia: {value})",
"instance_default_simple": "(lehenetsia)",
"interface": "Interfazea",
- "interfaceLanguage": "Interfaze hizkuntza",
+ "interfaceLanguage": "Interfazearen hizkuntza",
"invalid_theme_imported": "Hautatutako fitxategia ez da onartutako Pleroma gaia. Ez da zure gaian aldaketarik burutu.",
"limited_availability": "Ez dago erabilgarri zure nabigatzailean",
"links": "Estekak",
@@ -277,6 +289,8 @@
"no_mutes": "Ez daude erabiltzaile mututuak",
"hide_follows_description": "Ez erakutsi nor jarraitzen ari naizen",
"hide_followers_description": "Ez erakutsi nor ari den ni jarraitzen",
+ "hide_follows_count_description": "Ez erakutsi jarraitzen ari naizen kontuen kopurua",
+ "hide_followers_count_description": "Ez erakutsi nire jarraitzaileen kontuen kopurua",
"show_admin_badge": "Erakutsi Administratzaile etiketa nire profilan",
"show_moderator_badge": "Erakutsi Moderatzaile etiketa nire profilan",
"nsfw_clickthrough": "Gaitu klika hunkigarri eranskinak ezkutatzeko",
@@ -449,7 +463,7 @@
},
"version": {
"title": "Bertsioa",
- "backend_version": "Backend Bertsio",
+ "backend_version": "Backend Bertsioa",
"frontend_version": "Frontend Bertsioa"
}
},
@@ -529,6 +543,7 @@
"follows_you": "Jarraitzen dizu!",
"its_you": "Zu zara!",
"media": "Multimedia",
+ "mention": "Aipatu",
"mute": "Isilarazi",
"muted": "Isilduta",
"per_day": "eguneko",
@@ -543,6 +558,8 @@
"unmute": "Isiltasuna kendu",
"unmute_progress": "Isiltasuna kentzen...",
"mute_progress": "Isiltzen...",
+ "hide_repeats": "Ezkutatu errepikapenak",
+ "show_repeats": "Erakutsi errpekiapenak",
"admin_menu": {
"moderation": "Moderazioa",
"grant_admin": "Administratzaile baimena",
@@ -618,6 +635,8 @@
"return_home": "Itzuli hasierara",
"not_found": "Ezin izan dugu helbide elektroniko edo erabiltzaile hori aurkitu.",
"too_many_requests": "Saiakera gehiegi burutu ditzu, saiatu berriro geroxeago.",
- "password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin."
+ "password_reset_disabled": "Pasahitza berrezartzea debekatuta dago. Mesedez, jarri harremanetan instantzia administratzailearekin.",
+ "password_reset_required": "Pasahitza berrezarri behar duzu saioa hasteko.",
+ "password_reset_required_but_mailer_is_disabled": "Pasahitza berrezarri behar duzu, baina pasahitza berrezartzeko aukera desgaituta dago. Mesedez, jarri harremanetan instantziaren administratzailearekin."
}
} \ No newline at end of file
diff --git a/src/i18n/ru.json b/src/i18n/ru.json
index 16268425..f8bcd996 100644
--- a/src/i18n/ru.json
+++ b/src/i18n/ru.json
@@ -127,6 +127,9 @@
"cGreen": "Повторить",
"cOrange": "Нравится",
"cRed": "Отменить",
+ "change_email": "Сменить email",
+ "change_email_error": "Произошла ошибка при попытке изменить email.",
+ "changed_email": "Email изменён успешно.",
"change_password": "Сменить пароль",
"change_password_error": "Произошла ошибка при попытке изменить пароль.",
"changed_password": "Пароль изменён успешно.",
@@ -169,6 +172,7 @@
"loop_video_silent_only": "Зацикливать только беззвучные видео (т.е. \"гифки\" с Mastodon)",
"name": "Имя",
"name_bio": "Имя и описание",
+ "new_email": "Новый email",
"new_password": "Новый пароль",
"notification_visibility": "Показывать уведомления",
"notification_visibility_follows": "Подписки",
diff --git a/src/main.js b/src/main.js
index 7923ffe8..a9db1cff 100644
--- a/src/main.js
+++ b/src/main.js
@@ -41,7 +41,13 @@ Vue.use(VueChatScroll)
Vue.use(VueClickOutside)
Vue.use(PortalVue)
Vue.use(VBodyScrollLock)
-Vue.use(VTooltip)
+Vue.use(VTooltip, {
+ popover: {
+ defaultTrigger: 'hover click',
+ defaultContainer: false,
+ defaultOffset: 5
+ }
+})
const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary
diff --git a/src/modules/config.js b/src/modules/config.js
index cf04d14f..78314118 100644
--- a/src/modules/config.js
+++ b/src/modules/config.js
@@ -3,8 +3,9 @@ import { setPreset, applyTheme } from '../services/style_setter/style_setter.js'
const browserLocale = (window.navigator.language || 'en').split('-')[0]
-const defaultState = {
+export const defaultState = {
colors: {},
+ // bad name: actually hides posts of muted USERS
hideMutedPosts: undefined, // instance default
collapseMessageWithSubject: undefined, // instance default
padEmoji: true,
@@ -37,11 +38,37 @@ const defaultState = {
subjectLineBehavior: undefined, // instance default
alwaysShowSubjectInput: undefined, // instance default
postContentType: undefined, // instance default
- minimalScopesMode: undefined // instance default
+ minimalScopesMode: undefined, // instance default
+ // This hides statuses filtered via a word filter
+ hideFilteredStatuses: undefined, // instance default
+ playVideosInModal: false,
+ useOneClickNsfw: false,
+ useContainFit: false,
+ hidePostStats: undefined, // instance default
+ hideUserStats: undefined // instance default
}
+// caching the instance default properties
+export const instanceDefaultProperties = Object.entries(defaultState)
+ .filter(([key, value]) => value === undefined)
+ .map(([key, value]) => key)
+
const config = {
state: defaultState,
+ getters: {
+ mergedConfig (state, getters, rootState, rootGetters) {
+ const { instance } = rootState
+ return {
+ ...state,
+ ...instanceDefaultProperties
+ .map(key => [key, state[key] === undefined
+ ? instance[key]
+ : state[key]
+ ])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
+ }
+ }
+ },
mutations: {
setOption (state, { name, value }) {
set(state, name, value)
diff --git a/src/modules/instance.js b/src/modules/instance.js
index 15280e82..7b0e0da4 100644
--- a/src/modules/instance.js
+++ b/src/modules/instance.js
@@ -1,5 +1,6 @@
import { set } from 'vue'
import { setPreset } from '../services/style_setter/style_setter.js'
+import { instanceDefaultProperties } from './config.js'
const defaultState = {
// Stuff from static/config.json and apiConfig
@@ -74,6 +75,13 @@ const instance = {
}
}
},
+ getters: {
+ instanceDefaultConfig (state) {
+ return instanceDefaultProperties
+ .map(key => [key, state[key]])
+ .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})
+ }
+ },
actions: {
setInstanceOption ({ commit, dispatch }, { name, value }) {
commit('setInstanceOption', { name, value })
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index 918065d2..f11ffdcd 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -537,6 +537,10 @@ const statuses = {
setNotificationsSilence ({ rootState, commit }, { value }) {
commit('setNotificationsSilence', { value })
},
+ fetchStatus ({ rootState, dispatch }, id) {
+ rootState.api.backendInteractor.fetchStatus({ id })
+ .then((status) => dispatch('addNewStatuses', { statuses: [status] }))
+ },
deleteStatus ({ rootState, commit }, status) {
commit('setDeleted', { status })
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
diff --git a/src/modules/users.js b/src/modules/users.js
index 0b2f8a9e..1c9ff5e8 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -60,6 +60,18 @@ const unmuteUser = (store, id) => {
.then((relationship) => store.commit('updateUserRelationship', [relationship]))
}
+const hideReblogs = (store, userId) => {
+ return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: false })
+ .then((relationship) => {
+ store.commit('updateUserRelationship', [relationship])
+ })
+}
+
+const showReblogs = (store, userId) => {
+ return store.rootState.api.backendInteractor.followUser({ id: userId, reblogs: true })
+ .then((relationship) => store.commit('updateUserRelationship', [relationship]))
+}
+
export const mutations = {
setMuted (state, { user: { id }, muted }) {
const user = state.usersObject[id]
@@ -135,6 +147,7 @@ export const mutations = {
user.muted = relationship.muting
user.statusnet_blocking = relationship.blocking
user.subscribed = relationship.subscribing
+ user.showing_reblogs = relationship.showing_reblogs
}
})
},
@@ -272,6 +285,12 @@ const users = {
unmuteUser (store, id) {
return unmuteUser(store, id)
},
+ hideReblogs (store, id) {
+ return hideReblogs(store, id)
+ },
+ showReblogs (store, id) {
+ return showReblogs(store, id)
+ },
muteUsers (store, ids = []) {
return Promise.all(ids.map(id => muteUser(store, id)))
},
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 887d7d7a..8f5eb416 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -8,6 +8,7 @@ const QVITTER_USER_NOTIFICATIONS_READ_URL = '/api/qvitter/statuses/notifications
const BLOCKS_IMPORT_URL = '/api/pleroma/blocks_import'
const FOLLOW_IMPORT_URL = '/api/pleroma/follow_import'
const DELETE_ACCOUNT_URL = '/api/pleroma/delete_account'
+const CHANGE_EMAIL_URL = '/api/pleroma/change_email'
const CHANGE_PASSWORD_URL = '/api/pleroma/change_password'
const TAG_USER_URL = '/api/pleroma/admin/users/tag'
const PERMISSION_GROUP_URL = (screenName, right) => `/api/pleroma/admin/users/${screenName}/permission_group/${right}`
@@ -16,12 +17,12 @@ const ADMIN_USERS_URL = '/api/pleroma/admin/users'
const SUGGESTIONS_URL = '/api/v1/suggestions'
const NOTIFICATION_SETTINGS_URL = '/api/pleroma/notification_settings'
-const MFA_SETTINGS_URL = '/api/pleroma/profile/mfa'
-const MFA_BACKUP_CODES_URL = '/api/pleroma/profile/mfa/backup_codes'
+const MFA_SETTINGS_URL = '/api/pleroma/accounts/mfa'
+const MFA_BACKUP_CODES_URL = '/api/pleroma/accounts/mfa/backup_codes'
-const MFA_SETUP_OTP_URL = '/api/pleroma/profile/mfa/setup/totp'
-const MFA_CONFIRM_OTP_URL = '/api/pleroma/profile/mfa/confirm/totp'
-const MFA_DISABLE_OTP_URL = '/api/pleroma/profile/mfa/totp'
+const MFA_SETUP_OTP_URL = '/api/pleroma/accounts/mfa/setup/totp'
+const MFA_CONFIRM_OTP_URL = '/api/pleroma/accounts/mfa/confirm/totp'
+const MFA_DISABLE_OTP_URL = '/api/pleroma/account/mfa/totp'
const MASTODON_LOGIN_URL = '/api/v1/accounts/verify_credentials'
const MASTODON_REGISTRATION_URL = '/api/v1/accounts'
@@ -219,10 +220,16 @@ const authHeaders = (accessToken) => {
}
}
-const followUser = ({ id, credentials }) => {
+const followUser = ({ id, credentials, ...options }) => {
let url = MASTODON_FOLLOW_URL(id)
+ const form = {}
+ if (options.reblogs !== undefined) { form['reblogs'] = options.reblogs }
return fetch(url, {
- headers: authHeaders(credentials),
+ body: JSON.stringify(form),
+ headers: {
+ ...authHeaders(credentials),
+ 'Content-Type': 'application/json'
+ },
method: 'POST'
}).then((data) => data.json())
}
@@ -685,6 +692,20 @@ const deleteAccount = ({ credentials, password }) => {
.then((response) => response.json())
}
+const changeEmail = ({ credentials, email, password }) => {
+ const form = new FormData()
+
+ form.append('email', email)
+ form.append('password', password)
+
+ return fetch(CHANGE_EMAIL_URL, {
+ body: form,
+ method: 'POST',
+ headers: authHeaders(credentials)
+ })
+ .then((response) => response.json())
+}
+
const changePassword = ({ credentials, password, newPassword, newPasswordConfirmation }) => {
const form = new FormData()
@@ -960,6 +981,7 @@ const apiService = {
importBlocks,
importFollows,
deleteAccount,
+ changeEmail,
changePassword,
settingsMFA,
mfaDisableOTP,
diff --git a/src/services/backend_interactor_service/backend_interactor_service.js b/src/services/backend_interactor_service/backend_interactor_service.js
index 3c44a10c..d6617276 100644
--- a/src/services/backend_interactor_service/backend_interactor_service.js
+++ b/src/services/backend_interactor_service/backend_interactor_service.js
@@ -31,8 +31,8 @@ const backendInteractorService = credentials => {
return apiService.fetchUserRelationship({ id, credentials })
}
- const followUser = (id) => {
- return apiService.followUser({ credentials, id })
+ const followUser = ({ id, reblogs }) => {
+ return apiService.followUser({ credentials, id, reblogs })
}
const unfollowUser = (id) => {
@@ -131,6 +131,7 @@ const backendInteractorService = credentials => {
const importFollows = (file) => apiService.importFollows({ file, credentials })
const deleteAccount = ({ password }) => apiService.deleteAccount({ credentials, password })
+ const changeEmail = ({ email, password }) => apiService.changeEmail({ credentials, email, password })
const changePassword = ({ password, newPassword, newPasswordConfirmation }) =>
apiService.changePassword({ credentials, password, newPassword, newPasswordConfirmation })
@@ -195,6 +196,7 @@ const backendInteractorService = credentials => {
importBlocks,
importFollows,
deleteAccount,
+ changeEmail,
changePassword,
fetchSettingsMFA,
generateMfaBackupCodes,
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 67664af8..5f45660d 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -69,6 +69,7 @@ export const parseUser = (data) => {
output.following = relationship.following
output.statusnet_blocking = relationship.blocking
output.muted = relationship.muting
+ output.showing_reblogs = relationship.showing_reblogs
output.subscribed = relationship.subscribing
}
diff --git a/src/services/follow_manipulate/follow_manipulate.js b/src/services/follow_manipulate/follow_manipulate.js
index d82ce593..598cb5f7 100644
--- a/src/services/follow_manipulate/follow_manipulate.js
+++ b/src/services/follow_manipulate/follow_manipulate.js
@@ -14,7 +14,7 @@ const fetchUser = (attempt, user, store) => new Promise((resolve, reject) => {
})
export const requestFollow = (user, store) => new Promise((resolve, reject) => {
- store.state.api.backendInteractor.followUser(user.id)
+ store.state.api.backendInteractor.followUser({ id: user.id })
.then((updated) => {
store.commit('updateUserRelationship', [updated])
diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js
index b6c4cf80..47008026 100644
--- a/src/services/notifications_fetcher/notifications_fetcher.service.js
+++ b/src/services/notifications_fetcher/notifications_fetcher.service.js
@@ -8,11 +8,10 @@ const update = ({ store, notifications, older }) => {
const fetchAndUpdate = ({ store, credentials, older = false }) => {
const args = { credentials }
+ const { getters } = store
const rootState = store.rootState || store.state
const timelineData = rootState.statuses.notifications
- const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined'
- ? rootState.instance.hideMutedPosts
- : rootState.config.hideMutedPosts
+ const hideMutedPosts = getters.mergedConfig.hideMutedPosts
args['withMuted'] = !hideMutedPosts
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index f72688f8..9eb30c2d 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -15,13 +15,21 @@ const update = ({ store, statuses, timeline, showImmediately, userId }) => {
})
}
-const fetchAndUpdate = ({ store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until }) => {
+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 { getters } = store
const timelineData = rootState.statuses.timelines[camelCase(timeline)]
- const hideMutedPosts = typeof rootState.config.hideMutedPosts === 'undefined'
- ? rootState.instance.hideMutedPosts
- : rootState.config.hideMutedPosts
+ const hideMutedPosts = getters.mergedConfig.hideMutedPosts
if (older) {
args['until'] = until || timelineData.minId