Fix reactions synchronizations.

Generic reactions (support any emojies) but display in priority a fix list (defined in Constants).
Clean old codes and make a ChatReactions UI.
This commit is contained in:
Julien Wadel 2023-08-31 18:39:18 +02:00
parent 267d70535d
commit f496600004
14 changed files with 170 additions and 225 deletions

View file

@ -346,6 +346,7 @@
<file>ui/modules/Linphone/Chat/ChatFilePreview.qml</file>
<file>ui/modules/Linphone/Chat/ChatForwardMessage.qml</file>
<file>ui/modules/Linphone/Chat/ChatMessagePreview.qml</file>
<file>ui/modules/Linphone/Chat/ChatReactions.qml</file>
<file>ui/modules/Linphone/Chat/ChatReactionsDetails.qml</file>
<file>ui/modules/Linphone/Chat/ChatReplyMessage.qml</file>
<file>ui/modules/Linphone/Chat/ChatReplyPreview.qml</file>

View file

@ -262,7 +262,7 @@ void ChatMessageModel::sendChatReaction(const QString& reaction){
return; // TODO : remove return when sending empty emoji will be supported.
}
chatReaction->send();
mChatReactionListModel->updateChatReaction(chatReaction);
emit newMessageReaction(mChatMessage, chatReaction);
}
void ChatMessageModel::deleteEvent(){
@ -327,7 +327,7 @@ void ChatMessageModel::onMsgStateChanged (const std::shared_ptr<linphone::ChatMe
}
void ChatMessageModel::onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction){
mChatReactionListModel->updateChatReaction(reaction);
emit newMessageReaction(message, reaction);
}
void ChatMessageModel::onParticipantImdnStateChanged(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ParticipantImdnState> & state){

View file

@ -146,6 +146,7 @@ signals:
void fileContentChanged();
void remove(ChatMessageModel* model);
void newMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
private:
void connectTo(ChatMessageListener * listener);

View file

@ -27,11 +27,15 @@
// =============================================================================
ChatReactionListModel::ChatReactionListModel (ChatMessageModel * message, QObject* parent) : ProxyAbstractListModel<QVariantMap>(parent) {
mParent = message;
setChatMessageModel(message);
}
void ChatReactionListModel::setChatMessageModel(ChatMessageModel * message) {
if(mParent)
disconnect(message, &ChatMessageModel::newMessageReaction, this, &ChatReactionListModel::onNewMessageReaction);
mParent = message;
if(mParent)
connect(message, &ChatMessageModel::newMessageReaction, this, &ChatReactionListModel::onNewMessageReaction);
if(message){
auto reactions = message->getChatMessage()->getReactions();
mReactions.clear();
@ -45,6 +49,7 @@ void ChatReactionListModel::setChatMessageModel(ChatMessageModel * message) {
}
}
updateList();
emit bodiesChanged();
}
}
@ -69,41 +74,18 @@ QSharedPointer<ChatReactionModel> ChatReactionListModel::add(std::shared_ptr<lin
}
void ChatReactionListModel::remove(ChatReactionModel * model){/*
int count = 0;
for(auto it = mList.begin() ; it != mList.end() ; ++count, ++it) {
if( it->get() == model) {
removeRow(count, QModelIndex());
return;
}
}*/
void ChatReactionListModel::remove(ChatReactionModel * model){
}
void ChatReactionListModel::clear(){
resetData();
}
/*
QSharedPointer<ChatReactionModel> ChatReactionListModel::getChatReactionModel(const std::shared_ptr<const linphone::ChatMessageReaction>& reaction){
for(auto item : mList){
auto c = item.objectCast<ChatReactionModel>();
if(c->get() == content)
return c;
}
if(content->isFileTransfer() || content->isFile() || content->isFileEncrypted()){
for(auto item : mList){// Content object can be different for file (like while data transfer)
auto c = item.objectCast<ContentModel>();
if(c->getContent()->getFilePath() == content->getFilePath())
return c;
}
}
return nullptr;
}
*/
void ChatReactionListModel::updateChatReaction(const std::shared_ptr<const linphone::ChatMessageReaction>& reaction) {
QString address = Utils::coreStringToAppString(reaction->getFromAddress()->asStringUriOnly());
auto itReaction = mReactions.find(address);
int oldReactionCount = mReactions.size();
auto oldBodies = getBodies();
if( itReaction == mReactions.end()) {// New
auto reactionModel = QSharedPointer<ChatReactionModel>::create(reaction);
auto body = reactionModel->getBody();
@ -127,6 +109,7 @@ void ChatReactionListModel::updateChatReaction(const std::shared_ptr<const linph
updateList();
if(oldReactionCount != mReactions.size())
emit chatReactionCountChanged();
emit bodiesChanged();
}
void ChatReactionListModel::updateList(){
QList<QVariantMap> data;
@ -177,54 +160,19 @@ void ChatReactionListModel::setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode
}
}
/*
void ContentListModel::updateContent(std::shared_ptr<linphone::Content> oldContent, std::shared_ptr<linphone::Content> newContent){
int row = 0;
for(auto content = mList.begin() ; content != mList.end() ; ++content, ++row){
auto contentModel = content->objectCast<ContentModel>();
if( contentModel->getContent() == oldContent){
mList.replace(row, QSharedPointer<ContentModel>::create(newContent, contentModel->getChatMessageModel()));
emit dataChanged(index(row,0), index(row,0));
return;
QStringList ChatReactionListModel::getBodies() const {
auto bodies = mBodies.keys();
auto reactions = Constants::getReactionsList();
std::sort(bodies.begin(), bodies.end(), [&](const QString& a, const QString& b){
for(auto reaction : reactions){
if( a == reaction) return true;
if( b == reaction) return false;
}
}
return a < b;
});
return bodies;
}
void ContentListModel::updateContents(ChatMessageModel * messageModel){
std::list<std::shared_ptr<linphone::Content>> contents = messageModel->getChatMessage()->getContents() ;
int count = 0;
beginResetModel();
for(auto content : contents){
if( count >= mList.size()){// New content
mList.insert(count, QSharedPointer<ContentModel>::create(content, messageModel));
}else if(mList.at(count).objectCast<ContentModel>()->getContent() != content){ // This content is not at its place
int c = count + 1;
while( c < mList.size() && mList.at(c).objectCast<ContentModel>()->getContent() != content)
++c;
if( c < mList.size()){// Found => swap position
#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0)
mList.swap(count, c);
#else
mList.swapItemsAt(count, c);
#endif
}else{// content is new
mList.insert(count, QSharedPointer<ContentModel>::create(content, messageModel));
}
}
++count;
}
if(count < mList.size())// Remove all old contents
mList.erase(mList.begin()+count, mList.end());
endResetModel();
void ChatReactionListModel::onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction){
updateChatReaction(reaction);
}
void ContentListModel::updateAllTransferData(){
emit updateTransferDataRequested();
}
void ContentListModel::downloaded(){
for(auto content : mList)
content.objectCast<ContentModel>()->createThumbnail();
}*/

View file

@ -52,6 +52,8 @@ public:
ChatReactionListModel::GROUP_BY_TYPE getGroupBy() const;
void setGroupBy(ChatReactionListModel::GROUP_BY_TYPE mode);
QStringList getBodies() const;
//QSharedPointer<ChatReactionModel> getChatReactionModel(const std::shared_ptr<const linphone::ChatMessageReaction>& reaction);
bool exists(std::shared_ptr<linphone::ChatMessageReaction> reaction) const;
@ -61,13 +63,16 @@ public:
void updateChatReaction(ChatMessageModel * messageModel);
void updateList();
void onNewMessageReaction(const std::shared_ptr<linphone::ChatMessage> & message, const std::shared_ptr<const linphone::ChatMessageReaction> & reaction);
signals:
void chatReactionsChanged();
void chatReactionCountChanged();
void groupByChanged();
void bodiesChanged();
private:
ChatMessageModel * mParent;
ChatMessageModel * mParent = nullptr;
QMap<QString, QSharedPointer<ChatReactionModel>> mReactions;
QMap<QString, QVector<QSharedPointer<ChatReactionModel>>> mBodies;
GROUP_BY_TYPE mGroupBy = EMOJIES;

View file

@ -29,6 +29,7 @@ ChatReactionProxyModel::ChatReactionProxyModel (QObject * parent) : SortFilterPr
mContents = QSharedPointer<ChatReactionListModel>::create();
connect(mContents.get(), &ChatReactionListModel::chatReactionCountChanged, this, &ChatReactionProxyModel::chatReactionCountChanged);
connect(mContents.get(), &ChatReactionListModel::groupByChanged, this, &ChatReactionProxyModel::groupByChanged);
connect(mContents.get(), &ChatReactionListModel::bodiesChanged, this, &ChatReactionProxyModel::bodiesChanged);
setSourceModel(mContents.get());
sort(0);
}
@ -90,13 +91,13 @@ void ChatReactionProxyModel::setFilter(const QString& filter) {
}
}
/*
void ChatReactionProxyModel::setContentListModel(ContentListModel * model){
setSourceModel(model);
sort(0);
emit chatMessageModelChanged();
QStringList ChatReactionProxyModel::getBodies() const {
auto model = qobject_cast<ChatReactionListModel*>(sourceModel());
if(model) {
return model->getBodies();
}else
return QStringList();
}
*/
bool ChatReactionProxyModel::filterAcceptsRow (
int sourceRow,
@ -122,46 +123,8 @@ bool ChatReactionProxyModel::filterAcceptsRow (
/*
bool ChatReactionProxyModel::lessThan (const QModelIndex &left, const QModelIndex &right) const {
const ContentModel *contentA = sourceModel()->data(left).value<ContentModel *>();
const ContentModel *contentB = sourceModel()->data(right).value<ContentModel *>();
bool aIsForward = contentA->getChatMessageModel() && contentA->getChatMessageModel()->isForward();
bool aIsReply = contentA->getChatMessageModel() && contentA->getChatMessageModel()->isReply();
bool aIsVoiceRecording = contentA->isVoiceRecording();
bool aIsFile = contentA->isFile() || contentA->isFileEncrypted() || contentA->isFileTransfer();
bool aIsText = contentA->isText() ;
bool bIsForward = contentB->getChatMessageModel() && contentB->getChatMessageModel()->isForward();
bool bIsReply = contentB->getChatMessageModel() && contentB->getChatMessageModel()->isReply();
bool bIsVoiceRecording = contentB->isVoiceRecording();
bool bIsFile = contentB->isFile() || contentB->isFileEncrypted() || contentB->isFileTransfer();
bool bIsText = contentB->isText() ;
auto a = sourceModel()->data(left).value<QVariantMap>();
auto b = sourceModel()->data(right).value<QVariantMap>();
return !bIsForward && (aIsForward
|| !bIsReply && (aIsReply
|| !bIsVoiceRecording && (aIsVoiceRecording
|| !bIsFile && (aIsFile
|| aIsText && !bIsText
)
)
)
);
}
*/
/*
void ChatReactionProxyModel::remove(ContentModel * model){
qobject_cast<ContentListModel*>(sourceModel())->remove(model);
}
void ChatReactionProxyModel::clear(){
qobject_cast<ContentListModel*>(sourceModel())->clear();
}
ContentProxyModel::FilterContentType ChatReactionProxyModel::getFilter() const{
return mFilter;
}
void ChatReactionProxyModel::setFilter(const FilterContentType& contentType){
if(contentType != mFilter){
mFilter = contentType;
emit filterChanged();
invalidate();
}
}*/
*/

View file

@ -36,19 +36,8 @@ public:
Q_PROPERTY(int reactionCount READ getChatReactionCount NOTIFY chatReactionCountChanged)
Q_PROPERTY(ChatReactionListModel::GROUP_BY_TYPE groupBy READ getGroupBy WRITE setGroupBy NOTIFY groupByChanged)
Q_PROPERTY(QString filter READ getFilter WRITE setFilter NOTIFY filterChanged)
/*
Q_PROPERTY(FilterContentType filter READ getFilter WRITE setFilter NOTIFY filterChanged)
Q_PROPERTY(QStringList bodies READ getBodies NOTIFY bodiesChanged)
enum FilterContentType {
All,
File,
Text,
Voice,
Conference,
Unknown
};
Q_ENUM(FilterContentType)
*/
ChatMessageModel * getChatMessageModel() const;
void setChatMessageModel(ChatMessageModel * message);
Q_INVOKABLE void setChatMessageModel(ChatMessageModel * message, ChatReactionListModel::GROUP_BY_TYPE groupByMode);
@ -61,16 +50,15 @@ public:
QString getFilter() const;
void setFilter(const QString& filter);
//Q_INVOKABLE void setContentListModel(ContentListModel * model);
//Q_INVOKABLE void addFile(const QString& path);
//Q_INVOKABLE void remove(ContentModel * model);
//Q_INVOKABLE void clear();
QStringList getBodies() const;
signals:
void chatMessageModelChanged();
void chatReactionCountChanged();
void groupByChanged();
void filterChanged();
void bodiesChanged();
protected:

View file

@ -26,7 +26,9 @@ constexpr char Constants::DefaultFont[];
constexpr int Constants::DefaultFontPointSize;
constexpr char Constants::DefaultEmojiFont[];
constexpr int Constants::DefaultEmojiFontPointSize;
QStringList Constants::getReactionsList(){
return {"❤️", "👍", "😂", "😮", "😢"};
}
constexpr char Constants::QtDomain[];
constexpr size_t Constants::MaxLogsCollectionSize;
constexpr char Constants::SrcPattern[];

View file

@ -45,6 +45,7 @@ public:
static constexpr char DefaultEmojiFont[] = "Noto Color Emoji";
#endif
static constexpr int DefaultEmojiFontPointSize = 10;
static QStringList getReactionsList();
static constexpr size_t MaxLogsCollectionSize = 10485760*5; // 50MB.
@ -93,6 +94,7 @@ public:
Q_PROPERTY(QString ContactUrl MEMBER ContactUrl CONSTANT)
Q_PROPERTY(QString TranslationUrl MEMBER TranslationUrl CONSTANT)
Q_PROPERTY(int maxMosaicParticipants MEMBER MaxMosaicParticipants CONSTANT)
Q_PROPERTY(QStringList reactionsList READ getReactionsList CONSTANT)
// For Webviews
static constexpr char DefaultAssistantRegistrationUrl[] = "https://subscribe.linphone.org/register";

View file

@ -10,6 +10,7 @@ import Linphone.Styles 1.0
import TextToSpeech 1.0
import Utils 1.0
import Units 1.0
import ConstantsCpp 1.0
import UtilsCpp 1.0
import LinphoneEnums 1.0
@ -53,7 +54,7 @@ Item {
Layout.fillWidth: true
spacing:0
Repeater{
model: ['❤️','👍','😂','😮','😢']
model: ConstantsCpp.reactionsList
delegate: Text{
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter

View file

@ -0,0 +1,72 @@
import QtQuick 2.7
import QtQuick.Layouts 1.3
import QtQml.Models 2.15
import Common 1.0
import Linphone 1.0
import Units 1.0
// =============================================================================
Rectangle{
id: mainItem
property ChatReactionProxyModel model
property var modelData
signal reactionsClicked()
height: visible ? reactionList.height +10 : 0
width: visible ? reactionLayout.width + 10 : 0
visible: reactionList.count > 0
property font customFont : SettingsModel.textMessageFont
property font customEmojiFont : SettingsModel.emojiFont
RowLayout{
id: reactionLayout
anchors.centerIn: parent
ListView{
id: reactionList
Layout.preferredWidth: contentItem.childrenRect.width
Layout.preferredHeight: reactionMetrics.height
TextMetrics{
id: reactionMetrics
text: '😮'
font.family: mainItem.customEmojiFont.family
font.pointSize: Units.dp * mainItem.customEmojiFont.pointSize * 2
}
orientation: ListView.Horizontal
model: mainItem.model
spacing: 10
delegate:
Text{
width: reactionMetrics.width
height: reactionList.height
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: mainItem.customEmojiFont.family
font.pointSize: Units.dp * mainItem.customEmojiFont.pointSize * 2
text: $modelData.body || ''
}
}
Text{
Layout.preferredWidth: contentWidth
Layout.preferredHeight: reactionList.height
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: mainItem.customFont.family
font.pointSize: Units.dp * mainItem.customFont.pointSize
visible: mainItem.model && mainItem.model.count != mainItem.model.reactionCount
text: mainItem.model && mainItem.model.reactionCount || ''
}
}
MouseArea{
anchors.fill: parent
onClicked: mainItem.reactionsClicked()
}
}

View file

@ -28,9 +28,6 @@ Rectangle{
}
color: ChatReactionsDetailsStyle.backgroundColorModel.color
onVisibleChanged: if(visible){
tabBar.currentIndex = 0
}
MouseArea{
anchors.fill: parent
onClicked: mainItem.visible = false
@ -45,11 +42,23 @@ Rectangle{
ColumnLayout{
anchors.fill: parent
spacing: 0
TabBar {
Loader{
id: loader
Layout.fillWidth: true
id: tabBar
TabButton {
Layout.fillWidth: true
active: mainItem.visible
function refresh(){
active = false
active = true
}
Connections{
target: chatReactionsList
onChatMessageModelChanged: loader.refresh()
onBodiesChanged: loader.refresh()
}
sourceComponent: TabBar {
id: tabBar
Component.onCompleted: currentIndex = 0
TabButton {
//: "%1<br>reactions" : count of all chat reactions with a jump line between count and text.
text: UtilsCpp.encodeTextToQmlRichFormat(qsTr('reactionsCount', '', chatReactionsList.reactionCount).arg(chatReactionsList.reactionCount), {noLink:1}).toUpperCase()
// noLink=1 to avoid <br> convertion
@ -59,24 +68,20 @@ Rectangle{
stretchContent: false
style: TabButtonStyle.popup
}
Repeater{
model: ['❤️','👍','😂','😮','😢']
delegate: TabButton {
width: visible ? undefined : 0
property int reactionCount: 0
visible: reactionCount > 0
text: UtilsCpp.encodeTextToQmlRichFormat(modelData + ' '+reactionCount)
textFont.family: mainItem.textFont.family
textFont.pointSize: ChatReactionsDetailsStyle.tabBar.pointSize
onIsSelectedChanged: if(isSelected) chatReactionsList.filter = modelData
displaySelector: true
stretchContent: false
style: TabButtonStyle.popup
Connections{
target: chatReactionsList
onChatMessageModelChanged: reactionCount = chatReactionsList.getChatReactionCount(modelData)
Repeater{
model: chatReactionsList.bodies
delegate: TabButton {
width: visible ? undefined : 0
property int reactionCount: chatReactionsList.getChatReactionCount(modelData)
visible: reactionCount > 0
text: UtilsCpp.encodeTextToQmlRichFormat(modelData + ' '+reactionCount)
textFont.family: mainItem.textFont.family
textFont.pointSize: ChatReactionsDetailsStyle.tabBar.pointSize
onIsSelectedChanged: if(isSelected) chatReactionsList.filter = modelData
displaySelector: true
stretchContent: false
style: TabButtonStyle.popup
}
}
}

View file

@ -42,11 +42,11 @@ Item {
signal conferenceIcsCopied()
signal addContactClicked(string contactAddress)
signal viewContactClicked(string contactAddress)
signal reactionsClicked(ChatMessageModel message);
signal reactionsClicked(ChatMessageModel message)
// ---------------------------------------------------------------------------
property string lastTextSelected
implicitHeight: (deliveryLayout.visible? deliveryLayout.height : 0) +(ephemeralTimerRow.visible? 16 : 0) + chatContent.height + reactionItem.height-10
implicitHeight: (deliveryLayout.visible? deliveryLayout.height : 0) +(ephemeralTimerRow.visible? 16 : 0) + chatContent.height + reactionLoader.height-10
Rectangle {
id: rectangle
property int availableWidth: parent.width
@ -56,7 +56,7 @@ Item {
anchors.left: !$chatEntry.isOutgoing ? parent.left : undefined
anchors.right: $chatEntry.isOutgoing ? parent.right : undefined
height: parent.height - (deliveryLayout.visible? deliveryLayout.height : 0) - (reactionItem.height-10)
height: parent.height - (deliveryLayout.visible? deliveryLayout.height : 0) - (reactionLoader.height-10)
radius: ChatStyle.entry.message.radius
clip: false
color: colorModel.color
@ -144,69 +144,25 @@ Item {
chatMessageModel: $chatEntry
}
Rectangle{
id: reactionItem
Loader{
id: reactionLoader
anchors.top: rectangle.bottom
anchors.left: !$chatEntry.isOutgoing ? rectangle.left : undefined
anchors.right: $chatEntry.isOutgoing ? rectangle.right : undefined
anchors.topMargin: -10
anchors.leftMargin: 5
anchors.rightMargin: 5
height: visible ? reactionList.height +10 : 0
width: visible ? reactionLayout.width + 10 : 0
color: rectangle.color
radius: rectangle.radius
visible: reactionList.count > 0
property font customFont : SettingsModel.textMessageFont
property font customEmojiFont : SettingsModel.emojiFont
RowLayout{
id: reactionLayout
anchors.centerIn: parent
ListView{
id: reactionList
Layout.preferredWidth: contentItem.childrenRect.width
Layout.preferredHeight: reactionMetrics.height
TextMetrics{
id: reactionMetrics
text: '😮'
font.family: reactionItem.customEmojiFont.family
font.pointSize: Units.dp * reactionItem.customEmojiFont.pointSize * 2
}
orientation: ListView.Horizontal
model: ChatReactionProxyModel{
id: chatReactionProxyModel
chatMessageModel: $chatEntry
}
spacing: 10
delegate:
Text{
width: reactionMetrics.width
height: reactionList.height
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: reactionItem.customEmojiFont.family
font.pointSize: Units.dp * reactionItem.customEmojiFont.pointSize * 2
text: $modelData.body || ''
}
}
Text{
Layout.preferredWidth: contentWidth
Layout.preferredHeight: reactionList.height
Layout.alignment: Qt.AlignVCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.family: reactionItem.customFont.family
font.pointSize: Units.dp * reactionItem.customFont.pointSize
visible: chatReactionProxyModel.count != chatReactionProxyModel.reactionCount
text: chatReactionProxyModel.reactionCount
}
ChatReactionProxyModel{
id: chatReactionProxyModel
chatMessageModel: $chatEntry
}
MouseArea{
anchors.fill: parent
onClicked: container.reactionsClicked($chatEntry)
active: chatReactionProxyModel.count > 0
asynchronous: true
sourceComponent: ChatReactions{
color: rectangle.color
radius: rectangle.radius
model: chatReactionProxyModel
onReactionsClicked: container.reactionsClicked($chatEntry)
}
}

View file

@ -29,6 +29,7 @@ ChatEmojis 1.0 Chat/ChatEmojis.qml
ChatFullContent 1.0 Chat/ChatFullContent.qml
ChatMessagePreview 1.0 Chat/ChatMessagePreview.qml
ChatForwardMessage 1.0 Chat/ChatForwardMessage.qml
ChatReactions 1.0 Chat/ChatReactions.qml
ChatReactionsDetails 1.0 Chat/ChatReactionsDetails.qml
ChatReplyMessage 1.0 Chat/ChatReplyMessage.qml
ChatReplyPreview 1.0 Chat/ChatReplyPreview.qml