Page MenuHomePhorge

The Feed should not display the old content of an edited Comment as default
Open, WishlistPublic

Description

Today I edited a comment form the web interface to fix a big typo, and I noticed that the Feed was still there showing the old comment. So users were easily seeing my big typo in plain sight, in the Phorge home of Recent changes, without any indication that, that was a deprecated content, and that it was already fixed, and that a new content was available.

It is understandable that the current behavior may be a bit confusing: the old content of a comment should not be "under the spotlights" as default, and without any kind of visual help in understanding that that comment was deprecated.

To me this is not a feature, but a completion of the already-existing feature of comment editing.

NOTE: In part the Feed already addresses changes in a Task comment: when a comment is deleted, its body is not shown in the Feed anymore, and this is nice.

Steps to reproduce:

  • have phd running
  • add a comment in a Task, write "sex times" - Save (and you see "sex times" in the feed)
  • edit that comment, write "six times" instead - Save

What happens now:

  • in the Feed (for example in your home Recent Activity) you still see the old comment content ("sex times", instead of "six times")
    • and without any indication that, that comment was superseded
    • and without any indication that, what the new content is

What could happen instead (proposals - feel free to add):

Proposal nameProposed change
"Complete proposal 1"The Feed shows the most recent content instead AND it shows that it was "Edited"
"Lazy proposal 1"The Feed just shows the most recent content instead
"Partial Proposal 1"The content is omitted from the Feed - so you have to visit it.
"Unforgiving Blockchain 1"The old Feed stays as-is, showing your mistake but a new Feed is created, showing the new content. Example: "Foo Edited the comment to:"

I'm honestly inclined in trying to implement the "Complete proposal 1". If I'm not able to do it, I would be also OK with a "Lazy proposal 1". I don't think that this would be an auditing problem, since, if you want auditing, you just may like to visit the Task itself with the full edit history of that comment. I honestly also don't like the proposal called "Unforgiving Blockchain 1" since spawning a Feed for each damn comment edit is really too much folks.

Exploration

When you create a comment, phd also creates a similar entry in the database phabricator_feed.feed_storydata:

$ SELECT * FROM feed_storydata WHERE storyData LIKE '%PHID-XACT-TASK-s2szkfumltngrjk%'\G;
*************************** 1. row ***************************
              id: 403
            phid: PHID-STRY-m4ap44a4mvsiohvodcsq
chronologicalKey: 7502335209367005152
       storyType: PhabricatorApplicationTransactionFeedStory
       storyData: {"objectPHID":"PHID-TASK-cuca7wygzrk476xeo65o","transactionPHIDs":{"PHID-XACT-TASK-s2szkfumltngrjk":"PHID-XACT-TASK-s2szkfumltngrjk"}}
      authorPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
     dateCreated: 1746773535
    dateModified: 1746773535
1 row in set (0,001 sec)
NOTE: Unrelated task but visible from here: the storyData could be de-duplicated. See T15954.

The other tables in the same database does not contain extra data.

The only thing that is additionally happening, is a new cache entry added, about the feed content:

SELECT * FROM cache_markupcache\G;
*************************** 1. row ***************************
          id: 1
    cacheKey: feed:7502335209367005152:comment/378@21@KrA9lniBCyaV
   cacheData: a:3:{s:6:"output";O:14:"PhutilSafeHTML":1:{s:23:" PhutilSafeHTML content";s:11:"<p>MIAO</p>";}s:7:"storage";a:0:{}s:8:"metadata";a:0:{}}
    metadata: {"host":"boz-tuxedoinfinitybooksgen8"}
 dateCreated: 1746774070
dateModified: 1746774070

So, after you update the comment, it seems the related feed cache entry is not updated.

Workflow comparison on Comment Created VS Comment Updated:

