Merge pull request #2091 from csoler/v0.6-BugFixing_2

[WIP] Bug fixing before 0.6.6
This commit is contained in:
csoler 2020-11-08 20:07:16 +01:00 committed by GitHub
commit e88dfecc55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1524 additions and 1153 deletions

View File

@ -63,6 +63,72 @@ static const uint32_t INDEX_AUTHEN_ADMIN = 0x00000040; // admin key
#define GEN_EXCH_DEBUG 1
// Data flow in RsGenExchange
//
// publishGroup()
// |
// +--- stores in mGrpsToPublish
//
// updateGroup()
// |
// +--- stores in mGroupUpdatePublish
//
// tick()
// |
// +--- processRecvdData();
// | |
// | +-- processRecvdGroups();
// | | |
// | | +---- mGroupPendingValidate
// | | |
// | | +---- acceptance => validateGrp() => check existance => store in mGroupUpdates
// | | |
// | | +--- validateNxsGrp()
// | |
// | +-- processRecvdMessages();
// | |
// | +-- performUpdateValidation(); // validates group updates in mGroupUpdates received from friends
// | |
// | +---- mGroupUpdates => updateValid() => store new groups
// | | |
// | | +--- stamp keys, check admin keys are the same, validateNxsGrp()
// | | |
// | +---- stores RsGxsGroupChange in mNotifications +--- data signature check
// |
// +--- publishGrps(); // creates signatures and data for locally created groups
// | |
// | +-- reads mGrpsToPublish, creates the group, clears mGrpsToPublish
// | |
// | +--- generateGroupKeys()
// | |
// | +--- serviceCreateGroup()
// | |
// | +--- createGroup()
// | |
// | +--- createGroupSignatures()
// |
// +--- processGroupUpdatePublish(); // goes through list of group updates to perform locally
// | |
// | +-- reads/deletes mGroupUpdatePublish, stores in mGrpsToPublish
// |
// +--- processGrpMetaChanges();
// | |
// | +-- updates mNotifications, calls mDataAccess
// |
// +--- processGroupDelete();
// |
// +--- publishMsgs();
// |
// +--- processMsgMetaChanges();
// | |
// | +-- updates mNotifications, calls mDataAccess
// |
// +--- processMessageDelete();
// |
// +--- mDataAccess->processRequests();
// |
// +--- processRoutingClues() ;
//
static const uint32_t MSG_CLEANUP_PERIOD = 60*59; // 59 minutes
static const uint32_t INTEGRITY_CHECK_PERIOD = 60*31; // 31 minutes
@ -2596,8 +2662,17 @@ void RsGenExchange::publishGrps()
RS_STACK_MUTEX(mGenMtx) ;
NxsGrpSignPendVect::iterator vit = mGrpsToPublish.begin();
typedef std::pair<bool, RsGxsGroupId> GrpNote;
std::map<uint32_t, GrpNote> toNotify;
typedef struct _GrpNote {
_GrpNote(bool success,bool is_update,const RsGxsGroupId& gid) : mSuccess(success),mIsUpdate(is_update),mGroupId(gid){}
bool mSuccess;
bool mIsUpdate;
RsGxsGroupId mGroupId;
} GrpNote;
std::map<uint32_t, GrpNote> toNotify; // used to notify about token processing if success or fails because of timeout
while( vit != mGrpsToPublish.end() )
{
@ -2611,8 +2686,7 @@ void RsGenExchange::publishGrps()
if(now > (ggps.mStartTS + PENDING_SIGN_TIMEOUT) )
{
// timed out
toNotify.insert(std::make_pair(
token, GrpNote(false, RsGxsGroupId())));
toNotify.insert(std::make_pair( token, GrpNote(false,ggps.mIsUpdate, RsGxsGroupId())));
delete ggps.mItem;
vit = mGrpsToPublish.erase(vit);
@ -2723,12 +2797,36 @@ void RsGenExchange::publishGrps()
computeHash(grp->grp, grp->metaData->mHash);
grp->metaData->mRecvTS = time(NULL);
if(ggps.mIsUpdate)
mDataAccess->updateGroupData(grp);
else
mDataAccess->addGroupData(grp);
// Also push in notifications. We do it here when we still have pointers on the old and new groups
delete grp ;
RsGxsGroupChange *c = new RsGxsGroupChange(ggps.mIsUpdate?RsGxsNotify::TYPE_UPDATED:RsGxsNotify::TYPE_PUBLISHED, grpId,false);
c->mNewGroupItem = dynamic_cast<RsGxsGrpItem*>(mSerialiser->deserialise(grp->grp.bin_data,&grp->grp.bin_len));
c->mNewGroupItem->meta = *grp->metaData; // grp will be deleted because mDataStore will destroy it on update
if(ggps.mIsUpdate)
{
RsNxsGrpDataTemporaryMap oldGrpDatas;
oldGrpDatas.insert(std::make_pair(grpId, (RsNxsGrp*)NULL));
if(mDataStore->retrieveNxsGrps(oldGrpDatas,true,false) && oldGrpDatas.size() == 1)
{
auto oldGrp = oldGrpDatas[grpId];
c->mOldGroupItem = dynamic_cast<RsGxsGrpItem*>(mSerialiser->deserialise(oldGrp->grp.bin_data,&oldGrp->grp.bin_len));
c->mOldGroupItem->meta = *oldGrp->metaData;
}
}
mNotifications.push_back(c);
// now store (and incidently delete grp)
if(ggps.mIsUpdate)
mDataAccess->updateGroupData(grp);
else
mDataAccess->addGroupData(grp);
delete grp ;
groups_to_subscribe.push_back(grpId) ;
}
else
@ -2744,7 +2842,7 @@ void RsGenExchange::publishGrps()
// services should return SERVICE_CREATE_FAIL if the action timed out
// at the moment this is only important for the idservice:
// the idservice may ask the user for a password, and the user needs time
ggps.mStartTS = now;
ggps.mStartTS = now;
create = CREATE_FAIL_TRY_LATER;
}
else if(ret == SERVICE_CREATE_FAIL)
@ -2766,8 +2864,7 @@ void RsGenExchange::publishGrps()
delete grp;
delete grpItem;
vit = mGrpsToPublish.erase(vit);
toNotify.insert(std::make_pair(
token, GrpNote(false, grpId)));
toNotify.insert(std::make_pair(token, GrpNote(false, ggps.mIsUpdate,grpId)));
}
else if(create == CREATE_FAIL_TRY_LATER)
@ -2790,30 +2887,21 @@ void RsGenExchange::publishGrps()
#endif
// add to published to allow acknowledgement
toNotify.insert(std::make_pair(token,
GrpNote(true,grpId)));
toNotify.insert(std::make_pair(token, GrpNote(true,ggps.mIsUpdate,grpId)));
}
}
std::map<uint32_t, GrpNote>::iterator mit = toNotify.begin();
// toNotify contains the token and the group update/creation info. We parse it to
// - notify that group creation/update for a specific token was
std::list<RsGxsGroupId> grpChanged;
for(; mit != toNotify.end(); ++mit)
for(auto mit=toNotify.begin(); mit != toNotify.end(); ++mit)
{
GrpNote& note = mit->second;
RsTokenService::GxsRequestStatus status =
note.first ? RsTokenService::COMPLETE
: RsTokenService::FAILED;
RsTokenService::GxsRequestStatus status = note.mSuccess ? RsTokenService::COMPLETE : RsTokenService::FAILED;
mGrpNotify.insert(std::make_pair(mit->first, note.second));
mDataAccess->updatePublicRequestStatus(mit->first, status);
if(note.first)
grpChanged.push_back(note.second);
}
for(auto& groupId:grpChanged)
mNotifications.push_back(new RsGxsGroupChange(RsGxsNotify::TYPE_RECEIVED_NEW,groupId, true));
mGrpNotify.insert(std::make_pair(mit->first, note.mGroupId)); // always notify
mDataAccess->updatePublicRequestStatus(mit->first, status); // update the token request with the given status
}
}
// This is done off-mutex to avoid possible cross deadlocks with the net service.
@ -2823,8 +2911,6 @@ void RsGenExchange::publishGrps()
mNetService->subscribeStatusChanged((*it),true) ;
}
uint32_t RsGenExchange::generatePublicToken()
{
return mDataAccess->generatePublicToken();
@ -3082,7 +3168,7 @@ void RsGenExchange::processRecvdMessages()
mNotifications.push_back(c);
}
mDataStore->storeMessage(msgs_to_store); // we do that late because it destroys the items in msgs_to_store
mDataStore->storeMessage(msgs_to_store); // All items will be destroyed later on, since msgs_to_store is a temporary map
}
}
@ -3224,9 +3310,9 @@ void RsGenExchange::processRecvdGroups()
mDataStore->storeGroup(grps_to_store);
#ifdef GEN_EXCH_DEBUG
std::cerr << " adding the following grp ids to notification: " << std::endl;
for(std::list<RsGxsGroupId>::const_iterator it(grpIds.begin());it!=grpIds.end();++it)
std::cerr << " " << *it << std::endl;
std::cerr << " adding the following grp ids to notification: " << std::endl;
for(std::list<RsGxsGroupId>::const_iterator it(grpIds.begin());it!=grpIds.end();++it)
std::cerr << " " << *it << std::endl;
#endif
}
}
@ -3317,8 +3403,7 @@ void RsGenExchange::performUpdateValidation()
}
}
// Warning: updateGroup will destroy the objects in grps. Dont use it afterwards!
mDataStore->updateGroup(grps);
mDataStore->updateGroup(grps);
#ifdef GEN_EXCH_DEBUG
std::cerr << " adding the following grp ids to notification: " << std::endl;

