...
 
Commits (112)
{
"presets": ["es2015", "stage-2", "env"],
"plugins": ["transform-runtime", "lodash", "transform-vue-jsx"],
"presets": ["@babel/preset-env"],
"plugins": ["@babel/plugin-transform-runtime", "lodash", "@vue/babel-plugin-transform-vue-jsx"],
"comments": false
}
......@@ -24,7 +24,7 @@ test:
- apt install firefox-esr -y --no-install-recommends
- firefox --version
- yarn
- npm run unit
- yarn unit
build:
stage: build
......
......@@ -3,6 +3,7 @@ var config = require('../config')
var utils = require('./utils')
var projectRoot = path.resolve(__dirname, '../')
var ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')
var FontelloPlugin = require("fontello-webpack-plugin")
var env = process.env.NODE_ENV
// check env & config/index.js to decide weither to enable CSS Sourcemaps for the
......@@ -11,6 +12,8 @@ var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap)
var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap)
var useCssSourceMap = cssSourceMapDev || cssSourceMapProd
var now = Date.now()
module.exports = {
entry: {
app: './src/main.js'
......@@ -90,6 +93,14 @@ module.exports = {
new ServiceWorkerWebpackPlugin({
entry: path.join(__dirname, '..', 'src/sw.js'),
filename: 'sw-pleroma.js'
}),
new FontelloPlugin({
config: require('../static/fontello.json'),
name: 'fontello',
output: {
css: 'static/[name].' + now + '.css', // [hash] is not supported. Use the current timestamp instead for versioning.
font: 'static/font/[name].' + now + '.[ext]'
}
})
]
}
......@@ -77,6 +77,9 @@ Use custom image for NSFW'd images
### `showFeaturesPanel`
Show panel showcasing instance features/settings to logged-out visitors
### `hideSitename`
Hide instance name in header
## Indirect configuration
Some features are configured depending on how backend is configured. In general the approach is "if backend allows it there's no need to hide it, if backend doesn't allow it there's no need to show it.
......@@ -96,3 +99,6 @@ Setting this will change the warning text that is displayed for direct messages.
ATTENTION: If you actually want the behavior to change. You will need to set the appropriate option at the backend. See the backend documentation for information about that.
DO NOT activate this without checking the backend configuration first!
### Private Mode
If the `private` instance setting is enabled in the backend, features that are not accessible without authentication, such as the timelines and search will be disabled for unauthenticated users.
......@@ -6,8 +6,6 @@
<title>Pleroma</title>
<!--server-generated-meta-->
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="stylesheet" href="/static/font/css/fontello.css">
<link rel="stylesheet" href="/static/font/css/animation.css">
</head>
<body class="hidden">
<noscript>To use Pleroma, please enable JavaScript.</noscript>
......
......@@ -15,9 +15,8 @@
"lint-fix": "eslint --fix --ext .js,.vue src test/unit/specs test/e2e/specs"
},
"dependencies": {
"@babel/runtime": "^7.7.6",
"@chenfengyuan/vue-qrcode": "^1.0.0",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-lodash": "^3.2.11",
"body-scroll-lock": "^2.6.4",
"chromatism": "^3.0.0",
"cropperjs": "^1.4.3",
......@@ -40,20 +39,17 @@
"whatwg-fetch": "^2.0.3"
},
"devDependencies": {
"@babel/polyfill": "^7.0.0",
"@babel/core": "^7.7.5",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/preset-env": "^7.7.6",
"@babel/register": "^7.7.4",
"@vue/babel-helper-vue-jsx-merge-props": "^1.0.0",
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2",
"@vue/test-utils": "^1.0.0-beta.26",
"autoprefixer": "^6.4.0",
"babel-core": "^6.0.0",
"babel-eslint": "^7.0.0",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.0.0",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.0.0",
"babel-plugin-transform-vue-jsx": "3",
"babel-preset-env": "^1.7.0",
"babel-preset-es2015": "^6.0.0",
"babel-preset-stage-2": "^6.0.0",
"babel-register": "^6.0.0",
"babel-loader": "^8.0.6",
"babel-plugin-lodash": "^3.3.4",
"chai": "^3.5.0",
"chalk": "^1.1.3",
"chromedriver": "^2.21.2",
......@@ -72,6 +68,7 @@
"eventsource-polyfill": "^0.9.6",
"express": "^4.13.3",
"file-loader": "^3.0.1",
"fontello-webpack-plugin": "https://github.com/w3geo/fontello-webpack-plugin.git#6149eac8f2672ab6da089e8802fbfcac98487186",
"function-bind": "^1.0.2",
"html-webpack-plugin": "^3.0.0",
"http-proxy-middleware": "^0.17.2",
......
......@@ -90,6 +90,7 @@ export default {
},
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 &&
......@@ -97,7 +98,8 @@ export default {
this.$store.state.instance.instanceSpecificPanelContent
},
showFeaturesPanel () { return this.$store.state.instance.showFeaturesPanel },
isMobileLayout () { return this.$store.state.interface.mobileLayout }
isMobileLayout () { return this.$store.state.interface.mobileLayout },
privateMode () { return this.$store.state.instance.private }
},
methods: {
scrollToTop () {
......
......@@ -855,3 +855,31 @@ nav {
.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);
}
......@@ -31,6 +31,7 @@
</div>
<div class="item">
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
......@@ -40,6 +41,7 @@
</div>
<div class="item right">
<search-bar
v-if="currentUser || !privateMode"
class="nav-icon mobile-hidden"
@toggled="onSearchBarToggled"
@click.stop.native
......
......@@ -108,6 +108,7 @@ const setSettings = async ({ apiConfig, staticConfig, store }) => {
copyInstanceOption('alwaysShowSubjectInput')
copyInstanceOption('noAttachmentLinks')
copyInstanceOption('showFeaturesPanel')
copyInstanceOption('hideSitename')
return store.dispatch('setTheme', config['theme'])
}
......@@ -218,12 +219,21 @@ const getNodeInfo = async ({ store }) => {
store.dispatch('setInstanceOption', { name: 'backendVersion', value: software.version })
store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: software.name === 'pleroma' })
const priv = metadata.private
store.dispatch('setInstanceOption', { name: 'private', value: priv })
const frontendVersion = window.___pleromafe_commit_hash
store.dispatch('setInstanceOption', { name: 'frontendVersion', value: frontendVersion })
store.dispatch('setInstanceOption', { name: 'tagPolicyAvailable', value: metadata.federation.mrf_policies.includes('TagPolicy') })
const federation = metadata.federation
store.dispatch('setInstanceOption', { name: 'federationPolicy', value: federation })
store.dispatch('setInstanceOption', {
name: 'federating',
value: typeof federation.enabled === 'undefined'
? true
: federation.enabled
})
const accounts = metadata.staffAccounts
await resolveStaffAccounts({ store, accounts })
......
......@@ -25,9 +25,6 @@ const AccountActions = {
},
reportUser () {
this.$store.dispatch('openUserReportingModal', this.user.id)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}
......
......@@ -9,17 +9,7 @@
>
<div slot="popover">
<div class="dropdown-menu">
<button
class="btn btn-default btn-block dropdown-item"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
<template v-if="user.following">
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.showing_reblogs"
class="btn btn-default dropdown-item"
......@@ -34,11 +24,11 @@
>
{{ $t('user_card.show_repeats') }}
</button>
<div
role="separator"
class="dropdown-divider"
/>
</template>
<div
role="separator"
class="dropdown-divider"
/>
<button
v-if="user.statusnet_blocking"
class="btn btn-default btn-block dropdown-item"
......
......@@ -43,7 +43,8 @@ const conversation = {
'collapsable',
'isPage',
'pinnedStatusIdsObject',
'inProfile'
'inProfile',
'profileUserId'
],
created () {
if (this.isPage) {
......
......@@ -27,6 +27,7 @@
:highlight="getHighlight()"
:replies="getReplies(status.id)"
:in-profile="inProfile"
:profile-user-id="profileUserId"
class="status-fadein panel-body"
@goto="setHighlight"
@toggleExpanded="toggleExpanded"
......
......@@ -7,11 +7,11 @@ const FollowRequestCard = {
},
methods: {
approveUser () {
this.$store.state.api.backendInteractor.approveUser(this.user.id)
this.$store.state.api.backendInteractor.approveUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
},
denyUser () {
this.$store.state.api.backendInteractor.denyUser(this.user.id)
this.$store.state.api.backendInteractor.denyUser({ id: this.user.id })
this.$store.dispatch('removeFollowRequest', this.user)
}
}
......
......@@ -58,7 +58,7 @@ const LoginForm = {
).then((result) => {
if (result.error) {
if (result.error === 'mfa_required') {
this.requireMFA({ app: app, settings: result })
this.requireMFA({ settings: result })
} else if (result.identifier === 'password_reset_required') {
this.$router.push({ name: 'password-reset', params: { passwordResetRequested: true } })
} else {
......
......@@ -8,18 +8,23 @@ export default {
}),
computed: {
...mapGetters({
authApp: 'authFlow/app',
authSettings: 'authFlow/settings'
}),
...mapState({ instance: 'instance' })
...mapState({
instance: 'instance',
oauth: 'oauth'
})
},
methods: {
...mapMutations('authFlow', ['requireTOTP', 'abortMFA']),
...mapActions({ login: 'authFlow/login' }),
clearError () { this.error = false },
submit () {
const { clientId, clientSecret } = this.oauth
const data = {
app: this.authApp,
clientId,
clientSecret,
instance: this.instance.server,
mfaToken: this.authSettings.mfa_token,
code: this.code
......
......@@ -7,18 +7,23 @@ export default {
}),
computed: {
...mapGetters({
authApp: 'authFlow/app',
authSettings: 'authFlow/settings'
}),
...mapState({ instance: 'instance' })
...mapState({
instance: 'instance',
oauth: 'oauth'
})
},
methods: {
...mapMutations('authFlow', ['requireRecovery', 'abortMFA']),
...mapActions({ login: 'authFlow/login' }),
clearError () { this.error = false },
submit () {
const { clientId, clientSecret } = this.oauth
const data = {
app: this.authApp,
clientId,
clientSecret,
instance: this.instance.server,
mfaToken: this.authSettings.mfa_token,
code: this.code
......
......@@ -29,6 +29,7 @@ const MobileNav = {
unseenNotificationsCount () {
return this.unseenNotifications.length
},
hideSitename () { return this.$store.state.instance.hideSitename },
sitename () { return this.$store.state.instance.name }
},
methods: {
......
......@@ -17,6 +17,7 @@
<i class="button-icon icon-menu" />
</a>
<router-link
v-if="!hideSitename"
class="site-name"
:to="{ name: 'root' }"
active-class="home"
......
......@@ -45,12 +45,12 @@ const ModerationTools = {
toggleTag (tag) {
const store = this.$store
if (this.tagsSet.has(tag)) {
store.state.api.backendInteractor.untagUser(this.user, tag).then(response => {
store.state.api.backendInteractor.untagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return }
store.commit('untagUser', { user: this.user, tag })
})
} else {
store.state.api.backendInteractor.tagUser(this.user, tag).then(response => {
store.state.api.backendInteractor.tagUser({ user: this.user, tag }).then(response => {
if (!response.ok) { return }
store.commit('tagUser', { user: this.user, tag })
})
......@@ -59,24 +59,19 @@ const ModerationTools = {
toggleRight (right) {
const store = this.$store
if (this.user.rights[right]) {
store.state.api.backendInteractor.deleteRight(this.user, right).then(response => {
store.state.api.backendInteractor.deleteRight({ user: this.user, right }).then(response => {
if (!response.ok) { return }
store.commit('updateRight', { user: this.user, right: right, value: false })
store.commit('updateRight', { user: this.user, right, value: false })
})
} else {
store.state.api.backendInteractor.addRight(this.user, right).then(response => {
store.state.api.backendInteractor.addRight({ user: this.user, right }).then(response => {
if (!response.ok) { return }
store.commit('updateRight', { user: this.user, right: right, value: true })
store.commit('updateRight', { user: this.user, right, value: true })
})
}
},
toggleActivationStatus () {
const store = this.$store
const status = !!this.user.deactivated
store.state.api.backendInteractor.setActivationStatus(this.user, status).then(response => {
if (!response.ok) { return }
store.commit('updateActivationStatus', { user: this.user, status: status })
})
this.$store.dispatch('toggleActivationStatus', { user: this.user })
},
deleteUserDialog (show) {
this.showDeleteUserDialog = show
......@@ -85,7 +80,7 @@ const ModerationTools = {
const store = this.$store
const user = this.user
const { id, name } = user
store.state.api.backendInteractor.deleteUser(user)
store.state.api.backendInteractor.deleteUser({ user })
.then(e => {
this.$store.dispatch('markStatusesAsDeleted', status => user.id === status.user.id)
const isProfile = this.$route.name === 'external-user-profile' || this.$route.name === 'user-profile'
......
import { mapState } from 'vuex'
import { get } from 'lodash'
const MRFTransparencyPanel = {
computed: mapState({
federationPolicy: state => state.instance.federationPolicy,
mrfPolicies: state => state.instance.federationPolicy.mrf_policies,
acceptInstances: state => state.instance.federationPolicy.mrf_simple.accept,
rejectInstances: state => state.instance.federationPolicy.mrf_simple.reject,
quarantineInstances: state => state.instance.federationPolicy.quarantined_instances,
ftlRemovalInstances: state => state.instance.federationPolicy.mrf_simple.federated_timeline_removal,
mediaNsfwInstances: state => state.instance.federationPolicy.mrf_simple.media_nsfw,
mediaRemovalInstances: state => state.instance.federationPolicy.mrf_simple.media_removal
})
computed: {
...mapState({
federationPolicy: state => get(state, 'instance.federationPolicy'),
mrfPolicies: state => get(state, 'instance.federationPolicy.mrf_policies', []),
quarantineInstances: state => get(state, 'instance.federationPolicy.quarantined_instances', []),
acceptInstances: state => get(state, 'instance.federationPolicy.mrf_simple.accept', []),
rejectInstances: state => get(state, 'instance.federationPolicy.mrf_simple.reject', []),
ftlRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.federated_timeline_removal', []),
mediaNsfwInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_nsfw', []),
mediaRemovalInstances: state => get(state, 'instance.federationPolicy.mrf_simple.media_removal', [])
}),
hasInstanceSpecificPolicies () {
return this.quarantineInstances.length ||
this.acceptInstances.length ||
this.rejectInstances.length ||
this.ftlRemovalInstances.length ||
this.mediaNsfwInstances.length ||
this.mediaRemovalInstances.length
}
}
}
export default MRFTransparencyPanel
......@@ -22,7 +22,9 @@
/>
</ul>
<h2>{{ $t("about.mrf_policy_simple") }}</h2>
<h2 v-if="hasInstanceSpecificPolicies">
{{ $t("about.mrf_policy_simple") }}
</h2>
<div v-if="acceptInstances.length">
<h4>{{ $t("about.mrf_policy_simple_accept") }}</h4>
......
import followRequestFetcher from '../../services/follow_request_fetcher/follow_request_fetcher.service'
import { mapState } from 'vuex'
const NavPanel = {
created () {
if (this.currentUser && this.currentUser.locked) {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
followRequestFetcher.startFetching({ store, credentials })
this.$store.dispatch('startFetchingFollowRequest')
}
},
computed: {
currentUser () {
return this.$store.state.users.currentUser
},
chat () {
return this.$store.state.chat.channel
},
followRequestCount () {
return this.$store.state.api.followRequests.length
}
}
computed: mapState({
currentUser: state => state.users.currentUser,
chat: state => state.chat.channel,
followRequestCount: state => state.api.followRequests.length,
privateMode: state => state.instance.private,
federating: state => state.instance.federating
})
}
export default NavPanel
......@@ -4,22 +4,22 @@
<ul>
<li v-if="currentUser">
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
{{ $t("nav.interactions") }}
<i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
</router-link>
</li>
<li v-if="currentUser">
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
</router-link>
</li>
<li v-if="currentUser && currentUser.locked">
<router-link :to="{ name: 'friend-requests' }">
{{ $t("nav.friend_requests") }}
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
<span
v-if="followRequestCount > 0"
class="badge follow-request-count"
......@@ -28,19 +28,19 @@
</span>
</router-link>
</li>
<li>
<li v-if="currentUser || !privateMode">
<router-link :to="{ name: 'public-timeline' }">
{{ $t("nav.public_tl") }}
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
<li>
<li v-if="federating && !privateMode">
<router-link :to="{ name: 'public-external-timeline' }">
{{ $t("nav.twkn") }}
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link>
</li>
<li>
<router-link :to="{ name: 'about' }">
{{ $t("nav.about") }}
<i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}
</router-link>
</li>
</ul>
......@@ -113,4 +113,8 @@
}
}
}
.nav-panel .button-icon:before {
width: 1.1em;
}
</style>
......@@ -47,6 +47,11 @@ const Notifications = {
components: {
Notification
},
created () {
const { dispatch } = this.$store
dispatch('fetchAndUpdateNotifications')
},
watch: {
unseenCount (count) {
if (count > 0) {
......
......@@ -169,9 +169,7 @@ const PostStatusForm = {
if (this.submitDisabled) { return }
if (this.newStatus.status === '') {
if (this.newStatus.files.length > 0) {
this.newStatus.status = '\u200b' // hack
} else {
if (this.newStatus.files.length === 0) {
this.error = 'Cannot post an empty status with no files'
return
}
......
......@@ -10,7 +10,7 @@ const PublicAndExternalTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'publicAndExternal' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'publicAndExternal')
this.$store.dispatch('stopFetchingTimeline', 'publicAndExternal')
}
}
......
......@@ -10,7 +10,7 @@ const PublicTimeline = {
this.$store.dispatch('startFetchingTimeline', { timeline: 'public' })
},
destroyed () {
this.$store.dispatch('stopFetching', 'public')
this.$store.dispatch('stopFetchingTimeline', 'public')
}
}
......
......@@ -172,7 +172,7 @@
for="captcha-label"
>{{ $t('captcha') }}</label>
<template v-if="captcha.type == 'kocaptcha'">
<template v-if="['kocaptcha', 'native'].includes(captcha.type)">
<img
:src="captcha.url"
@click="setCaptcha"
......
......@@ -84,7 +84,7 @@ const settings = {
}
}])
.reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {}),
// Special cases (need to transform values)
// Special cases (need to transform values or perform actions first)
muteWordsString: {
get () { return this.$store.getters.mergedConfig.muteWords.join('\n') },
set (value) {
......@@ -93,6 +93,22 @@ const settings = {
value: filter(value.split('\n'), (word) => trim(word).length > 0)
})
}
},
useStreamingApi: {
get () { return this.$store.getters.mergedConfig.useStreamingApi },
set (value) {
const promise = value
? this.$store.dispatch('enableMastoSockets')
: this.$store.dispatch('disableMastoSockets')
promise.then(() => {
this.$store.dispatch('setOption', { name: 'useStreamingApi', value })
}).catch((e) => {
console.error('Failed starting MastoAPI Streaming socket', e)
this.$store.dispatch('disableMastoSockets')
this.$store.dispatch('setOption', { name: 'useStreamingApi', value: false })
})
}
}
},
// Updating nested properties
......
......@@ -73,6 +73,15 @@
</li>
</ul>
</li>
<li>
<Checkbox v-model="useStreamingApi">
{{ $t('settings.useStreamingApi') }}
<br/>
<small>
{{ $t('settings.useStreamingApiWarning') }}
</small>
</Checkbox>
</li>
<li>
<Checkbox v-model="autoLoad">
{{ $t('settings.autoload') }}
......@@ -270,6 +279,17 @@
</li>
</ul>
</div>
<div class="setting-item">
<h2>{{ $t('settings.fun') }}</h2>
<ul class="setting-list">
<li>
<Checkbox v-model="greentext">
{{ $t('settings.greentext') }} {{ $t('settings.instance_default', { value: greentextLocalizedValue }) }}
</Checkbox>
</li>
</ul>
</div>
</div>
<div :label="$t('settings.theme')">
......
......@@ -10,6 +10,10 @@ const SideDrawer = {
}),
created () {
this.closeGesture = GestureService.swipeGesture(GestureService.DIRECTION_LEFT, this.toggleDrawer)
if (this.currentUser && this.currentUser.locked) {
this.$store.dispatch('startFetchingFollowRequest')
}
},
components: { UserCard },
computed: {
......@@ -29,11 +33,20 @@ const SideDrawer = {
logo () {
return this.$store.state.instance.logo
},
hideSitename () {
return this.$store.state.instance.hideSitename
},
sitename () {
return this.$store.state.instance.name
},
followRequestCount () {
return this.$store.state.api.followRequests.length
},
privateMode () {
return this.$store.state.instance.private
},
federating () {
return this.$store.state.instance.federating
}
},
methods: {
......
......@@ -27,7 +27,7 @@
class="side-drawer-logo-wrapper"
>
<img :src="logo">
<span>{{ sitename }}</span>
<span v-if="!hideSitename">{{ sitename }}</span>
</div>
</div>
<ul>
......@@ -36,7 +36,7 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'login' }">
{{ $t("login.login") }}
<i class="button-icon icon-login" /> {{ $t("login.login") }}
</router-link>
</li>
<li
......@@ -44,7 +44,7 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }">
{{ $t("nav.dms") }}
<i class="button-icon icon-mail-alt" /> {{ $t("nav.dms") }}
</router-link>
</li>
<li
......@@ -52,7 +52,7 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'interactions', params: { username: currentUser.screen_name } }">
{{ $t("nav.interactions") }}
<i class="button-icon icon-bell-alt" /> {{ $t("nav.interactions") }}
</router-link>
</li>
</ul>
......@@ -62,7 +62,7 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'friends' }">
{{ $t("nav.timeline") }}
<i class="button-icon icon-home-2" /> {{ $t("nav.timeline") }}
</router-link>
</li>
<li
......@@ -70,7 +70,7 @@
@click="toggleDrawer"
>
<router-link to="/friend-requests">
{{ $t("nav.friend_requests") }}
<i class="button-icon icon-user-plus" /> {{ $t("nav.friend_requests") }}
<span
v-if="followRequestCount > 0"
class="badge follow-request-count"
......@@ -79,14 +79,20 @@
</span>
</router-link>
</li>
<li @click="toggleDrawer">
<li
v-if="currentUser || !privateMode"
@click="toggleDrawer"
>
<router-link to="/main/public">
{{ $t("nav.public_tl") }}
<i class="button-icon icon-users" /> {{ $t("nav.public_tl") }}
</router-link>
</li>
<li @click="toggleDrawer">
<li
v-if="federating && !privateMode"
@click="toggleDrawer"
>
<router-link to="/main/all">
{{ $t("nav.twkn") }}
<i class="button-icon icon-globe" /> {{ $t("nav.twkn") }}
</router-link>
</li>
<li
......@@ -94,14 +100,17 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'chat' }">
{{ $t("nav.chat") }}
<i class="button-icon icon-chat" /> {{ $t("nav.chat") }}
</router-link>
</li>
</ul>
<ul>
<li @click="toggleDrawer">
<li
v-if="currentUser || !privateMode"
@click="toggleDrawer"
>
<router-link :to="{ name: 'search' }">
{{ $t("nav.search") }}
<i class="button-icon icon-search" /> {{ $t("nav.search") }}
</router-link>
</li>
<li
......@@ -109,17 +118,17 @@
@click="toggleDrawer"
>
<router-link :to="{ name: 'who-to-follow' }">
{{ $t("nav.who_to_follow") }}
<i class="button-icon icon-user-plus" /> {{ $t("nav.who_to_follow") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'settings' }">
{{ $t("settings.settings") }}
<i class="button-icon icon-cog" /> {{ $t("settings.settings") }}
</router-link>
</li>
<li @click="toggleDrawer">
<router-link :to="{ name: 'about'}">
{{ $t("nav.about") }}
<i class="button-icon icon-info-circled" /> {{ $t("nav.about") }}
</router-link>
</li>
<li
......@@ -130,7 +139,7 @@
href="/pleroma/admin/#/login-pleroma"
target="_blank"
>
{{ $t("nav.administration") }}
<i class="button-icon icon-gauge" /> {{ $t("nav.administration") }}
</a>
</li>
<li
......@@ -141,7 +150,7 @@
href="#"
@click="doLogout"
>
{{ $t("login.logout") }}
<i class="button-icon icon-logout" /> {{ $t("login.logout") }}
</a>
</li>
</ul>
......@@ -215,6 +224,10 @@
box-shadow: var(--panelShadow);
background-color: $fallback--bg;
background-color: var(--bg, $fallback--bg);
.button-icon:before {
width: 1.1em;
}
}
.side-drawer-logo-wrapper {
......
......@@ -13,10 +13,11 @@ import Timeago from '../timeago/timeago.vue'
import StatusPopover from '../status_popover/status_popover.vue'
import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator'
import fileType from 'src/services/file_type/file_type.service'
import { processHtml } from 'src/services/tiny_post_html_processor/tiny_post_html_processor.service.js'
import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js'
import { mentionMatchesUrl, extractTagFromUrl } from 'src/services/matcher/matcher.service.js'
import { filter, unescape, uniqBy } from 'lodash'
import { mapGetters } from 'vuex'
import { mapGetters, mapState } from 'vuex'
const Status = {
name: 'Status',
......@@ -32,7 +33,8 @@ const Status = {
'noHeading',
'inlineExpanded',
'showPinned',
'inProfile'
'inProfile',
'profileUserId'
],
data () {
return {
......@@ -42,8 +44,8 @@ const Status = {
showingTall: this.inConversation && this.focused,
showingLongSubject: false,
error: null,
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject,
betterShadow: this.$store.state.interface.browserSupport.cssFilter
// not as computed because it sets the initial state which will be changed later
expandingSubject: !this.$store.getters.mergedConfig.collapseMessageWithSubject
}
},
computed: {
......@@ -103,7 +105,7 @@ const Status = {
return this.$store.state.statuses.allStatusesObject[this.status.id]
},
loggedIn () {
return !!this.$store.state.users.currentUser
return !!this.currentUser
},
muteWordHits () {
const statusText = this.status.text.toLowerCase()
......@@ -114,7 +116,7 @@ const Status = {
return hits
},
muted () { return !this.unmuted && ((!this.inProfile && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
muted () { return !this.unmuted && ((!(this.inProfile && this.status.user.id === this.profileUserId) && this.status.user.muted) || (!this.inConversation && this.status.thread_muted) || this.muteWordHits.length > 0) },
hideFilteredStatuses () {
return this.mergedConfig.hideFilteredStatuses
},
......@@ -163,7 +165,7 @@ const Status = {
if (this.inConversation || !this.isReply) {
return false
}
if (this.status.user.id === this.$store.state.users.currentUser.id) {
if (this.status.user.id === this.currentUser.id) {
return false
}
if (this.status.type === 'retweet') {
......@@ -178,7 +180,7 @@ const Status = {
if (checkFollowing && taggedUser && taggedUser.following) {
return false
}
if (this.status.attentions[i].id === this.$store.state.users.currentUser.id) {
if (this.status.attentions[i].id === this.currentUser.id) {
return false
}
}
......@@ -255,11 +257,41 @@ const Status = {
maxThumbnails () {
return this.mergedConfig.maxThumbnails
},
postBodyHtml () {
const html = this.status.statusnet_html
if (this.mergedConfig.greentext) {
try {
if (html.includes('&gt;')) {
// This checks if post has '>' at the beginning, excluding mentions so that @mention >impying works
return processHtml(html, (string) => {
if (string.includes('&gt;') &&
string
.replace(/<[^>]+?>/gi, '') // remove all tags
.replace(/@\w+/gi, '') // remove mentions (even failed ones)
.trim()
.startsWith('&gt;')) {
return `<span class='greentext'>${string}</span>`
} else {
return string
}
})
} else {
return html
}
} catch (e) {
console.err('Failed to process status html', e)
return html
}
} else {
return html
}
},
contentHtml () {
if (!this.status.summary_html) {
return this.status.statusnet_html
return this.postBodyHtml
}
return this.status.summary_html + '<br />' + this.status.statusnet_html
return this.status.summary_html + '<br />' + this.postBodyHtml
},
combinedFavsAndRepeatsUsers () {
// Use the status from the global status repository since favs and repeats are saved in it
......@@ -270,7 +302,7 @@ const Status = {
return uniqBy(combinedUsers, 'id')
},
ownStatus () {
return this.status.user.id === this.$store.state.users.currentUser.id
return this.status.user.id === this.currentUser.id
},
tags () {
return this.status.tags.filter(tagObj => tagObj.hasOwnProperty('name')).map(tagObj => tagObj.name).join(' ')
......@@ -278,7 +310,11 @@ const Status = {
hidePostStats () {
return this.mergedConfig.hidePostStats
},
...mapGetters(['mergedConfig'])
...mapGetters(['mergedConfig']),
...mapState({
betterShadow: state => state.interface.browserSupport.cssFilter,
currentUser: state => state.users.currentUser
})
},
components: {
Attachment,
......
......@@ -606,7 +606,7 @@ $status-margin: 0.75em;
height: 100%;
mask: linear-gradient(to top, white, transparent) bottom/100% 70px no-repeat,
linear-gradient(to top, white, white);
// Autoprefixed seem to ignore this one, and also syntax is different
/* Autoprefixed seem to ignore this one, and also syntax is different */
-webkit-mask-composite: xor;
mask-composite: exclude;
}
......@@ -752,7 +752,8 @@ $status-margin: 0.75em;
}
.greentext {
color: green;
color: $fallback--cGreen;
color: var(--cGreen, $fallback--cGreen);
}
.status-conversation {
......
......@@ -36,23 +36,23 @@
.sticker-picker {
width: 100%;
position: relative;
.tab-switcher {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.sticker-picker-content {
.sticker {
display: inline-block;
width: 20%;
height: 20%;
img {
width: 100%;
&:hover {
filter: drop-shadow(0 0 5px var(--link, $fallback--link));
.contents {
min-height: 250px;
.sticker-picker-content {
display: flex;
flex-wrap: wrap;
padding: 0 4px;
.sticker {
display: flex;
flex: 1 1 auto;
margin: 4px;
width: 56px;
height: 56px;
img {
height: 100%;
&:hover {
filter: drop-shadow(0 0 5px var(--link, $fallback--link));
}
}
}
}
......
......@@ -19,7 +19,7 @@ const TagTimeline = {
}
},
destroyed () {
this.$store.dispatch('stopFetching', 'tag')
this.$store.dispatch('stopFetchingTimeline', 'tag')
}
}
......
......@@ -36,7 +36,12 @@ const Timeline = {
}
},
computed: {
timelineError () { return this.$store.state.statuses.error },
timelineError () {
return this.$store.state.statuses.error
},
errorData () {
return this.$store.state.statuses.errorData
},
newStatusCount () {
return this.timeline.newStatusCount
},
......
......@@ -11,15 +11,22 @@
>
{{ $t('timeline.error_fetching') }}
</div>
<div
v-else-if="errorData"
class="loadmore-error alert error"
@click.prevent
>
{{ errorData.statusText }}
</div>
<button
v-if="timeline.newStatusCount > 0 && !timelineError"
v-if="timeline.newStatusCount > 0 && !timelineError && !errorData"
class="loadmore-button"
@click.prevent="showNewStatuses"
>
{{ $t('timeline.show_new') }}{{ newStatusCountStr }}
</button>
<div
v-if="!timeline.newStatusCount > 0 && !timelineError"
v-if="!timeline.newStatusCount > 0 && !timelineError && !errorData"
class="loadmore-text faint"
@click.prevent
>
......@@ -37,6 +44,7 @@
:collapsable="true"
:pinned-status-ids-object="pinnedStatusIdsObject"
:in-profile="inProfile"
:profile-user-id="userId"
/>
</template>
<template v-for="status in timeline.visibleStatuses">
......@@ -47,6 +55,7 @@
:status-id="status.id"
:collapsable="true"
:in-profile="inProfile"
:profile-user-id="userId"
/>
</template>
</div>
......@@ -65,12 +74,18 @@
{{ $t('timeline.no_more_statuses') }}
</div>
<a
v-else-if="!timeline.loading"
v-else-if="!timeline.loading && !errorData"
href="#"
@click.prevent="fetchOlderStatuses()"
>
<div class="new-status-notification text-center panel-footer">{{ $t('timeline.load_older') }}</div>
</a>
<a
v-else-if="errorData"
href="#"
>
<div class="new-status-notification text-center panel-footer">{{ errorData.error }}</div>
</a>
<div
v-else
class="new-status-notification text-center panel-footer"
......@@ -91,17 +106,4 @@
opacity: 1;
}
}
.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);
}
</style>
......@@ -149,6 +149,9 @@ export default {
}
this.$store.dispatch('setMedia', [attachment])
this.$store.dispatch('setCurrent', attachment)
},
mentionUser () {
this.$store.dispatch('openPostStatusModal', { replyTo: true, repliedUser: this.user })
}
}
}
......@@ -175,6 +175,14 @@
{{ $t('user_card.mute') }}
</button>
</div>
<div>
<button
class="btn btn-default btn-block"
@click="mentionUser"
>
{{ $t('user_card.mention') }}
</button>
</div>
<ModerationTools
v-if="loggedIn.role === &quot;admin&quot;"
:user="user"
......
......@@ -112,9 +112,9 @@ const UserProfile = {
}
},
stopFetching () {
this.$store.dispatch('stopFetching', 'user')
this.$store.dispatch('stopFetching', 'favorites')
this.$store.dispatch('stopFetching', 'media')
this.$store.dispatch('stopFetchingTimeline', 'user')
this.$store.dispatch('stopFetchingTimeline', 'favorites')
this.$store.dispatch('stopFetchingTimeline', 'media')
},
switchUser (userNameOrId) {
this.stopFetching()
......
......@@ -64,7 +64,7 @@ const UserReportingModal = {
forward: this.forward,
statusIds: this.statusIdsToReport
}
this.$store.state.api.backendInteractor.reportUser(params)
this.$store.state.api.backendInteractor.reportUser({ ...params })
.then(() => {
this.processing = false
this.resetState()
......
......@@ -331,7 +331,7 @@ const UserSettings = {
})
},
importFollows (file) {
return this.$store.state.api.backendInteractor.importFollows(file)
return this.$store.state.api.backendInteractor.importFollows({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
......@@ -339,7 +339,7 @@ const UserSettings = {
})
},
importBlocks (file) {
return this.$store.state.api.backendInteractor.importBlocks(file)
return this.$store.state.api.backendInteractor.importBlocks({ file })
.then((status) => {
if (!status) {
throw new Error('failed')
......@@ -386,7 +386,7 @@ const UserSettings = {
newPassword: this.changePasswordInputs[1],
newPasswordConfirmation: this.changePasswordInputs[2]
}
this.$store.state.api.backendInteractor.changePassword(params)
this.$store.state.api.backendInteractor.changePassword({ params })
.then((res) => {
if (res.status === 'success') {
this.changedPassword = true
......@@ -403,7 +403,7 @@ const UserSettings = {
email: this.newEmail,
password: this.changeEmailPassword
}
this.$store.state.api.backendInteractor.changeEmail(params)
this.$store.state.api.backendInteractor.changeEmail({ params })
.then((res) => {
if (res.status === 'success') {
this.changedEmail = true
......