Merge branch 'develop' into feature/fetch-statuses-from-given-instance

This commit is contained in:
Maxim Filippov 2019-11-29 17:23:19 +09:00
commit 60ccdd72cf
128 changed files with 2302 additions and 2812 deletions

View file

@ -6,10 +6,33 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Changed
- moves emoji pack configuration from the main menu to settings tab, redesigns it and fixes bugs
- `mailerEnabled` must be set to `true` in order to require password reset (password reset currently only works via email)
- remove fetching initial data for configuring server settings
- Actions in users module (ActivateUsers, AddRight, DeactivateUsers, DeleteRight, DeleteUsers) now accept an array of users instead of one user
- Leave dropdown menu open after clicking an action
- Move current try/catch error handling from view files to module, add it where necessary
### Added
- Optimistic update for actions in users module and fetching users after api function finished its execution
- Relay management
### Fixed
- Show checkmarks when tag is applied
- Reports update (also, now it's optimistic)
## [1.2.0] - 2019-09-27
### Added
- Emoji pack configuration
- Statuses page: fetch all statuses from a given instance
- Ability to require user's password reset
Ability to track admin/moderator actions, a.k.a. "the moderation log"
## [1.1.0] - 2019-09-15

View file

@ -4,7 +4,24 @@
## About
Admin UI for pleroma instance owners
Admin UI for pleroma instance owners.
### Branches
There are two main branches here:
- `develop`: ongoing work and all merge requests go here, *unstable*
- `master`: after `develop` is stabilized it is merged to `master`, `master` is *stable*, allegedly
### Features
1. User administration: grant roles to users (admin/moderator), deactivate/delete as well as force their statuses to have NSFW tag, strip media and many more
1. Invites management: generate invite tokens & send invites via email
1. Moderation log: track moderator/admin actions
1. Settings: configure your pleroma instance via friendly (hopefully) UI
1. Emoji packs: configure your emoji packs
You can have any combination of these features (i.e. you can disable anything, but user administration, see "Disabling features" section below).
## Usage
@ -18,8 +35,19 @@ To compile everything for production run `yarn build:prod`.
#### Disabling features
You can disable certain AdminFE features, like reports or settings by modifying `config/prod.env.js` env variable `DISABLED_FEATURES`, e.g. if you want to compile AdminFE without "Settings" you'll need to set it to: `DISABLED_FEATURES: '["settings"]'`,
to disable emoji pack settings add `"emoji-packs"` to the list.
You can disable certain AdminFE features, like reports or settings by modifying `config/prod.env.js` env variable `DISABLED_FEATURES`, e.g. if you want to compile AdminFE without "Settings" you'll need to set it to: `DISABLED_FEATURES: '["settings"]'`.
Features, that can be disabled:
- reports: `DISABLED_FEATURES: '["reports"]'`
- invites: `DISABLED_FEATURES: '["invites"]'`
- moderation log: `DISABLED_FEATURES: '["moderationLog"]'`
- settings: `DISABLED_FEATURES: '["settings"]'`
- emoji packs: `DISABLED_FEATURES: '["emojiPacks"]'`
Of course, you can disable multiple features just by adding to the array, e.g. `DISABLED_FEATURES: '["emojiPacks", "settings"]'` will have both emoji packs and settings disabled.
Users administration cannot be disabled.
## Changelog

View file

@ -8,7 +8,6 @@
<title>Admin FE</title>
</head>
<body>
<script src=<%= BASE_URL %>/tinymce4.7.5/tinymce.min.js></script>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>

View file

@ -0,0 +1,9 @@
export async function getNodeInfo(authHost) {
const data = {
metadata: {
mailerEnabled: true
}
}
return Promise.resolve({ data })
}

View file

@ -11,20 +11,42 @@ const reports = [
{ created_at: '2019-05-18T13:01:33.000Z', account: { acct: 'nick', display_name: 'Nick Keys', tags: [] }, actor: { acct: 'admin' }, state: 'closed', id: '4', content: '', statuses: [] }
]
export async function fetchReports(limit, max_id, authHost, token) {
const paginatedReports = max_id.length > 0 ? reports.slice(5) : reports.slice(0, 5)
return Promise.resolve({ data: { reports: paginatedReports }})
const groupedReports = [
{ account: { avatar: 'http://localhost:4000/images/avi.png', confirmation_pending: false, deactivated: false, display_name: 'leo', id: '9oG0YghgBi94EATI9I', local: true, nickname: 'leo', roles: { admin: false, moderator: false }, tags: [] },
actors: [{ acct: 'admin', avatar: 'http://localhost:4000/images/avi.png', deactivated: false, display_name: 'admin', id: '9oFz4pTauG0cnJ581w', local: true, nickname: 'admin', roles: { admin: false, moderator: false }, tags: [], url: 'http://localhost:4000/users/admin', username: 'admin' }],
date: '2019-11-23T12:56:11.969772Z',
reports: [
{ created_at: '2019-05-21T21:35:33.000Z', account: { acct: 'benj', display_name: 'Benjamin Fame', tags: [] }, actor: { acct: 'admin' }, state: 'open', id: '2', content: 'This is a report', statuses: [] },
{ created_at: '2019-05-20T22:45:33.000Z', account: { acct: 'alice', display_name: 'Alice Pool', tags: [] }, actor: { acct: 'admin2' }, state: 'resolved', id: '7', content: 'Please block this user', statuses: [
{ account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'public', sensitive: false, id: '11', content: 'Hey!', url: '', created_at: '2019-05-10T21:35:33.000Z' },
{ account: { display_name: 'Alice Pool', avatar: '' }, visibility: 'unlisted', sensitive: true, id: '10', content: 'Bye!', url: '', created_at: '2019-05-10T21:00:33.000Z' }
] }
],
status: {
account: { acct: 'leo' },
content: 'At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis',
created_at: '2019-11-23T12:55:20.000Z',
id: '9pFoQO69piu7cUDnJg',
url: 'http://localhost:4000/notice/9pFoQO69piu7cUDnJg',
visibility: 'unlisted',
sensitive: true
},
status_deleted: false
}
]
export async function fetchReports(filter, page, pageSize, authHost, token) {
return filter.length > 0
? Promise.resolve({ data: { reports: reports.filter(report => report.state === filter) }})
: Promise.resolve({ data: { reports }})
}
export async function filterReports(filter, limit, max_id, authHost, token) {
const filteredReports = reports.filter(report => report.state === filter)
const paginatedReports = max_id.length > 0 ? filteredReports.slice(5) : filteredReports.slice(0, 5)
return Promise.resolve({ data: { reports: paginatedReports }})
export async function fetchGroupedReports(authHost, token) {
return Promise.resolve({ data: { reports: groupedReports }})
}
export async function changeState(state, id, authHost, token) {
const report = reports.find(report => report.id === id)
return Promise.resolve({ data: { ...report, state }})
export async function changeState(reportsData, authHost, token) {
return Promise.resolve({ data: '' })
}
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {

View file

@ -0,0 +1,7 @@
export async function changeStatusScope(id, sensitive, visibility, authHost, token) {
return Promise.resolve()
}
export async function deleteStatus(id, authHost, token) {
return Promise.resolve()
}

View file

@ -4,6 +4,10 @@ export let users = [
{ active: false, deactivated: true, id: 'abc', nickname: 'john', local: true, external: false, roles: { admin: false, moderator: false }, tags: ['strip_media'] }
]
const userProfile = { avatar: 'avatar.jpg', display_name: 'Allis', nickname: 'allis', id: '2', tags: [], roles: { admin: true, moderator: false }, local: true, external: false }
const userStatuses = []
const filterUsers = (str) => {
const filters = str.split(',').filter(item => item.length > 0)
if (filters.length === 0) {
@ -20,6 +24,10 @@ const filterUsers = (str) => {
return applyFilters([], filters, users)
}
export async function fetchUser(id, authHost, token) {
return Promise.resolve({ data: userProfile })
}
export async function fetchUsers(filters, authHost, token, page = 1) {
const filteredUsers = filterUsers(filters)
return Promise.resolve({ data: {
@ -29,13 +37,12 @@ export async function fetchUsers(filters, authHost, token, page = 1) {
}})
}
export async function getPasswordResetToken(nickname, authHost, token) {
return Promise.resolve({ data: { token: 'g05lxnBJQnL', link: 'http://url/api/pleroma/password_reset/g05lxnBJQnL' }})
export async function fetchUserStatuses(id, authHost, godmode, token) {
return Promise.resolve({ data: userStatuses })
}
export async function toggleUserActivation(nickname, authHost, token) {
const response = users.find(user => user.nickname === nickname)
return Promise.resolve({ data: { ...response, deactivated: !response.deactivated }})
export async function getPasswordResetToken(nickname, authHost, token) {
return Promise.resolve({ data: { token: 'g05lxnBJQnL', link: 'http://url/api/pleroma/password_reset/g05lxnBJQnL' }})
}
export async function searchUsers(query, filters, authHost, token, page = 1) {
@ -48,21 +55,37 @@ export async function searchUsers(query, filters, authHost, token, page = 1) {
}})
}
export async function addRight(nickname, right, authHost, token) {
export async function activateUsers(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, deactivated: false }
})
return Promise.resolve({ data: response })
}
export async function addRight(nicknames, right, authHost, token) {
return Promise.resolve({ data:
{ [`is_${right}`]: true }
})
}
export async function deactivateUsers(nicknames, authHost, token) {
const response = nicknames.map(nickname => {
const currentUser = users.find(user => user.nickname === nickname)
return { ...currentUser, deactivated: true }
})
return Promise.resolve({ data: response })
}
export async function deleteRight(nickname, right, authHost, token) {
return Promise.resolve({ data:
{ [`is_${right}`]: false }
})
}
export async function deleteUser(nickname, authHost, token) {
export async function deleteUsers(nicknames, authHost, token) {
return Promise.resolve({ data:
nickname
nicknames
})
}

View file

@ -48,6 +48,16 @@ export async function listPacks(host) {
})
}
export async function listRemotePacks(host, token, instance) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/list_from`,
method: 'post',
headers: authHeaders(token),
data: { instance_address: baseName(instance) }
})
}
export async function downloadFrom(host, instance_address, pack_name, as, token) {
if (as.trim() === '') {
as = null
@ -58,7 +68,7 @@ export async function downloadFrom(host, instance_address, pack_name, as, token)
url: '/api/pleroma/emoji/packs/download_from',
method: 'post',
headers: authHeaders(token),
data: { instance_address, pack_name, as },
data: { instance_address: baseName(instance_address), pack_name, as },
timeout: 0
})
}

View file

@ -1,117 +0,0 @@
export const initialSettings = [
{
group: 'pleroma',
key: ':instance',
value: [
{ 'tuple': [':name', 'Pleroma'] },
{ 'tuple': [':email', 'example@example.com'] },
{ 'tuple': [':notify_email', 'noreply@example.com'] },
{ 'tuple': [':description', 'A Pleroma instance, an alternative fediverse server'] },
{ 'tuple': [':limit', 5000] },
{ 'tuple': [':remote_limit', 100000] },
{ 'tuple': [':upload_limit', 16 * 1048576] },
{ 'tuple': [':avatar_upload_limit', 2 * 1048576] },
{ 'tuple': [':background_upload_limit', 4 * 1048576] },
{ 'tuple': [':banner_upload_limit', 4 * 1048576] },
{ 'tuple': [':poll_limits', [
{ 'tuple': [':max_options', 20] },
{ 'tuple': [':max_option_chars', 200] },
{ 'tuple': [':min_expiration', 0] },
{ 'tuple': [':max_expiration', 365 * 86400] }
]] },
{ 'tuple': [':registrations_open', true] },
{ 'tuple': [':invites_enabled', false] },
{ 'tuple': [':account_activation_required', false] },
{ 'tuple': [':federating', true] },
{ 'tuple': [':federation_reachability_timeout_days', 7] },
{ 'tuple':
[':federation_publisher_modules', ['Pleroma.Web.ActivityPub.Publisher', 'Pleroma.Web.Websub', 'Pleroma.Web.Salmon']] },
{ 'tuple': [':allow_relay', true] },
{ 'tuple': [':rewrite_policy', 'Pleroma.Web.ActivityPub.MRF.NoOpPolicy'] },
{ 'tuple': [':public', true] },
{ 'tuple': [':managed_config', true] },
{ 'tuple': [':static_dir', 'instance/static/'] },
{ 'tuple': [':allowed_post_formats', ['text/plain', 'text/html', 'text/markdown', 'text/bbcode']] },
{ 'tuple': [':mrf_transparency', true] },
{ 'tuple': [':extended_nickname_format', false] },
{ 'tuple': [':max_pinned_statuses', 1] },
{ 'tuple': [':no_attachment_links', false] },
{ 'tuple': [':max_report_comment_size', 1000] },
{ 'tuple': [':safe_dm_mentions', false] },
{ 'tuple': [':healthcheck', false] },
{ 'tuple': [':remote_post_retention_days', 90] },
{ 'tuple': [':skip_thread_containment', true] },
{ 'tuple': [':limit_to_local_content', ':unauthenticated'] },
{ 'tuple': [':dynamic_configuration', true] },
{ 'tuple': [':max_account_fields', 10] },
{ 'tuple': [':max_remote_account_fields', 20] },
{ 'tuple': [':account_field_name_length', 255] },
{ 'tuple': [':account_field_value_length', 255] },
{ 'tuple': [':external_user_synchronization', true] },
{ 'tuple': [':user_bio_length', 5000] },
{ 'tuple': [':user_name_length', 100] }
]
},
{
group: 'mime',
key: ':types',
value: {
'application/activity+json': ['activity+json'],
'application/jrd+json': ['jrd+json'],
'application/ld+json': ['activity+json'],
'application/xml': ['xml'],
'application/xrd+xml': ['xrd+xml']
}
},
{
group: 'cors_plug',
key: ':max_age',
value: 86400
},
{
group: 'cors_plug',
key: ':methods',
value: ['POST', 'PUT', 'DELETE', 'GET', 'PATCH', 'OPTIONS']
},
{
group: 'cors_plug',
key: ':expose',
value: [
'Link',
'X-RateLimit-Reset',
'X-RateLimit-Limit',
'X-RateLimit-Remaining',
'X-Request-Id',
'Idempotency-Key'
]
},
{
group: 'cors_plug',
key: ':credentials',
value: true
},
{
group: 'cors_plug',
key: ':headers',
value: ['Authorization', 'Content-Type', 'Idempotency-Key']
},
{
group: 'tesla',
key: ':adapter',
value: 'Tesla.Adapter.Hackney'
},
{
group: 'pleroma',
key: ':markup',
value: [
{ 'tuple': [':allow_inline_images', true] },
{ 'tuple': [':allow_headings', false] },
{ 'tuple': [':allow_tables', false] },
{ 'tuple': [':allow_fonts', false] },
{ 'tuple': [':scrub_policy', [
'Pleroma.HTML.Transform.MediaProxy',
'Pleroma.HTML.Scrubber.Default'
]] }
]
}
]

38
src/api/moderationLog.js Normal file
View file

@ -0,0 +1,38 @@
import _ from 'lodash'
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function fetchLog(authHost, token, params, page = 1) {
const normalizedParams = new URLSearchParams(
_.omitBy({ ...params, page }, _.isUndefined)
).toString()
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/moderation_log?${normalizedParams}`,
method: 'get',
headers: authHeaders(token)
})
}
export async function fetchAdmins(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users?filters=is_admin`,
method: 'get',
headers: authHeaders(token)
})
}
export async function fetchModerators(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users?filters=is_moderator`,
method: 'get',
headers: authHeaders(token)
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}

10
src/api/nodeInfo.js Normal file
View file

@ -0,0 +1,10 @@
import request from '@/utils/request'
import { baseName } from './utils'
export async function getNodeInfo(authHost) {
return await request({
baseURL: baseName(authHost),
url: `/nodeinfo/2.0.json`,
method: 'get'
})
}

34
src/api/relays.js Normal file
View file

@ -0,0 +1,34 @@
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function fetchRelays(authHost, token) {
return await request({
baseURL: baseName(authHost),
url: '/api/pleroma/admin/relay',
method: 'get',
headers: authHeaders(token)
})
}
export async function addRelay(relay, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: '/api/pleroma/admin/relay',
method: 'post',
headers: authHeaders(token),
data: { relay_url: relay }
})
}
export async function deleteRelay(relay, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: '/api/pleroma/admin/relay',
method: 'delete',
headers: authHeaders(token),
data: { relay_url: `https://${relay}/actor` }
})
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}

View file

