mirror of
https://github.com/nomic-ai/gpt4all.git
synced 2025-09-16 15:58:36 +00:00
Add code blocks and python syntax highlighting.
This commit is contained in:
@@ -378,6 +378,12 @@ Window {
|
|||||||
text: qsTr("Conversation copied to clipboard.")
|
text: qsTr("Conversation copied to clipboard.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PopupDialog {
|
||||||
|
id: copyCodeMessage
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: qsTr("Code copied to clipboard.")
|
||||||
|
}
|
||||||
|
|
||||||
PopupDialog {
|
PopupDialog {
|
||||||
id: healthCheckFailed
|
id: healthCheckFailed
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -607,6 +613,10 @@ Window {
|
|||||||
var integer = parseInt(link.split("://")[1]);
|
var integer = parseInt(link.split("://")[1]);
|
||||||
referenceContextDialog.text = referencesContext[integer - 1];
|
referenceContextDialog.text = referencesContext[integer - 1];
|
||||||
referenceContextDialog.open();
|
referenceContextDialog.open();
|
||||||
|
} else {
|
||||||
|
var success = responseText.tryCopyAtPosition(clickedPos);
|
||||||
|
if (success)
|
||||||
|
copyCodeMessage.open();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -618,6 +628,7 @@ Window {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
responseText.textDocument = textDocument
|
responseText.textDocument = textDocument
|
||||||
responseText.setLinkColor(theme.linkColor);
|
responseText.setLinkColor(theme.linkColor);
|
||||||
|
responseText.setHeaderColor(theme.backgroundLight);
|
||||||
}
|
}
|
||||||
|
|
||||||
Accessible.role: Accessible.Paragraph
|
Accessible.role: Accessible.Paragraph
|
||||||
|
@@ -5,6 +5,92 @@
|
|||||||
#include <QTextDocumentFragment>
|
#include <QTextDocumentFragment>
|
||||||
#include <QFontMetricsF>
|
#include <QFontMetricsF>
|
||||||
#include <QTextTableCell>
|
#include <QTextTableCell>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QClipboard>
|
||||||
|
|
||||||
|
enum Language {
|
||||||
|
None,
|
||||||
|
Python
|
||||||
|
};
|
||||||
|
|
||||||
|
static QColor keywordColor = "#2e95d3"; // blue
|
||||||
|
static QColor functionColor = "#f22c3d"; // red
|
||||||
|
static QColor functionCallColor = "#e9950c"; // orange
|
||||||
|
static QColor commentColor = "#808080"; // gray
|
||||||
|
static QColor stringColor = "#00a37d"; // green
|
||||||
|
static QColor numberColor = "#df3079"; // fuschia
|
||||||
|
|
||||||
|
static Language stringToLanguage(const QString &language)
|
||||||
|
{
|
||||||
|
if (language == "python")
|
||||||
|
return Python;
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HighlightingRule {
|
||||||
|
QRegularExpression pattern;
|
||||||
|
QTextCharFormat format;
|
||||||
|
};
|
||||||
|
|
||||||
|
static QVector<HighlightingRule> pythonHighlightingRules()
|
||||||
|
{
|
||||||
|
static QVector<HighlightingRule> highlightingRules;
|
||||||
|
if (highlightingRules.isEmpty()) {
|
||||||
|
|
||||||
|
HighlightingRule rule;
|
||||||
|
|
||||||
|
QTextCharFormat functionCallFormat;
|
||||||
|
functionCallFormat.setForeground(functionCallColor);
|
||||||
|
rule.pattern = QRegularExpression("\\b(\\w+)\\s*(?=\\()");
|
||||||
|
rule.format = functionCallFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
QTextCharFormat functionFormat;
|
||||||
|
functionFormat.setForeground(functionColor);
|
||||||
|
rule.pattern = QRegularExpression("\\bdef\\s+(\\w+)\\b");
|
||||||
|
rule.format = functionFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
QTextCharFormat numberFormat;
|
||||||
|
numberFormat.setForeground(numberColor);
|
||||||
|
rule.pattern = QRegularExpression("\\b[0-9]*\\.?[0-9]+\\b");
|
||||||
|
rule.format = numberFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
QTextCharFormat keywordFormat;
|
||||||
|
keywordFormat.setForeground(keywordColor);
|
||||||
|
QStringList keywordPatterns = {
|
||||||
|
"\\bdef\\b", "\\bclass\\b", "\\bif\\b", "\\belse\\b", "\\belif\\b",
|
||||||
|
"\\bwhile\\b", "\\bfor\\b", "\\breturn\\b", "\\bprint\\b", "\\bimport\\b",
|
||||||
|
"\\bfrom\\b", "\\bas\\b", "\\btry\\b", "\\bexcept\\b", "\\braise\\b",
|
||||||
|
"\\bwith\\b", "\\bfinally\\b", "\\bcontinue\\b", "\\bbreak\\b", "\\bpass\\b"
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const QString &pattern : keywordPatterns) {
|
||||||
|
rule.pattern = QRegularExpression(pattern);
|
||||||
|
rule.format = keywordFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCharFormat stringFormat;
|
||||||
|
stringFormat.setForeground(stringColor);
|
||||||
|
rule.pattern = QRegularExpression("\".*?\"");
|
||||||
|
rule.format = stringFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
rule.pattern = QRegularExpression("\'.*?\'");
|
||||||
|
rule.format = stringFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
QTextCharFormat commentFormat;
|
||||||
|
commentFormat.setForeground(commentColor);
|
||||||
|
rule.pattern = QRegularExpression("#[^\n]*");
|
||||||
|
rule.format = commentFormat;
|
||||||
|
highlightingRules.append(rule);
|
||||||
|
|
||||||
|
}
|
||||||
|
return highlightingRules;
|
||||||
|
}
|
||||||
|
|
||||||
SyntaxHighlighter::SyntaxHighlighter(QObject *parent)
|
SyntaxHighlighter::SyntaxHighlighter(QObject *parent)
|
||||||
: QSyntaxHighlighter(parent)
|
: QSyntaxHighlighter(parent)
|
||||||
@@ -17,11 +103,19 @@ SyntaxHighlighter::~SyntaxHighlighter()
|
|||||||
|
|
||||||
void SyntaxHighlighter::highlightBlock(const QString &text)
|
void SyntaxHighlighter::highlightBlock(const QString &text)
|
||||||
{
|
{
|
||||||
for (const HighlightingRule &rule : qAsConst(m_highlightingRules)) {
|
QTextBlock block = this->currentBlock();
|
||||||
|
|
||||||
|
QVector<HighlightingRule> rules;
|
||||||
|
if (block.userState() == Python)
|
||||||
|
rules = pythonHighlightingRules();
|
||||||
|
|
||||||
|
for (const HighlightingRule &rule : qAsConst(rules)) {
|
||||||
QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text);
|
QRegularExpressionMatchIterator matchIterator = rule.pattern.globalMatch(text);
|
||||||
while (matchIterator.hasNext()) {
|
while (matchIterator.hasNext()) {
|
||||||
QRegularExpressionMatch match = matchIterator.next();
|
QRegularExpressionMatch match = matchIterator.next();
|
||||||
setFormat(match.capturedStart(), match.capturedLength(), rule.format);
|
int startIndex = match.capturedStart();
|
||||||
|
int length = match.capturedLength();
|
||||||
|
setFormat(startIndex, length, rule.format);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -59,6 +153,18 @@ QString ResponseText::getLinkAtPosition(int position) const
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ResponseText::tryCopyAtPosition(int position) const
|
||||||
|
{
|
||||||
|
for (const auto © : m_copies) {
|
||||||
|
if (position >= copy.startPos && position < copy.endPos) {
|
||||||
|
QClipboard *clipboard = QGuiApplication::clipboard();
|
||||||
|
clipboard->setText(copy.text);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void ResponseText::handleTextChanged()
|
void ResponseText::handleTextChanged()
|
||||||
{
|
{
|
||||||
if (!m_textDocument || m_isProcessingText)
|
if (!m_textDocument || m_isProcessingText)
|
||||||
@@ -66,25 +172,32 @@ void ResponseText::handleTextChanged()
|
|||||||
|
|
||||||
m_isProcessingText = true;
|
m_isProcessingText = true;
|
||||||
QTextDocument* doc = m_textDocument->textDocument();
|
QTextDocument* doc = m_textDocument->textDocument();
|
||||||
QTextCursor cursor(doc);
|
handleContextLinks();
|
||||||
|
handleCodeBlocks();
|
||||||
|
m_isProcessingText = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResponseText::handleContextLinks()
|
||||||
|
{
|
||||||
|
QTextDocument* doc = m_textDocument->textDocument();
|
||||||
|
QTextCursor cursor(doc);
|
||||||
QTextCharFormat linkFormat;
|
QTextCharFormat linkFormat;
|
||||||
linkFormat.setForeground(m_linkColor);
|
linkFormat.setForeground(m_linkColor);
|
||||||
linkFormat.setFontUnderline(true);
|
linkFormat.setFontUnderline(true);
|
||||||
|
|
||||||
// Loop through the document looking for context links
|
// Regex for context links
|
||||||
QRegularExpression re("\\[Context\\]\\((context://\\d+)\\)");
|
QRegularExpression reLink("\\[Context\\]\\((context://\\d+)\\)");
|
||||||
QRegularExpressionMatchIterator i = re.globalMatch(doc->toPlainText());
|
QRegularExpressionMatchIterator iLink = reLink.globalMatch(doc->toPlainText());
|
||||||
|
|
||||||
QList<QRegularExpressionMatch> matches;
|
QList<QRegularExpressionMatch> matchesLink;
|
||||||
while (i.hasNext())
|
while (iLink.hasNext())
|
||||||
matches.append(i.next());
|
matchesLink.append(iLink.next());
|
||||||
|
|
||||||
QVector<ContextLink> newLinks;
|
QVector<ContextLink> newLinks;
|
||||||
|
|
||||||
// Calculate new positions and store them in newLinks
|
// Calculate new positions and store them in newLinks
|
||||||
int positionOffset = 0;
|
int positionOffset = 0;
|
||||||
for(const auto &match : matches) {
|
for(const auto &match : matchesLink) {
|
||||||
ContextLink newLink;
|
ContextLink newLink;
|
||||||
newLink.href = match.captured(1);
|
newLink.href = match.captured(1);
|
||||||
newLink.text = "Context";
|
newLink.text = "Context";
|
||||||
@@ -95,9 +208,9 @@ void ResponseText::handleTextChanged()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace the context links with the word "Context" in reverse order
|
// Replace the context links with the word "Context" in reverse order
|
||||||
for(int index = matches.count() - 1; index >= 0; --index) {
|
for(int index = matchesLink.count() - 1; index >= 0; --index) {
|
||||||
cursor.setPosition(matches.at(index).capturedStart());
|
cursor.setPosition(matchesLink.at(index).capturedStart());
|
||||||
cursor.setPosition(matches.at(index).capturedEnd(), QTextCursor::KeepAnchor);
|
cursor.setPosition(matchesLink.at(index).capturedEnd(), QTextCursor::KeepAnchor);
|
||||||
cursor.removeSelectedText();
|
cursor.removeSelectedText();
|
||||||
cursor.setCharFormat(linkFormat);
|
cursor.setCharFormat(linkFormat);
|
||||||
cursor.insertText(newLinks.at(index).text);
|
cursor.insertText(newLinks.at(index).text);
|
||||||
@@ -105,5 +218,131 @@ void ResponseText::handleTextChanged()
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_links = newLinks;
|
m_links = newLinks;
|
||||||
m_isProcessingText = false;
|
}
|
||||||
|
|
||||||
|
void ResponseText::handleCodeBlocks()
|
||||||
|
{
|
||||||
|
QTextDocument* doc = m_textDocument->textDocument();
|
||||||
|
QTextCursor cursor(doc);
|
||||||
|
|
||||||
|
QTextCharFormat textFormat;
|
||||||
|
textFormat.setFontFamilies(QStringList() << "Monospace");
|
||||||
|
textFormat.setForeground(QColor("white"));
|
||||||
|
|
||||||
|
QTextFrameFormat frameFormatBase;
|
||||||
|
frameFormatBase.setBackground(QColor("black"));
|
||||||
|
|
||||||
|
QTextTableFormat tableFormat;
|
||||||
|
tableFormat.setMargin(0);
|
||||||
|
tableFormat.setPadding(0);
|
||||||
|
tableFormat.setBorder(0);
|
||||||
|
tableFormat.setBorderCollapse(true);
|
||||||
|
QList<QTextLength> constraints;
|
||||||
|
constraints << QTextLength(QTextLength::PercentageLength, 100);
|
||||||
|
tableFormat.setColumnWidthConstraints(constraints);
|
||||||
|
|
||||||
|
QTextTableFormat headerTableFormat;
|
||||||
|
headerTableFormat.setBackground(m_headerColor);
|
||||||
|
headerTableFormat.setPadding(0);
|
||||||
|
headerTableFormat.setBorder(0);
|
||||||
|
headerTableFormat.setBorderCollapse(true);
|
||||||
|
headerTableFormat.setTopMargin(15);
|
||||||
|
headerTableFormat.setBottomMargin(15);
|
||||||
|
headerTableFormat.setLeftMargin(30);
|
||||||
|
headerTableFormat.setRightMargin(30);
|
||||||
|
QList<QTextLength> headerConstraints;
|
||||||
|
headerConstraints << QTextLength(QTextLength::PercentageLength, 80);
|
||||||
|
headerConstraints << QTextLength(QTextLength::PercentageLength, 20);
|
||||||
|
headerTableFormat.setColumnWidthConstraints(headerConstraints);
|
||||||
|
|
||||||
|
QTextTableFormat codeBlockTableFormat;
|
||||||
|
codeBlockTableFormat.setBackground(QColor("black"));
|
||||||
|
codeBlockTableFormat.setPadding(0);
|
||||||
|
codeBlockTableFormat.setBorder(0);
|
||||||
|
codeBlockTableFormat.setBorderCollapse(true);
|
||||||
|
codeBlockTableFormat.setTopMargin(30);
|
||||||
|
codeBlockTableFormat.setBottomMargin(30);
|
||||||
|
codeBlockTableFormat.setLeftMargin(30);
|
||||||
|
codeBlockTableFormat.setRightMargin(30);
|
||||||
|
codeBlockTableFormat.setColumnWidthConstraints(constraints);
|
||||||
|
|
||||||
|
QTextImageFormat copyImageFormat;
|
||||||
|
copyImageFormat.setWidth(30);
|
||||||
|
copyImageFormat.setHeight(30);
|
||||||
|
copyImageFormat.setName("qrc:/gpt4all/icons/copy.svg");
|
||||||
|
|
||||||
|
// Regex for code blocks
|
||||||
|
QRegularExpression reCode("```(.*?)(```|$)", QRegularExpression::DotMatchesEverythingOption);
|
||||||
|
QRegularExpressionMatchIterator iCode = reCode.globalMatch(doc->toPlainText());
|
||||||
|
|
||||||
|
QList<QRegularExpressionMatch> matchesCode;
|
||||||
|
while (iCode.hasNext())
|
||||||
|
matchesCode.append(iCode.next());
|
||||||
|
|
||||||
|
QVector<CodeCopy> newCopies;
|
||||||
|
|
||||||
|
for(int index = matchesCode.count() - 1; index >= 0; --index) {
|
||||||
|
cursor.setPosition(matchesCode[index].capturedStart());
|
||||||
|
cursor.setPosition(matchesCode[index].capturedEnd(), QTextCursor::KeepAnchor);
|
||||||
|
cursor.removeSelectedText();
|
||||||
|
|
||||||
|
// Check if the first word in the code block is "python"
|
||||||
|
QTextFrameFormat frameFormat = frameFormatBase;
|
||||||
|
QStringList lines = matchesCode[index].captured(1).split('\n');
|
||||||
|
QString codeLanguage;
|
||||||
|
if (!lines.empty() && lines[0].trimmed() == "python")
|
||||||
|
codeLanguage = lines.takeFirst().trimmed();
|
||||||
|
\
|
||||||
|
QTextFrame *mainFrame = cursor.currentFrame();
|
||||||
|
cursor.setCharFormat(textFormat);
|
||||||
|
|
||||||
|
QTextFrame *frame = cursor.insertFrame(frameFormat);
|
||||||
|
QTextTable *table = cursor.insertTable(codeLanguage.isEmpty() ? 1 : 2, 1, tableFormat);
|
||||||
|
|
||||||
|
if (!codeLanguage.isEmpty()) {
|
||||||
|
QTextTableCell headerCell = table->cellAt(0, 0);
|
||||||
|
QTextCursor headerCellCursor = headerCell.firstCursorPosition();
|
||||||
|
QTextTable *headerTable = headerCellCursor.insertTable(1, 2, headerTableFormat);
|
||||||
|
QTextTableCell header = headerTable->cellAt(0, 0);
|
||||||
|
QTextCursor headerCursor = header.firstCursorPosition();
|
||||||
|
headerCursor.insertText(codeLanguage);
|
||||||
|
QTextTableCell copy = headerTable->cellAt(0, 1);
|
||||||
|
QTextCursor copyCursor = copy.firstCursorPosition();
|
||||||
|
int startPos = copyCursor.position();
|
||||||
|
CodeCopy newCopy;
|
||||||
|
newCopy.text = lines.join("\n");
|
||||||
|
newCopy.startPos = copyCursor.position();
|
||||||
|
newCopy.endPos = newCopy.startPos + 1;
|
||||||
|
newCopies.append(newCopy);
|
||||||
|
QTextBlockFormat blockFormat;
|
||||||
|
blockFormat.setAlignment(Qt::AlignRight);
|
||||||
|
copyCursor.setBlockFormat(blockFormat);
|
||||||
|
copyCursor.insertImage(copyImageFormat, QTextFrameFormat::FloatRight);
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextTableCell codeCell = table->cellAt(codeLanguage.isEmpty() ? 0 : 1, 0);
|
||||||
|
QTextCursor codeCellCursor = codeCell.firstCursorPosition();
|
||||||
|
QTextTable *codeTable = codeCellCursor.insertTable(1, 1, codeBlockTableFormat);
|
||||||
|
QTextTableCell code = codeTable->cellAt(0, 0);
|
||||||
|
QTextCursor codeCursor = code.firstCursorPosition();
|
||||||
|
if (!codeLanguage.isEmpty()) {
|
||||||
|
codeCursor.block().setUserState(stringToLanguage(codeLanguage));
|
||||||
|
for (const QString &line : lines) {
|
||||||
|
codeCursor.insertText(line);
|
||||||
|
codeCursor.insertBlock();
|
||||||
|
codeCursor.block().setUserState(stringToLanguage(codeLanguage));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
codeCursor.insertText(lines.join("\n"));
|
||||||
|
}
|
||||||
|
if (codeCursor.position() > 0) {
|
||||||
|
codeCursor.setPosition(codeCursor.position() - 1);
|
||||||
|
codeCursor.deleteChar();
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = mainFrame->lastCursorPosition();
|
||||||
|
cursor.setCharFormat(QTextCharFormat());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_copies = newCopies;
|
||||||
}
|
}
|
||||||
|
@@ -7,22 +7,12 @@
|
|||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
|
||||||
struct HighlightingRule
|
|
||||||
{
|
|
||||||
QRegularExpression pattern;
|
|
||||||
QTextCharFormat format;
|
|
||||||
};
|
|
||||||
|
|
||||||
class SyntaxHighlighter : public QSyntaxHighlighter {
|
class SyntaxHighlighter : public QSyntaxHighlighter {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
SyntaxHighlighter(QObject *parent);
|
SyntaxHighlighter(QObject *parent);
|
||||||
~SyntaxHighlighter();
|
~SyntaxHighlighter();
|
||||||
|
|
||||||
void highlightBlock(const QString &text) override;
|
void highlightBlock(const QString &text) override;
|
||||||
|
|
||||||
private:
|
|
||||||
QVector<HighlightingRule> m_highlightingRules;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ContextLink {
|
struct ContextLink {
|
||||||
@@ -32,6 +22,12 @@ struct ContextLink {
|
|||||||
QString href;
|
QString href;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CodeCopy {
|
||||||
|
int startPos = -1;
|
||||||
|
int endPos = -1;
|
||||||
|
QString text;
|
||||||
|
};
|
||||||
|
|
||||||
class ResponseText : public QObject
|
class ResponseText : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -44,19 +40,26 @@ public:
|
|||||||
void setTextDocument(QQuickTextDocument* textDocument);
|
void setTextDocument(QQuickTextDocument* textDocument);
|
||||||
|
|
||||||
Q_INVOKABLE void setLinkColor(const QColor &c) { m_linkColor = c; }
|
Q_INVOKABLE void setLinkColor(const QColor &c) { m_linkColor = c; }
|
||||||
|
Q_INVOKABLE void setHeaderColor(const QColor &c) { m_headerColor = c; }
|
||||||
|
|
||||||
Q_INVOKABLE QString getLinkAtPosition(int position) const;
|
Q_INVOKABLE QString getLinkAtPosition(int position) const;
|
||||||
|
Q_INVOKABLE bool tryCopyAtPosition(int position) const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void textDocumentChanged();
|
void textDocumentChanged();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void handleTextChanged();
|
void handleTextChanged();
|
||||||
|
void handleContextLinks();
|
||||||
|
void handleCodeBlocks();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QQuickTextDocument *m_textDocument;
|
QQuickTextDocument *m_textDocument;
|
||||||
SyntaxHighlighter *m_syntaxHighlighter;
|
SyntaxHighlighter *m_syntaxHighlighter;
|
||||||
QVector<ContextLink> m_links;
|
QVector<ContextLink> m_links;
|
||||||
|
QVector<CodeCopy> m_copies;
|
||||||
QColor m_linkColor;
|
QColor m_linkColor;
|
||||||
|
QColor m_headerColor;
|
||||||
bool m_isProcessingText = false;
|
bool m_isProcessingText = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user