Skip to content

Commit

Permalink
fixes #216 - add support for reactions
Browse files Browse the repository at this point in the history
  • Loading branch information
d99kris committed Apr 20, 2024
1 parent 887cae6 commit 1a652d9
Show file tree
Hide file tree
Showing 38 changed files with 1,584 additions and 140 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Features
- Show user status (online, away, typing)
- Toggle to view textized emojis vs. graphical
- View / save media files (documents, photos, videos)
- Send and display reactions


Usage
Expand Down Expand Up @@ -60,11 +61,11 @@ Interactive Commands:
Ctrl-x send message
Ctrl-y toggle show emojis
KeyUp select message
Alt-$ external spell check
Alt-, decrease contact list width
Alt-. increase contact list width
Alt-d delete/leave current chat
Alt-e external editor compose
Alt-s external spell check
Alt-t external telephone call

Interactive Commands for Selected Message:
Expand All @@ -77,6 +78,7 @@ Interactive Commands for Selected Message:
Ctrl-z edit selected message
Alt-w external message viewer
Alt-c copy selected message to clipboard
Alt-s add/remove reaction on selected message

Interactive Commands for Text Input:

Expand Down Expand Up @@ -339,6 +341,7 @@ This configuration file holds general user interface settings. Default content:
phone_number_indicator=
proxy_indicator=🔒
read_indicator=✓
reactions_enabled=1
spell_check_command=
syncing_indicator=⇄
terminal_bell_active=0
Expand Down Expand Up @@ -527,6 +530,10 @@ Specifies top bar text to indicate proxy is enabled.

Specifies text to indicate a message has been read by the receiver.

### reactions_enabled

Specifies whether to display reactions.

### spell_check_command

Specifies a custom command to use for spell checking composed messages. If not
Expand Down Expand Up @@ -654,6 +661,8 @@ This configuration file holds user interface color settings. Default content:
history_text_attr_selected=reverse
history_text_quoted_color_bg=
history_text_quoted_color_fg=gray
history_text_reaction_color_bg=
history_text_reaction_color_fg=gray
history_text_recv_color_bg=
history_text_recv_color_fg=
history_text_recv_group_color_bg=
Expand Down
92 changes: 88 additions & 4 deletions lib/common/src/protocol.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
#pragma once

#include <functional>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <unordered_set>
#include <vector>
Expand All @@ -24,6 +26,8 @@ enum ProtocolFeature
FeatureTypingTimeout = (1 << 1),
FeatureEditMessagesWithinTwoDays = (1 << 2),
FeatureEditMessagesWithinFifteenMins = (1 << 3),
FeatureLimitedReactions = (1 << 4),
FeatureMarkReadEveryView = (1 << 5),
};

class Protocol
Expand Down Expand Up @@ -77,6 +81,9 @@ enum MessageType
CreateChatRequestType,
SetCurrentChatRequestType,
DeferGetSponsoredMessagesRequestType,
GetAvailableReactionsRequestType,
SendReactionRequestType,
GetUnreadReactionsRequestType,
ServiceMessageType,
NewContactsNotifyType,
NewChatsNotifyType,
Expand All @@ -96,6 +103,8 @@ enum MessageType
UpdateMuteNotifyType,
ProtocolUiControlNotifyType,
RequestAppExitNotifyType,
NewMessageReactionsNotifyType,
AvailableReactionsNotifyType,
};

struct ContactInfo
Expand All @@ -110,7 +119,7 @@ struct ChatInfo
{
std::string id;
bool isUnread = false;
bool isUnreadMention = false; // tgchat only
bool isUnreadMention = false; // only required for tgchat
bool isMuted = false;
int64_t lastMessageTime = -1;
};
Expand All @@ -132,6 +141,31 @@ struct FileInfo
std::string fileType;
};

// ensure CacheUtil and Serialization are up-to-date after modifying Reactions
static const std::string s_ReactionsSelfId = "You";
struct Reactions
{
bool needConsolidationWithCache = false; // true = need consolidation with cache before usage
bool updateCountBasedOnSender = false; // true = need to update emojiCount based on senderEmoji
bool replaceCount = false; // true = replace emoji counts
std::map<std::string, std::string> senderEmojis;
std::map<std::string, size_t> emojiCounts;

bool operator==(const Reactions& p_Other) const
{
if (senderEmojis != p_Other.senderEmojis) return false;

if (emojiCounts != p_Other.emojiCounts) return false;

return true;
}

bool operator!=(const Reactions& p_Other) const
{
return (*this == p_Other);
}
};

