Siriとfirebaseを連携させて、アプリを操作してみる

こんにちは。はじめまして。
クライアントワーク事業部でフロントエンドエンジニアをしている
マチダヨウスケと申します。

www.kayac.com

この記事は、Tech KAYAC Advent Calendar 2020 の21日目の記事になります。

本記事の内容について

TouchDesignerの公式ツイッターでこのような動画がリツイートされていました。

この動画では、どうやらHomebridgeというものを使っているようです。
Homebridgeは、HomeKitに対応していないIoT機器でも制御出来るようにするソフトウェアです。

homebridge.io

今回は、このHomebridgeを使わないで、
Siriとfirebase(realtime database) を使って他のアプリ(TouchDesigner)を操作するシステムを作成してみます。

結果

この動画では、hey siriという音声が流れるので、もしかしたら手持ちのiPhoneが反応する可能性があります。
イヤホンをつけて視聴することをおすすめします。また滑舌が非常に悪いのでご注意ください。

vimeo.com

今回作ったファイルはこちらから閲覧できます。 github.com

システムの概要

システムは単純です。
firebaseのrealtime databaseで何らかの値を保持し、
firebase functionsで値を変更、更新するAPIを作成、
Siriに命令して、そのAPIを叩かせます。

他のアプリ側はdatabaseの更新を監視し、
変更に合わせて動かせばいいだけです。

今回はTouchDesignerで勧めていきますが、
realtime databaseが扱える環境(ブラウザとかvvvvとか、たぶんアンリアルエンジンもできる)
ならなんでも自由にSiriから操作する事ができます。
(UnityはDesktop向けのfirebaseがないので、OSCとかNodeとかで連携される形になると思います)
techblog.kayac.com

firebaseの設定

firebaseの初期設定やデプロイ方法は、たくさんの人が記事をあげてるので割愛します。
今回、firebase functionsを使いますが、
firebase functionsは無料プランで使うことはできなくなったので
従量課金プランに変更します。

速度や手軽さを重視したいので、realtime databaseを使用します。

TouchDesignerのPythonを使ってfirebaseと接続するので、
サービスアカウントから秘密鍵の作成をします。
f:id:machida-yosuke:20201219203242p:plain

Siriの設定

Siriとfirebaseと連携させます。
Apple公式からSiriKitというiphoneアプリとsiriを連携できるものがあり、
このkitを使って専用のアプリを作成する、といったことを最初に考えると思いますが、

SiriKitからできるのは
・メッセージの送信
・メモの作成
・お金の送金
などなど

Siriに対してなんでも命令させて、何でもやってもらえるというわけではないようです。
(検証期間はあまりとれなかったので、もしかしたらできるかもしれないです)

なので、今回はショートカットという機能を使って、アクションを作成します。

アクションからfirebaseのdatabaseを更新するAPIをたたき、
尚且、Siriに任意の言葉を喋らせるアクションを作成します。

下記添付の画像では、Siriに対して「データを初期化して」と命令したときに、

・firebase functionsで初期化するAPIを叩く
・「データを初期化しました」とSiriに言わせる
アクションです。

f:id:machida-yosuke:20201219205500p:plain

あとは、アクションを何個か作るだけです。

TouchDesignerの設定

TouchDesignerの操作は、たくさんの人が記事をあげてるので割愛します。

techblog.kayac.com

TouchDesignerでは、pythonのコードを実行することができます。
また、pipでpythonのライブラリをインストールし、インポートして実行することができます。
pythonとfirebaseを連携するライブラリはpyrebaseを使用します。

まずはTouchDesigner側でpipでインストールしたライブラリへのパスを教えてあげます。

import sys
mypath= '/Users/admins/.pyenv/versions/3.7.2/lib/python3.7/site-packages'
if mypath not in sys.path:
    sys.path.append(mypath)
print(sys.path)

つぎにfirebaseとの接続、databaseの値を取得するクラスを作成します

"""
Extension classes enhance TouchDesigner components with python. An
extension is accessed via ext.ExtensionClassName from any operator
within the extended component. If the extension is promoted via its
Promote Extension parameter, all its attributes with capitalized names
can be accessed externally, e.g. op('yourComp').PromotedFunction().

Help: search "Extensions" in wiki
"""

from TDStoreTools import StorageManager
TDF = op.TDModules.mod.TDFunctions

import pyrebase

