3

I was trying to make a Chatter Post from a Flow.

I have a ScreenFlow in order to get the richtext from the user, I save it in a variable and try to convert it in a Chatter Post via ConnectApiHelper.

However, the inputrichtext makes servlet images like this:

https://xxx--sal320v2.sandbox.file.force.com/servlet/rtaImage?refid=0EM5t000000CkHx

And ConnectApiHelper only accetps ContentDocument Objects and not ContentReference

The entire solutions looks like this for now:

Flow:

enter image description here

Main Screen Flow: enter image description here

Invocable Method:

 global without sharing class PostRichChatter {
   
   
     @InvocableMethod
     global static List <Results> post (List<Requests> requestList) {
         System.debug('entering PostRichChatter');
         String body = requestList[0].body;
         String targetNameOrId = requestList[0].targetNameOrId;
         String communityId = requestList[0].communityId;
         System.debug ('here are the values flow has passed');
         System.debug (requestList[0]);
         //System.debug ('targetNameOrId is: '  + targetNameOrId);
         //System.debug ('targetType is: '  + targetType);
         System.debug ('communityId is: ' + communityId);
         if (communityId == null)
             communityId = Network.getNetworkId();
 
         //check to see if the targetNameOrId matches a Group Name or a Username
         List<Group> targetGroup = [SELECT Id FROM Group WHERE Name =:targetNameOrId];
         System.debug('targetGroup is: ' + targetGroup);
         if (targetGroup.size() == 1) {
                         
             targetNameOrId = targetGroup[0].Id;
         } else {
             System.debug('targetNameOrId is: ' + targetNameOrId );
             List<User> targetUser = [SELECT Id FROM User WHERE username =:targetNameOrId];
             System.debug('targetUser is: ' + targetUser);
             if (targetUser.size() == 1){
                 targetNameOrId = targetUser[0].Id;
             }
         }
 
         //several things that Flow rich text supports, like color and font size, are represented by span tags. chatter doesn't support, so need to strip them out
         Pattern MyPattern = Pattern.compile('<\\/?span[^>]*>');
         Matcher MyMatcher = MyPattern.matcher(body);
         body = MyMatcher.replaceAll('');
 
         //mgarcia-> br from rich text in the other package are not supported, so i filtered here
         String br = '<br>';
         String newLineJump = '<p></p>';
         body = body.replaceAll(br, newLineJump);
         System.debug ('body after br replacement is: ' + body);
 
         //chatter's support for hyperlinks is unconventional and I haven't figured out how to convert a normal anchor link (<a href="http://www.salesforce.com" target="_blank">link</a>) into the chatter equivalent
         //so this hack puts the url into a parentheses next to the link word. anyone who has a better approach is encouraged.
         //extract the url
            //find all strings that start with <a and end with > i.e.extract url from an anchor tag
         //extract the text
         //craft a replacement string
 
         String anchorPattern = '(<a href="([^"]+)" target="_blank">([^<]+)<\\/a>)';
         MyPattern = Pattern.compile(anchorPattern);
         MyMatcher = MyPattern.matcher(body);
         body = MyMatcher.replaceAll('$3 ($2) ');
          System.debug ('body after replacement is: ' + body);
 
         //also have to strip out indent markup:
         //Here's some indented stuff:</p><p class="ql-indent-2">I'm indented</p><p>
         String indentedClassPattern = ' class="ql-indent-[1-4]"';
         MyPattern = Pattern.compile(indentedClassPattern);
         MyMatcher = MyPattern.matcher(body);
         body = MyMatcher.replaceAll('');
         System.debug ('body after replacement is: ' + body);
 
 
         //image links work fine, but we'll do a little reformatting to remove the html 
         String imagePattern = '<img src="([^"]+)">';
         MyPattern = Pattern.compile(imagePattern);
         MyMatcher = MyPattern.matcher(body);
         body = MyMatcher.replaceAll('image: $1');
         System.debug ('body after replacement is: ' + body);
 
         //Final process to convert the body into ConnectApi into a matchRichText with IDS
 
         String pStart   = '<p>';
         String pEnd     = '</p>';
         String imageDetector = 'image: https';
         String idDetector = 'refid=';
         Pattern pPattern = Pattern.compile('<\\/?p[^>]*>');
         Matcher pMatcher = pPattern.matcher(body);
         List<String> paragraphs = new List<String>();
 
         paragraphs = body.split('<\\/?p[^>]*>');
 
         if(paragraphs.size() > 0)
         {
             for (String paraf : paragraphs) 
             {
                 List<String> imagesDetected = new List<String>();
                 imagesDetected = paraf.split(imageDetector);
 
                 if(imagesDetected.size() > 0)
                 {
                     for (String imageRef : imagesDetected) 
                     {
                         System.debug(imageRef);
     
                         String testImg = imageRef.substringAfter(idDetector);
 
                         if(testImg != null && testImg != '')
                         {
                             System.debug('SUBSTRING');
                             System.debug(testImg);
                             Id imageToInsert = imageRef.substringAfter(idDetector);
                             body = body.replace(imageDetector + imageRef, ' {img:' + imageToInsert + ':The image is not load correctly.} ');
                         }
                     }
                 }
             }
         }
 
         body = body.replaceAll(pStart, '');
         body = body.replaceAll(pEnd, '\n');
         System.debug('FINAL BODY');
         System.debug(body);
 
         //ConnectApi.FeedItem fi = (ConnectApi.FeedItem) ConnectApiHelper.postFeedItemWithMentions(Network.getNetworkId(), 'me', 'Hey there {005D00000015tjz}, how are you?');
         ConnectApi.FeedItem feedItem = (ConnectApi.FeedItem) ConnectApiHelper.postFeedItemWithRichText(communityId, targetNameOrId, body);
 
 
        
         
         //Create a Results object to hold the return values
 
         Results response = new Results();
 
         //add the return values to the Results object
         response.feedItemId = feedItem.id;
 
         //Wrap the Results object in a List container (an extra step added to allow this interface to also support bulkification)
         List<Results> responseWrapper= new List<Results>();
         responseWrapper.add(response);
         return responseWrapper;
     
     }
 
     global without sharing class Requests {
         
       @InvocableVariable(required=true)
       public String body;
 
       @InvocableVariable(required=true description='Reference to the user, Chatter group, or record whose feed you want to post to.')
       public String targetNameOrId;
 
      // @InvocableVariable(required=false)
       // public String targetType;
 
       @InvocableVariable(required=false)
       public String communityId;
 
         
     }
     
     global without sharing class Results {
      
       @InvocableVariable
       public String feedItemId;
 
     }
 
 }