struct ChatMessage
{
std::string id;
Expand All @@ -141,11 +175,12 @@ struct ChatMessage
std::string quotedText;
std::string quotedSender;
std::string fileInfo;
std::string link; // tgchat sponsored msg only, not db cached
std::string link; // only required for tgchat, sponsored msg, not db cached
Reactions reactions;
int64_t timeSent = -1;
bool isOutgoing = true;
bool isRead = false;
bool hasMention = false; // tgchat only, not db cached
bool hasMention = false; // only required for tgchat, not db cached
};

enum DownloadFileAction
Expand Down Expand Up @@ -225,14 +260,15 @@ class MarkMessageReadRequest : public RequestMessage
std::string chatId;
std::string senderId; // only required for wmchat
std::string msgId;
bool readAllReactions = false;
};

class DeleteMessageRequest : public RequestMessage
{
public:
virtual MessageType GetMessageType() const { return DeleteMessageRequestType; }
std::string chatId;
std::string senderId; // only needed for wmchat
std::string senderId; // only required for wmchat
std::string msgId;
};

Expand Down Expand Up @@ -322,6 +358,32 @@ class DeferGetSponsoredMessagesRequest : public RequestMessage
std::string chatId;
};

class GetAvailableReactionsRequest : public RequestMessage
{
public:
virtual MessageType GetMessageType() const { return GetAvailableReactionsRequestType; }
std::string chatId;
std::string msgId;
};

class SendReactionRequest : public RequestMessage
{
public:
virtual MessageType GetMessageType() const { return SendReactionRequestType; }
std::string chatId;
std::string senderId; // only required for wmchat
std::string msgId;
std::string emoji;
std::string prevEmoji; // only required for tgchat, to clear reaction
};

class GetUnreadReactionsRequest : public RequestMessage
{
public:
virtual MessageType GetMessageType() const { return GetUnreadReactionsRequestType; }
std::string chatId;
};

// Service messages
class ServiceMessage
{
Expand Down Expand Up @@ -529,3 +591,25 @@ class RequestAppExitNotify : public ServiceMessage
ServiceMessage(p_ProfileId) { }
virtual MessageType GetMessageType() const { return RequestAppExitNotifyType; }
};

class NewMessageReactionsNotify : public ServiceMessage
{
public:
explicit NewMessageReactionsNotify(const std::string& p_ProfileId) :
ServiceMessage(p_ProfileId) { }
virtual MessageType GetMessageType() const { return NewMessageReactionsNotifyType; }
std::string chatId;
std::string msgId;
Reactions reactions;
};

class AvailableReactionsNotify : public ServiceMessage
{
public:
explicit AvailableReactionsNotify(const std::string& p_ProfileId) :
ServiceMessage(p_ProfileId) { }
virtual MessageType GetMessageType() const { return AvailableReactionsNotifyType; }
std::string chatId;
std::string msgId;
std::set<std::string> emojis;
};
2 changes: 1 addition & 1 deletion lib/common/src/version.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@

#pragma once