class firebase:
    """
  firebase description
  """
    def __init__(self, ownerComp):
        # The component to which this extension is attached
        
        self.ownerComp = ownerComp
        
        self.InitData = {}
        self.UpdateData = {}
        self.Firebase = {}
        self.DB = {}

        # properties
        TDF.createProperty(self, 'MyProperty', value=0, dependable=True,
                           readOnly=False)

        # attributes:
        self.a = 0 # attribute
        self.B = 1 # promoted attribute

        # stored items (persistent across saves and re-initialization):
        storedItems = [
            # Only 'name' is required...
            {'name': 'StoredProperty', 'default': None, 'readOnly': False,
                                    'property': True, 'dependable': True},
        ]
        # Uncomment the line below to store StoredProperty. To clear stored
        #  items, use the Storage section of the Component Editor
        
        # self.stored = StorageManager(self, ownerComp, storedItems)

    def myFunction(self, v):
        debug(v)

    def PromotedFunction(self, v):
        debug(v)
    
    def FirebaseSetup(self):
        config = {
            "apiKey": "AIzaSyDB9lRYBhq9K_IvRAkCnDqFQvSU3dlFLyM",
            "authDomain": "siri-to-app.firebaseapp.com",
            "databaseURL": "https://siri-to-app-default-rtdb.firebaseio.com",
            "storageBucket": "siri-to-app.appspot.com",
            "serviceAccount": "/Users/admins/siri-to-touchdesigner/siri-to-app-firebase-adminsdk-5hxtt-83294d32cf.json"
        }
        self.Firebase = pyrebase.initialize_app(config)
        self.DB = self.Firebase.database()
    
    def GetInitData(self):
        self.InitData = self.DB.child("status").get()

    def InitUpdateData(self):
        self.UpdateData = {}
        
    def StartStream(self):
        def StartDBStream(message):
            print(message["event"])
            print(message["path"])
            print(message["data"])
            self.UpdateData = message["data"]
        self.DB.child("status").stream(StartDBStream)

self.DB.child("status").stream(StartDBStream)
databeseの更新を監視し、更新があった場合、変数に入れます。

databaseの監視は別スレッドで行われ、
別スレッドで行われる処理の中で、TouchDesignerのオペレーターにアクセスすることはできないので、
更新内容を変数にいれ、
timerオペレーターを使って、2秒に1回の間隔で
メンバ変数に値が入ってるか監視します。(2秒に1回の間隔でonDoneを実行)
ここの間隔は使用するPCのスペックに応じて変更します。

def onDone(timerOp, segment, interrupt):
    print('onDone')
    # print(firebase.InitData, 'firebase.InitData')
    # print(firebase.UpdateData, 'firebase.UpdateData')

    if firebase.UpdateData == {}:
        print('none')
        return

    for k in firebase.UpdateData.keys():
        # print(k, firebase.UpdateData[k],'keys')

        for r in range(table.numRows):

            tableName = table[r,0]
            # print(tableName)
            
            if tableName == k:
                table[r,1] = firebase.UpdateData[k]
                print('!!BREAK!!')
                break
    firebase.InitUpdateData()
    return

firebaseクラスのメンバ変数の参照とメソッドを叩くコードを書きます。

firebase = op('firebase')

firebase.FirebaseSetup()
firebase.GetInitData()

for status in firebase.InitData.each():
    key = status.key()
    val = status.val()
    op('table_data').appendRow([key, val])

firebase.InitUpdateData()
firebase.StartStream()

あとは、値の変更にたいして、どのような演出をいれるかを考えて実装するだけです。
f:id:machida-yosuke:20201219212334p:plain

あとがき

Siriとfirebaseを使うことで遠隔でアプリを操作することができました。
これを実装すれば、
「Hey Siri デバックモードに変更して」
といえば、両手がふさがってても、または遠隔でも、
アプリをデバックモードにすることができたりします。
※実用性はほぼありません

最後まで読んでいただき、ありがとうございました!!

今回作ったファイルはこちらから閲覧できます。

github.com

その他リンク

maoudamashii.jokersounds.com

konchi.kayac.jp

音楽著作権:魔王魂

slackの反応がないと寂しいので、一気にたくさんリアクションできる「slack最高速でワイワイできるくん」を作った

f:id:chanchihiro:20201220132052p:plain



この記事は、「Tech KAYAC Advent Calendar 2020」の20日目の記事になります。



slackを返して仕事した気になっているみなさんこんにちは!!!

slackに負けるな!!!





