Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions docs/JSON-RPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ The request must be sent as a single line of JSON-encoded data, followed by a ne
Jamulus will also send **notifications** to the consumer:

```json
{"jsonrpc":"2.0","method":"jamulusclient/chatTextReceived","params":{"text":"<font color=\"mediumblue\">(01:23:45 AM) <b>user</b></font> test"}}
{"jsonrpc":"2.0","method":"jamulusclient/chatTextReceived","params":{"channelId":12,"timestamp":1786298460,"senderName":"user","text":"test"}}
```

## Method reference
Expand Down Expand Up @@ -600,13 +600,16 @@ Parameters:

### jamulusclient/chatTextReceived

Emitted when a chat text is received.
Emitted when a structured chat message (message 37) is received. Carries semantic data, never presentation markup.

Parameters:

| Name | Type | Description |
| --- | --- | --- |
| params.chatText | string | The chat text. |
| params.channelId | number | Channel ID of the sending client, or 255 for server/RPC-originated messages. |
| params.timestamp | number | Unix timestamp (seconds) stamped at the server. |
| params.senderName | string | Name of the sending client (empty for server/RPC-originated messages). |
| params.text | string | Chat message text. |


### jamulusclient/clientListReceived
Expand Down Expand Up @@ -698,8 +701,10 @@ Parameters:

| Name | Type | Description |
| --- | --- | --- |
| params.id | number | Channel ID of sending client or -1 for RPC sent messages. |
| params.chatMessage | string | Chat message text. |
| params.channelId | number | Channel ID of sending client or -1 for RPC sent messages. |
| params.timestamp | number | Unix timestamp (seconds) stamped at the server. |
| params.senderName | string | Name of the sending client (empty for RPC sent messages). |
| params.text | string | Chat message text. |


### jamulusserver/clientConnected
Expand Down
224 changes: 224 additions & 0 deletions docs/design-chat-redesign.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/channel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ CChannel::CChannel ( const bool bNIsServer ) :
iCurSockBufNumFrames ( INVALID_INDEX ),
bDoAutoSockBufSize ( true ),
bUseSequenceNumber ( false ), // this is important since in the client we reset on Channel.SetEnable ( false )
bSupportsStructuredChat ( false ),
iSendSequenceNumber ( 0 ),
iFadeInCnt ( 0 ),
iFadeInCntMax ( FADE_IN_NUM_FRAMES_DBLE_FRAMESIZE ),
Expand Down Expand Up @@ -113,6 +114,8 @@ CChannel::CChannel ( const bool bNIsServer ) :

QObject::connect ( &Protocol, &CProtocol::ChatTextReceived, this, &CChannel::ChatTextReceived );

QObject::connect ( &Protocol, &CProtocol::ChatTextChannelReceived, this, &CChannel::ChatTextChannelReceived );

QObject::connect ( &Protocol, &CProtocol::NetTranspPropsReceived, this, &CChannel::OnNetTranspPropsReceived );

QObject::connect ( &Protocol, &CProtocol::ReqNetTranspProps, this, &CChannel::OnReqNetTranspProps );
Expand All @@ -121,6 +124,10 @@ CChannel::CChannel ( const bool bNIsServer ) :

QObject::connect ( &Protocol, &CProtocol::SplitMessSupported, this, &CChannel::OnSplitMessSupported );

QObject::connect ( &Protocol, &CProtocol::ReqChatTextSupport, this, &CChannel::OnReqChatTextSupport );

QObject::connect ( &Protocol, &CProtocol::ChatTextSupported, this, &CChannel::OnChatTextSupported );

QObject::connect ( &Protocol, &CProtocol::LicenceRequired, this, &CChannel::LicenceRequired );

QObject::connect ( &Protocol, &CProtocol::VersionAndOSReceived, this, &CChannel::OnVersionAndOSReceived );
Expand Down
13 changes: 11 additions & 2 deletions src/channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ class CChannel : public QObject