#define NCHAT_VERSION "4.59"
#define NCHAT_VERSION "4.60"
11 changes: 10 additions & 1 deletion lib/duchat/src/duchat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ void DuChat::PerformRequest(std::shared_ptr<RequestMessage> p_RequestMessage)
{ "Michael",
"Pam, what you don't understand is that at my level you just don't "
"look in the want-ads for a job. You are head-hunted." },
{ "Jim", "You called any headhunters?" },
{ "Jim", "You've called any headhunters?" },
{ "Michael", "Any good headhunter knows I am available." },
{ "Dwight",
"Any really good headhunter would storm your village at sunset with "
Expand Down Expand Up @@ -315,6 +315,7 @@ void DuChat::PerformRequest(std::shared_ptr<RequestMessage> p_RequestMessage)
newContactsNotify->contactInfos.push_back(contactInfo);

// From others
bool isFirst = true;
for (auto& message : groupMessages)
{
std::string name = message.first;
Expand All @@ -327,6 +328,14 @@ void DuChat::PerformRequest(std::shared_ptr<RequestMessage> p_RequestMessage)
chatMessage.timeSent = (t * 1000);
chatMessage.isOutgoing = (id == sid);
chatMessage.isRead = true;

if (isFirst)
{
isFirst = false;
chatMessage.reactions.emojiCounts["😨"] = 4;
chatMessage.reactions.emojiCounts["🤣"] = 2;
}

t = t - 100;
s_Messages[gid].push_back(chatMessage);
}
Expand Down
4 changes: 4 additions & 0 deletions lib/ncutil/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ add_library(ncutil SHARED
src/appconfig.h
src/apputil.cpp
src/apputil.h
src/cacheutil.cpp
src/cacheutil.h
src/clipboard.cpp
src/clipboard.h
src/config.cpp
Expand All @@ -48,6 +50,7 @@ add_library(ncutil SHARED
src/protocolutil.h
src/scopeddirlock.cpp
src/scopeddirlock.h
src/serialization.h
src/sqlitehelp.cpp
src/sqlitehelp.h
src/status.cpp
Expand All @@ -72,6 +75,7 @@ target_include_directories(ncutil PRIVATE "../common/src")
target_include_directories(ncutil PRIVATE "../../ext/apathy")
target_include_directories(ncutil PRIVATE "../../ext/sqlite_modern_cpp/hdr")
target_include_directories(ncutil PRIVATE "../../ext/clip")
target_include_directories(ncutil PRIVATE "../../ext/cereal/include")

# Compiler flags
set_target_properties(ncutil PROPERTIES COMPILE_FLAGS
Expand Down
102 changes: 102 additions & 0 deletions lib/ncutil/src/cacheutil.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// messagecache.cpp
//
// Copyright (c) 2024 Kristofer Berggren
// All rights reserved.
//
// nchat is distributed under the MIT license, see LICENSE for details.

#include "cacheutil.h"

#include <string>
#include <sstream>

#include "log.h"

// #define DEBUG_UPDATE_REACTIONS

// Determines whether a Reactions instance needs serialization
bool CacheUtil::IsDefaultReactions(const Reactions& p_Reactions)
{
return p_Reactions.senderEmojis.empty() && p_Reactions.emojiCounts.empty() &&
!p_Reactions.updateCountBasedOnSender && !p_Reactions.needConsolidationWithCache && !p_Reactions.replaceCount;
}

// Debug helper
std::string CacheUtil::ReactionsToString(const Reactions& p_Reactions)
{
std::stringstream sstream;
sstream << "needConsolidation=" << p_Reactions.needConsolidationWithCache << " ";
sstream << "updateCount=" << p_Reactions.updateCountBasedOnSender << " ";
sstream << "replaceCount=" << p_Reactions.replaceCount << " ";
sstream << "senderEmojis=[ ";
for (const auto& senderEmoji : p_Reactions.senderEmojis)
{
sstream << "(" << senderEmoji.first << ": " << senderEmoji.second << ") ";
}
sstream << "] ";

sstream << "emojiCounts=[ ";
for (const auto& emojiCount : p_Reactions.emojiCounts)
{
sstream << "(" << emojiCount.first << ": " << emojiCount.second << ") ";
}
sstream << "] ";

return sstream.str();
}

// This function takes an original Reactions instance, p_Source, and adds/removes senderEmojis
// based on an "update" Reactions instance, p_Target. Then counting of emoji types is done and
// result is stored in emojiCounts.
void CacheUtil::UpdateReactions(const Reactions& p_Source, Reactions& p_Target)
{
#ifdef DEBUG_UPDATE_REACTIONS
LOG_INFO("update reactions");
LOG_INFO("source: %s", ReactionsToString(p_Source).c_str());
LOG_INFO("target: %s", ReactionsToString(p_Target).c_str());
#endif

// Update senderEmojis
std::map<std::string, std::string> combinedSenderEmojis = p_Source.senderEmojis;
for (const auto& senderEmoji : p_Target.senderEmojis)
{
if (senderEmoji.second.empty())
{
combinedSenderEmojis.erase(senderEmoji.first);
}
else
{
combinedSenderEmojis[senderEmoji.first] = senderEmoji.second;
}
}

p_Target.senderEmojis = combinedSenderEmojis;

// Handle replace count
if (p_Target.replaceCount)
{
// Do nothing, using provided emojiCounts
}
else
{
p_Target.emojiCounts = p_Source.emojiCounts;
}

// Update emojiCounts based on senderEmojis
if (p_Target.updateCountBasedOnSender)
{
p_Target.emojiCounts.clear();
for (const auto& senderEmoji : p_Target.senderEmojis)
{
p_Target.emojiCounts[senderEmoji.second] += 1;
}
}

p_Target.needConsolidationWithCache = false;
p_Target.updateCountBasedOnSender = false;
p_Target.replaceCount = false;

#ifdef DEBUG_UPDATE_REACTIONS
LOG_INFO("result: %s", ReactionsToString(p_Target).c_str());
#endif
}
Loading

0 comments on commit 1a652d9

Please sign in to comment.