いきなりごめんなさい。どうもこんにちは。

KAYACクライアントワーク事業部デザイナーのちゃんちーです。


f:id:chanchihiro:20201220100335p:plain




クライアントワークとして、今や40万人以上が持ってるうんこミュージアムで配られるマイうんこという不思議なものを製造したり、


f:id:chanchihiro:20201220133556g:plain

マイクロワーケーションという実験で、お客さんと川で仕事をしてみたり、


f:id:chanchihiro:20201220101927p:plain

いろんなことをしながらいろんなものを作って暮らしています。
本当はもっと真面目に仕事してます。
いや、本当です。よろしくお願いします。

f:id:chanchihiro:20201220103454p:plain





いきなりですが、slack、使っていますか?

弊社はというと、バリバリslackでやりとりをしていて、社内のコミュニケーションツールはslackにほぼ集約されております。

リモートワーク期間も、メンバーが会わずに完結した案件などもありましたが、slackのおかげでスムーズにやりとりができる状態を保っています。

最近ではSalesforceがslackを買収したというニュースでも話題になりました。(すごい!)


....さて、そんなとても便利で最高なslackですが、実は僕は最近あることに気づいてしまいました...。

slackが作業効率を下げてない?!?!

そんなばかなと思って今画面を閉じようとしたそこのあなた!

胸に手を当てて考え直してみて欲しいのです。

たしかにslackは便利なのですが、思いのほかなんでもできてしまうし、いかんせん拡張性や自由度、放牧度も高いです。




ということで、なぜslackで作業効率が下がってしまっている気がするのかもう少し分解して考えていきましょう。




slackで作業効率が下がっちゃう理由その① : 絵文字が楽しい


slackには絵文字文化みたいなものがあります。
例えばですが、既存の絵文字や、独自の絵文字に加えて、自分たちで作った絵文字などを会話の中で使ったり、会話に対してのリアクションとして絵文字で反応することもできます。

特にslackの絵文字で僕が好きなのは、絵文字に設定されているlabelです。大元の会社が海外だからでしょうか、日本語の設定がなんとなく独特で可愛くなってます。特に好きなのはここらへん。
f:id:chanchihiro:20201220020749p:plain f:id:chanchihiro:20201220020928p:plain

よだれがジュル。のーみそばくはつ。
よだれはもうジュルのレベルじゃないくらい出てるし、噴火している頭はのーみそだったのかとびっくりです。
ちょっと怖い。

ちなみに英語だと:tada:、日本語だと:クラッカー:として表現される絵文字がこちら、

f:id:chanchihiro:20201220021125p:plain f:id:chanchihiro:20201220021147p:plain

あまりクラッカーに対して:tada:というワードが連想しづらく、誰もが最初探すのに苦労する絵文字の一つではないでしょうか。
実はこれは、英語だとクラッカーの擬音がこのtadaという感じに聞こえるらしく、tadaという表現を用いているんだとか

他にも、弊社では自分たちで登録したカスタム絵文字が7500種類以上ああるので、誰かが追加したものを眺めたり、slack絵文字のことを考えるているだけで一日が終わってしまいます。

作業効率を悪くするのに、とても危険だ!!

slackで作業効率が下がっちゃう理由その② : 全然関係ないチャンネルが楽しい

弊社slackには、仕事のチャンネル以外に、#times-と呼ばれる個人運営のチャンネル、部活動のような#club-というチャンネルがいくつも存在します。

その中でも#club-dogは、好きな犬の画像をただ貼り合うだけのチャンネルなのですが、一生見てられてしまうので危険です。

f:id:chanchihiro:20201220104820g:plain

自分も何か貼ろうと犬の写真を探していると一時間くらい経っているということもあります。非常に危険です。

極力見ないことを心がけましょう。



このようにslackは楽しくて便利な一方で、楽しすぎて作業効率が悪くなるということがあるかもしれません。
だがしかし、どちらも楽しくてつい時間が経ってしまう系なので、気にしなければどうにかなりそうな気もします。
厄介なのは次の③です。これが今回の本題です。




slackで作業効率が下がっちゃう理由その③ : 相手の反応ないと寂しくて気になっちゃう〜〜〜〜


です。

slackでは時々、相手の反応が気になるような文章を書くことがあるのではないでしょうか。