ConnectApiHelper in case you want to check:

 /**
  *
  * Helper class that makes it easier to do common operations with the classes in the ConnectApi namespace.
  *
  * Includes convenience methods to:
  *
  *     - Post Chatter @-mentions, rich text, and inline images with Apex code.
  *     - Take a feed item or comment body and return an input body that matches it.
  *       This is useful for when you retrieve a feed item or comment and want to either
  *       re-post or edit it.
  *
  * This class works with API version 43.0 and later. There are separate classes
  * that work with v42.0 and earlier.
  *
  * See https://github.com/forcedotcom/ConnectApiHelper for more information.
  *
  */
 
 global without sharing class ConnectApiHelper {
 
     public class InvalidParameterException extends Exception {}
 
     private static final Map<String, ConnectApi.MarkupType> supportedMarkup = new Map<String, ConnectApi.MarkupType> {
         'b' => ConnectApi.MarkupType.Bold,
         'code' => ConnectApi.MarkupType.Code,
         'i' => ConnectApi.MarkupType.Italic,
         'li' => ConnectApi.MarkupType.ListItem,
         'ol' => ConnectApi.MarkupType.OrderedList,
         'p' => ConnectApi.MarkupType.Paragraph,
         's' => ConnectApi.MarkupType.Strikethrough,
         'u' => ConnectApi.MarkupType.Underline,
         'ul' => ConnectApi.MarkupType.UnorderedList
     };
 
     /**
      * Posts a feed item with @-mentions using an @-mention formatting syntax.
      *
      * @param communityId Use either the ID of a community, 'internal', or null.
      * @param subjectId The parent of the post. Can be a user ID, a group ID, or a record ID.
      * @param textWithMentions The text of the post. You can @-mention a user or group by using
      *                         the syntax {ID}, for example: 'Hello {005x0000000URNP}, have you
      *                         seen the group {0F9x00000000D7m}?' Links and hashtags will be
      *                         automatically parsed if provided.
      * @return The posted feed item.
      */
     public static ConnectApi.FeedElement postFeedItemWithMentions(String communityId, String subjectId, String textWithMentions) {
 
         return postFeedItemWithSpecialFormatting(communityId, subjectId, textWithMentions, 'textWithMentions');
     }
 
     /**
      * Posts a feed item with rich text using HTML tags and inline image formatting syntax.
      *
      * @param communityId Use either the ID of a community, 'internal', or null.
      * @param subjectId The parent of the post. Can be a user ID, a group ID, or a record ID.
      * @param textWithMentionsAndRichText The text of the post. You can @-mention a
      *                         user or group by using the syntax {ID}, for example:
      *                         'Hello {005x0000000URNP}, have you seen the group {0F9x00000000D7m}?'
      *                         You can include rich text by using supported HTML tags:
      *                         <b>, <i>, <u>, <s>, <ul>, <ol>, <li>, <p>, <code>.
      *                         You can include an inline image by using the syntax {img:ID} or
      *                         {img:ID:alt text}, for example: 'Have you seen this gorgeous view?
      *                         {img:069x00000000D7m:View of the Space Needle from our office.}?'
      *                         Links and hashtags will be automatically parsed if provided.
      * @return The posted feed item.
      */
     public static ConnectApi.FeedElement postFeedItemWithRichText(String communityId, String subjectId, String textWithMentionsAndRichText) {
         return postFeedItemWithSpecialFormatting(communityId, subjectId, textWithMentionsAndRichText, 'textWithMentionsAndRichText');
     }
 
     private static ConnectApi.FeedElement postFeedItemWithSpecialFormatting(String communityId, String subjectId, String formattedText, String textParameterName) {
         if (formattedText == null || formattedText.trim().length() == 0) {
             throw new InvalidParameterException('The ' + textParameterName + ' parameter must be non-empty.');
         }
 
         ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();
         messageInput.messageSegments = getMessageSegmentInputs(formattedText);
 
         ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput();
         input.body = messageInput;
         input.subjectId = subjectId;
 
         return ConnectApi.ChatterFeeds.postFeedElement(communityId, input);
     }
 
     /**
      * Posts a comment with @-mentions using an @-mention formatting syntax.
      *
      * @param communityId Use either the ID of a community, 'internal', or null.
      * @param feedItemId The ID of the feed item being commented on.
      * @param textWithMentions The text of the comment. You can @-mention a user or group by using
      *                         the syntax {ID}, for example: 'Hello {005x0000000URNP}, have you
      *                         seen the group {0F9x00000000D7m}?' Links and hashtags will be
      *                         automatically parsed if provided.
      * @return The posted comment.
      */
     public static ConnectApi.Comment postCommentWithMentions(String communityId, String feedItemId, String textWithMentions) {
 
         if (textWithMentions == null || textWithMentions.trim().length() == 0) {
             throw new InvalidParameterException('The textWithMentions parameter must be non-empty.');
         }
 
         ConnectApi.MessageBodyInput messageInput = new ConnectApi.MessageBodyInput();
         messageInput.messageSegments = getMessageSegmentInputs(textWithMentions);
 
         ConnectApi.CommentInput input = new ConnectApi.CommentInput();
         input.body = messageInput;
 
         return ConnectApi.ChatterFeeds.postCommentToFeedElement(communityId, feedItemId, input, null);
     }
 
     public static List<ConnectApi.MessageSegmentInput> getMessageSegmentInputs(String inputText) {
         if (inputText == null) {
             throw new InvalidParameterException('The inputText parameter cannot be null.');
         }
         System.debug('INFO TEXT PROCESS POST');
         System.debug(inputText);
         List<ConnectApi.MessageSegmentInput> messageSegmentInputs = new List<ConnectApi.MessageSegmentInput>();
         Integer strPos = 0;
         // The pattern for matching mentions, markup begin/end tags, and inline images.
         // The first group matches a 15 or 18 character ID surrounded by {}:
         //   (\\{[a-zA-Z0-9]{15}\\}|\\{[a-zA-Z0-9]{18}\\})
         // The second/third groups match beginning/ending HTML tags: (<[a-zA-Z]*>)|(</[a-zA-Z]*>)
         // The fourth group matches a 15 or 18 character content document ID preceded by "img:",
         //    optionally followed by a string (not containing '}'), and surrounded by {}:
         //    (\\{img:(069[a-zA-Z0-9]{12,15})(:[\\s\\S]*?)?\\})
         // The fifth group matches a 15 or 18 character record ID preceded by "record:" ex:{record:01t3E000002GCm9QAG}
         Pattern globalPattern = Pattern.compile('(\\{[a-zA-Z0-9]{15}\\}|\\{[a-zA-Z0-9]{18}\\})|(<[a-zA-Z]*>)|(<\\/[a-zA-Z]*>)|(\\{img:(069[a-zA-Z0-9]{12,15})(:[\\s\\S]*?)?\\})|(\\{record:([a-zA-Z0-9]){15,18}(:[\\s\\S]*?)?\\})');
 
         Matcher globalMatcher = globalPattern.matcher(inputText);
 
         while (globalMatcher.find()) {
             String textSegment = inputText.substring(strPos, globalMatcher.start());
             String matchingText = globalMatcher.group();
             System.debug('MATCH PATTERN TEXT PROCESS POST');
             System.debug(matchingText);
             if (matchingText.startsWith('{')) {
                 // Add a segment for any accumulated text (which includes unsupported HTML tags).
                 addTextSegment(messageSegmentInputs, textSegment);
 
                 // Strip off the { and }.
                 String innerMatchedText = matchingText.substring(1, matchingText.length() - 1);
 
                 if (innerMatchedText.startsWith('img:')) {
                     // This is an inline image.
                     String[] imageInfo = innerMatchedText.split(':', 3);
                     String altText = imageInfo.size() == 3 ? imageInfo[2] : null;
                     System.debug('INFO IMAGE PROCESS POST');
                     System.debug(imageInfo);
                     ConnectApi.InlineImageSegmentInput inlineImageSegmentInput = makeInlineImageSegmentInput(imageInfo[1], altText);
                     messageSegmentInputs.add(inlineImageSegmentInput);
                     strPos = globalMatcher.end();
                 }
                 else if (innerMatchedText.startsWith('record:')) {
                     // Inline record
                     String[] recordInfo = innerMatchedText.split(':');
                     ConnectApi.EntityLinkSegmentInput entityLinkSegmentInput = makeEntityLinkSegmentInput(recordInfo[1]);
                     messageSegmentInputs.add(entityLinkSegmentInput);
                     strPos = globalMatcher.end();
                 }
                 else {
                     // This is a mention id.
                     ConnectApi.MentionSegmentInput mentionSegmentInput = makeMentionSegmentInput(innerMatchedText);
                     messageSegmentInputs.add(mentionSegmentInput);
                     strPos = globalMatcher.end();
                 }
             }
             else {
                 // This is an HTML tag.
                 boolean isBeginTag = !matchingText.startsWith('</');
                 if (isBeginTag) {
                     // Strip off the < and >.
                     String tag = matchingText.substring(1, matchingText.indexOf('>'));
                     if (supportedMarkup.containsKey(tag.toLowerCase())) {
                         // Add a segment for any accumulated text (which includes unsupported HTML tags).
                         addTextSegment(messageSegmentInputs, textSegment);
 
                         ConnectApi.MarkupBeginSegmentInput markupBeginSegmentInput = makeMarkupBeginSegmentInput(tag);
                         messageSegmentInputs.add(markupBeginSegmentInput);
                         strPos = globalMatcher.end();
                     }
                 }
                 else { // This is an end tag.
                     // Strip off the </ and >.
                     String tag = matchingText.substring(2, matchingText.indexOf('>'));
                     if (supportedMarkup.containsKey(tag.toLowerCase())) {
                         // Add a segment for any accumulated text (which includes unsupported HTML tags).
                         addTextSegment(messageSegmentInputs, textSegment);
 
                         ConnectApi.MarkupEndSegmentInput markupEndSegmentInput = makeMarkupEndSegmentInput(tag);
                         messageSegmentInputs.add(markupEndSegmentInput);
                         strPos = globalMatcher.end();
                     }
                 }
             }
         }
 
         // Take care of any text that comes after the last match.
         if (strPos < inputText.length()) {
             String trailingText = inputText.substring(strPos, inputText.length());
             addTextSegment(messageSegmentInputs, trailingText);
         }
 
         return messageSegmentInputs;
     }
 
     private static void addTextSegment(List<ConnectApi.MessageSegmentInput> messageSegmentInputs, String text) {
         if (text != null && text.length() > 0) {
             ConnectApi.TextSegmentInput textSegmentInput = makeTextSegmentInput(text);
             messageSegmentInputs.add(textSegmentInput);
         }
     }
 
     private static ConnectApi.TextSegmentInput makeTextSegmentInput(String text) {
         ConnectApi.TextSegmentInput textSegment = new ConnectApi.TextSegmentInput();
         textSegment.text = text;
         return textSegment;
     }
 
     private static ConnectApi.MentionSegmentInput makeMentionSegmentInput(String mentionId) {
         ConnectApi.MentionSegmentInput mentionSegment = new ConnectApi.MentionSegmentInput();
         mentionSegment.id = mentionId;
         return mentionSegment;
     }
 
     // Create entity link segment input using record ID
     // In the chatter post, it will display the Name of the record, with a hyperlink to view the record 
     private static ConnectApi.EntityLinkSegmentInput makeEntityLinkSegmentInput(String recordId) {
         ConnectApi.EntityLinkSegmentInput entityLinkSegment = new ConnectApi.EntityLinkSegmentInput();
         entityLinkSegment.entityId = recordId;
         return entityLinkSegment;
     }
 
 
     /**
      * Create a MarkupBeginSegmentInput corresponding to the tag.  Checking whether the tag is
      * supported markup should happen before calling this method.
      */
     private static ConnectApi.MarkupBeginSegmentInput makeMarkupBeginSegmentInput(String tag) {
         ConnectApi.MarkupBeginSegmentInput markupBeginSegment = new ConnectApi.MarkupBeginSegmentInput();
         markupBeginSegment.markupType = supportedMarkup.get(tag.toLowerCase());
         return markupBeginSegment;
     }
 
     /**
      * Create a MarkupEndSegmentInput corresponding to the tag.  Checking whether the tag is
      * supported markup should happen before calling this method.
      */
     private static ConnectApi.MarkupEndSegmentInput makeMarkupEndSegmentInput(String tag) {
         ConnectApi.MarkupEndSegmentInput markupEndSegment = new ConnectApi.MarkupEndSegmentInput();
         markupEndSegment.markupType = supportedMarkup.get(tag.toLowerCase());
         return markupEndSegment;
     }
 
     private static ConnectApi.InlineImageSegmentInput makeInlineImageSegmentInput(String fileId, String altText) {
         ConnectApi.InlineImageSegmentInput inlineImageSegment = new ConnectApi.InlineImageSegmentInput();
         inlineImageSegment.fileId = fileId;
         if (String.isNotBlank(altText)) {
             inlineImageSegment.altText = altText;
         }
         return inlineImageSegment;
     }
 
     /**
      * Takes an output feed body and returns a message body input that matches it.
      * This is useful for when you retrieve a feed item or comment and want to either re-post or edit it.
      */
     public static ConnectApi.MessageBodyInput createInputFromBody(ConnectApi.FeedBody body) {
         ConnectApi.MessageBodyInput input = new ConnectApi.MessageBodyInput();
         input.messageSegments = new List<ConnectApi.MessageSegmentInput>();
         
         // Identify newline and replace it with encoded values that MessageSegment will handle correctly.
         String newline = EncodingUtil.urlDecode('%0A', 'UTF-8'); 
 
         for (ConnectApi.MessageSegment segment : body.messageSegments) {
             if (segment instanceof ConnectApi.TextSegment) {
                 ConnectApi.TextSegment textOutput = (ConnectApi.TextSegment) segment;
                 if (textOutput.text.indexOf(newline) > -1) {
                     // If newline is found, create multiple message segments instead of just a textSegment.
                     List<ConnectApi.MessageSegmentInput> newSegments = 
                         ConnectApiHelper.getMessageSegmentInputs(textOutput.text
                                         .replaceAll(newline + newline, '<p>&nbsp;</p>')
                                         .replaceAll(newline, '<p></p>'));
                     input.messageSegments.addAll(newSegments);
                 }
                 else {
                     ConnectApi.TextSegmentInput textInput = new ConnectApi.TextSegmentInput();
                     textInput.text = textOutput.text;
                     input.messageSegments.add(textInput);
                 }
             }
             else if (segment instanceof ConnectApi.MentionSegment) {
                 ConnectApi.MentionSegment mentionOutput = (ConnectApi.MentionSegment) segment;
                 ConnectApi.MentionSegmentInput mentionInput = new ConnectApi.MentionSegmentInput();
                 mentionInput.id = mentionOutput.record.id;
                 input.messageSegments.add(mentionInput);
             }
             else if (segment instanceof ConnectApi.HashtagSegment) {
                 ConnectApi.HashtagSegment hashtagOutput = (ConnectApi.HashtagSegment) segment;
                 ConnectApi.HashtagSegmentInput hashtagInput = new ConnectApi.HashtagSegmentInput();
                 hashtagInput.tag = hashtagOutput.tag;
                 input.messageSegments.add(hashtagInput);
             }
             else if (segment instanceof ConnectApi.LinkSegment) {
                 ConnectApi.LinkSegment linkOutput = (ConnectApi.LinkSegment) segment;
                 ConnectApi.LinkSegmentInput linkInput = new ConnectApi.LinkSegmentInput();
                 linkInput.url = linkOutput.url;
                 input.messageSegments.add(linkInput);
             }
             else if (segment instanceof ConnectApi.MarkupBeginSegment) {
                 ConnectApi.MarkupBeginSegment markupBeginOutput = (ConnectApi.MarkupBeginSegment) segment;
                 ConnectApi.MarkupBeginSegmentInput markupBeginInput = new ConnectApi.MarkupBeginSegmentInput();
                 markupBeginInput.markupType = markupBeginOutput.markupType;
                 input.messageSegments.add(markupBeginInput);
             }
             else if (segment instanceof ConnectApi.MarkupEndSegment) {
                 ConnectApi.MarkupEndSegment markupEndOutput = (ConnectApi.MarkupEndSegment) segment;
                 ConnectApi.MarkupEndSegmentInput markupEndInput = new ConnectApi.MarkupEndSegmentInput();
                 markupEndInput.markupType = markupEndOutput.markupType;
                 input.messageSegments.add(markupEndInput);
             }
             else if (segment instanceof ConnectApi.InlineImageSegment) {
                 ConnectApi.InlineImageSegment inlineImageOutput = (ConnectApi.InlineImageSegment) segment;
                 ConnectApi.InlineImageSegmentInput inlineImageInput = new ConnectApi.InlineImageSegmentInput();
                 inlineImageInput.fileId = inlineImageOutput.thumbnails.fileId;
                 inlineImageInput.altText = inlineImageOutput.altText;
                 input.messageSegments.add(inlineImageInput);
             }
             else {
                 // The other segment types are system-generated and have no corresponding input types.
             }
 
         }
         return input;
     }
 
     /**
      * Takes an output body and returns a feed item input body that matches it.
      * This is useful for when you retrieve a feed item and want to either re-post or edit it.
      */
     public static ConnectApi.FeedItemInput createFeedItemInputFromBody(ConnectApi.FeedBody body) {
         ConnectApi.MessageBodyInput bodyInput = createInputFromBody(body);
 
         ConnectApi.FeedItemInput input = new ConnectApi.FeedItemInput();
         input.body = bodyInput;
         return input;
     }
 
     /**
      * Takes an output body and returns a comment input body that matches it.
      * This is useful for when you retrieve a comment and want to either re-post or edit it.
      */
     public static ConnectApi.CommentInput createCommentInputFromBody(ConnectApi.FeedBody body) {
         ConnectApi.MessageBodyInput bodyInput = createInputFromBody(body);
 
         ConnectApi.CommentInput input = new ConnectApi.CommentInput();
         input.body = bodyInput;
         return input;
     }
 }

