tyoshikawa1106のブログ

- Force.com Developer Blog -

SFDC:LEXのメッセージ機能が利用できない問題で対応したこと

Salesforce ClassicからLightning Experienceに移行する際にひとつ問題がありました。Chatterのメッセージ機能が利用できない問題です。
f:id:tyoshikawa1106:20180414175245p:plain


「メッセージ機能ですか?Salesforceではサポートを終了しました。そういうものなんです。」で押し通そうと思っていたのですが、GitHubに公開されている+Messageを使わせてもらうことで解決しました。



Lightning Experienceにはユーティリティバーというどのページからもアクセスできる機能が利用できます。これをつかってメッセージ機能にアクセスできるようにしました。

f:id:tyoshikawa1106:20180414175548p:plain

f:id:tyoshikawa1106:20180414175601p:plain


もともとはLightning Expcerienceが公開されるよりも前、Salesforce1モバイルアプリでの利用を想定されているのでLEXでの利用は想定されていません。ただ、非公開パッケージも公開されているので開発環境を用意することは簡単にできる状態でした。


SalesforceはLightning Design SystemというCSSフレームワークを公開してくれています。これを利用すればLEX的な見た目に調整することができそうでした。

Lightning Design System


実際にやってみたのがこちら。
f:id:tyoshikawa1106:20180414180144p:plain

f:id:tyoshikawa1106:20180414180201p:plain


少し強引にやってごまかしたところがありますが (Clickリンクのところなど) ひとまずうまくいきました。LEXでのメッセージ機能はこれで運用してみようと思います。

変更した箇所

変更したのはHTML部分とJSの一部処理だけです。

