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 #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 MSG_CLEANUP_PERIOD = 60*59; // 59 minutes
static const uint32_t INTEGRITY_CHECK_PERIOD = 60*31; // 31 minutes static const uint32_t INTEGRITY_CHECK_PERIOD = 60*31; // 31 minutes
@ -2596,8 +2662,17 @@ void RsGenExchange::publishGrps()
RS_STACK_MUTEX(mGenMtx) ; RS_STACK_MUTEX(mGenMtx) ;
NxsGrpSignPendVect::iterator vit = mGrpsToPublish.begin(); NxsGrpSignPendVect::iterator vit = mGrpsToPublish.begin();
typedef std::pair<bool, RsGxsGroupId> GrpNote; typedef struct _GrpNote {
std::map<uint32_t, GrpNote> toNotify;
_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() ) while( vit != mGrpsToPublish.end() )
{ {
@ -2611,8 +2686,7 @@ void RsGenExchange::publishGrps()
if(now > (ggps.mStartTS + PENDING_SIGN_TIMEOUT) ) if(now > (ggps.mStartTS + PENDING_SIGN_TIMEOUT) )
{ {
// timed out // timed out
toNotify.insert(std::make_pair( toNotify.insert(std::make_pair( token, GrpNote(false,ggps.mIsUpdate, RsGxsGroupId())));
token, GrpNote(false, RsGxsGroupId())));
delete ggps.mItem; delete ggps.mItem;
vit = mGrpsToPublish.erase(vit); vit = mGrpsToPublish.erase(vit);
@ -2723,6 +2797,30 @@ void RsGenExchange::publishGrps()
computeHash(grp->grp, grp->metaData->mHash); computeHash(grp->grp, grp->metaData->mHash);
grp->metaData->mRecvTS = time(NULL); grp->metaData->mRecvTS = time(NULL);
// Also push in notifications. We do it here when we still have pointers on the old and new groups
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) if(ggps.mIsUpdate)
mDataAccess->updateGroupData(grp); mDataAccess->updateGroupData(grp);
else else
@ -2766,8 +2864,7 @@ void RsGenExchange::publishGrps()
delete grp; delete grp;
delete grpItem; delete grpItem;
vit = mGrpsToPublish.erase(vit); vit = mGrpsToPublish.erase(vit);
toNotify.insert(std::make_pair( toNotify.insert(std::make_pair(token, GrpNote(false, ggps.mIsUpdate,grpId)));
token, GrpNote(false, grpId)));
} }
else if(create == CREATE_FAIL_TRY_LATER) else if(create == CREATE_FAIL_TRY_LATER)
@ -2790,30 +2887,21 @@ void RsGenExchange::publishGrps()
#endif #endif
// add to published to allow acknowledgement // add to published to allow acknowledgement
toNotify.insert(std::make_pair(token, toNotify.insert(std::make_pair(token, GrpNote(true,ggps.mIsUpdate,grpId)));
GrpNote(true,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(auto mit=toNotify.begin(); mit != toNotify.end(); ++mit)
for(; mit != toNotify.end(); ++mit)
{ {
GrpNote& note = mit->second; GrpNote& note = mit->second;
RsTokenService::GxsRequestStatus status = RsTokenService::GxsRequestStatus status = note.mSuccess ? RsTokenService::COMPLETE : RsTokenService::FAILED;
note.first ? RsTokenService::COMPLETE
: RsTokenService::FAILED;
mGrpNotify.insert(std::make_pair(mit->first, note.second)); mGrpNotify.insert(std::make_pair(mit->first, note.mGroupId)); // always notify
mDataAccess->updatePublicRequestStatus(mit->first, status); mDataAccess->updatePublicRequestStatus(mit->first, status); // update the token request with the given status
if(note.first)
grpChanged.push_back(note.second);
} }
for(auto& groupId:grpChanged)
mNotifications.push_back(new RsGxsGroupChange(RsGxsNotify::TYPE_RECEIVED_NEW,groupId, true));
} }
// This is done off-mutex to avoid possible cross deadlocks with the net service. // 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) ; mNetService->subscribeStatusChanged((*it),true) ;
} }
uint32_t RsGenExchange::generatePublicToken() uint32_t RsGenExchange::generatePublicToken()
{ {
return mDataAccess->generatePublicToken(); return mDataAccess->generatePublicToken();
@ -3082,7 +3168,7 @@ void RsGenExchange::processRecvdMessages()
mNotifications.push_back(c); 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
} }
} }
@ -3317,7 +3403,6 @@ 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 #ifdef GEN_EXCH_DEBUG

View file