それは、大事なデザインのレビュー依頼であったり、
ローンチ報告と呼ばれる、案件が公開されたことを社内に知らせる連絡であったり、
はたまた日常的な会話であったり、
多岐に渡ります。
たくさん反応してもらいたいものが結構無反応だと、少し寂しくなっていき、次のように集中できなくなっていきます。

f:id:chanchihiro:20201220111252g:plain

かといって、反応する方はする方で、無関心なのではなく、何個もリアクションを押すのはめんどくさかったり、わざわざスタンプを押すのがめんどくさいという場合がほとんどなのではないでしょうか。



あ〜〜〜〜もっとこれが、簡単にリアクションできるような仕組みだったらな〜〜〜〜〜!!!!


は!!!!!

リアクションを何個もするのがめんどさいのなら、一気にリアクションできる装置を作ってしまえばいいのでは?!


そう、新しくボタンを設置して、一つのボタンを押すだけでいろんなワイワイ反応が簡単に行うことができるようになればみんなもっとリアクションが活発になり、心配することもなくなっていくのではないでしょうか。



今まで一個一個押してきたボタンを一回押しただけでドバッと反応できるようになるのはとても画期的なはずです。

そうと決まれば、いざ、作っていきましょう!!!


chrome拡張を使おう!


まずは何で作るかを考えていきましょう。
いろいろスクレイピングできるとか、やれることの幅を考えるとPuppeteerというのが良さそうだ!
となったのですが、

- いろんな人が簡単に使えるようにする
- 記事公開まで時間がもうあと三日くらいしかない...

ということで、過去にchrome拡張でtwitterをslackっぽくするのを作ったり、slackのやりとりをギャル絵文字だけで会話しているように見せたりをしたことがあったので、chrome拡張を使うことに決めました。

これは妥協じゃないです。方向転換です。

f:id:chanchihiro:20201220110513p:plain

ちなみに、普段会社の仕事として、デザインから実装まですることはほぼほぼないですが、
エンジニアとのやりとりでモックだけ自分で実装してみたり、議論のたたきとして実装ベースで進めたり、
最後らへんに調整する時などに自分が実装に入って調整していくなどをするなどはたまにあるくらいの感じです。
なので多少不安はありますが、がんばっていきましょう!


実際に作ってみる

どうやって作ろう

まず、slackのどこに「slack最高速でワイワイできるくん」のボタンを設置すればいいかを考えてみます。
順当に考えると、ここらへんですね。

f:id:chanchihiro:20201220032442p:plain

ここら辺にボタンを追加して、押すとこの中のボタンがいくつか何回か押されて閉じるのが良さそうです。

chrome拡張を使うにあたって、実際に拡張したいウェブの中身を理解することがまず重要です。
なのでslackの中身についていろいろみながら進めていきます。
(この時間が一番自分は何をやっているのだろうと不安になりますので注意してください。)

デベロッパーツールでみてみると、このリアクションボタンがリスト化されている小さいモーダル部分ですが、特定のセルのリアクションボタンにhoverすることで、ReactModalPortalが準備され、押すと発火する仕組みのようです。

f:id:chanchihiro:20201220033246g:plain

しかしここで問題が発生します。
このslackのリアクションボタンですが、なんと一個押すとモーダルが閉じてReactModalPortalも消滅してしまいます。 そうなると、いくつかあるセルの中から何回も特定のリアクションボタンを取得してモーダルを立ち上げリアクションボタンを複数回押すこと自体が難しく、 やるとしたら結構めんどくさそうでした。


なのでここも妥協....ではなく方向転換していきます。

常に表示されてるものをフックにする〜〜〜

数分前の反省から、まずhoverしなければ表示されないボタンに頼るのはやめました。
そこで見つけたのが、このボタンです。


f:id:chanchihiro:20201220114159p:plain


このボタンであれば、常に表示されており、繰り返し特定のものを何度も押してあげることができそうです。


しかしここで問題が再度発生します。


それは、一つでもリアクションがついていない場合、このボタンは出現しないということです。
ここは考え方を妥協して....ではなく方向転換して、ファーストオピニオンではなく、ファーストオピニオンに続く大事な二人目以降の場合にのみ有効なものにしていくことにします。

これも、戦略的撤退いわば方向転換です。

ということでここらへんに「slack最高速でワイワイできるくん」のボタンを設置していくことにします。

f:id:chanchihiro:20201220035420p:plain


chrome拡張の準備する!