@ -2,13 +2,13 @@ import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function changeState(state, id, authHost, token) {
export async function changeState(reports, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/reports/${id}`,
method: 'put',
url: `/api/pleroma/admin/reports`,
method: 'patch',
headers: authHeaders(token),
data: { state }
data: { reports }
})
}

View file

@ -2,12 +2,23 @@ import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
export async function addRight(nickname, right, authHost, token) {
export async function activateUsers(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/permission_group/${right}`,
url: `/api/pleroma/admin/users/activate`,
method: 'patch',
headers: authHeaders(token),
data: { nicknames }
})
}
export async function addRight(nicknames, right, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/permission_group/${right}`,
method: 'post',
headers: authHeaders(token)
headers: authHeaders(token),
data: { nicknames }
})
}
@ -21,21 +32,33 @@ export async function createNewAccount(nickname, email, password, authHost, toke
})
}
export async function deleteRight(nickname, right, authHost, token) {
export async function deactivateUsers(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/permission_group/${right}`,
method: 'delete',
headers: authHeaders(token)
url: `/api/pleroma/admin/users/deactivate`,
method: 'patch',
headers: authHeaders(token),
data: { nicknames }
})
}
export async function deleteUser(nickname, authHost, token) {
export async function deleteRight(nicknames, right, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users?nickname=${nickname}`,
url: `/api/pleroma/admin/users/permission_group/${right}`,
method: 'delete',
headers: authHeaders(token)
headers: authHeaders(token),
data: { nicknames }
})
}
export async function deleteUsers(nicknames, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users`,
method: 'delete',
headers: authHeaders(token),
data: { nicknames }
})
}
@ -66,6 +89,15 @@ export async function getPasswordResetToken(nickname, authHost, token) {
})
}
export async function requirePasswordReset(nickname, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/force_password_reset`,
method: 'patch',
headers: authHeaders(token)
})
}
export async function searchUsers(query, filters, authHost, token, page = 1) {
return await request({
baseURL: baseName(authHost),
@ -85,15 +117,6 @@ export async function tagUser(nicknames, tags, authHost, token) {
})
}
export async function toggleUserActivation(nickname, authHost, token) {
return await request({
baseURL: baseName(authHost),
url: `/api/pleroma/admin/users/${nickname}/toggle_activation`,
method: 'patch',
headers: authHeaders(token)
})
}
export async function untagUser(nicknames, tags, authHost, token) {
return await request({
baseURL: baseName(authHost),

View file

@ -1,5 +1,10 @@
const isLocalhost = (instanceName) =>
instanceName.startsWith('localhost:') || instanceName.startsWith('127.0.0.1:')
export const baseName = (instanceName) =>
isLocalhost(instanceName) ? `http://${instanceName}` : `https://${instanceName}`
export const baseName = (instanceName = 'localhost') => {
if (instanceName.match(/https?:\/\//)) {
return instanceName
} else {
return isLocalhost(instanceName) ? `http://${instanceName}` : `https://${instanceName}`
}
}

View file

@ -1,103 +0,0 @@
<template>
<div class="upload-container">
<el-button :style="{background:color,borderColor:color}" icon="el-icon-upload" size="mini" type="primary" @click=" dialogVisible=true">上传图片
</el-button>
<el-dialog :visible.sync="dialogVisible">
<el-upload
:multiple="true"
:file-list="fileList"
:show-file-list="true"
:on-remove="handleRemove"
:on-success="handleSuccess"
:before-upload="beforeUpload"
class="editor-slide-upload"
action="https://httpbin.org/post"
list-type="picture-card">
<el-button size="small" type="primary">点击上传</el-button>
</el-upload>
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</el-dialog>
</div>
</template>
<script>
// import { getToken } from 'api/qiniu'
export default {
name: 'EditorSlideUpload',
props: {
color: {
type: String,
default: '#1890ff'
}
},
data: function() {
return {
dialogVisible: false,
listObj: {},
fileList: []
}
},
methods: {
checkAllSuccess() {
return Object.keys(this.listObj).every(item => this.listObj[item].hasSuccess)
},
handleSubmit() {
const arr = Object.keys(this.listObj).map(v => this.listObj[v])
if (!this.checkAllSuccess()) {
this.$message('请等待所有图片上传成功 或 出现了网络问题,请刷新页面重新上传!')
return
}
this.$emit('successCBK', arr)
this.listObj = {}
this.fileList = []
this.dialogVisible = false
},
handleSuccess(response, file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
this.listObj[objKeyArr[i]].url = response.files.file
this.listObj[objKeyArr[i]].hasSuccess = true
return
}
}
},
handleRemove(file) {
const uid = file.uid
const objKeyArr = Object.keys(this.listObj)
for (let i = 0, len = objKeyArr.length; i < len; i++) {
if (this.listObj[objKeyArr[i]].uid === uid) {
delete this.listObj[objKeyArr[i]]
return
}
}
},
beforeUpload(file) {
const _self = this
const _URL = window.URL || window.webkitURL
const fileName = file.uid
this.listObj[fileName] = {}
return new Promise((resolve, reject) => {
const img = new Image()
img.src = _URL.createObjectURL(file)
img.onload = function() {
_self.listObj[fileName] = { hasSuccess: false, uid: file.uid, width: this.width, height: this.height }
}
resolve(true)
})
}
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.editor-slide-upload {
margin-bottom: 20px;
/deep/ .el-upload--picture-card {
width: 100%;
}
}
</style>

View file

@ -1,210 +0,0 @@
<template>
<div :class="{fullscreen:fullscreen}" class="tinymce-container editor-container">
<textarea :id="tinymceId" class="tinymce-textarea"/>
<div class="editor-custom-btn-container">
<editorImage color="#1890ff" class="editor-upload-btn" @successCBK="imageSuccessCBK"/>
</div>
</div>
</template>
<script>
import editorImage from './components/editorImage'
import plugins from './plugins'
import toolbar from './toolbar'
export default {
name: 'Tinymce',
components: { editorImage },
props: {
id: {
type: String,
default: function() {
return 'vue-tinymce-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
}
},
value: {
type: String,
default: ''
},
toolbar: {
type: Array,
required: false,
default() {
return []
}
},
menubar: {
type: String,
default: 'file edit insert view format table'
},
height: {
type: Number,
required: false,
default: 360
}
},
data: function() {
return {
hasChange: false,
hasInit: false,
tinymceId: this.id,
fullscreen: false,
languageTypeList: {
'en': 'en',
'zh': 'zh_CN'
}
}
},
computed: {
language() {
return this.languageTypeList[this.$store.getters.language]
}
},
watch: {
value(val) {
if (!this.hasChange && this.hasInit) {
this.$nextTick(() =>
window.tinymce.get(this.tinymceId).setContent(val || ''))
}
},
language() {
this.destroyTinymce()
this.$nextTick(() => this.initTinymce())
}
},
mounted() {
this.initTinymce()
},
activated() {
this.initTinymce()
},
deactivated() {
this.destroyTinymce()
},
destroyed() {
this.destroyTinymce()
},
methods: {
initTinymce() {
const _this = this
window.tinymce.init({
language: this.language,
selector: `#${this.tinymceId}`,
height: this.height,
body_class: 'panel-body ',
object_resizing: false,
toolbar: this.toolbar.length > 0 ? this.toolbar : toolbar,
menubar: this.menubar,
plugins: plugins,
end_container_on_empty_block: true,
powerpaste_word_import: 'clean',
code_dialog_height: 450,
code_dialog_width: 1000,
advlist_bullet_styles: 'square',
advlist_number_styles: 'default',
imagetools_cors_hosts: ['www.tinymce.com', 'codepen.io'],
default_link_target: '_blank',
link_title: false,
nonbreaking_force_tab: true, // inserting nonbreaking space &nbsp; need Nonbreaking Space Plugin
init_instance_callback: editor => {
if (_this.value) {
editor.setContent(_this.value)
}
_this.hasInit = true
editor.on('NodeChange Change KeyUp SetContent', () => {
this.hasChange = true
this.$emit('input', editor.getContent())
})
},
setup(editor) {
editor.on('FullscreenStateChanged', (e) => {
_this.fullscreen = e.state
})
}
//
// images_dataimg_filter(img) {
// setTimeout(() => {
// const $image = $(img);
// $image.removeAttr('width');
// $image.removeAttr('height');
// if ($image[0].height && $image[0].width) {
// $image.attr('data-wscntype', 'image');
// $image.attr('data-wscnh', $image[0].height);
// $image.attr('data-wscnw', $image[0].width);
// $image.addClass('wscnph');
// }
// }, 0);
// return img
// },
// images_upload_handler(blobInfo, success, failure, progress) {
// progress(0);
// const token = _this.$store.getters.token;
// getToken(token).then(response => {
// const url = response.data.qiniu_url;
// const formData = new FormData();
// formData.append('token', response.data.qiniu_token);
// formData.append('key', response.data.qiniu_key);
// formData.append('file', blobInfo.blob(), url);
// upload(formData).then(() => {
// success(url);
// progress(100);
// })
// }).catch(err => {
// failure('')
// console.log(err);
// });
// },
})
},
destroyTinymce() {
const tinymce = window.tinymce.get(this.tinymceId)
if (this.fullscreen) {
tinymce.execCommand('mceFullScreen')
}
if (tinymce) {
tinymce.destroy()
}
},
setContent(value) {
window.tinymce.get(this.tinymceId).setContent(value)
},
getContent() {
window.tinymce.get(this.tinymceId).getContent()
},
imageSuccessCBK(arr) {
const _this = this
arr.forEach(v => {
window.tinymce.get(_this.tinymceId).insertContent(`<img class="wscnph" src="${v.url}" >`)
})
}
}
}
</script>
<style scoped>
.tinymce-container {
position: relative;
line-height: normal;
}
.tinymce-container>>>.mce-fullscreen {
z-index: 10000;
}
.tinymce-textarea {
visibility: hidden;
z-index: -1;
}
.editor-custom-btn-container {
position: absolute;
right: 4px;
top: 4px;
/*z-index: 2005;*/
}
.fullscreen .editor-custom-btn-container {
z-index: 10000;
position: fixed;
}
.editor-upload-btn {
display: inline-block;
}
</style>

View file

@ -1,7 +0,0 @@
// Any plugins you want to use has to be imported
// Detail plugins list see https://www.tinymce.com/docs/plugins/
// Custom builds see https://www.tinymce.com/download/custom-builds/
const plugins = ['advlist anchor autolink autosave code codesample colorpicker colorpicker contextmenu directionality emoticons fullscreen hr image imagetools insertdatetime link lists media nonbreaking noneditable pagebreak paste preview print save searchreplace spellchecker tabfocus table template textcolor textpattern visualblocks visualchars wordcount']
export default plugins

View file

@ -1,6 +0,0 @@
// Here is a list of the toolbar
// Detail list see https://www.tinymce.com/docs/advanced/editor-control-identifiers/#toolbarcontrols
const toolbar = ['searchreplace bold italic underline strikethrough alignleft aligncenter alignright outdent indent blockquote undo redo removeformat subscript superscript code codesample', 'hr bullist numlist link image charmap preview anchor pagebreak insertdatetime media table emoticons forecolor backcolor fullscreen']
export default toolbar

View file

@ -10,7 +10,6 @@ export default {
icons: 'Icons',
components: 'Components',
componentIndex: 'Introduction',
tinymce: 'Tinymce',
markdown: 'Markdown',
jsonEditor: 'JSON Editor',
dndList: 'Dnd List',
@ -67,6 +66,7 @@ export default {
users: 'Users',
reports: 'Reports',
settings: 'Settings',
moderationLog: 'Moderation Log',
'emoji-packs': 'Emoji packs'
},
navbar: {
@ -104,7 +104,6 @@ export default {
},
components: {
documentation: 'Documentation',
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',
@ -200,6 +199,7 @@ export default {
disableRemoteSubscriptionForMultiple: 'Disallow following users from remote instances',
disableAnySubscription: 'Disallow following user at all',
disableAnySubscriptionForMultiple: 'Disallow following users at all',
requirePasswordReset: 'Require password reset on next login',
selectUsers: 'Select users to apply actions to multiple users',
moderateUsers: 'Moderate multiple users',
createAccount: 'Create new account',
@ -212,6 +212,8 @@ export default {
deleteMultipleUsersConfirmation: 'Are you sure you want to delete accounts of all selected users?',
addTagForMultipleUsersConfirmation: 'Are you sure you want to apply tag to all selected users?',
removeTagFromMultipleUsersConfirmation: 'Are you sure you want to remove tag from all selected users?',
requirePasswordResetConfirmation: 'Are you sure you want to require password reset for all selected users?',
mailerMustBeEnabled: 'To require user\'s password reset you must enable mailer.',
ok: 'Okay',
completed: 'Completed',
cancel: 'Cancel',
@ -239,11 +241,17 @@ export default {
tags: 'Tags',
moderator: 'Moderator',
admin: 'Admin',
local: 'Local',
local: 'local',
external: 'external',
localUppercase: 'Local',
nickname: 'Nickname',
deactivated: 'Deactivated',
recentStatuses: 'Recent Statues',
showPrivateStatuses: 'Show private statuses'
showPrivateStatuses: 'Show private statuses',
roles: 'Roles',
activeUppercase: 'Active',
active: 'active',
deactivated: 'deactivated',
noStatuses: 'No statuses to show'
},
usersFilter: {
inputPlaceholder: 'Select filter',
@ -256,6 +264,7 @@ export default {
},
reports: {
reports: 'Reports',
groupedReports: 'Grouped reports',
reply: 'Reply',
from: 'From',
showNotes: 'Show notes',
@ -291,7 +300,8 @@ export default {
actor: 'Actor',
actors: 'Actors',
content: 'Content',
reportedStatus: 'Reported status'
reportedStatus: 'Reported status',
statusDeleted: 'This status has been deleted'
},
reportsFilter: {
inputPlaceholder: 'Select filter',
@ -299,6 +309,9 @@ export default {
closed: 'Closed',
resolved: 'Resolved'
},
moderationLog: {
moderationLog: 'Moderation Log'
},
settings: {
settings: 'Settings',
instance: 'Instance',
@ -322,7 +335,66 @@ export default {
rateLimiters: 'Rate limiters',
database: 'Database',
other: 'Other',
success: 'Settings changed successfully!'
relays: 'Relays',
follow: 'Follow',
followRelay: 'Follow new relay',
instanceUrl: 'Instance URL',
success: 'Settings changed successfully!',
emojiPacks: 'Emoji packs',
reloadEmoji: 'Reload emoji',
importPacks: 'Import packs from the server filesystem',
importEmojiTooltip: 'Importing from the filesystem will scan the directories and import those without pack.json but with emoji.txt or without neither',
localPacks: 'Local packs',
refreshLocalPacks: 'Refresh local packs',
createLocalPack: 'Create a new local pack',
packs: 'Packs',
remotePacks: 'Remote packs',
remoteInstanceAddress: 'Remote instance address',
refreshRemote: 'Refresh remote packs',
sharePack: 'Share pack',
homepage: 'Homepage',
description: 'Description',
license: 'License',
fallbackSrc: 'Fallback source',
fallbackSrcSha: 'Fallback source SHA',
savePackMetadata: 'Save pack metadata',
addNewEmoji: 'Add new emoji to the pack',
shortcode: 'Shortcode',
uploadFile: 'Upload a file',
customFilename: 'Custom filename',
optional: 'optional',
customFilenameDesc: 'Custom file name (optional)',
url: 'URL',
required: 'required',
clickToUpload: 'Click to upload',
showPackContents: 'Show pack contents',
manageEmoji: 'Manage existing emoji',
file: 'File',
update: 'Update',
remove: 'Remove',
selectLocalPack: 'Select the local pack to copy to',
localPack: 'Local pack',
specifyShortcode: 'Specify a custom shortcode',
specifyFilename: 'Specify a custom filename',
leaveEmptyShortcode: 'leave empty to use the same shortcode',
leaveEmptyFilename: 'leave empty to use the same filename',
copy: 'Copy',
copyToLocalPack: 'Copy to local pack',
thisWillDownload: 'This will download the',
downloadToCurrentInstance: 'pack to the current instance under the name',
canBeChanged: 'can be changed below',
willBeUsable: 'It will then be usable and shareable from the current instance',
downloadPack: 'Download pack',
deletePack: 'Delete pack',
downloadSharedPack: 'Download shared pack to current instance',
downloadAsOptional: 'Download as (optional)',
downloadPackArchive: 'Download pack archive',
successfullyDownloaded: 'Successfully downloaded',
successfullyImported: 'Successfully imported',
nowNewPacksToImport: 'No new packs to import',
successfullyUpdated: 'Successfully updated',
metadatLowerCase: 'metadata',
files: 'files'
},
invites: {
inviteTokens: 'Invite tokens',

View file

@ -10,7 +10,6 @@ export default {
icons: 'Iconos',
components: 'Componentes',
componentIndex: 'Introducción',
tinymce: 'Tinymce',
markdown: 'Markdown',
jsonEditor: 'Editor JSON',
dndList: 'Lista Dnd',
@ -96,7 +95,6 @@ export default {
},
components: {
documentation: 'Documentación',
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',

View file

@ -10,7 +10,6 @@ export default {
icons: 'Icònas',
components: 'Compausants',
componentIndex: 'Introduccion',
tinymce: 'Tinymce',
markdown: 'Markdown',
jsonEditor: 'JSON Editor',
dndList: 'Dnd List',
@ -97,7 +96,6 @@ export default {
},
components: {
documentation: 'Documentacion',
tinymceTips: 'Rich text editor is a core part of management system, but at the same time is a place with lots of problems. In the process of selecting rich texts, I also walked a lot of detours. The common rich text editors in the market are basically used, and the finally chose Tinymce. See documentation for more detailed rich text editor comparisons and introductions.',
dropzoneTips: 'Because my business has special needs, and has to upload images to qiniu, so instead of a third party, I chose encapsulate it by myself. It is very simple, you can see the detail code in @/components/Dropzone.',
stickyTips: 'when the page is scrolled to the preset position will be sticky on the top.',
backToTopTips1: 'When the page is scrolled to the specified position, the Back to Top button appears in the lower right corner',

View file

@ -10,7 +10,6 @@ export default {
icons: '图标',
components: '组件',
componentIndex: '介绍',
tinymce: '富文本编辑器',
markdown: 'Markdown',
jsonEditor: 'JSON编辑器',
dndList: '列表拖拽',
@ -96,7 +95,6 @@ export default {
},
components: {
documentation: '文档',
tinymceTips: '富文本是管理后台一个核心的功能但同时又是一个有很多坑的地方。在选择富文本的过程中我也走了不少的弯路市面上常见的富文本都基本用过了最终权衡了一下选择了Tinymce。更详细的富文本比较和介绍见',
dropzoneTips: '由于我司业务有特殊需求,而且要传七牛 所以没用第三方,选择了自己封装。代码非常的简单,具体代码你可以在这里看到 @/components/Dropzone',
stickyTips: '当页面滚动到预设的位置会吸附在顶部',
backToTopTips1: '页面滚动到指定位置会在右下角出现返回顶部按钮',

View file

@ -63,16 +63,16 @@ const invites = {
]
}
const emojiPacksDisabled = disabledFeatures.includes('emoji-packs')
const emojiPacks = {
path: '/emoji-packs',
const moderationLogDisabled = disabledFeatures.includes('moderation-log')
const moderationLog = {
path: '/moderation_log',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/emoji-packs/index'),
name: 'Emoji packs',
meta: { title: 'emoji-packs', icon: 'settings', noCache: true }
component: () => import('@/views/moderation_log/index'),
name: 'Moderation Log',
meta: { title: 'moderationLog', icon: 'list', noCache: true }
}
]
}
@ -136,15 +136,15 @@ export const asyncRouterMap = [
path: 'index',
component: () => import('@/views/users/index'),
name: 'Users',
meta: { title: 'Users', icon: 'peoples', noCache: true }
meta: { title: 'users', icon: 'peoples', noCache: true }
}
]
},
...(statusesDisabled ? [] : [statuses]),
...(settingsDisabled ? [] : [settings]),
...(reportsDisabled ? [] : [reports]),
...(invitesDisabled ? [] : [invites]),
...(emojiPacksDisabled ? [] : [emojiPacks]),
...(moderationLogDisabled ? [] : [moderationLog]),
...(settingsDisabled ? [] : [settings]),
{
path: '/users/:id',
component: Layout,

View file

@ -2,9 +2,11 @@ import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import errorLog from './modules/errorLog'
import moderationLog from './modules/moderationLog'
import invites from './modules/invites'
import peers from './modules/peers'
import permission from './modules/permission'
import relays from './modules/relays'
import reports from './modules/reports'
import settings from './modules/settings'
import status from './modules/status'
@ -13,7 +15,7 @@ import user from './modules/user'
import userProfile from './modules/userProfile'
import users from './modules/users'
import getters from './getters'
import emoji_packs from './modules/emoji_packs.js'
import emojiPacks from './modules/emojiPacks.js'
Vue.use(Vuex)
@ -21,9 +23,11 @@ const store = new Vuex.Store({
modules: {
app,
errorLog,
moderationLog,
invites,
peers,
permission,
relays,
reports,
settings,
status,
@ -31,7 +35,7 @@ const store = new Vuex.Store({
user,
userProfile,
users,
emoji_packs
emojiPacks
},
getters
})

View file

@ -1,12 +1,14 @@
import { listPacks,
import {
listPacks,
listRemotePacks,
downloadFrom,
reloadEmoji,
createPack,
deletePack,
savePackMetadata,
importFromFS,
updatePackFile } from '@/api/emoji_packs'
updatePackFile } from '@/api/emojiPacks'
import i18n from '@/lang'
import { Message } from 'element-ui'
import Vue from 'vue'
@ -42,33 +44,30 @@ const packs = {
}
},
actions: {
async SetLocalEmojiPacks({ commit, getters, state }) {
const { data } = await listPacks(getters.authHost)
commit('SET_LOCAL_PACKS', data)
async CreatePack({ getters }, { name }) {
await createPack(getters.authHost, getters.token, name)
},
async SetRemoteEmojiPacks({ commit, getters, state }, { remoteInstance }) {
const { data } = await listPacks(remoteInstance)
commit('SET_REMOTE_PACKS', data)
async DeletePack({ getters }, { name }) {
await deletePack(getters.authHost, getters.token, name)
},
async DownloadFrom({ commit, getters, state }, { instanceAddress, packName, as }) {
async DownloadFrom({ getters }, { instanceAddress, packName, as }) {
const result = await downloadFrom(getters.authHost, instanceAddress, packName, as, getters.token)
if (result.data === 'ok') {
Message({
message: `Successfully downloaded ${packName}`,
message: `${i18n.t('settings.successfullyDownloaded')} ${packName}`,
type: 'success',
duration: 5 * 1000
})
}
},
async ReloadEmoji({ commit, getters, state }) {
await reloadEmoji(getters.authHost, getters.token)
},
async ImportFromFS({ commit, getters, state }) {
async ImportFromFS({ getters }) {
const result = await importFromFS(getters.authHost, getters.token)
if (result.status === 200) {
const message = result.data.length > 0 ? `Successfully imported ${result.data}` : 'No new packs to import'
const message = result.data.length > 0
? `${i18n.t('settings.successfullyImported')} ${result.data}`
: i18n.t('settings.nowNewPacksToImport')
Message({
message,
@ -77,17 +76,9 @@ const packs = {
})
}
},
async DeletePack({ commit, getters, state }, { name }) {
await deletePack(getters.authHost, getters.token, name)
async ReloadEmoji({ getters }) {
await reloadEmoji(getters.authHost, getters.token)
},
async CreatePack({ commit, getters, state }, { name }) {
await createPack(getters.authHost, getters.token, name)
},
async UpdateLocalPackVal({ commit, getters, state }, args) {
commit('UPDATE_LOCAL_PACK_VAL', args)
},
async SavePackMetadata({ commit, getters, state }, { packName }) {
const result =
await savePackMetadata(
@ -99,7 +90,7 @@ const packs = {
if (result.status === 200) {
Message({
message: `Successfully updated ${packName} metadata`,
message: `${i18n.t('settings.successfullyUpdated')} ${packName} ${i18n.t('settings.metadatLowerCase')}`,
type: 'success',
duration: 5 * 1000
})
@ -107,21 +98,32 @@ const packs = {
commit('UPDATE_LOCAL_PACK_PACK', { name: packName, pack: result.data })
}
},
async SetLocalEmojiPacks({ commit, getters }) {
const { data } = await listPacks(getters.authHost)
commit('SET_LOCAL_PACKS', data)
},
async SetRemoteEmojiPacks({ commit, getters }, { remoteInstance }) {
const { data } = await listRemotePacks(getters.authHost, getters.token, remoteInstance)
async UpdateAndSavePackFile({ commit, getters, state }, args) {
commit('SET_REMOTE_PACKS', data)
},
async UpdateAndSavePackFile({ commit, getters }, args) {
const result = await updatePackFile(getters.authHost, getters.token, args)
if (result.status === 200) {
const { packName } = args
Message({
message: `Successfully updated ${packName} files`,
message: `${i18n.t('settings.successfullyUpdated')} ${packName} ${i18n.t('settings.metadatLowerCase')}`,
type: 'success',
duration: 5 * 1000
})
commit('UPDATE_LOCAL_PACK_FILES', { name: packName, files: result.data })
}
},
async UpdateLocalPackVal({ commit }, args) {
commit('UPDATE_LOCAL_PACK_VAL', args)
}
}
}

View file

@ -1,4 +1,6 @@
import { generateInviteToken, inviteViaEmail, listInviteTokens, revokeToken } from '@/api/invites'
import { Message } from 'element-ui'
import i18n from '@/lang'
const invites = {
state: {
@ -25,18 +27,35 @@ const invites = {
commit('SET_LOADING', false)
},
async GenerateInviteToken({ commit, dispatch, getters }, { maxUse, expiresAt }) {
const { data } = await generateInviteToken(maxUse, expiresAt, getters.authHost, getters.token)
commit('SET_NEW_TOKEN', { token: data.token, maxUse: data.max_use, expiresAt: data.expires_at })
try {
const { data } = await generateInviteToken(maxUse, expiresAt, getters.authHost, getters.token)
commit('SET_NEW_TOKEN', { token: data.token, maxUse: data.max_use, expiresAt: data.expires_at })
} catch (_e) {
return
}
dispatch('FetchInviteTokens')
},
async InviteUserViaEmail({ commit, dispatch, getters }, { email, name }) {
await inviteViaEmail(email, name, getters.authHost, getters.token)
try {
await inviteViaEmail(email, name, getters.authHost, getters.token)
} catch (_e) {
return
}
Message({
message: i18n.t('invites.emailSent'),
type: 'success',
duration: 5 * 1000
})
},
RemoveNewToken({ commit }) {
commit('SET_NEW_TOKEN', {})
},
async RevokeToken({ commit, dispatch, getters }, token) {
await revokeToken(token, getters.authHost, getters.token)
try {
await revokeToken(token, getters.authHost, getters.token)
} catch (_e) {
return
}
dispatch('FetchInviteTokens')
}
}

View file

@ -0,0 +1,51 @@
import { fetchLog, fetchAdmins, fetchModerators } from '@/api/moderationLog'
const moderationLog = {
state: {
fetchedLog: [],
logItemsCount: 0,
admins: [],
moderators: [],
logLoading: true,
adminsLoading: true
},
mutations: {
SET_LOG_LOADING: (state, status) => {
state.logLoading = status
},
SET_ADMINS_LOADING: (state, status) => {
state.adminsLoading = status
},
SET_MODERATION_LOG: (state, log) => {
state.fetchedLog = log
},
SET_MODERATION_LOG_COUNT: (state, count) => {
state.logItemsCount = count
},
SET_ADMINS: (state, admins) => {
state.admins = admins
},
SET_MODERATORS: (state, moderators) => {
state.moderators = moderators
}
},
actions: {
async FetchModerationLog({ commit, getters }, opts = {}) {
const response = await fetchLog(getters.authHost, getters.token, opts)
commit('SET_MODERATION_LOG', response.data.items)
commit('SET_MODERATION_LOG_COUNT', response.data.total)
commit('SET_LOG_LOADING', false)
},
async FetchAdmins({ commit, getters }) {
const adminsResponse = await fetchAdmins(getters.authHost, getters.token)
const moderatorsResponse = await fetchModerators(getters.authHost, getters.token)
commit('SET_ADMINS', adminsResponse.data)
commit('SET_MODERATORS', moderatorsResponse.data)
commit('SET_ADMINS_LOADING', false)
}
}
}
export default moderationLog

View file

@ -0,0 +1,56 @@
import { fetchRelays, addRelay, deleteRelay } from '@/api/relays'
const relays = {
state: {
fetchedRelays: [],
loading: true
},
mutations: {
SET_LOADING: (state, loading) => {
state.loading = loading
},
SET_RELAYS: (state, relays) => {
state.fetchedRelays = relays
},
ADD_RELAY: (state, relay) => {
state.fetchedRelays = [...state.fetchedRelays, relay]
},
DELETE_RELAY: (state, relay) => {
state.fetchedRelays = state.fetchedRelays.filter(fetchedRelay => fetchedRelay !== relay)
}
},
actions: {
async FetchRelays({ commit, getters }) {
commit('SET_LOADING', true)
const response = await fetchRelays(getters.authHost, getters.token)
commit('SET_RELAYS', response.data.relays)
commit('SET_LOADING', false)
},
async AddRelay({ commit, dispatch, getters }, relay) {
commit('ADD_RELAY', relay)
try {
await addRelay(relay, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('FetchRelays')
}
},
async DeleteRelay({ commit, dispatch, getters }, relay) {
commit('DELETE_RELAY', relay)
try {
await deleteRelay(relay, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('FetchRelays')
}
}
}
}
export default relays

View file

@ -38,13 +38,25 @@ const reports = {
}
},
actions: {
async ChangeReportState({ dispatch, getters, state }, { reportState, reportId }) {
await changeState(reportState, reportId, getters.authHost, getters.token)
dispatch('FetchReports', state.currentPage)
async ChangeReportState({ commit, getters, state }, reportsData) {
changeState(reportsData, getters.authHost, getters.token)
const updatedReports = state.fetchedReports.map(report => {
const updatedReportsIds = reportsData.map(({ id }) => id)
return updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report
})
const updatedGroupedReports = state.fetchedGroupedReports.map(group => {
const updatedReportsIds = reportsData.map(({ id }) => id)
const updatedReports = group.reports.map(report => updatedReportsIds.includes(report.id) ? { ...report, state: reportsData[0].state } : report)
return { ...group, reports: updatedReports }
})
commit('SET_REPORTS', updatedReports)
commit('SET_GROUPED_REPORTS', updatedGroupedReports)
},
ClearFetchedReports({ commit }) {
commit('SET_REPORTS', [])
commit('SET_LAST_REPORT_ID', '')
},
async FetchReports({ commit, getters, state }, page) {
commit('SET_LOADING', true)
@ -58,7 +70,6 @@ const reports = {
async FetchGroupedReports({ commit, getters }) {
commit('SET_LOADING', true)
const { data } = await fetchGroupedReports(getters.authHost, getters.token)
console.log(reports)
commit('SET_GROUPED_REPORTS', data.reports)
commit('SET_LOADING', false)

View file

@ -1,6 +1,7 @@
import i18n from '@/lang'
import { fetchSettings, updateSettings, uploadMedia } from '@/api/settings'
import { initialSettings } from '@/api/initialDataForConfig'
import { filterIgnored, parseTuples, valueHasTuples, wrapConfig } from './normalizers'
import { Message } from 'element-ui'
const settings = {
state: {
@ -116,11 +117,7 @@ const settings = {
async FetchSettings({ commit, dispatch, getters }) {
commit('SET_LOADING', true)
const response = await fetchSettings(getters.authHost, getters.token)
if (response.data.configs.length === 0) {
dispatch('SubmitChanges', initialSettings)
} else {
commit('SET_SETTINGS', response.data.configs)
}
commit('SET_SETTINGS', response.data.configs)
commit('SET_LOADING', false)
},
RewriteConfig({ commit }, { tab, data }) {
@ -129,10 +126,17 @@ const settings = {
async SubmitChanges({ getters, commit, state }, data) {
const filteredSettings = filterIgnored(state.settings, state.ignoredIfNotEnabled)
const configs = data || wrapConfig(filteredSettings)
const response = await updateSettings(configs, getters.authHost, getters.token)
if (data) {
try {
const response = await updateSettings(configs, getters.authHost, getters.token)
commit('SET_SETTINGS', response.data.configs)
} catch (_e) {
return
}
Message({
message: i18n.t('settings.success'),
type: 'success',
duration: 5 * 1000
})
},
UpdateSettings({ commit }, { tab, data }) {
commit('UPDATE_SETTINGS', { tab, data })

View file

@ -19,18 +19,22 @@ const status = {
actions: {
async ChangeStatusScope({ dispatch, getters }, { statusId, isSensitive, visibility, reportCurrentPage, userId, godmode }) {
await changeStatusScope(statusId, isSensitive, visibility, getters.authHost, getters.token)
if (reportCurrentPage !== 0) {
if (reportCurrentPage !== 0) { // called from Reports
dispatch('FetchReports', reportCurrentPage)
} else if (userId.length > 0) {
} else if (userId.length > 0) { // called from User profile
dispatch('FetchUserStatuses', { userId, godmode })
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async DeleteStatus({ dispatch, getters }, { statusId, reportCurrentPage, userId, godmode }) {
await deleteStatus(statusId, getters.authHost, getters.token)
if (reportCurrentPage !== 0) {
if (reportCurrentPage !== 0) { // called from Reports
dispatch('FetchReports', reportCurrentPage)
} else if (userId.length > 0) {
} else if (userId.length > 0) { // called from User profile
dispatch('FetchUserStatuses', { userId, godmode })
} else { // called from GroupedReports
dispatch('FetchGroupedReports')
}
},
async FetchStatusesByInstance({ commit, getters }, { instance, page, pageSize }) {

View file

@ -1,4 +1,5 @@
import { loginByUsername, getUserInfo } from '@/api/login'
import { getNodeInfo } from '@/api/nodeInfo'
import { getToken, setToken, removeToken, getAuthHost, setAuthHost, removeAuthHost } from '@/utils/auth'
const user = {
@ -15,7 +16,8 @@ const user = {
roles: [],
setting: {
articlePlatform: []
}
},
nodeInfo: {}
},
mutations: {
@ -48,6 +50,9 @@ const user = {
},
SET_AUTH_HOST: (state, authHost) => {
state.authHost = authHost
},
SET_NODE_INFO: (state, nodeInfo) => {
state.nodeInfo = nodeInfo
}
},
@ -67,7 +72,11 @@ const user = {
})
})
},
async GetNodeInfo({ commit, state }) {
const nodeInfo = await getNodeInfo(state.authHost)
commit('SET_NODE_INFO', nodeInfo.data)
},
GetUserInfo({ commit, state }) {
return new Promise((resolve, reject) => {
getUserInfo(state.token, state.authHost).then(response => {

View file

@ -1,4 +1,19 @@
import { addRight, createNewAccount, deleteRight, deleteUser, fetchUsers, getPasswordResetToken, searchUsers, tagUser, toggleUserActivation, untagUser } from '@/api/users'
import { Message } from 'element-ui'
import i18n from '@/lang'
import {
activateUsers,
addRight,
createNewAccount,
deactivateUsers,
deleteRight,
deleteUsers,
fetchUsers,
getPasswordResetToken,
searchUsers,
tagUser,
untagUser,
requirePasswordReset
} from '@/api/users'
const users = {
state: {
@ -25,12 +40,6 @@ const users = {
SET_LOADING: (state, status) => {
state.loading = status
},
SWAP_USER: (state, updatedUser) => {
const updated = state.fetchedUsers.map(user => user.id === updatedUser.id ? updatedUser : user)
state.fetchedUsers = updated
.map(user => user.nickname ? user : { ...user, nickname: '' })
.sort((a, b) => a.nickname.localeCompare(b.nickname))
},
SWAP_USERS: (state, users) => {
const usersWithoutSwapped = users.reduce((acc, user) => {
return acc.filter(u => u.id !== user.id)
@ -64,43 +73,149 @@ const users = {
}
},
actions: {
async AddTag({ commit, getters }, { users, tag }) {
const nicknames = users.map(user => user.nickname)
await tagUser(nicknames, [tag], getters.authHost, getters.token)
async ActivateUsers({ commit, dispatch, getters, state }, users) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: false }
})
commit('SWAP_USERS', updatedUsers)
commit('SWAP_USERS', users.map(user => ({ ...user, tags: [...user.tags, tag] })))
const usersNicknames = users.map(user => user.nickname)
try {
await activateUsers(usersNicknames, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async AddRight({ commit, dispatch, getters, state }, { users, right }) {
const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: true }} : user
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
try {
await addRight(usersNicknames, right, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async AddTag({ commit, dispatch, getters, state }, { users, tag }) {
const updatedUsers = users.map(user => {
return { ...user, tags: [...user.tags, tag] }
})
commit('SWAP_USERS', updatedUsers)
const nicknames = users.map(user => user.nickname)
try {
await tagUser(nicknames, [tag], getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async ClearFilters({ commit, dispatch, state }) {
commit('CLEAR_USERS_FILTERS')
dispatch('SearchUsers', { query: state.searchQuery, page: 1 })
},
async CreateNewAccount({ dispatch, getters, state }, { nickname, email, password }) {
await createNewAccount(nickname, email, password, getters.authHost, getters.token)
dispatch('FetchUsers', { page: state.currentPage })
try {
await createNewAccount(nickname, email, password, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async DeleteUser({ commit, getters, state }, user) {
const { data } = await deleteUser(user.nickname, getters.authHost, getters.token)
const users = state.fetchedUsers.filter(user => user.nickname !== data)
commit('SET_USERS', users)
async DeactivateUsers({ commit, dispatch, getters, state }, users) {
const updatedUsers = users.map(user => {
return { ...user, deactivated: true }
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
try {
await deactivateUsers(usersNicknames, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async FetchUsers({ commit, state, getters }, { page }) {
async DeleteRight({ commit, dispatch, getters, state }, { users, right }) {
const updatedUsers = users.map(user => {
return user.local ? { ...user, roles: { ...user.roles, [right]: false }} : user
})
commit('SWAP_USERS', updatedUsers)
const usersNicknames = users.map(user => user.nickname)
try {
await deleteRight(usersNicknames, right, getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async DeleteUsers({ commit, dispatch, getters, state }, users) {
const usersNicknames = users.map(user => user.nickname)
try {
await deleteUsers(usersNicknames, getters.authHost, getters.token)
} catch (_e) {
return
}
const deletedUsersIds = users.map(deletedUser => deletedUser.id)
const updatedUsers = state.fetchedUsers.filter(user => !deletedUsersIds.includes(user.id))
commit('SET_USERS', updatedUsers)
dispatch('SuccessMessage')
},
async FetchUsers({ commit, dispatch, getters, state }, { page }) {
commit('SET_LOADING', true)
const filters = Object.keys(state.filters).filter(filter => state.filters[filter]).join()
const response = await fetchUsers(filters, getters.authHost, getters.token, page)
await dispatch('GetNodeInfo')
loadUsers(commit, page, response.data)
},
async GetPasswordResetToken({ commit, state, getters }, nickname) {
async GetPasswordResetToken({ commit, getters }, nickname) {
const { data } = await getPasswordResetToken(nickname, getters.authHost, getters.token)
commit('SET_PASSWORD_RESET_TOKEN', data)
},
RemovePasswordToken({ commit }) {
commit('SET_PASSWORD_RESET_TOKEN', { link: '', token: '' })
},
async RemoveTag({ commit, getters }, { users, tag }) {
const nicknames = users.map(user => user.nickname)
await untagUser(nicknames, [tag], getters.authHost, getters.token)
async RemoveTag({ commit, dispatch, getters, state }, { users, tag }) {
const updatedUsers = users.map(user => {
return { ...user, tags: user.tags.filter(userTag => userTag !== tag) }
})
commit('SWAP_USERS', updatedUsers)
commit('SWAP_USERS', users.map(user => ({ ...user, tags: user.tags.filter(userTag => userTag !== tag) })))
const nicknames = users.map(user => user.nickname)
try {
await untagUser(nicknames, [tag], getters.authHost, getters.token)
} catch (_e) {
return
} finally {
dispatch('SearchUsers', { query: state.searchQuery, page: state.currentPage })
}
dispatch('SuccessMessage')
},
async RequirePasswordReset({ dispatch, getters }, user) {
try {
await requirePasswordReset(user.nickname, getters.authHost, getters.token)
} catch (_e) {
return
}
dispatch('SuccessMessage')
},
async SearchUsers({ commit, dispatch, state, getters }, { query, page }) {
if (query.length === 0) {
@ -116,9 +231,12 @@ const users = {
loadUsers(commit, page, response.data)
}
},
async ToggleUserActivation({ commit, getters }, nickname) {
const { data } = await toggleUserActivation(nickname, getters.authHost, getters.token)
commit('SWAP_USER', data)
SuccessMessage() {
return Message({
message: i18n.t('users.completed'),
type: 'success',
duration: 5 * 1000
})
},
async ToggleUsersFilter({ commit, dispatch, state }, filters) {
const defaultFilters = {
@ -130,14 +248,6 @@ const users = {
const currentFilters = { ...defaultFilters, ...filters }
commit('SET_USERS_FILTERS', currentFilters)
dispatch('SearchUsers', { query: state.searchQuery, page: 1 })
},
async ToggleRight({ commit, getters }, { user, right }) {
user.roles[right]
? await deleteRight(user.nickname, right, getters.authHost, getters.token)
: await addRight(user.nickname, right, getters.authHost, getters.token)
const updatedUser = { ...user, roles: { ...user.roles, [right]: !user.roles[right] }}
commit('SWAP_USER', updatedUser)
}
}
}

View file

@ -10,9 +10,20 @@ const service = axios.create({
service.interceptors.response.use(
response => response,
error => {
console.log('Error ' + error)
let errorMessage
console.log(`Error ${error}`)
if (error.response) {
const edata = error.response.data.error ? error.response.data.error : error.response.data
errorMessage = !error.response.headers['content-type'].includes('application/json')
? `${error.message}`
: `${error.message} - ${edata}`
} else {
errorMessage = error
}
Message({
message: `${error.message} - ${error.response.data}`,
message: errorMessage,
type: 'error',
duration: 5 * 1000
})

View file

@ -1,218 +0,0 @@
<template>
<div>
<h2>{{ name }}</h2>
<prop-editing-row name="Share pack">
<el-switch v-model="share" :disabled="!isLocal" />
</prop-editing-row>
<prop-editing-row name="Homepage">
<el-input v-if="isLocal" v-model="homepage" />
<el-input v-else :value="homepage" />
</prop-editing-row>
<prop-editing-row name="Description">
<el-input v-if="isLocal" :rows="2" v-model="description" type="textarea" />
<el-input v-else :rows="2" :value="description" type="textarea" />
</prop-editing-row>
<prop-editing-row name="License">
<el-input v-if="isLocal" v-model="license" />
<el-input v-else :value="license" />
</prop-editing-row>
<prop-editing-row name="Fallback source">
<el-input v-if="isLocal" v-model="fallbackSrc" />
<el-input v-else :value="fallbackSrc" />
</prop-editing-row>
<prop-editing-row v-if="fallbackSrc && fallbackSrc.trim() !== ''" name="Fallback source SHA">
{{ pack.pack["fallback-src-sha256"] }}
</prop-editing-row>
<el-button v-if="isLocal" type="success" @click="savePackMetadata">Save pack metadata</el-button>
<el-collapse v-model="shownPackEmoji" class="contents-collapse">
<el-collapse-item :name="name" title="Show pack contents">
<new-emoji-uploader v-if="isLocal" :pack-name="name" class="new-emoji-uploader" />
<h4>Manage existing emoji</h4>
<single-emoji-editor
v-for="(file, ename) in pack.files"
:key="ename"
:host="host"
:pack-name="name"
:name="ename"
:file="file"
:is-local="isLocal" />
</el-collapse-item>
</el-collapse>
<div v-if="!isLocal" class="shared-pack-dl-box">
<div>
This will download the "{{ name }}" pack to the current instance under the name
"{{ downloadSharedAs.trim() === '' ? name : downloadSharedAs }}" (can be changed below).
It will then be usable and shareable from the current instance.
</div>
<el-button type="primary" @click="downloadFromInstance">
Download shared pack to current instance
</el-button>
<el-input v-model="downloadSharedAs" class="dl-as-input" placeholder="Download as (optional)" />
</div>
<el-link
v-if="pack.pack['can-download']"
:href="`//${host}/api/pleroma/emoji/packs/${name}/download_shared`"
type="primary"
target="_blank">
Download pack archive
</el-link>
<div v-if="isLocal" class="pack-actions">
<el-button type="danger" @click="deletePack">
Delete the local pack
</el-button>
</div>
</div>
</template>
<style>
.shared-pack-dl-box {
margin: 1em;
}
.dl-as-input {
margin: 1em;
max-width: 30%;
}
.contents-collapse {
margin: 1em;
}
.pack-actions {
margin-top: 1em;
}
.new-emoji-uploader {
margin-bottom: 3em;
}
</style>
<script>
import PropEditingRow from './PropertyEditingRow.vue'
import SingleEmojiEditor from './SingleEmojiEditor.vue'
import NewEmojiUploader from './NewEmojiUploader.vue'
export default {
components: { PropEditingRow, SingleEmojiEditor, NewEmojiUploader },
props: {
name: {
type: String,
required: true
},
pack: {
type: Object,
required: true
},
host: {
type: String,
required: true
},
isLocal: {
type: Boolean,
required: true
}
},
data() {
return {
shownPackEmoji: [],
downloadSharedAs: ''
}
},
computed: {
share: {
get() { return this.pack.pack['share-files'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'share-files', value }
)
}
},
homepage: {
get() { return this.pack.pack['homepage'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'homepage', value }
)
}
},
description: {
get() { return this.pack.pack['description'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'description', value }
)
}
},
license: {
get() { return this.pack.pack['license'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'license', value }
)
}
},
fallbackSrc: {
get() { return this.pack.pack['fallback-src'] },
set(value) {
if (value.trim() !== '') {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src', value }
)
} else {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src', value: null }
)
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src-sha256', value: null }
)
}
}
}
},
methods: {
downloadFromInstance() {
this.$store.dispatch(
'DownloadFrom',
{ instanceAddress: this.host, packName: this.name, as: this.downloadSharedAs }
).then(() => this.$store.dispatch('ReloadEmoji'))
.then(() => this.$store.dispatch('SetLocalEmojiPacks'))
},
deletePack() {
this.$confirm('This will delete the pack, are you sure?', 'Warning', {
confirmButtonText: 'Yes, delete the pack',
cancelButtonText: 'No, leave it be',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeletePack', { name: this.name })
.then(() => this.$store.dispatch('ReloadEmoji'))
.then(() => this.$store.dispatch('SetLocalEmojiPacks'))
}).catch(() => {})
},
savePackMetadata() {
this.$store.dispatch('SavePackMetadata', { packName: this.name })
}
}
}
</script>

View file

@ -1,93 +0,0 @@
<template>
<div>
<h4>Add new emoji to the pack</h4>
<el-row :gutter="20">
<el-col :span="4" class="new-emoji-col">
<el-input v-model="shortcode" placeholder="Shortcode" />
</el-col>
<el-col :span="8">
<div>
<h5>Upload a file</h5>
</div>
File name
<el-input v-model="customFileName" size="mini" placeholder="Custom file name (optional)"/>
<input ref="fileUpload" type="file" accept="image/*" >
<div class="or">
or
</div>
<div>
<h5>Enter a URL</h5>
</div>
<el-input v-model="imageUploadURL" placeholder="Image URL" />
<small>
(If both are filled, the file is used)
</small>
</el-col>
<el-col :span="4" class="new-emoji-col">
<el-button :disabled="shortcode.trim() == ''" @click="upload">Upload</el-button>
</el-col>
</el-row>
</div>
</template>
<style>
.new-emoji-col {
margin-top: 8em;
}
.or {
margin: 1em;
}
</style>
<script>
export default {
props: {
packName: {
type: String,
required: true
}
},
data() {
return {
shortcode: '',
imageUploadURL: '',
customFileName: ''
}
},
methods: {
upload() {
let file = null
if (this.$refs.fileUpload.files.length > 0) {
file = this.$refs.fileUpload.files[0]
} else if (this.imageUploadURL.trim() !== '') {
file = this.imageUploadURL
}
if (file !== null) {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'add',
packName: this.packName,
shortcode: this.shortcode,
file: file,
fileName: this.customFileName
}).then(() => {
this.shortcode = ''
this.imageUploadURL = ''
this.$store.dispatch('ReloadEmoji')
})
}
}
}
}
</script>

View file

@ -1,27 +0,0 @@
<template>
<el-row :gutter="20" class="prop-row">
<el-col :span="4">
<b>{{ name }}</b>
</el-col>
<el-col :span="10">
<slot/>
</el-col>
</el-row>
</template>
<style>
.prop-row {
margin-bottom: 1em;
}
</style>
<script>
export default {
props: {
name: {
type: String,
required: true
}
}
}
</script>

View file

@ -1,151 +0,0 @@
<template>
<el-container class="emoji-packs-container">
<el-header>
<h1>
Emoji packs
</h1>
</el-header>
<el-row class="local-packs-actions">
<el-button type="primary" @click="reloadEmoji">
Reload emoji
</el-button>
<el-tooltip effects="dark" content="Importing from the filesystem will scan the directories and import those without pack.json but with emoji.txt or without neither" placement="bottom">
<el-button type="success" @click="importFromFS">
Import packs from the server filesystem
</el-button>
</el-tooltip>
</el-row>
<el-tabs v-model="activeName">
<el-tab-pane label="Local packs" name="local">
<div>
Local packs can be viewed and downloaded for backup here.
</div>
<div class="local-packs-actions">
<el-popover
v-model="createNewPackVisible"
placement="bottom"
trigger="click">
<el-input v-model="newPackName" placeholder="Name" />
<el-button
:disabled="newPackName.trim() === ''"
class="create-pack-button"
type="success"
@click="createLocalPack" >
Create
</el-button>
<el-button slot="reference" type="success">
Create a new local pack
</el-button>
</el-popover>
<el-button type="primary" @click="refreshLocalPacks">
Refresh local packs
</el-button>
</div>
<div v-for="(pack, name) in $store.state.emoji_packs.localPacks" :key="name">
<emoji-pack :name="name" :pack="pack" :host="$store.getters.authHost" :is-local="true" />
<el-divider />
</div>
</el-tab-pane>
<el-tab-pane label="Remote packs" name="remote">
<el-input
v-model="remoteInstanceAddress"
class="remote-instance-input"
placeholder="Remote instance address" />
<el-button type="primary" @click="refreshRemotePacks">
Refresh remote packs
</el-button>
<div v-for="(pack, name) in $store.state.emoji_packs.remotePacks" :key="name">
<emoji-pack :name="name" :pack="pack" :host="remoteInstanceAddress" :is-local="false" />
<el-divider />
</div>
</el-tab-pane>
</el-tabs>
</el-container>
</template>
<style>
.emoji-packs-container {
margin: 22px 0 0 15px;
}
.local-packs-actions {
margin-top: 1em;
margin-bottom: 1em;
}
.remote-instance-input {
max-width: 10%;
}
.create-pack-button {
margin-top: 1em;
}
</style>
<script>
import EmojiPack from './components/EmojiPack'
export default {
components: { EmojiPack },
data() {
return {
activeName: 'local',
remoteInstanceAddress: '',
downloadFromState: null,
newPackName: '',
createNewPackVisible: false
}
},
mounted() {
this.refreshLocalPacks()
},
methods: {
createLocalPack() {
this.createNewPackVisible = false
this.$store.dispatch('CreatePack', { name: this.newPackName })
.then(() => {
this.newPackName = ''
this.$store.dispatch('SetLocalEmojiPacks')
this.$store.dispatch('ReloadEmoji')
})
},
refreshLocalPacks() {
this.$store.dispatch('SetLocalEmojiPacks')
},
refreshRemotePacks() {
this.$store.dispatch('SetRemoteEmojiPacks', { remoteInstance: this.remoteInstanceAddress })
},
reloadEmoji() {
this.$store.dispatch('ReloadEmoji')
},
importFromFS() {
this.$store.dispatch('ImportFromFS')
.then(() => {
this.$store.dispatch('SetLocalEmojiPacks')
this.$store.dispatch('ReloadEmoji')
})
}
}
}
</script>

View file

@ -0,0 +1,257 @@
<template>
<el-collapse-item :title="name" :name="name" class="has-background">
<el-form v-if="isLocal" label-width="120px" label-position="left" size="small" class="emoji-pack-metadata">
<el-form-item :label="$t('settings.sharePack')">
<el-switch v-model="share" />
</el-form-item>
<el-form-item :label="$t('settings.homepage')">
<el-input v-model="homepage" />
</el-form-item>
<el-form-item :label="$t('settings.description')">
<el-input v-model="description" type="textarea" />
</el-form-item>
<el-form-item :label="$t('settings.license')">
<el-input v-model="license" />
</el-form-item>
<el-form-item :label="$t('settings.fallbackSrc')">
<el-input v-model="fallbackSrc" />
</el-form-item>
<el-form-item
v-if="fallbackSrc && fallbackSrc.trim() !== ''"
:label="$t('settings.fallbackSrcSha')">
{{ pack.pack["fallback-src-sha256"] }}
</el-form-item>
<el-form-item class="save-pack-button">
<el-button type="primary" @click="savePackMetadata">{{ $t('settings.savePackMetadata') }}</el-button>
<el-button @click="deletePack">{{ $t('settings.deletePack') }}</el-button>
</el-form-item>
<el-form-item>
<el-link
v-if="pack.pack['can-download']"
:href="`//${host}/api/pleroma/emoji/packs/${name}/download_shared`"
:underline="false"
type="primary"
target="_blank">
<el-button class="download-archive">{{ $t('settings.downloadPackArchive') }}</el-button>
</el-link>
</el-form-item>
</el-form>
<el-form v-if="!isLocal" label-width="120px" label-position="left" size="small" class="emoji-pack-metadata">
<el-form-item :label="$t('settings.sharePack')">
<el-switch v-model="share" disabled />
</el-form-item>
<el-form-item v-if="homepage" :label="$t('settings.homepage')">
<span>{{ homepage }}</span>
</el-form-item>
<el-form-item v-if="description" :label="$t('settings.description')">
<span>{{ description }}</span>
</el-form-item>
<el-form-item v-if="license" :label="$t('settings.license')">
<span>{{ license }}</span>
</el-form-item>
<el-form-item v-if="fallbackSrc" :label="$t('settings.fallbackSrc')">
<span>{{ fallbackSrc }}</span>
</el-form-item>
<el-form-item
v-if="fallbackSrc && fallbackSrc.trim() !== ''"
:label="$t('settings.fallbackSrcSha')">
{{ pack.pack["fallback-src-sha256"] }}
</el-form-item>
<el-form-item>
<el-link
v-if="pack.pack['can-download']"
:href="`//${host}/api/pleroma/emoji/packs/${name}/download_shared`"
:underline="false"
type="primary"
target="_blank">
<el-button class="download-archive">{{ $t('settings.downloadPackArchive') }}</el-button>
</el-link>
</el-form-item>
</el-form>
<el-collapse v-model="showPackContent" class="contents-collapse">
<el-collapse-item v-if="isLocal" :title="$t('settings.addNewEmoji')" name="addEmoji" class="no-background">
<new-emoji-uploader :pack-name="name"/>
</el-collapse-item>
<el-collapse-item v-if="Object.keys(pack.files).length > 0" :title="$t('settings.manageEmoji')" name="manageEmoji" class="no-background">
<single-emoji-editor
v-for="(file, ename) in pack.files"
:key="ename"
:host="host"
:pack-name="name"
:name="ename"
:file="file"
:is-local="isLocal" />
</el-collapse-item>
<el-collapse-item v-if="!isLocal" :title="$t('settings.downloadPack')" name="downloadPack" class="no-background">
<p>
{{ $t('settings.thisWillDownload') }} "{{ name }}" {{ $t('settings.downloadToCurrentInstance') }}
"{{ downloadSharedAs.trim() === '' ? name : downloadSharedAs }}" ({{ $t('settings.canBeChanged') }}).
{{ $t('settings.willBeUsable') }}.
</p>
<div class="download-shared-pack">
<el-input v-model="downloadSharedAs" :placeholder="$t('settings.downloadAsOptional')"/>
<el-button type="primary" class="download-shared-pack-button" @click="downloadFromInstance">
{{ $t('settings.downloadSharedPack') }}
</el-button>
</div>
</el-collapse-item>
</el-collapse>
</el-collapse-item>
</template>
<script>
import SingleEmojiEditor from './SingleEmojiEditor.vue'
import NewEmojiUploader from './NewEmojiUploader.vue'
export default {
components: { SingleEmojiEditor, NewEmojiUploader },
props: {
name: {
type: String,
required: true
},
pack: {
type: Object,
required: true
},
host: {
type: String,
required: true
},
isLocal: {
type: Boolean,
required: true
}
},
data() {
return {
showPackContent: [],
downloadSharedAs: ''
}
},
computed: {
share: {
get() { return this.pack.pack['share-files'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'share-files', value }
)
}
},
homepage: {
get() { return this.pack.pack['homepage'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'homepage', value }
)
}
},
description: {
get() { return this.pack.pack['description'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'description', value }
)
}
},
license: {
get() { return this.pack.pack['license'] },
set(value) {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'license', value }
)
}
},
fallbackSrc: {
get() { return this.pack.pack['fallback-src'] },
set(value) {
if (value.trim() !== '') {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src', value }
)
} else {
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src', value: null }
)
this.$store.dispatch(
'UpdateLocalPackVal',
{ name: this.name, key: 'fallback-src-sha256', value: null }
)
}
}
}
},
methods: {
downloadFromInstance() {
this.$store.dispatch(
'DownloadFrom',
{ instanceAddress: this.host, packName: this.name, as: this.downloadSharedAs }
).then(() => this.$store.dispatch('ReloadEmoji'))
.then(() => this.$store.dispatch('SetLocalEmojiPacks'))
},
deletePack() {
this.$confirm('This will delete the pack, are you sure?', 'Warning', {
confirmButtonText: 'Yes, delete the pack',
cancelButtonText: 'No, leave it be',
type: 'warning'
}).then(() => {
this.$store.dispatch('DeletePack', { name: this.name })
.then(() => this.$store.dispatch('ReloadEmoji'))
.then(() => this.$store.dispatch('SetLocalEmojiPacks'))
}).catch(() => {})
},
savePackMetadata() {
this.$store.dispatch('SavePackMetadata', { packName: this.name })
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.download-archive {
width: 250px
}
.download-shared-pack {
display: flex;
margin-bottom: 10px;
}
.download-shared-pack-button {
margin-left: 10px;
}
.el-collapse-item__content {
padding-bottom: 0;
}
.el-collapse-item__header {
height: 36px;
font-size: 14px;
font-weight: 700;
color: #606266;
}
.emoji-pack-card {
margin-top: 5px;
}
.emoji-pack-metadata {
.el-form-item {
margin-bottom: 10px;
}
}
.has-background .el-collapse-item__header {
background: #f6f6f6;
}
.no-background .el-collapse-item__header {
background: white;
}
.save-pack-button {
margin-bottom: 5px
}
</style>

View file

@ -0,0 +1,90 @@
<template>
<el-form label-width="130px" label-position="left" size="small">
<el-form-item :label="$t('settings.shortcode')">
<el-input v-model="shortcode" :placeholder="$t('settings.required')"/>
</el-form-item>
<el-form-item :label="$t('settings.customFilename')">
<el-input v-model="customFileName" :placeholder="$t('settings.optional')"/>
</el-form-item>
<el-form-item :label="$t('settings.uploadFile')">
<div class="upload-file-url">
<el-input v-model="imageUploadURL" :placeholder="$t('settings.url')"/>
<el-button :disabled="shortcodePresent" type="primary" class="upload-button" @click="uploadEmoji">{{ $t('settings.upload') }}</el-button>
</div>
<div class="upload-container">
<p class="text">or</p>
<el-upload
:http-request="uploadEmoji"
:multiple="false"
:show-file-list="false"
action="add">
<el-button :disabled="shortcodePresent" type="primary">{{ $t('settings.clickToUpload') }}</el-button>
</el-upload>
</div>
</el-form-item>
</el-form>
</template>
<style>
.add-new-emoji {
height: 36px;
font-size: 14px;
font-weight: 700;
color: #606266;
}
.text {
line-height: 20px;
margin-right: 15px
}
.upload-container {
display: flex;
align-items: baseline;
}
.upload-button {
margin-left: 10px;
}
.upload-file-url {
display: flex;
justify-content: space-between
}
</style>
<script>
export default {
props: {
packName: {
type: String,
required: true
}
},
data() {
return {
shortcode: '',
imageUploadURL: '',
customFileName: ''
}
},
computed: {
shortcodePresent() {
return this.shortcode.trim() === ''
}
},
methods: {
uploadEmoji({ file }) {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'add',
packName: this.packName,
shortcode: this.shortcode,
file: file || this.imageUploadURL,
fileName: this.customFileName
}).then(() => {
this.shortcode = ''
this.imageUploadURL = ''
this.customFileName = ''
this.$store.dispatch('ReloadEmoji')
})
}
}
}
</script>

View file

@ -1,71 +1,50 @@
<template>
<el-row :gutter="20">
<el-col :span="4">
<el-input v-if="isLocal" v-model="modifyingName" placeholder="Name/Shortcode" />
<el-input v-else :value="modifyingName" placeholder="Name/Shortcode" />
</el-col>
<el-col :span="6">
<el-input v-if="isLocal" v-model="modifyingFile" placeholder="File"/>
<el-input v-else :value="modifyingFile" placeholder="File"/>
</el-col>
<div>
<div v-if="isLocal" class="emoji-container">
<img
:src="addressOfEmojiInPack(host, packName, file)"
class="emoji-preview-img">
<el-input v-model="emojiName" :placeholder="$t('settings.shortcode')" class="emoji-info"/>
<el-input v-model="emojiFile" :placeholder="$t('settings.file')" class="emoji-info"/>
<div class="emoji-buttons">
<el-button type="primary" class="emoji-button" @click="update">{{ $t('settings.update') }}</el-button>
<el-button class="emoji-button" @click="remove">{{ $t('settings.remove') }}</el-button>
</div>
</div>
<el-col v-if="isLocal" :span="2">
<el-button type="primary" @click="update">Update</el-button>
</el-col>
<el-col v-if="isLocal" :span="2">
<el-button type="danger" @click="remove">Remove</el-button>
</el-col>
<el-col v-if="!isLocal" :span="4">
<el-popover v-model="copyToLocalVisible" placement="bottom">
<p>Select the local pack to copy to</p>
<el-select v-model="copyToLocalPackName" placeholder="Local pack">
<div v-if="!isLocal" class="emoji-container">
<img
:src="addressOfEmojiInPack(host, packName, file)"
class="emoji-preview-img">
<el-input :value="emojiName" :placeholder="$t('settings.shortcode')" class="emoji-info"/>
<el-input :value="emojiFile" :placeholder="$t('settings.file')" class="emoji-info"/>
<el-popover v-model="copyPopoverVisible" placement="left-start" popper-class="copy-popover">
<p>{{ $t('settings.selectLocalPack') }}</p>
<el-select v-model="copyToLocalPackName" :placeholder="$t('settings.localPack')">
<el-option
v-for="(_pack, name) in $store.state.emoji_packs.localPacks"
v-for="(_pack, name) in localPacks"
:key="name"
:label="name"
:value="name" />
</el-select>
<p>Specify a custom shortcode (leave empty to use the same shortcode)</p>
<el-input v-model="copyToShortcode" placeholder="Shortcode (optional)" />
<p>Specify a custom filename (leavy empty to use the same filename)</p>
<el-input v-model="copyToFilename" placeholder="Filename (optional)" />
<p>{{ $t('settings.specifyShortcode') }}</p>
<el-input v-model="copyToShortcode" :placeholder="$t('settings.leaveEmptyShortcode')"/>
<p>{{ $t('settings.specifyFilename') }}</p>
<el-input v-model="copyToFilename" :placeholder="$t('settings.leaveEmptyFilename')"/>
<el-button
:disabled="!copyToLocalPackName"
type="success"
type="primary"
class="copy-to-local-button"
@click="copyToLocal">Copy</el-button>
<el-button slot="reference" type="primary">Copy to local pack...</el-button>
@click="copyToLocal">{{ $t('settings.copy') }}</el-button>
<el-button slot="reference" type="primary" class="emoji-button">{{ $t('settings.copyToLocalPack') }}</el-button>
</el-popover>
</el-col>
<el-col :span="2">
<img
:src="addressOfEmojiInPack(host, packName, file)"
class="emoji-preview-img">
</el-col>
</el-row>
</div>
</div>
</template>
<style>
.emoji-preview-img {
max-width: 5em;
}
.copy-to-local-button {
margin-top: 2em;
float: right;
}
</style>
<script>
import { addressOfEmojiInPack } from '@/api/emoji_packs'
import { addressOfEmojiInPack } from '@/api/emojiPacks'
export default {
props: {
host: {
@ -89,33 +68,33 @@ export default {
required: true
}
},
data() {
return {
newName: null,
newFile: null,
copyToLocalPackName: null,
copyToLocalVisible: false,
copyPopoverVisible: false,
copyToShortcode: '',
copyToFilename: ''
}
},
computed: {
modifyingName: {
emojiName: {
get() {
// Return a modified name if it was actually modified, otherwise return the old name
// Return a modified name if it was modified, otherwise return the old name
return this.newName !== null ? this.newName : this.name
},
set(val) { this.newName = val }
},
modifyingFile: {
emojiFile: {
get() {
// Return a modified name if it was actually modified, otherwise return the old name
// Return a modified name if it was modified, otherwise return the old name
return this.newFile !== null ? this.newFile : this.file
},
set(val) { this.newFile = val }
},
localPacks() {
return this.$store.state.emojiPacks.localPacks
}
},
methods: {
@ -124,8 +103,8 @@ export default {
action: 'update',
packName: this.packName,
oldName: this.name,
newName: this.modifyingName,
newFilename: this.modifyingFile
newName: this.emojiName,
newFilename: this.emojiFile
}).then(() => {
this.newName = null
this.newFile = null
@ -151,7 +130,6 @@ export default {
})
})
},
copyToLocal() {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'add',
@ -168,8 +146,35 @@ export default {
this.$store.dispatch('ReloadEmoji')
})
},
addressOfEmojiInPack
}
}
</script>
<style>
.copy-popover {
width: 330px
}
.emoji-button {
margin-left: 10px
}
.emoji-buttons {
min-width: 210px
}
.emoji-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.emoji-preview-img {
max-width: 5em;
}
.emoji-info {
margin-left: 10px
}
.copy-to-local-button {
margin-top: 12px;
float: right;
}
</style>

View file

@ -0,0 +1,132 @@
<template>
<div>
<div class="button-container">
<el-button type="primary" @click="reloadEmoji">{{ $t('settings.reloadEmoji') }}</el-button>
<el-tooltip :content="$t('settings.importEmojiTooltip')" effects="dark" placement="bottom">
<el-button type="primary" @click="importFromFS">
{{ $t('settings.importPacks') }}
</el-button>
</el-tooltip>
</div>
<div class="line"/>
<el-form :label-width="labelWidth">
<el-form-item :label="$t('settings.localPacks')">
<el-button type="primary" @click="refreshLocalPacks">{{ $t('settings.refreshLocalPacks') }}</el-button>
</el-form-item>
<el-form-item :label="$t('settings.createLocalPack')">
<div class="create-pack">
<el-input v-model="newPackName" :placeholder="$t('users.name')" />
<el-button
:disabled="newPackName.trim() === ''"
class="create-pack-button"
@click="createLocalPack">
{{ $t('users.create') }}
</el-button>
</div>
</el-form-item>
<el-form-item v-if="Object.keys(localPacks).length > 0" :label="$t('settings.packs')">
<el-collapse v-for="(pack, name) in localPacks" :key="name" v-model="activeLocalPack">
<emoji-pack :name="name" :pack="pack" :host="$store.getters.authHost" :is-local="true" />
</el-collapse>
</el-form-item>
<div class="line"/>
<el-form-item :label="$t('settings.remotePacks')">
<div class="create-pack">
<el-input
v-model="remoteInstanceAddress"
:placeholder="$t('settings.remoteInstanceAddress')" />
<el-button
:disabled="remoteInstanceAddress.trim() === ''"
class="create-pack-button"
@click="refreshRemotePacks">
{{ $t('settings.refreshRemote') }}
</el-button>
</div>
</el-form-item>
<el-form-item v-if="Object.keys(remotePacks).length > 0" :label="$t('settings.packs')">
<el-collapse v-for="(pack, name) in remotePacks" :key="name" v-model="activeRemotePack">
<emoji-pack :name="name" :pack="pack" :host="$store.getters.authHost" :is-local="false" />
</el-collapse>
</el-form-item>
</el-form>
</div>
</template>
<script>
import EmojiPack from './components/EmojiPack'
export default {
components: { EmojiPack },
data() {
return {
remoteInstanceAddress: '',
newPackName: '',
activeLocalPack: [],
activeRemotePack: []
}
},
computed: {
isMobile() {
return this.$store.state.app.device === 'mobile'
},
labelWidth() {
return this.isMobile ? '100px' : '210px'
},
localPacks() {
return this.$store.state.emojiPacks.localPacks
},
remotePacks() {
return this.$store.state.emojiPacks.remotePacks
}
},
mounted() {
this.refreshLocalPacks()
},
methods: {
createLocalPack() {
this.$store.dispatch('CreatePack', { name: this.newPackName })
.then(() => {
this.newPackName = ''
this.$store.dispatch('SetLocalEmojiPacks')
this.$store.dispatch('ReloadEmoji')
})
},
refreshLocalPacks() {
this.$store.dispatch('SetLocalEmojiPacks')
},
refreshRemotePacks() {
this.$store.dispatch('SetRemoteEmojiPacks', { remoteInstance: this.remoteInstanceAddress })
},
reloadEmoji() {
this.$store.dispatch('ReloadEmoji')
},
importFromFS() {
this.$store.dispatch('ImportFromFS')
.then(() => {
this.$store.dispatch('SetLocalEmojiPacks')
this.$store.dispatch('ReloadEmoji')
})
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
.button-container {
margin: 0 0 22px 20px;
}
.create-pack {
display: flex;
justify-content: space-between
}
.create-pack-button {
margin-left: 10px;
}
.line {
width: 100%;
height: 0;
border: 1px solid #eee;
margin-bottom: 22px;
}
</style>

View file

@ -192,17 +192,8 @@ export default {
async inviteUserViaEmail() {
this.$refs['inviteUserForm'].validate(async(valid) => {
if (valid) {
try {
await this.$store.dispatch('InviteUserViaEmail', this.$data.inviteUserForm)
} catch (_e) {
return
} finally {
this.closeDialogWindow()
}
this.$message({
type: 'success',
message: this.$t('invites.emailSent')
})
await this.$store.dispatch('InviteUserViaEmail', this.$data.inviteUserForm)
this.closeDialogWindow()
} else {
this.$message({
type: 'error',

View file

@ -0,0 +1,151 @@
<template>
<div v-if="!loading" class="moderation-log-container">
<h1>{{ $t('moderationLog.moderationLog') }}</h1>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="9">
<el-select
v-model="user"
class="user-select"
clearable
placeholder="Filter by admin/moderator"
@change="fetchLogWithFilters">
<el-option-group
v-for="group in users"
:key="group.label"
:label="group.label">
<el-option
v-for="item in group.options"
:key="item.id"
:label="item.nickname"
:value="item.id" />
</el-option-group>
</el-select>
</el-col>
<el-col :span="6" class="search-container">
<el-input
v-model="search"
placeholder="Search logs"
clearable
@input="handleDebounceSearchInput" />
</el-col>
</el-row>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="9" class="date-container">
<el-date-picker
:default-time="['00:00:00', '23:59:59']"
v-model="dateRange"
type="daterange"
start-placeholder="Start date"
end-placeholder="End date"
unlink-panels
@change="fetchLogWithFilters" />
</el-col>
</el-row>
<el-timeline>
<el-timeline-item
v-for="(logEntry, index) in log"
:key="index"
:timestamp="normalizeTimestamp(logEntry.time)">
{{ logEntry.message }}
</el-timeline-item>
</el-timeline>
<div class="pagination">
<el-pagination
:current-page.sync="currentPage"
:hide-on-single-page="true"
:page-size="50"
:total="total"
layout="prev, pager, next"
@current-change="fetchLogWithFilters" />
</div>
</div>
</template>
<script>
import moment from 'moment'
import _ from 'lodash'
import debounce from 'lodash.debounce'
export default {
data() {
return {
dateRange: '',
search: '',
user: '',
currentPage: 1
}
},
computed: {
loading() {
return this.$store.state.moderationLog.logLoading &&
this.$store.state.moderationLog.adminsLoading
},
log() {
return this.$store.state.moderationLog.fetchedLog
},
total() {
return this.$store.state.moderationLog.logItemsCount
},
users() {
return [
{
label: 'Admins',
options: this.$store.state.moderationLog.admins.users
},
{
label: 'Moderators',
options: this.$store.state.moderationLog.moderators.users
}
]
}
},
created() {
this.handleDebounceSearchInput = debounce((query) => {
this.fetchLogWithFilters()
}, 500)
},
mounted() {
this.$store.dispatch('FetchModerationLog')
this.$store.dispatch('FetchAdmins')
},
methods: {
normalizeTimestamp(timestamp) {
return moment(timestamp * 1000).format('YYYY-MM-DD HH:mm')
},
fetchLogWithFilters() {
const filters = _.omitBy({
start_date: this.dateRange ? this.dateRange[0].toISOString() : null,
end_date: this.dateRange ? this.dateRange[1].toISOString() : null,
user_id: this.user,
search: this.search,
page: this.currentPage
}, val => val === '' || val === null)
this.$store.dispatch('FetchModerationLog', filters)
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss' scoped>
.moderation-log-container {
margin: 0 15px;
}
h1 {
margin: 22px 0 20px 0;
}
.el-timeline {
margin: 25px 45px 0 0;
padding: 0px;
}
.user-select {
margin: 0 0 20px;
width: 350px;
}
.search-container {
text-align: right;
}
.pagination {
text-align: center;
}
</style>

View file

@ -1,62 +1,67 @@
<template>
<el-timeline class="timeline">
<el-card v-for="group in groups" :key="group.id">
<div class="header-container">
<div>
<h3 class="report-title">{{ $t('reports.reportsOn') }} {{ group.account.display_name }}</h3>
<el-timeline-item
v-for="groupedReport in groupedReports"
:key="groupedReport.id"
:timestamp="parseTimestamp(groupedReport.date)"
placement="top"
class="timeline-item-container">
<el-card class="grouped-report">
<div class="header-container">
<div>
<h3 class="report-title">{{ $t('reports.reportsOn') }} {{ groupedReport.account.display_name }}</h3>
</div>
<div>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeAllReports') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeAllReports('resolved', groupedReport.reports)">{{ $t('reports.resolveAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('open', groupedReport.reports)">{{ $t('reports.reopenAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('closed', groupedReport.reports)">{{ $t('reports.closeAll') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<moderate-user-dropdown :account="groupedReport.account"/>
</div>
</div>
<div>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit">{{ $t('reports.changeAllReports') }}<i class="el-icon-arrow-down el-icon--right"/></el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="changeAllReports('resolved', group.reports)">{{ $t('reports.resolveAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('open', group.reports)">{{ $t('reports.reopenAll') }}</el-dropdown-item>
<el-dropdown-item @click.native="changeAllReports('closed', group.reports)">{{ $t('reports.closeAll') }}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<moderate-user-dropdown :account="group.account"/>
</div>
</div>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.account') }}:</span>
<img
:src="group.account.avatar"
alt="avatar"
class="avatar-img">
<a :href="group.account.url" target="_blank">
<span>{{ group.account.acct }}</span>
</a>
</div>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.actors') }}:</span>
<span v-for="actor in group.actors" :key="actor.id">
<a :href="actor.url" target="_blank">
<span>{{ actor.acct }}, </span>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.account') }}:</span>
<img
:src="groupedReport.account.avatar"
alt="avatar"
class="avatar-img">
<a :href="groupedReport.account.url" target="_blank">
<span>{{ groupedReport.account.nickname }}</span>
</a>
</span>
</div>
<div v-if="group.status">
<div class="line"/>
<span class="report-row-key">{{ $t('reports.reportedStatus') }}:</span>
<div v-for="status in group.status" :key="status.id">
<status :status="status" :page="1" class="reported-status"/> <!-- Change page value when pagination is implemented -->
</div>
</div>
<div v-if="group.reports">
<div class="line"/>
<el-collapse>
<el-collapse-item :title="$t('reports.reports')">
<report-card :reports="group.reports"/>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
<div>
<div class="line"/>
<span class="report-row-key">{{ $t('reports.actors') }}:</span>
<span v-for="(actor, index) in groupedReport.actors" :key="actor.id">
<a :href="actor.url" target="_blank">
{{ actor.acct }}<span v-if="index < groupedReport.actors.length - 1">, </span>
</a>
</span>
</div>
<div v-if="groupedReport.status">
<div class="line"/>
<span class="report-row-key">{{ $t('reports.reportedStatus') }}:</span>
<status :status="groupedReport.status" class="reported-status"/>
</div>
<div v-if="groupedReport.reports">
<el-collapse>
<el-collapse-item :title="$t('reports.reports')">
<report-card :reports="groupedReport.reports"/>
</el-collapse-item>
</el-collapse>
</div>
</el-card>
</el-timeline-item>
</el-timeline>
</template>
<script>
import moment from 'moment'
import ModerateUserDropdown from './ModerateUserDropdown'
import ReportCard from './ReportCard'
import Status from '../../status/Status'
@ -65,17 +70,20 @@ export default {
name: 'Report',
components: { ModerateUserDropdown, ReportCard, Status },
props: {
groups: {
groupedReports: {
type: Array,
required: true
}
},
mounted() {
this.$store.dispatch('FetchGroupedReports', 1)
},
methods: {
changeAllReports(reportState, groupOfReports) {
console.log(groupOfReports)
const reportsData = groupOfReports.map(report => {
return { id: report.id, state: reportState }
})
this.$store.dispatch('ChangeReportState', reportsData)
},
parseTimestamp(timestamp) {
return moment(timestamp).format('L HH:mm')
}
}
}

View file

@ -107,12 +107,9 @@ export default {
return this.$store.state.reports.currentPage
}
},
mounted() {
this.$store.dispatch('FetchReports', 1)
},
methods: {
changeReportState(reportState, reportId) {
this.$store.dispatch('ChangeReportState', { reportState, reportId })
changeReportState(state, id) {
this.$store.dispatch('ChangeReportState', [{ state, id }])
},
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)

View file

@ -48,8 +48,8 @@ export default {
capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
changeReportState(reportState, reportId) {
this.$store.dispatch('ChangeReportState', { reportState, reportId })
changeReportState(state, id) {
this.$store.dispatch('ChangeReportState', [{ state, id }])
},
getStateType(state) {
switch (state) {

View file

@ -1,17 +1,21 @@
<template>
<div class="reports-container">
<h1>
<h1 v-if="groupReports">
{{ $t('reports.groupedReports') }}
<span class="report-count">({{ normalizedReportsCount }})</span>
</h1>
<h1 v-else>
{{ $t('reports.reports') }}
<span class="report-count">({{ normalizedReportsCount }})</span>
</h1>
<div class="filter-container">
<reports-filter/>
<reports-filter v-if="!groupReports"/>
<el-checkbox v-model="groupReports" class="group-reports-checkbox">
Group reports by statuses
</el-checkbox>
</div>
<div class="block">
<grouped-report v-loading="loading" v-if="groupReports" :groups="groups"/>
<grouped-report v-loading="loading" v-if="groupReports" :grouped-reports="groupedReports"/>
<report v-loading="loading" v-else :reports="reports"/>
<div v-if="reports.length === 0" class="no-reports-message">
<p>There are no reports to display</p>
@ -29,166 +33,8 @@ import ReportsFilter from './components/ReportsFilter'
export default {
components: { GroupedReport, Report, ReportsFilter },
computed: {
groups() {
return [{
date: '19-07-2019',
account: {
'acct': 'user',
'avatar': 'https://pleroma.example.org/images/avi.png',
'avatar_static': 'https://pleroma.example.org/images/avi.png',
'bot': false,
'created_at': '2019-04-23T17:32:04.000Z',
'display_name': 'User',
'emojis': [],
'fields': [],
'followers_count': 1,
'following_count': 1,
'header': 'https://pleroma.example.org/images/banner.png',
'header_static': 'https://pleroma.example.org/images/banner.png',
'id': '9i6dAJqSGSKMzLG2Lo',
'locked': false,
'note': '',
'pleroma': {
'confirmation_pending': false,
'hide_favorites': true,
'hide_followers': false,
'hide_follows': false,
'is_admin': false,
'is_moderator': false,
'relationship': {},
'tags': []
},
'source': {
'note': '',
'pleroma': {},
'sensitive': false
},
'tags': ['force_unlisted'],
'statuses_count': 3,
'url': 'https://pleroma.example.org/users/user',
'username': 'user'
},
actors: [
{ 'acct': 'lain', 'url': 'https://pleroma.example.org/users/lain' },
{ 'acct': 'linafilippova1', 'url': 'https://pleroma.example.org/users/linafilippova1' }
],
reports: [
{
'actor': {
'acct': 'lain',
'avatar': 'https://pleroma.example.org/images/avi.png',
'display_name': 'Roger Braun',
'url': 'https://pleroma.example.org/users/lain'
},
'content': 'Please delete it',
'created_at': '2019-04-29T19:48:15.000Z',
'id': '9iJGOv1j8hxuw19bcm',
'state': 'open'
},
{ 'actor': {
'acct': 'linafilippova1',
'avatar': 'https://pleroma.example.org/images/avi.png',
'display_name': 'Lina Filippova',
'url': 'https://pleroma.example.org/users/linafilippova1'
},
'content': 'This is an assault',
'created_at': '2019-03-01T19:48:15.000Z',
'id': '9iJGOv1alksjdf3r',
'state': 'resolve'
}
],
status: [{
'account': {
'acct': 'user',
'avatar': 'https://pleroma.example.org/images/avi.png',
'avatar_static': 'https://pleroma.example.org/images/avi.png',
'bot': false,
'created_at': '2019-04-23T17:32:04.000Z',
'display_name': 'User',
'emojis': [],
'fields': [],
'followers_count': 1,
'following_count': 1,
'header': 'https://pleroma.example.org/images/banner.png',
'header_static': 'https://pleroma.example.org/images/banner.png',
'id': '9i6dAJqSGSKMzLG2Lo',
'locked': false,
'note': '',
'pleroma': {
'confirmation_pending': false,
'hide_favorites': true,
'hide_followers': false,
'hide_follows': false,
'is_admin': false,
'is_moderator': false,
'relationship': {},
'tags': []
},
'source': {
'note': '',
'pleroma': {},
'sensitive': false
},
'tags': ['force_unlisted'],
'statuses_count': 3,
'url': 'https://pleroma.example.org/users/user',
'username': 'user'
},
'application': {
'name': 'Web',
'website': null
},
'bookmarked': false,
'card': null,
'content': '<span class=\"h-card\"><a data-user=\"9hEkA5JsvAdlSrocam\" class=\"u-url mention\" href=\"https://pleroma.example.org/users/lain\">@<span>lain</span></a></span> click on my link <a href=\"https://www.google.com/\">https://www.google.com/</a>',
'created_at': '2019-04-23T19:15:47.000Z',
'emojis': [],
'favourited': false,
'favourites_count': 0,
'id': '9i6mQ9uVrrOmOime8m',
'in_reply_to_account_id': null,
'in_reply_to_id': null,
'language': null,
'media_attachments': [],
'mentions': [
{
'acct': 'lain',
'id': '9hEkA5JsvAdlSrocam',
'url': 'https://pleroma.example.org/users/lain',
'username': 'lain'
},
{
'acct': 'user',
'id': '9i6dAJqSGSKMzLG2Lo',
'url': 'https://pleroma.example.org/users/user',
'username': 'user'
}
],
'muted': false,
'pinned': false,
'pleroma': {
'content': {
'text/plain': '@lain click on my link https://www.google.com/'
},
'conversation_id': 28,
'in_reply_to_account_acct': null,
'local': true,
'spoiler_text': {
'text/plain': ''
}
},
'reblog': null,
'reblogged': false,
'reblogs_count': 0,
'replies_count': 0,
'sensitive': false,
'spoiler_text': '',
'tags': [],
'uri': 'https://pleroma.example.org/objects/8717b90f-8e09-4b58-97b0-e3305472b396',
'url': 'https://pleroma.example.org/notice/9i6mQ9uVrrOmOime8m',
'visibility': 'direct'
}]
}]
groupedReports() {
return this.$store.state.reports.fetchedGroupedReports
},
groupReports: {
get() {
@ -202,12 +48,18 @@ export default {
return this.$store.state.reports.loading
},
normalizedReportsCount() {
return numeral(this.$store.state.reports.totalReportsCount).format('0a')
return this.groupReports
? numeral(this.$store.state.reports.fetchedGroupedReports.length).format('0a')
: numeral(this.$store.state.reports.totalReportsCount).format('0a')
},
reports() {
return this.$store.state.reports.fetchedReports
}
},
mounted() {
this.$store.dispatch('FetchReports', 1)
this.$store.dispatch('FetchGroupedReports')
},
methods: {
toggleReportsGrouping() {
this.$store.dispatch('ToggleReportsGrouping')

View file

@ -37,7 +37,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
export default {
name: 'ActivityPub',
@ -57,16 +56,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -232,7 +232,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
export default {
name: 'Authentication',
@ -259,16 +258,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -52,7 +52,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
export default {
name: 'AutoLinker',
@ -107,16 +106,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -33,7 +33,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
export default {
name: 'Captcha',
@ -53,16 +52,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -135,7 +135,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -157,16 +156,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -172,7 +172,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
import { options } from './options'
import AceEditor from 'vue2-ace-editor'
import 'brace/mode/elixir'
@ -246,16 +245,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -55,7 +55,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -82,16 +81,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -318,7 +318,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
import { options } from './options'
export default {
@ -419,16 +418,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -23,7 +23,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -43,16 +42,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -129,7 +129,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -161,16 +160,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -337,7 +337,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
import { options } from './options'
@ -396,16 +395,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -51,7 +51,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -72,16 +71,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -173,7 +173,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
import { options } from './options'
export default {
@ -212,16 +211,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -145,7 +145,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
import { options } from './options'
export default {
@ -257,16 +256,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -202,7 +202,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
import { options } from './options'
import AceEditor from 'vue2-ace-editor'
@ -249,16 +248,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -75,7 +75,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
import { options } from './options'
@ -126,16 +125,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -52,7 +52,6 @@
<script>
import { mapGetters } from 'vuex'
import i18n from '@/lang'
export default {
name: 'Metadata',
@ -72,16 +71,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -30,7 +30,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -65,16 +64,8 @@ export default {
}, {})
this.updateSetting(updatedValue, 'types', 'value')
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
},
parseMimeTypes(value, inputType, index) {
const updatedValue = this.mimeTypes.reduce((acc, el, i) => {

View file

@ -223,7 +223,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -379,16 +378,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -0,0 +1,67 @@
<template>
<div v-if="!loading">
<el-row :gutter="5">
<el-col :span="8">
<el-input v-model="newRelay" :placeholder="$t('settings.followRelay')" @keyup.enter.native="followRelay"/>
</el-col>
<el-col :span="8">
<el-button type="primary" @click.native="followRelay">{{ $t('settings.follow') }}</el-button>
</el-col>
</el-row>
<el-table :data="relaysTable">
<el-table-column
:label="$t('settings.instanceUrl')"
prop="instance"/>
<el-table-column fixed="right" width="120">
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click.native="deleteRelay(scope.row.instance)">
{{ $t('table.delete') }}
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'Relays',
data() {
return {
newRelay: ''
}
},
computed: {
relays() {
return this.$store.state.relays.fetchedRelays
},
relaysTable() {
return this.relays.map(relay => {
return { instance: relay }
})
},
loading() {
return this.$store.state.relays.loading
}
},
mounted() {
this.$store.dispatch('FetchRelays')
},
methods: {
followRelay() {
this.$store.dispatch('AddRelay', this.newRelay)
},
deleteRelay(relay) {
this.$store.dispatch('DeleteRelay', relay)
}
}
}
</script>
<style rel='stylesheet/scss' lang='scss'>
@import '../styles/main';
@include settings
</style>

View file

@ -144,7 +144,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
import { options } from './options'
@ -207,16 +206,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -21,7 +21,6 @@
</template>
<script>
import i18n from '@/lang'
import { mapGetters } from 'vuex'
export default {
@ -41,16 +40,8 @@ export default {
updateSetting(value, tab, input) {
this.$store.dispatch('UpdateSettings', { tab, data: { [input]: value }})
},
async onSubmit() {
try {
await this.$store.dispatch('SubmitChanges')
} catch (e) {
return
}
this.$message({
type: 'success',
message: i18n.t('settings.success')
})
onSubmit() {
this.$store.dispatch('SubmitChanges')
}
}
}

View file

@ -17,5 +17,6 @@ export { default as Metadata } from './Metadata'
export { default as Mrf } from './MRF'
export { default as Other } from './Other'
export { default as RateLimiters } from './RateLimiters'
export { default as Relays } from './Relays'
export { default as Upload } from './Upload'
export { default as WebPush } from './WebPush'

View file

@ -23,6 +23,9 @@
<el-tab-pane :label="$t('settings.endpoint')">
<endpoint/>
</el-tab-pane>
<el-tab-pane :label="$t('settings.emojiPacks')">
<emoji-packs/>
</el-tab-pane>
<el-tab-pane :label="$t('settings.frontend')">
<frontend/>
</el-tab-pane>
@ -56,6 +59,9 @@
<el-tab-pane :label="$t('settings.rateLimiters')">
<rate-limiters/>
</el-tab-pane>
<el-tab-pane :label="$t('settings.relays')">
<relays/>
</el-tab-pane>
<el-tab-pane :label="$t('settings.upload')">
<upload/>
</el-tab-pane>
@ -70,10 +76,11 @@
</template>
<script>
import { ActivityPub, Authentication, AutoLinker, Captcha, Database, Endpoint, Esshd, Frontend, Gopher, Http, Instance, JobQueue, Logger, Mailer, MediaProxy, Metadata, Mrf, Other, RateLimiters, Upload, WebPush } from './components'
import { ActivityPub, Authentication, AutoLinker, Captcha, Database, Endpoint, Esshd, Frontend, Gopher, Http, Instance, JobQueue, Logger, Mailer, MediaProxy, Metadata, Mrf, Other, RateLimiters, Relays, Upload, WebPush } from './components'
import EmojiPacks from '../emojiPacks/index'
export default {
components: { ActivityPub, Authentication, AutoLinker, Captcha, Database, Endpoint, Esshd, Frontend, Gopher, Http, Instance, JobQueue, Logger, Mailer, MediaProxy, Metadata, Mrf, Other, RateLimiters, Upload, WebPush },
components: { ActivityPub, Authentication, AutoLinker, Captcha, Database, Endpoint, EmojiPacks, Esshd, Frontend, Gopher, Http, Instance, JobQueue, Logger, Mailer, MediaProxy, Metadata, Mrf, Other, RateLimiters, Relays, Upload, WebPush },
computed: {
isMobile() {
return this.$store.state.app.device === 'mobile'

View file

@ -1,64 +1,80 @@
<template>
<el-card class="status-card">
<div slot="header">
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<img :src="status.account.avatar" class="status-avatar-img">
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
<div>
<el-card v-if="!status.deleted" class="status-card">
<div slot="header">
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<img :src="status.account.avatar" class="status-avatar-img">
<h3 class="status-account-name">{{ status.account.display_name }}</h3>
</div>
<a :href="status.account.url" target="_blank" class="account">
@{{ status.account.acct }}
</a>
</div>
<div class="status-actions">
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag>
<el-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button">
{{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="!status.sensitive"
@click.native="changeStatus(status.id, true, status.visibility)">
{{ $t('reports.addSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.sensitive"
@click.native="changeStatus(status.id, false, status.visibility)">
{{ $t('reports.removeSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'public'"
@click.native="changeStatus(status.id, status.sensitive, 'public')">
{{ $t('reports.public') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'private'"
@click.native="changeStatus(status.id, status.sensitive, 'private')">
{{ $t('reports.private') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'unlisted'"
@click.native="changeStatus(status.id, status.sensitive, 'unlisted')">
{{ $t('reports.unlisted') }}
</el-dropdown-item>
<el-dropdown-item
@click.native="deleteStatus(status.id)">
{{ $t('reports.deleteStatus') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<a :href="status.account.url" target="_blank" class="account">
@{{ status.account.acct }}
</a>
</div>
<div class="status-actions">
<el-tag v-if="status.sensitive" type="warning" size="large">{{ $t('reports.sensitive') }}</el-tag>
<el-tag size="large">{{ capitalizeFirstLetter(status.visibility) }}</el-tag>
<el-dropdown trigger="click">
<el-button plain size="small" icon="el-icon-edit" class="status-actions-button">
{{ $t('reports.changeScope') }}<i class="el-icon-arrow-down el-icon--right"/>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item
v-if="!status.sensitive"
@click.native="changeStatus(status.id, true, status.visibility)">
{{ $t('reports.addSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.sensitive"
@click.native="changeStatus(status.id, false, status.visibility)">
{{ $t('reports.removeSensitive') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'public'"
@click.native="changeStatus(status.id, status.sensitive, 'public')">
{{ $t('reports.public') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'private'"
@click.native="changeStatus(status.id, status.sensitive, 'private')">
{{ $t('reports.private') }}
</el-dropdown-item>
<el-dropdown-item
v-if="status.visibility !== 'unlisted'"
@click.native="changeStatus(status.id, status.sensitive, 'unlisted')">
{{ $t('reports.unlisted') }}
</el-dropdown-item>
<el-dropdown-item
@click.native="deleteStatus(status.id)">
{{ $t('reports.deleteStatus') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</div>
<div class="status-body">
<div v-if="status.spoiler_text">
<strong>{{ status.spoiler_text }}</strong>
<el-button v-if="!showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = true">Show more</el-button>
<el-button v-if="showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = false">Show less</el-button>
<div v-if="showHiddenStatus">
<div class="status-body">
<div v-if="status.spoiler_text">
<strong>{{ status.spoiler_text }}</strong>
<el-button v-if="!showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = true">Show more</el-button>
<el-button v-if="showHiddenStatus" size="mini" class="show-more-button" @click="showHiddenStatus = false">Show less</el-button>
<div v-if="showHiddenStatus">
<span class="status-content" v-html="status.content"/>
<div v-if="status.poll" class="poll">
<ul>
<li v-for="(option, index) in status.poll.options" :key="index">
{{ option.title }}
<el-progress :percentage="optionPercent(status.poll, option)" />
</li>
</ul>
</div>
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
<img :src="attachment.preview_url">
</div>
</div>
</div>
<div v-if="!status.spoiler_text">
<span class="status-content" v-html="status.content"/>
<div v-if="status.poll" class="poll">
<ul>
@ -72,27 +88,30 @@
<img :src="attachment.preview_url">
</div>
</div>
<a :href="status.url" target="_blank" class="account">
{{ parseTimestamp(status.created_at) }}
</a>
</div>
<div v-if="!status.spoiler_text">
<span class="status-content" v-html="status.content"/>
<div v-if="status.poll" class="poll">
<ul>
<li v-for="(option, index) in status.poll.options" :key="index">
{{ option.title }}
<el-progress :percentage="optionPercent(status.poll, option)" />
</li>
</ul>
</div>
<div v-for="(attachment, index) in status.media_attachments" :key="index" class="image">
<img :src="attachment.preview_url">
</el-card>
<el-card v-else class="status-card">
<div slot="header">
<div class="status-header">
<div class="status-account-container">
<div class="status-account">
<h4 class="status-deleted">{{ $t('reports.statusDeleted') }}</h4>
</div>
</div>
</div>
</div>
<a :href="status.url" target="_blank" class="account">
<div class="status-body">
<span v-if="status.content" class="status-content" v-html="status.content"/>
<span v-else class="status-without-content">no content</span>
</div>
<a v-if="status.created_at" :href="status.url" target="_blank" class="account">
{{ parseTimestamp(status.created_at) }}
</a>
</div>
</el-card>
</el-card>
</div>
</template>
<script>
@ -166,47 +185,56 @@ export default {
</script>
<style rel='stylesheet/scss' lang='scss'>
.account {
text-decoration: underline;
line-height: 26px;
font-size: 13px;
}
.image {
width: 20%;
img {
width: 100%;
}
}
.show-more-button {
margin-left: 5px;
}
.status-account {
display: flex;
align-items: center;
}
.status-avatar-img {
width: 15px;
height: 15px;
margin-right: 5px;
}
.status-account-name {
margin: 0;
height: 22px;
}
.status-body {
display: flex;
flex-direction: column;
}
.status-content {
font-size: 15px;
line-height: 26px;
}
.status-card {
margin-bottom: 15px;
}
.status-header {
display: flex;
justify-content: space-between;
.account {
text-decoration: underline;
line-height: 26px;
font-size: 13px;
}
.image {
width: 20%;
img {
width: 100%;
}
}
.show-more-button {
margin-left: 5px;
}
.status-account {
display: flex;
align-items: center;
}
.status-avatar-img {
width: 15px;
height: 15px;
margin-right: 5px;
}
.status-account-name {
margin: 0;
height: 22px;
}
.status-body {
display: flex;
flex-direction: column;
}
.status-content {
font-size: 15px;
line-height: 26px;
}
.status-card {
margin-bottom: 15px;
}
.status-deleted {
font-style: italic;
margin-top: 3px;
}
.status-header {
display: flex;
justify-content: space-between;
}
.status-without-content {
font-style: italic;
}
}
@media
only screen and (max-width: 760px),

View file

@ -39,6 +39,10 @@
@click.native="deleteMultipleUsers">
{{ $t('users.deleteAccounts') }}
</el-dropdown-item>
<el-dropdown-item
@click.native="requirePasswordReset">
{{ $t('users.requirePasswordReset') }}
</el-dropdown-item>
<el-dropdown-item divided class="no-hover">
<div class="tag-container">
<span class="tag-text">{{ $t('users.forceNsfw') }}</span>
@ -146,86 +150,64 @@ export default {
},
methods: {
mappers() {
const applyActionToAllUsers = (filteredUsers, fn) => Promise.all(filteredUsers.map(fn))
.then(() => {
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
}).catch((err) => {
console.log(err)
return
})
const applyAction = async(users, dispatchAction) => {
await dispatchAction(users)
this.$emit('apply-action')
}
return {
grantRight: (right) => () => {
const filterUsersFn = user => user.local && !user.roles[right] && this.$store.state.user.id !== user.id
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
const addRightFn = async(users) => await this.$store.dispatch('AddRight', { users, right })
const filtered = this.selectedUsers.filter(filterUsersFn)
applyActionToAllUsers(filtered, toggleRightFn)
applyAction(filtered, addRightFn)
},
revokeRight: (right) => () => {
const filterUsersFn = user => user.local && user.roles[right] && this.$store.state.user.id !== user.id
const toggleRightFn = async(user) => await this.$store.dispatch('ToggleRight', { user, right })
const deleteRightFn = async(users) => await this.$store.dispatch('DeleteRight', { users, right })
const filtered = this.selectedUsers.filter(filterUsersFn)
applyActionToAllUsers(filtered, toggleRightFn)
applyAction(filtered, deleteRightFn)
},
activate: () => {
const filtered = this.selectedUsers.filter(user => user.deactivated && this.$store.state.user.id !== user.id)
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
const activateUsersFn = async(users) => await this.$store.dispatch('ActivateUsers', users)
applyActionToAllUsers(filtered, toggleActivationFn)
applyAction(filtered, activateUsersFn)
},
deactivate: () => {
const filtered = this.selectedUsers.filter(user => !user.deactivated && this.$store.state.user.id !== user.id)
const toggleActivationFn = async(user) => await this.$store.dispatch('ToggleUserActivation', user.nickname)
const deactivateUsersFn = async(users) => await this.$store.dispatch('DeactivateUsers', users)
applyActionToAllUsers(filtered, toggleActivationFn)
applyAction(filtered, deactivateUsersFn)
},
remove: () => {
const filtered = this.selectedUsers.filter(user => this.$store.state.user.id !== user.id)
const deleteAccountFn = async(user) => await this.$store.dispatch('DeleteUser', user)
const deleteAccountFn = async(users) => await this.$store.dispatch('DeleteUsers', users)
applyActionToAllUsers(filtered, deleteAccountFn)
applyAction(filtered, deleteAccountFn)
},
addTag: (tag) => async() => {
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && !user.tags.includes(tag)
: !user.tags.includes(tag)
const users = this.selectedUsers.filter(filterUsersFn)
addTag: (tag) => () => {
const filtered = this.selectedUsers.filter(user =>
tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && !user.tags.includes(tag)
: !user.tags.includes(tag))
const addTagFn = async(users) => await this.$store.dispatch('AddTag', { users, tag })
try {
await this.$store.dispatch('AddTag', { users, tag })
} catch (err) {
console.log(err)
return
}
this.$message({
type: 'success',
message: this.$t('users.completed')
})
this.$emit('apply-action')
applyAction(filtered, addTagFn)
},
removeTag: (tag) => async() => {
const filterUsersFn = user => tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && user.tags.includes(tag)
: user.tags.includes(tag)
const users = this.selectedUsers.filter(filterUsersFn)
const filtered = this.selectedUsers.filter(user =>
tag === 'disable_remote_subscription' || tag === 'disable_any_subscription'
? user.local && user.tags.includes(tag)
: user.tags.includes(tag))
const removeTagFn = async(users) => await this.$store.dispatch('RemoveTag', { users, tag })
try {
await this.$store.dispatch('RemoveTag', { users, tag })
} catch (err) {
console.log(err)
return
}
this.$message({
type: 'success',
message: this.$t('users.completed')
})
applyAction(filtered, removeTagFn)
},
requirePasswordReset: () => {
const filtered = this.selectedUsers.filter(user => user.local)
filtered.map(user => this.$store.dispatch('RequirePasswordReset', user))
this.$emit('apply-action')
}
}
@ -265,6 +247,21 @@ export default {
remove
)
},
requirePasswordReset() {
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
if (!mailerEnabled) {
this.$alert(this.$t('users.mailerMustBeEnabled'), 'Error', { type: 'error' })
return
}
const { requirePasswordReset } = this.mappers()
this.confirmMessage(
this.$t('users.requirePasswordResetConfirmation'),
requirePasswordReset
)
},
addTagForMultipleUsers(tag) {
const { addTag } = this.mappers()
this.confirmMessage(

View file

@ -61,7 +61,7 @@
</el-table-column>
<el-table-column :label="$t('users.actions')" fixed="right">
<template slot-scope="scope">
<el-dropdown size="small" trigger="click">
<el-dropdown :hide-on-click="false" size="small" trigger="click">
<span class="el-dropdown-link">
{{ $t('users.moderation') }}
<i v-if="isDesktop" class="el-icon-arrow-down el-icon--right"/>
@ -80,7 +80,7 @@
<el-dropdown-item
v-if="showDeactivatedButton(scope.row.id)"
:divided="showAdminAction(scope.row)"
@click.native="handleDeactivation(scope.row)">
@click.native="toggleActivation(scope.row)">
{{ scope.row.deactivated ? $t('users.activateAccount') : $t('users.deactivateAccount') }}
</el-dropdown-item>
<el-dropdown-item
@ -133,6 +133,11 @@
@click.native="getPasswordResetToken(scope.row.nickname)">
{{ $t('users.getPasswordResetToken') }}
</el-dropdown-item>
<el-dropdown-item
v-if="scope.row.local"
@click.native="requirePasswordReset(scope.row.nickname)">
{{ $t('users.requirePasswordReset') }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
@ -151,9 +156,6 @@
</p>
</div>
</el-dialog>
<div v-if="users.length === 0" class="no-users-message">
<p>There are no users to display</p>
</div>
<div v-if="!loading" class="pagination">
<el-pagination
:total="usersCount"
@ -240,17 +242,8 @@ export default {
this.$refs.usersTable.clearSelection()
},
async createNewAccount(accountData) {
try {
await this.$store.dispatch('CreateNewAccount', accountData)
} catch (_e) {
return
} finally {
this.createAccountDialogOpen = false
}
this.$message({
type: 'success',
message: this.$t('users.accountCreated')
})
await this.$store.dispatch('CreateNewAccount', accountData)
this.createAccountDialogOpen = false
},
getFirstLetter(str) {
return str.charAt(0).toUpperCase()
@ -259,11 +252,24 @@ export default {
this.resetPasswordDialogOpen = true
this.$store.dispatch('GetPasswordResetToken', nickname)
},
handleDeactivation({ nickname }) {
this.$store.dispatch('ToggleUserActivation', nickname)
requirePasswordReset(nickname) {
const mailerEnabled = this.$store.state.user.nodeInfo.metadata.mailerEnabled
if (!mailerEnabled) {
this.$alert(this.$t('users.mailerMustBeEnabled'), 'Error', { type: 'error' })
return
}
this.$store.dispatch('RequirePasswordReset', { nickname })
},
toggleActivation(user) {
user.deactivated
? this.$store.dispatch('ActivateUsers', [user])
: this.$store.dispatch('DeactivateUsers', [user])
},
handleDeletion(user) {
this.$store.dispatch('DeleteUser', user)
this.$store.dispatch('DeleteUsers', [user])
},
handlePageChange(page) {
const searchQuery = this.$store.state.users.searchQuery
@ -292,7 +298,9 @@ export default {
: this.$store.dispatch('AddTag', { users: [user], tag })
},
toggleUserRight(user, right) {
this.$store.dispatch('ToggleRight', { user, right })
user.roles[right]
? this.$store.dispatch('DeleteRight', { users: [user], right })
: this.$store.dispatch('AddRight', { users: [user], right })
}
}
}

View file

@ -5,74 +5,77 @@
<h1>{{ user.display_name }}</h1>
</header>
<el-row>
<el-col :span="6">
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium">
<table class="el-table__body">
<tbody>
<tr class="el-table__row">
<td class="name-col">ID</td>
<td class="value-col">
{{ user.id }}
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.tags') }}</td>
<td>
<el-tag v-for="tag in user.tags" :key="tag">{{ tag }}</el-tag>
<span v-if="user.tags.length === 0">None</span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.moderator') }}</td>
<td>
<el-tag v-if="user.roles.moderator" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.roles.moderator" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.admin') }}</td>
<td>
<el-tag v-if="user.roles.admin" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.roles.admin" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.local') }}</td>
<td>
<el-tag v-if="user.local" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.local" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.deactivated') }}</td>
<td>
<el-tag v-if="user.deactivated" type="success"><i class="el-icon-check" /></el-tag>
<el-tag v-if="!user.deactivated" type="danger"><i class="el-icon-error" /></el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.nickname') }}</td>
<td>
{{ user.nickname }}
</td>
</tr>
</tbody>
</table>
</div>
<el-col :span="8">
<el-card class="user-profile-card">
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition el-table--medium">
<table class="user-profile-table">
<tbody>
<tr class="el-table__row">
<td>{{ $t('userProfile.nickname') }}</td>
<td>
{{ user.nickname }}
</td>
</tr>
<tr class="el-table__row">
<td class="name-col">ID</td>
<td class="value-col">
{{ user.id }}
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.tags') }}</td>
<td>
<el-tag v-for="tag in user.tags" :key="tag" class="user-profile-tag">{{ tag }}</el-tag>
<span v-if="user.tags.length === 0"></span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.roles') }}</td>
<td>
<el-tag v-if="user.roles.admin" class="user-profile-tag">
{{ $t('users.admin') }}
</el-tag>
<el-tag v-if="user.roles.moderator" class="user-profile-tag">
{{ $t('users.moderator') }}
</el-tag>
<span v-if="!user.roles.moderator && !user.roles.admin"></span>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.localUppercase') }}</td>
<td>
<el-tag v-if="user.local" type="info">{{ $t('userProfile.local') }}</el-tag>
<el-tag v-if="!user.local" type="info">{{ $t('userProfile.external') }}</el-tag>
</td>
</tr>
<tr class="el-table__row">
<td>{{ $t('userProfile.activeUppercase') }}</td>
<td>
<el-tag v-if="user.deactivated" type="success">{{ $t('userProfile.active') }}</el-tag>
<el-tag v-if="!user.deactivated" type="danger">{{ $t('userProfile.deactivated') }}</el-tag>
</td>
</tr>
</tbody>
</table>
</div>
</el-card>
</el-col>
<el-row type="flex" class="row-bg" justify="space-between">
<el-col :span="18"><h2>{{ $t('userProfile.recentStatuses') }}</h2></el-col>
<el-col :span="18">
<h2 class="recent-statuses">{{ $t('userProfile.recentStatuses') }}</h2>
</el-col>
<el-col :span="6" class="show-private">
<el-checkbox v-model="showPrivate" @change="onTogglePrivate">
{{ $t('userProfile.showPrivateStatuses') }}
</el-checkbox>
</el-col>
</el-row>
<el-col :span="18">
<el-col :span="16">
<el-timeline v-if="!statusesLoading" class="statuses">
<el-timeline-item v-for="status in statuses" :key="status.id">
<status :status="status" :user-id="user.id" :godmode="showPrivate"/>
</el-timeline-item>
<p v-if="statuses.length === 0" class="no-statuses">{{ $t('userProfile.noStatuses') }}</p>
</el-timeline>
</el-col>
</el-row>
@ -145,12 +148,33 @@ table {
width: 100%;
}
}
.no-statuses {
margin-left: 28px;
color: #606266;
}
.recent-statuses-header {
margin-top: 10px;
}
.statuses {
padding-right: 20px;
padding: 0 20px 0 0;
}
.show-private {
text-align: right;
line-height: 67px;
padding-right: 20px;
}
.recent-statuses {
margin-left: 28px;
}
.user-profile-card {
margin-left: 15px;
margin-right: 20px;
}
.user-profile-table {
margin: 0;
}
.user-profile-tag {
margin: 0 4px 4px 0;
}
</style>

0
static/.keep Normal file
View file

View file

@ -1,230 +0,0 @@
tinymce.addI18n('zh_CN',{
"Cut": "\u526a\u5207",
"Heading 5": "\u6807\u98985",
"Header 2": "\u6807\u98982",
"Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X\/C\/V keyboard shortcuts instead.": "\u4f60\u7684\u6d4f\u89c8\u5668\u4e0d\u652f\u6301\u5bf9\u526a\u8d34\u677f\u7684\u8bbf\u95ee\uff0c\u8bf7\u4f7f\u7528Ctrl+X\/C\/V\u952e\u8fdb\u884c\u590d\u5236\u7c98\u8d34\u3002",
"Heading 4": "\u6807\u98984",
"Div": "Div\u533a\u5757",
"Heading 2": "\u6807\u98982",
"Paste": "\u7c98\u8d34",
"Close": "\u5173\u95ed",
"Font Family": "\u5b57\u4f53",
"Pre": "\u9884\u683c\u5f0f\u6587\u672c",
"Align right": "\u53f3\u5bf9\u9f50",
"New document": "\u65b0\u6587\u6863",
"Blockquote": "\u5f15\u7528",
"Numbered list": "\u7f16\u53f7\u5217\u8868",
"Heading 1": "\u6807\u98981",
"Headings": "\u6807\u9898",
"Increase indent": "\u589e\u52a0\u7f29\u8fdb",
"Formats": "\u683c\u5f0f",
"Headers": "\u6807\u9898",
"Select all": "\u5168\u9009",
"Header 3": "\u6807\u98983",
"Blocks": "\u533a\u5757",
"Undo": "\u64a4\u6d88",
"Strikethrough": "\u5220\u9664\u7ebf",
"Bullet list": "\u9879\u76ee\u7b26\u53f7",
"Header 1": "\u6807\u98981",
"Superscript": "\u4e0a\u6807",
"Clear formatting": "\u6e05\u9664\u683c\u5f0f",
"Font Sizes": "\u5b57\u53f7",
"Subscript": "\u4e0b\u6807",
"Header 6": "\u6807\u98986",
"Redo": "\u91cd\u590d",
"Paragraph": "\u6bb5\u843d",
"Ok": "\u786e\u5b9a",
"Bold": "\u7c97\u4f53",
"Code": "\u4ee3\u7801",
"Italic": "\u659c\u4f53",
"Align center": "\u5c45\u4e2d",
"Header 5": "\u6807\u98985",
"Heading 6": "\u6807\u98986",
"Heading 3": "\u6807\u98983",
"Decrease indent": "\u51cf\u5c11\u7f29\u8fdb",
"Header 4": "\u6807\u98984",
"Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.": "\u5f53\u524d\u4e3a\u7eaf\u6587\u672c\u7c98\u8d34\u6a21\u5f0f\uff0c\u518d\u6b21\u70b9\u51fb\u53ef\u4ee5\u56de\u5230\u666e\u901a\u7c98\u8d34\u6a21\u5f0f\u3002",
"Underline": "\u4e0b\u5212\u7ebf",
"Cancel": "\u53d6\u6d88",
"Justify": "\u4e24\u7aef\u5bf9\u9f50",
"Inline": "\u6587\u672c",
"Copy": "\u590d\u5236",
"Align left": "\u5de6\u5bf9\u9f50",
"Visual aids": "\u7f51\u683c\u7ebf",
"Lower Greek": "\u5c0f\u5199\u5e0c\u814a\u5b57\u6bcd",
"Square": "\u65b9\u5757",
"Default": "\u9ed8\u8ba4",
"Lower Alpha": "\u5c0f\u5199\u82f1\u6587\u5b57\u6bcd",
"Circle": "\u7a7a\u5fc3\u5706",
"Disc": "\u5b9e\u5fc3\u5706",
"Upper Alpha": "\u5927\u5199\u82f1\u6587\u5b57\u6bcd",
"Upper Roman": "\u5927\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Lower Roman": "\u5c0f\u5199\u7f57\u9a6c\u5b57\u6bcd",
"Id should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.": "\u6807\u8bc6\u7b26\u5e94\u8be5\u4ee5\u5b57\u6bcd\u5f00\u5934\uff0c\u540e\u8ddf\u5b57\u6bcd\u3001\u6570\u5b57\u3001\u7834\u6298\u53f7\u3001\u70b9\u3001\u5192\u53f7\u6216\u4e0b\u5212\u7ebf\u3002",
"Name": "\u540d\u79f0",
"Anchor": "\u951a\u70b9",
"Id": "\u6807\u8bc6\u7b26",
"You have unsaved changes are you sure you want to navigate away?": "\u4f60\u8fd8\u6709\u6587\u6863\u5c1a\u672a\u4fdd\u5b58\uff0c\u786e\u5b9a\u8981\u79bb\u5f00\uff1f",
"Restore last draft": "\u6062\u590d\u4e0a\u6b21\u7684\u8349\u7a3f",
"Special character": "\u7279\u6b8a\u7b26\u53f7",
"Source code": "\u6e90\u4ee3\u7801",
"Language": "\u8bed\u8a00",
"Insert\/Edit code sample": "\u63d2\u5165\/\u7f16\u8f91\u4ee3\u7801\u793a\u4f8b",
"B": "B",
"R": "R",
"G": "G",
"Color": "\u989c\u8272",
"Right to left": "\u4ece\u53f3\u5230\u5de6",
"Left to right": "\u4ece\u5de6\u5230\u53f3",
"Emoticons": "\u8868\u60c5",
"Robots": "\u673a\u5668\u4eba",
"Document properties": "\u6587\u6863\u5c5e\u6027",
"Title": "\u6807\u9898",
"Keywords": "\u5173\u952e\u8bcd",
"Encoding": "\u7f16\u7801",
"Description": "\u63cf\u8ff0",
"Author": "\u4f5c\u8005",
"Fullscreen": "\u5168\u5c4f",
"Horizontal line": "\u6c34\u5e73\u5206\u5272\u7ebf",
"Horizontal space": "\u6c34\u5e73\u8fb9\u8ddd",
"Insert\/edit image": "\u63d2\u5165\/\u7f16\u8f91\u56fe\u7247",
"General": "\u666e\u901a",
"Advanced": "\u9ad8\u7ea7",
"Source": "\u5730\u5740",
"Border": "\u8fb9\u6846",
"Constrain proportions": "\u4fdd\u6301\u7eb5\u6a2a\u6bd4",
"Vertical space": "\u5782\u76f4\u8fb9\u8ddd",
"Image description": "\u56fe\u7247\u63cf\u8ff0",
"Style": "\u6837\u5f0f",
"Dimensions": "\u5927\u5c0f",
"Insert image": "\u63d2\u5165\u56fe\u7247",
"Image": "\u56fe\u7247",
"Zoom in": "\u653e\u5927",
"Contrast": "\u5bf9\u6bd4\u5ea6",
"Back": "\u540e\u9000",
"Gamma": "\u4f3d\u9a6c\u503c",
"Flip horizontally": "\u6c34\u5e73\u7ffb\u8f6c",
"Resize": "\u8c03\u6574\u5927\u5c0f",
"Sharpen": "\u9510\u5316",
"Zoom out": "\u7f29\u5c0f",
"Image options": "\u56fe\u7247\u9009\u9879",
"Apply": "\u5e94\u7528",
"Brightness": "\u4eae\u5ea6",
"Rotate clockwise": "\u987a\u65f6\u9488\u65cb\u8f6c",
"Rotate counterclockwise": "\u9006\u65f6\u9488\u65cb\u8f6c",
"Edit image": "\u7f16\u8f91\u56fe\u7247",
"Color levels": "\u989c\u8272\u5c42\u6b21",
"Crop": "\u88c1\u526a",
"Orientation": "\u65b9\u5411",
"Flip vertically": "\u5782\u76f4\u7ffb\u8f6c",
"Invert": "\u53cd\u8f6c",
"Date\/time": "\u65e5\u671f\/\u65f6\u95f4",
"Insert date\/time": "\u63d2\u5165\u65e5\u671f\/\u65f6\u95f4",
"Remove link": "\u5220\u9664\u94fe\u63a5",
"Url": "\u5730\u5740",
"Text to display": "\u663e\u793a\u6587\u5b57",
"Anchors": "\u951a\u70b9",
"Insert link": "\u63d2\u5165\u94fe\u63a5",
"Link": "\u94fe\u63a5",
"New window": "\u5728\u65b0\u7a97\u53e3\u6253\u5f00",
"None": "\u65e0",
"The URL you entered seems to be an external link. Do you want to add the required http:\/\/ prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u5c5e\u4e8e\u5916\u90e8\u94fe\u63a5\uff0c\u9700\u8981\u52a0\u4e0ahttp:\/\/:\u524d\u7f00\u5417\uff1f",
"Paste or type a link": "\u7c98\u8d34\u6216\u8f93\u5165\u94fe\u63a5",
"Target": "\u6253\u5f00\u65b9\u5f0f",
"The URL you entered seems to be an email address. Do you want to add the required mailto: prefix?": "\u4f60\u6240\u586b\u5199\u7684URL\u5730\u5740\u4e3a\u90ae\u4ef6\u5730\u5740\uff0c\u9700\u8981\u52a0\u4e0amailto:\u524d\u7f00\u5417\uff1f",
"Insert\/edit link": "\u63d2\u5165\/\u7f16\u8f91\u94fe\u63a5",
"Insert\/edit video": "\u63d2\u5165\/\u7f16\u8f91\u89c6\u9891",
"Media": "\u5a92\u4f53",
"Alternative source": "\u955c\u50cf",
"Paste your embed code below:": "\u5c06\u5185\u5d4c\u4ee3\u7801\u7c98\u8d34\u5728\u4e0b\u9762:",
"Insert video": "\u63d2\u5165\u89c6\u9891",
"Poster": "\u5c01\u9762",
"Insert\/edit media": "\u63d2\u5165\/\u7f16\u8f91\u5a92\u4f53",
"Embed": "\u5185\u5d4c",
"Nonbreaking space": "\u4e0d\u95f4\u65ad\u7a7a\u683c",
"Page break": "\u5206\u9875\u7b26",
"Paste as text": "\u7c98\u8d34\u4e3a\u6587\u672c",
"Preview": "\u9884\u89c8",
"Print": "\u6253\u5370",
"Save": "\u4fdd\u5b58",
"Could not find the specified string.": "\u672a\u627e\u5230\u641c\u7d22\u5185\u5bb9.",
"Replace": "\u66ff\u6362",
"Next": "\u4e0b\u4e00\u4e2a",
"Whole words": "\u5168\u5b57\u5339\u914d",
"Find and replace": "\u67e5\u627e\u548c\u66ff\u6362",
"Replace with": "\u66ff\u6362\u4e3a",
"Find": "\u67e5\u627e",
"Replace all": "\u5168\u90e8\u66ff\u6362",
"Match case": "\u533a\u5206\u5927\u5c0f\u5199",
"Prev": "\u4e0a\u4e00\u4e2a",
"Spellcheck": "\u62fc\u5199\u68c0\u67e5",
"Finish": "\u5b8c\u6210",
"Ignore all": "\u5168\u90e8\u5ffd\u7565",
"Ignore": "\u5ffd\u7565",
"Add to Dictionary": "\u6dfb\u52a0\u5230\u5b57\u5178",
"Insert row before": "\u5728\u4e0a\u65b9\u63d2\u5165",
"Rows": "\u884c",
"Height": "\u9ad8",
"Paste row after": "\u7c98\u8d34\u5230\u4e0b\u65b9",
"Alignment": "\u5bf9\u9f50\u65b9\u5f0f",
"Border color": "\u8fb9\u6846\u989c\u8272",
"Column group": "\u5217\u7ec4",
"Row": "\u884c",
"Insert column before": "\u5728\u5de6\u4fa7\u63d2\u5165",
"Split cell": "\u62c6\u5206\u5355\u5143\u683c",
"Cell padding": "\u5355\u5143\u683c\u5185\u8fb9\u8ddd",
"Cell spacing": "\u5355\u5143\u683c\u5916\u95f4\u8ddd",
"Row type": "\u884c\u7c7b\u578b",
"Insert table": "\u63d2\u5165\u8868\u683c",
"Body": "\u8868\u4f53",
"Caption": "\u6807\u9898",
"Footer": "\u8868\u5c3e",
"Delete row": "\u5220\u9664\u884c",
"Paste row before": "\u7c98\u8d34\u5230\u4e0a\u65b9",
"Scope": "\u8303\u56f4",
"Delete table": "\u5220\u9664\u8868\u683c",
"H Align": "\u6c34\u5e73\u5bf9\u9f50",
"Top": "\u9876\u90e8\u5bf9\u9f50",
"Header cell": "\u8868\u5934\u5355\u5143\u683c",
"Column": "\u5217",
"Row group": "\u884c\u7ec4",
"Cell": "\u5355\u5143\u683c",
"Middle": "\u5782\u76f4\u5c45\u4e2d",
"Cell type": "\u5355\u5143\u683c\u7c7b\u578b",
"Copy row": "\u590d\u5236\u884c",
"Row properties": "\u884c\u5c5e\u6027",
"Table properties": "\u8868\u683c\u5c5e\u6027",
"Bottom": "\u5e95\u90e8\u5bf9\u9f50",
"V Align": "\u5782\u76f4\u5bf9\u9f50",
"Header": "\u8868\u5934",
"Right": "\u53f3\u5bf9\u9f50",
"Insert column after": "\u5728\u53f3\u4fa7\u63d2\u5165",
"Cols": "\u5217",
"Insert row after": "\u5728\u4e0b\u65b9\u63d2\u5165",
"Width": "\u5bbd",
"Cell properties": "\u5355\u5143\u683c\u5c5e\u6027",
"Left": "\u5de6\u5bf9\u9f50",
"Cut row": "\u526a\u5207\u884c",
"Delete column": "\u5220\u9664\u5217",
"Center": "\u5c45\u4e2d",
"Merge cells": "\u5408\u5e76\u5355\u5143\u683c",
"Insert template": "\u63d2\u5165\u6a21\u677f",
"Templates": "\u6a21\u677f",
"Background color": "\u80cc\u666f\u8272",
"Custom...": "\u81ea\u5b9a\u4e49...",
"Custom color": "\u81ea\u5b9a\u4e49\u989c\u8272",
"No color": "\u65e0",
"Text color": "\u6587\u5b57\u989c\u8272",
"Table of Contents": "\u5185\u5bb9\u5217\u8868",
"Show blocks": "\u663e\u793a\u533a\u5757\u8fb9\u6846",
"Show invisible characters": "\u663e\u793a\u4e0d\u53ef\u89c1\u5b57\u7b26",
"Words: {0}": "\u5b57\u6570\uff1a{0}",
"Insert": "\u63d2\u5165",
"File": "\u6587\u4ef6",
"Edit": "\u7f16\u8f91",
"Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help": "\u5728\u7f16\u8f91\u533a\u6309ALT-F9\u6253\u5f00\u83dc\u5355\uff0c\u6309ALT-F10\u6253\u5f00\u5de5\u5177\u680f\uff0c\u6309ALT-0\u67e5\u770b\u5e2e\u52a9",
"Tools": "\u5de5\u5177",
"View": "\u89c6\u56fe",
"Table": "\u8868\u683c",
"Format": "\u683c\u5f0f"
});

View file

@ -1,138 +0,0 @@
/* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
code[class*="language-"]::selection, code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #a67f59;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

View file

@ -1,154 +0,0 @@
.mce-visualblocks p {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhCQAJAJEAAAAAAP///7u7u////yH5BAEAAAMALAAAAAAJAAkAAAIQnG+CqCN/mlyvsRUpThG6AgA7);
background-repeat: no-repeat;
}
.mce-visualblocks h1 {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGu1JuxHoAfRNRW3TWXyF2YiRUAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks h2 {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8Hybbx4oOuqgTynJd6bGlWg3DkJzoaUAAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks h3 {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIZjI8Hybbx4oOuqgTynJf2Ln2NOHpQpmhAAQA7);
background-repeat: no-repeat;
}
.mce-visualblocks h4 {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxInR0zqeAdhtJlXwV1oCll2HaWgAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks h5 {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjane4iq5GlW05GgIkIZUAAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks h6 {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhDgAKAIABALu7u////yH5BAEAAAEALAAAAAAOAAoAAAIajI8HybbxIoiuwjan04jep1iZ1XRlAo5bVgAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks div:not([data-mce-bogus]) {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhEgAKAIABALu7u////yH5BAEAAAEALAAAAAASAAoAAAIfjI9poI0cgDywrhuxfbrzDEbQM2Ei5aRjmoySW4pAAQA7);
background-repeat: no-repeat;
}
.mce-visualblocks section {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhKAAKAIABALu7u////yH5BAEAAAEALAAAAAAoAAoAAAI5jI+pywcNY3sBWHdNrplytD2ellDeSVbp+GmWqaDqDMepc8t17Y4vBsK5hDyJMcI6KkuYU+jpjLoKADs=);
background-repeat: no-repeat;
}
.mce-visualblocks article {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhKgAKAIABALu7u////yH5BAEAAAEALAAAAAAqAAoAAAI6jI+pywkNY3wG0GBvrsd2tXGYSGnfiF7ikpXemTpOiJScasYoDJJrjsG9gkCJ0ag6KhmaIe3pjDYBBQA7);
background-repeat: no-repeat;
}
.mce-visualblocks blockquote {
padding-top: 10px;
border: 1px dashed #BBB;
background-image: url(data:image/gif;base64,R0lGODlhPgAKAIABALu7u////yH5BAEAAAEALAAAAAA+AAoAAAJPjI+py+0Knpz0xQDyuUhvfoGgIX5iSKZYgq5uNL5q69asZ8s5rrf0yZmpNkJZzFesBTu8TOlDVAabUyatguVhWduud3EyiUk45xhTTgMBBQA7);
background-repeat: no-repeat;
}
.mce-visualblocks address {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhLQAKAIABALu7u////yH5BAEAAAEALAAAAAAtAAoAAAI/jI+pywwNozSP1gDyyZcjb3UaRpXkWaXmZW4OqKLhBmLs+K263DkJK7OJeifh7FicKD9A1/IpGdKkyFpNmCkAADs=);
background-repeat: no-repeat;
}
.mce-visualblocks pre {
padding-top: 10px;
border: 1px dashed #BBB;
margin-left: 3px;
background-image: url(data:image/gif;base64,R0lGODlhFQAKAIABALu7uwAAACH5BAEAAAEALAAAAAAVAAoAAAIjjI+ZoN0cgDwSmnpz1NCueYERhnibZVKLNnbOq8IvKpJtVQAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks figure {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhJAAKAIAAALu7u////yH5BAEAAAEALAAAAAAkAAoAAAI0jI+py+2fwAHUSFvD3RlvG4HIp4nX5JFSpnZUJ6LlrM52OE7uSWosBHScgkSZj7dDKnWAAgA7);
background-repeat: no-repeat;
}
.mce-visualblocks hgroup {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhJwAKAIABALu7uwAAACH5BAEAAAEALAAAAAAnAAoAAAI3jI+pywYNI3uB0gpsRtt5fFnfNZaVSYJil4Wo03Hv6Z62uOCgiXH1kZIIJ8NiIxRrAZNMZAtQAAA7);
background-repeat: no-repeat;
}
.mce-visualblocks aside {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhHgAKAIABAKqqqv///yH5BAEAAAEALAAAAAAeAAoAAAItjI+pG8APjZOTzgtqy7I3f1yehmQcFY4WKZbqByutmW4aHUd6vfcVbgudgpYCADs=);
background-repeat: no-repeat;
}
.mce-visualblocks figcaption {
border: 1px dashed #BBB;
}
.mce-visualblocks ul {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhDQAKAIAAALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybGuYnqUVSjvw26DzzXiqIDlVwAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks ol {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybH6HHt0qourxC6CvzXieHyeWQAAOw==);
background-repeat: no-repeat;
}
.mce-visualblocks dl {
padding-top: 10px;
border: 1px dashed #BBB;
margin: 0 0 1em 3px;
background-image: url(data:image/gif;base64,R0lGODlhDQAKAIABALu7u////yH5BAEAAAEALAAAAAANAAoAAAIXjI8GybEOnmOvUoWznTqeuEjNSCqeGRUAOw==);
background-repeat: no-repeat;
}

View file

@ -1 +0,0 @@
.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid rgba(208,2,27,0.5);cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#2276d2 !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2276d2}.mce-content-body *[data-mce-selected="inline-boundary"]{background:#bfe6ff}.mce-content-body .mce-item-anchor[data-mce-selected]{background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-content-body hr{cursor:default}.ephox-snooker-resizer-bar{background-color:#2276d2;opacity:0}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:.2}.mce-content-body{line-height:1.3}

View file

@ -1 +0,0 @@
body{background-color:#FFFFFF;color:#000000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px;line-height:1.3;scrollbar-3dlight-color:#F0F0EE;scrollbar-arrow-color:#676662;scrollbar-base-color:#F0F0EE;scrollbar-darkshadow-color:#DDDDDD;scrollbar-face-color:#E0E0DD;scrollbar-highlight-color:#F0F0EE;scrollbar-shadow-color:#F0F0EE;scrollbar-track-color:#F5F5F5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px}.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid rgba(208,2,27,0.5);cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#2276d2 !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2276d2}.mce-content-body *[data-mce-selected="inline-boundary"]{background:#bfe6ff}.mce-content-body .mce-item-anchor[data-mce-selected]{background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-content-body hr{cursor:default}.ephox-snooker-resizer-bar{background-color:#2276d2;opacity:0}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:.2} a {color: #1478F0;}

Some files were not shown because too many files have changed in this diff Show more