PlusMessageView.page
<apex:page docType="html-5.0" applyHtmlTag="false" showHeader="false" sidebar="false" standardStylesheets="false" controller="PlusMessageCtrl">
<html lang="ja" data-framework="angularjs" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
    <title>Chatter Message</title>
    <apex:stylesheet value="{!URLFOR($Resource.PlusMessageResource,'css/bootstrap.min.css')}" />
    <apex:slds />
  </head>
  <body ng-app="msgapp" ng-init="userId='{!$User.Id}'; languageLocaleKey='{!languageLocaleKey}'" class="slds-scope">
    <ng-view />
    <!-- conversations.html -->
    <script type="text/ng-template" id="conversations.html">
      <section id="msgapp">
        <div class="slds-text-align--right">
          <button onclick="location.href='#/send/'" class="slds-button slds-button_icon slds-button_icon-border-filled" aria-pressed="false">
            <svg class="slds-button__icon" aria-hidden="true">
              <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Asset.SLDS, '/assets/icons/utility-sprite/svg/symbols.svg#new_direct_message')}" />
            </svg>
            <span class="slds-assistive-text">New</span>
          </button>
        </div>
        <div>
          <div ng-show="err!=null" class="slds-m-vertical_small slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_error" role="alert">
            <h2>{{err.message}}</h2>
          </div>
          <div ng-show="loading">
            <div style="height: 6rem;">
              <div role="status" class="slds-spinner slds-spinner_medium">
                <span class="slds-assistive-text">Loading</span>
                <div class="slds-spinner__dot-a"></div>
                <div class="slds-spinner__dot-b"></div>
              </div>
            </div>
          </div>
          <div class="slds-feed">
            <ul class="slds-feed__list">
              <li class="slds-feed__item" ng-repeat="conv in convs.conversations">
                <article class="slds-post">
                  <header class="slds-post__header slds-media">
                    <div class="slds-media__figure">
                      <a href="#/{{conv.id}}" class="slds-avatar slds-avatar_circle slds-avatar_medium">
                        <img src="{{conv.latestMessage.sender.photo.smallPhotoUrl}}" />
                      </a>
                    </div>
                    <div class="slds-media__body">
                      <div class="slds-grid slds-grid_align-spread slds-has-flexi-truncate">
                        <p><a href="#/{{conv.id}}">{{conv.latestMessage.sender.name}}</a></p>
                      </div>
                      <p class="slds-text-body_small"><a href="#/{{conv.id}}" class="slds-text-link_reset">{{conv.latestMessage.sentDate}}</a></p>
                    </div>
                  </header>
                  <div class="slds-post__content slds-text-longform">
                    <p><span ng-bind="conv.latestMessage.body.text" style="white-space: pre-wrap;"/></p>
                  </div>
                  <footer class="slds-post__footer">
                    <div class="slds-text-align--right"><a href="#/{{conv.id}}">Click!</a></div>
                  </footer>
                </article>
              </li>
            </ul>
          </div>
        </div>
      </section>
    </script><!-- conversations.html -->

    <!-- send-message.html -->
    <script type="text/ng-template" id="send-message.html">
      <section id="msgapp">
        <div class="slds-clearfix">
          <div class="slds-clearfix">
            <div class="slds-float_left">
              <button onclick="location.href='#/'" class="slds-button slds-button_icon slds-button_icon-border-filled" aria-pressed="false" title="Like">
                <svg class="slds-button__icon" aria-hidden="true">
                  <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Asset.SLDS, '/assets/icons/utility-sprite/svg/symbols.svg#back')}" />
                </svg>
                <span class="slds-assistive-text">Back</span>
              </button>
            </div>
          </div>
        </div>
        <div >
          <div ng-show="err!=null" class="slds-m-vertical_small slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_error" role="alert">
            <h2>{{err.message}}</h2>
          </div>
          <div class="slds-m-top--small">
            <textarea name="message" ng-model="message" class="slds-textarea" placeholder="{{ 'MESSAGE' | translate }}"></textarea>
            <div class="input-group-btn"><button type="button" class="slds-button slds-button_brand" ng-click="sendMessage()" translate="SEND">Send</button></div>
          </div>
          <div ng-show="loading">
            <div style="height: 6rem;">
              <div role="status" class="slds-spinner slds-spinner_medium">
                <span class="slds-assistive-text">Loading</span>
                <div class="slds-spinner__dot-a"></div>
                <div class="slds-spinner__dot-b"></div>
              </div>
            </div>
          </div>
          <div style="padding-top: 14px">
            <ul class="list-group">
              <li class="list-group-item list-group-item-info">
                <span translate="RECIPIENTS">Recipients</span>
                <span style="padding-left: 14px;">
                  <button ng-click="openSearchUsers()" ng-show="members.length<9" class="slds-button slds-button_icon slds-button_icon-border-filled" aria-pressed="false">
                    <svg class="slds-button__icon" aria-hidden="true">
                      <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Asset.SLDS, '/assets/icons/utility-sprite/svg/symbols.svg#adduser')}" />
                    </svg>
                    <span class="slds-assistive-text">Add</span>
                  </button>
                </span>
            </li>
              <li class="list-group-item" ng-show="members.length==0" translate="NO_RECIPIENTS_MESSAGE">Add Recipients</li>
              <li class="list-group-item" ng-repeat="member in members">
                <div class="slds-size_3-of-4">
                  <div class="slds-media">
                    <div class="slds-media__figure">
                      <span class="slds-avatar slds-avatar_large">
                        <img src="{{member.photo.smallPhotoUrl}}" />
                      </span>
                    </div>
                    <div class="slds-media__body">
                      <div class="name">{{member.name}}</div>
                      <div class="title">{{member.title}}</div>
                    </div>
                  </div>
                </div>
              </li>
            </ul>
          </div>
        </div>
      </section>
    </script><!-- send-message.html -->

    <!-- search-users-dialog.html -->
    <script type="text/ng-template" id="search-user-dialog.html">
      <div class="modal-header">
        <div class="input-group">
          <span class="input-group-addon">@</span>
          <input type="text" name="query" ng-model="searchUsers.query" class="form-control" placeholder="{{ 'RECIPIENTS' | translate }}" x-webkit-speech lang="ja"/>
        </div>
      </div>
      <div class="modal-body">
        <div class="alert alert-danger" ng-show="errDialog!=null">{{errDialog.message}}</div>
        <div ng-show="loadingDialog">
          <div style="height: 6rem;">
            <div role="status" class="slds-spinner slds-spinner_medium">
              <span class="slds-assistive-text">Loading</span>
              <div class="slds-spinner__dot-a"></div>
              <div class="slds-spinner__dot-b"></div>
            </div>
          </div>
        </div>
        <form role="form">
          <div class="list-group" ng-hide="loadingDialog">
            <div class="list-group-item" ng-show="users.length==0" translate="NO_MATCH_USER_MESSAGE">No match user</div>
            <a class="list-group-item" ng-repeat="user in users" ng-click="addUser(user)">
              <div class="slds-size_3-of-4">
                <div class="slds-media">
                  <div class="slds-media__figure">
                    <span class="slds-avatar slds-avatar_large">
                      <img src="{{user.photo.smallPhotoUrl}}" />
                    </span>
                  </div>
                  <div class="slds-media__body">
                    <div class="name">{{user.name}}</div>
                    <div class="title">{{user.title}}</div>
                  </div>
                </div>
              </div>
            </a>
          </div>
        </form>
      </div>
      <div class="modal-footer">
        <button type="button" class="slds-button slds-button_neutral" ng-click="$close()" translate="CLOSE">Close</button>
      </div>
    </script><!-- search-users-dialog.html -->

    <!-- messages.html -->
    <script type="text/ng-template" id="messages.html">
      <section id="msgapp">
        <div class="slds-clearfix">
          <div class="slds-clearfix">
            <div class="slds-float_left">
              <button onclick="location.href='#/'" class="slds-button slds-button_icon slds-button_icon-border-filled" aria-pressed="false" title="Like">
                <svg class="slds-button__icon" aria-hidden="true">
                  <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Asset.SLDS, '/assets/icons/utility-sprite/svg/symbols.svg#back')}" />
                </svg>
                <span class="slds-assistive-text">Back</span>
              </button>
            </div>
            <div class="slds-float_right">
              <button ng-click="openUsersDialog()" class="slds-button slds-button_icon slds-button_icon-border-filled" aria-pressed="false" title="Like">
                <svg class="slds-button__icon" aria-hidden="true">
                  <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="{!URLFOR($Asset.SLDS, '/assets/icons/utility-sprite/svg/symbols.svg#user')}" />
                </svg>
                <span class="slds-assistive-text">Chat Member</span>
              </button>
            </div>
          </div>
        </div>
        <div>
          <div ng-show="err!=null" class="slds-m-vertical_small slds-notify slds-notify_alert slds-theme_alert-texture slds-theme_error" role="alert">
            <h2>{{err.message}}</h2>
          </div>
          <div class="slds-m-top--small">
            <textarea name="message" ng-model="message" class="slds-textarea" placeholder="{{ 'MESSAGE' | translate }}" rows="3"></textarea>
            <div class="input-group-btn"><button type="button" class="slds-button slds-button_brand" ng-click="replyToMessage()" translate="SEND">Send</button></div>
          </div>
          <div ng-show="loading">
            <div style="height: 6rem;">
              <div role="status" class="slds-spinner slds-spinner_medium">
                <span class="slds-assistive-text">Loading</span>
                <div class="slds-spinner__dot-a"></div>
                <div class="slds-spinner__dot-b"></div>
              </div>
            </div>
          </div>
          <div class="slds-feed">
            <ul class="slds-feed__list" ng-hide="loading">
              <li class="slds-feed__item" ng-repeat="msg in msgs.messages.messages">
                <article class="slds-post">
                  <header class="slds-post__header slds-media">
                    <div class="slds-media__figure">
                      <a class="slds-avatar slds-avatar_circle slds-avatar_medium">
                        <img src="{{msg.sender.photo.smallPhotoUrl}}" />
                      </a>
                    </div>
                    <div class="slds-media__body">
                      <div class="slds-grid slds-grid_align-spread slds-has-flexi-truncate">
                        <p><a>{{msg.sender.name}}</a></p>
                      </div>
                      <p class="slds-text-body_small"><a class="slds-text-link_reset">{{msg.sentDate}}</a></p>
                    </div>
                  </header>
                  <div class="slds-post__content slds-text-longform">
                    <p><span ng-bind="msg.body.text" style="white-space: pre-wrap;"/></p>
                  </div>
                </article>
              </li>
            </ul>
          </div>
        </div>
      </section>
    </script><!-- messages.html -->

    <!-- users-dialog.html -->
    <script type="text/ng-template" id="users-dialog.html">
      <div class="modal-header" translate="MEMBER">
        Member
      </div>
      <div class="modal-body">
        <ul class="list-group">
          <li class="list-group-item" ng-repeat="member in msgs.members">
            <div class="slds-size_3-of-4">
              <div class="slds-media">
                <div class="slds-media__figure">
                  <span class="slds-avatar slds-avatar_large">
                    <img src="{{member.photo.smallPhotoUrl}}" />
                  </span>
                </div>
                <div class="slds-media__body">
                  <div class="name">{{member.name}}</div>
                  <div class="title">{{member.title}}</div>
                </div>
              </div>
            </div>
          </li>
        </ul>
      </div>
      <div class="modal-footer">
        <button type="button" class="slds-button slds-button_neutral" ng-click="$close()" translate="CLOSE">Close</button>
      </div>
    </script><!-- susers-dialog.html -->


    <!-- waiting-dialog.html -->
    <script type="text/ng-template" id="waiting-dialog.html">
      <div class="modal-header" translate="SENDING_MESSAGE">
        Sending...
      </div>
      <div class="modal-body">
        <div style="height: 6rem;">
          <div role="status" class="slds-spinner slds-spinner_medium">
            <span class="slds-assistive-text">Loading</span>
            <div class="slds-spinner__dot-a"></div>
            <div class="slds-spinner__dot-b"></div>
          </div>
        </div>
      </div>
    </script><!-- waiting-dialog.html -->

    <apex:includeScript value="{!URLFOR($Resource.PlusMessageResource, 'js/angular.min.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageResource, 'js/angular-route.min.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageResource, 'js/angular-translate.min.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageResource, 'js/ui-bootstrap-tpls-0.10.0.min.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageResource, 'js/jquery-2.1.0.min.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageResource, 'js/bootstrap.min.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageJS, 'app.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageJS, 'controllers/messageCtrl.js')}" />
    <apex:includeScript value="{!URLFOR($Resource.PlusMessageJS, 'services/messageService.js')}" />
  </body>
