Skip to content
Snippets Groups Projects
Commit 2841de76 authored by vaartis's avatar vaartis Committed by Maxim Filippov
Browse files

Add configuration for sharing emoji packs

parent 795800e9
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased
### Added
- Emoji pack configuration
## [1.1.0] - 2019-09-15
### Added
......
......@@ -18,7 +18,8 @@ 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"]'`.
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.
## Changelog
......
......@@ -48,7 +48,7 @@ const devWebpackConfig = merge(baseWebpackConfig, {
poll: config.dev.poll
},
headers: {
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self';"
'content-security-policy': "base-uri 'self'; frame-ancestors 'none'; img-src 'self' data: https: http:; media-src 'self' https:; style-src 'self' 'unsafe-inline'; font-src 'self'; manifest-src 'self'; script-src 'self';"
}
},
plugins: [
......
import request from '@/utils/request'
import { getToken } from '@/utils/auth'
import { baseName } from './utils'
import _ from 'lodash'
export async function deletePack(host, token, name) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${name}`,
method: 'delete',
headers: authHeaders(token)
})
}
export async function reloadEmoji(host, token) {
return await request({
baseURL: baseName(host),
url: '/api/pleroma/admin/reload_emoji',
method: 'post',
headers: authHeaders(token)
})
}
export async function importFromFS(host, token) {
return await request({
baseURL: baseName(host),
url: '/api/pleroma/emoji/packs/import_from_fs',
method: 'post',
headers: authHeaders(token)
})
}
export async function createPack(host, token, name) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${name}`,
method: 'put',
headers: authHeaders(token)
})
}
export async function listPacks(host) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/`,
method: 'get'
})
}
export async function downloadFrom(host, instance_address, pack_name, as, token) {
if (as.trim() === '') {
as = null
}
return await request({
baseURL: baseName(host),
url: '/api/pleroma/emoji/packs/download_from',
method: 'post',
headers: authHeaders(token),
data: { instance_address, pack_name, as },
timeout: 0
})
}
export async function savePackMetadata(host, token, name, new_data) {
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${name}/update_metadata`,
method: 'post',
headers: authHeaders(token),
data: { name, new_data },
timeout: 0 // This might take a long time
})
}
function fileUpdateFormData(d) {
const data = new FormData()
_.each(d, (v, k) => {
data.set(k, v)
})
return data
}
export async function updatePackFile(host, token, args) {
let data = null
switch (args.action) {
case 'add': {
const { shortcode, file, fileName } = args
data = fileUpdateFormData({
action: 'add',
shortcode: shortcode,
file: file
})
if (fileName.trim() !== '') {
data.set('filename', fileName)
}
break
}
case 'update': {
const { oldName, newName, newFilename } = args
data = fileUpdateFormData({
action: 'update',
shortcode: oldName,
new_shortcode: newName,
new_filename: newFilename
})
break
}
case 'remove': {
const { name } = args
data = fileUpdateFormData({
action: 'remove',
shortcode: name
})
break
}
}
const { packName } = args
return await request({
baseURL: baseName(host),
url: `/api/pleroma/emoji/packs/${packName}/update_file`,
method: 'post',
headers: authHeaders(token),
data: data,
timeout: 0
})
}
export function addressOfEmojiInPack(host, packName, name) {
// This needs http because hackney on the BE does not understand URLs with just "//"
return `http://${baseName(host)}/emoji/${packName}/${name}`
}
const authHeaders = (token) => token ? { 'Authorization': `Bearer ${getToken()}` } : {}
......@@ -66,7 +66,8 @@ export default {
externalLink: 'External Link',
users: 'Users',
reports: 'Reports',
settings: 'Settings'
settings: 'Settings',
'emoji-packs': 'Emoji packs'
},
navbar: {
logOut: 'Log Out',
......
......@@ -35,6 +35,20 @@ const reports = {
]
}
const emojiPacksDisabled = disabledFeatures.includes('emoji-packs')
const emojiPacks = {
path: '/emoji-packs',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/emoji-packs/index'),
name: 'Emoji packs',
meta: { title: 'emoji-packs', icon: 'settings', noCache: true }
}
]
}
export const constantRouterMap = [
{
path: '/redirect',
......@@ -100,6 +114,7 @@ export const asyncRouterMap = [
},
...(settingsDisabled ? [] : [settings]),
...(reportsDisabled ? [] : [reports]),
...(emojiPacksDisabled ? [] : [emojiPacks]),
{
path: '/users/:id',
component: Layout,
......
......@@ -10,6 +10,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'
Vue.use(Vuex)
......@@ -23,7 +24,8 @@ const store = new Vuex.Store({
tagsView,
user,
userProfile,
users
users,
emoji_packs
},
getters
})
......
import { listPacks,
downloadFrom,
reloadEmoji,
createPack,
deletePack,
savePackMetadata,
importFromFS,
updatePackFile } from '@/api/emoji_packs'
import { Message } from 'element-ui'
import Vue from 'vue'
const packs = {
state: {
localPacks: {},
remotePacks: {}
},
mutations: {
SET_LOCAL_PACKS: (state, packs) => {
state.localPacks = packs
},
SET_REMOTE_PACKS: (state, packs) => {
state.remotePacks = packs
},
UPDATE_LOCAL_PACK_VAL: (state, { name, key, value }) => {
Vue.set(state.localPacks[name]['pack'], key, value)
},
UPDATE_LOCAL_PACK_PACK: (state, { name, pack }) => {
state.localPacks[name]['pack'] = pack
},
UPDATE_LOCAL_PACK_FILES: (state, { name, files }) => {
// Use vue.set in case "files" was null
Vue.set(
state.localPacks[name],
'files',
files
)
}
},
actions: {
async SetLocalEmojiPacks({ commit, getters, state }) {
const { data } = await listPacks(getters.authHost)
commit('SET_LOCAL_PACKS', data)
},
async SetRemoteEmojiPacks({ commit, getters, state }, { remoteInstance }) {
const { data } = await listPacks(remoteInstance)
commit('SET_REMOTE_PACKS', data)
},
async DownloadFrom({ commit, getters, state }, { instanceAddress, packName, as }) {
const result = await downloadFrom(getters.authHost, instanceAddress, packName, as, getters.token)
if (result.data === 'ok') {
Message({
message: `Successfully downloaded ${packName}`,
type: 'success',
duration: 5 * 1000
})
}
},
async ReloadEmoji({ commit, getters, state }) {
await reloadEmoji(getters.authHost, getters.token)
},
async ImportFromFS({ commit, getters, state }) {
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'
Message({
message,
type: 'success',
duration: 5 * 1000
})
}
},
async DeletePack({ commit, getters, state }, { name }) {
await deletePack(getters.authHost, getters.token, name)
},
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(
getters.authHost,
getters.token,
packName,
state.localPacks[packName]['pack']
)
if (result.status === 200) {
Message({
message: `Successfully updated ${packName} metadata`,
type: 'success',
duration: 5 * 1000
})
commit('UPDATE_LOCAL_PACK_PACK', { name: packName, pack: result.data })
}
},
async UpdateAndSavePackFile({ commit, getters, state }, args) {
const result = await updatePackFile(getters.authHost, getters.token, args)
if (result.status === 200) {
const { packName } = args
Message({
message: `Successfully updated ${packName} files`,
type: 'success',
duration: 5 * 1000
})
commit('UPDATE_LOCAL_PACK_FILES', { name: packName, files: result.data })
}
}
}
}
export default packs
......@@ -10,9 +10,9 @@ const service = axios.create({
service.interceptors.response.use(
response => response,
error => {
console.log('err' + error)
console.log('Error ' + error)
Message({
message: error.message,
message: `${error.message} - ${error.response.data}`,
type: 'error',
duration: 5 * 1000
})
......
<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>
<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>
<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>
<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>
<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">
<el-option
v-for="(_pack, name) in $store.state.emoji_packs.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)" />
<el-button
:disabled="!copyToLocalPackName"
type="success"
class="copy-to-local-button"
@click="copyToLocal">Copy</el-button>
<el-button slot="reference" type="primary">Copy to local pack...</el-button>
</el-popover>
</el-col>
<el-col :span="2">
<img
:src="addressOfEmojiInPack(host, packName, file)"
class="emoji-preview-img">
</el-col>
</el-row>
</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'
export default {
props: {
host: {
type: String,
required: true
},
packName: {
type: String,
required: true
},
name: {
type: String,
required: true
},
file: {
type: String,
required: true
},
isLocal: {
type: Boolean,
required: true
}
},
data() {
return {
newName: null,
newFile: null,
copyToLocalPackName: null,
copyToLocalVisible: false,
copyToShortcode: '',
copyToFilename: ''
}
},
computed: {
modifyingName: {
get() {
// Return a modified name if it was actually modified, otherwise return the old name
return this.newName !== null ? this.newName : this.name
},
set(val) { this.newName = val }
},
modifyingFile: {
get() {
// Return a modified name if it was actually modified, otherwise return the old name
return this.newFile !== null ? this.newFile : this.file
},
set(val) { this.newFile = val }
}
},
methods: {
update() {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'update',
packName: this.packName,
oldName: this.name,
newName: this.modifyingName,
newFilename: this.modifyingFile
}).then(() => {
this.newName = null
this.newFile = null
this.$store.dispatch('ReloadEmoji')
})
},
remove() {
this.$confirm('This will delete the emoji, are you sure?', 'Warning', {
confirmButtonText: 'Yes, delete the emoji',
cancelButtonText: 'No, leave it be',
type: 'warning'
}).then(() => {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'remove',
packName: this.packName,
name: this.name
}).then(() => {
this.newName = null
this.newFile = null
this.$store.dispatch('ReloadEmoji')
})
})
},
copyToLocal() {
this.$store.dispatch('UpdateAndSavePackFile', {
action: 'add',
packName: this.copyToLocalPackName,
shortcode: this.copyToShortcode.trim() !== '' ? this.copyToShortcode.trim() : this.name,
fileName: this.copyToFilename.trim() !== '' ? this.copyToFilename.trim() : this.file,
file: this.addressOfEmojiInPack(this.host, this.packName, this.file)
}).then(() => {
this.copyToLocalPackName = null
this.copyToLocalVisible = false
this.copyToShortcode = ''
this.copyToFilename = ''
this.$store.dispatch('ReloadEmoji')
})
},
addressOfEmojiInPack
}
}
</script>
<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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment