diff --git a/ChangeLog b/ChangeLog
index edd0e00d8b..671bee51d9 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -4,6 +4,7 @@
* Added animation timeline editor
* Added screen blend mode
* Disabled built-in server pending rewrite
+ * Added `/alert` chat command
2021-09-12 Version 2.1.20
* Updated Portugese translations
diff --git a/src/desktop/chat/chatbox.cpp b/src/desktop/chat/chatbox.cpp
index ef7171a866..df7a1ab06e 100644
--- a/src/desktop/chat/chatbox.cpp
+++ b/src/desktop/chat/chatbox.cpp
@@ -59,6 +59,11 @@ ChatBox::ChatBox(Document *doc, QWidget *parent)
connect(m_chatWidget, &ChatWidget::message, this, &ChatBox::message);
connect(m_chatWidget, &ChatWidget::detachRequested, this, &ChatBox::detachFromParent);
+ connect(m_chatWidget, &ChatWidget::expandRequested, this, [this]() {
+ if(m_state == State::Collapsed) {
+ emit expandPlease();
+ }
+ });
connect(doc, &Document::canvasChanged, this, &ChatBox::onCanvasChanged);
connect(doc, &Document::serverLoggedIn, this, &ChatBox::onServerLogin);
diff --git a/src/desktop/chat/chatbox.h b/src/desktop/chat/chatbox.h
index 6c7c944f6a..21c0dd05f2 100644
--- a/src/desktop/chat/chatbox.h
+++ b/src/desktop/chat/chatbox.h
@@ -60,6 +60,9 @@ private slots:
//! The chatbox was either expanded or collapsed
void expandedChanged(bool isExpanded);
+ //! Request that the chatbox be expanded
+ void expandPlease();
+
//! Detached chat box should be re-attached and reparented (or it will be destroyed)
void reattachNowPlease();
diff --git a/src/desktop/chat/chatwidget.cpp b/src/desktop/chat/chatwidget.cpp
index addda847bc..289cc75a7a 100644
--- a/src/desktop/chat/chatwidget.cpp
+++ b/src/desktop/chat/chatwidget.cpp
@@ -62,6 +62,7 @@ struct Chat {
"color: #eff0f1;"
"margin: 1px 0 1px 0"
"}"
+ ".alert { background: #da4453 }"
".shout { background: #34292c }"
".shout .tab { background: #da4453 }"
".action { font-style: italic }"
@@ -71,6 +72,7 @@ struct Chat {
".op { color: #f47750 }"
".mod { color: #ed1515 }"
".timestamp { color: #8d8d8d }"
+ ".alert .timestamp { color: #eff0f1 }"
"a:link { color: #1d99f3 }"
);
}
@@ -79,6 +81,7 @@ struct Chat {
void appendMessage(int userId, const QString &usernameSpan, const QString &message, bool shout);
void appendMessageCompact(int userId, const QString &usernameSpan, const QString &message, bool shout);
void appendAction(const QString &usernameSpan, const QString &message);
+ void appendAlert(const QString &usernameSpan, const QString &message);
void appendNotification(const QString &message);
};
@@ -418,6 +421,32 @@ void Chat::appendMessage(int userId, const QString &usernameSpan, const QString
lastMessageTs = ts;
}
+void Chat::appendAlert(const QString &usernameSpan, const QString &message)
+{
+ QTextCursor cursor(doc);
+ cursor.movePosition(QTextCursor::End);
+
+ lastAppendedId = -2;
+
+ cursor.insertHtml(QStringLiteral(
+ "
"
+ ""
+ " | "
+ "%1 | "
+ "%2 | "
+ "
"
+ ""
+ "%3 | "
+ "
"
+ "
"
+ ).arg(
+ usernameSpan,
+ timestamp(),
+ htmlutils::newlineToBr(message)
+ )
+ );
+}
+
void Chat::appendAction(const QString &usernameSpan, const QString &message)
{
QTextCursor cursor(doc);
@@ -557,15 +586,15 @@ void ChatWidget::receiveMessage(int sender, int recipient, uint8_t tflags, uint8
Q_ASSERT(d->chats.contains(chatId));
Chat &chat = d->chats[chatId];
- if(oflags & rustpile::ChatMessage_OFLAGS_ACTION) {
+ if(tflags & rustpile::ChatMessage_TFLAGS_ALERT) {
+ chat.appendAlert(d->usernameSpan(sender), safetext);
+ emit expandRequested();
+ } else if(oflags & rustpile::ChatMessage_OFLAGS_ACTION)
chat.appendAction(d->usernameSpan(sender), safetext);
-
- } else {
- if(d->compactMode)
- chat.appendMessageCompact(sender, d->usernameSpan(sender), safetext, oflags & rustpile::ChatMessage_OFLAGS_SHOUT);
- else
- chat.appendMessage(sender, d->usernameSpan(sender), safetext, oflags & rustpile::ChatMessage_OFLAGS_SHOUT);
- }
+ else if(d->compactMode)
+ chat.appendMessageCompact(sender, d->usernameSpan(sender), safetext, oflags & rustpile::ChatMessage_OFLAGS_SHOUT);
+ else
+ chat.appendMessage(sender, d->usernameSpan(sender), safetext, oflags & rustpile::ChatMessage_OFLAGS_SHOUT);
if(chatId != d->currentChat) {
for(int i=0;itabs->count();++i) {
@@ -590,16 +619,20 @@ void ChatWidget::setPinnedMessage(const QString &message)
void ChatWidget::systemMessage(const QString& message, bool alert)
{
- Q_UNUSED(alert);
const bool wasAtEnd = d->isAtEnd();
- d->publicChat().appendNotification(message.toHtmlEscaped());
+ if(alert) {
+ d->publicChat().appendAlert(QString(), message);
+ emit expandRequested();
+ } else
+ d->publicChat().appendNotification(message.toHtmlEscaped());
+
if(wasAtEnd)
d->scrollToEnd(0);
}
void ChatWidget::sendMessage(const QString &msg)
{
- const uint8_t tflags = d->preserveChat ? rustpile::ChatMessage_TFLAGS_BYPASS : 0;
+ uint8_t tflags = d->preserveChat ? rustpile::ChatMessage_TFLAGS_BYPASS : 0;
uint8_t oflags = 0;
auto chatmsg = msg;
@@ -623,6 +656,12 @@ void ChatWidget::sendMessage(const QString &msg)
oflags = rustpile::ChatMessage_OFLAGS_SHOUT;
}
+ } else if(cmd == QStringLiteral("alert")) {
+ if(msg.length() > 2) {
+ chatmsg = params;
+ tflags |= rustpile::ChatMessage_TFLAGS_ALERT;
+ }
+
} else if(cmd == QStringLiteral("me")) {
if(!params.isEmpty()) {
oflags = rustpile::ChatMessage_OFLAGS_ACTION;
@@ -656,6 +695,7 @@ void ChatWidget::sendMessage(const QString &msg)
"/help - show this message\n"
"/clear - clear chat window\n"
"/! - make an announcement (recorded in session history)\n"
+ "/alert - send a high priority alert\n"
"/me - send action type message\n"
"/pin - pin a message to the top of the chat box (Ops only)\n"
"/unpin - remove pinned message\n"
diff --git a/src/desktop/chat/chatwidget.h b/src/desktop/chat/chatwidget.h
index f22b40d6d7..e90cb02680 100644
--- a/src/desktop/chat/chatwidget.h
+++ b/src/desktop/chat/chatwidget.h
@@ -84,6 +84,7 @@ private slots:
signals:
void message(const net::Envelope &msg);
void detachRequested();
+ void expandRequested();
private:
struct Private;
diff --git a/src/desktop/mainwindow.cpp b/src/desktop/mainwindow.cpp
index ea8b29e83e..446267daa2 100644
--- a/src/desktop/mainwindow.cpp
+++ b/src/desktop/mainwindow.cpp
@@ -2539,6 +2539,7 @@ void MainWindow::setupActions()
connect(m_chatbox, &widgets::ChatBox::expandedChanged, toggleChat, &QAction::setChecked);
connect(m_chatbox, &widgets::ChatBox::expandedChanged, m_statusChatButton, &QToolButton::hide);
+ connect(m_chatbox, &widgets::ChatBox::expandPlease, toggleChat, &QAction::trigger);
connect(toggleChat, &QAction::triggered, this, [this](bool show) {
QList sizes;
if(show) {
diff --git a/src/dpcore/src/protocol/message.rs b/src/dpcore/src/protocol/message.rs
index 45a6f6b300..20c8eb6c04 100644
--- a/src/dpcore/src/protocol/message.rs
+++ b/src/dpcore/src/protocol/message.rs
@@ -110,7 +110,8 @@ pub struct ChatMessage {
impl ChatMessage {
pub const TFLAGS_BYPASS: u8 = 0x1;
- pub const TFLAGS: &'static [&'static str] = &["bypass"];
+ pub const TFLAGS_ALERT: u8 = 0x2;
+ pub const TFLAGS: &'static [&'static str] = &["bypass", "alert"];
pub const OFLAGS_SHOUT: u8 = 0x1;
pub const OFLAGS_ACTION: u8 = 0x2;
pub const OFLAGS_PIN: u8 = 0x4;
@@ -1690,6 +1691,9 @@ pub enum CommandMessage {
/// Specifying a sublayer requires session operator privileges. Currently, it is used
/// only when sublayers are needed at canvas initialization.
///
+ /// Note: the `fixed` flag is unused since version 2.2. It's functionality is replaced
+ /// by the custom timeline feature.
+ ///
LayerAttributes(u8, LayerAttributesMessage),
/// Change a layer's title
diff --git a/src/dpcore/src/protocol/protocol.yaml b/src/dpcore/src/protocol/protocol.yaml
index 1433bc649d..993a088389 100644
--- a/src/dpcore/src/protocol/protocol.yaml
+++ b/src/dpcore/src/protocol/protocol.yaml
@@ -110,7 +110,7 @@ Chat:
(Typically a Command message is used for server announcements, but the Chat message
is used for those messages that must be stored in the session history.)
fields:
- - tflags flags: [bypass]
+ - tflags flags: [bypass, alert]
- oflags flags: [shout, action, pin]
- message utf8
diff --git a/src/libserver/session.cpp b/src/libserver/session.cpp
index 5160f57f38..ddf9f2e3b4 100644
--- a/src/libserver/session.cpp
+++ b/src/libserver/session.cpp
@@ -646,6 +646,8 @@ void Session::handleClientMessage(Client &client, protocol::MessagePtr msg)
case protocol::MSG_CHAT: {
if(client.isMuted())
return;
+ if(!client.isOperator() && msg.cast().isAlert())
+ return;
if(msg.cast().isBypass()) {
directToAll(msg);
return;
diff --git a/src/libshared/net/meta.h b/src/libshared/net/meta.h
index dc66bc8bf1..6f817de3f4 100644
--- a/src/libshared/net/meta.h
+++ b/src/libshared/net/meta.h
@@ -160,6 +160,7 @@ class Chat : public Message {
public:
// Transparent flags: these affect serverside behavior
static const uint8_t FLAG_BYPASS = 0x01; // bypass session history and send directly to logged in users
+ static const uint8_t FLAG_ALERT = 0x02; // high priority alert (can be send by operators only)
// Opaque flags: the server doesn't know anything about these
static const uint8_t FLAG_SHOUT = 0x01; // public announcement
@@ -197,6 +198,11 @@ class Chat : public Message {
*/
bool isBypass() const { return m_tflags & FLAG_BYPASS; }
+ /**
+ * @brief Is this an alert message?
+ */
+ bool isAlert() const { return m_tflags & FLAG_ALERT; }
+
/**
* @brief Is this a shout?
*
diff --git a/src/rustpile/rustpile.h b/src/rustpile/rustpile.h
index cb73ced3c6..aaa001543d 100644
--- a/src/rustpile/rustpile.h
+++ b/src/rustpile/rustpile.h
@@ -42,6 +42,8 @@ static const uint8_t ChatMessage_OFLAGS_PIN = 4;
static const uint8_t ChatMessage_OFLAGS_SHOUT = 1;
+static const uint8_t ChatMessage_TFLAGS_ALERT = 2;
+
static const uint8_t ChatMessage_TFLAGS_BYPASS = 1;
enum class AnimationExportMode {