mirror of
https://github.com/BoostIo/Boostnote
synced 2025-12-13 17:56:25 +00:00
@@ -19,5 +19,8 @@
|
|||||||
"FileReader": true,
|
"FileReader": true,
|
||||||
"localStorage": true,
|
"localStorage": true,
|
||||||
"fetch": true
|
"fetch": true
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ node_js:
|
|||||||
- 6
|
- 6
|
||||||
script:
|
script:
|
||||||
- npm run lint && npm run test
|
- npm run lint && npm run test
|
||||||
|
- yarn jest
|
||||||
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
|
||||||
after_success:
|
after_success:
|
||||||
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ class MarkdownEditor extends React.Component {
|
|||||||
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
|
noteKey={noteKey}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -211,8 +211,9 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
|
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams()
|
||||||
|
|
||||||
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
|
||||||
const body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
let body = this.markdown.render(escapeHtmlCharacters(noteContent))
|
||||||
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
|
||||||
|
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath)
|
||||||
|
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
file = file.replace('file://', '')
|
file = file.replace('file://', '')
|
||||||
@@ -221,6 +222,13 @@ export default class MarkdownPreview extends React.Component {
|
|||||||
dst: 'css'
|
dst: 'css'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
attachmentsAbsolutePaths.forEach((attachment) => {
|
||||||
|
exportTasks.push({
|
||||||
|
src: attachment,
|
||||||
|
dst: attachmentManagement.DESTINATION_FOLDER
|
||||||
|
})
|
||||||
|
})
|
||||||
|
body = attachmentManagement.removeStorageAndNoteReferences(body, this.props.noteKey)
|
||||||
|
|
||||||
let styles = ''
|
let styles = ''
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ class MarkdownSplitEditor extends React.Component {
|
|||||||
onScroll={this.handleScroll.bind(this)}
|
onScroll={this.handleScroll.bind(this)}
|
||||||
showCopyNotification={config.ui.showCopyNotification}
|
showCopyNotification={config.ui.showCopyNotification}
|
||||||
storagePath={storage.path}
|
storagePath={storage.path}
|
||||||
|
noteKey={noteKey}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const fs = require('fs')
|
|||||||
const path = require('path')
|
const path = require('path')
|
||||||
const findStorage = require('browser/lib/findStorage')
|
const findStorage = require('browser/lib/findStorage')
|
||||||
const mdurl = require('mdurl')
|
const mdurl = require('mdurl')
|
||||||
|
const escapeStringRegexp = require('escape-string-regexp')
|
||||||
|
|
||||||
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
const STORAGE_FOLDER_PLACEHOLDER = ':storage'
|
||||||
const DESTINATION_FOLDER = 'attachments'
|
const DESTINATION_FOLDER = 'attachments'
|
||||||
@@ -104,8 +105,8 @@ function handleAttachmentDrop (codeEditor, storageKey, noteKey, dropEvent) {
|
|||||||
const fileType = file['type']
|
const fileType = file['type']
|
||||||
|
|
||||||
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
|
copyAttachment(filePath, storageKey, noteKey).then((fileName) => {
|
||||||
let showPreview = fileType.startsWith('image')
|
const showPreview = fileType.startsWith('image')
|
||||||
let imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
|
const imageMd = generateAttachmentMarkdown(originalFileName, path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName), showPreview)
|
||||||
codeEditor.insertAttachmentMd(imageMd)
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -139,7 +140,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
|||||||
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
|
||||||
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
createAttachmentDestinationFolder(targetStorage.path, noteKey)
|
||||||
|
|
||||||
let imageName = `${uniqueSlug()}.png`
|
const imageName = `${uniqueSlug()}.png`
|
||||||
const imagePath = path.join(destinationDir, imageName)
|
const imagePath = path.join(destinationDir, imageName)
|
||||||
|
|
||||||
reader.onloadend = function () {
|
reader.onloadend = function () {
|
||||||
@@ -147,18 +148,57 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
|
|||||||
base64data += base64data.replace('+', ' ')
|
base64data += base64data.replace('+', ' ')
|
||||||
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
const binaryData = new Buffer(base64data, 'base64').toString('binary')
|
||||||
fs.writeFile(imagePath, binaryData, 'binary')
|
fs.writeFile(imagePath, binaryData, 'binary')
|
||||||
let imageMd = generateAttachmentMarkdown(imageName, imagePath, true)
|
const imageMd = generateAttachmentMarkdown(imageName, imagePath, true)
|
||||||
codeEditor.insertAttachmentMd(imageMd)
|
codeEditor.insertAttachmentMd(imageMd)
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(blob)
|
reader.readAsDataURL(blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns all attachment paths of the given markdown
|
||||||
|
* @param {String} markdownContent content in which the attachment paths should be found
|
||||||
|
* @returns {String[]} Array of the relativ paths (starting with :storage) of the attachments of the given markdown
|
||||||
|
*/
|
||||||
|
function getAttachmentsInContent (markdownContent) {
|
||||||
|
const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep)
|
||||||
|
const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '([a-zA-Z0-9]|-)+' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g')
|
||||||
|
return preparedInput.match(regexp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Returns an array of the absolute paths of the attachments referenced in the given markdown code
|
||||||
|
* @param {String} markdownContent content in which the attachment paths should be found
|
||||||
|
* @param {String} storagePath path of the current storage
|
||||||
|
* @returns {String[]} Absolute paths of the referenced attachments
|
||||||
|
*/
|
||||||
|
function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
|
||||||
|
const temp = getAttachmentsInContent(markdownContent)
|
||||||
|
const result = []
|
||||||
|
for (const relativePath of temp) {
|
||||||
|
result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Deletes all :storage and noteKey references from the given input.
|
||||||
|
* @param input Input in which the references should be deleted
|
||||||
|
* @param noteKey Key of the current note
|
||||||
|
* @returns {String} Input without the references
|
||||||
|
*/
|
||||||
|
function removeStorageAndNoteReferences (input, noteKey) {
|
||||||
|
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey, 'g'), DESTINATION_FOLDER)
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
copyAttachment,
|
copyAttachment,
|
||||||
fixLocalURLS,
|
fixLocalURLS,
|
||||||
generateAttachmentMarkdown,
|
generateAttachmentMarkdown,
|
||||||
handleAttachmentDrop,
|
handleAttachmentDrop,
|
||||||
handlePastImageEvent,
|
handlePastImageEvent,
|
||||||
|
getAttachmentsInContent,
|
||||||
|
getAbsolutePathsOfAttachmentsInContent,
|
||||||
|
removeStorageAndNoteReferences,
|
||||||
STORAGE_FOLDER_PLACEHOLDER,
|
STORAGE_FOLDER_PLACEHOLDER,
|
||||||
DESTINATION_FOLDER
|
DESTINATION_FOLDER
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
import copyFile from 'browser/main/lib/dataApi/copyFile'
|
||||||
import { findStorage } from 'browser/lib/findStorage'
|
import { findStorage } from 'browser/lib/findStorage'
|
||||||
import filenamify from 'filenamify'
|
|
||||||
|
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
|
|
||||||
const LOCAL_STORED_REGEX = /!\[(.*?)]\(\s*?\/:storage\/(.*\.\S*?)\)/gi
|
|
||||||
// TODO: ehhc: check this -> attachmentManagement
|
|
||||||
const IMAGES_FOLDER_NAME = 'images'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export note together with images
|
* Export note together with images
|
||||||
*
|
*
|
||||||
@@ -29,21 +24,7 @@ function exportNote (storageKey, noteContent, targetPath, outputFormatter) {
|
|||||||
throw new Error('Storage path is not found')
|
throw new Error('Storage path is not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
let exportedData = noteContent.replace(LOCAL_STORED_REGEX, (match, dstFilename, srcFilename) => {
|
let exportedData = noteContent
|
||||||
dstFilename = filenamify(dstFilename, {replacement: '_'})
|
|
||||||
if (!path.extname(dstFilename)) {
|
|
||||||
dstFilename += path.extname(srcFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
const dstRelativePath = path.join(IMAGES_FOLDER_NAME, dstFilename)
|
|
||||||
|
|
||||||
exportTasks.push({
|
|
||||||
src: path.join(IMAGES_FOLDER_NAME, srcFilename),
|
|
||||||
dst: dstRelativePath
|
|
||||||
})
|
|
||||||
|
|
||||||
return ``
|
|
||||||
})
|
|
||||||
|
|
||||||
if (outputFormatter) {
|
if (outputFormatter) {
|
||||||
exportedData = outputFormatter(exportedData, exportTasks)
|
exportedData = outputFormatter(exportedData, exportTasks)
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ exports[`TagListItem renders correctly 1`] = `
|
|||||||
# Test
|
# Test
|
||||||
<span
|
<span
|
||||||
className="tagList-item-count"
|
className="tagList-item-count"
|
||||||
>
|
/>
|
||||||
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -142,13 +142,13 @@ it('should replace the all ":storage" path with the actual storage path', functi
|
|||||||
' <body data-theme="default">\n' +
|
' <body data-theme="default">\n' +
|
||||||
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
' <p data-line="2">\n' +
|
' <p data-line="2">\n' +
|
||||||
' <img src="file:///' + storagePath + '\\' + storageFolder + '\\0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
' </p>\n' +
|
' </p>\n' +
|
||||||
' <p data-line="4">\n' +
|
' <p data-line="4">\n' +
|
||||||
' <a href="file:///' + storagePath + '\\' + storageFolder + '\\0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
' <a href="file:///' + storagePath + path.sep + storageFolder + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
' </p>\n' +
|
' </p>\n' +
|
||||||
' <p data-line="6">\n' +
|
' <p data-line="6">\n' +
|
||||||
' <img src="file:///' + storagePath + '\\' + storageFolder + '\\d6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
' </p>\n' +
|
' </p>\n' +
|
||||||
' </body>\n' +
|
' </body>\n' +
|
||||||
'</html>'
|
'</html>'
|
||||||
@@ -166,3 +166,97 @@ it('should test that generateAttachmentMarkdown works correct both with previews
|
|||||||
actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, false)
|
actual = systemUnderTest.generateAttachmentMarkdown(fileName, path, false)
|
||||||
expect(actual).toEqual(expected)
|
expect(actual).toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should test that getAttachmentsInContent finds all attachments', function () {
|
||||||
|
const testInput =
|
||||||
|
'<html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' //header\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body data-theme="default">\n' +
|
||||||
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
|
' <p data-line="2">\n' +
|
||||||
|
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="4">\n' +
|
||||||
|
' <a href=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="6">\n' +
|
||||||
|
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' </body>\n' +
|
||||||
|
'</html>'
|
||||||
|
const actual = systemUnderTest.getAttachmentsInContent(testInput)
|
||||||
|
const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
|
||||||
|
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute paths', function () {
|
||||||
|
const dummyStoragePath = 'dummyStoragePath'
|
||||||
|
const testInput =
|
||||||
|
'<html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' //header\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body data-theme="default">\n' +
|
||||||
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
|
' <p data-line="2">\n' +
|
||||||
|
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="4">\n' +
|
||||||
|
' <a href=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="6">\n' +
|
||||||
|
' <img src=":storage' + mdurl.encode(path.sep) + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' </body>\n' +
|
||||||
|
'</html>'
|
||||||
|
const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath)
|
||||||
|
const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp',
|
||||||
|
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx',
|
||||||
|
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
|
||||||
|
expect(actual).toEqual(expect.arrayContaining(expected))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should remove the all ":storage" and noteKey references', function () {
|
||||||
|
const storageFolder = systemUnderTest.DESTINATION_FOLDER
|
||||||
|
const noteKey = 'noteKey'
|
||||||
|
const testInput =
|
||||||
|
'<html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' //header\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body data-theme="default">\n' +
|
||||||
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
|
' <p data-line="2">\n' +
|
||||||
|
' <img src=":storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="4">\n' +
|
||||||
|
' <a href=":storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="6">\n' +
|
||||||
|
' <img src=":storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' </body>\n' +
|
||||||
|
'</html>'
|
||||||
|
const expectedOutput =
|
||||||
|
'<html>\n' +
|
||||||
|
' <head>\n' +
|
||||||
|
' //header\n' +
|
||||||
|
' </head>\n' +
|
||||||
|
' <body data-theme="default">\n' +
|
||||||
|
' <h2 data-line="0" id="Headline">Headline</h2>\n' +
|
||||||
|
' <p data-line="2">\n' +
|
||||||
|
' <img src="' + storageFolder + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="4">\n' +
|
||||||
|
' <a href="' + storageFolder + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' <p data-line="6">\n' +
|
||||||
|
' <img src="' + storageFolder + path.sep + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
|
||||||
|
' </p>\n' +
|
||||||
|
' </body>\n' +
|
||||||
|
'</html>'
|
||||||
|
const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey)
|
||||||
|
expect(actual).toEqual(expectedOutput)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user