Mastodon JS まわりのメモ

トゥート表示の status に関するファイルが、タイムラインのほうと、詳細表示のほうと混乱したので、あとモーダル関連読んでみたので、メモ。誤りがあれば ご指摘ください。 ソースは執筆時点のものです。

Status 関連のファイル

以下、 /app/javascript/mastodon 配下のパス。 (リストの階層は import の関係を示したつもり)

  • features/ui/index.js - UI クラスがある。
    • features/ui/util/async-components.js - webpackChunkName を指定して import している。 webpack の Magic Comment という機能らしい。
      • features/home_timeline/index.js - Home タイムライン。
        • features/ui/containers/status_list_container.js - StatusList コンテナ。
          • components/status_list.js - StatusList コンポーネント。
            • containers/status_container.js - Status コンテナ。
              • components/status.js - Status コンポーネント。タイムラインのほう。
      • features/status/index.js - Status コンポーネント。詳細表示のほう。

パス順に並べるとこう

  • features/ui/index.js - UI クラス
  • features/ui/util/async-components.js - webpackChunkName を指定して import している
  • features/home_timeline/index.js - Home タイムライン
  • features/ui/containers/status_list_container.js - StatusList コンテナ (タイムライン)
  • components/status_list.js - StatusList コンポーネント (タイムライン)
  • containers/status_container.js - Status コンテナ (タイムライン)
  • components/status.js - Status コンポーネント (タイムライン)
  • features/status/index.js - Status コンポーネント (詳細表示)

モーダル関連

新しくモーダルを作りたい場合どうするか。例としてミュートの処理を見ていく。

Status container

containers/status_container.jsmapDispatchToPropsonMute というのがある。

// containers/status_container.js
const mapDispatchToProps = (dispatch, { intl }) => ({
  /* ... */
  onMute (account) {
    dispatch(initMuteModal(account));
  },
  /* ... */
});

export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

ちなみに onMute の書き方は Shorthand method names というもの。 mapDispatchToProps 関数については公式ガイドおよび これとか 。従って component のほうで this.props.onMute(...) とか呼ばれる。が、 components/status.js では onMute は使われていない。はて。

と思ったら、その下の StatusActionBar に渡されていた。

Status component

// components/status.js
export default class Status extends ImmutablePureComponent {
  /* ... */
  render () {
    /* ... */
    let { status, account, ...other } = this.props;
    /* ... */
    return (
            /* ... */
            <StatusActionBar status={status} account={account} {...other} />

let { status, account, ...other } = this.props; という書き方は 分割代入でのスプレッド演算 というやつ。 StatusActionBar のほうの {...other}JSX の Spread Attributes で、子供である StatusActionBar component に props をそのまま引き継ぐ。

StatusActionBar component

// components/status_action_bar.js
export default class StatusActionBar extends ImmutablePureComponent {
  /* ... */
  handleMuteClick = () => {
    this.props.onMute(this.props.status.get('account'));
  }

普通に呼んでるだけだ。というかこの辺、去年書いた記事でも見た気がする。それはさておき openModal の方を見る。

mutes action creator

// actions/mutes.js
export function initMuteModal(account) {
  return dispatch => {
    dispatch({
      type: MUTES_INIT_MODAL,
      account,
    });

    dispatch(openModal('MUTE'));
  };
}

Redux の action creator だ。 MUTES_INIT_MODAL を grep して reducer を見てみよう。

mutes reducer

// reducers/mutes.js
const initialState = Immutable.Map({
  new: Immutable.Map({
    isSubmitting: false,
    account: null,
    notifications: true,
  }),
});

export default function mutes(state = initialState, action) {
  switch (action.type) {
  case MUTES_INIT_MODAL:
    return state.withMutations((state) => {
      state.setIn(['new', 'isSubmitting'], false);
      state.setIn(['new', 'account'], action.account);
      state.setIn(['new', 'notifications'], true);
    });
  case MUTES_TOGGLE_HIDE_NOTIFICATIONS:
    return state.updateIn(['new', 'notifications'], (old) => !old);
  default:
    return state;
  }
}

withMutationssetInImmutable.js のメソッド。書き方が違うだけでやっていることは新たな state の生成だ。 MUTES_INIT_MODAL ではモーダルを出すための情報を state に書いているのだろう。次に openModal('MUTE') の方を見る。

// actions/modal.js
export function openModal(type, props) {
  return {
    type: MODAL_OPEN,
    modalType: type,
    modalProps: props,
  };
};

MODAL_OPEN を grep 。

// reducers/modal.js
const initialState = {
  modalType: null,
  modalProps: {},
};

export default function modal(state = initialState, action) {
  switch(action.type) {
  case MODAL_OPEN:
    return { modalType: action.modalType, modalProps: action.modalProps };
  case MODAL_CLOSE:
    return initialState;
  default:
    return state;
  }
};

state を作って返してるだけだ (reducer なのでそれはそうか) 。さて、実際に state を受け取ってモーダルを表示する処理は一体どこにあるのか??? modalType で grep してみると modal_container.js という container が見つかる。そしてこれはテッペンである UI クラスの render で使われている。

// features/ui/containers/modal_container.js
const mapStateToProps = state => ({
  type: state.get('modal').modalType,
  props: state.get('modal').modalProps,
});

const mapDispatchToProps = dispatch => ({
  onClose () {
    dispatch(closeModal());
  },
});

export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot);

state から先程の modalTypemodalProps を props に map している (これ this.props.props ということになるけど、ややこしくないですか?これが普通なんでしょうか??) 。これに接続される ModalRoot component を見ると、先程の文字列 'MUTE'MuteModal という component を紐付ける定数がある。たどり着きましたね。さて、 ModalRoot のソースを読み進めると、

ModalRoot component

// features/ui/components/modal_root.js
  render () {
    const { type, props, onClose } = this.props;
    const { revealed } = this.state;
    const visible = !!type;

    if (!visible) {
      return (
        <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} />
      );
    }

visible で表示非表示を制御しているっぽいが、 !!type とは???となり、ググると こういうこと らしく、つまり visible = type != null の shorthand ということか。すごく初歩的なことのような気がするけど知らなかった…。

とりあえずモーダル component を出すとこまではたどり着いた。 component の中身は後日ということで今日はここまで。


See also