separated out changes into branch
commit
b2d260b884
@ -0,0 +1,69 @@
|
||||
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
|
||||
#
|
||||
# If you find yourself ignoring temporary files generated by your text editor
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile '~/.gitignore_global'
|
||||
|
||||
# Ignore bundler config and downloaded libraries.
|
||||
/.bundle
|
||||
/vendor/bundle
|
||||
|
||||
# Ignore the default SQLite database.
|
||||
/db/*.sqlite3
|
||||
/db/*.sqlite3-journal
|
||||
|
||||
# Ignore all logfiles and tempfiles.
|
||||
.eslintcache
|
||||
/log/*
|
||||
!/log/.keep
|
||||
/tmp
|
||||
/coverage
|
||||
/public/system
|
||||
/public/assets
|
||||
/public/packs
|
||||
/public/packs-test
|
||||
.env
|
||||
.env.production
|
||||
.env.development
|
||||
/node_modules/
|
||||
/build/
|
||||
|
||||
# Ignore Vagrant files
|
||||
.vagrant/
|
||||
|
||||
# Ignore Capistrano customizations
|
||||
/config/deploy/*
|
||||
|
||||
# Ignore IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Ignore postgres + redis + elasticsearch volume optionally created by docker-compose
|
||||
/postgres
|
||||
/redis
|
||||
/elasticsearch
|
||||
|
||||
# ignore Helm lockfile, dependency charts, and local values file
|
||||
/chart/Chart.lock
|
||||
/chart/charts/*.tgz
|
||||
/chart/values.yaml
|
||||
|
||||
# Ignore Apple files
|
||||
.DS_Store
|
||||
|
||||
# Ignore vim files
|
||||
*~
|
||||
*.swp
|
||||
|
||||
# Ignore npm debug log
|
||||
npm-debug.log
|
||||
|
||||
# Ignore yarn log files
|
||||
yarn-error.log
|
||||
yarn-debug.log
|
||||
|
||||
# Ignore vagrant log files
|
||||
*-cloudimg-console.log
|
||||
|
||||
# Ignore Docker option files
|
||||
docker-compose.override.yml
|
@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Permalink from './permalink';
|
||||
import ShortNumber from 'flavours/glitch/components/short_number';
|
||||
|
||||
/**
|
||||
* Used to render counter of how much people are talking about hashtag
|
||||
*
|
||||
* @type {(displayNumber: JSX.Element, pluralReady: number) => JSX.Element}
|
||||
*/
|
||||
const accountsCountRenderer = (displayNumber, pluralReady) => (
|
||||
<FormattedMessage
|
||||
id='trends.counter_by_accounts'
|
||||
defaultMessage='{count, plural, one {{counter} person} other {{counter} people}} talking'
|
||||
values={{
|
||||
count: pluralReady,
|
||||
counter: <strong>{displayNumber}</strong>,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const Hashtag = ({ hashtag }) => (
|
||||
<Permalink className='hashtag' href={hashtag.get('url')} to={`/timelines/tag/${hashtag.get('name')}`}>
|
||||
#<span>{hashtag.get('name')}</span>
|
||||
</Permalink>
|
||||
);
|
||||
|
||||
Hashtag.propTypes = {
|
||||
hashtag: ImmutablePropTypes.map.isRequired,
|
||||
};
|
||||
|
||||
export default Hashtag;
|
@ -0,0 +1,356 @@
|
||||
import React from 'react';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { autoPlayGif, me, isStaff } from 'flavours/glitch/util/initial_state';
|
||||
import { preferencesLink, profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'flavours/glitch/components/icon';
|
||||
import IconButton from 'flavours/glitch/components/icon_button';
|
||||
import Avatar from 'flavours/glitch/components/avatar';
|
||||
import Button from 'flavours/glitch/components/button';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container';
|
||||
import AccountNoteContainer from '../containers/account_note_container';
|
||||
|
||||
const messages = defineMessages({
|
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
|
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' },
|
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
|
||||
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
|
||||
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
|
||||
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
|
||||
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
|
||||
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
|
||||
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
|
||||
direct: { id: 'account.direct', defaultMessage: 'Direct message @{name}' },
|
||||
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
|
||||
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
|
||||
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
|
||||
report: { id: 'account.report', defaultMessage: 'Report @{name}' },
|
||||
share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' },
|
||||
media: { id: 'account.media', defaultMessage: 'Media' },
|
||||
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
|
||||
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
|
||||
hideReblogs: { id: 'account.hide_reblogs', defaultMessage: 'Hide boosts from @{name}' },
|
||||
showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' },
|
||||
enableNotifications: { id: 'account.enable_notifications', defaultMessage: 'Notify me when @{name} posts' },
|
||||
disableNotifications: { id: 'account.disable_notifications', defaultMessage: 'Stop notifying me when @{name} posts' },
|
||||
pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned toots' },
|
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
|
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' },
|
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' },
|
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' },
|
||||
blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' },
|
||||
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Blocked domains' },
|
||||
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
|
||||
endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' },
|
||||
unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' },
|
||||
add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' },
|
||||
admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' },
|
||||
add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' },
|
||||
});
|
||||
|
||||
const dateFormatOptions = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
};
|
||||
|
||||
export default @injectIntl
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
account: ImmutablePropTypes.map,
|
||||
identity_props: ImmutablePropTypes.list,
|
||||
onFollow: PropTypes.func.isRequired,
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onReblogToggle: PropTypes.func.isRequired,
|
||||
onNotifyToggle: PropTypes.func.isRequired,
|
||||
onReport: PropTypes.func.isRequired,
|
||||
onMute: PropTypes.func.isRequired,
|
||||
onBlockDomain: PropTypes.func.isRequired,
|
||||
onUnblockDomain: PropTypes.func.isRequired,
|
||||
onEndorseToggle: PropTypes.func.isRequired,
|
||||
onAddToList: PropTypes.func.isRequired,
|
||||
onEditAccountNote: PropTypes.func.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
domain: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
openEditProfile = () => {
|
||||
window.open(profileLink, '_blank');
|
||||
}
|
||||
|
||||
_updateEmojis () {
|
||||
const node = this.node;
|
||||
|
||||
if (!node || autoPlayGif) {
|
||||
return;
|
||||
}
|
||||
|
||||
const emojis = node.querySelectorAll('.custom-emoji');
|
||||
|
||||
for (var i = 0; i < emojis.length; i++) {
|
||||
let emoji = emojis[i];
|
||||
if (emoji.classList.contains('status-emoji')) {
|
||||
continue;
|
||||
}
|
||||
emoji.classList.add('status-emoji');
|
||||
|
||||
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
|
||||
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
this._updateEmojis();
|
||||
}
|
||||
|
||||
componentDidUpdate () {
|
||||
this._updateEmojis();
|
||||
}
|
||||
|
||||
handleEmojiMouseEnter = ({ target }) => {
|
||||
target.src = target.getAttribute('data-original');
|
||||
}
|
||||
|
||||
handleEmojiMouseLeave = ({ target }) => {
|
||||
target.src = target.getAttribute('data-static');
|
||||
}
|
||||
|
||||
setRef = (c) => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { account, intl, domain, identity_proofs } = this.props;
|
||||
|
||||
if (!account) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountNote = account.getIn(['relationship', 'note']);
|
||||
|
||||
const suspended = account.get('suspended');
|
||||
|
||||
let info = [];
|
||||
let actionBtn = '';
|
||||
let bellBtn = '';
|
||||
let lockedIcon = '';
|
||||
let menu = [];
|
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
|
||||
}
|
||||
else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
|
||||
}
|
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>);
|
||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) {
|
||||
info.push(<span className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>);
|
||||
}
|
||||
|
||||
if (me !== account.get('id')) {
|
||||
if (!account.get('relationship')) { // Wait until the relationship is loaded
|
||||
actionBtn = '';
|
||||
} else if (account.getIn(['relationship', 'requested'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
|
||||
} else if (!account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
|
||||
} else if (account.getIn(['relationship', 'blocking'])) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />;
|
||||
}
|
||||
} else if (profileLink) {
|
||||
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />;
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
|
||||
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
|
||||
}
|
||||
|
||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
||||
if (suspended && !account.getIn(['relationship', 'following'])) {
|
||||
actionBtn = '';
|
||||
}
|
||||
|
||||
if (account.get('locked')) {
|
||||
lockedIcon = <Icon id='lock' title={intl.formatMessage(messages.account_locked)} />;
|
||||
}
|
||||
|
||||
if (account.get('id') !== me && !suspended) {
|
||||
menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.props.onMention });
|
||||
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.props.onDirect });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if ('share' in navigator && !suspended) {
|
||||
menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (accountNote === null || accountNote === '') {
|
||||
menu.push({ text: intl.formatMessage(messages.add_account_note, { name: account.get('username') }), action: this.props.onEditAccountNote });
|
||||
}
|
||||
|
||||
if (account.get('id') === me) {
|
||||
if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink });
|
||||
if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink });
|
||||
menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
|
||||
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
|
||||
menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' });
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' });
|
||||
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
|
||||
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
|
||||
} else {
|
||||
if (account.getIn(['relationship', 'following'])) {
|
||||
if (!account.getIn(['relationship', 'muting'])) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.hideReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.showReblogs, { name: account.get('username') }), action: this.props.onReblogToggle });
|
||||
}
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle });
|
||||
menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList });
|
||||
menu.push(null);
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'muting'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.props.onMute });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.props.onMute });
|
||||
}
|
||||
|
||||
if (account.getIn(['relationship', 'blocking'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.props.onBlock });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.props.onBlock });
|
||||
}
|
||||
|
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport });
|
||||
}
|
||||
|
||||
if (account.get('acct') !== account.get('username')) {
|
||||
const domain = account.get('acct').split('@')[1];
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (account.getIn(['relationship', 'domain_blocking'])) {
|
||||
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.props.onUnblockDomain });
|
||||
} else {
|
||||
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.props.onBlockDomain });
|
||||
}
|
||||
}
|
||||
|
||||
if (account.get('id') !== me && isStaff && accountAdminLink) {
|
||||
menu.push(null);
|
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) });
|
||||
}
|
||||
|
||||
const content = { __html: account.get('note_emojified') };
|
||||
const displayNameHtml = { __html: account.get('display_name_html') };
|
||||
const fields = account.get('fields');
|
||||
const badge_bot = account.get('bot') ? (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>) : null;
|
||||
const badge_ao = account.get('adults_only') ? (<div className='account-role adults-only'><FormattedMessage id='account.badges.adults_only' defaultMessage="🔞 Adult content" /></div>) : null;
|
||||
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
|
||||
|
||||
let badge;
|
||||
|
||||
if (account.get('bot')) {
|
||||
badge = (<div className='account-role bot'><FormattedMessage id='account.badges.bot' defaultMessage='Bot' /></div>);
|
||||
} else if (account.get('group')) {
|
||||
badge = (<div className='account-role group'><FormattedMessage id='account.badges.group' defaultMessage='Group' /></div>);
|
||||
} else {
|
||||
badge = null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||
<div className='account__header__image'>
|
||||
<div className='account__header__info'>
|
||||
{info}
|
||||
</div>
|
||||
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||
</div>
|
||||
|
||||
<div className='account__header__bar'>
|
||||
<div className='account__header__tabs'>
|
||||
<a className='avatar' href={account.get('url')} rel='noopener noreferrer' target='_blank'>
|
||||
<Avatar account={account} size={90} />
|
||||
</a>
|
||||
|
||||
<div className='spacer' />
|
||||
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
<h1>
|
||||
<span dangerouslySetInnerHTML={displayNameHtml} /> {badge_ao}{badge_bot}
|
||||
<small>@{acct} {lockedIcon}</small>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<AccountNoteContainer account={account} />
|
||||
|
||||
{!suspended && (
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{ (fields.size > 0 || identity_proofs.size > 0) && (
|
||||
<div className='account__header__fields'>
|
||||
{identity_proofs.map((proof, i) => (
|
||||
<dl key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: proof.get('provider') }} />
|
||||
|
||||
<dd className='verified'>
|
||||
<a href={proof.get('proof_url')} target='_blank' rel='noopener noreferrer'><span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(proof.get('updated_at'), dateFormatOptions) })}>
|
||||
<Icon id='check' className='verified__mark' />
|
||||
</span></a>
|
||||
<a href={proof.get('profile_url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={{ __html: ' '+proof.get('provider_username') }} /></a>
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
{fields.map((pair, i) => (
|
||||
<dl key={i}>
|
||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
|
||||
|
||||
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
|
||||
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} />
|
||||
</dd>
|
||||
</dl>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,466 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { changeSetting } from 'flavours/glitch/actions/settings';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { useEmoji } from 'flavours/glitch/actions/emojis';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { EmojiPicker as EmojiPickerAsync } from 'flavours/glitch/util/async-components';
|
||||
import Overlay from 'react-overlays/lib/Overlay';
|
||||
import classNames from 'classnames';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import detectPassiveEvents from 'detect-passive-events';
|
||||
import { buildCustomEmojis, categoriesFromEmojis } from 'flavours/glitch/util/emoji';
|
||||
import { useSystemEmojiFont } from 'flavours/glitch/util/initial_state';
|
||||
|
||||
const messages = defineMessages({
|
||||
emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' },
|
||||
emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' },
|
||||
emoji_not_found: { id: 'emoji_button.not_found', defaultMessage: 'No emojos!! (╯°□°)╯︵ ┻━┻' },
|
||||
custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' },
|
||||
recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' },
|
||||
search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' },
|
||||
people: { id: 'emoji_button.people', defaultMessage: 'People' },
|
||||
nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' },
|
||||
food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' },
|
||||
activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' },
|
||||
travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' },
|
||||
objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' },
|
||||
symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' },
|
||||
flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' },
|
||||
});
|
||||
|
||||
const perLine = 8;
|
||||
const lines = 2;
|
||||
|
||||
const DEFAULTS = [
|
||||
'+1',
|
||||
'grinning',
|
||||
'kissing_heart',
|
||||
'heart_eyes',
|
||||
'laughing',
|
||||
'stuck_out_tongue_winking_eye',
|
||||
'sweat_smile',
|
||||
'joy',
|
||||
'yum',
|
||||
'disappointed',
|
||||
'thinking_face',
|
||||
'weary',
|
||||
'sob',
|
||||
'sunglasses',
|
||||
'heart',
|
||||
'ok_hand',
|
||||
];
|
||||
|
||||
const getFrequentlyUsedEmojis = createSelector([
|
||||
state => state.getIn(['settings', 'frequentlyUsedEmojis'], ImmutableMap()),
|
||||
], emojiCounters => {
|
||||
let emojis = emojiCounters
|
||||
.keySeq()
|
||||
.sort((a, b) => emojiCounters.get(a) - emojiCounters.get(b))
|
||||
.reverse()
|
||||
.slice(0, perLine * lines)
|
||||
.toArray();
|
||||
|
||||
if (emojis.length < DEFAULTS.length) {
|
||||
emojis = emojis.concat(DEFAULTS.slice(0, DEFAULTS.length - emojis.length));
|
||||
}
|
||||
|
||||
return emojis;
|
||||
});
|
||||
|
||||
const getCustomEmojis = createSelector([
|
||||
state => state.get('custom_emojis'),
|
||||
], emojis => emojis.filter(e => e.get('visible_in_picker')).sort((a, b) => {
|
||||
const aShort = a.get('shortcode').toLowerCase();
|
||||
const bShort = b.get('shortcode').toLowerCase();
|
||||
|
||||
if (aShort < bShort) {
|
||||
return -1;
|
||||
} else if (aShort > bShort ) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}));
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
custom_emojis: getCustomEmojis(state),
|
||||
skinTone: state.getIn(['settings', 'skinTone']),
|
||||
frequentlyUsedEmojis: getFrequentlyUsedEmojis(state),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch, { onPickEmoji }) => ({
|
||||
onSkinTone: skinTone => {
|
||||
dispatch(changeSetting(['skinTone'], skinTone));
|
||||
},
|
||||
|
||||
onPickEmoji: emoji => {
|
||||
dispatch(useEmoji(emoji));
|
||||
|
||||
if (onPickEmoji) {
|
||||
onPickEmoji(emoji);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const assetHost = process.env.CDN_HOST || '';
|
||||
let EmojiPicker, Emoji; // load asynchronously
|
||||
|
||||
const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`;
|
||||
const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false;
|
||||
|
||||
class ModifierPickerMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClick = e => {
|
||||
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.active) {
|
||||
this.attachListeners();
|
||||
} else {
|
||||
this.removeListeners();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
this.removeListeners();
|
||||
}
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
attachListeners () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
removeListeners () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { active } = this.props;
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers__menu' style={{ display: active ? 'block' : 'none' }} ref={this.setRef}>
|
||||
<button onClick={this.handleClick} data-index={1}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={1} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={2}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={2} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={3}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={3} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={4}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={4} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={5}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={5} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
<button onClick={this.handleClick} data-index={6}><Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={6} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} /></button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ModifierPicker extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
active: PropTypes.bool,
|
||||
modifier: PropTypes.number,
|
||||
onChange: PropTypes.func,
|
||||
onClose: PropTypes.func,
|
||||
onOpen: PropTypes.func,
|
||||
};
|
||||
|
||||
handleClick = () => {
|
||||
if (this.props.active) {
|
||||
this.props.onClose();
|
||||
} else {
|
||||
this.props.onOpen();
|
||||
}
|
||||
}
|
||||
|
||||
handleSelect = modifier => {
|
||||
this.props.onChange(modifier);
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { active, modifier } = this.props;
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown__modifiers'>
|
||||
<Emoji emoji='fist' set='twitter' size={22} sheetSize={32} skin={modifier} onClick={this.handleClick} backgroundImageFn={backgroundImageFn} native={useSystemEmojiFont} />
|
||||
<ModifierPickerMenu active={active} onSelect={this.handleSelect} onClose={this.props.onClose} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectIntl
|
||||
class EmojiPickerMenu extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
custom_emojis: ImmutablePropTypes.list,
|
||||
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
|
||||
loading: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
onPick: PropTypes.func.isRequired,
|
||||
style: PropTypes.object,
|
||||
placement: PropTypes.string,
|
||||
arrowOffsetLeft: PropTypes.string,
|
||||
arrowOffsetTop: PropTypes.string,
|
||||
intl: PropTypes.object.isRequired,
|
||||
skinTone: PropTypes.number.isRequired,
|
||||
onSkinTone: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
style: {},
|
||||
loading: true,
|
||||
frequentlyUsedEmojis: [],
|
||||
};
|
||||
|
||||
state = {
|
||||
modifierOpen: false,
|
||||
placement: null,
|
||||
};
|
||||
|
||||
handleDocumentClick = e => {
|
||||
if (this.node && !this.node.contains(e.target)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount () {
|
||||
document.addEventListener('click', this.handleDocumentClick, false);
|
||||
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
componentWillUnmount () {
|
||||
document.removeEventListener('click', this.handleDocumentClick, false);
|
||||
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
|
||||
}
|
||||
|
||||
setRef = c => {
|
||||
this.node = c;
|
||||
}
|
||||
|
||||
getI18n = () => {
|
||||
const { intl } = this.props;
|
||||
|
||||
return {
|
||||
search: intl.formatMessage(messages.emoji_search),
|
||||
notfound: intl.formatMessage(messages.emoji_not_found),
|
||||
categories: {
|
||||
search: intl.formatMessage(messages.search_results),
|
||||
recent: intl.formatMessage(messages.recent),
|
||||
people: intl.formatMessage(messages.people),
|
||||
nature: intl.formatMessage(messages.nature),
|
||||
foods: intl.formatMessage(messages.food),
|
||||
activity: intl.formatMessage(messages.activity),
|
||||
places: intl.formatMessage(messages.travel),
|
||||
objects: intl.formatMessage(messages.objects),
|
||||
symbols: intl.formatMessage(messages.symbols),
|
||||
flags: intl.formatMessage(messages.flags),
|
||||
custom: intl.formatMessage(messages.custom),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleClick = (emoji, event) => {
|
||||
if (!emoji.native) {
|
||||
emoji.native = emoji.colons;
|
||||
}
|
||||
if (!(event.ctrlKey || event.metaKey)) {
|
||||
this.props.onClose();
|
||||
}
|
||||
this.props.onPick(emoji);
|
||||
}
|
||||
|
||||
handleModifierOpen = () => {
|
||||
this.setState({ modifierOpen: true });
|
||||
}
|
||||
|
||||
handleModifierClose = () => {
|
||||
this.setState({ modifierOpen: false });
|
||||
}
|
||||
|
||||
handleModifierChange = modifier => {
|
||||
this.props.onSkinTone(modifier);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ width: 299 }} />;
|
||||
}
|
||||
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
|
||||
const { modifierOpen } = this.state;
|
||||
|
||||
const categoriesSort = [
|
||||
'recent',
|
||||
'people',
|
||||
'nature',
|
||||
'foods',
|
||||
'activity',
|
||||
'places',
|
||||
'objects',
|
||||
'symbols',
|
||||
'flags',
|
||||
];
|
||||
|
||||
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());
|
||||
|
||||
return (
|
||||
<div className={classNames('emoji-picker-dropdown__menu', { selecting: modifierOpen })} style={style} ref={this.setRef}>
|
||||
<EmojiPicker
|
||||
perLine={8}
|
||||
emojiSize={22}
|
||||
sheetSize={32}
|
||||
custom={buildCustomEmojis(custom_emojis)}
|
||||
color=''
|
||||
emoji=''
|
||||
set='twitter'
|
||||
title={title}
|
||||
i18n={this.getI18n()}
|
||||
onClick={this.handleClick}
|
||||
include={categoriesSort}
|
||||
recent={frequentlyUsedEmojis}
|
||||
skin={skinTone}
|
||||
showPreview={false}
|
||||
backgroundImageFn={backgroundImageFn}
|
||||
autoFocus
|
||||
emojiTooltip
|
||||
native={useSystemEmojiFont}
|
||||
/>
|
||||
|
||||
<ModifierPicker
|
||||
active={modifierOpen}
|
||||
modifier={skinTone}
|
||||
onOpen={this.handleModifierOpen}
|
||||
onClose={this.handleModifierClose}
|
||||
onChange={this.handleModifierChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default @connect(mapStateToProps, mapDispatchToProps)
|
||||
@injectIntl
|
||||
class EmojiPickerDropdown extends React.PureComponent {
|
||||
|
||||
static propTypes = {
|
||||
custom_emojis: ImmutablePropTypes.list,
|
||||
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
|
||||
intl: PropTypes.object.isRequired,
|
||||
onPickEmoji: PropTypes.func.isRequired,
|
||||
onSkinTone: PropTypes.func.isRequired,
|
||||
skinTone: PropTypes.number.isRequired,
|
||||
button: PropTypes.node,
|
||||
};
|
||||
|
||||
state = {
|
||||
active: false,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
setRef = (c) => {
|
||||
this.dropdown = c;
|
||||
}
|
||||
|
||||
onShowDropdown = ({ target }) => {
|
||||
this.setState({ active: true });
|
||||
|
||||
if (!EmojiPicker) {
|
||||
this.setState({ loading: true });
|
||||
|
||||
EmojiPickerAsync().then(EmojiMart => {
|
||||
EmojiPicker = EmojiMart.Picker;
|
||||
Emoji = EmojiMart.Emoji;
|
||||
|
||||
this.setState({ loading: false });
|
||||
}).catch(() => {
|
||||
this.setState({ loading: false });
|
||||
});
|
||||
}
|
||||
|
||||
const { top } = target.getBoundingClientRect();
|
||||
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
|
||||
}
|
||||
|
||||
onHideDropdown = () => {
|
||||
this.setState({ active: false });
|
||||
}
|
||||
|
||||
onToggle = (e) => {
|
||||
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
|
||||
if (this.state.active) {
|
||||
this.onHideDropdown();
|
||||
} else {
|
||||
this.onShowDropdown(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleKeyDown = e => {
|
||||
if (e.key === 'Escape') {
|
||||
this.onHideDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
setTargetRef = c => {
|
||||
this.target = c;
|
||||
}
|
||||
|
||||
findTarget = () => {
|
||||
return this.target;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
|
||||
const title = intl.formatMessage(messages.emoji);
|
||||
const { active, loading, placement } = this.state;
|
||||
|
||||
return (
|
||||
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
|
||||
<div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}>
|
||||
{button || <img
|
||||
className={classNames('emojione', { 'pulse-loading': active && loading })}
|
||||
alt='🦊'
|
||||
src={`${assetHost}/emoji/fuwux_flower.svg`}
|
||||
/>}
|
||||
</div>
|
||||
|
||||
<Overlay show={active} placement={placement} target={this.findTarget}>
|
||||
<EmojiPickerMenu
|
||||
custom_emojis={this.props.custom_emojis}
|
||||
loading={loading}
|
||||
onClose={this.onHideDropdown}
|
||||
onPick={onPickEmoji}
|
||||
onSkinTone={onSkinTone}
|
||||
skinTone={skinTone}
|
||||
frequentlyUsedEmojis={frequentlyUsedEmojis}
|
||||
/>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
import inherited from 'mastodon/locales/en-FOX.json';
|
||||
|
||||
const messages = {
|
||||
'getting_started.open_source_notice': 'Glitchsoc is free open source software forked from {Mastodon}. You can contribute or report issues on GitHub at {github}.',
|
||||
'layout.auto': 'Auto',
|
||||
'layout.current_is': 'Your current layout is:',
|
||||
'layout.desktop': 'Desktop',
|
||||
'layout.mobile': 'Mobile',
|
||||
'navigation_bar.app_settings': 'App settings',
|
||||
'getting_started.onboarding': 'Show me around',
|
||||
'onboarding.page_one.federation': '{domain} is an \'instance\' of Mastodon. Mastodon is a network of independent servers joining up to make one larger social network. We call these servers instances.',
|
||||
'onboarding.page_one.welcome': 'Welcome to {domain}!',
|
||||
'onboarding.page_six.github': '{domain} runs on Glitchsoc. Glitchsoc is a friendly {fork} of {Mastodon}, and is compatible with any Mastodon instance or app. Glitchsoc is entirely free and open-source. You can report bugs, request features, or contribute to the code on {github}.',
|
||||
'settings.auto_collapse': 'Automatic collapsing',
|
||||
'settings.auto_collapse_all': 'Everything',
|
||||
'settings.auto_collapse_lengthy': 'Lengthy posts',
|
||||
'settings.auto_collapse_media': 'Posts with media',
|
||||
'settings.auto_collapse_notifications': 'geckerings',
|
||||
'settings.auto_collapse_reblogs': 'Boosts',
|
||||
'settings.auto_collapse_replies': 'Replies',
|
||||
'settings.show_action_bar': 'Show action buttons in collapsed posts',
|
||||
'settings.close': 'Close',
|
||||
'settings.collapsed_statuses': 'Collapsed posts',
|
||||
'settings.enable_collapsed': 'Enable collapsed posts',
|
||||
'settings.general': 'General',
|
||||
'settings.image_backgrounds': 'Image backgrounds',
|
||||
'settings.image_backgrounds_media': 'Preview collapsed post media',
|
||||
'settings.image_backgrounds_users': 'Give collapsed posts an image background',
|
||||
'settings.media': 'Media',
|
||||
'settings.media_letterbox': 'Letterbox media',
|
||||
'settings.media_fullwidth': 'Full-width media previews',
|
||||
'settings.preferences': 'User preferences',
|
||||
'settings.wide_view': 'Wide view (Desktop mode only)',
|
||||
'settings.navbar_under': 'Navbar at the bottom (Mobile only)',
|
||||
'status.collapse': 'Collapse',
|
||||
'status.uncollapse': 'Uncollapse',
|
||||
|
||||
'media_gallery.sensitive': 'Sensitive',
|
||||
|
||||
'favourite_modal.combo': 'You can press {combo} to skip this next time',
|
||||
|
||||
'home.column_settings.show_direct': 'Show DMs',
|
||||
|
||||
'notification.markForDeletion': 'Mark for deletion',
|
||||
'notifications.clear': 'Cease all the geckering',
|
||||
'notifications.marked_clear_confirmation': 'Are you sure you want to permanently cease ALL geckering????',
|
||||
'notifications.marked_clear': 'Clear selected notifications',
|
||||
|
||||
'notification_purge.btn_all': 'Select\nall',
|
||||
'notification_purge.btn_none': 'Select\nnone',
|
||||
'notification_purge.btn_invert': 'Invert\nselection',
|
||||
'notification_purge.btn_apply': 'Clear\nselected',
|
||||
|
||||
'compose.attach.upload': 'Upload a file',
|
||||
'compose.attach.doodle': 'Draw something',
|
||||
'compose.attach': 'Attach...',
|
||||
|
||||
'advanced_options.local-only.short': 'Local-only',
|
||||
'advanced_options.local-only.long': 'Do not post to other instances',
|
||||
'advanced_options.local-only.tooltip': 'This post is local-only',
|
||||
'advanced_options.icon_title': 'Advanced options',
|
||||
'advanced_options.threaded_mode.short': 'Threaded mode',
|
||||
'advanced_options.threaded_mode.long': 'Automatically opens a reply on posting',
|
||||
'advanced_options.threaded_mode.tooltip': 'Threaded mode enabled',
|
||||
};
|
||||
|
||||
export default Object.assign({}, inherited, messages);
|
@ -0,0 +1,563 @@
|
||||
.column__wrapper {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.columns-area {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
overflow-x: auto;
|
||||
position: relative;
|
||||
|
||||
&__panels {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 100vh;
|
||||
|
||||
&__pane {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 285px;
|
||||
|
||||
&--start {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
&__inner {
|
||||
position: fixed;
|
||||
width: 285px;
|
||||
pointer-events: auto;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
&__main {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@media screen and (min-width: $no-gap-breakpoint) {
|
||||
padding: 0 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-bar__wrapper {
|
||||
background: darken($ui-base-color, 8%);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
padding-top: 0;
|
||||
|
||||
@media screen and (min-width: $no-gap-breakpoint) {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.tabs-bar {
|
||||
margin-bottom: 0;
|
||||
|
||||
@media screen and (min-width: $no-gap-breakpoint) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.react-swipeable-view-container {
|
||||
&,
|
||||
.columns-area,
|
||||
.column {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.react-swipeable-view-container > * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
width: 330px;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .scrollable {
|
||||
background: $ui-base-color;
|
||||
}
|
||||
}
|
||||
|
||||
.ui {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.column {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.column-back-button {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
background: lighten($ui-base-color, 4%);
|
||||
color: $highlight-text-color;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
border: 0;
|
||||
text-align: unset;
|
||||
padding: 15px;
|
||||
margin: 0;
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.column-header__back-button {
|
||||
background: lighten($ui-base-color, 4%);
|
||||
border: 0;
|
||||
font-family: inherit;
|
||||
color: $highlight-text-color;
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
padding: 0 5px 0 0;
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.column-back-button__icon {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.column-back-button--slim {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.column-back-button--slim-button {
|
||||
cursor: pointer;
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -48px;
|
||||
}
|
||||
|
||||
.column-link {
|
||||
background: lighten($ui-base-color, 8%);
|
||||
color: $primary-text-color;
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
padding: 15px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: lighten($ui-base-color, 11%);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&--transparent {
|
||||
background: transparent;
|
||||
color: $ui-secondary-color;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: transparent;
|
||||
color: $primary-text-color;
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: $ui-highlight-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.column-link__icon {
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.column-subheading {
|
||||
background: $ui-base-color;
|
||||
color: $dark-text-color;
|
||||
padding: 8px 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.column-header__wrapper {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
z-index: 1;
|
||||
|
||||
&.active {
|
||||
box-shadow: 0 1px 0 rgba($highlight-text-color, 0.3);
|
||||
|
||||
&::before {
|
||||
display: block;
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -13px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 60%;
|
||||
pointer-events: none;
|
||||