Any thoughts?

3
  • How are you getting rich text input in the screen flow? Through unofficialsf? Commented Mar 16, 2022 at 12:01
  • 1
    Yes im using that package to get the inputrichtext in the screenflow. However i need to transform the ContentReference in a ContentDocument for the ChatterPost
    – mgarcia
    Commented Mar 16, 2022 at 12:07
  • Does this or this help? Commented Mar 16, 2022 at 14:33

2 Answers 2

1

There's a pretty viable workaround. You could update the LWC that unofficialsf uses to remove Image from formats used by lightning-input-rich-text. This prevents users from putting images in the rich text field (and removes image icon from toolbar). Or, to be more dynamic, you can update formats to be an input variable that you can specify per usage in flow which formats to support

Then, you'd add a separate File Upload standard component on the same screen below the rich text field. This component will allow you store the ContentDocumentIds in a text collection. As such, it'll then just be passing those Ids into the invocable apex and posting it, inline, in chatter at the end. The only con is that last point, you wouldn't know if there's specific placement needed for inline images.

If that doesn't meet the need, below contains a longer version on getting it working through your invocable apex - but, requires greater complexity and callout(s).


There's some good, existing questions on rtaImage and ContentReference - the link you're getting from the inline image is not really something that customers are supposed to leverage (servlet/rtaImage). The supported avenue is this REST API endpoint to retrieve sObject Rich Text Image. However, as your URL in the question shows, all you have is the refId with no sObject.