TopicWhatComment CreatedComment Updated
EditorManiphestTransactionEditor (or specific editor)โœ… in useโŒ not in use
Editor\ PhabricatorApplicationTransactionEditorโœ… in useโŒ not in use
Editor\ PhabricatorApplicationTransactionEditor shouldPublishโœ… in useโŒ not in use
Cache\ PhabricatorCacheEngineโœ… in useโŒ not in use
Editor\ PhabricatorApplicationTransactionCommentEditorโœ… in use (automatic)โœ… in use (manually)
ControllerPhabricatorApplicationTransactionCommentEditControllerโŒโœ… in use
  • โ“ Question "Why not App Editor": could the "Comment Updated"'s controller (PhabricatorApplicationTransactionCommentEditController) avoid to use that current editor (PhabricatorApplicationTransactionCommentEditor) that seems somehow internal, and does not trigger the story, and does not refresh the cache; and use instead the app-specific ManiphestTransactionEditor? Uhm?
    • โŒ Maybe no we can get the broad editor using ManiphestTask#getApplicationTransactionEditor() that returns a ManiphestTransactionEditor().

In fact, this attempt fails miserably:

diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php
index 84fcecfa6f..f6a2ff7034 100644
--- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php
+++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php
@@ -58,12 +58,12 @@ final class PhabricatorApplicationTransactionCommentEditController
         $comment->setIsDeleted(true);
       }
 
-      $editor = id(new PhabricatorApplicationTransactionCommentEditor())
+      $object->getApplicationTransactionEditor()
         ->setActor($viewer)
         ->setContentSource(PhabricatorContentSource::newFromRequest($request))
         ->setRequest($request)
         ->setCancelURI($done_uri)
-        ->applyEdit($xaction, $comment);
+        ->applyTransactions($object, array($xaction));
 
       if ($request->isAjax()) {
         return id(new AphrontAjaxResponse())->setContent(array());

โŒ If you try the "Why not App Editor" approach, we fail miserably;

Attempting to apply a transaction (of class "ManiphestTransaction", with type "core:comment") which has not been constructed correctly: You can not apply transactions which already have IDs/PHIDs!

https://we.phorge.it/source/phorge/browse/master/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php;8d688f59d123596f1f252dc450427ebb5c9235db$1774-1778

Workaround

Known workaround since 2025-05-09 09:00:00 CEST:

./bin/cache purge --caches remarkup

That's all. So this workaround implements the "Lazy proposal 1", so, the old entry is just refreshed to reflect the new comment.

Proposed Implementation: fix missing cache update

Given the nature of the already existing workaround, that seems related to the remarkup cache,

Given the fact that the current workaround is already in line with an already mentioned proposal (the "Lazy proposal 1"),

Given the fact that the current workaround is very probably already automatically fired when the cache expires, so causing that "Lazy Proposal 1",

Probably we should just reset that specific cache entry when an user updates a comment.

Event Timeline

valerio.bozzolan triaged this task as Low priority.
valerio.bozzolan created this object in space S1 Public.
avivey renamed this task from The Feed should not display the old content of an edited Task comment as default to The Feed should not display the old content of an edited Comment as default.Jul 18 2023, 13:10
avivey updated the task description. (Show Details)

Uh I've found a workaround.

./bin/cache purge --caches remarkup

Trying the ManiphestTransactionEditor leads to a dead end. Added personal notes about it...

valerio.bozzolan lowered the priority of this task from Low to Wishlist.Fri, May 9, 14:53

Uhm this is a damn rabbit hole.

Premising that we should probably drop that PhabricatorMarkupCache cache entry. This would be possible with:

$cache_key = 'feed:7502363188833074262:comment/381@21@KrA9lniBCyaV'; // something lol
$table = new PhabricatorMarkupCache();
$conn_w = $table->establishConnection('w');
queryfx(
  $conn_w,
  'DELETE FROM %T WHERE cacheKey = %s',
  $table->getTableName(),
  $cache_key);

So, the sub-problem is: knowing the $cache_key of that web feed. It seems we can get that with ->getCacheKey() from the feed object, that I think it's PhabricatorFeedStoryData.

So, how to get a PhabricatorFeedStoryData object starting from a Maniphest comment?

Let's go down-top.

$ SHOW CREATE TABLE feed_storydata;
CREATE TABLE `feed_storydata` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `phid` varbinary(64) NOT NULL,
  `chronologicalKey` bigint(20) unsigned NOT NULL,
  `storyType` varchar(64) NOT NULL,
  `storyData` longtext NOT NULL,
  `authorPHID` varbinary(64) NOT NULL,
  `dateCreated` int(10) unsigned NOT NULL,
  `dateModified` int(10) unsigned NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `chronologicalKey` (`chronologicalKey`),
  UNIQUE KEY `phid` (`phid`)
);
$ SHOW CREATE TABLE feed_storyreference;
CREATE TABLE `feed_storyreference` (
  `objectPHID` varbinary(64) NOT NULL,
  `chronologicalKey` bigint(20) unsigned NOT NULL,
  UNIQUE KEY `objectPHID` (`objectPHID`,`chronologicalKey`),
  KEY `chronologicalKey` (`chronologicalKey`)
);