void ResetInfo()
{
bIsIdentified = false;
ChannelInfo = CChannelCoreInfo();
bIsIdentified = false;
bSupportsStructuredChat = false;
ChannelInfo = CChannelCoreInfo();
} // reset does not emit a message
QString GetName();
void SetChanInfo ( const CChannelCoreInfo& NChanInf );
Expand Down Expand Up @@ -178,6 +179,10 @@ class CChannel : public QObject
void CreateReqJitBufMes() { Protocol.CreateReqJitBufMes(); }
void CreateReqConnClientsList() { Protocol.CreateReqConnClientsList(); }
void CreateChatTextMes ( const QString& strChatText ) { Protocol.CreateChatTextMes ( strChatText ); }
void CreateChatTextChannelMes ( const uint8_t iChannelID, const uint32_t iTimestamp, const QString strSenderName, const QString strChatText ) { Protocol.CreateChatTextChannelMes ( iChannelID, iTimestamp, strSenderName, strChatText ); }
void CreateReqChatTextSupportMes() { Protocol.CreateReqChatTextSupportMes(); }
void CreateChatTextSupportedMes() { Protocol.CreateChatTextSupportedMes(); }
bool SupportsStructuredChat() const { return bSupportsStructuredChat; }
void CreateLicReqMes ( const ELicenceType eLicenceType ) { Protocol.CreateLicenceRequiredMes ( eLicenceType ); }

//### TODO: BEGIN ###//
Expand Down Expand Up @@ -224,6 +229,7 @@ class CChannel : public QObject
int iCurSockBufNumFrames;
bool bDoAutoSockBufSize;
bool bUseSequenceNumber;
bool bSupportsStructuredChat;
uint8_t iSendSequenceNumber;

// network output conversion buffer
Expand Down Expand Up @@ -265,6 +271,8 @@ public slots:
void OnReqNetTranspProps();
void OnReqSplitMessSupport();
void OnSplitMessSupported() { Protocol.SetSplitMessageSupported ( true ); }
void OnReqChatTextSupport() { Protocol.CreateChatTextSupportedMes(); }
void OnChatTextSupported() { bSupportsStructuredChat = true; }

void OnVersionAndOSReceived ( COSUtil::EOpSystemType eOSType, QString strVersion );

Expand Down Expand Up @@ -301,6 +309,7 @@ public slots:
void MuteStateHasChangedReceived ( int iChanID, bool bIsMuted );
void ReqChanInfo();
void ChatTextReceived ( QString strChatText );
void ChatTextChannelReceived ( uint8_t iChannelID, uint32_t iTimestamp, QString strSenderName, QString strChatText );
void ReqNetTranspProps();
void LicenceRequired ( ELicenceType eLicenceType );
void VersionAndOSReceived ( COSUtil::EOpSystemType eOSType, QString strVersion );
Expand Down
90 changes: 69 additions & 21 deletions src/chatdlg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,20 @@

#include "chatdlg.h"

#include <QDateTime>
#include <QLocale>
#if QT_VERSION >= QT_VERSION_CHECK( 6, 8, 0 )
# include <QAccessibleAnnouncementEvent>
#endif

/* Implementation *************************************************************/
namespace
{
// client-controlled sender colours, stable per channel ID (presentation is a
// client concern; the server only sends data)
const char* const astrChatColors[6] = { "mediumblue", "red", "darkorchid", "green", "maroon", "coral" };
}

CChatDlg::CChatDlg ( QWidget* parent ) : CBaseDlg ( parent, Qt::Window ) // use Qt::Window to get min/max window buttons
{
setupUi ( this );
Expand Down Expand Up @@ -133,33 +146,68 @@ void CChatDlg::OnClearChatHistory()

void CChatDlg::AddChatText ( QString strChatText )
{
// notify accessibility plugin that text has changed
QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( txvChatWindow, strChatText ) );
// legacy (message 18) path: the server sent already-escaped HTML; we only
// linkify bare http(s):// URLs, the text itself is never re-interpreted
LinkifyURLs ( strChatText );

// analyze strChatText to check if hyperlink (limit ourselves to http(s)://) but do not
// replace the hyperlinks if any HTML code for a hyperlink was found (the user has done the HTML
// coding hisself and we should not mess with that)
if ( !strChatText.contains ( QRegularExpression ( "href\\s*=|src\\s*=" ) ) )
{
// searches for all occurrences of http(s) and cuts until a space (\S matches any non-white-space
// character and the + means that matches the previous element one or more times.)
// This regex now contains three parts:
// - https?://\\S+ matches as much non-whitespace as possible after the http:// or https://,
// subject to the next two parts, which exclude terminating punctuation
// - (?<![!\"'()+,.:;<=>?\\[\\]{}]) is a negative look-behind assertion that disallows the match
// from ending with one of the characters !"'()+,.:;<=>?[]{}
// - (?<!\\?[!\"'()+,.:;<=>?\\[\\]{}]) is a negative look-behind assertion that disallows the match
// from ending with a ? followed by one of the characters !"'()+,.:;<=>?[]{}
// These last two parts must be separate, as a look-behind assertion must be fixed length.
#define PUNCT_NOEND_URL "[!\"'()+,.:;<=>?\\[\\]{}]"
strChatText.replace ( QRegularExpression ( "(https?://\\S+(?<!" PUNCT_NOEND_URL ")(?<!\\?" PUNCT_NOEND_URL "))" ),
"<a href=\"\\1\">\\1</a>" );
}
AnnounceNewChatMessage ( strChatText );

// add new text in chat window
txvChatWindow->append ( strChatText );
}

