about summary refs log tree commit diff
path: root/app/javascript/mastodon/features/account/components/header.js
blob: 6f802ceda9bbfba87f093110c6e6ab010392e943 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import emojify from '../../../emoji';
import escapeTextContentForBrowser from 'escape-html';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Avatar from '../../../components/avatar';
import ImmutablePureComponent from 'react-immutable-pure-component';

const messages = defineMessages({
  unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
  follow: { id: 'account.follow', defaultMessage: 'Follow' },
  requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
});

/*
  THIS IS A MESS BECAUSE EFFING MASTODON AND ITS EFFING HTML BIOS
  INSTEAD OF JUST STORING EVERYTHING IN PLAIN EFFING TEXT ! ! ! !
  BLANK LINES ALSO WON'T WORK BECAUSE RIGHT NOW MASTODON CONVERTS
  THOSE INTO `<P>` ELEMENTS INSTEAD OF LEAVING IT AS `<BR><BR>` !
  TL:DR; THIS IS LARGELY A HACK. WITH BETTER BACKEND STUFF WE CAN
  IMPROVE THIS BY BETTER PREDICTING HOW THE METADATA WILL BE SENT
  WHILE MAINTAINING BASIC PLAIN-TEXT PROCESSING. THE OTHER OPTION
  IS TO TURN ALL BIOS INTO PLAIN-TEXT VIA A TREE-WALKER, AND THEN
  PROCESS THE YAML AND LINKS AND EVERYTHING OURSELVES. THIS WOULD
  BE INCREDIBLY COMPLICATED, AND IT WOULD BE A MILLION TIMES LESS
  DIFFICULT IF MASTODON JUST GAVE US PLAIN-TEXT BIOS (WHICH QUITE
  FRANKLY MAKES THE MOST SENSE SINCE THAT'S WHAT USERS PROVIDE IN
  SETTINGS) TO BEGIN WITH AND LEFT ALL PROCESSING TO THE FRONTEND
  TO HANDLE ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !
  ANYWAY I KNOW WHAT NEEDS TO BE DONE REGARDING BACKEND STUFF BUT
  I'M NOT SMART ENOUGH TO FIGURE OUT HOW TO ACTUALLY IMPLEMENT IT
  SO FEEL FREE TO @ ME IF YOU NEED MY IDEAS REGARDING THAT. UNTIL
  THEN WE'LL JUST HAVE TO MAKE DO WITH THIS MESSY AND UNFORTUNATE
  HACKING ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! ! !

                                           with love,
                                           @kibi@glitch.social <3
*/

const NEW_LINE    = /(?:^|\r?\n|<br\s*\/?>)/g;
const YAML_OPENER = /---/;
const YAML_CLOSER = /(?:---|\.\.\.)/;
const YAML_STRING = /(?:"(?:[^"\n]){1,32}"|'(?:[^'\n]){1,32}'|(?:[^'":\n]){1,32})/g;
const YAML_LINE = new RegExp('\\s*' + YAML_STRING.source + '\\s*:\\s*' + YAML_STRING.source + '\\s*', 'g');
const BIO_REGEX = new RegExp(NEW_LINE.source + '*' + YAML_OPENER.source + NEW_LINE.source + '+(?:' + YAML_LINE.source + NEW_LINE.source + '+){0,4}' + YAML_CLOSER.source + NEW_LINE.source + '*');

const processBio = (data) => {
  let props = { text: data, metadata: [] };
  let yaml = data.match(BIO_REGEX);
  if (!yaml) return props;
  else yaml = yaml[0];
  let start = props.text.indexOf(yaml);
  let end = start + yaml.length;
  props.text = props.text.substr(0, start) + props.text.substr(end);
  yaml = yaml.replace(NEW_LINE, '\n');
  let metadata = (yaml ? yaml.match(YAML_LINE) : []) || [];
  for (let i = 0; i < metadata.length; i++) {
    let result = metadata[i].match(YAML_STRING);
    if (result[0][0] === '"' || result[0][0] === '\'') result[0] = result[0].substr(1, result[0].length - 2);
    if (result[1][0] === '"' || result[1][0] === '\'') result[0] = result[1].substr(1, result[1].length - 2);
    props.metadata.push(result);
  }
  return props;
};

@injectIntl
export default class Header extends ImmutablePureComponent {

  static propTypes = {
    account: ImmutablePropTypes.map,
    me: PropTypes.number.isRequired,
    onFollow: PropTypes.func.isRequired,
    intl: PropTypes.object.isRequired,
  };

  render () {
    const { account, me, intl } = this.props;

    if (!account) {
      return null;
    }

    let displayName = account.get('display_name');
    let info        = '';
    let actionBtn   = '';
    let lockedIcon  = '';

    if (displayName.length === 0) {
      displayName = account.get('username');
    }

    if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
      info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>;
    }

    if (me !== account.get('id')) {
      if (account.getIn(['relationship', 'requested'])) {
        actionBtn = (
          <div className='account--action-button'>
            <IconButton size={26} disabled icon='hourglass' title={intl.formatMessage(messages.requested)} />
          </div>
        );
      } else if (!account.getIn(['relationship', 'blocking'])) {
        actionBtn = (
          <div className='account--action-button'>
            <IconButton size={26} icon={account.getIn(['relationship', 'following']) ? 'user-times' : 'user-plus'} active={account.getIn(['relationship', 'following'])} title={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />
          </div>
        );
      }
    }

    if (account.get('locked')) {
      lockedIcon = <i className='fa fa-lock' />;
    }

    const displayNameHTML    = { __html: emojify(escapeTextContentForBrowser(displayName)) };
    const { text, metadata } = processBio(account.get('note'));

    return (
      <div className='account__header__wrapper'>
        <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}>
          <div>
            <a href={account.get('url')} target='_blank' rel='noopener'>
              <span className='account__header__avatar'><Avatar src={account.get('avatar')} animate size={90} /></span>
              <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} />
            </a>
            <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span>
            <div className='account__header__content' dangerouslySetInnerHTML={{ __html: emojify(text) }} />

            {info}
            {actionBtn}
          </div>
        </div>

        {metadata.length && (
          <div className='account__metadata'>
            {(() => {
              let data = [];
              for (let i = 0; i < metadata.length; i++) {
                data.push(
                  <div
                    className='account__metadata-item'
                    title={metadata[i][0] + ':' + metadata[i][1]}
                    key={i}
                  >
                    <span dangerouslySetInnerHTML={{ __html: emojify(metadata[i][0]) }} />
                    <strong dangerouslySetInnerHTML={{ __html: emojify(metadata[i][1]) }} />
                  </div>
                );
              }
              return data;
            })()}
          </div>
        ) || null}
      </div>
    );
  }

}