Monacaのサンプルアプリである「フォトシェアアプリデザインテンプレート」にバックエンド機能を実装する連載の第5回目。今回は写真のいいねとコメント機能を作ります。

その1:認証編
その2:プロフィール編集
その3:写真アップロード
その4:写真検索

データの保存方法について

いいねとコメントは基本的に写真をアップロードしたユーザ以外が行うアクションになります。写真データを扱うPhotoクラスはアップロードしたユーザのみに編集、書き込み権限を付けていますので、いいねやコメントのデータを追加することができません。そこでLikeクラスを新しく作り、そこにデータを記録します。いいねとコメントはデータの形式上は大きな違いはありませんので、一つのクラスで扱うことにします。

ハートを押した時の処理

ハートボタンを押した時にはlike関数が実行され、ハートに色がつくようになっています。この処理の後、Likeクラスにデータを登録する処理を実行します。変数numには post-xxxxx といった具合に、「post- + PhotoクラスのobjectId」形式の文字列が入ってきます。そこで頭のpost-を消して likeCreateOrUpdate() にobjectIdを渡します。

const like = (num) => {
  if ($("#button-post-like-"+num).hasClass("like")) {
    // Like済み
    $("#button-post-like-"+num)
      .removeClass('ion-ios-heart like')
      .addClass('ion-ios-heart-outline');
  } else {
    // まだLikeしていない
    $("#button-post-like-"+num)
      .removeClass('ion-ios-heart-outline')
      .addClass('ion-ios-heart like');
    // 写真の上のハート表示/600ms後に消す処理
    $("#post-like-"+num).css("opacity", 1);
    setTimeout(function(){
      $("#post-like-"+num).css('opacity', 0);
    }, 600);
  }
  // Likeを追加する処理
  likeCreateOrUpdate(num.replace(/^post\-/, ''));
}

likeCreateOrUpdate()の処理は次のようになります。まずLikeクラスを検索して、既存のデータがあるか調べます。なければ新規でデータを作ります。既存データの有無は、返却値likeの中にキーが存在するか否かで判定できます(fetchメソッドは、既存データが存在しない場合に空のオブジェクトを返すためです)。

そしてコメントの入力有無を元に、いいね処理か否かを判断します。

const likeCreateOrUpdate = (photoObjectId, comment) => {
  // Likeクラスを検索して既存データがあるか調べる
  const Like = ncmb.DataStore('Like');
  Like
    .equalTo('photoObjectId', photoObjectId)
    .fetch()
    .then((like) => {
      // 既存データがなければ新しいインスタンスを作成
      if (Object.keys(like).length == 0) {
        // データなし
        like = new Like;
        like
          .set('users', [])
          .set('messages', [])
          .set('photoObjectId', photoObjectId);
      }
      // コメントが指定されている場合
      if (comment) {
        let messages = like.messages;
        const message = {
          profileImage: current_user.profileImage,
          username: current_user.userName,
          comment: comment
        };
        messages.push(message);
        like.set('messages', messages);
      }else{
        // いいね処理の場合
        if (like.users.indexOf(current_user.objectId) > -1) {
          // データあり
          like.remove('users', current_user.objectId);
        } else {
          if (like.objectId) {
            // 既存データに対する処理
            like.addUnique('users', current_user.objectId);
          } else {
            // 新規データに対する処理
            like.set('users', [current_user.objectId]);
          }
        }
      }
      // データを保存
      return like.objectId ? like.update() : like.save();
    })
    .then((like) => {
    })
}

いいね処理の場合、処理が若干複雑になっています。すでにいいねされている場合にはいいねを取り消す処理、いいねされていなかった場合にはデータの新規作成(save)または更新(update)を行います。データストアには配列のデータをユニークにするためのメソッド addUnique がありますが、これは既存データがある場合だけしか使えません。そのため、既存データがない場合には配列を新規作成する必要があります。

コメント処理

コメントはpromptを使ってメッセージを受け取ったら、後はいいねと同じく likeCreateOrUpdate() を呼び出し、最後にコメントをテンプレートに描画します。

const comment = (num) => {
  ons.notification.prompt({
    message: 'Comment?'
  })
  .then((text) => {
    if (text) {
      likeCreateOrUpdate(num.replace(/^post\-/, ''), text);
      const messageTemplate = $('#comment').html();
      const message = Mustache.render(messageTemplate, {
        messages: [{
          profileImage: current_user.profileImage,
          username: current_user.userName,
          comment: text
        }]
      });
      $(`#${num}`).find('.post-comments').append(ons.createElement(message));
    }
  })
}

この描画処理は写真を一覧表示する際にも使います。

const appendPhoto = (dom, photo, like) => {
  // 省略
  const messageTemplate = $('#comment').html();
  const messages = Mustache.render(messageTemplate, {
    messages: like ? like.messages : []
  });
  const content = Mustache.render(template, {
    id: id,
    photo: photo,
    favorite_message: favorite_message,
    messages: messages
  });
}

テンプレートは次のようになっています。

<template id="comment">
  {{#messages}}
  <ons-list>
    <ons-list-item modifier="nodivider">
      <div class="left">
        <img class="list-item__thumbnail" src="{{profileImage}}">
      </div>
      <div class="center">
        <span class="list-item__title">{{username}}</span>
        <span class="list-item__subtitle">{{comment}}</span>
      </div>
    </ons-list-item>
  </ons-list>
  {{/messages}}
</template>

コメントの一覧を表示する場所を写真表示テンプレートにも追加しておきます。

<div class="post-comments">
  {{{messages}}}
</div>

既存のいいねデータの取得処理

写真を一覧表示する際に一つずついいねデータを取得していると非効率的です。そこで写真検索の際にいいねデータも併せて取得します。この時には一つのフィールドに配列内のいずれかが含まれているかどうかという条件で検索する、inメソッドを使います。

const updateTimeline = (photos) => {
  const Like = ncmb.DataStore('Like');
  Like
    .in('photoObjectId', Object.keys(photos))
    .fetchAll()
    .then((likes) => {
      for (let i in photos) {
        const photo = photos[i];
        const like = likes.filter((like) => photo.objectId === like.photoObjectId)[0];
        appendPhoto($('.posts'), photo, like);
      }
    });
};

この処理は一件分の写真データを取得する処理でも同様です。ただし一件の場合は配列ではないのでequalToを使ってデータを絞り込めます。

const showSinglePage = (dom, photo) => { 
  const Like = ncmb.DataStore('Like');
  Like
    .equalTo('photoObjectId', photo.objectId)
    .fetch()
    .then((like) => {
      appendPhoto(dom.find('.post'), photo, like);
    });
  dom.find('.photo-title').text(photo.message);
}

ここまでの処理で写真に対していいねしたり、コメントしたりできるようになりました。mBaaSでは権限の設定方法に応じて、クラス設計を工夫する必要があります。クラスを分けすぎるとデータ量が増えて通信の負荷が増えてしまうので注意してください。また、何度も問い合わせると、その分APIのコール数を消費してしまうので、その点についても注意が必要です。

ここまでのソースコードはNCMBMania/photoshare at v0.6にアップロードしてあります。実装時の参考にしてください。