void CChatDlg::AddChatMessage ( const ChatMessage& message )
{
// announce the plain content (sender and text) to screen readers
QString strAnnouncement;
if ( !message.senderName.isEmpty() )
{
strAnnouncement = message.senderName + ": " + message.text;
}
else
{
strAnnouncement = message.text;
}
AnnounceNewChatMessage ( strAnnouncement );

// add new structured message in chat window
txvChatWindow->append ( FormatChatMessage ( message ) );
}

void CChatDlg::AnnounceNewChatMessage ( const QString& strAnnouncement )
{
#if QT_VERSION >= QT_VERSION_CHECK( 6, 8, 0 )
// prefer a proper live region announcement over the value-change event
QAccessible::updateAccessibility ( new QAccessibleAnnouncementEvent ( txvChatWindow, strAnnouncement ) );
#else
QAccessible::updateAccessibility ( new QAccessibleValueChangeEvent ( txvChatWindow, strAnnouncement ) );
#endif
}

QString CChatDlg::FormatChatMessage ( const ChatMessage& message ) const
{
// the client supplies all presentation: local, locale-aware time, a stable
// per-channel sender colour and escaped plain text; user data is escaped so
// that it is never interpreted as HTML
const QString strTime = QLocale().toString ( QDateTime::fromSecsSinceEpoch ( message.timestamp ).toLocalTime().time(),
QLocale::ShortFormat );

QString strSenderName = message.senderName;
if ( strSenderName.isEmpty() )
{
// server/RPC-originated messages carry the wire sentinel channel ID and
// no sender name; unknown channels get a neutral placeholder
strSenderName = ( message.channelId == SERVER_CHAT_CHANNEL_ID ) ? tr ( "Server" ) : tr ( "Unknown" );
}

const QString sCurColor = astrChatColors[message.channelId % 6];

const QString strHeader =
"<font color=\"" + sCurColor + "\">(" + strTime + ") <b>" + strSenderName.toHtmlEscaped() + "</b></font> ";

return strHeader + EscapeAndLinkifyText ( message.text );
}

void CChatDlg::OnAnchorClicked ( const QUrl& Url )
{
// only allow http(s) URLs to be opened in an external browser
Expand Down
6 changes: 6 additions & 0 deletions src/chatdlg.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#include <QRegularExpression>
#include "global.h"
#include "util.h"
#include "chatmessage.h"
#include "ui_chatdlgbase.h"

/* Classes ********************************************************************/
Expand All @@ -70,6 +71,7 @@ class CChatDlg : public CBaseDlg, private Ui_CChatDlgBase
CChatDlg ( QWidget* parent = nullptr );

void AddChatText ( QString strChatText );
void AddChatMessage ( const ChatMessage& message );

public slots:
void OnSendText();
Expand All @@ -82,4 +84,8 @@ public slots:

signals:
void NewLocalInputText ( QString strNewText );

private:
void AnnounceNewChatMessage ( const QString& strAnnouncement );
QString FormatChatMessage ( const ChatMessage& message ) const;
};
43 changes: 43 additions & 0 deletions src/chatmessage.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/******************************************************************************\
* Copyright (c) 2026
*
* This file is part of Jamulus.
*
* Author(s):
* Jamulus contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
\******************************************************************************/

#pragma once

#include <QMetaType>
#include <QString>

#include <cstdint>

// Semantic chat message data as carried by protocol message 37. Contains
// facts, not presentation markup: the sender name is the wire snapshot taken
// at server fan-out, and the channel ID serves filtering/muting rather than
// identity. SERVER_CHAT_CHANNEL_ID (255) marks server/RPC-originated messages.
struct ChatMessage
{
uint8_t channelId; // server channel ID, or SERVER_CHAT_CHANNEL_ID (255) for server/RPC messages
uint32_t timestamp; // epoch seconds (UTC), stamped at the server
QString senderName; // sender name snapshotted on the wire (empty for server/RPC messages)
QString text; // plain UTF-8 chat text, never HTML
};

Q_DECLARE_METATYPE ( ChatMessage )
11 changes: 11 additions & 0 deletions src/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
#include "settings.h"
#include "util.h"

#include <utility>