Editing the unofficialsf's lwc usage of lightning-input-rich-text to include share-with-entity-id didn't seem to work. Uploading images just stopped working even with valid Ids. The hope was that by sharing it with a record, it would then be in a format you could use (ContentDocumentLink).

As such, you're probably left with tackling this in your invocable apex. Get Images from Rich Text Area via Apex shows you can do this via PageReference so you can create a ContentVersion/ContentDocumentId you can leverage in chatter post.

You'll need to update your parsing of the URL to keep https. You'll also need to add the URL to your Remote Site Settings to make the callout (https://yourdomain.file.force.com)

String imageDetector = 'image: ';

Now, you can pass the full URL you get into a method to grab the ContentDocumentId you'll be creating

public static String createContentDocumentId(String url){
    // note, as of API 34.0 or later, getContent() is considered an http callout
    // so take that into consideration for your unit tests and governor limits
    // https://developer.salesforce.com/docs/atlas.en-us.pages.meta/pages/apex_System_PageReference_getContent.htm
    PageReference page = new PageReference( url );
    Blob b = page.getContent();
        
    //create contentversion from inline image
    ContentVersion cv = new ContentVersion(
        Title='hello',
        PathOnClient='hello.jpg',
        VersionData = b,
        origin='H'
    );

    insert cv;
    
    //query for ContentDocumentId
    ContentVersion contentVersionWithDocumentId = [SELECT ContentDocumentId FROM ContentVersion WHERE Id =: cv.Id];
    return contentVersionWithDocumentId.ContentDocumentId;
}

Now, in your existing code you'll want to add the ContentDocumentId and can remove any parsing you did to get the Id from the servlet URL.

for (String imageRef : imagesDetected) {
    if(imageRef.length() > 0) {
        String contentDocumentId = createContentDocumentId(imageRef);
        body = body.replace(imageDetector + imageRef, ' {img:' + contentDocumentId + ':The image is not load correctly.} ');
    }                    
}

You can see below, using your code and the modifications above, that the inline image worked

enter image description here

Notes & Considerations

  • It's a code comment - but, getContent is treated as a callout since API 34.0. As such, the code above is just a proof-of-concept and you'll want to consider that information with the fact that you'll loop through all inline images (performance, limits, etc) and how that fits in your overall flow (if there's more steps). Also, you will have to consider this when writing a test class.
  • Looking at the workaround mentioned at the top of the answer, you'll have to consider the tradeoffs for inline image placement and limits using the callout approach.
  • I noticed you had code comments about hyperlink issues - might be worth another question - however, I do have a PR on ConnectApiHelper related to hyperlink support. I'm assuming some more work will need to be done to convert how the lightning-input-rich-text stores hyperlinks and what ConnectApiHelper expects.