Oh no...

WARNING: It seems the database does NOT know which feeds are related to which comments! Some details are available (and indexed) in the phabricator_feed database, like which feeds are related to which object (e.g. the task, not the comment), which user or which project, and these are stored in the feed_storyreference but the comment PHID itself is only stored in the JSON "story blob".

I arrived to this conclusion looking at this example of what is stored in feed_storydata and feed_storyreference tables, about the comment PHID-XACT-TASK-zoindw3d4fke4hj. Look that I have to inspect the JSON blob (!) from feed_storydata to find the story PHID-STRY-pkza5gzsx3xq5wresktt:

$ SELECT * FROM feed_storyreference JOIN feed_storydata ON ( feed_storyreference.chronologicalKey = feed_storydata.chronologicalKey) WHERE storyData LIKE '%"PHID-XACT-TASK-zoindw3d4fke4hj"%'\G;
*************************** 1. row ***************************
      objectPHID: PHID-PROJ-plufiz5pclartkuv5ggz
chronologicalKey: 7502387652723566928
              id: 426
            phid: PHID-STRY-pkza5gzsx3xq5wresktt
chronologicalKey: 7502387652723566928
       storyType: PhabricatorApplicationTransactionFeedStory
       storyData: {"objectPHID":"PHID-TASK-4yesfewxvjsrhjgevdcd","transactionPHIDs":{"PHID-XACT-TASK-zoindw3d4fke4hj":"PHID-XACT-TASK-zoindw3d4fke4hj"}}
      authorPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
     dateCreated: 1746785746
    dateModified: 1746785746
*************************** 2. row ***************************
      objectPHID: PHID-PROJ-vsiodka4ga3tgspsxfgh
chronologicalKey: 7502387652723566928
              id: 426
            phid: PHID-STRY-pkza5gzsx3xq5wresktt
chronologicalKey: 7502387652723566928
       storyType: PhabricatorApplicationTransactionFeedStory
       storyData: {"objectPHID":"PHID-TASK-4yesfewxvjsrhjgevdcd","transactionPHIDs":{"PHID-XACT-TASK-zoindw3d4fke4hj":"PHID-XACT-TASK-zoindw3d4fke4hj"}}
      authorPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
     dateCreated: 1746785746
    dateModified: 1746785746
*************************** 3. row ***************************
      objectPHID: PHID-TASK-4yesfewxvjsrhjgevdcd
chronologicalKey: 7502387652723566928
              id: 426
            phid: PHID-STRY-pkza5gzsx3xq5wresktt
chronologicalKey: 7502387652723566928
       storyType: PhabricatorApplicationTransactionFeedStory
       storyData: {"objectPHID":"PHID-TASK-4yesfewxvjsrhjgevdcd","transactionPHIDs":{"PHID-XACT-TASK-zoindw3d4fke4hj":"PHID-XACT-TASK-zoindw3d4fke4hj"}}
      authorPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
     dateCreated: 1746785746
    dateModified: 1746785746