ディレクトリを作成し、特定のファイルを置いた後、chromeでURLにchrome://extensionsとうって拡張のページを開き、「パッケージ化されていない拡張機能を読み込む」から簡単に自作のものを試していくことができます。
細かいのは去年書いているので、それをみた方が早いのでオススメです。

techblog.kayac.com

今回はディレクトリ構成はこんな感じです。

    .
    └── src
         ├── icon.png
         ├── images
             └──  *
         ├── style
             └──  style.css
         ├── js
             ├──  main.js
             ├──  load.js
             └──  background.js
         ├── package.json
         └── manifest.json


いつもと違くて大変そうなのは、あとでも出てきますが、SPAのページ更新をみるために、background.jsでURLを少し特殊な感じで監視することです。



テンションが上がるように、まずはボタンの画像を作るぞ!


ここら辺で体力が切れてくるので画像を作ります。
今回ワイワイするために人の顔をたくさん出したいので、アシュラみたいな感じの絵文字にしましょう。

f:id:chanchihiro:20201220124846g:plain

こんな感じでしょうか。好きな三つを並べてみました。いい感じですね。

(chrome拡張で画像を使ったりcssを追加しアクセスするには、いちいちmanifest.jsonのweb_accessible_resourcesに追加していく必要があるので気をつけましょう)



ボタンを配置しよう!


用意したボタンを配置します。
slackのリアクションモーダルを出すボタンは、.c-reaction_bar というクラスがついていますので、それで指定してあげます。

   const imagePath = 'images/button_faces.png';
    const imageUrl = chrome.extension.getURL(imagePath);

    let elem = document.querySelectorAll('.c-reaction_bar');

    let ARRAYS = document.querySelectorAll('.c-reaction_add--light');
    let countArray = ARRAYS.length - 1;
    for (let i = 0; i < countArray + 1; i++) {
        const addButton = document.createElement('img');
        addButton.classList.add('addition');
        addButton.setAttribute('src', imageUrl)
        elem[i].appendChild(addButton)
    }

そんな大変なことはしてませんが、一旦これでボタンの横に「slack最高速でワイワイできるくん」のボタンを置くことに成功しました。

f:id:chanchihiro:20201220043709p:plain

こんな感じになれば成功です。可愛いですね。

ちなみにボタンについて調べることで、slackのボタンが34x24pxでできていて、12pxのアールであることがわかりました。
このように作っているうちにいろいろデザインルール的なのが見えてくるのが面白いですね。

SPAに対応しなきゃいけないの!?


基本、chrome拡張はページがリロードされたタイミングで発火され、SPAでなければロードがかかるたびに読み込まれるのでいい感じに発火しますが、 slackは基盤がReactでできているので、他のチャンネルに移った時に正常に発火せずに、そういう場合はボタンが追加されません。

f:id:chanchihiro:20201220044308g:plain

こんな感じで、他のチャンネルに移動すると、ボタンは消えてしまいます。
これを解決するために先ほどのbackground.jsと、manifest.jsonをいい感じにしていきます。

chrome.tabs.onUpdated.addListener(function (tabId, changeInfo, tab) {
    if (changeInfo.status === "complete" && tab.url.indexOf('https://app.slack.com/client') !== -1) {
      chrome.tabs.executeScript(
        tabId,
        {
           file: 'js/load.js',
        }
      );
    }
});

background.jsは、ページのURLが変わったタイミングでload.jsを読み込むようにchrome.tabs.onUpdatedを設定してあげます。 load.jsには先ほどのボタンを設置するやつを移行して書いてあげます。 その上でmanifest.jsonでは、backgroundでもちゃんと動くように下記を足します。

  "background": {
    "persistent": false,
    "scripts": ['js/background.js']
  },
  "permissions": [
    "tabs",
    "activeTab",
    "https://app.slack.com/client/*",
    "http://app.slack.com/client/*"
  ]

これで、チャンネルを移動してもボタンがついてるようになりました。



無限スクロールにも対応するぞ!


また、ここで違う問題が発生します。
slack上では最初に表示されているDOMは画面上に表示されているものだけです。
その他過去の文章は、スクロールすることで現れます。
なのでこの新しく現れた文章に対しては、先ほどのように、ボタンが追加されていないようになってしまいます。