1
  • Wow, that is a really clever idea, may be i can try to cut the images number limit and inform my users through the lwc, but it could fix incredible. I'll try It. Thank you so much!
    – mgarcia
    Commented Mar 16, 2022 at 14:43
0

Late to the party but there is an unapproved pull request on the ConnectApiHelper that solves this problem.

It's here

To Call:

String body = 'Hello all, have you seen this nicely formatted record? <a 
href="https://xxx--sal320v2.sandbox.file.force.com/servlet/rtaImage?refid=0EM5t000000CkHx">Your image title</a>';
ConnectApiHelper.postFeedItemWithMentions(communityId, targetNameOrId, body);

For completeness, major changes are:

line 60:

'a href=' => ConnectApi.MarkupType.Hyperlink

line 157:

Pattern globalPattern = Pattern.compile('(\\{[a-zA-Z0-9]{15}\\}|\\{[a-zA-Z0-9]{18}\\})|(<[aA]\\s+[hH][rR][eE][fF]=")(.*?)">(.*?)</[aA]>|(<[a-zA-Z]*>)|(</[a-zA-Z]*>)|(\\{img:(069[a-zA-Z0-9]{12,15})(:[\\s\\S]*?)?\\})|(\\{record:([a-zA-Z0-9]){15,18}(:[\\s\\S]*?)?\\})');

