Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
154 KB
Referenced Files
None
Subscribers
None
diff --git a/src/App.js b/src/App.js
index 61b5eec1c6..4d9d50d439 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,123 +1,128 @@
import UserPanel from './components/user_panel/user_panel.vue'
import NavPanel from './components/nav_panel/nav_panel.vue'
import Notifications from './components/notifications/notifications.vue'
import SearchBar from './components/search_bar/search_bar.vue'
import InstanceSpecificPanel from './components/instance_specific_panel/instance_specific_panel.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue'
import ChatPanel from './components/chat_panel/chat_panel.vue'
+import SettingsModal from './components/settings_modal/settings_modal.vue'
import MediaModal from './components/media_modal/media_modal.vue'
import SideDrawer from './components/side_drawer/side_drawer.vue'
import MobilePostStatusButton from './components/mobile_post_status_button/mobile_post_status_button.vue'
import MobileNav from './components/mobile_nav/mobile_nav.vue'
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
import { windowWidth } from './services/window_utils/window_utils'
export default {
name: 'app',
components: {
UserPanel,
NavPanel,
Notifications,
SearchBar,
InstanceSpecificPanel,
FeaturesPanel,
WhoToFollowPanel,
ChatPanel,
MediaModal,
SideDrawer,
MobilePostStatusButton,
MobileNav,
+ SettingsModal,
UserReportingModal,
PostStatusModal
},
data: () => ({
mobileActivePanel: 'timeline',
searchBarHidden: true,
supportsMask: window.CSS && window.CSS.supports && (
window.CSS.supports('mask-size', 'contain') ||
window.CSS.supports('-webkit-mask-size', 'contain') ||
window.CSS.supports('-moz-mask-size', 'contain') ||
window.CSS.supports('-ms-mask-size', 'contain') ||
window.CSS.supports('-o-mask-size', 'contain')
)
}),
created () {
// Load the locale from the storage
this.$i18n.locale = this.$store.getters.mergedConfig.interfaceLanguage
window.addEventListener('resize', this.updateMobileState)
},
destroyed () {
window.removeEventListener('resize', this.updateMobileState)
},
computed: {
currentUser () { return this.$store.state.users.currentUser },
background () {
return this.currentUser.background_image || this.$store.state.instance.background
},
enableMask () { return this.supportsMask && this.$store.state.instance.logoMask },
logoStyle () {
return {
'visibility': this.enableMask ? 'hidden' : 'visible'
}
},
logoMaskStyle () {
return this.enableMask ? {
'mask-image': `url(${this.$store.state.instance.logo})`
} : {
'background-color': this.enableMask ? '' : 'transparent'
}
},
logoBgStyle () {
return Object.assign({
'margin': `${this.$store.state.instance.logoMargin} 0`,
opacity: this.searchBarHidden ? 1 : 0
}, this.enableMask ? {} : {
'background-color': this.enableMask ? '' : 'transparent'
})
},
logo () { return this.$store.state.instance.logo },
bgStyle () {
return {
'background-image': `url(${this.background})`
}
},
bgAppStyle () {
return {
'--body-background-image': `url(${this.background})`
}
},
sitename () { return this.$store.state.instance.name },
chat () { return this.$store.state.chat.channel.state === 'joined' },
hideSitename () { return this.$store.state.instance.hideSitename },
suggestionsEnabled () { return this.$store.state.instance.suggestionsEnabled },
showInstanceSpecificPanel () {
return this.$store.state.instance.showInstanceSpecificPanel &&
!this.$store.getters.mergedConfig.hideISP &&
this.$store.state.instance.instanceSpecificPanelContent
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
isMobileLayout () { return this.$store.state.interface.mobileLayout },
privateMode () { return this.$store.state.instance.private }
},
methods: {
scrollToTop () {
window.scrollTo(0, 0)
},
logout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout')
},
onSearchBarToggled (hidden) {
this.searchBarHidden = hidden
},
+ openSettingsModal () {
+ this.$store.dispatch('openSettingsModal')
+ },
updateMobileState () {
const mobileLayout = windowWidth() <= 800
const changed = mobileLayout !== this.isMobileLayout
if (changed) {
this.$store.dispatch('setMobileLayout', mobileLayout)
}
}
}
}
diff --git a/src/App.scss b/src/App.scss
index 89aa321551..7db9461c45 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,971 +1,974 @@
@import './_variables.scss';
#app {
min-height: 100vh;
max-width: 100%;
overflow: hidden;
}
.app-bg-wrapper {
position: fixed;
z-index: -1;
height: 100%;
left: 0;
right: -20px;
background-size: cover;
background-repeat: no-repeat;
background-position: 0 50%;
}
i[class^='icon-'] {
user-select: none;
}
h4 {
margin: 0;
}
#content {
box-sizing: border-box;
padding-top: 60px;
margin: auto;
min-height: 100vh;
max-width: 980px;
align-content: flex-start;
}
.underlay {
background-color: rgba(0,0,0,0.15);
background-color: var(--underlay, rgba(0,0,0,0.15));
}
.text-center {
text-align: center;
}
html {
font-size: 14px;
}
body {
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
margin: 0;
color: $fallback--text;
color: var(--text, $fallback--text);
max-width: 100vw;
overflow-x: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
&.hidden {
display: none;
}
}
a {
text-decoration: none;
color: $fallback--link;
color: var(--link, $fallback--link);
}
button {
user-select: none;
color: $fallback--text;
color: var(--btnText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btn, $fallback--fg);
border: none;
border-radius: $fallback--btnRadius;
border-radius: var(--btnRadius, $fallback--btnRadius);
cursor: pointer;
box-shadow: $fallback--buttonShadow;
box-shadow: var(--buttonShadow);
font-size: 14px;
font-family: sans-serif;
font-family: var(--interfaceFont, sans-serif);
i[class*=icon-] {
color: $fallback--text;
color: var(--btnText, $fallback--text);
}
&::-moz-focus-inner {
border: none;
}
&:hover {
box-shadow: 0px 0px 4px rgba(255, 255, 255, 0.3);
box-shadow: var(--buttonHoverShadow);
}
&:active {
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnPressed, $fallback--fg);
i {
color: $fallback--text;
color: var(--btnPressedText, $fallback--text);
}
}
&:disabled {
cursor: not-allowed;
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnDisabled, $fallback--fg);
i {
color: $fallback--text;
color: var(--btnDisabledText, $fallback--text);
}
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggled, $fallback--fg);
box-shadow: 0px 0px 4px 0px rgba(255, 255, 255, 0.3), 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset;
box-shadow: var(--buttonPressedShadow);
i {
color: $fallback--text;
color: var(--btnToggledText, $fallback--text);
}
}
&.danger {
// TODO: add better color variable
color: $fallback--text;
color: var(--alertErrorPanelText, $fallback--text);
background-color: $fallback--alertError;
background-color: var(--alertError, $fallback--alertError);
}
}
input, textarea, .select, .input {
&.unstyled {
border-radius: 0;
background: none;
box-shadow: none;
height: unset;
}
border: none;
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
box-shadow: 0px 1px 0px 0px rgba(0, 0, 0, 0.2) inset, 0px -1px 0px 0px rgba(255, 255, 255, 0.2) inset, 0px 0px 2px 0px rgba(0, 0, 0, 1) inset;
box-shadow: var(--inputShadow);
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
color: $fallback--lightText;
color: var(--inputText, $fallback--lightText);
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
font-size: 14px;
margin: 0;
box-sizing: border-box;
display: inline-block;
position: relative;
height: 28px;
line-height: 16px;
hyphens: none;
padding: 8px .5em;
&.select {
padding: 0;
}
&:disabled, &[disabled=disabled] {
cursor: not-allowed;
opacity: 0.5;
}
.icon-down-open {
position: absolute;
top: 0;
bottom: 0;
right: 5px;
height: 100%;
color: $fallback--text;
color: var(--inputText, $fallback--text);
line-height: 28px;
z-index: 0;
pointer-events: none;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: transparent;
border: none;
color: $fallback--text;
color: var(--inputText, --text, $fallback--text);
margin: 0;
padding: 0 2em 0 .2em;
font-family: sans-serif;
font-family: var(--inputFont, sans-serif);
font-size: 14px;
width: 100%;
z-index: 1;
height: 28px;
line-height: 16px;
}
&[type=range] {
background: none;
border: none;
margin: 0;
box-shadow: none;
flex: 1;
}
&[type=radio] {
display: none;
&:checked + label::before {
box-shadow: 0px 0px 2px black inset, 0px 0px 0px 4px $fallback--fg inset;
box-shadow: var(--inputShadow), 0px 0px 0px 4px var(--fg, $fallback--fg) inset;
background-color: var(--accent, $fallback--link);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: .5;
}
}
+ label::before {
flex-shrink: 0;
display: inline-block;
content: '';
transition: box-shadow 200ms;
width: 1.1em;
height: 1.1em;
border-radius: 100%; // Radio buttons should always be circle
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
margin-right: .5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
line-height: 1.1em;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
box-sizing: border-box;
}
}
&[type=checkbox] {
display: none;
&:checked + label::before {
color: $fallback--text;
color: var(--inputText, $fallback--text);
}
&:disabled {
&,
& + label,
& + label::before {
opacity: .5;
}
}
+ label::before {
flex-shrink: 0;
display: inline-block;
content: '✔';
transition: color 200ms;
width: 1.1em;
height: 1.1em;
border-radius: $fallback--checkboxRadius;
border-radius: var(--checkboxRadius, $fallback--checkboxRadius);
box-shadow: 0px 0px 2px black inset;
box-shadow: var(--inputShadow);
margin-right: .5em;
background-color: $fallback--fg;
background-color: var(--input, $fallback--fg);
vertical-align: top;
text-align: center;
line-height: 1.1em;
font-size: 1.1em;
box-sizing: border-box;
color: transparent;
overflow: hidden;
box-sizing: border-box;
}
}
}
option {
color: $fallback--text;
color: var(--text, $fallback--text);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
}
.hide-number-spinner {
-moz-appearance: textfield;
&[type=number]::-webkit-inner-spin-button,
&[type=number]::-webkit-outer-spin-button {
opacity: 0;
display: none;
}
}
i[class*=icon-] {
color: $fallback--icon;
color: var(--icon, $fallback--icon)
}
.btn-block {
display: block;
width: 100%;
}
.btn-group {
position: relative;
display: inline-flex;
vertical-align: middle;
button {
position: relative;
flex: 1 1 auto;
&:not(:last-child) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
&:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0 10px 0 10px;
}
.item {
flex: 1;
line-height: 50px;
height: 50px;
overflow: hidden;
display: flex;
flex-wrap: wrap;
.nav-icon {
margin-left: 0.4em;
}
&.right {
justify-content: flex-end;
}
}
.auto-size {
flex: 1
}
.nav-bar {
padding: 0;
width: 100%;
align-items: center;
position: fixed;
height: 50px;
box-sizing: border-box;
button {
&, i[class*=icon-] {
color: $fallback--text;
color: var(--btnTopBarText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedTopBar, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedTopBarText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledTopBarText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledTopBarText, $fallback--text);
background-color: $fallback--fg;
background-color: var(--btnToggledTopBar, $fallback--fg)
}
}
.logo {
display: flex;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
align-items: stretch;
justify-content: center;
flex: 0 0 auto;
z-index: -1;
transition: opacity;
transition-timing-function: ease-out;
transition-duration: 100ms;
.mask {
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $fallback--fg;
background-color: var(--topBarText, $fallback--fg);
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
img {
height: 100%;
object-fit: contain;
display: block;
flex: 0;
}
}
.inner-nav {
position: relative;
margin: auto;
box-sizing: border-box;
padding-left: 10px;
padding-right: 10px;
display: flex;
align-items: center;
flex-basis: 970px;
height: 50px;
a, a i {
color: $fallback--link;
color: var(--topBarLink, $fallback--link);
}
}
}
main-router {
flex: 1;
}
.status.compact {
color: rgba(0, 0, 0, 0.42);
font-weight: 300;
p {
margin: 0;
font-size: 0.8em
}
}
/* Panel */
.panel {
display: flex;
position: relative;
flex-direction: column;
margin: 0.5em;
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
&::after, & {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
&::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
pointer-events: none;
box-shadow: 1px 1px 4px rgba(0,0,0,.6);
box-shadow: var(--panelShadow);
}
}
.panel-body:empty::before {
content: "¯\\_(ツ)_/¯"; // Could use words but it'd require translations
display: block;
margin: 1em;
text-align: center;
}
.panel-heading {
display: flex;
flex: none;
border-radius: $fallback--panelRadius $fallback--panelRadius 0 0;
border-radius: var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius) 0 0;
background-size: cover;
padding: .6em .6em;
text-align: left;
line-height: 28px;
color: var(--panelText);
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
align-items: baseline;
box-shadow: var(--panelHeaderShadow);
.title {
flex: 1 0 auto;
font-size: 1.3em;
}
.faint {
background-color: transparent;
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faintLink, $fallback--faint);
}
.alert {
white-space: nowrap;
text-overflow: ellipsis;
overflow-x: hidden;
}
button {
flex-shrink: 0;
}
button, .alert {
// height: 100%;
line-height: 21px;
min-height: 0;
box-sizing: border-box;
margin: 0;
margin-left: .25em;
min-width: 1px;
align-self: stretch;
}
button {
&, i[class*=icon-] {
color: $fallback--text;
color: var(--btnPanelText, $fallback--text);
}
&:active {
background-color: $fallback--fg;
background-color: var(--btnPressedPanel, $fallback--fg);
color: $fallback--text;
color: var(--btnPressedPanelText, $fallback--text);
}
&:disabled {
color: $fallback--text;
color: var(--btnDisabledPanelText, $fallback--text);
}
&.toggled {
color: $fallback--text;
color: var(--btnToggledPanelText, $fallback--text);
}
}
a {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
}
}
.panel-heading.stub {
border-radius: $fallback--panelRadius;
border-radius: var(--panelRadius, $fallback--panelRadius);
}
.panel-footer {
border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius;
border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius);
.faint {
color: $fallback--faint;
color: var(--panelFaint, $fallback--faint);
}
a {
color: $fallback--link;
color: var(--panelLink, $fallback--link)
}
}
.panel-body > p {
line-height: 18px;
padding: 1em;
margin: 0;
}
.container > * {
min-width: 0px;
}
.fa {
color: grey;
}
nav {
z-index: 1000;
color: var(--topBarText);
background-color: $fallback--fg;
background-color: var(--topBar, $fallback--fg);
color: $fallback--faint;
color: var(--faint, $fallback--faint);
box-shadow: 0px 0px 4px rgba(0,0,0,.6);
box-shadow: var(--topBarShadow);
}
.fade-enter-active, .fade-leave-active {
transition: opacity .2s
}
.fade-enter, .fade-leave-active {
opacity: 0
}
.main {
flex-basis: 50%;
flex-grow: 1;
flex-shrink: 1;
}
.sidebar-bounds {
flex: 0;
flex-basis: 35%;
}
.sidebar-flexer {
flex: 1;
flex-basis: 345px;
width: 365px;
}
.mobile-shown {
display: none;
}
@media all and (min-width: 800px) {
body {
overflow-y: scroll;
}
.sidebar-bounds {
overflow: hidden;
max-height: 100vh;
width: 345px;
position: fixed;
margin-top: -10px;
.sidebar-scroller {
height: 96vh;
width: 365px;
padding-top: 10px;
padding-right: 50px;
overflow-x: hidden;
overflow-y: scroll;
}
.sidebar {
width: 345px;
}
}
.sidebar-flexer {
max-height: 96vh;
flex-shrink: 0;
flex-grow: 0;
}
}
.badge {
display: inline-block;
border-radius: 99px;
min-width: 22px;
max-width: 22px;
min-height: 22px;
max-height: 22px;
font-size: 15px;
line-height: 22px;
text-align: center;
vertical-align: middle;
white-space: nowrap;
padding: 0;
&.badge-notification {
background-color: $fallback--cRed;
background-color: var(--badgeNotification, $fallback--cRed);
color: white;
color: var(--badgeNotificationText, white);
}
}
.alert {
margin: 0.35em;
padding: 0.25em;
border-radius: $fallback--tooltipRadius;
border-radius: var(--tooltipRadius, $fallback--tooltipRadius);
min-height: 28px;
line-height: 28px;
&.error {
background-color: $fallback--alertError;
background-color: var(--alertError, $fallback--alertError);
color: $fallback--text;
color: var(--alertErrorText, $fallback--text);
.panel-heading & {
color: $fallback--text;
color: var(--alertErrorPanelText, $fallback--text);
}
}
&.warning {
background-color: $fallback--alertWarning;
background-color: var(--alertWarning, $fallback--alertWarning);
color: $fallback--text;
color: var(--alertWarningText, $fallback--text);
.panel-heading & {
color: $fallback--text;
color: var(--alertWarningPanelText, $fallback--text);
}
}
}
.faint {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
}
.faint-link {
color: $fallback--faint;
color: var(--faint, $fallback--faint);
&:hover {
text-decoration: underline;
}
}
@media all and (min-width: 800px) {
.logo {
opacity: 1 !important;
}
}
.item.right {
text-align: right;
}
.visibility-notice {
padding: .5em;
border: 1px solid $fallback--faint;
border: 1px solid var(--faint, $fallback--faint);
border-radius: $fallback--inputRadius;
border-radius: var(--inputRadius, $fallback--inputRadius);
}
.notice-dismissible {
padding-right: 4rem;
position: relative;
.dismiss {
position: absolute;
top: 0;
right: 0;
padding: .5em;
color: inherit;
}
}
.button-icon {
font-size: 1.2em;
}
@keyframes shakeError {
0% {
transform: translateX(0);
}
15% {
transform: translateX(0.375rem);
}
30% {
transform: translateX(-0.375rem);
}
45% {
transform: translateX(0.375rem);
}
60% {
transform: translateX(-0.375rem);
}
75% {
transform: translateX(0.375rem);
}
90% {
transform: translateX(-0.375rem);
}
100% {
transform: translateX(0);
}
}
@media all and (max-width: 800px) {
.mobile-hidden {
display: none;
}
.panel-switcher {
display: flex;
}
.container {
padding: 0;
}
.panel {
margin: 0.5em 0 0.5em 0;
}
.menu-button {
display: block;
margin-right: 0.8em;
}
}
+// DELETE
.setting-item {
border-bottom: 2px solid var(--fg, $fallback--fg);
margin: 1em 1em 1.4em;
padding-bottom: 1.4em;
> div {
margin-bottom: .5em;
&:last-child {
margin-bottom: 0;
}
}
&:last-child {
border-bottom: none;
padding-bottom: 0;
margin-bottom: 1em;
}
select {
min-width: 10em;
}
textarea {
width: 100%;
max-width: 100%;
height: 100px;
}
.unavailable,
.unavailable i {
color: var(--cRed, $fallback--cRed);
color: $fallback--cRed;
}
.btn {
min-height: 28px;
min-width: 10em;
padding: 0 2em;
}
.number-input {
max-width: 6em;
}
}
+// DELETE
+
.select-multiple {
display: flex;
.option-list {
margin: 0;
padding-left: .5em;
}
}
.setting-list,
.option-list{
list-style-type: none;
padding-left: 2em;
li {
margin-bottom: 0.5em;
}
.suboptions {
margin-top: 0.3em
}
}
.login-hint {
text-align: center;
@media all and (min-width: 801px) {
display: none;
}
a {
display: inline-block;
padding: 1em 0px;
width: 100%;
}
}
.btn.btn-default {
min-height: 28px;
}
.animate-spin {
animation: spin 2s infinite linear;
display: inline-block;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
.new-status-notification {
position:relative;
margin-top: -1px;
font-size: 1.1em;
border-width: 1px 0 0 0;
border-style: solid;
border-color: var(--border, $fallback--border);
padding: 10px;
z-index: 1;
background-color: $fallback--fg;
background-color: var(--panel, $fallback--fg);
}
diff --git a/src/App.vue b/src/App.vue
index ff62fc514d..db3f981ffc 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,130 +1,131 @@
<template>
<div
id="app"
:style="bgAppStyle"
>
<div
id="app_bg_wrapper"
class="app-bg-wrapper"
:style="bgStyle"
/>
<MobileNav v-if="isMobileLayout" />
<nav
v-else
id="nav"
class="nav-bar container"
@click="scrollToTop()"
>
<div class="inner-nav">
<div
class="logo"
:style="logoBgStyle"
>
<div
class="mask"
:style="logoMaskStyle"
/>
<img
:src="logo"
:style="logoStyle"
>
</div>
<div class="item">
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
>
{{ sitename }}
</router-link>
</div>
<div class="item right">
<search-bar
v-if="currentUser || !privateMode"
class="nav-icon mobile-hidden"
@toggled="onSearchBarToggled"
@click.stop.native
/>
- <router-link
+ <a
class="mobile-hidden"
- :to="{ name: 'settings'}"
+ @click.stop="openSettingsModal"
>
<i
class="button-icon icon-cog nav-icon"
:title="$t('nav.preferences')"
/>
- </router-link>
+ </a>
<a
v-if="currentUser && currentUser.role === 'admin'"
href="/pleroma/admin/#/login-pleroma"
class="mobile-hidden"
target="_blank"
><i
class="button-icon icon-gauge nav-icon"
:title="$t('nav.administration')"
/></a>
<a
v-if="currentUser"
href="#"
class="mobile-hidden"
@click.prevent="logout"
><i
class="button-icon icon-logout nav-icon"
:title="$t('login.logout')"
/></a>
</div>
</div>
</nav>
<div
id="content"
class="container underlay"
>
<div class="sidebar-flexer mobile-hidden">
<div class="sidebar-bounds">
<div class="sidebar-scroller">
<div class="sidebar">
<user-panel />
<div v-if="!isMobileLayout">
<nav-panel />
<instance-specific-panel v-if="showInstanceSpecificPanel" />
<features-panel v-if="!currentUser && showFeaturesPanel" />
<who-to-follow-panel v-if="currentUser && suggestionsEnabled" />
<notifications v-if="currentUser" />
</div>
</div>
</div>
</div>
</div>
<div class="main">
<div
v-if="!currentUser"
class="login-hint panel panel-default"
>
<router-link
:to="{ name: 'login' }"
class="panel-body"
>
{{ $t("login.hint") }}
</router-link>
</div>
<transition name="fade">
<router-view />
</transition>
</div>
<media-modal />
</div>
<chat-panel
v-if="currentUser && chat"
:floating="true"
class="floating-chat mobile-hidden"
/>
<MobilePostStatusButton />
<UserReportingModal />
<PostStatusModal />
+ <SettingsModal />
<portal-target name="modal" />
</div>
</template>
<script src="./App.js"></script>
<style lang="scss" src="./App.scss"></style>
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 0ad12bb118..ff2d4eaa8b 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
@@ -1,93 +1,93 @@
import { debounce } from 'lodash'
const MobilePostStatusButton = {
data () {
return {
hidden: false,
scrollingDown: false,
inputActive: false,
oldScrollPos: 0,
amountScrolled: 0
}
},
created () {
if (this.autohideFloatingPostButton) {
this.activateFloatingPostButtonAutohide()
}
window.addEventListener('resize', this.handleOSK)
},
destroyed () {
if (this.autohideFloatingPostButton) {
this.deactivateFloatingPostButtonAutohide()
}
window.removeEventListener('resize', this.handleOSK)
},
computed: {
isLoggedIn () {
return !!this.$store.state.users.currentUser
},
isHidden () {
return this.autohideFloatingPostButton && (this.hidden || this.inputActive)
},
autohideFloatingPostButton () {
return !!this.$store.getters.mergedConfig.autohideFloatingPostButton
}
},
watch: {
autohideFloatingPostButton: function (isEnabled) {
if (isEnabled) {
this.activateFloatingPostButtonAutohide()
} else {
this.deactivateFloatingPostButtonAutohide()
}
}
},
methods: {
activateFloatingPostButtonAutohide () {
window.addEventListener('scroll', this.handleScrollStart)
window.addEventListener('scroll', this.handleScrollEnd)
},
deactivateFloatingPostButtonAutohide () {
window.removeEventListener('scroll', this.handleScrollStart)
window.removeEventListener('scroll', this.handleScrollEnd)
},
openPostForm () {
- this.$store.dispatch('openPostStatusModal')
+ this.$store.dispatch('openSettingsModal')
},
handleOSK () {
// This is a big hack: we're guessing from changed window sizes if the
// on-screen keyboard is active or not. This is only really important
// for phones in portrait mode and it's more important to show the button
// in normal scenarios on all phones, than it is to hide it when the
// keyboard is active.
// Guesswork based on https://www.mydevice.io/#compare-devices
// for example, iphone 4 and android phones from the same time period
const smallPhone = window.innerWidth < 350
const smallPhoneKbOpen = smallPhone && window.innerHeight < 345
const biggerPhone = !smallPhone && window.innerWidth < 450
const biggerPhoneKbOpen = biggerPhone && window.innerHeight < 560
if (smallPhoneKbOpen || biggerPhoneKbOpen) {
this.inputActive = true
} else {
this.inputActive = false
}
},
handleScrollStart: debounce(function () {
if (window.scrollY > this.oldScrollPos) {
this.hidden = true
} else {
this.hidden = false
}
this.oldScrollPos = window.scrollY
}, 100, { leading: true, trailing: false }),
handleScrollEnd: debounce(function () {
this.hidden = false
this.oldScrollPos = window.scrollY
}, 100, { leading: false, trailing: true })
}
}
export default MobilePostStatusButton
diff --git a/src/components/post_status_modal/post_status_modal.js b/src/components/post_status_modal/post_status_modal.js
index b44354db19..be945400df 100644
--- a/src/components/post_status_modal/post_status_modal.js
+++ b/src/components/post_status_modal/post_status_modal.js
@@ -1,51 +1,48 @@
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,
Modal
},
data () {
return {
resettingForm: false
}
},
computed: {
- isLoggedIn () {
- return !!this.$store.state.users.currentUser
- },
modalActivated () {
return this.$store.state.postStatus.modalActivated
},
isFormVisible () {
return this.isLoggedIn && !this.resettingForm && this.modalActivated
},
params () {
return this.$store.state.postStatus.params || {}
}
},
watch: {
params (newVal, oldVal) {
if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) {
this.resettingForm = true
this.$nextTick(() => {
this.resettingForm = false
})
}
},
isFormVisible (val) {
if (val) {
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
}
}
},
methods: {
closeModal () {
this.$store.dispatch('closePostStatusModal')
}
}
}
export default PostStatusModal
diff --git a/src/components/post_status_modal/post_status_modal.vue b/src/components/post_status_modal/post_status_modal.vue
index dbcd321eb4..07c58f74d8 100644
--- a/src/components/post_status_modal/post_status_modal.vue
+++ b/src/components/post_status_modal/post_status_modal.vue
@@ -1,39 +1,38 @@
<template>
<Modal
- v-if="isLoggedIn && !resettingForm"
:is-open="modalActivated"
class="post-form-modal-view"
@backdropClicked="closeModal"
>
<div class="post-form-modal-panel panel">
<div class="panel-heading">
{{ $t('post_status.new_status') }}
</div>
<PostStatusForm
class="panel-body"
v-bind="params"
@posted="closeModal"
/>
</div>
</Modal>
</template>
<script src="./post_status_modal.js"></script>
<style lang="scss">
.modal-view.post-form-modal-view {
align-items: flex-start;
}
.post-form-modal-panel {
flex-shrink: 0;
margin-top: 25%;
margin-bottom: 2em;
width: 100%;
max-width: 700px;
@media (orientation: landscape) {
margin-top: 8%;
}
}
</style>
diff --git a/src/components/settings_modal/settings_modal.js b/src/components/settings_modal/settings_modal.js
new file mode 100644
index 0000000000..1f4c038fbe
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.js
@@ -0,0 +1,39 @@
+import Modal from '../modal/modal.vue'
+import TabSwitcher from '../tab_switcher/tab_switcher.js'
+
+import Profile from './tabs/profile.vue'
+import Security from './tabs/security.vue'
+import Notifications from './tabs/notifications.vue'
+import DataImportExport from './tabs/data_import_export.vue'
+import MutesAndBlocks from './tabs/mutes_and_blocks.vue'
+
+const SettingsModal = {
+ components: {
+ Modal,
+ TabSwitcher,
+ Profile,
+ Security,
+ Notifications,
+ DataImportExport,
+ MutesAndBlocks
+ },
+ data () {
+ return {
+ resettingForm: false
+ }
+ },
+ computed: {
+ isLoggedIn () {
+ return !!this.$store.state.users.currentUser
+ },
+ modalActivated () {
+ return this.$store.state.interface.settingsModalState !== 'hidden'
+ }
+ },
+ watch: {
+ },
+ methods: {
+ }
+}
+
+export default SettingsModal
diff --git a/src/components/settings_modal/settings_modal.scss b/src/components/settings_modal/settings_modal.scss
new file mode 100644
index 0000000000..8cea52d21c
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.scss
@@ -0,0 +1,59 @@
+@import '../../_variables.scss';
+.settings-modal {
+ .settings_tab-switcher {
+ height: 100%;
+ }
+ .settings-modal-panel {
+ width: 1000px;
+ max-width: 90vw;
+ height: 90vh;
+ }
+ .panel-body {
+ overflow-y: hidden;
+ }
+ .setting-item {
+ border-bottom: 2px solid var(--fg, $fallback--fg);
+ margin: 1em 1em 1.4em;
+ padding-bottom: 1.4em;
+
+ > div {
+ margin-bottom: .5em;
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+
+ &:last-child {
+ border-bottom: none;
+ padding-bottom: 0;
+ margin-bottom: 1em;
+ }
+
+ select {
+ min-width: 10em;
+ }
+
+
+ textarea {
+ width: 100%;
+ max-width: 100%;
+ height: 100px;
+ }
+
+ .unavailable,
+ .unavailable i {
+ color: var(--cRed, $fallback--cRed);
+ color: $fallback--cRed;
+ }
+
+ .btn {
+ min-height: 28px;
+ min-width: 10em;
+ padding: 0 2em;
+ }
+
+ .number-input {
+ max-width: 6em;
+ }
+ }
+}
diff --git a/src/components/settings_modal/settings_modal.vue b/src/components/settings_modal/settings_modal.vue
new file mode 100644
index 0000000000..9e35d3f679
--- /dev/null
+++ b/src/components/settings_modal/settings_modal.vue
@@ -0,0 +1,31 @@
+<template>
+<Modal
+ v-if="isLoggedIn && !resettingForm"
+ :is-open="modalActivated"
+ class="settings-modal"
+ >
+ <div class="settings-modal-panel panel">
+ <div class="panel-heading">
+ {{ $t('settings.settings') }}
+ </div>
+ <div class="panel-body">
+ <tab-switcher
+ class="settings_tab-switcher"
+ :sideTabBar="true"
+ :scrollableTabs="true"
+ ref="tabSwitcher"
+ >
+ <div :label="$t('settings.profile_tab')"><Profile /></div>
+ <div :label="$t('settings.security_tab')"><Security /></div>
+ <div :label="$t('settings.notifications')"><Notifications /></div>
+ <div :label="$t('settings.data_import_export_tab')"><DataImportExport /></div>
+ <div :label="$t('settings.mutes_and_blocks')"><MutesAndBlocks /></div>
+ </tab-switcher>
+ </div>
+ </div>
+</Modal>
+</template>
+
+<script src="./settings_modal.js"></script>
+
+<style src="./settings_modal.scss" lang="scss"></style>
diff --git a/src/components/settings_modal/tabs/data_import_export.js b/src/components/settings_modal/tabs/data_import_export.js
new file mode 100644
index 0000000000..f68d12e921
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export.js
@@ -0,0 +1,65 @@
+import Importer from '../../importer/importer.vue'
+import Exporter from '../../exporter/exporter.vue'
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const DataImportExport = {
+ data () {
+ return {
+ activeTab: 'profile',
+ newDomainToMute: ''
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
+ components: {
+ Importer,
+ Exporter,
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ }
+ },
+ methods: {
+ getFollowsContent () {
+ return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
+ .then(this.generateExportableUsersContent)
+ },
+ getBlocksContent () {
+ return this.$store.state.api.backendInteractor.fetchBlocks()
+ .then(this.generateExportableUsersContent)
+ },
+ importFollows (file) {
+ return this.$store.state.api.backendInteractor.importFollows({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importBlocks (file) {
+ return this.$store.state.api.backendInteractor.importBlocks({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ generateExportableUsersContent (users) {
+ // Get addresses
+ return users.map((user) => {
+ // check is it's a local user
+ if (user && user.is_local) {
+ // append the instance address
+ // eslint-disable-next-line no-undef
+ return user.screen_name + '@' + location.hostname
+ }
+ return user.screen_name
+ }).join('\n')
+ }
+ }
+}
+
+export default DataImportExport
diff --git a/src/components/settings_modal/tabs/data_import_export.vue b/src/components/settings_modal/tabs/data_import_export.vue
new file mode 100644
index 0000000000..464df6d398
--- /dev/null
+++ b/src/components/settings_modal/tabs/data_import_export.vue
@@ -0,0 +1,43 @@
+<template>
+<div
+ :label="$t('settings.data_import_export_tab')"
+ >
+ <div class="setting-item">
+ <h2>{{ $t('settings.follow_import') }}</h2>
+ <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
+ <Importer
+ :submit-handler="importFollows"
+ :success-message="$t('settings.follows_imported')"
+ :error-message="$t('settings.follow_import_error')"
+ />
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.follow_export') }}</h2>
+ <Exporter
+ :get-content="getFollowsContent"
+ filename="friends.csv"
+ :export-button-label="$t('settings.follow_export_button')"
+ />
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.block_import') }}</h2>
+ <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
+ <Importer
+ :submit-handler="importBlocks"
+ :success-message="$t('settings.blocks_imported')"
+ :error-message="$t('settings.block_import_error')"
+ />
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.block_export') }}</h2>
+ <Exporter
+ :get-content="getBlocksContent"
+ filename="blocks.csv"
+ :export-button-label="$t('settings.block_export_button')"
+ />
+ </div>
+</div>
+</template>
+
+<script src="./data_import_export.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.js b/src/components/settings_modal/tabs/mutes_and_blocks.js
new file mode 100644
index 0000000000..51895ddc9c
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks.js
@@ -0,0 +1,124 @@
+import get from 'lodash/get'
+import map from 'lodash/map'
+import reject from 'lodash/reject'
+import Autosuggest from '../../autosuggest/autosuggest.vue'
+import TabSwitcher from '../../tab_switcher/tab_switcher.js'
+import BlockCard from '../../block_card/block_card.vue'
+import MuteCard from '../../mute_card/mute_card.vue'
+import DomainMuteCard from '../../domain_mute_card/domain_mute_card.vue'
+import SelectableList from '../../selectable_list/selectable_list.vue'
+import ProgressButton from '../../progress_button/progress_button.vue'
+import withSubscription from '../../../hocs/with_subscription/with_subscription'
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const BlockList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchBlocks'),
+ select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
+ childPropName: 'items'
+})(SelectableList)
+
+const MuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
+ childPropName: 'items'
+})(SelectableList)
+
+const DomainMuteList = withSubscription({
+ fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
+ select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
+ childPropName: 'items'
+})(SelectableList)
+
+const MutesAndBlocks = {
+ data () {
+ return {
+ activeTab: 'profile',
+ newDomainToMute: ''
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
+ components: {
+ TabSwitcher,
+ BlockList,
+ MuteList,
+ DomainMuteList,
+ BlockCard,
+ MuteCard,
+ DomainMuteCard,
+ ProgressButton,
+ Autosuggest,
+ Checkbox
+ },
+ methods: {
+ importFollows (file) {
+ return this.$store.state.api.backendInteractor.importFollows({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ importBlocks (file) {
+ return this.$store.state.api.backendInteractor.importBlocks({ file })
+ .then((status) => {
+ if (!status) {
+ throw new Error('failed')
+ }
+ })
+ },
+ generateExportableUsersContent (users) {
+ // Get addresses
+ return users.map((user) => {
+ // check is it's a local user
+ if (user && user.is_local) {
+ // append the instance address
+ // eslint-disable-next-line no-undef
+ return user.screen_name + '@' + location.hostname
+ }
+ return user.screen_name
+ }).join('\n')
+ },
+ activateTab (tabName) {
+ this.activeTab = tabName
+ },
+ filterUnblockedUsers (userIds) {
+ return reject(userIds, (userId) => {
+ const user = this.$store.getters.findUser(userId)
+ return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
+ })
+ },
+ filterUnMutedUsers (userIds) {
+ return reject(userIds, (userId) => {
+ const user = this.$store.getters.findUser(userId)
+ return !user || user.muted || user.id === this.$store.state.users.currentUser.id
+ })
+ },
+ queryUserIds (query) {
+ return this.$store.dispatch('searchUsers', query)
+ .then((users) => map(users, 'id'))
+ },
+ blockUsers (ids) {
+ return this.$store.dispatch('blockUsers', ids)
+ },
+ unblockUsers (ids) {
+ return this.$store.dispatch('unblockUsers', ids)
+ },
+ muteUsers (ids) {
+ return this.$store.dispatch('muteUsers', ids)
+ },
+ unmuteUsers (ids) {
+ return this.$store.dispatch('unmuteUsers', ids)
+ },
+ unmuteDomains (domains) {
+ return this.$store.dispatch('unmuteDomains', domains)
+ },
+ muteDomain () {
+ return this.$store.dispatch('muteDomain', this.newDomainToMute)
+ .then(() => { this.newDomainToMute = '' })
+ }
+ }
+}
+
+export default MutesAndBlocks
diff --git a/src/components/settings_modal/tabs/mutes_and_blocks.vue b/src/components/settings_modal/tabs/mutes_and_blocks.vue
new file mode 100644
index 0000000000..3aff47a0e9
--- /dev/null
+++ b/src/components/settings_modal/tabs/mutes_and_blocks.vue
@@ -0,0 +1,173 @@
+<template>
+ <tab-switcher>
+ <div :label="$t('settings.blocks_tab')">
+ <div class="profile-edit-usersearch-wrapper">
+ <Autosuggest
+ :filter="filterUnblockedUsers"
+ :query="queryUserIds"
+ :placeholder="$t('settings.search_user_to_block')"
+ >
+ <BlockCard
+ slot-scope="row"
+ :user-id="row.item"
+ />
+ </Autosuggest>
+ </div>
+ <BlockList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => blockUsers(selected)"
+ >
+ {{ $t('user_card.block') }}
+ <template slot="progress">
+ {{ $t('user_card.block_progress') }}
+ </template>
+ </ProgressButton>
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unblockUsers(selected)"
+ >
+ {{ $t('user_card.unblock') }}
+ <template slot="progress">
+ {{ $t('user_card.unblock_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <BlockCard :user-id="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_blocks') }}
+ </template>
+ </BlockList>
+ </div>
+
+ <div :label="$t('settings.mutes_tab')">
+ <tab-switcher>
+ <div label="Users">
+ <div class="profile-edit-usersearch-wrapper">
+ <Autosuggest
+ :filter="filterUnMutedUsers"
+ :query="queryUserIds"
+ :placeholder="$t('settings.search_user_to_mute')"
+ >
+ <MuteCard
+ slot-scope="row"
+ :user-id="row.item"
+ />
+ </Autosuggest>
+ </div>
+ <MuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => muteUsers(selected)"
+ >
+ {{ $t('user_card.mute') }}
+ <template slot="progress">
+ {{ $t('user_card.mute_progress') }}
+ </template>
+ </ProgressButton>
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteUsers(selected)"
+ >
+ {{ $t('user_card.unmute') }}
+ <template slot="progress">
+ {{ $t('user_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <MuteCard :user-id="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
+ </MuteList>
+ </div>
+
+ <div :label="$t('settings.domain_mutes')">
+ <div class="profile-edit-domain-mute-form">
+ <input
+ v-model="newDomainToMute"
+ :placeholder="$t('settings.type_domains_to_mute')"
+ type="text"
+ @keyup.enter="muteDomain"
+ >
+ <ProgressButton
+ class="btn btn-default"
+ :click="muteDomain"
+ >
+ {{ $t('domain_mute_card.mute') }}
+ <template slot="progress">
+ {{ $t('domain_mute_card.mute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ <DomainMuteList
+ :refresh="true"
+ :get-key="identity"
+ >
+ <template
+ slot="header"
+ slot-scope="{selected}"
+ >
+ <div class="profile-edit-bulk-actions">
+ <ProgressButton
+ v-if="selected.length > 0"
+ class="btn btn-default"
+ :click="() => unmuteDomains(selected)"
+ >
+ {{ $t('domain_mute_card.unmute') }}
+ <template slot="progress">
+ {{ $t('domain_mute_card.unmute_progress') }}
+ </template>
+ </ProgressButton>
+ </div>
+ </template>
+ <template
+ slot="item"
+ slot-scope="{item}"
+ >
+ <DomainMuteCard :domain="item" />
+ </template>
+ <template slot="empty">
+ {{ $t('settings.no_mutes') }}
+ </template>
+ </DomainMuteList>
+ </div>
+ </tab-switcher>
+ </div>
+ </tab-switcher>
+</template>
+
+<script src="./mutes_and_blocks.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/notifications.js b/src/components/settings_modal/tabs/notifications.js
new file mode 100644
index 0000000000..0a870b3f32
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications.js
@@ -0,0 +1,27 @@
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const Notifications = {
+ data () {
+ return {
+ activeTab: 'profile',
+ notificationSettings: this.$store.state.users.currentUser.notification_settings,
+ newDomainToMute: ''
+ }
+ },
+ components: {
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ }
+ },
+ methods: {
+ updateNotificationSettings () {
+ this.$store.state.api.backendInteractor
+ .updateNotificationSettings({ settings: this.notificationSettings })
+ }
+ }
+}
+
+export default Notifications
diff --git a/src/components/settings_modal/tabs/notifications.vue b/src/components/settings_modal/tabs/notifications.vue
new file mode 100644
index 0000000000..f9a7c17b1f
--- /dev/null
+++ b/src/components/settings_modal/tabs/notifications.vue
@@ -0,0 +1,42 @@
+<template>
+<div :label="$t('settings.notifications')">
+ <div class="setting-item">
+ <div class="select-multiple">
+ <span class="label">{{ $t('settings.notification_setting') }}</span>
+ <ul class="option-list">
+ <li>
+ <Checkbox v-model="notificationSettings.follows">
+ {{ $t('settings.notification_setting_follows') }}
+ </Checkbox>
+ </li>
+ <li>
+ <Checkbox v-model="notificationSettings.followers">
+ {{ $t('settings.notification_setting_followers') }}
+ </Checkbox>
+ </li>
+ <li>
+ <Checkbox v-model="notificationSettings.non_follows">
+ {{ $t('settings.notification_setting_non_follows') }}
+ </Checkbox>
+ </li>
+ <li>
+ <Checkbox v-model="notificationSettings.non_followers">
+ {{ $t('settings.notification_setting_non_followers') }}
+ </Checkbox>
+ </li>
+ </ul>
+ </div>
+ <p>{{ $t('settings.notification_mutes') }}</p>
+ <p>{{ $t('settings.notification_blocks') }}</p>
+ <button
+ class="btn btn-default"
+ @click="updateNotificationSettings"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ </div>
+</div>
+</template>
+
+<script src="./notifications.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/settings_modal/tabs/profile.js b/src/components/settings_modal/tabs/profile.js
new file mode 100644
index 0000000000..18c440243d
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.js
@@ -0,0 +1,179 @@
+import unescape from 'lodash/unescape'
+import ImageCropper from '../../image_cropper/image_cropper.vue'
+import ScopeSelector from '../../scope_selector/scope_selector.vue'
+import fileSizeFormatService from '../../../services/file_size_format/file_size_format.js'
+import ProgressButton from '../../progress_button/progress_button.vue'
+import EmojiInput from '../../emoji_input/emoji_input.vue'
+import suggestor from '../../emoji_input/suggestor.js'
+import Autosuggest from '../../autosuggest/autosuggest.vue'
+import Checkbox from '../../checkbox/checkbox.vue'
+
+const ProfileTab = {
+ data () {
+ return {
+ newName: this.$store.state.users.currentUser.name,
+ newBio: unescape(this.$store.state.users.currentUser.description),
+ newLocked: this.$store.state.users.currentUser.locked,
+ newNoRichText: this.$store.state.users.currentUser.no_rich_text,
+ newDefaultScope: this.$store.state.users.currentUser.default_scope,
+ hideFollows: this.$store.state.users.currentUser.hide_follows,
+ hideFollowers: this.$store.state.users.currentUser.hide_followers,
+ hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
+ hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
+ showRole: this.$store.state.users.currentUser.show_role,
+ role: this.$store.state.users.currentUser.role,
+ discoverable: this.$store.state.users.currentUser.discoverable,
+ allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
+ pickAvatarBtnVisible: true,
+ bannerUploading: false,
+ backgroundUploading: false,
+ banner: null,
+ bannerPreview: null,
+ background: null,
+ backgroundPreview: null,
+ bannerUploadError: null,
+ backgroundUploadError: null,
+ }
+ },
+ components: {
+ ScopeSelector,
+ ImageCropper,
+ EmojiInput,
+ Autosuggest,
+ ProgressButton,
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ emojiUserSuggestor () {
+ return suggestor({
+ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ],
+ users: this.$store.state.users.users,
+ updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
+ })
+ },
+ emojiSuggestor () {
+ return suggestor({ emoji: [
+ ...this.$store.state.instance.emoji,
+ ...this.$store.state.instance.customEmoji
+ ] })
+ }
+ },
+ methods: {
+ updateProfile () {
+ this.$store.state.api.backendInteractor
+ .updateProfile({
+ params: {
+ note: this.newBio,
+ locked: this.newLocked,
+ // Backend notation.
+ /* eslint-disable camelcase */
+ display_name: this.newName,
+ default_scope: this.newDefaultScope,
+ no_rich_text: this.newNoRichText,
+ hide_follows: this.hideFollows,
+ hide_followers: this.hideFollowers,
+ discoverable: this.discoverable,
+ allow_following_move: this.allowFollowingMove,
+ hide_follows_count: this.hideFollowsCount,
+ hide_followers_count: this.hideFollowersCount,
+ show_role: this.showRole
+ /* eslint-enable camelcase */
+ } }).then((user) => {
+ this.$store.commit('addNewUsers', [user])
+ this.$store.commit('setCurrentUser', user)
+ })
+ },
+ changeVis (visibility) {
+ this.newDefaultScope = visibility
+ },
+ uploadFile (slot, e) {
+ const file = e.target.files[0]
+ if (!file) { return }
+ if (file.size > this.$store.state.instance[slot + 'limit']) {
+ const filesize = fileSizeFormatService.fileSizeFormat(file.size)
+ const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
+ this[slot + 'UploadError'] = [
+ this.$t('upload.error.base'),
+ this.$t(
+ 'upload.error.file_too_big',
+ {
+ filesize: filesize.num,
+ filesizeunit: filesize.unit,
+ allowedsize: allowedsize.num,
+ allowedsizeunit: allowedsize.unit
+ }
+ )
+ ].join(' ')
+ return
+ }
+ // eslint-disable-next-line no-undef
+ const reader = new FileReader()
+ reader.onload = ({ target }) => {
+ const img = target.result
+ this[slot + 'Preview'] = img
+ this[slot] = file
+ }
+ reader.readAsDataURL(file)
+ },
+ submitAvatar (cropper, file) {
+ const that = this
+ return new Promise((resolve, reject) => {
+ function updateAvatar (avatar) {
+ that.$store.state.api.backendInteractor.updateAvatar({ avatar })
+ .then((user) => {
+ that.$store.commit('addNewUsers', [user])
+ that.$store.commit('setCurrentUser', user)
+ resolve()
+ })
+ .catch((err) => {
+ reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
+ })
+ }
+
+ if (cropper) {
+ cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
+ } else {
+ updateAvatar(file)
+ }
+ })
+ },
+ submitBanner () {
+ if (!this.bannerPreview) { return }
+
+ this.bannerUploading = true
+ this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
+ .then((user) => {
+ this.$store.commit('addNewUsers', [user])
+ this.$store.commit('setCurrentUser', user)
+ this.bannerPreview = null
+ })
+ .catch((err) => {
+ this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
+ })
+ .then(() => { this.bannerUploading = false })
+ },
+ submitBg () {
+ if (!this.backgroundPreview) { return }
+ let background = this.background
+ this.backgroundUploading = true
+ this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
+ if (!data.error) {
+ this.$store.commit('addNewUsers', [data])
+ this.$store.commit('setCurrentUser', data)
+ this.backgroundPreview = null
+ } else {
+ this.backgroundUploadError = this.$t('upload.error.base') + data.error
+ }
+ this.backgroundUploading = false
+ })
+ }
+ }
+}
+
+export default ProfileTab
diff --git a/src/components/settings_modal/tabs/profile.scss b/src/components/settings_modal/tabs/profile.scss
new file mode 100644
index 0000000000..4aab81eb7e
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.scss
@@ -0,0 +1,82 @@
+@import '../../../_variables.scss';
+.profile-tab {
+ .bio {
+ margin: 0;
+ }
+
+ .visibility-tray {
+ padding-top: 5px;
+ }
+
+ input[type=file] {
+ padding: 5px;
+ height: auto;
+ }
+
+ .banner {
+ max-width: 100%;
+ }
+
+ .uploading {
+ font-size: 1.5em;
+ margin: 0.25em;
+ }
+
+ .name-changer {
+ width: 100%;
+ }
+
+ .bg {
+ max-width: 100%;
+ }
+
+ .current-avatar {
+ display: block;
+ width: 150px;
+ height: 150px;
+ border-radius: $fallback--avatarRadius;
+ border-radius: var(--avatarRadius, $fallback--avatarRadius);
+ }
+
+ .oauth-tokens {
+ width: 100%;
+
+ th {
+ text-align: left;
+ }
+
+ .actions {
+ text-align: right;
+ }
+ }
+
+ &-usersearch-wrapper {
+ padding: 1em;
+ }
+
+ &-bulk-actions {
+ text-align: right;
+ padding: 0 1em;
+ min-height: 28px;
+
+ button {
+ width: 10em;
+ }
+ }
+
+ &-domain-mute-form {
+ padding: 1em;
+ display: flex;
+ flex-direction: column;
+
+ button {
+ align-self: flex-end;
+ margin-top: 1em;
+ width: 10em;
+ }
+ }
+
+ .setting-subitem {
+ margin-left: 1.75em;
+ }
+}
diff --git a/src/components/settings_modal/tabs/profile.vue b/src/components/settings_modal/tabs/profile.vue
new file mode 100644
index 0000000000..335fc12e31
--- /dev/null
+++ b/src/components/settings_modal/tabs/profile.vue
@@ -0,0 +1,213 @@
+<template>
+<div class="profile-tab">
+ <div class="setting-item">
+ <h2>{{ $t('settings.name_bio') }}</h2>
+ <p>{{ $t('settings.name') }}</p>
+ <EmojiInput
+ v-model="newName"
+ enable-emoji-picker
+ :suggest="emojiSuggestor"
+ >
+ <input
+ id="username"
+ v-model="newName"
+ classname="name-changer"
+ >
+ </EmojiInput>
+ <p>{{ $t('settings.bio') }}</p>
+ <EmojiInput
+ v-model="newBio"
+ enable-emoji-picker
+ :suggest="emojiUserSuggestor"
+ >
+ <textarea
+ v-model="newBio"
+ classname="bio"
+ />
+ </EmojiInput>
+ <p>
+ <Checkbox v-model="newLocked">
+ {{ $t('settings.lock_account_description') }}
+ </Checkbox>
+ </p>
+ <div>
+ <label for="default-vis">{{ $t('settings.default_vis') }}</label>
+ <div
+ id="default-vis"
+ class="visibility-tray"
+ >
+ <scope-selector
+ :show-all="true"
+ :user-default="newDefaultScope"
+ :initial-scope="newDefaultScope"
+ :on-scope-change="changeVis"
+ />
+ </div>
+ </div>
+ <p>
+ <Checkbox v-model="newNoRichText">
+ {{ $t('settings.no_rich_text_description') }}
+ </Checkbox>
+ </p>
+ <p>
+ <Checkbox v-model="hideFollows">
+ {{ $t('settings.hide_follows_description') }}
+ </Checkbox>
+ </p>
+ <p class="setting-subitem">
+ <Checkbox
+ v-model="hideFollowsCount"
+ :disabled="!hideFollows"
+ >
+ {{ $t('settings.hide_follows_count_description') }}
+ </Checkbox>
+ </p>
+ <p>
+ <Checkbox v-model="hideFollowers">
+ {{ $t('settings.hide_followers_description') }}
+ </Checkbox>
+ </p>
+ <p class="setting-subitem">
+ <Checkbox
+ v-model="hideFollowersCount"
+ :disabled="!hideFollowers"
+ >
+ {{ $t('settings.hide_followers_count_description') }}
+ </Checkbox>
+ </p>
+ <p>
+ <Checkbox v-model="allowFollowingMove">
+ {{ $t('settings.allow_following_move') }}
+ </Checkbox>
+ </p>
+ <p v-if="role === 'admin' || role === 'moderator'">
+ <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>
+ <Checkbox v-model="discoverable">
+ {{ $t('settings.discoverable') }}
+ </Checkbox>
+ </p>
+ <button
+ :disabled="newName && newName.length === 0"
+ class="btn btn-default"
+ @click="updateProfile"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.avatar') }}</h2>
+ <p class="visibility-notice">
+ {{ $t('settings.avatar_size_instruction') }}
+ </p>
+ <p>{{ $t('settings.current_avatar') }}</p>
+ <img
+ :src="user.profile_image_url_original"
+ class="current-avatar"
+ >
+ <p>{{ $t('settings.set_new_avatar') }}</p>
+ <button
+ v-show="pickAvatarBtnVisible"
+ id="pick-avatar"
+ class="btn"
+ type="button"
+ >
+ {{ $t('settings.upload_a_photo') }}
+ </button>
+ <image-cropper
+ trigger="#pick-avatar"
+ :submit-handler="submitAvatar"
+ @open="pickAvatarBtnVisible=false"
+ @close="pickAvatarBtnVisible=true"
+ />
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.profile_banner') }}</h2>
+ <p>{{ $t('settings.current_profile_banner') }}</p>
+ <img
+ :src="user.cover_photo"
+ class="banner"
+ >
+ <p>{{ $t('settings.set_new_profile_banner') }}</p>
+ <img
+ v-if="bannerPreview"
+ class="banner"
+ :src="bannerPreview"
+ >
+ <div>
+ <input
+ type="file"
+ @change="uploadFile('banner', $event)"
+ >
+ </div>
+ <i
+ v-if="bannerUploading"
+ class=" icon-spin4 animate-spin uploading"
+ />
+ <button
+ v-else-if="bannerPreview"
+ class="btn btn-default"
+ @click="submitBanner"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <div
+ v-if="bannerUploadError"
+ class="alert error"
+ >
+ Error: {{ bannerUploadError }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearUploadError('banner')"
+ />
+ </div>
+ </div>
+ <div class="setting-item">
+ <h2>{{ $t('settings.profile_background') }}</h2>
+ <p>{{ $t('settings.set_new_profile_background') }}</p>
+ <img
+ v-if="backgroundPreview"
+ class="bg"
+ :src="backgroundPreview"
+ >
+ <div>
+ <input
+ type="file"
+ @change="uploadFile('background', $event)"
+ >
+ </div>
+ <i
+ v-if="backgroundUploading"
+ class=" icon-spin4 animate-spin uploading"
+ />
+ <button
+ v-else-if="backgroundPreview"
+ class="btn btn-default"
+ @click="submitBg"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <div
+ v-if="backgroundUploadError"
+ class="alert error"
+ >
+ Error: {{ backgroundUploadError }}
+ <i
+ class="button-icon icon-cancel"
+ @click="clearUploadError('background')"
+ />
+ </div>
+ </div>
+</div>
+</template>
+
+<script src="./profile.js"></script>
+<style lang="scss" src="./profile.scss"></style>
diff --git a/src/components/settings_modal/tabs/security.js b/src/components/settings_modal/tabs/security.js
new file mode 100644
index 0000000000..cc791b7aed
--- /dev/null
+++ b/src/components/settings_modal/tabs/security.js
@@ -0,0 +1,106 @@
+import ProgressButton from '../../progress_button/progress_button.vue'
+import Checkbox from '../../checkbox/checkbox.vue'
+import Mfa from '../../user_settings/mfa.vue'
+
+const Security = {
+ data () {
+ return {
+ newEmail: '',
+ changeEmailError: false,
+ changeEmailPassword: '',
+ changedEmail: false,
+ deletingAccount: false,
+ deleteAccountConfirmPasswordInput: '',
+ deleteAccountError: false,
+ changePasswordInputs: [ '', '', '' ],
+ changedPassword: false,
+ changePasswordError: false
+ }
+ },
+ created () {
+ this.$store.dispatch('fetchTokens')
+ },
+ components: {
+ ProgressButton,
+ Mfa,
+ Checkbox
+ },
+ computed: {
+ user () {
+ return this.$store.state.users.currentUser
+ },
+ pleromaBackend () {
+ return this.$store.state.instance.pleromaBackend
+ },
+ oauthTokens () {
+ return this.$store.state.oauthTokens.tokens.map(oauthToken => {
+ return {
+ id: oauthToken.id,
+ appName: oauthToken.app_name,
+ validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
+ }
+ })
+ }
+ },
+ methods: {
+ confirmDelete () {
+ this.deletingAccount = true
+ },
+ deleteAccount () {
+ this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
+ .then((res) => {
+ if (res.status === 'success') {
+ this.$store.dispatch('logout')
+ this.$router.push({ name: 'root' })
+ } else {
+ this.deleteAccountError = res.error
+ }
+ })
+ },
+ changePassword () {
+ const params = {
+ password: this.changePasswordInputs[0],
+ newPassword: this.changePasswordInputs[1],
+ newPasswordConfirmation: this.changePasswordInputs[2]
+ }
+ this.$store.state.api.backendInteractor.changePassword(params)
+ .then((res) => {
+ if (res.status === 'success') {
+ this.changedPassword = true
+ this.changePasswordError = false
+ this.logout()
+ } else {
+ this.changedPassword = false
+ this.changePasswordError = res.error
+ }
+ })
+ },
+ 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
+ }
+ })
+ },
+ logout () {
+ this.$store.dispatch('logout')
+ this.$router.replace('/')
+ },
+ revokeToken (id) {
+ if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
+ this.$store.dispatch('revokeToken', id)
+ }
+ }
+ }
+}
+
+export default Security
diff --git a/src/components/settings_modal/tabs/security.vue b/src/components/settings_modal/tabs/security.vue
new file mode 100644
index 0000000000..603c9a04a4
--- /dev/null
+++ b/src/components/settings_modal/tabs/security.vue
@@ -0,0 +1,143 @@
+<template>
+<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>
+ <input
+ v-model="changePasswordInputs[0]"
+ type="password"
+ >
+ </div>
+ <div>
+ <p>{{ $t('settings.new_password') }}</p>
+ <input
+ v-model="changePasswordInputs[1]"
+ type="password"
+ >
+ </div>
+ <div>
+ <p>{{ $t('settings.confirm_new_password') }}</p>
+ <input
+ v-model="changePasswordInputs[2]"
+ type="password"
+ >
+ </div>
+ <button
+ class="btn btn-default"
+ @click="changePassword"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ <p v-if="changedPassword">
+ {{ $t('settings.changed_password') }}
+ </p>
+ <p v-else-if="changePasswordError !== false">
+ {{ $t('settings.change_password_error') }}
+ </p>
+ <p v-if="changePasswordError">
+ {{ changePasswordError }}
+ </p>
+ </div>
+
+ <div class="setting-item">
+ <h2>{{ $t('settings.oauth_tokens') }}</h2>
+ <table class="oauth-tokens">
+ <thead>
+ <tr>
+ <th>{{ $t('settings.app_name') }}</th>
+ <th>{{ $t('settings.valid_until') }}</th>
+ <th />
+ </tr>
+ </thead>
+ <tbody>
+ <tr
+ v-for="oauthToken in oauthTokens"
+ :key="oauthToken.id"
+ >
+ <td>{{ oauthToken.appName }}</td>
+ <td>{{ oauthToken.validUntil }}</td>
+ <td class="actions">
+ <button
+ class="btn btn-default"
+ @click="revokeToken(oauthToken.id)"
+ >
+ {{ $t('settings.revoke_token') }}
+ </button>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <mfa />
+ <div class="setting-item">
+ <h2>{{ $t('settings.delete_account') }}</h2>
+ <p v-if="!deletingAccount">
+ {{ $t('settings.delete_account_description') }}
+ </p>
+ <div v-if="deletingAccount">
+ <p>{{ $t('settings.delete_account_instructions') }}</p>
+ <p>{{ $t('login.password') }}</p>
+ <input
+ v-model="deleteAccountConfirmPasswordInput"
+ type="password"
+ >
+ <button
+ class="btn btn-default"
+ @click="deleteAccount"
+ >
+ {{ $t('settings.delete_account') }}
+ </button>
+ </div>
+ <p v-if="deleteAccountError !== false">
+ {{ $t('settings.delete_account_error') }}
+ </p>
+ <p v-if="deleteAccountError">
+ {{ deleteAccountError }}
+ </p>
+ <button
+ v-if="!deletingAccount"
+ class="btn btn-default"
+ @click="confirmDelete"
+ >
+ {{ $t('general.submit') }}
+ </button>
+ </div>
+</div>
+</template>
+
+<script src="./security.js"></script>
+<!-- <style lang="scss" src="./profile.scss"></style> -->
diff --git a/src/components/tab_switcher/tab_switcher.js b/src/components/tab_switcher/tab_switcher.js
index 008e1e9549..97791de38d 100644
--- a/src/components/tab_switcher/tab_switcher.js
+++ b/src/components/tab_switcher/tab_switcher.js
@@ -1,118 +1,123 @@
import Vue from 'vue'
import './tab_switcher.scss'
export default Vue.component('tab-switcher', {
name: 'TabSwitcher',
props: {
renderOnlyFocused: {
required: false,
type: Boolean,
default: false
},
onSwitch: {
required: false,
type: Function,
default: undefined
},
activeTab: {
required: false,
type: String,
default: undefined
},
scrollableTabs: {
required: false,
type: Boolean,
default: false
+ },
+ sideTabBar: {
+ required: false,
+ type: Boolean,
+ default: false
}
},
data () {
return {
active: this.$slots.default.findIndex(_ => _.tag)
}
},
computed: {
activeIndex () {
// In case of controlled component
if (this.activeTab) {
return this.$slots.default.findIndex(slot => this.activeTab === slot.key)
} else {
return this.active
}
}
},
beforeUpdate () {
const currentSlot = this.$slots.default[this.active]
if (!currentSlot.tag) {
this.active = this.$slots.default.findIndex(_ => _.tag)
}
},
methods: {
activateTab (index) {
return (e) => {
e.preventDefault()
if (typeof this.onSwitch === 'function') {
this.onSwitch.call(null, this.$slots.default[index].key)
}
this.active = index
}
}
},
render (h) {
const tabs = this.$slots.default
.map((slot, index) => {
if (!slot.tag) return
const classesTab = ['tab']
const classesWrapper = ['tab-wrapper']
if (this.activeIndex === index) {
classesTab.push('active')
classesWrapper.push('active')
}
if (slot.data.attrs.image) {
return (
<div class={classesWrapper.join(' ')}>
<button
disabled={slot.data.attrs.disabled}
onClick={this.activateTab(index)}
class={classesTab.join(' ')}>
<img src={slot.data.attrs.image} title={slot.data.attrs['image-tooltip']}/>
{slot.data.attrs.label ? '' : slot.data.attrs.label}
</button>
</div>
)
}
return (
<div class={classesWrapper.join(' ')}>
<button
disabled={slot.data.attrs.disabled}
onClick={this.activateTab(index)}
class={classesTab.join(' ')}>
{slot.data.attrs.label}</button>
</div>
)
})
const contents = this.$slots.default.map((slot, index) => {
if (!slot.tag) return
const active = this.activeIndex === index
if (this.renderOnlyFocused) {
return active
? <div class="active">{slot}</div>
: <div class="hidden"></div>
}
return <div class={active ? 'active' : 'hidden' }>{slot}</div>
})
return (
- <div class="tab-switcher">
+ <div class={'tab-switcher ' + (this.sideTabBar ? 'side-tabs' : 'top-tabs')}>
<div class="tabs">
{tabs}
</div>
<div class={'contents' + (this.scrollableTabs ? ' scrollable-tabs' : '')}>
{contents}
</div>
</div>
)
}
})
diff --git a/src/components/tab_switcher/tab_switcher.scss b/src/components/tab_switcher/tab_switcher.scss
index df585faa38..a443531eca 100644
--- a/src/components/tab_switcher/tab_switcher.scss
+++ b/src/components/tab_switcher/tab_switcher.scss
@@ -1,98 +1,186 @@
@import '../../_variables.scss';
.tab-switcher {
display: flex;
- flex-direction: column;
+
+ &.top-tabs {
+ flex-direction: column;
+ > .tabs {
+ width: 100%;
+ overflow-y: hidden;
+ overflow-x: auto;
+ padding-top: 5px;
+ flex-direction: row;
+ &::after, &::before {
+ content: '';
+ flex: 1 1 auto;
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+ }
+ .tab-wrapper {
+ height: 28px;
+
+ &:not(.active)::after {
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-bottom: 1px solid;
+ border-bottom-color: $fallback--border;
+ border-bottom-color: var(--border, $fallback--border);
+ }
+ }
+ .tab {
+ width: 100%;
+ min-width: 1px;
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ padding-bottom: 99px;
+ margin-bottom: 6px - 99px;
+ }
+ }
+ .contents.scrollable-tabs {
+ flex-basis: 0;
+ }
+ }
+
+ &.side-tabs {
+ flex-direction: row;
+ > .contents {
+ flex: 0 1 80%;
+ }
+ > .tabs {
+ flex: 1 0 auto;
+ overflow-y: auto;
+ overflow-x: hidden;
+ padding-top: 5px;
+ flex-direction: column;
+ &::after {
+ content: '';
+ flex: 1 1 auto;
+ border-right: 1px solid;
+ border-right-color: $fallback--border;
+ border-right-color: var(--border, $fallback--border);
+ }
+ .tab-wrapper {
+ min-width: 10em;
+ &:not(.active)::after {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ border-right: 1px solid;
+ border-right-color: $fallback--border;
+ border-right-color: var(--border, $fallback--border);
+ }
+ }
+ .tab {
+ box-sizing: content-box;
+ width: 100%;
+ margin-bottom: 5px;
+ min-width: 10em;
+ min-width: 1px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ // padding-right: 200px;
+ // margin-right: 6px - 200px;
+ margin-left: 6px;
+ }
+
+ .tab-wrapper {
+ min-width: 10em;
+ &:not(.active)::after {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ border-right: 1px solid;
+ border-right-color: $fallback--border;
+ border-right-color: var(--border, $fallback--border);
+ }
+ }
+ .tab {
+ box-sizing: content-box;
+ width: 100%;
+ margin-bottom: 5px;
+ min-width: 10em;
+ min-width: 1px;
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ // padding-right: 200px;
+ // margin-right: 6px - 200px;
+ margin-left: 6px;
+ }
+ }
+ }
+
.contents {
flex: 1 0 auto;
min-height: 0px;
.hidden {
display: none;
}
&.scrollable-tabs {
- flex-basis: 0;
overflow-y: auto;
}
}
+
+ .tab {
+ position: relative;
+ white-space: nowrap;
+
+ padding: 6px 1em;
+ color: $fallback--text;
+ color: var(--tabText, $fallback--text);
+ background-color: $fallback--fg;
+ background-color: var(--tab, $fallback--fg);
+
+ &:not(.active) {
+ z-index: 4;
+
+ &:hover {
+ z-index: 6;
+ }
+ }
+
+ &.active {
+ background: transparent;
+ z-index: 5;
+ color: $fallback--text;
+ color: var(--tabActiveText, $fallback--text);
+ }
+
+ img {
+ max-height: 26px;
+ vertical-align: top;
+ margin-top: -5px;
+ }
+ }
+
+
.tabs {
display: flex;
position: relative;
- width: 100%;
- overflow-y: hidden;
- overflow-x: auto;
- padding-top: 5px;
box-sizing: border-box;
&::after, &::before {
display: block;
- content: '';
flex: 1 1 auto;
- border-bottom: 1px solid;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
}
+ }
- .tab-wrapper {
- height: 28px;
- position: relative;
- display: flex;
- flex: 0 0 auto;
-
- .tab {
- width: 100%;
- min-width: 1px;
- position: relative;
- border-bottom-left-radius: 0;
- border-bottom-right-radius: 0;
- padding: 6px 1em;
- padding-bottom: 99px;
- margin-bottom: 6px - 99px;
- white-space: nowrap;
-
- color: $fallback--text;
- color: var(--tabText, $fallback--text);
- background-color: $fallback--fg;
- background-color: var(--tab, $fallback--fg);
-
- &:not(.active) {
- z-index: 4;
-
- &:hover {
- z-index: 6;
- }
- }
-
- &.active {
- background: transparent;
- z-index: 5;
- color: $fallback--text;
- color: var(--tabActiveText, $fallback--text);
- }
-
- img {
- max-height: 26px;
- vertical-align: top;
- margin-top: -5px;
- }
- }
+ .tab-wrapper {
+ position: relative;
+ display: flex;
+ flex: 0 0 auto;
- &:not(.active) {
- &::after {
- content: '';
- position: absolute;
- left: 0;
- right: 0;
- bottom: 0;
- z-index: 7;
- border-bottom: 1px solid;
- border-bottom-color: $fallback--border;
- border-bottom-color: var(--border, $fallback--border);
- }
+ &:not(.active) {
+ &::after {
+ content: '';
+ position: absolute;
+ z-index: 7;
}
}
-
}
}
diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js
index eca6f9b16f..e07d4e56a3 100644
--- a/src/components/user_settings/user_settings.js
+++ b/src/components/user_settings/user_settings.js
@@ -1,393 +1,140 @@
-import unescape from 'lodash/unescape'
import get from 'lodash/get'
import map from 'lodash/map'
import reject from 'lodash/reject'
+import Autosuggest from '../autosuggest/autosuggest.vue'
import TabSwitcher from '../tab_switcher/tab_switcher.js'
-import ImageCropper from '../image_cropper/image_cropper.vue'
-import StyleSwitcher from '../style_switcher/style_switcher.vue'
-import ScopeSelector from '../scope_selector/scope_selector.vue'
-import fileSizeFormatService from '../../services/file_size_format/file_size_format.js'
import BlockCard from '../block_card/block_card.vue'
import MuteCard from '../mute_card/mute_card.vue'
import DomainMuteCard from '../domain_mute_card/domain_mute_card.vue'
import SelectableList from '../selectable_list/selectable_list.vue'
import ProgressButton from '../progress_button/progress_button.vue'
-import EmojiInput from '../emoji_input/emoji_input.vue'
-import suggestor from '../emoji_input/suggestor.js'
-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({
fetch: (props, $store) => $store.dispatch('fetchBlocks'),
select: (props, $store) => get($store.state.users.currentUser, 'blockIds', []),
childPropName: 'items'
})(SelectableList)
const MuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'muteIds', []),
childPropName: 'items'
})(SelectableList)
const DomainMuteList = withSubscription({
fetch: (props, $store) => $store.dispatch('fetchDomainMutes'),
select: (props, $store) => get($store.state.users.currentUser, 'domainMutes', []),
childPropName: 'items'
})(SelectableList)
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,
- newNoRichText: this.$store.state.users.currentUser.no_rich_text,
- newDefaultScope: this.$store.state.users.currentUser.default_scope,
- hideFollows: this.$store.state.users.currentUser.hide_follows,
- hideFollowers: this.$store.state.users.currentUser.hide_followers,
- hideFollowsCount: this.$store.state.users.currentUser.hide_follows_count,
- hideFollowersCount: this.$store.state.users.currentUser.hide_followers_count,
- showRole: this.$store.state.users.currentUser.show_role,
- role: this.$store.state.users.currentUser.role,
- discoverable: this.$store.state.users.currentUser.discoverable,
- allowFollowingMove: this.$store.state.users.currentUser.allow_following_move,
- pickAvatarBtnVisible: true,
- bannerUploading: false,
- backgroundUploading: false,
- banner: null,
- bannerPreview: null,
- background: null,
- backgroundPreview: null,
- bannerUploadError: null,
- backgroundUploadError: null,
- changeEmailError: false,
- changeEmailPassword: '',
- changedEmail: false,
- deletingAccount: false,
- deleteAccountConfirmPasswordInput: '',
- deleteAccountError: false,
- changePasswordInputs: [ '', '', '' ],
- changedPassword: false,
- changePasswordError: false,
activeTab: 'profile',
- notificationSettings: this.$store.state.users.currentUser.notification_settings,
newDomainToMute: ''
}
},
created () {
this.$store.dispatch('fetchTokens')
},
components: {
- StyleSwitcher,
- ScopeSelector,
TabSwitcher,
- ImageCropper,
BlockList,
MuteList,
DomainMuteList,
- EmojiInput,
- Autosuggest,
BlockCard,
MuteCard,
DomainMuteCard,
ProgressButton,
- Importer,
- Exporter,
- Mfa,
+ Autosuggest,
Checkbox
},
computed: {
user () {
return this.$store.state.users.currentUser
},
- emojiUserSuggestor () {
- return suggestor({
- emoji: [
- ...this.$store.state.instance.emoji,
- ...this.$store.state.instance.customEmoji
- ],
- users: this.$store.state.users.users,
- updateUsersList: (input) => this.$store.dispatch('searchUsers', input)
- })
- },
- emojiSuggestor () {
- return suggestor({ emoji: [
- ...this.$store.state.instance.emoji,
- ...this.$store.state.instance.customEmoji
- ] })
- },
pleromaBackend () {
return this.$store.state.instance.pleromaBackend
},
- minimalScopesMode () {
- return this.$store.state.instance.minimalScopesMode
- },
- vis () {
- return {
- public: { selected: this.newDefaultScope === 'public' },
- unlisted: { selected: this.newDefaultScope === 'unlisted' },
- private: { selected: this.newDefaultScope === 'private' },
- direct: { selected: this.newDefaultScope === 'direct' }
- }
- },
currentSaveStateNotice () {
return this.$store.state.interface.settings.currentSaveStateNotice
- },
- oauthTokens () {
- return this.$store.state.oauthTokens.tokens.map(oauthToken => {
- return {
- id: oauthToken.id,
- appName: oauthToken.app_name,
- validUntil: new Date(oauthToken.valid_until).toLocaleDateString()
- }
- })
}
},
methods: {
- updateProfile () {
- this.$store.state.api.backendInteractor
- .updateProfile({
- params: {
- note: this.newBio,
- locked: this.newLocked,
- // Backend notation.
- /* eslint-disable camelcase */
- display_name: this.newName,
- default_scope: this.newDefaultScope,
- no_rich_text: this.newNoRichText,
- hide_follows: this.hideFollows,
- hide_followers: this.hideFollowers,
- discoverable: this.discoverable,
- allow_following_move: this.allowFollowingMove,
- hide_follows_count: this.hideFollowsCount,
- hide_followers_count: this.hideFollowersCount,
- show_role: this.showRole
- /* eslint-enable camelcase */
- } }).then((user) => {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
- })
- },
- updateNotificationSettings () {
- this.$store.state.api.backendInteractor
- .updateNotificationSettings({ settings: this.notificationSettings })
- },
- changeVis (visibility) {
- this.newDefaultScope = visibility
- },
- uploadFile (slot, e) {
- const file = e.target.files[0]
- if (!file) { return }
- if (file.size > this.$store.state.instance[slot + 'limit']) {
- const filesize = fileSizeFormatService.fileSizeFormat(file.size)
- const allowedsize = fileSizeFormatService.fileSizeFormat(this.$store.state.instance[slot + 'limit'])
- this[slot + 'UploadError'] = this.$t('upload.error.base') + ' ' + this.$t('upload.error.file_too_big', { filesize: filesize.num, filesizeunit: filesize.unit, allowedsize: allowedsize.num, allowedsizeunit: allowedsize.unit })
- return
- }
- // eslint-disable-next-line no-undef
- const reader = new FileReader()
- reader.onload = ({ target }) => {
- const img = target.result
- this[slot + 'Preview'] = img
- this[slot] = file
- }
- reader.readAsDataURL(file)
- },
- submitAvatar (cropper, file) {
- const that = this
- return new Promise((resolve, reject) => {
- function updateAvatar (avatar) {
- that.$store.state.api.backendInteractor.updateAvatar({ avatar })
- .then((user) => {
- that.$store.commit('addNewUsers', [user])
- that.$store.commit('setCurrentUser', user)
- resolve()
- })
- .catch((err) => {
- reject(new Error(that.$t('upload.error.base') + ' ' + err.message))
- })
- }
-
- if (cropper) {
- cropper.getCroppedCanvas().toBlob(updateAvatar, file.type)
- } else {
- updateAvatar(file)
- }
- })
- },
- clearUploadError (slot) {
- this[slot + 'UploadError'] = null
- },
- submitBanner () {
- if (!this.bannerPreview) { return }
-
- this.bannerUploading = true
- this.$store.state.api.backendInteractor.updateBanner({ banner: this.banner })
- .then((user) => {
- this.$store.commit('addNewUsers', [user])
- this.$store.commit('setCurrentUser', user)
- this.bannerPreview = null
- })
- .catch((err) => {
- this.bannerUploadError = this.$t('upload.error.base') + ' ' + err.message
- })
- .then(() => { this.bannerUploading = false })
- },
- submitBg () {
- if (!this.backgroundPreview) { return }
- let background = this.background
- this.backgroundUploading = true
- this.$store.state.api.backendInteractor.updateBg({ background }).then((data) => {
- if (!data.error) {
- this.$store.commit('addNewUsers', [data])
- this.$store.commit('setCurrentUser', data)
- this.backgroundPreview = null
- } else {
- this.backgroundUploadError = this.$t('upload.error.base') + data.error
- }
- this.backgroundUploading = false
- })
- },
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
}
})
},
generateExportableUsersContent (users) {
// Get addresses
return users.map((user) => {
// check is it's a local user
if (user && user.is_local) {
// append the instance address
// eslint-disable-next-line no-undef
return user.screen_name + '@' + location.hostname
}
return user.screen_name
}).join('\n')
},
- getFollowsContent () {
- return this.$store.state.api.backendInteractor.exportFriends({ id: this.$store.state.users.currentUser.id })
- .then(this.generateExportableUsersContent)
- },
- getBlocksContent () {
- return this.$store.state.api.backendInteractor.fetchBlocks()
- .then(this.generateExportableUsersContent)
- },
- confirmDelete () {
- this.deletingAccount = true
- },
- deleteAccount () {
- this.$store.state.api.backendInteractor.deleteAccount({ password: this.deleteAccountConfirmPasswordInput })
- .then((res) => {
- if (res.status === 'success') {
- this.$store.dispatch('logout')
- this.$router.push({ name: 'root' })
- } else {
- this.deleteAccountError = res.error
- }
- })
- },
- changePassword () {
- const params = {
- password: this.changePasswordInputs[0],
- newPassword: this.changePasswordInputs[1],
- newPasswordConfirmation: this.changePasswordInputs[2]
- }
- this.$store.state.api.backendInteractor.changePassword(params)
- .then((res) => {
- if (res.status === 'success') {
- this.changedPassword = true
- this.changePasswordError = false
- this.logout()
- } else {
- this.changedPassword = false
- this.changePasswordError = res.error
- }
- })
- },
- 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
},
- logout () {
- this.$store.dispatch('logout')
- this.$router.replace('/')
- },
- revokeToken (id) {
- if (window.confirm(`${this.$i18n.t('settings.revoke_token')}?`)) {
- this.$store.dispatch('revokeToken', id)
- }
- },
filterUnblockedUsers (userIds) {
return reject(userIds, (userId) => {
const user = this.$store.getters.findUser(userId)
return !user || user.statusnet_blocking || user.id === this.$store.state.users.currentUser.id
})
},
filterUnMutedUsers (userIds) {
return reject(userIds, (userId) => {
const user = this.$store.getters.findUser(userId)
return !user || user.muted || user.id === this.$store.state.users.currentUser.id
})
},
queryUserIds (query) {
return this.$store.dispatch('searchUsers', query)
.then((users) => map(users, 'id'))
},
blockUsers (ids) {
return this.$store.dispatch('blockUsers', ids)
},
unblockUsers (ids) {
return this.$store.dispatch('unblockUsers', ids)
},
muteUsers (ids) {
return this.$store.dispatch('muteUsers', ids)
},
unmuteUsers (ids) {
return this.$store.dispatch('unmuteUsers', ids)
},
unmuteDomains (domains) {
return this.$store.dispatch('unmuteDomains', domains)
},
muteDomain () {
return this.$store.dispatch('muteDomain', this.newDomainToMute)
.then(() => { this.newDomainToMute = '' })
},
identity (value) {
return value
}
}
}
export default UserSettings
diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue
index 8b2336b4dd..2a88714f9f 100644
--- a/src/components/user_settings/user_settings.vue
+++ b/src/components/user_settings/user_settings.vue
@@ -1,716 +1,119 @@
<template>
<div class="settings panel panel-default">
<div class="panel-heading">
<div class="title">
{{ $t('settings.user_settings') }}
</div>
<transition name="fade">
<template v-if="currentSaveStateNotice">
<div
v-if="currentSaveStateNotice.error"
class="alert error"
@click.prevent
>
{{ $t('settings.saving_err') }}
</div>
<div
v-if="!currentSaveStateNotice.error"
class="alert transparent"
@click.prevent
>
{{ $t('settings.saving_ok') }}
</div>
</template>
</transition>
</div>
<div class="panel-body profile-edit">
- <tab-switcher>
- <div :label="$t('settings.profile_tab')">
- <div class="setting-item">
- <h2>{{ $t('settings.name_bio') }}</h2>
- <p>{{ $t('settings.name') }}</p>
- <EmojiInput
- v-model="newName"
- enable-emoji-picker
- :suggest="emojiSuggestor"
- >
- <input
- id="username"
- v-model="newName"
- classname="name-changer"
- >
- </EmojiInput>
- <p>{{ $t('settings.bio') }}</p>
- <EmojiInput
- v-model="newBio"
- enable-emoji-picker
- :suggest="emojiUserSuggestor"
- >
- <textarea
- v-model="newBio"
- classname="bio"
- />
- </EmojiInput>
- <p>
- <Checkbox v-model="newLocked">
- {{ $t('settings.lock_account_description') }}
- </Checkbox>
- </p>
- <div>
- <label for="default-vis">{{ $t('settings.default_vis') }}</label>
- <div
- id="default-vis"
- class="visibility-tray"
- >
- <scope-selector
- :show-all="true"
- :user-default="newDefaultScope"
- :initial-scope="newDefaultScope"
- :on-scope-change="changeVis"
- />
- </div>
- </div>
- <p>
- <Checkbox v-model="newNoRichText">
- {{ $t('settings.no_rich_text_description') }}
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="hideFollows">
- {{ $t('settings.hide_follows_description') }}
- </Checkbox>
- </p>
- <p class="setting-subitem">
- <Checkbox
- v-model="hideFollowsCount"
- :disabled="!hideFollows"
- >
- {{ $t('settings.hide_follows_count_description') }}
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="hideFollowers">
- {{ $t('settings.hide_followers_description') }}
- </Checkbox>
- </p>
- <p class="setting-subitem">
- <Checkbox
- v-model="hideFollowersCount"
- :disabled="!hideFollowers"
- >
- {{ $t('settings.hide_followers_count_description') }}
- </Checkbox>
- </p>
- <p>
- <Checkbox v-model="allowFollowingMove">
- {{ $t('settings.allow_following_move') }}
- </Checkbox>
- </p>
- <p v-if="role === 'admin' || role === 'moderator'">
- <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>
- <Checkbox v-model="discoverable">
- {{ $t('settings.discoverable') }}
- </Checkbox>
- </p>
- <button
- :disabled="newName && newName.length === 0"
- class="btn btn-default"
- @click="updateProfile"
- >
- {{ $t('general.submit') }}
- </button>
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.avatar') }}</h2>
- <p class="visibility-notice">
- {{ $t('settings.avatar_size_instruction') }}
- </p>
- <p>{{ $t('settings.current_avatar') }}</p>
- <img
- :src="user.profile_image_url_original"
- class="current-avatar"
- >
- <p>{{ $t('settings.set_new_avatar') }}</p>
- <button
- v-show="pickAvatarBtnVisible"
- id="pick-avatar"
- class="btn"
- type="button"
- >
- {{ $t('settings.upload_a_photo') }}
- </button>
- <image-cropper
- trigger="#pick-avatar"
- :submit-handler="submitAvatar"
- @open="pickAvatarBtnVisible=false"
- @close="pickAvatarBtnVisible=true"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.profile_banner') }}</h2>
- <p>{{ $t('settings.current_profile_banner') }}</p>
- <img
- :src="user.cover_photo"
- class="banner"
- >
- <p>{{ $t('settings.set_new_profile_banner') }}</p>
- <img
- v-if="bannerPreview"
- class="banner"
- :src="bannerPreview"
- >
- <div>
- <input
- type="file"
- @change="uploadFile('banner', $event)"
- >
- </div>
- <i
- v-if="bannerUploading"
- class=" icon-spin4 animate-spin uploading"
- />
- <button
- v-else-if="bannerPreview"
- class="btn btn-default"
- @click="submitBanner"
- >
- {{ $t('general.submit') }}
- </button>
- <div
- v-if="bannerUploadError"
- class="alert error"
- >
- Error: {{ bannerUploadError }}
- <i
- class="button-icon icon-cancel"
- @click="clearUploadError('banner')"
- />
- </div>
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.profile_background') }}</h2>
- <p>{{ $t('settings.set_new_profile_background') }}</p>
- <img
- v-if="backgroundPreview"
- class="bg"
- :src="backgroundPreview"
- >
- <div>
- <input
- type="file"
- @change="uploadFile('background', $event)"
- >
- </div>
- <i
- v-if="backgroundUploading"
- class=" icon-spin4 animate-spin uploading"
- />
- <button
- v-else-if="backgroundPreview"
- class="btn btn-default"
- @click="submitBg"
- >
- {{ $t('general.submit') }}
- </button>
- <div
- v-if="backgroundUploadError"
- class="alert error"
- >
- Error: {{ backgroundUploadError }}
- <i
- class="button-icon icon-cancel"
- @click="clearUploadError('background')"
- />
- </div>
- </div>
- </div>
-
- <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>
- <input
- v-model="changePasswordInputs[0]"
- type="password"
- >
- </div>
- <div>
- <p>{{ $t('settings.new_password') }}</p>
- <input
- v-model="changePasswordInputs[1]"
- type="password"
- >
- </div>
- <div>
- <p>{{ $t('settings.confirm_new_password') }}</p>
- <input
- v-model="changePasswordInputs[2]"
- type="password"
- >
- </div>
- <button
- class="btn btn-default"
- @click="changePassword"
- >
- {{ $t('general.submit') }}
- </button>
- <p v-if="changedPassword">
- {{ $t('settings.changed_password') }}
- </p>
- <p v-else-if="changePasswordError !== false">
- {{ $t('settings.change_password_error') }}
- </p>
- <p v-if="changePasswordError">
- {{ changePasswordError }}
- </p>
- </div>
-
- <div class="setting-item">
- <h2>{{ $t('settings.oauth_tokens') }}</h2>
- <table class="oauth-tokens">
- <thead>
- <tr>
- <th>{{ $t('settings.app_name') }}</th>
- <th>{{ $t('settings.valid_until') }}</th>
- <th />
- </tr>
- </thead>
- <tbody>
- <tr
- v-for="oauthToken in oauthTokens"
- :key="oauthToken.id"
- >
- <td>{{ oauthToken.appName }}</td>
- <td>{{ oauthToken.validUntil }}</td>
- <td class="actions">
- <button
- class="btn btn-default"
- @click="revokeToken(oauthToken.id)"
- >
- {{ $t('settings.revoke_token') }}
- </button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <mfa />
- <div class="setting-item">
- <h2>{{ $t('settings.delete_account') }}</h2>
- <p v-if="!deletingAccount">
- {{ $t('settings.delete_account_description') }}
- </p>
- <div v-if="deletingAccount">
- <p>{{ $t('settings.delete_account_instructions') }}</p>
- <p>{{ $t('login.password') }}</p>
- <input
- v-model="deleteAccountConfirmPasswordInput"
- type="password"
- >
- <button
- class="btn btn-default"
- @click="deleteAccount"
- >
- {{ $t('settings.delete_account') }}
- </button>
- </div>
- <p v-if="deleteAccountError !== false">
- {{ $t('settings.delete_account_error') }}
- </p>
- <p v-if="deleteAccountError">
- {{ deleteAccountError }}
- </p>
- <button
- v-if="!deletingAccount"
- class="btn btn-default"
- @click="confirmDelete"
- >
- {{ $t('general.submit') }}
- </button>
- </div>
- </div>
-
- <div
- v-if="pleromaBackend"
- :label="$t('settings.notifications')"
- >
- <div class="setting-item">
- <div class="select-multiple">
- <span class="label">{{ $t('settings.notification_setting') }}</span>
- <ul class="option-list">
- <li>
- <Checkbox v-model="notificationSettings.follows">
- {{ $t('settings.notification_setting_follows') }}
- </Checkbox>
- </li>
- <li>
- <Checkbox v-model="notificationSettings.followers">
- {{ $t('settings.notification_setting_followers') }}
- </Checkbox>
- </li>
- <li>
- <Checkbox v-model="notificationSettings.non_follows">
- {{ $t('settings.notification_setting_non_follows') }}
- </Checkbox>
- </li>
- <li>
- <Checkbox v-model="notificationSettings.non_followers">
- {{ $t('settings.notification_setting_non_followers') }}
- </Checkbox>
- </li>
- </ul>
- </div>
- <p>{{ $t('settings.notification_mutes') }}</p>
- <p>{{ $t('settings.notification_blocks') }}</p>
- <button
- class="btn btn-default"
- @click="updateNotificationSettings"
- >
- {{ $t('general.submit') }}
- </button>
- </div>
- </div>
-
- <div
- v-if="pleromaBackend"
- :label="$t('settings.data_import_export_tab')"
- >
- <div class="setting-item">
- <h2>{{ $t('settings.follow_import') }}</h2>
- <p>{{ $t('settings.import_followers_from_a_csv_file') }}</p>
- <Importer
- :submit-handler="importFollows"
- :success-message="$t('settings.follows_imported')"
- :error-message="$t('settings.follow_import_error')"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.follow_export') }}</h2>
- <Exporter
- :get-content="getFollowsContent"
- filename="friends.csv"
- :export-button-label="$t('settings.follow_export_button')"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.block_import') }}</h2>
- <p>{{ $t('settings.import_blocks_from_a_csv_file') }}</p>
- <Importer
- :submit-handler="importBlocks"
- :success-message="$t('settings.blocks_imported')"
- :error-message="$t('settings.block_import_error')"
- />
- </div>
- <div class="setting-item">
- <h2>{{ $t('settings.block_export') }}</h2>
- <Exporter
- :get-content="getBlocksContent"
- filename="blocks.csv"
- :export-button-label="$t('settings.block_export_button')"
- />
- </div>
- </div>
-
- <div :label="$t('settings.blocks_tab')">
- <div class="profile-edit-usersearch-wrapper">
- <Autosuggest
- :filter="filterUnblockedUsers"
- :query="queryUserIds"
- :placeholder="$t('settings.search_user_to_block')"
- >
- <BlockCard
- slot-scope="row"
- :user-id="row.item"
- />
- </Autosuggest>
- </div>
- <BlockList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => blockUsers(selected)"
- >
- {{ $t('user_card.block') }}
- <template slot="progress">
- {{ $t('user_card.block_progress') }}
- </template>
- </ProgressButton>
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => unblockUsers(selected)"
- >
- {{ $t('user_card.unblock') }}
- <template slot="progress">
- {{ $t('user_card.unblock_progress') }}
- </template>
- </ProgressButton>
- </div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <BlockCard :user-id="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_blocks') }}
- </template>
- </BlockList>
- </div>
-
- <div :label="$t('settings.mutes_tab')">
- <tab-switcher>
- <div label="Users">
- <div class="profile-edit-usersearch-wrapper">
- <Autosuggest
- :filter="filterUnMutedUsers"
- :query="queryUserIds"
- :placeholder="$t('settings.search_user_to_mute')"
- >
- <MuteCard
- slot-scope="row"
- :user-id="row.item"
- />
- </Autosuggest>
- </div>
- <MuteList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => muteUsers(selected)"
- >
- {{ $t('user_card.mute') }}
- <template slot="progress">
- {{ $t('user_card.mute_progress') }}
- </template>
- </ProgressButton>
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => unmuteUsers(selected)"
- >
- {{ $t('user_card.unmute') }}
- <template slot="progress">
- {{ $t('user_card.unmute_progress') }}
- </template>
- </ProgressButton>
- </div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <MuteCard :user-id="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_mutes') }}
- </template>
- </MuteList>
- </div>
-
- <div :label="$t('settings.domain_mutes')">
- <div class="profile-edit-domain-mute-form">
- <input
- v-model="newDomainToMute"
- :placeholder="$t('settings.type_domains_to_mute')"
- type="text"
- @keyup.enter="muteDomain"
- >
- <ProgressButton
- class="btn btn-default"
- :click="muteDomain"
- >
- {{ $t('domain_mute_card.mute') }}
- <template slot="progress">
- {{ $t('domain_mute_card.mute_progress') }}
- </template>
- </ProgressButton>
- </div>
- <DomainMuteList
- :refresh="true"
- :get-key="identity"
- >
- <template
- slot="header"
- slot-scope="{selected}"
- >
- <div class="profile-edit-bulk-actions">
- <ProgressButton
- v-if="selected.length > 0"
- class="btn btn-default"
- :click="() => unmuteDomains(selected)"
- >
- {{ $t('domain_mute_card.unmute') }}
- <template slot="progress">
- {{ $t('domain_mute_card.unmute_progress') }}
- </template>
- </ProgressButton>
- </div>
- </template>
- <template
- slot="item"
- slot-scope="{item}"
- >
- <DomainMuteCard :domain="item" />
- </template>
- <template slot="empty">
- {{ $t('settings.no_mutes') }}
- </template>
- </DomainMuteList>
- </div>
- </tab-switcher>
- </div>
- </tab-switcher>
</div>
</div>
</template>
<script src="./user_settings.js">
</script>
<style lang="scss">
@import '../../_variables.scss';
.profile-edit {
.bio {
margin: 0;
}
.visibility-tray {
padding-top: 5px;
}
input[type=file] {
padding: 5px;
height: auto;
}
.banner {
max-width: 100%;
}
.uploading {
font-size: 1.5em;
margin: 0.25em;
}
.name-changer {
width: 100%;
}
.bg {
max-width: 100%;
}
.current-avatar {
display: block;
width: 150px;
height: 150px;
border-radius: $fallback--avatarRadius;
border-radius: var(--avatarRadius, $fallback--avatarRadius);
}
.oauth-tokens {
width: 100%;
th {
text-align: left;
}
.actions {
text-align: right;
}
}
&-usersearch-wrapper {
padding: 1em;
}
&-bulk-actions {
text-align: right;
padding: 0 1em;
min-height: 28px;
button {
width: 10em;
}
}
&-domain-mute-form {
padding: 1em;
display: flex;
flex-direction: column;
button {
align-self: flex-end;
margin-top: 1em;
width: 10em;
}
}
.setting-subitem {
margin-left: 1.75em;
}
}
</style>
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 37d9591c19..312f728329 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1,739 +1,740 @@
{
"about": {
"mrf": {
"federation": "Federation",
"keyword": {
"keyword_policies": "Keyword Policies",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"reject": "Reject",
"replace": "Replace",
"is_replaced_by": "→"
},
"mrf_policies": "Enabled MRF Policies",
"mrf_policies_desc": "MRF policies manipulate the federation behaviour of the instance. The following policies are enabled:",
"simple": {
"simple_policies": "Instance-specific Policies",
"accept": "Accept",
"accept_desc": "This instance only accepts messages from the following instances:",
"reject": "Reject",
"reject_desc": "This instance will not accept messages from the following instances:",
"quarantine": "Quarantine",
"quarantine_desc": "This instance will send only public posts to the following instances:",
"ftl_removal": "Removal from \"The Whole Known Network\" Timeline",
"ftl_removal_desc": "This instance removes these instances from \"The Whole Known Network\" timeline:",
"media_removal": "Media Removal",
"media_removal_desc": "This instance removes media from posts on the following instances:",
"media_nsfw": "Media Force-set As Sensitive",
"media_nsfw_desc": "This instance forces media to be set sensitive in posts on the following instances:"
}
},
"staff": "Staff"
},
"chat": {
"title": "Chat"
},
"domain_mute_card": {
"mute": "Mute",
"mute_progress": "Muting...",
"unmute": "Unmute",
"unmute_progress": "Unmuting..."
},
"exporter": {
"export": "Export",
"processing": "Processing, you'll soon be asked to download your file"
},
"features_panel": {
"chat": "Chat",
"gopher": "Gopher",
"media_proxy": "Media proxy",
"scope_options": "Scope options",
"text_limit": "Text limit",
"title": "Features",
"who_to_follow": "Who to follow"
},
"finder": {
"error_fetching_user": "Error fetching user",
"find_user": "Find user"
},
"general": {
"apply": "Apply",
"submit": "Submit",
"more": "More",
"generic_error": "An error occured",
"optional": "optional",
"show_more": "Show more",
"show_less": "Show less",
"dismiss": "Dismiss",
"cancel": "Cancel",
"disable": "Disable",
"enable": "Enable",
"confirm": "Confirm",
"verify": "Verify"
},
"image_cropper": {
"crop_picture": "Crop picture",
"save": "Save",
"save_without_cropping": "Save without cropping",
"cancel": "Cancel"
},
"importer": {
"submit": "Submit",
"success": "Imported successfully.",
"error": "An error occured while importing this file."
},
"login": {
"login": "Log in",
"description": "Log in with OAuth",
"logout": "Log out",
"password": "Password",
"placeholder": "e.g. lain",
"register": "Register",
"username": "Username",
"hint": "Log in to join the discussion",
"authentication_code": "Authentication code",
"enter_recovery_code": "Enter a recovery code",
"enter_two_factor_code": "Enter a two-factor code",
"recovery_code": "Recovery code",
"heading" : {
"totp" : "Two-factor authentication",
"recovery" : "Two-factor recovery"
}
},
"media_modal": {
"previous": "Previous",
"next": "Next"
},
"nav": {
"about": "About",
"administration": "Administration",
"back": "Back",
"chat": "Local Chat",
"friend_requests": "Follow Requests",
"mentions": "Mentions",
"interactions": "Interactions",
"dms": "Direct Messages",
"public_tl": "Public Timeline",
"timeline": "Timeline",
"twkn": "The Whole Known Network",
"user_search": "User Search",
"search": "Search",
"who_to_follow": "Who to follow",
"preferences": "Preferences"
},
"notifications": {
"broken_favorite": "Unknown status, searching for it...",
"favorited_you": "favorited your status",
"followed_you": "followed you",
"follow_request": "wants to follow you",
"load_older": "Load older notifications",
"notifications": "Notifications",
"read": "Read!",
"repeated_you": "repeated your status",
"no_more_notifications": "No more notifications",
"migrated_to": "migrated to",
"reacted_with": "reacted with {0}"
},
"polls": {
"add_poll": "Add Poll",
"add_option": "Add Option",
"option": "Option",
"votes": "votes",
"vote": "Vote",
"type": "Poll type",
"single_choice": "Single choice",
"multiple_choices": "Multiple choices",
"expiry": "Poll age",
"expires_in": "Poll ends in {0}",
"expired": "Poll ended {0} ago",
"not_enough_options": "Too few unique options in poll"
},
"emoji": {
"stickers": "Stickers",
"emoji": "Emoji",
"keep_open": "Keep picker open",
"search_emoji": "Search for an emoji",
"add_emoji": "Insert emoji",
"custom": "Custom emoji",
"unicode": "Unicode emoji",
"load_all_hint": "Loaded first {saneAmount} emoji, loading all emoji may cause performance issues.",
"load_all": "Loading all {emojiAmount} emoji"
},
"interactions": {
"favs_repeats": "Repeats and Favorites",
"follows": "New follows",
"moves": "User migrates",
"load_older": "Load older interactions"
},
"post_status": {
"new_status": "Post new status",
"account_not_locked_warning": "Your account is not {0}. Anyone can follow you to view your follower-only posts.",
"account_not_locked_warning_link": "locked",
"attachments_sensitive": "Mark attachments as sensitive",
"content_type": {
"text/plain": "Plain text",
"text/html": "HTML",
"text/markdown": "Markdown",
"text/bbcode": "BBCode"
},
"content_warning": "Subject (optional)",
"default": "Just landed in L.A.",
"direct_warning_to_all": "This post will be visible to all the mentioned users.",
"direct_warning_to_first_only": "This post will only be visible to the mentioned users at the beginning of the message.",
"posting": "Posting",
"scope_notice": {
"public": "This post will be visible to everyone",
"private": "This post will be visible to your followers only",
"unlisted": "This post will not be visible in Public Timeline and The Whole Known Network"
},
"scope": {
"direct": "Direct - Post to mentioned users only",
"private": "Followers-only - Post to followers only",
"public": "Public - Post to public timelines",
"unlisted": "Unlisted - Do not post to public timelines"
}
},
"registration": {
"bio": "Bio",
"email": "Email",
"fullname": "Display name",
"password_confirm": "Password confirmation",
"registration": "Registration",
"token": "Invite token",
"captcha": "CAPTCHA",
"new_captcha": "Click the image to get a new captcha",
"username_placeholder": "e.g. lain",
"fullname_placeholder": "e.g. Lain Iwakura",
"bio_placeholder": "e.g.\nHi, I'm Lain.\nI’m an anime girl living in suburban Japan. You may know me from the Wired.",
"validations": {
"username_required": "cannot be left blank",
"fullname_required": "cannot be left blank",
"email_required": "cannot be left blank",
"password_required": "cannot be left blank",
"password_confirmation_required": "cannot be left blank",
"password_confirmation_match": "should be the same as password"
}
},
"remote_user_resolver": {
"remote_user_resolver": "Remote user resolver",
"searching_for": "Searching for",
"error": "Not found."
},
"selectable_list": {
"select_all": "Select all"
},
"settings": {
"app_name": "App name",
"security": "Security",
"enter_current_password_to_confirm": "Enter your current password to confirm your identity",
"mfa": {
"otp" : "OTP",
"setup_otp" : "Setup OTP",
"wait_pre_setup_otp" : "presetting OTP",
"confirm_and_enable" : "Confirm & enable OTP",
"title": "Two-factor Authentication",
"generate_new_recovery_codes" : "Generate new recovery codes",
"warning_of_generate_new_codes" : "When you generate new recovery codes, your old codes won’t work anymore.",
"recovery_codes" : "Recovery codes.",
"waiting_a_recovery_codes": "Receiving backup codes...",
"recovery_codes_warning" : "Write the codes down or save them somewhere secure - otherwise you won't see them again. If you lose access to your 2FA app and recovery codes you'll be locked out of your account.",
"authentication_methods" : "Authentication methods",
"scan": {
"title": "Scan",
"desc": "Using your two-factor app, scan this QR code or enter text key:",
"secret_code": "Key"
},
"verify": {
"desc": "To enable two-factor authentication, enter the code from your two-factor app:"
}
},
"allow_following_move": "Allow auto-follow when following account moves",
"attachmentRadius": "Attachments",
"attachments": "Attachments",
"autoload": "Enable automatic loading when scrolled to the bottom",
"avatar": "Avatar",
"avatarAltRadius": "Avatars (Notifications)",
"avatarRadius": "Avatars",
"background": "Background",
"bio": "Bio",
"block_export": "Block export",
"block_export_button": "Export your blocks to a csv file",
"block_import": "Block import",
"block_import_error": "Error importing blocks",
"blocks_imported": "Blocks imported! Processing them will take a while.",
"blocks_tab": "Blocks",
"btnRadius": "Buttons",
"cBlue": "Blue (Reply, follow)",
"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!",
"collapse_subject": "Collapse posts with subjects",
"composing": "Composing",
"confirm_new_password": "Confirm new password",
"current_avatar": "Your current avatar",
"current_password": "Current password",
"current_profile_banner": "Your current profile banner",
+ "mutes_and_blocks": "Mutes and Blocks",
"data_import_export_tab": "Data Import / Export",
"default_vis": "Default visibility scope",
"delete_account": "Delete Account",
"delete_account_description": "Permanently delete your account and all your messages.",
"delete_account_error": "There was an issue deleting your account. If this persists please contact your instance administrator.",
"delete_account_instructions": "Type your password in the input below to confirm account deletion.",
"discoverable": "Allow discovery of this account in search results and other services",
"domain_mutes": "Domains",
"avatar_size_instruction": "The recommended minimum size for avatar images is 150x150 pixels.",
"pad_emoji": "Pad emoji with spaces when adding from picker",
"emoji_reactions_on_timeline": "Show emoji reactions on timeline",
"export_theme": "Save preset",
"filtering": "Filtering",
"filtering_explanation": "All statuses containing these words will be muted, one per line",
"follow_export": "Follow export",
"follow_export_button": "Export your follows to a csv file",
"follow_import": "Follow import",
"follow_import_error": "Error importing followers",
"follows_imported": "Follows imported! Processing them will take a while.",
"accent": "Accent",
"foreground": "Foreground",
"general": "General",
"hide_attachments_in_convo": "Hide attachments in conversations",
"hide_attachments_in_tl": "Hide attachments in timeline",
"hide_muted_posts": "Hide posts of muted users",
"max_thumbnails": "Maximum amount of thumbnails per post",
"hide_isp": "Hide instance-specific panel",
"preload_images": "Preload images",
"use_one_click_nsfw": "Open NSFW attachments with just one click",
"hide_post_stats": "Hide post statistics (e.g. the number of favorites)",
"hide_user_stats": "Hide user statistics (e.g. the number of followers)",
"hide_filtered_statuses": "Hide filtered statuses",
"import_blocks_from_a_csv_file": "Import blocks from a csv file",
"import_followers_from_a_csv_file": "Import follows from a csv file",
"import_theme": "Load preset",
"inputRadius": "Input fields",
"checkboxRadius": "Checkboxes",
"instance_default": "(default: {value})",
"instance_default_simple": "(default)",
"interface": "Interface",
"interfaceLanguage": "Interface language",
"invalid_theme_imported": "The selected file is not a supported Pleroma theme. No changes to your theme were made.",
"limited_availability": "Unavailable in your browser",
"links": "Links",
"lock_account_description": "Restrict your account to approved followers only",
"loop_video": "Loop videos",
"loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")",
"mutes_tab": "Mutes",
"play_videos_in_modal": "Play videos in a popup frame",
"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",
"notification_visibility_likes": "Likes",
"notification_visibility_mentions": "Mentions",
"notification_visibility_repeats": "Repeats",
"notification_visibility_moves": "User Migrates",
"notification_visibility_emoji_reactions": "Reactions",
"no_rich_text_description": "Strip rich text formatting from all posts",
"no_blocks": "No blocks",
"no_mutes": "No mutes",
"hide_follows_description": "Don't show who I'm following",
"hide_followers_description": "Don't show who's following me",
"hide_follows_count_description": "Don't show follow count",
"hide_followers_count_description": "Don't show follower count",
"show_admin_badge": "Show Admin badge in my profile",
"show_moderator_badge": "Show Moderator badge in my profile",
"nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding",
"oauth_tokens": "OAuth tokens",
"token": "Token",
"refresh_token": "Refresh Token",
"valid_until": "Valid Until",
"revoke_token": "Revoke",
"panelRadius": "Panels",
"pause_on_unfocused": "Pause streaming when tab is not focused",
"presets": "Presets",
"profile_background": "Profile Background",
"profile_banner": "Profile Banner",
"profile_tab": "Profile",
"radii_help": "Set up interface edge rounding (in pixels)",
"replies_in_timeline": "Replies in timeline",
"reply_link_preview": "Enable reply-link preview on mouse hover",
"reply_visibility_all": "Show all replies",
"reply_visibility_following": "Only show replies directed at me or users I'm following",
"reply_visibility_self": "Only show replies directed at me",
"autohide_floating_post_button": "Automatically hide New Post button (mobile)",
"saving_err": "Error saving settings",
"saving_ok": "Settings saved",
"search_user_to_block": "Search whom you want to block",
"search_user_to_mute": "Search whom you want to mute",
"security_tab": "Security",
"scope_copy": "Copy scope when replying (DMs are always copied)",
"minimal_scopes_mode": "Minimize post scope selection options",
"set_new_avatar": "Set new avatar",
"set_new_profile_background": "Set new profile background",
"set_new_profile_banner": "Set new profile banner",
"settings": "Settings",
"subject_input_always_show": "Always show subject field",
"subject_line_behavior": "Copy subject when replying",
"subject_line_email": "Like email: \"re: subject\"",
"subject_line_mastodon": "Like mastodon: copy as is",
"subject_line_noop": "Do not copy",
"post_status_content_type": "Post status content type",
"stop_gifs": "Play-on-hover GIFs",
"streaming": "Enable automatic streaming of new posts when scrolled to the top",
"user_mutes": "Users",
"useStreamingApi": "Receive posts and notifications real-time",
"useStreamingApiWarning": "(Not recommended, experimental, known to skip posts)",
"text": "Text",
"theme": "Theme",
"theme_help": "Use hex color codes (#rrggbb) to customize your color theme.",
"theme_help_v2_1": "You can also override certain component's colors and opacity by toggling the checkbox, use \"Clear all\" button to clear all overrides.",
"theme_help_v2_2": "Icons underneath some entries are background/text contrast indicators, hover over for detailed info. Please keep in mind that when using transparency contrast indicators show the worst possible case.",
"tooltipRadius": "Tooltips/alerts",
"type_domains_to_mute": "Type in domains to mute",
"upload_a_photo": "Upload a photo",
"user_settings": "User Settings",
"values": {
"false": "no",
"true": "yes"
},
"fun": "Fun",
"greentext": "Meme arrows",
"notifications": "Notifications",
"notification_setting": "Receive notifications from:",
"notification_setting_follows": "Users you follow",
"notification_setting_non_follows": "Users you do not follow",
"notification_setting_followers": "Users who follow you",
"notification_setting_non_followers": "Users who do not follow you",
"notification_mutes": "To stop receiving notifications from a specific user, use a mute.",
"notification_blocks": "Blocking a user stops all notifications as well as unsubscribes them.",
"enable_web_push_notifications": "Enable web push notifications",
"style": {
"switcher": {
"keep_color": "Keep colors",
"keep_shadows": "Keep shadows",
"keep_opacity": "Keep opacity",
"keep_roundness": "Keep roundness",
"keep_fonts": "Keep fonts",
"save_load_hint": "\"Keep\" options preserve currently set options when selecting or loading themes, it also stores said options when exporting a theme. When all checkboxes unset, exporting theme will save everything.",
"reset": "Reset",
"clear_all": "Clear all",
"clear_opacity": "Clear opacity",
"load_theme": "Load theme",
"keep_as_is": "Keep as is",
"use_snapshot": "Old version",
"use_source": "New version",
"help": {
"upgraded_from_v2": "PleromaFE has been upgraded, theme could look a little bit different than you remember.",
"v2_imported": "File you imported was made for older FE. We try to maximize compatibility but there still could be inconsitencies.",
"future_version_imported": "File you imported was made in newer version of FE.",
"older_version_imported": "File you imported was made in older version of FE.",
"snapshot_present": "Theme snapshot is loaded, so all values are overriden. You can load theme's actual data instead.",
"snapshot_missing": "No theme snapshot was in the file so it could look different than originally envisioned.",
"fe_upgraded": "PleromaFE's theme engine upgraded after version update.",
"fe_downgraded": "PleromaFE's version rolled back.",
"migration_snapshot_ok": "Just to be safe, theme snapshot loaded. You can try loading theme data.",
"migration_napshot_gone": "For whatever reason snapshot was missing, some stuff could look different than you remember.",
"snapshot_source_mismatch": "Versions conflict: most likely FE was rolled back and updated again, if you changed theme using older version of FE you most likely want to use old version, otherwise use new version."
}
},
"common": {
"color": "Color",
"opacity": "Opacity",
"contrast": {
"hint": "Contrast ratio is {ratio}, it {level} {context}",
"level": {
"aa": "meets Level AA guideline (minimal)",
"aaa": "meets Level AAA guideline (recommended)",
"bad": "doesn't meet any accessibility guidelines"
},
"context": {
"18pt": "for large (18pt+) text",
"text": "for text"
}
}
},
"common_colors": {
"_tab_label": "Common",
"main": "Common colors",
"foreground_hint": "See \"Advanced\" tab for more detailed control",
"rgbo": "Icons, accents, badges"
},
"advanced_colors": {
"_tab_label": "Advanced",
"alert": "Alert background",
"alert_error": "Error",
"alert_warning": "Warning",
"alert_neutral": "Neutral",
"post": "Posts/User bios",
"badge": "Badge background",
"popover": "Tooltips, menus, popovers",
"badge_notification": "Notification",
"panel_header": "Panel header",
"top_bar": "Top bar",
"borders": "Borders",
"buttons": "Buttons",
"inputs": "Input fields",
"faint_text": "Faded text",
"underlay": "Underlay",
"poll": "Poll graph",
"icons": "Icons",
"highlight": "Highlighted elements",
"pressed": "Pressed",
"selectedPost": "Selected post",
"selectedMenu": "Selected menu item",
"disabled": "Disabled",
"toggled": "Toggled",
"tabs": "Tabs"
},
"radii": {
"_tab_label": "Roundness"
},
"shadows": {
"_tab_label": "Shadow and lighting",
"component": "Component",
"override": "Override",
"shadow_id": "Shadow #{value}",
"blur": "Blur",
"spread": "Spread",
"inset": "Inset",
"hintV3": "For shadows you can also use the {0} notation to use other color slot.",
"filter_hint": {
"always_drop_shadow": "Warning, this shadow always uses {0} when browser supports it.",
"drop_shadow_syntax": "{0} does not support {1} parameter and {2} keyword.",
"avatar_inset": "Please note that combining both inset and non-inset shadows on avatars might give unexpected results with transparent avatars.",
"spread_zero": "Shadows with spread > 0 will appear as if it was set to zero",
"inset_classic": "Inset shadows will be using {0}"
},
"components": {
"panel": "Panel",
"panelHeader": "Panel header",
"topBar": "Top bar",
"avatar": "User avatar (in profile view)",
"avatarStatus": "User avatar (in post display)",
"popup": "Popups and tooltips",
"button": "Button",
"buttonHover": "Button (hover)",
"buttonPressed": "Button (pressed)",
"buttonPressedHover": "Button (pressed+hover)",
"input": "Input field"
}
},
"fonts": {
"_tab_label": "Fonts",
"help": "Select font to use for elements of UI. For \"custom\" you have to enter exact font name as it appears in system.",
"components": {
"interface": "Interface",
"input": "Input fields",
"post": "Post text",
"postCode": "Monospaced text in a post (rich text)"
},
"family": "Font name",
"size": "Size (in px)",
"weight": "Weight (boldness)",
"custom": "Custom"
},
"preview": {
"header": "Preview",
"content": "Content",
"error": "Example error",
"button": "Button",
"text": "A bunch of more {0} and {1}",
"mono": "content",
"input": "Just landed in L.A.",
"faint_link": "helpful manual",
"fine_print": "Read our {0} to learn nothing useful!",
"header_faint": "This is fine",
"checkbox": "I have skimmed over terms and conditions",
"link": "a nice lil' link"
}
},
"version": {
"title": "Version",
"backend_version": "Backend Version",
"frontend_version": "Frontend Version"
}
},
"time": {
"day": "{0} day",
"days": "{0} days",
"day_short": "{0}d",
"days_short": "{0}d",
"hour": "{0} hour",
"hours": "{0} hours",
"hour_short": "{0}h",
"hours_short": "{0}h",
"in_future": "in {0}",
"in_past": "{0} ago",
"minute": "{0} minute",
"minutes": "{0} minutes",
"minute_short": "{0}min",
"minutes_short": "{0}min",
"month": "{0} month",
"months": "{0} months",
"month_short": "{0}mo",
"months_short": "{0}mo",
"now": "just now",
"now_short": "now",
"second": "{0} second",
"seconds": "{0} seconds",
"second_short": "{0}s",
"seconds_short": "{0}s",
"week": "{0} week",
"weeks": "{0} weeks",
"week_short": "{0}w",
"weeks_short": "{0}w",
"year": "{0} year",
"years": "{0} years",
"year_short": "{0}y",
"years_short": "{0}y"
},
"timeline": {
"collapse": "Collapse",
"conversation": "Conversation",
"error_fetching": "Error fetching updates",
"load_older": "Load older statuses",
"no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated",
"repeated": "repeated",
"show_new": "Show new",
"up_to_date": "Up-to-date",
"no_more_statuses": "No more statuses",
"no_statuses": "No statuses"
},
"status": {
"favorites": "Favorites",
"repeats": "Repeats",
"delete": "Delete status",
"pin": "Pin on profile",
"unpin": "Unpin from profile",
"pinned": "Pinned",
"delete_confirm": "Do you really want to delete this status?",
"reply_to": "Reply to",
"replies_list": "Replies:",
"mute_conversation": "Mute conversation",
"unmute_conversation": "Unmute conversation",
"status_unavailable": "Status unavailable"
},
"user_card": {
"approve": "Approve",
"block": "Block",
"blocked": "Blocked!",
"deny": "Deny",
"favorites": "Favorites",
"follow": "Follow",
"follow_sent": "Request sent!",
"follow_progress": "Requesting…",
"follow_again": "Send request again?",
"follow_unfollow": "Unfollow",
"followees": "Following",
"followers": "Followers",
"following": "Following!",
"follows_you": "Follows you!",
"hidden": "Hidden",
"its_you": "It's you!",
"media": "Media",
"mention": "Mention",
"mute": "Mute",
"muted": "Muted",
"per_day": "per day",
"remote_follow": "Remote follow",
"report": "Report",
"statuses": "Statuses",
"subscribe": "Subscribe",
"unsubscribe": "Unsubscribe",
"unblock": "Unblock",
"unblock_progress": "Unblocking...",
"block_progress": "Blocking...",
"unmute": "Unmute",
"unmute_progress": "Unmuting...",
"mute_progress": "Muting...",
"hide_repeats": "Hide repeats",
"show_repeats": "Show repeats",
"admin_menu": {
"moderation": "Moderation",
"grant_admin": "Grant Admin",
"revoke_admin": "Revoke Admin",
"grant_moderator": "Grant Moderator",
"revoke_moderator": "Revoke Moderator",
"activate_account": "Activate account",
"deactivate_account": "Deactivate account",
"delete_account": "Delete account",
"force_nsfw": "Mark all posts as NSFW",
"strip_media": "Remove media from posts",
"force_unlisted": "Force posts to be unlisted",
"sandbox": "Force posts to be followers-only",
"disable_remote_subscription": "Disallow following user from remote instances",
"disable_any_subscription": "Disallow following user at all",
"quarantine": "Disallow user posts from federating",
"delete_user": "Delete user",
"delete_user_confirmation": "Are you absolutely sure? This action cannot be undone."
}
},
"user_profile": {
"timeline_title": "User Timeline",
"profile_does_not_exist": "Sorry, this profile does not exist.",
"profile_loading_error": "Sorry, there was an error loading this profile."
},
"user_reporting": {
"title": "Reporting {0}",
"add_comment_description": "The report will be sent to your instance moderators. You can provide an explanation of why you are reporting this account below:",
"additional_comments": "Additional comments",
"forward_description": "The account is from another server. Send a copy of the report there as well?",
"forward_to": "Forward to {0}",
"submit": "Submit",
"generic_error": "An error occurred while processing your request."
},
"who_to_follow": {
"more": "More",
"who_to_follow": "Who to follow"
},
"tool_tip": {
"media_upload": "Upload Media",
"repeat": "Repeat",
"reply": "Reply",
"favorite": "Favorite",
"add_reaction": "Add Reaction",
"user_settings": "User Settings",
"accept_follow_request": "Accept follow request",
"reject_follow_request": "Reject follow request"
},
"upload":{
"error": {
"base": "Upload failed.",
"file_too_big": "File too big [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
"default": "Try again later"
},
"file_size_units": {
"B": "B",
"KiB": "KiB",
"MiB": "MiB",
"GiB": "GiB",
"TiB": "TiB"
}
},
"search": {
"people": "People",
"hashtags": "Hashtags",
"person_talking": "{count} person talking",
"people_talking": "{count} people talking",
"no_results": "No results"
},
"password_reset": {
"forgot_password": "Forgot password?",
"password_reset": "Password reset",
"instruction": "Enter your email address or username. We will send you a link to reset your password.",
"placeholder": "Your email or username",
"check_email": "Check your email for a link to reset your password.",
"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_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/modules/interface.js b/src/modules/interface.js
index 5b2762e5ba..e55b729047 100644
--- a/src/modules/interface.js
+++ b/src/modules/interface.js
@@ -1,56 +1,84 @@
import { set, delete as del } from 'vue'
const defaultState = {
+ settingsModalState: 'hidden',
settings: {
currentSaveStateNotice: null,
noticeClearTimeout: null,
notificationPermission: null
},
browserSupport: {
cssFilter: window.CSS && window.CSS.supports && (
window.CSS.supports('filter', 'drop-shadow(0 0)') ||
window.CSS.supports('-webkit-filter', 'drop-shadow(0 0)')
)
},
mobileLayout: false
}
const interfaceMod = {
state: defaultState,
mutations: {
settingsSaved (state, { success, error }) {
if (success) {
if (state.noticeClearTimeout) {
clearTimeout(state.noticeClearTimeout)
}
set(state.settings, 'currentSaveStateNotice', { error: false, data: success })
set(state.settings, 'noticeClearTimeout',
setTimeout(() => del(state.settings, 'currentSaveStateNotice'), 2000))
} else {
set(state.settings, 'currentSaveStateNotice', { error: true, errorData: error })
}
},
setNotificationPermission (state, permission) {
state.notificationPermission = permission
},
setMobileLayout (state, value) {
state.mobileLayout = value
+ },
+ closeSettingsModal (state) {
+ state.settingsModalState = 'hidden'
+ },
+ togglePeekSettingsModal (state) {
+ switch (state.settingsModalState) {
+ case 'minimized':
+ state.settingsModalState = 'visible'
+ return
+ case 'visible':
+ state.settingsModalState = 'minimized'
+ return
+ default:
+ throw new Error('Illegal minimization state of settings modal')
+ }
+ },
+ openSettingsModal (state) {
+ state.settingsModalState = 'visible'
}
},
actions: {
setPageTitle ({ rootState }, option = '') {
document.title = `${option} ${rootState.instance.name}`
},
settingsSaved ({ commit, dispatch }, { success, error }) {
commit('settingsSaved', { success, error })
},
setNotificationPermission ({ commit }, permission) {
commit('setNotificationPermission', permission)
},
setMobileLayout ({ commit }, value) {
commit('setMobileLayout', value)
+ },
+ closeSettingsModal ({ commit }) {
+ commit('closeSettingsModal')
+ },
+ openSettingsModal ({ commit }) {
+ commit('openSettingsModal')
+ },
+ togglePeekSettingsModal ({ commit }) {
+ commit('togglePeekSettingsModal')
}
}
}
export default interfaceMod

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 1, 2:59 AM (16 h, 24 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1802291
Default Alt Text
(154 KB)

Event Timeline