View File

@ -71,14 +71,15 @@ struct RsGxsChannelGroup : RsSerializable, RsGxsGenericGroupData
struct RsGxsChannelPost : RsSerializable, RsGxsGenericMsgData
{
RsGxsChannelPost() : mCount(0), mSize(0) {}
RsGxsChannelPost() : mAttachmentCount(0), mCommentCount(0), mSize(0) {}
std::set<RsGxsMessageId> mOlderVersions;
std::string mMsg; // UTF8 encoded.
std::list<RsGxsFile> mFiles;
uint32_t mCount; // auto calced.
uint64_t mSize; // auto calced.
uint32_t mAttachmentCount; // auto calced.
uint32_t mCommentCount; // auto calced. WARNING: not computed yet. In the future we need a background process to update this and store in the service string.
uint64_t mSize; // auto calced.
RsGxsImage mThumbnail;
@ -92,8 +93,9 @@ struct RsGxsChannelPost : RsSerializable, RsGxsGenericMsgData
RS_SERIAL_PROCESS(mMsg);
RS_SERIAL_PROCESS(mFiles);
RS_SERIAL_PROCESS(mCount);
RS_SERIAL_PROCESS(mSize);
RS_SERIAL_PROCESS(mAttachmentCount);
RS_SERIAL_PROCESS(mCommentCount);
RS_SERIAL_PROCESS(mSize);
RS_SERIAL_PROCESS(mThumbnail);
}
@ -114,6 +116,8 @@ enum class RsChannelEventCode: uint8_t
RECEIVED_DISTANT_SEARCH_RESULT = 0x08, // result for the given group id available for the given turtle request id
STATISTICS_CHANGED = 0x09, // stats (nb of supplier friends, how many msgs they have etc) has changed
SYNC_PARAMETERS_UPDATED = 0x0a, // sync and storage times have changed
NEW_COMMENT = 0x0b, // new comment arrived/published. mChannelThreadId gives the ID of the commented message
NEW_VOTE = 0x0c, // new vote arrived/published. mChannelThreadId gives the ID of the votes message comment
};
struct RsGxsChannelEvent: RsEvent
@ -123,6 +127,7 @@ struct RsGxsChannelEvent: RsEvent
RsChannelEventCode mChannelEventCode;
RsGxsGroupId mChannelGroupId;
RsGxsMessageId mChannelMsgId;
RsGxsMessageId mChannelThreadId;
///* @see RsEvent @see RsSerializable
void serial_process( RsGenericSerializer::SerializeJob j,RsGenericSerializer::SerializeContext& ctx) override
@ -323,8 +328,11 @@ public:
std::vector<RsGxsChannelGroup>& channelsInfo ) = 0;
/**
* @brief Get all channel messages and comments in a given channel
* @jsonapi{development}
* @brief Get all channel messages, comments and votes in a given channel
* @note It's the client's responsibility to figure out which message (resp. comment)
* a comment (resp. vote) refers to.
*
* @jsonapi{development}
* @param[in] channelId id of the channel of which the content is requested
* @param[out] posts storage for posts
* @param[out] comments storage for the comments
@ -337,11 +345,13 @@ public:
std::vector<RsGxsVote>& votes ) = 0;
/**
* @brief Get channel messages and comments corresponding to the given IDs.
* If the set is empty, nothing is returned.
* @note Since comments are internally themselves messages, it is possible
* to get comments only by supplying their IDs.
* @jsonapi{development}
* @brief Get channel messages, comments and votes corresponding to the given IDs.
* @note Since comments are internally themselves messages, this function actually
* returns the data for messages, comments or votes that have the given ID.
* It *does not* automatically retrieve the comments or votes for a given message
* which Id you supplied.
*
* @jsonapi{development}
* @param[in] channelId id of the channel of which the content is requested
* @param[in] contentsIds ids of requested contents
* @param[out] posts storage for posts
@ -356,8 +366,9 @@ public:
std::vector<RsGxsVote>& votes ) = 0;
/**
* @brief Get channel comments corresponding to the given IDs.
* @brief Get channel comments corresponding to the given message IDs.
* If the set is empty, nothing is returned.
*
* @jsonapi{development}
* @param[in] channelId id of the channel of which the content is requested
* @param[in] contentIds ids of requested contents

View File

@ -116,6 +116,7 @@ enum class RsForumEventCode: uint8_t
STATISTICS_CHANGED = 0x07, /// suppliers and how many messages they have changed
MODERATOR_LIST_CHANGED = 0x08, /// forum moderation list has changed.
SYNC_PARAMETERS_UPDATED = 0x0a, /// sync and storage times have changed
PINNED_POSTS_CHANGED = 0x0b, /// some posts where pinned or un-pinned
};
struct RsGxsForumEvent: RsEvent

View File

@ -117,6 +117,8 @@ enum class RsPostedEventCode: uint8_t
STATISTICS_CHANGED = 0x07,
MESSAGE_VOTES_UPDATED = 0x08,
SYNC_PARAMETERS_UPDATED = 0x09,
NEW_COMMENT = 0x0a,
NEW_VOTE = 0x0b,
};

View File

@ -137,7 +137,7 @@ bool RsGxsChannelPostItem::toChannelPost(RsGxsChannelPost &post, bool moveImage)
post.mThumbnail.copy((uint8_t *) mThumbnail.binData.bin_data, mThumbnail.binData.bin_len);
}
post.mCount = 0;
post.mAttachmentCount = 0;
post.mSize = 0;
std::list<RsTlvFileItem>::iterator fit;
for(fit = mAttachment.items.begin(); fit != mAttachment.items.end(); ++fit)
@ -148,7 +148,7 @@ bool RsGxsChannelPostItem::toChannelPost(RsGxsChannelPost &post, bool moveImage)
fi.mHash = fit->hash;
post.mFiles.push_back(fi);
post.mCount++;
post.mAttachmentCount++;
post.mSize += fi.mSize;
}
return true;

View File

@ -259,7 +259,15 @@ void p3GxsChannels::notifyChanges(std::vector<RsGxsNotify *> &changes)
ev->mChannelMsgId = msgChange->mMsgId;
ev->mChannelGroupId = msgChange->mGroupId;
ev->mChannelEventCode = RsChannelEventCode::NEW_MESSAGE;
if(nullptr != dynamic_cast<RsGxsCommentItem*>(msgChange->mNewMsgItem))
ev->mChannelEventCode = RsChannelEventCode::NEW_COMMENT;
else
if(nullptr != dynamic_cast<RsGxsVoteItem*>(msgChange->mNewMsgItem))
ev->mChannelEventCode = RsChannelEventCode::NEW_VOTE;
else
ev->mChannelEventCode = RsChannelEventCode::NEW_MESSAGE;
rsEvents->postEvent(ev);
}
}
@ -477,10 +485,9 @@ bool p3GxsChannels::groupShareKeys(
* at the moment - fix it up later
*/
bool p3GxsChannels::getPostData(
const uint32_t &token, std::vector<RsGxsChannelPost> &msgs,
std::vector<RsGxsComment> &cmts,
std::vector<RsGxsVote> &vots)
bool p3GxsChannels::getPostData( const uint32_t& token, std::vector<RsGxsChannelPost>& msgs,
std::vector<RsGxsComment>& cmts,
std::vector<RsGxsVote>& vots)
{
#ifdef GXSCHANNELS_DEBUG
RsDbg() << __PRETTY_FUNCTION__ << std::endl;
@ -675,7 +682,7 @@ bool p3GxsChannels::setChannelDownloadDirectory(
}
/* extract from ServiceString */
SSGxsChannelGroup ss;
GxsChannelGroupInfo ss;
ss.load(it->second.mServiceString);
if (directory == ss.mDownloadDirectory)
@ -728,7 +735,7 @@ bool p3GxsChannels::getChannelDownloadDirectory(const RsGxsGroupId & groupId,std
}
/* extract from ServiceString */
SSGxsChannelGroup ss;
GxsChannelGroupInfo ss;
ss.load(it->second.mServiceString);
directory = ss.mDownloadDirectory;
@ -1729,14 +1736,14 @@ bool p3GxsChannels::autoDownloadEnabled(const RsGxsGroupId &groupId,bool& enable
}
/* extract from ServiceString */
SSGxsChannelGroup ss;
GxsChannelGroupInfo ss;
ss.load(it->second.mServiceString);
enabled = ss.mAutoDownload;
return true;
}
bool SSGxsChannelGroup::load(const std::string &input)
bool GxsChannelGroupInfo::load(const std::string &input)
{
if(input.empty())
{
@ -1785,7 +1792,7 @@ bool SSGxsChannelGroup::load(const std::string &input)
return true;
}
std::string SSGxsChannelGroup::save() const
std::string GxsChannelGroupInfo::save() const
{
std::string output = "v2 ";
@ -1827,7 +1834,7 @@ bool p3GxsChannels::setAutoDownload(const RsGxsGroupId& groupId, bool enabled)
}
/* extract from ServiceString */
SSGxsChannelGroup ss;
GxsChannelGroupInfo ss;
ss.load(it->second.mServiceString);
if (enabled == ss.mAutoDownload)
{

View File

@ -35,11 +35,12 @@
#include <string>
// This class is only a helper to parse the channel group service string.
class SSGxsChannelGroup
class GxsChannelGroupInfo
{
public:
SSGxsChannelGroup(): mAutoDownload(false), mDownloadDirectory("") {}
GxsChannelGroupInfo(): mAutoDownload(false), mDownloadDirectory("") {}
bool load(const std::string &input);
std::string save() const;
@ -55,31 +56,31 @@ class p3GxsChannels: public RsGenExchange, public RsGxsChannels,
public:
p3GxsChannels( RsGeneralDataService* gds, RsNetworkExchangeService* nes,
RsGixs* gixs );
virtual RsServiceInfo getServiceInfo();
virtual RsServiceInfo getServiceInfo() override;
virtual void service_tick();
virtual void service_tick() override;
protected:
virtual RsSerialiser* setupSerialiser(); // @see p3Config::setupSerialiser()
virtual bool saveList(bool &cleanup, std::list<RsItem *>&saveList); // @see p3Config::saveList(bool &cleanup, std::list<RsItem *>&)
virtual bool loadList(std::list<RsItem *>& loadList); // @see p3Config::loadList(std::list<RsItem *>&)
virtual RsSerialiser* setupSerialiser() override; // @see p3Config::setupSerialiser()
virtual bool saveList(bool &cleanup, std::list<RsItem *>&saveList) override; // @see p3Config::saveList(bool &cleanup, std::list<RsItem *>&)
virtual bool loadList(std::list<RsItem *>& loadList) override; // @see p3Config::loadList(std::list<RsItem *>&)
virtual TurtleRequestId turtleGroupRequest(const RsGxsGroupId& group_id);
virtual TurtleRequestId turtleSearchRequest(const std::string& match_string);
virtual bool retrieveDistantSearchResults(TurtleRequestId req, std::map<RsGxsGroupId, RsGxsGroupSearchResults> &results) ;
virtual bool clearDistantSearchResults(TurtleRequestId req);
virtual bool getDistantSearchResultGroupData(const RsGxsGroupId& group_id,RsGxsChannelGroup& distant_group);
virtual DistantSearchGroupStatus getDistantSearchStatus(const RsGxsGroupId& group_id) ;
virtual TurtleRequestId turtleGroupRequest(const RsGxsGroupId& group_id) override;
virtual TurtleRequestId turtleSearchRequest(const std::string& match_string) override;
virtual bool retrieveDistantSearchResults(TurtleRequestId req, std::map<RsGxsGroupId, RsGxsGroupSearchResults> &results) override;
virtual bool clearDistantSearchResults(TurtleRequestId req) override;
virtual bool getDistantSearchResultGroupData(const RsGxsGroupId& group_id,RsGxsChannelGroup& distant_group) override;
virtual DistantSearchGroupStatus getDistantSearchStatus(const RsGxsGroupId& group_id) override;
// Overloaded to cache new groups.
virtual RsGenExchange::ServiceCreate_Return service_CreateGroup(RsGxsGrpItem* grpItem, RsTlvSecurityKeySet& keySet);
virtual RsGenExchange::ServiceCreate_Return service_CreateGroup(RsGxsGrpItem* grpItem, RsTlvSecurityKeySet& keySet) override;
virtual void notifyChanges(std::vector<RsGxsNotify*>& changes);
virtual void notifyChanges(std::vector<RsGxsNotify*>& changes) override;
// Overloaded from RsTickEvent.
virtual void handle_event(uint32_t event_type, const std::string &elabel);
virtual void handle_event(uint32_t event_type, const std::string &elabel) override;
public:
@ -97,18 +98,18 @@ virtual bool getPostData(const uint32_t &token, std::vector<RsGxsChannelPost> &p
//virtual bool groupRestoreKeys(const std::string &groupId);
virtual bool groupShareKeys(
const RsGxsGroupId &groupId, const std::set<RsPeerId>& peers);
const RsGxsGroupId &groupId, const std::set<RsPeerId>& peers) override;
virtual bool createGroup(uint32_t &token, RsGxsChannelGroup &group);
virtual bool createPost(uint32_t &token, RsGxsChannelPost &post);
virtual bool createGroup(uint32_t &token, RsGxsChannelGroup &group) override;
virtual bool createPost(uint32_t &token, RsGxsChannelPost &post) override;
virtual bool updateGroup(uint32_t &token, RsGxsChannelGroup &group);
virtual bool updateGroup(uint32_t &token, RsGxsChannelGroup &group) override;
// no tokens... should be cached.
virtual bool setChannelAutoDownload(const RsGxsGroupId &groupId, bool enabled);
virtual bool getChannelAutoDownload(const RsGxsGroupId &groupid, bool& enabled);
virtual bool setChannelDownloadDirectory(const RsGxsGroupId &groupId, const std::string& directory);
virtual bool getChannelDownloadDirectory(const RsGxsGroupId &groupId, std::string& directory);
virtual bool setChannelAutoDownload(const RsGxsGroupId &groupId, bool enabled) override;
virtual bool getChannelAutoDownload(const RsGxsGroupId &groupid, bool& enabled) override;
virtual bool setChannelDownloadDirectory(const RsGxsGroupId &groupId, const std::string& directory) override;
virtual bool getChannelDownloadDirectory(const RsGxsGroupId &groupId, std::string& directory) override;
#ifdef TO_REMOVE
/// @see RsGxsChannels::turtleSearchRequest

View File

@ -683,8 +683,6 @@ void p3GxsCircles::notifyChanges(std::vector<RsGxsNotify *> &changes)
RsGxsCircleGroupItem *old_circle_grp_item = dynamic_cast<RsGxsCircleGroupItem*>(groupChange->mOldGroupItem);
RsGxsCircleGroupItem *new_circle_grp_item = dynamic_cast<RsGxsCircleGroupItem*>(groupChange->mNewGroupItem);
const RsGxsCircleId circle_id ( old_circle_grp_item->meta.mGroupId );
if(old_circle_grp_item == nullptr || new_circle_grp_item == nullptr)
{
RsErr() << __PRETTY_FUNCTION__ << " received GxsGroupUpdate item with mOldGroup and mNewGroup not of type RsGxsCircleGroupItem. This is inconsistent!" << std::endl;
@ -692,7 +690,9 @@ void p3GxsCircles::notifyChanges(std::vector<RsGxsNotify *> &changes)
continue;
}
// First of all, we check if there is a difference between the old and new list of invited members
const RsGxsCircleId circle_id ( old_circle_grp_item->meta.mGroupId );
// First of all, we check if there is a difference between the old and new list of invited members
for(auto& gxs_id: new_circle_grp_item->gxsIdSet.ids)
if(old_circle_grp_item->gxsIdSet.ids.find(gxs_id) == old_circle_grp_item->gxsIdSet.ids.end())

View File

@ -307,7 +307,7 @@ void p3GxsForums::notifyChanges(std::vector<RsGxsNotify *> &changes)
if(old_forum_grp_item == nullptr || new_forum_grp_item == nullptr)
{
RsErr() << __PRETTY_FUNCTION__ << " received GxsGroupUpdate item with mOldGroup and mNewGroup not of type RsGxsForumGroupItem. This is inconsistent!" << std::endl;
RsErr() << __PRETTY_FUNCTION__ << " received GxsGroupUpdate item with mOldGroup and mNewGroup not of type RsGxsForumGroupItem or NULL. This is inconsistent!" << std::endl;
delete grpChange;
continue;
}
@ -335,6 +335,29 @@ void p3GxsForums::notifyChanges(std::vector<RsGxsNotify *> &changes)
rsEvents->postEvent(ev);
}
// check the list of pinned posts
std::list<RsGxsMessageId> added_pins, removed_pins;
for(auto& msg_id: new_forum_grp_item->mGroup.mPinnedPosts.ids)
if(old_forum_grp_item->mGroup.mPinnedPosts.ids.find(msg_id) == old_forum_grp_item->mGroup.mPinnedPosts.ids.end())
added_pins.push_back(msg_id);
for(auto& msg_id: old_forum_grp_item->mGroup.mPinnedPosts.ids)
if(new_forum_grp_item->mGroup.mPinnedPosts.ids.find(msg_id) == new_forum_grp_item->mGroup.mPinnedPosts.ids.end())
removed_pins.push_back(msg_id);
if(!added_pins.empty() || !removed_pins.empty())
{
auto ev = std::make_shared<RsGxsForumEvent>();
ev->mForumGroupId = new_forum_grp_item->meta.mGroupId;
ev->mForumEventCode = RsForumEventCode::PINNED_POSTS_CHANGED;
rsEvents->postEvent(ev);
}
}
break;

View File

@ -107,10 +107,18 @@ void p3PostBase::notifyChanges(std::vector<RsGxsNotify *> &changes)
case RsGxsNotify::TYPE_PUBLISHED:
{
auto ev = std::make_shared<RsGxsPostedEvent>();
ev->mPostedMsgId = msgChange->mMsgId;
ev->mPostedMsgId = msgChange->mMsgId;
ev->mPostedThreadId = msgChange->mNewMsgItem->meta.mThreadId;
ev->mPostedGroupId = msgChange->mGroupId;
ev->mPostedEventCode = RsPostedEventCode::NEW_MESSAGE;
ev->mPostedGroupId = msgChange->mGroupId;
if(nullptr != dynamic_cast<RsGxsCommentItem*>(msgChange->mNewMsgItem))
ev->mPostedEventCode = RsPostedEventCode::NEW_COMMENT;
else
if(nullptr != dynamic_cast<RsGxsVoteItem*>(msgChange->mNewMsgItem))
ev->mPostedEventCode = RsPostedEventCode::NEW_VOTE;
else
ev->mPostedEventCode = RsPostedEventCode::NEW_MESSAGE;
rsEvents->postEvent(ev);
#ifdef POSTBASE_DEBUG
std::cerr << "p3PostBase::notifyChanges() Found Message Change Notification: NEW/PUBLISHED ID=" << msgChange->mMsgId << " in group " << msgChange->mGroupId << ", thread ID = " << msgChange->mNewMsgItem->meta.mThreadId << std::endl;

View File

@ -146,7 +146,7 @@ void PostedPostDelegate::paint(QPainter * painter, const QStyleOptionViewItem &
painter->restore();
}
QSize PostedPostDelegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const
QSize PostedPostDelegate::sizeHint(const QStyleOptionViewItem&, const QModelIndex& index) const
{
// This is the only place where we actually set the size of cells
@ -474,7 +474,8 @@ void PostedListWidgetWithModel::handleEvent_main_thread(std::shared_ptr<const Rs
switch(e->mPostedEventCode)
{
case RsPostedEventCode::NEW_MESSAGE: // [[fallthrough]];
case RsPostedEventCode::NEW_COMMENT: // [[fallthrough]];
case RsPostedEventCode::NEW_VOTE: // [[fallthrough]];
{
// special treatment here because the message might be a comment, so we need to refresh the comment tab if openned
@ -486,6 +487,7 @@ void PostedListWidgetWithModel::handleEvent_main_thread(std::shared_ptr<const Rs
t->refresh();
}
}
case RsPostedEventCode::NEW_MESSAGE: // [[fallthrough]];
case RsPostedEventCode::NEW_POSTED_GROUP: // [[fallthrough]];
case RsPostedEventCode::UPDATED_POSTED_GROUP: // [[fallthrough]];
case RsPostedEventCode::UPDATED_MESSAGE:
@ -877,8 +879,10 @@ void PostedListWidgetWithModel::insertBoardDetails(const RsPostedGroup& group)
sync_string = tr("Unknown");
}
if(group.mMeta.mLastPost > 0 && group.mMeta.mLastPost + rsPosted->getSyncPeriod(group.mMeta.mGroupId) < time(NULL) && IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags))
sync_string += " (Warning: will not allow latest posts to sync)";
auto sync_period = rsPosted->getSyncPeriod(group.mMeta.mGroupId) ;
if(sync_period > 0 && group.mMeta.mLastPost > 0 && group.mMeta.mLastPost + sync_period < time(NULL) && IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags))
sync_string += " (Warning: will not allow posts to sync)";
ui->syncPeriodLabel->setText(sync_string);

View File

@ -589,6 +589,23 @@ p, li { white-space: pre-wrap; }
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEditClear</class>
<extends>QLineEdit</extends>
<header location="global">gui/common/LineEditClear.h</header>
</customwidget>
<customwidget>
<class>RSTabWidget</class>
<extends>QTabWidget</extends>
<header>gui/common/RSTabWidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>RSTreeView</class>
<extends>QTreeView</extends>
<header>gui/common/RSTreeView.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>GxsIdLabel</class>
<extends>QLabel</extends>
@ -599,27 +616,11 @@ p, li { white-space: pre-wrap; }
<extends>QToolButton</extends>
<header>gui/common/SubscribeToolButton.h</header>
</customwidget>
<customwidget>
<class>RSTreeView</class>
<extends>QTreeView</extends>
<header>gui/common/RSTreeView.h</header>
</customwidget>
<customwidget>
<class>LineEditClear</class>
<extends>QLineEdit</extends>
<header>gui/common/LineEditClear.h</header>
</customwidget>
<customwidget>
<class>GxsIdChooser</class>
<extends>QComboBox</extends>
<header>gui/gxs/GxsIdChooser.h</header>
</customwidget>
<customwidget>
<class>RSTabWidget</class>
<extends>QTabWidget</extends>
<header>gui/common/RSTabWidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../icons.qrc"/>

View File

@ -578,10 +578,7 @@ void NewFriendList::peerTreeWidgetCustomPopupMenu()
mModel->getGroupData(index,group_info);
bool standard = group_info.flag & RS_GROUP_FLAG_STANDARD;
#ifdef RS_DIRECT_CHAT
contextMenu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MSG), tr("Send message to whole group"), this, SLOT(msgGroup()));
contextMenu.addSeparator();
#endif // RS_DIRECT_CHAT
contextMenu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_EDIT), tr("Edit Group"), this, SLOT(editGroup()));
QAction *action = contextMenu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_REMOVE), tr("Remove Group"), this, SLOT(removeGroup()));