line 197:

    if (isBeginTag) {
  // Strip off the < and >.
  String tag = matchingText.substring(1, matchingText.indexOf('>'));
  if (supportedMarkup.containsKey(tag.toLowerCase())) {
    // Add a segment for any accumulated text (which includes unsupported HTML tags).
    addTextSegment(messageSegmentInputs, textSegment);

    ConnectApi.MarkupBeginSegmentInput markupBeginSegmentInput = makeMarkupBeginSegmentInput(tag, null, null);
    messageSegmentInputs.add(markupBeginSegmentInput);
    strPos = globalMatcher.end();
  } else if (tag.toLowerCase().contains('href=')) {
    // Add a segment for any accumulated text (which includes unsupported HTML tags).
    addTextSegment(messageSegmentInputs, textSegment);
    //hyperlink tag - get the URL. Assumes format of <a href="url">altText</a>
    String url = matchingText.substring(matchingText.indexOf('"') + 1, matchingText.indexOf('>') - 1);
    String htmlTag = matchingText.substring(1, matchingText.indexOf('"'));
    String altText = matchingText.substring(matchingText.indexOf('>') + 1, matchingText.indexOf('</'));
    ConnectApi.MarkupBeginSegmentInput markupBeginSegmentInput = makeMarkupBeginSegmentInput(htmlTag, url, altText);
    ConnectApi.MarkupEndSegmentInput markupEndSegmentInput = makeMarkupEndSegmentInput(htmlTag);
    messageSegmentInputs.add(markupBeginSegmentInput);
    addTextSegment(messageSegmentInputs, altText);
    messageSegmentInputs.add(markupEndSegmentInput);
    strPos = globalMatcher.end();
  }
}

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .