mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 05:28:59 +01:00 
			
		
		
		
	Merge pull request #3988 from SiriusXT/Highlighted-Text
Show highlighted text in the right pane
This commit is contained in:
		
						commit
						eff3e1df85
					
				@ -44,6 +44,7 @@ import BacklinksWidget from "../widgets/floating_buttons/zpetne_odkazy.js";
 | 
			
		||||
import SharedInfoWidget from "../widgets/shared_info.js";
 | 
			
		||||
import FindWidget from "../widgets/find.js";
 | 
			
		||||
import TocWidget from "../widgets/toc.js";
 | 
			
		||||
import HighlightedTextWidget from "../widgets/highlighted_text.js";
 | 
			
		||||
import BulkActionsDialog from "../widgets/dialogs/bulk_actions.js";
 | 
			
		||||
import AboutDialog from "../widgets/dialogs/about.js";
 | 
			
		||||
import HelpDialog from "../widgets/dialogs/help.js";
 | 
			
		||||
@ -184,6 +185,7 @@ export default class DesktopLayout {
 | 
			
		||||
                    )
 | 
			
		||||
                    .child(new RightPaneContainer()
 | 
			
		||||
                        .child(new TocWidget())
 | 
			
		||||
                        .child(new HighlightedTextWidget())
 | 
			
		||||
                        .child(...this.customWidgets.get('right-pane'))
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										255
									
								
								src/public/app/widgets/highlighted_text.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								src/public/app/widgets/highlighted_text.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,255 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Widget: Show highlighted text in the right pane
 | 
			
		||||
 *
 | 
			
		||||
 * By design there's no support for nonsensical or malformed constructs:
 | 
			
		||||
 * - For example, if there is a formula in the middle of the highlighted text, the two ends of the formula will be regarded as two entries
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import attributeService from "../services/attributes.js";
 | 
			
		||||
import RightPanelWidget from "./right_panel_widget.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
import OnClickButtonWidget from "./buttons/onclick_button.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `<div class="highlighted-text-widget">
 | 
			
		||||
    <style>
 | 
			
		||||
        .highlighted-text-widget {
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            contain: none; 
 | 
			
		||||
            overflow: auto;
 | 
			
		||||
            position: relative;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .highlighted-text > ol {
 | 
			
		||||
            padding-left: 20px;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .highlighted-text li {
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            margin-bottom: 3px;
 | 
			
		||||
            text-align: justify;
 | 
			
		||||
            text-justify: distribute;
 | 
			
		||||
            word-wrap: break-word;
 | 
			
		||||
            hyphens: auto;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .highlighted-text li:hover {
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .close-highlighted-text {
 | 
			
		||||
            position: absolute;
 | 
			
		||||
            top: 2px;
 | 
			
		||||
            right: 2px;
 | 
			
		||||
        }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <span class="highlighted-text"></span>
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class HighlightedTextWidget extends RightPanelWidget {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.closeHltButton = new CloseHltButton();
 | 
			
		||||
        this.child(this.closeHltButton);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    get widgetTitle() {
 | 
			
		||||
        return "Highlighted Text";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
        return super.isEnabled()
 | 
			
		||||
            && this.note.type === 'text'
 | 
			
		||||
            && !this.noteContext.viewScope.highlightedTextTemporarilyHidden
 | 
			
		||||
            && this.noteContext.viewScope.viewMode === 'default';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async doRenderBody() {
 | 
			
		||||
        this.$body.empty().append($(TPL));
 | 
			
		||||
        this.$hlt = this.$body.find('.highlighted-text');
 | 
			
		||||
        this.$body.find('.highlighted-text-widget').append(this.closeHltButton.render());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note) {
 | 
			
		||||
        /*The reason for adding highlightedTextPreviousVisible is to record whether the previous state of the highlightedText is hidden or displayed, 
 | 
			
		||||
        * and then let it be displayed/hidden at the initial time. 
 | 
			
		||||
        * If there is no such value, when the right panel needs to display toc but not highlighttext, every time the note content is changed, 
 | 
			
		||||
        * highlighttext Widget will appear and then close immediately, because getHlt function will consume time*/
 | 
			
		||||
        if (this.noteContext.viewScope.highlightedTextPreviousVisible == true) {
 | 
			
		||||
            this.toggleInt(true);
 | 
			
		||||
        } else {
 | 
			
		||||
            this.toggleInt(false);
 | 
			
		||||
        }
 | 
			
		||||
        const hltLabel = note.getLabel('hideHighlightWidget');
 | 
			
		||||
 | 
			
		||||
        const optionsHlt = JSON.parse(options.get('highlightedText'));
 | 
			
		||||
 | 
			
		||||
        if (hltLabel?.value == "" || hltLabel?.value === "true" || optionsHlt == "") {
 | 
			
		||||
            this.toggleInt(false);
 | 
			
		||||
            this.triggerCommand("reEvaluateRightPaneVisibility");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let $hlt = "", hltLiCount = -1;
 | 
			
		||||
        // Check for type text unconditionally in case alwaysShowWidget is set
 | 
			
		||||
        if (this.note.type === 'text') {
 | 
			
		||||
            const { content } = await note.getNoteComplement();
 | 
			
		||||
            ({ $hlt, hltLiCount } = await this.getHlt(content, optionsHlt));
 | 
			
		||||
        }
 | 
			
		||||
        this.$hlt.html($hlt);
 | 
			
		||||
        if ([undefined, "false"].includes(hltLabel?.value) && hltLiCount > 0) {
 | 
			
		||||
            this.toggleInt(true);
 | 
			
		||||
            this.noteContext.viewScope.highlightedTextPreviousVisible = true;
 | 
			
		||||
        } else {
 | 
			
		||||
            this.toggleInt(false);
 | 
			
		||||
            this.noteContext.viewScope.highlightedTextPreviousVisible = false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.triggerCommand("reEvaluateRightPaneVisibility");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Builds a table of helight text.      
 | 
			
		||||
     */
 | 
			
		||||
    getHlt(html, optionsHlt) {
 | 
			
		||||
        // matches a span containing background-color
 | 
			
		||||
        const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi; 
 | 
			
		||||
        // matches a span containing color
 | 
			
		||||
        const regex2 = /<span[^>]*style\s*=\s*[^>]*[^-]color:[^>]*?>[\s\S]*?<\/span>/gi;
 | 
			
		||||
        // match italics
 | 
			
		||||
        const regex3 = /<i>[\s\S]*?<\/i>/gi;
 | 
			
		||||
        // match bold
 | 
			
		||||
        const regex4 = /<strong>[\s\S]*?<\/strong>/gi;
 | 
			
		||||
        // match underline
 | 
			
		||||
        const regex5 = /<u>[\s\S]*?<\/u>/g;
 | 
			
		||||
        // Possible values in optionsHlt: '["bold","italic","underline","color","bgColor"]'
 | 
			
		||||
        // element priority: span>i>strong>u
 | 
			
		||||
        let findSubStr="", combinedRegexStr = "";
 | 
			
		||||
        if (optionsHlt.indexOf("bgColor") >= 0){
 | 
			
		||||
            findSubStr+=`,span[style*="background-color"]`;
 | 
			
		||||
            combinedRegexStr+=`|${regex1.source}`;
 | 
			
		||||
        }
 | 
			
		||||
        if (optionsHlt.indexOf("color") >= 0){
 | 
			
		||||
            findSubStr+=`,span[style*="color"]`;
 | 
			
		||||
            combinedRegexStr+=`|${regex2.source}`;
 | 
			
		||||
        }
 | 
			
		||||
        if (optionsHlt.indexOf("italic") >= 0){
 | 
			
		||||
            findSubStr+=`,i`;
 | 
			
		||||
            combinedRegexStr+=`|${regex3.source}`;
 | 
			
		||||
        }
 | 
			
		||||
        if (optionsHlt.indexOf("bold") >= 0){
 | 
			
		||||
            findSubStr+=`,strong`;
 | 
			
		||||
            combinedRegexStr+=`|${regex4.source}`;
 | 
			
		||||
        }
 | 
			
		||||
        if (optionsHlt.indexOf("underline") >= 0){
 | 
			
		||||
            findSubStr+=`,u`;
 | 
			
		||||
            combinedRegexStr+=`|${regex5.source}`;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        findSubStr = findSubStr.substring(1)
 | 
			
		||||
        combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
 | 
			
		||||
        const combinedRegex = new RegExp(combinedRegexStr, 'gi');
 | 
			
		||||
        let $hlt = $("<ol>");
 | 
			
		||||
        let prevEndIndex = -1, hltLiCount = 0;
 | 
			
		||||
        for (let match = null, hltIndex=0; ((match = combinedRegex.exec(html)) !== null); hltIndex++) {
 | 
			
		||||
            var subHtml = match[0];
 | 
			
		||||
            const startIndex = match.index;
 | 
			
		||||
            const endIndex = combinedRegex.lastIndex;
 | 
			
		||||
            if (prevEndIndex != -1 && startIndex === prevEndIndex) {
 | 
			
		||||
                //If the previous element is connected to this element in HTML, then concatenate them into one.
 | 
			
		||||
                $hlt.children().last().append(subHtml);
 | 
			
		||||
            } else {
 | 
			
		||||
                //hide li if its text content is empty
 | 
			
		||||
                if ([...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim() != ""){
 | 
			
		||||
                    var $li = $('<li>');
 | 
			
		||||
                    $li.html(subHtml);
 | 
			
		||||
                    $li.on("click", () => this.jumpToHlt(findSubStr,hltIndex));
 | 
			
		||||
                    $hlt.append($li);
 | 
			
		||||
                    hltLiCount++;
 | 
			
		||||
                }else{
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            prevEndIndex = endIndex;
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
            $hlt,
 | 
			
		||||
            hltLiCount
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
    async jumpToHlt(findSubStr,hltIndex) {
 | 
			
		||||
        const isReadOnly = await this.noteContext.isReadOnly();
 | 
			
		||||
        let targetElement;
 | 
			
		||||
        if (isReadOnly) {
 | 
			
		||||
            const $container = await this.noteContext.getContentElement();
 | 
			
		||||
            targetElement=$container.find(findSubStr).filter(function() {
 | 
			
		||||
                if (findSubStr.indexOf("color")>=0 &&  findSubStr.indexOf("background-color")<0){
 | 
			
		||||
                    let color = this.style.color;
 | 
			
		||||
                    return $(this).prop('tagName')=="SPAN" && color==""?false:true;
 | 
			
		||||
                }else{
 | 
			
		||||
                    return true;
 | 
			
		||||
                }                
 | 
			
		||||
            }).filter(function() {
 | 
			
		||||
                return $(this).parent(findSubStr).length === 0 
 | 
			
		||||
                && $(this).parent().parent(findSubStr).length === 0
 | 
			
		||||
                && $(this).parent().parent().parent(findSubStr).length === 0
 | 
			
		||||
                && $(this).parent().parent().parent().parent(findSubStr).length === 0;
 | 
			
		||||
            })
 | 
			
		||||
        } else {
 | 
			
		||||
            const textEditor = await this.noteContext.getTextEditor();
 | 
			
		||||
            targetElement=$(textEditor.editing.view.domRoots.values().next().value).find(findSubStr).filter(function() {
 | 
			
		||||
                // When finding span[style*="color"] but not looking for span[style*="background-color"], 
 | 
			
		||||
                // the background-color error will be regarded as color, so it needs to be filtered
 | 
			
		||||
                if (findSubStr.indexOf("color")>=0 &&  findSubStr.indexOf("background-color")<0){
 | 
			
		||||
                    let color = this.style.color;
 | 
			
		||||
                    return $(this).prop('tagName')=="SPAN" && color==""?false:true;
 | 
			
		||||
                }else{
 | 
			
		||||
                    return true;
 | 
			
		||||
                }                
 | 
			
		||||
            }).filter(function() {
 | 
			
		||||
                //Need to filter out the child elements of the element that has been found
 | 
			
		||||
                return $(this).parent(findSubStr).length === 0 
 | 
			
		||||
                && $(this).parent().parent(findSubStr).length === 0
 | 
			
		||||
                && $(this).parent().parent().parent(findSubStr).length === 0
 | 
			
		||||
                && $(this).parent().parent().parent().parent(findSubStr).length === 0;
 | 
			
		||||
            })
 | 
			
		||||
        }
 | 
			
		||||
        targetElement[hltIndex].scrollIntoView({
 | 
			
		||||
            behavior: "smooth", block: "center"
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async closeHltCommand() {
 | 
			
		||||
        this.noteContext.viewScope.highlightedTextTemporarilyHidden = true;
 | 
			
		||||
        await this.refresh();
 | 
			
		||||
        this.triggerCommand('reEvaluateRightPaneVisibility');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.isNoteContentReloaded(this.noteId)) {
 | 
			
		||||
            await this.refresh();
 | 
			
		||||
        } else if (loadResults.getAttributes().find(attr => attr.type === 'label'
 | 
			
		||||
            && (attr.name.toLowerCase().includes('readonly') || attr.name === 'hideHighlightWidget')
 | 
			
		||||
            && attributeService.isAffecting(attr, this.note))) {
 | 
			
		||||
            await this.refresh();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CloseHltButton extends OnClickButtonWidget {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.icon("bx-x")
 | 
			
		||||
            .title("Close HighlightedTextWidget")
 | 
			
		||||
            .titlePlacement("bottom")
 | 
			
		||||
            .onClick((widget, e) => {
 | 
			
		||||
                e.stopPropagation();
 | 
			
		||||
 | 
			
		||||
                widget.triggerCommand("closeHlt");
 | 
			
		||||
            })
 | 
			
		||||
            .class("icon-action close-highlighted-text");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -38,6 +38,10 @@ const TPL = `<div class="toc-widget">
 | 
			
		||||
        
 | 
			
		||||
        .toc li {
 | 
			
		||||
            cursor: pointer;
 | 
			
		||||
            text-align: justify;
 | 
			
		||||
            text-justify: distribute;
 | 
			
		||||
            word-wrap: break-word;
 | 
			
		||||
            hyphens: auto;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .toc li:hover {
 | 
			
		||||
@ -80,6 +84,16 @@ export default class TocWidget extends RightPanelWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refreshWithNote(note) {
 | 
			
		||||
        /*The reason for adding tocPreviousVisible is to record whether the previous state of the toc is hidden or displayed, 
 | 
			
		||||
        * and then let it be displayed/hidden at the initial time. If there is no such value, 
 | 
			
		||||
        * when the right panel needs to display highlighttext but not toc, every time the note content is changed, 
 | 
			
		||||
        * toc will appear and then close immediately, because getToc(html) function will consume time*/
 | 
			
		||||
        if (this.noteContext.viewScope.tocPreviousVisible ==true){
 | 
			
		||||
            this.toggleInt(true);
 | 
			
		||||
        }else{
 | 
			
		||||
            this.toggleInt(false);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const tocLabel = note.getLabel('toc');
 | 
			
		||||
 | 
			
		||||
        if (tocLabel?.value === 'hide') {
 | 
			
		||||
@ -96,10 +110,13 @@ export default class TocWidget extends RightPanelWidget {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$toc.html($toc);
 | 
			
		||||
        this.toggleInt(
 | 
			
		||||
            ["", "show"].includes(tocLabel?.value)
 | 
			
		||||
            || headingCount >= options.getInt('minTocHeadings')
 | 
			
		||||
        );
 | 
			
		||||
        if (["", "show"].includes(tocLabel?.value) || headingCount >= options.getInt('minTocHeadings')){
 | 
			
		||||
            this.toggleInt(true);
 | 
			
		||||
            this.noteContext.viewScope.tocPreviousVisible=true;  
 | 
			
		||||
        }else{
 | 
			
		||||
            this.toggleInt(false);
 | 
			
		||||
            this.noteContext.viewScope.tocPreviousVisible=false;  
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.triggerCommand("reEvaluateRightPaneVisibility");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
 | 
			
		||||
import KeyboardShortcutsOptions from "./options/shortcuts.js";
 | 
			
		||||
import HeadingStyleOptions from "./options/text_notes/heading_style.js";
 | 
			
		||||
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
 | 
			
		||||
import HighlightedTextOptions from "./options/text_notes/highlighted_text.js";
 | 
			
		||||
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
 | 
			
		||||
import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js";
 | 
			
		||||
import WrapLinesOptions from "./options/code_notes/wrap_lines.js";
 | 
			
		||||
@ -61,6 +62,7 @@ const CONTENT_WIDGETS = {
 | 
			
		||||
    _optionsTextNotes: [
 | 
			
		||||
        HeadingStyleOptions,
 | 
			
		||||
        TableOfContentsOptions,
 | 
			
		||||
        HighlightedTextOptions,
 | 
			
		||||
        TextAutoReadOnlySizeOptions
 | 
			
		||||
    ],
 | 
			
		||||
    _optionsCodeNotes: [
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,38 @@
 | 
			
		||||
import OptionsWidget from "../options_widget.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="options-section">
 | 
			
		||||
    <h4>Highlighted Text</h4>
 | 
			
		||||
 | 
			
		||||
    You can customize the highlighted text displayed in the right panel:<br>
 | 
			
		||||
 | 
			
		||||
<label><input type="checkbox" class="highlighted-text-check" value="bold"> Bold font  </label>
 | 
			
		||||
<label><input type="checkbox" class="highlighted-text-check" value="italic"> Italic font  </label>
 | 
			
		||||
<label><input type="checkbox" class="highlighted-text-check" value="underline"> Underlined font  </label>
 | 
			
		||||
<label><input type="checkbox" class="highlighted-text-check" value="color"> Font with color  </label>
 | 
			
		||||
<label><input type="checkbox" class="highlighted-text-check" value="bgColor"> Font with background color  </label>
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class HighlightedTextOptions extends OptionsWidget {
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$hlt = this.$widget.find("input.highlighted-text-check");
 | 
			
		||||
        this.$hlt.on('change', () => {
 | 
			
		||||
            const hltVals=this.$widget.find('input.highlighted-text-check[type="checkbox"]:checked').map(function() {
 | 
			
		||||
                return this.value;
 | 
			
		||||
              }).get();
 | 
			
		||||
            this.updateOption('highlightedText', JSON.stringify(hltVals));
 | 
			
		||||
            });        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async optionsLoaded(options) {
 | 
			
		||||
        const hltVals=JSON.parse(options.highlightedText);
 | 
			
		||||
        this.$widget.find('input.highlighted-text-check[type="checkbox"]').each(function () {
 | 
			
		||||
            if ($.inArray($(this).val(), hltVals) !== -1) {
 | 
			
		||||
                    $(this).prop("checked", true);
 | 
			
		||||
            } else {
 | 
			
		||||
                    $(this).prop("checked", false);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -60,6 +60,7 @@ const ALLOWED_OPTIONS = new Set([
 | 
			
		||||
    'compressImages',
 | 
			
		||||
    'downloadImagesAutomatically',
 | 
			
		||||
    'minTocHeadings',
 | 
			
		||||
    'highlightedText',
 | 
			
		||||
    'checkForUpdates',
 | 
			
		||||
    'disableTray',
 | 
			
		||||
    'customSearchEngineName',
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,7 @@ const defaultOptions = [
 | 
			
		||||
    { name: 'compressImages', value: 'true', isSynced: true },
 | 
			
		||||
    { name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
 | 
			
		||||
    { name: 'minTocHeadings', value: '5', isSynced: true },
 | 
			
		||||
    { name: 'highlightedText', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
 | 
			
		||||
    { name: 'checkForUpdates', value: 'true', isSynced: true },
 | 
			
		||||
    { name: 'disableTray', value: 'false', isSynced: false },
 | 
			
		||||
    { name: 'customSearchEngineName', value: 'Duckduckgo', isSynced: false },
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user