/* Implementation *************************************************************/
CClient::CClient ( const quint16 iPortNumber,
const quint16 iQosNumber,
Expand Down Expand Up @@ -151,6 +153,8 @@ CClient::CClient ( const quint16 iPortNumber,

QObject::connect ( &Channel, &CChannel::ChatTextReceived, this, &CClient::ChatTextReceived );

QObject::connect ( &Channel, &CChannel::ChatTextChannelReceived, this, &CClient::OnChatTextChannelReceived );

QObject::connect ( &Channel, &CChannel::ClientIDReceived, this, &CClient::OnClientIDReceived );

QObject::connect ( &Channel, &CChannel::RawAudioSupported, this, &CClient::OnRawAudioSupported );
Expand Down Expand Up @@ -420,6 +424,13 @@ void CClient::OnConClientListMesReceived ( CVector<CChannelInfo> vecChanInfo )
emit ConClientListMesReceived ( vecChanInfo );
}

void CClient::OnChatTextChannelReceived ( uint8_t iChannelID, uint32_t iTimestamp, QString strSenderName, QString strChatText )
{
// build the semantic chat message and pass it on to the model/presentation
// layer; senderName is the wire snapshot, no live lookup is needed
emit ChatTextChannelReceived ( ChatMessage{ iChannelID, iTimestamp, std::move ( strSenderName ), std::move ( strChatText ) } );
}

void CClient::CreateServerJitterBufferMessage()
{
// per definition in the client: if auto jitter buffer is enabled, both,
Expand Down
3 changes: 3 additions & 0 deletions src/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
#include "socket.h"
#include "channel.h"
#include "util.h"
#include "chatmessage.h"
#include "plugins/audioreverb.h"
#include "buffer.h"
#include "signalhandler.h"
Expand Down Expand Up @@ -485,10 +486,12 @@ protected slots:
void OnMuteStateHasChangedReceived ( int iServerChanID, bool bIsMuted );
void OnCLChannelLevelListReceived ( CHostAddress InetAddr, CVector<uint16_t> vecLevelList );
void OnConClientListMesReceived ( CVector<CChannelInfo> vecChanInfo );
void OnChatTextChannelReceived ( uint8_t iChannelID, uint32_t iTimestamp, QString strSenderName, QString strChatText );

signals:
void ConClientListMesReceived ( CVector<CChannelInfo> vecChanInfo );
void ChatTextReceived ( QString strChatText );
void ChatTextChannelReceived ( ChatMessage chatMessage );
void ClientIDReceived ( int iChanID );
void MuteStateHasChangedReceived ( int iChanID, bool bIsMuted );
void LicenceRequired ( ELicenceType eLicenceType );
Expand Down
17 changes: 17 additions & 0 deletions src/clientdlg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,8 @@ CClientDlg::CClientDlg ( CClient* pNCliP,

QObject::connect ( pClient, &CClient::ChatTextReceived, this, &CClientDlg::OnChatTextReceived );

QObject::connect ( pClient, &CClient::ChatTextChannelReceived, this, &CClientDlg::OnChatTextChannelReceived );

QObject::connect ( pClient, &CClient::ClientIDReceived, this, &CClientDlg::OnClientIDReceived );

QObject::connect ( pClient, &CClient::MuteStateHasChangedReceived, this, &CClientDlg::OnMuteStateHasChangedReceived );
Expand Down Expand Up @@ -890,6 +892,21 @@ void CClientDlg::OnChatTextReceived ( QString strChatText )
UpdateDisplay();
}

void CClientDlg::OnChatTextChannelReceived ( ChatMessage chatMessage )
{
if ( pSettings->bEnableAudioAlerts )
{
PlayAudioAlert ( QUrl::fromLocalFile ( ":sounds/res/sounds/new_message.wav" ) );
}
ChatDlg.AddChatMessage ( chatMessage );

// structured messages never carry the server welcome message (that stays
// on the legacy path), so never force the dialog to be upfront
ShowChatWindow ( false );

UpdateDisplay();
}

void CClientDlg::OnLicenceRequired ( ELicenceType eLicenceType )
{
// right now only the creative common licence is supported
Expand Down
1 change: 1 addition & 0 deletions src/clientdlg.h
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ public slots:

void OnConClientListMesReceived ( CVector<CChannelInfo> vecChanInfo );
void OnChatTextReceived ( QString strChatText );
void OnChatTextChannelReceived ( ChatMessage chatMessage );
void OnLicenceRequired ( ELicenceType eLicenceType );
void OnSoundDeviceChanged ( QString strError );

Expand Down
Loading
Loading