View File

@ -573,9 +573,9 @@ void GxsChannelPostItem::fill()
ui->datetimelabel->setText(DateTime::formatLongDateTime(mPost.mMeta.mPublishTs));
if ( (mPost.mCount != 0) || (mPost.mSize != 0) ) {
if ( (mPost.mAttachmentCount != 0) || (mPost.mSize != 0) ) {
ui->filelabel->setVisible(true);
ui->filelabel->setText(QString("(%1 %2) %3").arg(mPost.mCount).arg( (mPost.mCount > 1)?tr("Files"):tr("File")).arg(misc::friendlyUnit(mPost.mSize)));
ui->filelabel->setText(QString("(%1 %2) %3").arg(mPost.mAttachmentCount).arg( (mPost.mAttachmentCount > 1)?tr("Files"):tr("File")).arg(misc::friendlyUnit(mPost.mSize)));
} else {
ui->filelabel->setVisible(false);
}

View File

@ -86,6 +86,13 @@ GxsCommentDialog::~GxsCommentDialog()
delete(ui);
}
void GxsCommentDialog::commentClear()
{
ui->treeWidget->clear();
mGrpId.clear();
mMostRecentMsgId.clear();
mMsgVersions.clear();
}
void GxsCommentDialog::commentLoad(const RsGxsGroupId &grpId, const std::set<RsGxsMessageId>& msg_versions,const RsGxsMessageId& most_recent_msgId,bool use_cache)
{
std::cerr << "GxsCommentDialog::commentLoad(" << grpId << ", most recent msg version: " << most_recent_msgId << ")";

View File

@ -39,6 +39,7 @@ public:
void setTokenService(RsTokenService *token_service, RsGxsCommentService *comment_service);
void setCommentHeader(QWidget *header);
void commentLoad(const RsGxsGroupId &grpId, const std::set<RsGxsMessageId> &msg_versions, const RsGxsMessageId &most_recent_msgId, bool use_cache=false);
void commentClear();
RsGxsGroupId groupId() { return mGrpId; }
RsGxsMessageId messageId() { return mMostRecentMsgId; }

View File

@ -28,6 +28,8 @@
#include <QAbstractTextDocumentLayout>
#include <QApplication>
#include <QTextEdit>
#include <QHeaderView>
#include <QClipboard>
#include <QDateTime>
#include <QMenu>
@ -68,6 +70,8 @@
std::map<RsGxsMessageId, std::vector<RsGxsComment> > GxsCommentTreeWidget::mCommentsCache;
QMutex GxsCommentTreeWidget::mCacheMutex;
//#define USE_NEW_DELEGATE 1
// This class allows to draw the item using an appropriate size
class MultiLinesCommentDelegate: public QStyledItemDelegate
@ -84,7 +88,7 @@ public:
{
Q_ASSERT(index.isValid());
QStyleOptionViewItemV4 opt = option;
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
// disable default icon
opt.icon = QIcon();
@ -107,7 +111,6 @@ public:
QSizeF s = td.documentLayout()->documentSize();
int m = QFontMetricsF(QFont()).height();
QSize full_area(std::min(r.width(),(int)s.width())+m,std::min(r.height(),(int)s.height())+m);
QPixmap px(full_area.width(),full_area.height());
@ -115,6 +118,7 @@ public:
QPainter p(&px) ;
p.setRenderHint(QPainter::Antialiasing);
QPainterPath path ;
path.addRoundedRect(QRectF(m/4.0,m/4.0,s.width()+m/2.0,s.height()+m/2.0),m,m) ;
QPen pen(Qt::gray,m/7.0f);
@ -127,6 +131,7 @@ public:
p.translate(QPointF(m/2.0,m/2.0));
td.documentLayout()->draw( &p, ctx );
painter->drawPixmap(r.topLeft(),px);
const_cast<QAbstractItemModel*>(index.model())->setData(index,px.size(),POST_CELL_SIZE_ROLE);
@ -136,11 +141,114 @@ private:
QFontMetricsF qf;
};
GxsCommentTreeWidget::GxsCommentTreeWidget(QWidget *parent)
:QTreeWidget(parent), mTokenQueue(NULL), mRsTokenService(NULL), mCommentService(NULL)
#ifdef USE_NEW_DELEGATE
class NoEditDelegate: public QStyledItemDelegate
{
// QTreeWidget* widget = this;
public:
NoEditDelegate(QObject* parent=0): QStyledItemDelegate(parent) {}
virtual QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
return nullptr;
}
};
class GxsCommentDelegate: public QStyledItemDelegate
{
public:
GxsCommentDelegate(QFontMetricsF f) : qf(f){}
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex &index) const override
{
return index.data(POST_CELL_SIZE_ROLE).toSize() ;
}
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex& index) const override
{
if(index.column() == PCITEM_COLUMN_COMMENT)
editor->setGeometry(option.rect);
}
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
if(index.column() == PCITEM_COLUMN_COMMENT)
{
QTextEdit *b = new QTextEdit(parent);
b->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
b->setFixedSize(option.rect.size());
b->setAcceptRichText(true);
b->setTextInteractionFlags(Qt::TextSelectableByMouse|Qt::LinksAccessibleByMouse);
b->document()->setHtml("<html>"+index.data(Qt::DisplayRole).toString()+"</html>");
b->adjustSize();
return b;
}
else
return nullptr;
}
virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override
{
if((option.state & QStyle::State_Selected)) // Avoids double display. The selected widget is never exactly the size of the rendered one,
return; // so when selected, we only draw the selected one.
Q_ASSERT(index.isValid());
QStyleOptionViewItem opt = option;
initStyleOption(&opt, index);
// disable default icon
opt.icon = QIcon();
opt.text = QString();
const QRect r = option.rect.adjusted(0,0,-option.decorationSize.width(),0);
QTextDocument td ;
td.setHtml("<html>"+index.data(Qt::DisplayRole).toString()+"</html>");
td.setTextWidth(r.width());
QSizeF s = td.documentLayout()->documentSize();
int m = QFontMetricsF(QFont()).height() ;
//int m = 2;
QSize full_area(std::min(r.width(),(int)s.width())+m,std::min(r.height(),(int)s.height())+m);
QPixmap px(full_area.width(),full_area.height());
px.fill(QColor(0,0,0,0));//Transparent background as item background is already paint.
QPainter p(&px) ;
p.setRenderHint(QPainter::Antialiasing);
QTextEdit b;
b.setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
b.setFixedSize(full_area);
b.setAcceptRichText(true);
b.setTextInteractionFlags(Qt::TextSelectableByMouse|Qt::LinksAccessibleByMouse);
b.document()->setHtml("<html>"+index.data(Qt::DisplayRole).toString()+"</html>");
b.adjustSize();
b.render(&p,QPoint(),QRegion(),QWidget::DrawChildren );// draw the widgets, not the background
painter->drawPixmap(opt.rect.topLeft(),px);
const_cast<QAbstractItemModel*>(index.model())->setData(index,px.size(),POST_CELL_SIZE_ROLE);
}
private:
QFontMetricsF qf;
};
#endif
void GxsCommentTreeWidget::mouseMoveEvent(QMouseEvent *e)
{
QModelIndex idx = indexAt(e->pos());
if(idx != selectionModel()->currentIndex())
selectionModel()->setCurrentIndex(idx,QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
QTreeView::mouseMoveEvent(e);
}
GxsCommentTreeWidget::GxsCommentTreeWidget(QWidget *parent)
:QTreeWidget(parent), mTokenQueue(NULL), mRsTokenService(NULL), mCommentService(NULL)
{
setVerticalScrollMode(ScrollPerPixel);
setContextMenuPolicy(Qt::CustomContextMenu);
RSElidedItemDelegate *itemDelegate = new RSElidedItemDelegate(this);
@ -148,29 +256,45 @@ GxsCommentTreeWidget::GxsCommentTreeWidget(QWidget *parent)
setItemDelegate(itemDelegate);
setWordWrap(true);
setSelectionBehavior(QAbstractItemView::SelectRows);
#ifdef USE_NEW_DELEGATE
setMouseTracking(true); // for auto selection
setItemDelegateForColumn(PCITEM_COLUMN_COMMENT,new GxsCommentDelegate(QFontMetricsF(font()))) ;
// Apparently the following below is needed, since there is no way to set item flags for a single column
// so after setting flags Qt will believe that all columns are editable.
setItemDelegateForColumn(PCITEM_COLUMN_AUTHOR, new NoEditDelegate(this));
setItemDelegateForColumn(PCITEM_COLUMN_DATE, new NoEditDelegate(this));
setItemDelegateForColumn(PCITEM_COLUMN_SCORE, new NoEditDelegate(this));
setItemDelegateForColumn(PCITEM_COLUMN_UPVOTES, new NoEditDelegate(this));
setItemDelegateForColumn(PCITEM_COLUMN_DOWNVOTES,new NoEditDelegate(this));
setItemDelegateForColumn(PCITEM_COLUMN_OWNVOTE, new NoEditDelegate(this));
QObject::connect(header(),SIGNAL(geometriesChanged()),this,SLOT(updateContent()));
QObject::connect(header(),SIGNAL(sectionResized(int,int,int)),this,SLOT(updateContent()));
setEditTriggers(QAbstractItemView::CurrentChanged | QAbstractItemView::SelectedClicked);
#else
setItemDelegateForColumn(PCITEM_COLUMN_COMMENT,new MultiLinesCommentDelegate(QFontMetricsF(font()))) ;
#endif
commentsRole = new RSTreeWidgetItemCompareRole;
commentsRole->setRole(PCITEM_COLUMN_DATE, ROLE_SORT);
mUseCache = false;
// QFont font = QFont("ARIAL", 10);
// font.setBold(true);
// QString name("test");
// QTreeWidgetItem *item = new QTreeWidgetItem();
// item->setText(0, name);
// item->setFont(0, font);
// item->setSizeHint(0, QSize(18, 18));
// item->setForeground(0, QBrush(QColor(79, 79, 79)));
// addTopLevelItem(item);
// item->setExpanded(true);
return;
//header()->setSectionResizeMode(PCITEM_COLUMN_COMMENT,QHeaderView::ResizeToContents);
return;
}
void GxsCommentTreeWidget::updateContent()
{
model()->dataChanged(QModelIndex(),QModelIndex());
std::cerr << "Updating content" << std::endl;
}
GxsCommentTreeWidget::~GxsCommentTreeWidget()
{
if (mTokenQueue) {
@ -193,15 +317,18 @@ void GxsCommentTreeWidget::setCurrentCommentMsgId(QTreeWidgetItem *current, QTre
}
}
void GxsCommentTreeWidget::customPopUpMenu(const QPoint& /*point*/)
void GxsCommentTreeWidget::customPopUpMenu(const QPoint& point)
{
QTreeWidgetItem *item = itemAt(point);
QMenu contextMnu( this );
QAction* action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_REPLY), tr("Reply to Comment"), this, SLOT(replyToComment()));
action->setDisabled(mCurrentCommentMsgId.isNull());
action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MESSAGE), tr("Submit Comment"), this, SLOT(makeComment()));
action->setDisabled(mMsgVersions.empty());
action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_COPY), tr("Copy Comment"), this, SLOT(copyComment()));
action->setDisabled(mCurrentCommentMsgId.isNull());
action->setData( item->data(PCITEM_COLUMN_COMMENT,Qt::DisplayRole) );
action->setDisabled(mCurrentCommentMsgId.isNull());
contextMnu.addSeparator();
@ -342,8 +469,10 @@ void GxsCommentTreeWidget::replyToComment()
void GxsCommentTreeWidget::copyComment()
{
QString txt = dynamic_cast<QAction*>(sender())->data().toString();
QMimeData *mimeData = new QMimeData();
mimeData->setHtml("<html>"+mCurrentCommentText+"</html>");
mimeData->setHtml("<html>"+txt+"</html>");
QClipboard *clipboard = QApplication::clipboard();
clipboard->setMimeData(mimeData, QClipboard::Clipboard);
}
@ -540,23 +669,20 @@ void GxsCommentTreeWidget::addItem(RsGxsMessageId itemId, RsGxsMessageId parentI
int treeCount(QTreeWidget *tree, QTreeWidgetItem *parent = 0)
{
int count = 0;
if (parent == 0) {
if (parent == 0)
{
int topCount = tree->topLevelItemCount();
for (int i = 0; i < topCount; i++) {
QTreeWidgetItem *item = tree->topLevelItem(i);
if (item->isExpanded()) {
count += treeCount(tree, item);
}
}
for (int i = 0; i < topCount; i++)
count += treeCount(tree, tree->topLevelItem(i));
count += topCount;
} else {
}
else
{
int childCount = parent->childCount();
for (int i = 0; i < childCount; i++) {
QTreeWidgetItem *item = parent->child(i);
if (item->isExpanded()) {
count += treeCount(tree, item);
}
}
for (int i = 0; i < childCount; i++)
count += treeCount(tree, parent->child(i));
count += childCount;
}
return count;
@ -642,14 +768,14 @@ void GxsCommentTreeWidget::insertComments(const std::vector<RsGxsComment>& comme
}
text = QString::fromUtf8(comment.mComment.c_str());
item->setText(PCITEM_COLUMN_COMMENT, text);
item->setToolTip(PCITEM_COLUMN_COMMENT, text);
item->setText(PCITEM_COLUMN_COMMENT, text);
item->setToolTip(PCITEM_COLUMN_COMMENT, text);
RsGxsId authorId = comment.mMeta.mAuthorId;
RsGxsId authorId = comment.mMeta.mAuthorId;
item->setId(authorId, PCITEM_COLUMN_AUTHOR, false);
item->setData(PCITEM_COLUMN_COMMENT,POST_COLOR_ROLE,QVariant(authorId.toByteArray()[1]));
text = QString::number(comment.mScore);
text = QString::number(comment.mScore);
item->setText(PCITEM_COLUMN_SCORE, text);
text = QString::number(comment.mUpVotes);
@ -670,6 +796,11 @@ void GxsCommentTreeWidget::insertComments(const std::vector<RsGxsComment>& comme
text = QString::fromUtf8(comment.mMeta.mAuthorId.toStdString().c_str());
item->setText(PCITEM_COLUMN_AUTHORID, text);
#ifdef USE_NEW_DELEGATE
// Allows to call createEditor() in the delegate. Without this, createEditor() is never called in
// the styled item delegate.
item->setFlags(Qt::ItemIsEditable | item->flags());
#endif
addItem(comment.mMeta.mMsgId, comment.mMeta.mParentId, item);
}

View File

@ -47,7 +47,12 @@ public:
void setVoteId(const RsGxsId &voterId);
void setUseCache(bool b) { mUseCache = b ;}
protected slots:
void updateContent();
protected:
void mouseMoveEvent(QMouseEvent *e) override;
/* to be overloaded */
virtual void service_requestComments(const RsGxsGroupId &group_id, const std::set<RsGxsMessageId> &msgIds);

View File

@ -63,8 +63,8 @@ void GxsCreateCommentDialog::createComment()
{
RsGxsComment comment;
QString text = ui->commentTextEdit->toHtml();
RsHtml::optimizeHtml(text);
QString text = ui->commentTextEdit->toPlainText();
// RsHtml::optimizeHtml(text);
std::string msg = text.toUtf8().constData();
comment.mComment = msg;

View File

@ -160,8 +160,11 @@ p, li { white-space: pre-wrap; }
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="acceptRichText">
<bool>false</bool>
</property>
<property name="placeholderText">
<string>Type your comment</string>
<string>Type in your comment</string>
</property>
</widget>
</item>
@ -254,9 +257,6 @@ p, li { white-space: pre-wrap; }
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="../icons.qrc"/>
<include location="../images.qrc"/>
</resources>
<resources/>
<connections/>
</ui>

View File

@ -37,7 +37,7 @@ const uint32_t ChannelCreateEnabledFlags = (
// GXS_GROUP_FLAGS_PUBLISHSIGN |
// GXS_GROUP_FLAGS_SHAREKEYS | // disabled because the UI doesn't handle it, so no need to show the disabled button. The user can do it in a second step from the channel menu.
// GXS_GROUP_FLAGS_PERSONALSIGN |
GXS_GROUP_FLAGS_COMMENTS |
// GXS_GROUP_FLAGS_COMMENTS | // disabled because the UI doesn't handle it yet. Better to hide it then.
0);
const uint32_t ChannelCreateDefaultsFlags = ( GXS_GROUP_DEFAULTS_DISTRIB_PUBLIC |

View File

@ -72,6 +72,25 @@ void RsGxsChannelPostsModel::setMode(TreeMode mode)
triggerViewUpdate();
}
void updateCommentCounts( std::vector<RsGxsChannelPost>& posts, std::vector<RsGxsComment>& comments)
{
// Store posts IDs in a std::map to avoid a quadratic cost
std::map<RsGxsMessageId,uint32_t> post_indices;
for(uint32_t i=0;i<posts.size();++i)
{
post_indices[posts[i].mMeta.mMsgId] = i;
posts[i].mCommentCount = 0; // should be 0 already, but we secure that value.
}
// now look into comments and increase the count
for(uint32_t i=0;i<comments.size();++i)
++posts[post_indices[comments[i].mMeta.mThreadId]].mCommentCount;
}
void RsGxsChannelPostsModel::handleEvent_main_thread(std::shared_ptr<const RsEvent> event)
{
const RsGxsChannelEvent *e = dynamic_cast<const RsGxsChannelEvent*>(event.get());
@ -88,24 +107,44 @@ void RsGxsChannelPostsModel::handleEvent_main_thread(std::shared_ptr<const RsEve
//
// We need to update the data!
if(e->mChannelGroupId == mChannelGroup.mMeta.mGroupId)
RsThread::async([this, e]()
// make a copy of e, so as to avoid destruction of the shared pointer during async thread execution, since [e] doesn't actually tell
// the shared_ptr that it is copied! So no counter is updated.
RsGxsChannelEvent E(*e);
if(E.mChannelGroupId == mChannelGroup.mMeta.mGroupId)
RsThread::async([this, E]()
{
// 1 - get message data from p3GxsChannels
// 1 - get message data from p3GxsChannels. No need for pointers here, because we send only a single post to postToObject()
std::vector<RsGxsChannelPost> posts;
std::vector<RsGxsComment> comments;
std::vector<RsGxsVote> votes;
if(!rsGxsChannels->getChannelContent(mChannelGroup.mMeta.mGroupId,std::set<RsGxsMessageId>{ e->mChannelMsgId }, posts,comments,votes))
std::cerr << "display of e 1: " << E << std::endl;
if(!rsGxsChannels->getChannelContent(E.mChannelGroupId,std::set<RsGxsMessageId>{ E.mChannelMsgId }, posts,comments,votes))
{
std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel message data for channel/msg " << e->mChannelGroupId << "/" << e->mChannelMsgId << std::endl;
std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel message data for channel/msg " << E.mChannelGroupId << "/" << E.mChannelMsgId << std::endl;
return;
}
// 2 - update the model in the UI thread.
// Need to call this in order to get the actuall comment count. The previous call only retrieves the message, since we supplied the message ID.
// another way to go would be to save the comment ids of the existing message and re-insert them before calling getChannelContent.
RsQThreadUtils::postToObject( [posts,comments,votes,this]()
std::cerr << "display of e 2: " << E << std::endl;
std::cerr << "Before call : IS_MSG_READ=" << IS_MSG_NEW(posts[0].mMeta.mMsgFlags) << " for message id " << E.mChannelMsgId << std::endl;
if(!rsGxsChannels->getChannelComments(E.mChannelGroupId,std::set<RsGxsMessageId>{ E.mChannelMsgId },comments))
{
std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve message comment data for channel/msg " << E.mChannelGroupId << "/" << E.mChannelMsgId << std::endl;
return;
}
updateCommentCounts(posts,comments);
std::cerr << "After call : IS_MSG_READ=" << IS_MSG_NEW(posts[0].mMeta.mMsgFlags) << std::endl;
// 2 - update the model in the UI thread.
RsQThreadUtils::postToObject( [posts,this]()
{
for(uint32_t i=0;i<posts.size();++i)
{
@ -113,7 +152,7 @@ void RsGxsChannelPostsModel::handleEvent_main_thread(std::shared_ptr<const RsEve
for(uint32_t j=0;j<mPosts.size();++j)
if(mPosts[j].mMeta.mMsgId == posts[i].mMeta.mMsgId)
{
{
mPosts[j] = posts[i];
triggerViewUpdate();
@ -121,10 +160,10 @@ void RsGxsChannelPostsModel::handleEvent_main_thread(std::shared_ptr<const RsEve
}
},this);
});
}
default:
break;
}
}
}
@ -195,9 +234,7 @@ void RsGxsChannelPostsModel::setFilter(const QStringList& strings,bool only_unre
count = mFilteredPosts.size();
std::cerr << "After filtering: " << count << " posts remain." << std::endl;
beginInsertRows(QModelIndex(),0,rowCount()-1);
beginInsertRows(QModelIndex(),0,rowCount()-1);
endInsertRows();
postMods();
@ -326,7 +363,7 @@ bool RsGxsChannelPostsModel::setNumColumns(int n)
RsErr() << __PRETTY_FUNCTION__ << " Attempt to set a number of column of 0. This is wrong." << std::endl;
return false;
}
if(mColumns == n)
if((int)mColumns == n)
return false;
preMods();
@ -556,6 +593,15 @@ void RsGxsChannelPostsModel::update_posts(const RsGxsGroupId& group_id)
std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel messages for channel " << group_id << std::endl;
return;
}
std::cerr << "Got channel all content for channel " << group_id << std::endl;
std::cerr << " posts : " << posts->size() << std::endl;
std::cerr << " comments: " << comments->size() << std::endl;
std::cerr << " votes : " << votes->size() << std::endl;
// This shouldn't be needed normally. We need it until a background process computes the number of comments per
// post and stores it in the service string. Since we request all data, this process isn't costing much anyway.
updateCommentCounts(*posts,*comments);
// 2 - update the model in the UI thread.

View File

@ -76,6 +76,7 @@ QColor SelectedColor = QRgb(0xff308dc7);
#define COLUMN_SIZE_FONT_FACTOR_H 10
#define STAR_OVERLAY_IMAGE ":icons/star_overlay_128.png"
#define COMMENT_OVERLAY_IMAGE ":images/white-bubble-64.png"
#define IMAGE_COPYLINK ":icons/png/copy.png"
#define IMAGE_GRID_VIEW ":icons/png/menu.png"
#define IMAGE_DOWNLOAD ":icons/png/download.png"
@ -179,8 +180,19 @@ void ChannelPostDelegate::paint(QPainter * painter, const QStyleOptionViewItem &
QPainter p(&pixmap);
QFontMetricsF fm(option.font);
p.drawPixmap(mZoom*QPoint(0.1*fm.height(),-3.6*fm.height()),FilesDefs::getPixmapFromQtResourcePath(STAR_OVERLAY_IMAGE).scaled(mZoom*7*fm.height(),mZoom*7*fm.height(),Qt::KeepAspectRatio,Qt::SmoothTransformation));
p.drawPixmap(mZoom*QPoint(0.1*fm.height(),-3.4*fm.height()),FilesDefs::getPixmapFromQtResourcePath(STAR_OVERLAY_IMAGE).scaled(mZoom*6*fm.height(),mZoom*6*fm.height(),Qt::KeepAspectRatio,Qt::SmoothTransformation));
}
if(post.mCommentCount)
{
QPainter p(&pixmap);
QFontMetricsF fm(option.font);
p.drawPixmap(QPoint(pixmap.width(),0.0)+mZoom*QPoint(-2.9*fm.height(),0.4*fm.height()),
FilesDefs::getPixmapFromQtResourcePath(COMMENT_OVERLAY_IMAGE).scaled(mZoom*3*fm.height(),mZoom*3*fm.height(),
Qt::KeepAspectRatio,Qt::SmoothTransformation));
}
}
painter->drawPixmap(option.rect.topLeft(),
@ -222,8 +234,8 @@ void ChannelPostDelegate::paint(QPainter * painter, const QStyleOptionViewItem &
QString info_text = QDateTime::fromMSecsSinceEpoch(qint64(1000)*post.mMeta.mPublishTs).toString(Qt::DefaultLocaleShortDate);
if(post.mCount > 0)
info_text += ", " + QString::number(post.mCount)+ " " +((post.mCount>1)?tr("files"):tr("file")) + " (" + misc::friendlyUnit(qulonglong(post.mSize)) + ")" ;
if(post.mAttachmentCount > 0)
info_text += ", " + QString::number(post.mAttachmentCount)+ " " +((post.mAttachmentCount>1)?tr("files"):tr("file")) + " (" + misc::friendlyUnit(qulonglong(post.mSize)) + ")" ;
painter->drawText(QPoint(p.x()+0.5*font_height,y),info_text);
y += font_height;
@ -738,7 +750,9 @@ void GxsChannelPostsWidgetWithModel::handleEvent_main_thread(std::shared_ptr<con
switch(e->mChannelEventCode)
{
case RsChannelEventCode::NEW_CHANNEL: // [[fallthrough]];
case RsChannelEventCode::UPDATED_CHANNEL: // [[fallthrough]];
case RsChannelEventCode::NEW_COMMENT: // [[fallthrough]];
case RsChannelEventCode::NEW_VOTE: // [[fallthrough]];
case RsChannelEventCode::UPDATED_CHANNEL: // [[fallthrough]];
case RsChannelEventCode::NEW_MESSAGE: // [[fallthrough]];
case RsChannelEventCode::UPDATED_MESSAGE:
case RsChannelEventCode::SYNC_PARAMETERS_UPDATED:
@ -756,7 +770,10 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
{
QModelIndex index = ui->postsTree->selectionModel()->currentIndex();
RsGxsChannelPost post = index.data(Qt::UserRole).value<RsGxsChannelPost>() ;
#ifdef DEBUG_CHANNEL_POSTS_WIDGET
std::cerr << "showPostDetails: current index is " << index.row() << "," << index.column() << std::endl;
#endif
QTextDocument doc;
doc.setHtml(post.mMsg.c_str());
@ -768,6 +785,7 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
ui->postTime_LB->hide();
mChannelPostFilesModel->clear();
ui->details_TW->setEnabled(false);
mSelectedPost.clear();
return;
}
@ -777,7 +795,9 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
ui->postName_LB->show();
ui->postTime_LB->show();
std::cerr << "showPostDetails: setting mSelectedPost to current post Id " << post.mMeta.mMsgId << ". Previous value: " << mSelectedPost << std::endl;
#ifdef DEBUG_CHANNEL_POSTS_WIDGET
std::cerr << "showPostDetails: setting mSelectedPost to current post Id " << post.mMeta.mMsgId << ". Previous value: " << mSelectedPost << std::endl;
#endif
mSelectedPost = post.mMeta.mMsgId;
std::list<ChannelPostFileInfo> files;
@ -791,7 +811,9 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
ui->commentsDialog->commentLoad(post.mMeta.mGroupId, all_msgs_versions, post.mMeta.mMsgId,true);
#ifdef DEBUG_CHANNEL_POSTS_WIDGET
std::cerr << "Showing details about selected index : "<< index.row() << "," << index.column() << std::endl;
#endif
ui->postDetails_TE->setText(RsHtml().formatText(NULL, QString::fromUtf8(post.mMsg.c_str()), RSHTML_FORMATTEXT_EMBED_SMILEYS | RSHTML_FORMATTEXT_EMBED_LINKS));
@ -864,6 +886,16 @@ void GxsChannelPostsWidgetWithModel::updateGroupData()
RsQThreadUtils::postToObject( [this,group]()
{
if(mGroup.mMeta.mGroupId != group.mMeta.mGroupId) // this prevents any attempt to display the wrong index. Navigate() if needed will use mSelectedPost
{
#ifdef DEBUG_CHANNEL_POSTS_WIDGET
std::cerr << "Old group: " << mGroup.mMeta.mGroupId << ", new group: " << group.mMeta.mGroupId << ". Celaring selection" << std::endl;
#endif
whileBlocking(ui->postsTree->selectionModel())->clear();
whileBlocking(ui->commentsDialog)->commentClear();
updateCommentsCount(0);
}
mGroup = group;
mChannelPostsModel->updateChannel(groupId());
whileBlocking(ui->filterLineEdit)->clear();
@ -1132,8 +1164,10 @@ void GxsChannelPostsWidgetWithModel::insertChannelDetails(const RsGxsChannelGrou
sync_string = tr("Unknown");
}
if(group.mMeta.mLastPost > 0 && group.mMeta.mLastPost + rsGxsChannels->getSyncPeriod(group.mMeta.mGroupId) < time(NULL) && IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags))
sync_string += " (Warning: will not allow latest posts to sync)";
auto sync_period = rsGxsChannels->getSyncPeriod(group.mMeta.mGroupId) ;
if(sync_period > 0 && group.mMeta.mLastPost > 0 && group.mMeta.mLastPost + rsGxsChannels->getSyncPeriod(group.mMeta.mGroupId) < time(NULL) && IS_GROUP_SUBSCRIBED(group.mMeta.mSubscribeFlags))
sync_string += " (Warning: will not allow posts to sync)";
ui->infoSyncTimeLabel->setText(sync_string);

View File

@ -410,7 +410,7 @@ QVariant RsGxsForumModel::data(const QModelIndex &index, int role) const
if(role == Qt::FontRole)
{
QFont font ;
font.setBold( (fmpe.mPostFlags & (ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN | ForumModelPostEntry::FLAG_POST_IS_PINNED)) || IS_MSG_UNREAD(fmpe.mMsgStatus));
font.setBold( (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_HAS_UNREAD_CHILDREN) || IS_MSG_UNREAD(fmpe.mMsgStatus));
return QVariant(font);
}
@ -445,15 +445,15 @@ QVariant RsGxsForumModel::textColorRole(const ForumModelPostEntry& fmpe,int /*co
if( (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING))
return QVariant(mTextColorMissing);
if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)
return QVariant(mTextColorPinned);
if(IS_MSG_UNREAD(fmpe.mMsgStatus))
return QVariant(mTextColorUnread);
else
if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)
return QVariant(mTextColorPinned);
else
return QVariant(mTextColorRead);
return QVariant(mTextColorRead);
return QVariant();
return QVariant();
}
QVariant RsGxsForumModel::statusRole(const ForumModelPostEntry& fmpe,int column) const
@ -595,8 +595,8 @@ QVariant RsGxsForumModel::pinnedRole(const ForumModelPostEntry& fmpe,int /*colum
QVariant RsGxsForumModel::backgroundRole(const ForumModelPostEntry& fmpe,int /*column*/) const
{
if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)
return QVariant(QBrush(mBackgroundColorPinned));
// if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)
// return QVariant(QBrush(mBackgroundColorPinned));
if(mFilteringEnabled && (fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_PASSES_FILTER))
return QVariant(QBrush(mBackgroundColorFiltered));
@ -656,12 +656,13 @@ QVariant RsGxsForumModel::displayRole(const ForumModelPostEntry& fmpe,int col) c
{
case COLUMN_THREAD_TITLE: if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_REDACTED)
return QVariant(tr("[ ... Redacted message ... ]"));
else if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)
return QVariant(tr("[PINNED] ") + QString::fromUtf8(fmpe.mTitle.c_str()));
else
// else if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_PINNED)
// return QVariant( QString("<img src=\":/icons/pinned_64.png\" height=%1/>").arg(QFontMetricsF(QFont()).height())
// + QString::fromUtf8(fmpe.mTitle.c_str()));
else
return QVariant(QString::fromUtf8(fmpe.mTitle.c_str()));
case COLUMN_THREAD_READ:return QVariant();
case COLUMN_THREAD_READ:return QVariant();
case COLUMN_THREAD_DATE:{
if(fmpe.mPostFlags & ForumModelPostEntry::FLAG_POST_IS_MISSING)
return QVariant(QString());

File diff suppressed because it is too large Load Diff