@ -71,13 +71,14 @@ struct RsGxsChannelGroup : RsSerializable, RsGxsGenericGroupData
struct RsGxsChannelPost : RsSerializable, RsGxsGenericMsgData struct RsGxsChannelPost : RsSerializable, RsGxsGenericMsgData
{ {
RsGxsChannelPost() : mCount(0), mSize(0) {} RsGxsChannelPost() : mAttachmentCount(0), mCommentCount(0), mSize(0) {}
std::set<RsGxsMessageId> mOlderVersions; std::set<RsGxsMessageId> mOlderVersions;
std::string mMsg; // UTF8 encoded. std::string mMsg; // UTF8 encoded.
std::list<RsGxsFile> mFiles; std::list<RsGxsFile> mFiles;
uint32_t mCount; // 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. uint64_t mSize; // auto calced.
RsGxsImage mThumbnail; RsGxsImage mThumbnail;
@ -92,7 +93,8 @@ struct RsGxsChannelPost : RsSerializable, RsGxsGenericMsgData
RS_SERIAL_PROCESS(mMsg); RS_SERIAL_PROCESS(mMsg);
RS_SERIAL_PROCESS(mFiles); RS_SERIAL_PROCESS(mFiles);
RS_SERIAL_PROCESS(mCount); RS_SERIAL_PROCESS(mAttachmentCount);
RS_SERIAL_PROCESS(mCommentCount);
RS_SERIAL_PROCESS(mSize); RS_SERIAL_PROCESS(mSize);
RS_SERIAL_PROCESS(mThumbnail); 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 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 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 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 struct RsGxsChannelEvent: RsEvent
@ -123,6 +127,7 @@ struct RsGxsChannelEvent: RsEvent
RsChannelEventCode mChannelEventCode; RsChannelEventCode mChannelEventCode;
RsGxsGroupId mChannelGroupId; RsGxsGroupId mChannelGroupId;
RsGxsMessageId mChannelMsgId; RsGxsMessageId mChannelMsgId;
RsGxsMessageId mChannelThreadId;
///* @see RsEvent @see RsSerializable ///* @see RsEvent @see RsSerializable
void serial_process( RsGenericSerializer::SerializeJob j,RsGenericSerializer::SerializeContext& ctx) override void serial_process( RsGenericSerializer::SerializeJob j,RsGenericSerializer::SerializeContext& ctx) override
@ -323,7 +328,10 @@ public:
std::vector<RsGxsChannelGroup>& channelsInfo ) = 0; std::vector<RsGxsChannelGroup>& channelsInfo ) = 0;
/** /**
* @brief Get all channel messages and comments in a given channel * @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} * @jsonapi{development}
* @param[in] channelId id of the channel of which the content is requested * @param[in] channelId id of the channel of which the content is requested
* @param[out] posts storage for posts * @param[out] posts storage for posts
@ -337,10 +345,12 @@ public:
std::vector<RsGxsVote>& votes ) = 0; std::vector<RsGxsVote>& votes ) = 0;
/** /**
* @brief Get channel messages and comments corresponding to the given IDs. * @brief Get channel messages, comments and votes corresponding to the given IDs.
* If the set is empty, nothing is returned. * @note Since comments are internally themselves messages, this function actually
* @note Since comments are internally themselves messages, it is possible * returns the data for messages, comments or votes that have the given ID.
* to get comments only by supplying their IDs. * It *does not* automatically retrieve the comments or votes for a given message
* which Id you supplied.
*
* @jsonapi{development} * @jsonapi{development}
* @param[in] channelId id of the channel of which the content is requested * @param[in] channelId id of the channel of which the content is requested
* @param[in] contentsIds ids of requested contents * @param[in] contentsIds ids of requested contents
@ -356,8 +366,9 @@ public:
std::vector<RsGxsVote>& votes ) = 0; 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. * If the set is empty, nothing is returned.
*
* @jsonapi{development} * @jsonapi{development}
* @param[in] channelId id of the channel of which the content is requested * @param[in] channelId id of the channel of which the content is requested
* @param[in] contentIds ids of requested contents * @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 STATISTICS_CHANGED = 0x07, /// suppliers and how many messages they have changed
MODERATOR_LIST_CHANGED = 0x08, /// forum moderation list has changed. MODERATOR_LIST_CHANGED = 0x08, /// forum moderation list has changed.
SYNC_PARAMETERS_UPDATED = 0x0a, /// sync and storage times have 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 struct RsGxsForumEvent: RsEvent

View file

@ -117,6 +117,8 @@ enum class RsPostedEventCode: uint8_t
STATISTICS_CHANGED = 0x07, STATISTICS_CHANGED = 0x07,
MESSAGE_VOTES_UPDATED = 0x08, MESSAGE_VOTES_UPDATED = 0x08,
SYNC_PARAMETERS_UPDATED = 0x09, 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.mThumbnail.copy((uint8_t *) mThumbnail.binData.bin_data, mThumbnail.binData.bin_len);
} }
post.mCount = 0; post.mAttachmentCount = 0;
post.mSize = 0; post.mSize = 0;
std::list<RsTlvFileItem>::iterator fit; std::list<RsTlvFileItem>::iterator fit;
for(fit = mAttachment.items.begin(); fit != mAttachment.items.end(); ++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; fi.mHash = fit->hash;
post.mFiles.push_back(fi); post.mFiles.push_back(fi);
post.mCount++; post.mAttachmentCount++;
post.mSize += fi.mSize; post.mSize += fi.mSize;
} }
return true; return true;

View file

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

View file

@ -35,11 +35,12 @@
#include <string> #include <string>
// This class is only a helper to parse the channel group service string.
class SSGxsChannelGroup class GxsChannelGroupInfo
{ {
public: public:
SSGxsChannelGroup(): mAutoDownload(false), mDownloadDirectory("") {} GxsChannelGroupInfo(): mAutoDownload(false), mDownloadDirectory("") {}
bool load(const std::string &input); bool load(const std::string &input);
std::string save() const; std::string save() const;
@ -55,31 +56,31 @@ class p3GxsChannels: public RsGenExchange, public RsGxsChannels,
public: public:
p3GxsChannels( RsGeneralDataService* gds, RsNetworkExchangeService* nes, p3GxsChannels( RsGeneralDataService* gds, RsNetworkExchangeService* nes,
RsGixs* gixs ); RsGixs* gixs );
virtual RsServiceInfo getServiceInfo(); virtual RsServiceInfo getServiceInfo() override;
virtual void service_tick(); virtual void service_tick() override;
protected: protected:
virtual RsSerialiser* setupSerialiser(); // @see p3Config::setupSerialiser() virtual RsSerialiser* setupSerialiser() override; // @see p3Config::setupSerialiser()
virtual bool saveList(bool &cleanup, std::list<RsItem *>&saveList); // @see p3Config::saveList(bool &cleanup, std::list<RsItem *>&) 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); // @see p3Config::loadList(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 turtleGroupRequest(const RsGxsGroupId& group_id) override;
virtual TurtleRequestId turtleSearchRequest(const std::string& match_string); virtual TurtleRequestId turtleSearchRequest(const std::string& match_string) override;
virtual bool retrieveDistantSearchResults(TurtleRequestId req, std::map<RsGxsGroupId, RsGxsGroupSearchResults> &results) ; virtual bool retrieveDistantSearchResults(TurtleRequestId req, std::map<RsGxsGroupId, RsGxsGroupSearchResults> &results) override;
virtual bool clearDistantSearchResults(TurtleRequestId req); virtual bool clearDistantSearchResults(TurtleRequestId req) override;
virtual bool getDistantSearchResultGroupData(const RsGxsGroupId& group_id,RsGxsChannelGroup& distant_group); virtual bool getDistantSearchResultGroupData(const RsGxsGroupId& group_id,RsGxsChannelGroup& distant_group) override;
virtual DistantSearchGroupStatus getDistantSearchStatus(const RsGxsGroupId& group_id) ; virtual DistantSearchGroupStatus getDistantSearchStatus(const RsGxsGroupId& group_id) override;
// Overloaded to cache new groups. // 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. // 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: 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 groupRestoreKeys(const std::string &groupId);
virtual bool groupShareKeys( 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 createGroup(uint32_t &token, RsGxsChannelGroup &group) override;
virtual bool createPost(uint32_t &token, RsGxsChannelPost &post); 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. // no tokens... should be cached.
virtual bool setChannelAutoDownload(const RsGxsGroupId &groupId, bool enabled); virtual bool setChannelAutoDownload(const RsGxsGroupId &groupId, bool enabled) override;
virtual bool getChannelAutoDownload(const RsGxsGroupId &groupid, bool& enabled); virtual bool getChannelAutoDownload(const RsGxsGroupId &groupid, bool& enabled) override;
virtual bool setChannelDownloadDirectory(const RsGxsGroupId &groupId, const std::string& directory); virtual bool setChannelDownloadDirectory(const RsGxsGroupId &groupId, const std::string& directory) override;
virtual bool getChannelDownloadDirectory(const RsGxsGroupId &groupId, std::string& directory); virtual bool getChannelDownloadDirectory(const RsGxsGroupId &groupId, std::string& directory) override;
#ifdef TO_REMOVE #ifdef TO_REMOVE
/// @see RsGxsChannels::turtleSearchRequest /// @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 *old_circle_grp_item = dynamic_cast<RsGxsCircleGroupItem*>(groupChange->mOldGroupItem);
RsGxsCircleGroupItem *new_circle_grp_item = dynamic_cast<RsGxsCircleGroupItem*>(groupChange->mNewGroupItem); 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) 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; RsErr() << __PRETTY_FUNCTION__ << " received GxsGroupUpdate item with mOldGroup and mNewGroup not of type RsGxsCircleGroupItem. This is inconsistent!" << std::endl;
@ -692,6 +690,8 @@ void p3GxsCircles::notifyChanges(std::vector<RsGxsNotify *> &changes)
continue; continue;
} }
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 // 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) for(auto& gxs_id: new_circle_grp_item->gxsIdSet.ids)

View file

@ -307,7 +307,7 @@ void p3GxsForums::notifyChanges(std::vector<RsGxsNotify *> &changes)
if(old_forum_grp_item == nullptr || new_forum_grp_item == nullptr) 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; delete grpChange;
continue; continue;
} }
@ -335,6 +335,29 @@ void p3GxsForums::notifyChanges(std::vector<RsGxsNotify *> &changes)
rsEvents->postEvent(ev); 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; break;

View file

@ -110,7 +110,15 @@ void p3PostBase::notifyChanges(std::vector<RsGxsNotify *> &changes)
ev->mPostedMsgId = msgChange->mMsgId; ev->mPostedMsgId = msgChange->mMsgId;
ev->mPostedThreadId = msgChange->mNewMsgItem->meta.mThreadId; ev->mPostedThreadId = msgChange->mNewMsgItem->meta.mThreadId;
ev->mPostedGroupId = msgChange->mGroupId; 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; ev->mPostedEventCode = RsPostedEventCode::NEW_MESSAGE;
rsEvents->postEvent(ev); rsEvents->postEvent(ev);
#ifdef POSTBASE_DEBUG #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; 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(); 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 // 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) 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 // 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(); t->refresh();
} }
} }
case RsPostedEventCode::NEW_MESSAGE: // [[fallthrough]];
case RsPostedEventCode::NEW_POSTED_GROUP: // [[fallthrough]]; case RsPostedEventCode::NEW_POSTED_GROUP: // [[fallthrough]];
case RsPostedEventCode::UPDATED_POSTED_GROUP: // [[fallthrough]]; case RsPostedEventCode::UPDATED_POSTED_GROUP: // [[fallthrough]];
case RsPostedEventCode::UPDATED_MESSAGE: case RsPostedEventCode::UPDATED_MESSAGE:
@ -877,8 +879,10 @@ void PostedListWidgetWithModel::insertBoardDetails(const RsPostedGroup& group)
sync_string = tr("Unknown"); 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)) auto sync_period = rsPosted->getSyncPeriod(group.mMeta.mGroupId) ;
sync_string += " (Warning: will not allow latest posts to sync)";
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); ui->syncPeriodLabel->setText(sync_string);

View file

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

View file

@ -578,10 +578,7 @@ void NewFriendList::peerTreeWidgetCustomPopupMenu()
mModel->getGroupData(index,group_info); mModel->getGroupData(index,group_info);
bool standard = group_info.flag & RS_GROUP_FLAG_STANDARD; 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())); 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())); 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)); 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->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 { } else {
ui->filelabel->setVisible(false); ui->filelabel->setVisible(false);
} }

View file

@ -86,6 +86,13 @@ GxsCommentDialog::~GxsCommentDialog()
delete(ui); 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) 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 << ")"; 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 setTokenService(RsTokenService *token_service, RsGxsCommentService *comment_service);
void setCommentHeader(QWidget *header); 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 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; } RsGxsGroupId groupId() { return mGrpId; }
RsGxsMessageId messageId() { return mMostRecentMsgId; } RsGxsMessageId messageId() { return mMostRecentMsgId; }

View file

@ -28,6 +28,8 @@
#include <QAbstractTextDocumentLayout> #include <QAbstractTextDocumentLayout>
#include <QApplication> #include <QApplication>
#include <QTextEdit>
#include <QHeaderView>
#include <QClipboard> #include <QClipboard>
#include <QDateTime> #include <QDateTime>
#include <QMenu> #include <QMenu>
@ -68,6 +70,8 @@
std::map<RsGxsMessageId, std::vector<RsGxsComment> > GxsCommentTreeWidget::mCommentsCache; std::map<RsGxsMessageId, std::vector<RsGxsComment> > GxsCommentTreeWidget::mCommentsCache;
QMutex GxsCommentTreeWidget::mCacheMutex; QMutex GxsCommentTreeWidget::mCacheMutex;
//#define USE_NEW_DELEGATE 1
// This class allows to draw the item using an appropriate size // This class allows to draw the item using an appropriate size
class MultiLinesCommentDelegate: public QStyledItemDelegate class MultiLinesCommentDelegate: public QStyledItemDelegate
@ -84,7 +88,7 @@ public:
{ {
Q_ASSERT(index.isValid()); Q_ASSERT(index.isValid());
QStyleOptionViewItemV4 opt = option; QStyleOptionViewItem opt = option;
initStyleOption(&opt, index); initStyleOption(&opt, index);
// disable default icon // disable default icon
opt.icon = QIcon(); opt.icon = QIcon();
@ -107,7 +111,6 @@ public:
QSizeF s = td.documentLayout()->documentSize(); QSizeF s = td.documentLayout()->documentSize();
int m = QFontMetricsF(QFont()).height(); int m = QFontMetricsF(QFont()).height();
QSize full_area(std::min(r.width(),(int)s.width())+m,std::min(r.height(),(int)s.height())+m); 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()); QPixmap px(full_area.width(),full_area.height());
@ -115,6 +118,7 @@ public:
QPainter p(&px) ; QPainter p(&px) ;
p.setRenderHint(QPainter::Antialiasing); p.setRenderHint(QPainter::Antialiasing);
QPainterPath path ; QPainterPath path ;
path.addRoundedRect(QRectF(m/4.0,m/4.0,s.width()+m/2.0,s.height()+m/2.0),m,m) ; 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); QPen pen(Qt::gray,m/7.0f);
@ -127,6 +131,7 @@ public:
p.translate(QPointF(m/2.0,m/2.0)); p.translate(QPointF(m/2.0,m/2.0));
td.documentLayout()->draw( &p, ctx ); td.documentLayout()->draw( &p, ctx );
painter->drawPixmap(r.topLeft(),px); painter->drawPixmap(r.topLeft(),px);
const_cast<QAbstractItemModel*>(index.model())->setData(index,px.size(),POST_CELL_SIZE_ROLE); const_cast<QAbstractItemModel*>(index.model())->setData(index,px.size(),POST_CELL_SIZE_ROLE);
@ -136,11 +141,114 @@ private:
QFontMetricsF qf; QFontMetricsF qf;
}; };
#ifdef USE_NEW_DELEGATE
class NoEditDelegate: public QStyledItemDelegate
{
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) GxsCommentTreeWidget::GxsCommentTreeWidget(QWidget *parent)
:QTreeWidget(parent), mTokenQueue(NULL), mRsTokenService(NULL), mCommentService(NULL) :QTreeWidget(parent), mTokenQueue(NULL), mRsTokenService(NULL), mCommentService(NULL)
{ {
// QTreeWidget* widget = this;
setVerticalScrollMode(ScrollPerPixel); setVerticalScrollMode(ScrollPerPixel);
setContextMenuPolicy(Qt::CustomContextMenu); setContextMenuPolicy(Qt::CustomContextMenu);
RSElidedItemDelegate *itemDelegate = new RSElidedItemDelegate(this); RSElidedItemDelegate *itemDelegate = new RSElidedItemDelegate(this);
@ -148,29 +256,45 @@ GxsCommentTreeWidget::GxsCommentTreeWidget(QWidget *parent)
setItemDelegate(itemDelegate); setItemDelegate(itemDelegate);
setWordWrap(true); 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()))) ; setItemDelegateForColumn(PCITEM_COLUMN_COMMENT,new MultiLinesCommentDelegate(QFontMetricsF(font()))) ;
#endif
commentsRole = new RSTreeWidgetItemCompareRole; commentsRole = new RSTreeWidgetItemCompareRole;
commentsRole->setRole(PCITEM_COLUMN_DATE, ROLE_SORT); commentsRole->setRole(PCITEM_COLUMN_DATE, ROLE_SORT);
mUseCache = false; mUseCache = false;
// QFont font = QFont("ARIAL", 10); //header()->setSectionResizeMode(PCITEM_COLUMN_COMMENT,QHeaderView::ResizeToContents);
// 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; return;
} }
void GxsCommentTreeWidget::updateContent()
{
model()->dataChanged(QModelIndex(),QModelIndex());
std::cerr << "Updating content" << std::endl;
}
GxsCommentTreeWidget::~GxsCommentTreeWidget() GxsCommentTreeWidget::~GxsCommentTreeWidget()
{ {
if (mTokenQueue) { if (mTokenQueue) {
@ -193,14 +317,17 @@ 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 ); QMenu contextMnu( this );
QAction* action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_REPLY), tr("Reply to Comment"), this, SLOT(replyToComment())); QAction* action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_REPLY), tr("Reply to Comment"), this, SLOT(replyToComment()));
action->setDisabled(mCurrentCommentMsgId.isNull()); action->setDisabled(mCurrentCommentMsgId.isNull());
action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MESSAGE), tr("Submit Comment"), this, SLOT(makeComment())); action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_MESSAGE), tr("Submit Comment"), this, SLOT(makeComment()));
action->setDisabled(mMsgVersions.empty()); action->setDisabled(mMsgVersions.empty());
action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_COPY), tr("Copy Comment"), this, SLOT(copyComment())); action = contextMnu.addAction(FilesDefs::getIconFromQtResourcePath(IMAGE_COPY), tr("Copy Comment"), this, SLOT(copyComment()));
action->setData( item->data(PCITEM_COLUMN_COMMENT,Qt::DisplayRole) );
action->setDisabled(mCurrentCommentMsgId.isNull()); action->setDisabled(mCurrentCommentMsgId.isNull());
contextMnu.addSeparator(); contextMnu.addSeparator();
@ -342,8 +469,10 @@ void GxsCommentTreeWidget::replyToComment()
void GxsCommentTreeWidget::copyComment() void GxsCommentTreeWidget::copyComment()
{ {
QString txt = dynamic_cast<QAction*>(sender())->data().toString();
QMimeData *mimeData = new QMimeData(); QMimeData *mimeData = new QMimeData();
mimeData->setHtml("<html>"+mCurrentCommentText+"</html>"); mimeData->setHtml("<html>"+txt+"</html>");
QClipboard *clipboard = QApplication::clipboard(); QClipboard *clipboard = QApplication::clipboard();
clipboard->setMimeData(mimeData, QClipboard::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 treeCount(QTreeWidget *tree, QTreeWidgetItem *parent = 0)
{ {
int count = 0; int count = 0;
if (parent == 0) { if (parent == 0)
{
int topCount = tree->topLevelItemCount(); int topCount = tree->topLevelItemCount();
for (int i = 0; i < topCount; i++) { for (int i = 0; i < topCount; i++)
QTreeWidgetItem *item = tree->topLevelItem(i); count += treeCount(tree, tree->topLevelItem(i));
if (item->isExpanded()) {
count += treeCount(tree, item);
}
}
count += topCount; count += topCount;
} else { }
else
{
int childCount = parent->childCount(); int childCount = parent->childCount();
for (int i = 0; i < childCount; i++) { for (int i = 0; i < childCount; i++)
QTreeWidgetItem *item = parent->child(i); count += treeCount(tree, parent->child(i));
if (item->isExpanded()) {
count += treeCount(tree, item);
}
}
count += childCount; count += childCount;
} }
return count; return count;
@ -670,6 +796,11 @@ void GxsCommentTreeWidget::insertComments(const std::vector<RsGxsComment>& comme
text = QString::fromUtf8(comment.mMeta.mAuthorId.toStdString().c_str()); text = QString::fromUtf8(comment.mMeta.mAuthorId.toStdString().c_str());
item->setText(PCITEM_COLUMN_AUTHORID, text); 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); addItem(comment.mMeta.mMsgId, comment.mMeta.mParentId, item);
} }

View file

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

View file

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

View file

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

View file

@ -37,7 +37,7 @@ const uint32_t ChannelCreateEnabledFlags = (
// GXS_GROUP_FLAGS_PUBLISHSIGN | // 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_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_PERSONALSIGN |
GXS_GROUP_FLAGS_COMMENTS | // GXS_GROUP_FLAGS_COMMENTS | // disabled because the UI doesn't handle it yet. Better to hide it then.
0); 0);
const uint32_t ChannelCreateDefaultsFlags = ( GXS_GROUP_DEFAULTS_DISTRIB_PUBLIC | const uint32_t ChannelCreateDefaultsFlags = ( GXS_GROUP_DEFAULTS_DISTRIB_PUBLIC |

View file

@ -72,6 +72,25 @@ void RsGxsChannelPostsModel::setMode(TreeMode mode)
triggerViewUpdate(); 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) void RsGxsChannelPostsModel::handleEvent_main_thread(std::shared_ptr<const RsEvent> event)
{ {
const RsGxsChannelEvent *e = dynamic_cast<const RsGxsChannelEvent*>(event.get()); 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! // We need to update the data!
if(e->mChannelGroupId == mChannelGroup.mMeta.mGroupId) // make a copy of e, so as to avoid destruction of the shared pointer during async thread execution, since [e] doesn't actually tell
RsThread::async([this, e]() // 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<RsGxsChannelPost> posts;
std::vector<RsGxsComment> comments; std::vector<RsGxsComment> comments;
std::vector<RsGxsVote> votes; 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; return;
} }
// 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.
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. // 2 - update the model in the UI thread.
RsQThreadUtils::postToObject( [posts,comments,votes,this]() RsQThreadUtils::postToObject( [posts,this]()
{ {
for(uint32_t i=0;i<posts.size();++i) for(uint32_t i=0;i<posts.size();++i)
{ {
@ -121,11 +160,11 @@ void RsGxsChannelPostsModel::handleEvent_main_thread(std::shared_ptr<const RsEve
} }
},this); },this);
}); });
}
default: default:
break; break;
} }
}
} }
void RsGxsChannelPostsModel::initEmptyHierarchy() void RsGxsChannelPostsModel::initEmptyHierarchy()
@ -195,8 +234,6 @@ void RsGxsChannelPostsModel::setFilter(const QStringList& strings,bool only_unre
count = mFilteredPosts.size(); count = mFilteredPosts.size();
std::cerr << "After filtering: " << count << " posts remain." << std::endl;
beginInsertRows(QModelIndex(),0,rowCount()-1); beginInsertRows(QModelIndex(),0,rowCount()-1);
endInsertRows(); endInsertRows();
@ -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; RsErr() << __PRETTY_FUNCTION__ << " Attempt to set a number of column of 0. This is wrong." << std::endl;
return false; return false;
} }
if(mColumns == n) if((int)mColumns == n)
return false; return false;
preMods(); 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; std::cerr << __PRETTY_FUNCTION__ << " failed to retrieve channel messages for channel " << group_id << std::endl;
return; 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. // 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 COLUMN_SIZE_FONT_FACTOR_H 10
#define STAR_OVERLAY_IMAGE ":icons/star_overlay_128.png" #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_COPYLINK ":icons/png/copy.png"
#define IMAGE_GRID_VIEW ":icons/png/menu.png" #define IMAGE_GRID_VIEW ":icons/png/menu.png"
#define IMAGE_DOWNLOAD ":icons/png/download.png" #define IMAGE_DOWNLOAD ":icons/png/download.png"
@ -179,8 +180,19 @@ void ChannelPostDelegate::paint(QPainter * painter, const QStyleOptionViewItem &
QPainter p(&pixmap); QPainter p(&pixmap);
QFontMetricsF fm(option.font); 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(), 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); QString info_text = QDateTime::fromMSecsSinceEpoch(qint64(1000)*post.mMeta.mPublishTs).toString(Qt::DefaultLocaleShortDate);
if(post.mCount > 0) if(post.mAttachmentCount > 0)
info_text += ", " + QString::number(post.mCount)+ " " +((post.mCount>1)?tr("files"):tr("file")) + " (" + misc::friendlyUnit(qulonglong(post.mSize)) + ")" ; 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); painter->drawText(QPoint(p.x()+0.5*font_height,y),info_text);
y += font_height; y += font_height;
@ -738,6 +750,8 @@ void GxsChannelPostsWidgetWithModel::handleEvent_main_thread(std::shared_ptr<con
switch(e->mChannelEventCode) switch(e->mChannelEventCode)
{ {
case RsChannelEventCode::NEW_CHANNEL: // [[fallthrough]]; case RsChannelEventCode::NEW_CHANNEL: // [[fallthrough]];
case RsChannelEventCode::NEW_COMMENT: // [[fallthrough]];
case RsChannelEventCode::NEW_VOTE: // [[fallthrough]];
case RsChannelEventCode::UPDATED_CHANNEL: // [[fallthrough]]; case RsChannelEventCode::UPDATED_CHANNEL: // [[fallthrough]];
case RsChannelEventCode::NEW_MESSAGE: // [[fallthrough]]; case RsChannelEventCode::NEW_MESSAGE: // [[fallthrough]];
case RsChannelEventCode::UPDATED_MESSAGE: case RsChannelEventCode::UPDATED_MESSAGE:
@ -756,6 +770,9 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
{ {
QModelIndex index = ui->postsTree->selectionModel()->currentIndex(); QModelIndex index = ui->postsTree->selectionModel()->currentIndex();
RsGxsChannelPost post = index.data(Qt::UserRole).value<RsGxsChannelPost>() ; 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; QTextDocument doc;
doc.setHtml(post.mMsg.c_str()); doc.setHtml(post.mMsg.c_str());
@ -768,6 +785,7 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
ui->postTime_LB->hide(); ui->postTime_LB->hide();
mChannelPostFilesModel->clear(); mChannelPostFilesModel->clear();
ui->details_TW->setEnabled(false); ui->details_TW->setEnabled(false);
mSelectedPost.clear();
return; return;
} }
@ -777,7 +795,9 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
ui->postName_LB->show(); ui->postName_LB->show();
ui->postTime_LB->show(); ui->postTime_LB->show();
#ifdef DEBUG_CHANNEL_POSTS_WIDGET
std::cerr << "showPostDetails: setting mSelectedPost to current post Id " << post.mMeta.mMsgId << ". Previous value: " << mSelectedPost << std::endl; std::cerr << "showPostDetails: setting mSelectedPost to current post Id " << post.mMeta.mMsgId << ". Previous value: " << mSelectedPost << std::endl;
#endif
mSelectedPost = post.mMeta.mMsgId; mSelectedPost = post.mMeta.mMsgId;
std::list<ChannelPostFileInfo> files; std::list<ChannelPostFileInfo> files;
@ -791,7 +811,9 @@ void GxsChannelPostsWidgetWithModel::showPostDetails()
ui->commentsDialog->commentLoad(post.mMeta.mGroupId, all_msgs_versions, post.mMeta.mMsgId,true); 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; 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)); 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]() 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; mGroup = group;
mChannelPostsModel->updateChannel(groupId()); mChannelPostsModel->updateChannel(groupId());
whileBlocking(ui->filterLineEdit)->clear(); whileBlocking(ui->filterLineEdit)->clear();
@ -1132,8 +1164,10 @@ void GxsChannelPostsWidgetWithModel::insertChannelDetails(const RsGxsChannelGrou
sync_string = tr("Unknown"); 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)) auto sync_period = rsGxsChannels->getSyncPeriod(group.mMeta.mGroupId) ;
sync_string += " (Warning: will not allow latest posts to sync)";
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); ui->infoSyncTimeLabel->setText(sync_string);

View file

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

View file

@ -170,12 +170,15 @@ public:
// get pixmap // get pixmap
unsigned int read_status = qvariant_cast<uint32_t>(index.data(Qt::DecorationRole)); unsigned int read_status = qvariant_cast<uint32_t>(index.data(Qt::DecorationRole));
bool pinned = index.data(RsGxsForumModel::ThreadPinnedRole).toBool();
bool unread = IS_MSG_UNREAD(read_status); bool unread = IS_MSG_UNREAD(read_status);
bool missing = index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_DATA).data(RsGxsForumModel::MissingRole).toBool(); bool missing = index.sibling(index.row(),RsGxsForumModel::COLUMN_THREAD_DATA).data(RsGxsForumModel::MissingRole).toBool();
// set icon // set icon
if (missing) if (missing)
icon = QIcon(); icon = QIcon();
else if(pinned)
icon = FilesDefs::getIconFromQtResourcePath(IMAGE_PINPOST);
else else
{ {
if (unread) if (unread)
@ -383,6 +386,7 @@ void GxsForumThreadWidget::handleEvent_main_thread(std::shared_ptr<const RsEvent
case RsForumEventCode::NEW_FORUM: // [[fallthrough]]; case RsForumEventCode::NEW_FORUM: // [[fallthrough]];
case RsForumEventCode::UPDATED_MESSAGE: // [[fallthrough]]; case RsForumEventCode::UPDATED_MESSAGE: // [[fallthrough]];
case RsForumEventCode::NEW_MESSAGE: case RsForumEventCode::NEW_MESSAGE:
case RsForumEventCode::PINNED_POSTS_CHANGED:
case RsForumEventCode::SYNC_PARAMETERS_UPDATED: case RsForumEventCode::SYNC_PARAMETERS_UPDATED:
if(e->mForumGroupId == mForumGroup.mMeta.mGroupId) if(e->mForumGroupId == mForumGroup.mMeta.mGroupId)
updateDisplay(true); updateDisplay(true);
@ -1568,8 +1572,10 @@ void GxsForumThreadWidget::togglePinUpPost()
uint32_t token; uint32_t token;
rsGxsForums->updateGroup(token,mForumGroup); rsGxsForums->updateGroup(token,mForumGroup);
groupIdChanged(); // reloads all posts. We could also update the model directly, but the cost is so small now ;-) // We dont call this from here anymore. The update will be called by libretroshare using the rsEvent system when
updateDisplay(true) ; // the data is actually updated.
// groupIdChanged(); // reloads all posts. We could also update the model directly, but the cost is so small now ;-)
// updateDisplay(true) ;
} }
void GxsForumThreadWidget::createthread() void GxsForumThreadWidget::createthread()