なので、今回はMutationObserverを使います。

   const BODY = document.querySelectorAll('.c-virtual_list__scroll_container');
    const BODY2 = BODY[1];
    const config = {
        childList: true, 
    };
    var observer = new MutationObserver( () => {
        let elem = document.querySelectorAll('.c-reaction_bar');
    
        let ARRAYS = document.querySelectorAll('.c-reaction_add--light');
        let countArray = ARRAYS.length - 1;
        for (let i = 0; i < countArray + 1; i++) {
            if(elem[i].classList.contains("is-active")) {
                console.log('何もなし')
            } else {
                elem[i].classList.add('is-active');
                const addButton = document.createElement('img');
                addButton.classList.add('addition');
                addButton.setAttribute('src', imageUrl)
                elem[i].appendChild(addButton)
            }
        }
        
    });
    observer.observe(BODY2, config);

slackの構造をみてみると、.c-virtual_list__scroll_container の子要素がDOMとして減ったり増えたりすることで要素の量を調整してるぽいです。
なのでこの.c-virtual_list__scroll_container を監視対象としてイベントを発火します。
なぜかこの.c-virtual_list__scroll_container は二つあるっぽく、[1]を指定することでうまくいったのでそうしています。
要素の変更を感知するたびに数を数えなおしてずれないようにボタンを追加しなおしています。(もっといい感じのやり方ありそうですが...)

これで、スクロールして、後から出てきた文章に対しても、ボタンがついてるようになりました。

f:id:chanchihiro:20201220091249g:plain


ボタンを押したらリアクションがたくさんつくようにする


最後に、ボタンを押した時の処理を書いていきます。

   ARRAYS[i].click();
    const TARGET1 = document.querySelector('#emoji-picker-grinning');
    TARGET1.click();

残りは簡単で、先ほどのようにスクロールやURL変更の際に差分を見ながら、
ボタンが押されると、その箇所の各c-reaction_add--lightを強制的にclickし、立ち上がったモーダルの特定の絵文字をさらにclickするようにします。
これを、絵文字の数だけ処理を追加してあげれば完成です。

ということで一旦完成!!!



実際に使ってみる

ということで、早速使ってみましょう。

誰かのコメントを待っているでもいいですが、まずは過去の自分のコメントに対して、少しワイワイさせてみましょう。

まず思い当たるのは、一ヶ月前の自分が書いた社内へのローンチ報告です。それがこちら。

f:id:chanchihiro:20201220134428p:plain

以下略ではありますが、ちょうどよくローンチ報告をしています。 この時についているリアクションがこちら。

f:id:chanchihiro:20201220134735p:plain

花金の夜ということもあり、まぁまぁな印象ですね。こちらをこの「slack最高速でワイワイできるくん」を使って、もっとワイワイさせてみましょう。

すると...

f:id:chanchihiro:20201220135700g:plain

ワイワイ!!!!!

一気にリアクションが増えて、盛り上がってる感も倍増です。
これを使えば、今後どんなローンチ報告があっても全然使っていけそうです。

他にも例えばこんな時でも使えます。

f:id:chanchihiro:20201220140534p:plain

後輩がいきなり滑った時でも....

f:id:chanchihiro:20201220140848g:plain

ワイワイ!!!!!

他にも、誰かが悩んで落ち込んでいる時や、ただただ盛り上げたい時でも使えること間違いなし!!!

一瞬でワイワイさせることができました!!!!

数日後同僚からこんなメッセージが...。


f:id:chanchihiro:20201220121413p:plain

使い過ぎには気をつけよう!!!



作った後に気づいたこと


今回、一度にたくさんのスタンプを押すようにするために試行錯誤してきましたが、
本当にslackで一気にスタンプを押すような機能がないのか不安になってきました。



でもまさかできるわけないよな....


できてないでくれ....


f:id:chanchihiro:20201220092350p:plain

f:id:chanchihiro:20201220092500p:plain




...


f:id:chanchihiro:20201220092711g:plain

できちゃった。

でもこれはshift + clickをすることで、モーダルを閉じないように連続で何個もスタンプを押すことができる機能のようです。
なので、これと「slack最高速でワイワイできるくん」を組み合わせれば最強だ!



おわりに

いかがだったでしょうか。

今回は大事な時にslackの反応がないと寂しいので、一気にたくさんリアクションできる「slack最高速でワイワイできるくん」を作ってみました。
実際に試してみて、ぜひ会社のslackをワイワイさせていって欲しいです。

こちらに後ほどソースコードを公開する予定です。(整えるの間に合わなかった)

そして、現在カヤックではslack大好きなデザイナーを募集中です!

www.kayac.com



以上でした!!!また来年お会いしましょう。

明日は、@machilda777さんです!