*************************** 4. row ***************************
      objectPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
chronologicalKey: 7502387652723566928
              id: 426
            phid: PHID-STRY-pkza5gzsx3xq5wresktt
chronologicalKey: 7502387652723566928
       storyType: PhabricatorApplicationTransactionFeedStory
       storyData: {"objectPHID":"PHID-TASK-4yesfewxvjsrhjgevdcd","transactionPHIDs":{"PHID-XACT-TASK-zoindw3d4fke4hj":"PHID-XACT-TASK-zoindw3d4fke4hj"}}
      authorPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
     dateCreated: 1746785746
    dateModified: 1746785746
4 rows in set (0,002 sec)

So, this is the very inefficient (but most efficient?) query to find the story PHID of a comment PHID-XACT-TASK-zoindw3d4fke4hj (still note the scary blob inspection):

$ SELECT feed_storydata.phid FROM feed_storydata WHERE EXISTS (SELECT * FROM feed_storyreference WHERE objectPHID = 'PHID-TASK-4yesfewxvjsrhjgevdcd' AND feed_storydata.chronologicalKey = feed_storyreference.chronologicalKey) AND authorPHID = 'PHID-USER-ktgdqflnaqzfirxfpu4z' AND storydata LIKE '%PHID-XACT-TASK-zoindw3d4fke4hj%';
+--------------------------------+
| phid                           |
+--------------------------------+
| PHID-STRY-pkza5gzsx3xq5wresktt |
+--------------------------------+

\o/

Performance consideration n.1:

In an ideal world, feed_storyreference would also contain a row about the comment PHID. This could be introduced but may weigh down the DB. This is probably the easiest thing ever to speedup this query. So we would do just this (now not possible):

SELECT feed_storydata.phid FROM feed_storydata WHERE EXISTS (SELECT * FROM feed_storyreference WHERE objectPHID = 'PHID-XACT-TASK-zoindw3d4fke4hj' AND feed_storydata.chronologicalKey = feed_storyreference.chronologicalKey);

Performance consideration n.2:

If we don't to the above thing, at least we could add an index on feed_storydata.authorPHID. So it can be used as great optimization to inspect the storyData only on the right user.

Performance consideration n.3:

If we don't to the above things(s), we could at least try to guess the chronologicalKey and use that in the WHERE condition so it would be blazing fast.

Unfortunately I do not see a relationship between the comment creation timestamp and the chronologicalKey.

e.g.

$ SELECT * FROM phabricator_maniphest.maniphest_transaction_comment\G;
             id: 40
           phid: PHID-XCMT-cydbumhaeoedrbhp5qfn
transactionPHID: PHID-XACT-TASK-zoindw3d4fke4hj
     authorPHID: PHID-USER-ktgdqflnaqzfirxfpu4z
     viewPolicy: public
     editPolicy: PHID-USER-ktgdqflnaqzfirxfpu4z
 commentVersion: 1
        content: fuf
  contentSource: {"source":"web","params":[]}
      isDeleted: 0
    dateCreated: 1746785746
   dateModified: 1746785746

See the dateCreated = 1746785746 VS chronologicalKey = 7502387652723566928.

Uhm. So, this chronologicalKey could be re-created by creating another PhabricatorFeedStoryPublisher (?)

https://we.phorge.it/source/phorge/browse/master/src/applications/feed/PhabricatorFeedStoryPublisher.php;6619fef2ff977ea81092b970e58abbb33e78f644$0-299

So, to build a PhabricatorFeedStoryPublisher, it seems these are required:

ArgValue
typePhabricatorApplicationTransactionFeedStory
`storyTimeThe Task creation date? 1746785746

So the sub-task now is... let's try to generate a PhabricatorFeedStoryPublisher and get the generateChronologicalKey().

But... ouch...

WARNING: The method generateChronologicalKey() in PhabricatorFeedStoryPublisher is private. Probably it's a good idea to make it public...?

Uff. This is becoming even less actionable. Let's downgrade.