aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
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