</html>
</apex:page>


JSの方は日本時間に調整したいところがあったので他で実装されていた処理をコピペする形でちょっと手を入れました。

messageService.js

f:id:tyoshikawa1106:20180414180627p:plain


ということでGitHubに公開されている+MessageのおかげでLightning Experienceにメッセージ機能を表示することができました。メッセージ通知の機能とかの要望がくるかもしれませんが、おそらく「メールで気づいてください」で押し通せると思います。いつか標準でサポートされればいいなと思います。(Skype for Salesforceが用意されていましたが試してみたところメッセージ機能とはすこし用途がことなりました。)

管理パッケージ対応

上記で用意したカスタマイズバージョンの+メッセージですが、組織にインストールするときは管理パッケージとしてインストールします。未管理パッケージでも同じ用にインストール可能ですが、管理パッケージにすることで開発時に組織コードに混ざって表示されないようになります。組織に合わせてバンバンカスタマイズしてく場合は未管理パッケージで気軽に開発できるようにした方がいいと思うのですが、通常さわらないのであれば管理パッケージの方が良いと思います。 ※書いた後にしったのですが、管理パッケージはパートナー組織じゃないとバージョンアップできない落とし穴がありました。

管理パッケージ化するときの注意

管理パッケージにすると組織に名前空間プレフィックスが追加されます。JSからApexクラスにアクセスする処理は下記の規則で修正が必要になります。

<名前空間プレフィックス>.SampleController.geSampleMethod()


先頭に名前空間プレフィックスをつけるだけなので規則がわかればそれほど大変ではないと思います。

追記

Salesforceモバイルアプリでも利用可能ですが、タブの作成が必要になります。またモバイルアプリで表示したときに気づいたのですが、paddingを入れとけばよかったです。