Browse Source
Work to maintain - Permalinks (aka links outside the app instead of inside) - Alt badges and no-alt badges and media-description-missing class for audio/video - Federation dropdowns and icons (local-only UI)dariusk-working/4_4_0
18 changed files with 26 additions and 1807 deletions
@ -1,52 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; |
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||
|
||||
import { Hashtag } from 'mastodon/components/hashtag'; |
||||
|
||||
const messages = defineMessages({ |
||||
lastStatusAt: { id: 'account.featured_tags.last_status_at', defaultMessage: 'Last post on {date}' }, |
||||
empty: { id: 'account.featured_tags.last_status_never', defaultMessage: 'No posts' }, |
||||
}); |
||||
|
||||
class FeaturedTags extends ImmutablePureComponent { |
||||
|
||||
static propTypes = { |
||||
account: ImmutablePropTypes.record, |
||||
featuredTags: ImmutablePropTypes.list, |
||||
tagged: PropTypes.string, |
||||
intl: PropTypes.object.isRequired, |
||||
}; |
||||
|
||||
render () { |
||||
const { account, featuredTags, intl } = this.props; |
||||
|
||||
if (!account || account.get('suspended') || featuredTags.isEmpty()) { |
||||
return null; |
||||
} |
||||
|
||||
return ( |
||||
<div className='getting-started__trends'> |
||||
<h4><FormattedMessage id='account.featured_tags.title' defaultMessage="{name}'s featured hashtags" values={{ name: <bdi dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /> }} /></h4> |
||||
|
||||
{featuredTags.take(3).map(featuredTag => ( |
||||
<Hashtag |
||||
key={featuredTag.get('name')} |
||||
name={featuredTag.get('name')} |
||||
href={featuredTag.get('url')} |
||||
to={`/@${account.get('acct')}/tagged/${featuredTag.get('name')}`} |
||||
uses={featuredTag.get('statuses_count') * 1} |
||||
withGraph={false} |
||||
description={((featuredTag.get('statuses_count') * 1) > 0) ? intl.formatMessage(messages.lastStatusAt, { date: intl.formatDate(featuredTag.get('last_status_at'), { month: 'short', day: '2-digit' }) }) : intl.formatMessage(messages.empty)} |
||||
/> |
||||
))} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default injectIntl(FeaturedTags); |
||||
@ -1,520 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; |
||||
|
||||
import classNames from 'classnames'; |
||||
import { Helmet } from 'react-helmet'; |
||||
import { NavLink, withRouter } from 'react-router-dom'; |
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||
|
||||
import CheckIcon from '@/material-icons/400-24px/check.svg?react'; |
||||
import LockIcon from '@/material-icons/400-24px/lock.svg?react'; |
||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; |
||||
import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; |
||||
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; |
||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; |
||||
import ShareIcon from '@/material-icons/400-24px/share.svg?react'; |
||||
import { Avatar } from 'mastodon/components/avatar'; |
||||
import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge'; |
||||
import { Button } from 'mastodon/components/button'; |
||||
import { CopyIconButton } from 'mastodon/components/copy_icon_button'; |
||||
import { FollowersCounter, FollowingCounter, StatusesCounter } from 'mastodon/components/counters'; |
||||
import { Icon } from 'mastodon/components/icon'; |
||||
import { IconButton } from 'mastodon/components/icon_button'; |
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator'; |
||||
import { ShortNumber } from 'mastodon/components/short_number'; |
||||
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container'; |
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; |
||||
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state'; |
||||
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions'; |
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; |
||||
|
||||
import AccountNoteContainer from '../containers/account_note_container'; |
||||
import FollowRequestNoteContainer from '../containers/follow_request_note_container'; |
||||
|
||||
import { DomainPill } from './domain_pill'; |
||||
|
||||
const messages = defineMessages({ |
||||
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' }, |
||||
follow: { id: 'account.follow', defaultMessage: 'Follow' }, |
||||
followBack: { id: 'account.follow_back', defaultMessage: 'Follow back' }, |
||||
mutual: { id: 'account.mutual', defaultMessage: 'Mutual' }, |
||||
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Withdraw 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: 'Privately mention @{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' }, |
||||
copy: { id: 'account.copy', defaultMessage: 'Copy link to 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 posts' }, |
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, |
||||
follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, |
||||
favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, |
||||
lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, |
||||
followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, |
||||
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}' }, |
||||
admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, |
||||
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, |
||||
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, |
||||
}); |
||||
|
||||
const titleFromAccount = account => { |
||||
const displayName = account.get('display_name'); |
||||
const acct = account.get('acct') === account.get('username') ? `${account.get('username')}@${localDomain}` : account.get('acct'); |
||||
const prefix = displayName.trim().length === 0 ? account.get('username') : displayName; |
||||
|
||||
return `${prefix} (@${acct})`; |
||||
}; |
||||
|
||||
const messageForFollowButton = relationship => { |
||||
if(!relationship) return messages.follow; |
||||
|
||||
if (relationship.get('following') && relationship.get('followed_by')) { |
||||
return messages.mutual; |
||||
} else if (relationship.get('following') || relationship.get('requested')) { |
||||
return messages.unfollow; |
||||
} else if (relationship.get('followed_by')) { |
||||
return messages.followBack; |
||||
} else { |
||||
return messages.follow; |
||||
} |
||||
}; |
||||
|
||||
const dateFormatOptions = { |
||||
month: 'short', |
||||
day: 'numeric', |
||||
year: 'numeric', |
||||
hour: '2-digit', |
||||
minute: '2-digit', |
||||
}; |
||||
|
||||
class Header extends ImmutablePureComponent { |
||||
|
||||
static propTypes = { |
||||
identity: identityContextPropShape, |
||||
account: ImmutablePropTypes.record, |
||||
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, |
||||
onChangeLanguages: PropTypes.func.isRequired, |
||||
onInteractionModal: PropTypes.func.isRequired, |
||||
onOpenAvatar: PropTypes.func.isRequired, |
||||
onOpenURL: PropTypes.func.isRequired, |
||||
intl: PropTypes.object.isRequired, |
||||
domain: PropTypes.string.isRequired, |
||||
hidden: PropTypes.bool, |
||||
...WithRouterPropTypes, |
||||
}; |
||||
|
||||
setRef = c => { |
||||
this.node = c; |
||||
}; |
||||
|
||||
openEditProfile = () => { |
||||
window.open('/settings/profile', '_blank'); |
||||
}; |
||||
|
||||
isStatusesPageActive = (match, location) => { |
||||
if (!match) { |
||||
return false; |
||||
} |
||||
|
||||
return !location.pathname.match(/\/(followers|following)\/?$/); |
||||
}; |
||||
|
||||
handleMouseEnter = ({ currentTarget }) => { |
||||
if (autoPlayGif) { |
||||
return; |
||||
} |
||||
|
||||
const emojis = currentTarget.querySelectorAll('.custom-emoji'); |
||||
|
||||
for (var i = 0; i < emojis.length; i++) { |
||||
let emoji = emojis[i]; |
||||
emoji.src = emoji.getAttribute('data-original'); |
||||
} |
||||
}; |
||||
|
||||
handleMouseLeave = ({ currentTarget }) => { |
||||
if (autoPlayGif) { |
||||
return; |
||||
} |
||||
|
||||
const emojis = currentTarget.querySelectorAll('.custom-emoji'); |
||||
|
||||
for (var i = 0; i < emojis.length; i++) { |
||||
let emoji = emojis[i]; |
||||
emoji.src = emoji.getAttribute('data-static'); |
||||
} |
||||
}; |
||||
|
||||
handleAvatarClick = e => { |
||||
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { |
||||
e.preventDefault(); |
||||
this.props.onOpenAvatar(); |
||||
} |
||||
}; |
||||
|
||||
handleDisplayNameClick = () => window.open(this.props.account.get('url')); |
||||
|
||||
handleShare = () => { |
||||
const { account } = this.props; |
||||
|
||||
navigator.share({ |
||||
url: account.get('url'), |
||||
}).catch((e) => { |
||||
if (e.name !== 'AbortError') console.error(e); |
||||
}); |
||||
}; |
||||
|
||||
handleHashtagClick = e => { |
||||
const { history } = this.props; |
||||
const value = e.currentTarget.textContent.replace(/^#/, ''); |
||||
|
||||
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { |
||||
e.preventDefault(); |
||||
history.push(`/tags/${value}`); |
||||
} |
||||
}; |
||||
|
||||
handleMentionClick = e => { |
||||
const { history, onOpenURL } = this.props; |
||||
|
||||
if (history && e.button === 0 && !(e.ctrlKey || e.metaKey)) { |
||||
e.preventDefault(); |
||||
|
||||
const link = e.currentTarget; |
||||
|
||||
onOpenURL(link.href, history, () => { |
||||
window.location = link.href; |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
_attachLinkEvents () { |
||||
const node = this.node; |
||||
|
||||
if (!node) { |
||||
return; |
||||
} |
||||
|
||||
const links = node.querySelectorAll('a'); |
||||
|
||||
let link; |
||||
|
||||
for (var i = 0; i < links.length; ++i) { |
||||
link = links[i]; |
||||
|
||||
if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) { |
||||
link.addEventListener('click', this.handleHashtagClick, false); |
||||
} else if (link.classList.contains('mention')) { |
||||
link.addEventListener('click', this.handleMentionClick, false); |
||||
} |
||||
} |
||||
} |
||||
|
||||
componentDidMount () { |
||||
this._attachLinkEvents(); |
||||
} |
||||
|
||||
componentDidUpdate () { |
||||
this._attachLinkEvents(); |
||||
} |
||||
|
||||
render () { |
||||
const { account, hidden, intl } = this.props; |
||||
const { signedIn, permissions } = this.props.identity; |
||||
|
||||
if (!account) { |
||||
return null; |
||||
} |
||||
|
||||
const suspended = account.get('suspended'); |
||||
const isRemote = account.get('acct') !== account.get('username'); |
||||
const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null; |
||||
|
||||
let actionBtn, bellBtn, lockedIcon, shareBtn; |
||||
|
||||
let info = []; |
||||
let menu = []; |
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) { |
||||
info.push(<span key='blocked' className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>); |
||||
} |
||||
|
||||
if (me !== account.get('id') && account.getIn(['relationship', 'muting'])) { |
||||
info.push(<span key='muted' className='relationship-tag'><FormattedMessage id='account.muted' defaultMessage='Muted' /></span>); |
||||
} else if (me !== account.get('id') && account.getIn(['relationship', 'domain_blocking'])) { |
||||
info.push(<span key='domain_blocked' className='relationship-tag'><FormattedMessage id='account.domain_blocked' defaultMessage='Domain blocked' /></span>); |
||||
} |
||||
|
||||
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) { |
||||
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} 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 ('share' in navigator) { |
||||
shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />; |
||||
} else { |
||||
shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />; |
||||
} |
||||
|
||||
if (me !== account.get('id')) { |
||||
if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded |
||||
actionBtn = <Button disabled><LoadingIndicator /></Button>; |
||||
} else if (!account.getIn(['relationship', 'blocking'])) { |
||||
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames({ 'button--destructive': (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) })} text={intl.formatMessage(messageForFollowButton(account.get('relationship')))} onClick={signedIn ? this.props.onFollow : this.props.onInteractionModal} />; |
||||
} else if (account.getIn(['relationship', 'blocking'])) { |
||||
actionBtn = <Button text={intl.formatMessage(messages.unblock, { name: account.get('username') })} onClick={this.props.onBlock} />; |
||||
} |
||||
} else { |
||||
actionBtn = <Button text={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} />; |
||||
} |
||||
|
||||
if (account.get('moved') && !account.getIn(['relationship', 'following'])) { |
||||
actionBtn = ''; |
||||
} |
||||
|
||||
if (account.get('locked')) { |
||||
lockedIcon = <Icon id='lock' icon={LockIcon} title={intl.formatMessage(messages.account_locked)} />; |
||||
} |
||||
|
||||
if (signedIn && account.get('id') !== me && !account.get('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 (isRemote) { |
||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); |
||||
menu.push(null); |
||||
} |
||||
|
||||
if (account.get('id') === me) { |
||||
menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); |
||||
menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); |
||||
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({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); |
||||
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 (signedIn) { |
||||
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(messages.languages), action: this.props.onChangeLanguages }); |
||||
menu.push(null); |
||||
} |
||||
|
||||
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, dangerous: true }); |
||||
} |
||||
|
||||
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, dangerous: true }); |
||||
} |
||||
|
||||
if (!account.get('suspended')) { |
||||
menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.props.onReport, dangerous: true }); |
||||
} |
||||
} |
||||
|
||||
if (signedIn && isRemote) { |
||||
menu.push(null); |
||||
|
||||
if (account.getIn(['relationship', 'domain_blocking'])) { |
||||
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain: remoteDomain }), action: this.props.onUnblockDomain }); |
||||
} else { |
||||
menu.push({ text: intl.formatMessage(messages.blockDomain, { domain: remoteDomain }), action: this.props.onBlockDomain, dangerous: true }); |
||||
} |
||||
} |
||||
|
||||
if ((account.get('id') !== me && (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { |
||||
menu.push(null); |
||||
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { |
||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); |
||||
} |
||||
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { |
||||
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` }); |
||||
} |
||||
} |
||||
|
||||
const content = { __html: account.get('note_emojified') }; |
||||
const displayNameHtml = { __html: account.get('display_name_html') }; |
||||
const fields = account.get('fields'); |
||||
const isLocal = account.get('acct').indexOf('@') === -1; |
||||
const username = account.get('acct').split('@')[0]; |
||||
const domain = isLocal ? localDomain : account.get('acct').split('@')[1]; |
||||
const isIndexable = !account.get('noindex'); |
||||
|
||||
const badges = []; |
||||
|
||||
if (account.get('bot')) { |
||||
badges.push(<AutomatedBadge key='bot-badge' />); |
||||
} else if (account.get('group')) { |
||||
badges.push(<GroupBadge key='group-badge' />); |
||||
} |
||||
|
||||
account.get('roles', []).forEach((role) => { |
||||
badges.push(<Badge key={`role-badge-${role.get('id')}`} label={<span>{role.get('name')}</span>} domain={domain} roleId={role.get('id')} />); |
||||
}); |
||||
|
||||
return ( |
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> |
||||
{!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />} |
||||
|
||||
<div className='account__header__image'> |
||||
<div className='account__header__info'> |
||||
{info} |
||||
</div> |
||||
|
||||
{!(suspended || hidden) && <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('avatar')} rel='noopener noreferrer' target='_blank' onClick={this.handleAvatarClick}> |
||||
<Avatar account={suspended || hidden ? undefined : account} size={90} /> |
||||
</a> |
||||
|
||||
<div className='account__header__tabs__buttons'> |
||||
{!hidden && bellBtn} |
||||
{!hidden && shareBtn} |
||||
<DropdownMenuContainer disabled={menu.length === 0} items={menu} icon='ellipsis-v' iconComponent={MoreHorizIcon} size={24} direction='right' /> |
||||
{!hidden && actionBtn} |
||||
</div> |
||||
</div> |
||||
|
||||
<div className='account__header__tabs__name'> |
||||
<h1> |
||||
<div> |
||||
<a href={account.get('url')} target='_blank' rel='noopener noreferrer'><span dangerouslySetInnerHTML={displayNameHtml} /></a>{isRemote ? <span> <IconButton className="account-header__open-in-new__icon" icon='open_in_new' iconComponent={OpenInNewIcon} onClick={this.handleDisplayNameClick} /></span> : null} |
||||
</div> |
||||
<small> |
||||
<span>@{username}<span className='invisible'>@{domain}</span></span> |
||||
<DomainPill username={username} domain={domain} isSelf={me === account.get('id')} /> |
||||
{lockedIcon} |
||||
</small> |
||||
</h1> |
||||
</div> |
||||
|
||||
{badges.length > 0 && ( |
||||
<div className='account__header__badges'> |
||||
{badges} |
||||
</div> |
||||
)} |
||||
|
||||
{!(suspended || hidden) && ( |
||||
<div className='account__header__extra'> |
||||
<div className='account__header__bio' ref={this.setRef}> |
||||
{(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />} |
||||
|
||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />} |
||||
|
||||
<div className='account__header__fields'> |
||||
<dl> |
||||
<dt><FormattedMessage id='account.joined_short' defaultMessage='Joined' /></dt> |
||||
<dd>{intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' })}</dd> |
||||
</dl> |
||||
|
||||
{fields.map((pair, i) => ( |
||||
<dl key={i} className={classNames({ verified: pair.get('verified_at') })}> |
||||
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' /> |
||||
|
||||
<dd className='translate' 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' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> |
||||
</dd> |
||||
</dl> |
||||
))} |
||||
</div> |
||||
</div> |
||||
|
||||
<div className='account__header__extra__links'> |
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/@${account.get('acct')}`} title={intl.formatNumber(account.get('statuses_count'))}> |
||||
<ShortNumber |
||||
value={account.get('statuses_count')} |
||||
renderer={StatusesCounter} |
||||
/> |
||||
</NavLink> |
||||
|
||||
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/following`} title={intl.formatNumber(account.get('following_count'))}> |
||||
<ShortNumber |
||||
value={account.get('following_count')} |
||||
renderer={FollowingCounter} |
||||
/> |
||||
</NavLink> |
||||
|
||||
<NavLink exact activeClassName='active' to={`/@${account.get('acct')}/followers`} title={intl.formatNumber(account.get('followers_count'))}> |
||||
<ShortNumber |
||||
value={account.get('followers_count')} |
||||
renderer={FollowersCounter} |
||||
/> |
||||
</NavLink> |
||||
</div> |
||||
</div> |
||||
)} |
||||
</div> |
||||
|
||||
<Helmet> |
||||
<title>{titleFromAccount(account)}</title> |
||||
<meta name='robots' content={(isLocal && isIndexable) ? 'all' : 'noindex'} /> |
||||
<link rel='canonical' href={account.get('url')} /> |
||||
</Helmet> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default withRouter(withIdentity(injectIntl(Header))); |
||||
@ -1,39 +0,0 @@
|
||||
import { FormattedMessage } from 'react-intl'; |
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||
|
||||
import { Permalink } from 'mastodon/components/permalink'; |
||||
|
||||
import { AvatarOverlay } from '../../../components/avatar_overlay'; |
||||
import { DisplayName } from '../../../components/display_name'; |
||||
|
||||
export default class MovedNote extends ImmutablePureComponent { |
||||
|
||||
static propTypes = { |
||||
from: ImmutablePropTypes.map.isRequired, |
||||
to: ImmutablePropTypes.map.isRequired, |
||||
}; |
||||
|
||||
render () { |
||||
const { from, to } = this.props; |
||||
|
||||
return ( |
||||
<div className='moved-account-banner'> |
||||
<div className='moved-account-banner__message'> |
||||
<FormattedMessage id='account.moved_to' defaultMessage='{name} has indicated that their new account is now:' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: from.get('display_name_html') }} /></bdi> }} /> |
||||
</div> |
||||
|
||||
<div className='moved-account-banner__action'> |
||||
<Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='detailed-status__display-name'> |
||||
<div className='detailed-status__display-avatar'><AvatarOverlay account={to} friend={from} /></div> |
||||
<DisplayName account={to} /> |
||||
</Permalink> |
||||
|
||||
<Permalink href={to.get('url')} to={`/@${to.get('acct')}`} className='button'><FormattedMessage id='account.go_to_profile' defaultMessage='Go to profile' /></Permalink> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
@ -1,602 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
import { PureComponent } from 'react'; |
||||
|
||||
import { defineMessages, FormattedMessage, injectIntl } from 'react-intl'; |
||||
|
||||
import classNames from 'classnames'; |
||||
|
||||
import { is } from 'immutable'; |
||||
|
||||
import { throttle, debounce } from 'lodash'; |
||||
|
||||
import DownloadIcon from '@/material-icons/400-24px/download.svg?react'; |
||||
import PauseIcon from '@/material-icons/400-24px/pause.svg?react'; |
||||
import PlayArrowIcon from '@/material-icons/400-24px/play_arrow-fill.svg?react'; |
||||
import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?react'; |
||||
import VolumeOffIcon from '@/material-icons/400-24px/volume_off-fill.svg?react'; |
||||
import VolumeUpIcon from '@/material-icons/400-24px/volume_up-fill.svg?react'; |
||||
import { Icon } from 'mastodon/components/icon'; |
||||
import { formatTime, getPointerPosition, fileNameFromURL } from 'mastodon/features/video'; |
||||
import { AltTextBadge } from 'mastodon/components/alt_text_badge'; |
||||
import { NoAltTextBadge } from 'mastodon/components/no_alt_text_badge'; |
||||
|
||||
import { Blurhash } from '../../components/blurhash'; |
||||
import { displayMedia, useBlurhash } from '../../initial_state'; |
||||
|
||||
import Visualizer from './visualizer'; |
||||
|
||||
const messages = defineMessages({ |
||||
play: { id: 'video.play', defaultMessage: 'Play' }, |
||||
pause: { id: 'video.pause', defaultMessage: 'Pause' }, |
||||
mute: { id: 'video.mute', defaultMessage: 'Mute sound' }, |
||||
unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' }, |
||||
download: { id: 'video.download', defaultMessage: 'Download file' }, |
||||
hide: { id: 'audio.hide', defaultMessage: 'Hide audio' }, |
||||
no_descriptive_text: { id: 'media.no_descriptive_text', defaultMessage: 'No descriptive text was provided for this media.' }, |
||||
}); |
||||
|
||||
const TICK_SIZE = 10; |
||||
const PADDING = 180; |
||||
|
||||
class Audio extends PureComponent { |
||||
|
||||
static propTypes = { |
||||
src: PropTypes.string.isRequired, |
||||
alt: PropTypes.string, |
||||
lang: PropTypes.string, |
||||
poster: PropTypes.string, |
||||
duration: PropTypes.number, |
||||
width: PropTypes.number, |
||||
height: PropTypes.number, |
||||
sensitive: PropTypes.bool, |
||||
editable: PropTypes.bool, |
||||
fullscreen: PropTypes.bool, |
||||
intl: PropTypes.object.isRequired, |
||||
blurhash: PropTypes.string, |
||||
cacheWidth: PropTypes.func, |
||||
visible: PropTypes.bool, |
||||
onToggleVisibility: PropTypes.func, |
||||
backgroundColor: PropTypes.string, |
||||
foregroundColor: PropTypes.string, |
||||
accentColor: PropTypes.string, |
||||
currentTime: PropTypes.number, |
||||
autoPlay: PropTypes.bool, |
||||
volume: PropTypes.number, |
||||
muted: PropTypes.bool, |
||||
deployPictureInPicture: PropTypes.func, |
||||
}; |
||||
|
||||
state = { |
||||
width: this.props.width, |
||||
currentTime: 0, |
||||
buffer: 0, |
||||
duration: null, |
||||
paused: true, |
||||
muted: false, |
||||
volume: 1, |
||||
dragging: false, |
||||
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'), |
||||
}; |
||||
|
||||
constructor (props) { |
||||
super(props); |
||||
this.visualizer = new Visualizer(TICK_SIZE); |
||||
} |
||||
|
||||
setPlayerRef = c => { |
||||
this.player = c; |
||||
|
||||
if (this.player) { |
||||
this._setDimensions(); |
||||
} |
||||
}; |
||||
|
||||
_pack() { |
||||
return { |
||||
src: this.props.src, |
||||
volume: this.state.volume, |
||||
muted: this.state.muted, |
||||
currentTime: this.audio.currentTime, |
||||
poster: this.props.poster, |
||||
backgroundColor: this.props.backgroundColor, |
||||
foregroundColor: this.props.foregroundColor, |
||||
accentColor: this.props.accentColor, |
||||
sensitive: this.props.sensitive, |
||||
visible: this.props.visible, |
||||
}; |
||||
} |
||||
|
||||
_setDimensions () { |
||||
const width = this.player.offsetWidth; |
||||
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9)); |
||||
|
||||
if (this.props.cacheWidth) { |
||||
this.props.cacheWidth(width); |
||||
} |
||||
|
||||
this.setState({ width, height }); |
||||
} |
||||
|
||||
setSeekRef = c => { |
||||
this.seek = c; |
||||
}; |
||||
|
||||
setVolumeRef = c => { |
||||
this.volume = c; |
||||
}; |
||||
|
||||
setAudioRef = c => { |
||||
this.audio = c; |
||||
|
||||
if (this.audio) { |
||||
this.audio.volume = 1; |
||||
this.audio.muted = false; |
||||
} |
||||
}; |
||||
|
||||
setCanvasRef = c => { |
||||
this.canvas = c; |
||||
|
||||
this.visualizer.setCanvas(c); |
||||
}; |
||||
|
||||
componentDidMount () { |
||||
window.addEventListener('scroll', this.handleScroll); |
||||
window.addEventListener('resize', this.handleResize, { passive: true }); |
||||
} |
||||
|
||||
componentDidUpdate (prevProps, prevState) { |
||||
if (prevProps.src !== this.props.src || this.state.width !== prevState.width || this.state.height !== prevState.height || prevProps.accentColor !== this.props.accentColor) { |
||||
this._clear(); |
||||
this._draw(); |
||||
} |
||||
} |
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) { |
||||
if (!is(nextProps.visible, this.props.visible) && nextProps.visible !== undefined) { |
||||
this.setState({ revealed: nextProps.visible }); |
||||
} |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
window.removeEventListener('scroll', this.handleScroll); |
||||
window.removeEventListener('resize', this.handleResize); |
||||
|
||||
if (!this.state.paused && this.audio && this.props.deployPictureInPicture) { |
||||
this.props.deployPictureInPicture('audio', this._pack()); |
||||
} |
||||
} |
||||
|
||||
togglePlay = () => { |
||||
if (!this.audioContext) { |
||||
this._initAudioContext(); |
||||
} |
||||
|
||||
if (this.state.paused) { |
||||
this.setState({ paused: false }, () => this.audio.play()); |
||||
} else { |
||||
this.setState({ paused: true }, () => this.audio.pause()); |
||||
} |
||||
}; |
||||
|
||||
handleResize = debounce(() => { |
||||
if (this.player) { |
||||
this._setDimensions(); |
||||
} |
||||
}, 250, { |
||||
trailing: true, |
||||
}); |
||||
|
||||
handlePlay = () => { |
||||
this.setState({ paused: false }); |
||||
|
||||
if (this.audioContext && this.audioContext.state === 'suspended') { |
||||
this.audioContext.resume(); |
||||
} |
||||
|
||||
this._renderCanvas(); |
||||
}; |
||||
|
||||
handlePause = () => { |
||||
this.setState({ paused: true }); |
||||
|
||||
if (this.audioContext) { |
||||
this.audioContext.suspend(); |
||||
} |
||||
}; |
||||
|
||||
handleProgress = () => { |
||||
const lastTimeRange = this.audio.buffered.length - 1; |
||||
|
||||
if (lastTimeRange > -1) { |
||||
this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) }); |
||||
} |
||||
}; |
||||
|
||||
toggleMute = () => { |
||||
const muted = !(this.state.muted || this.state.volume === 0); |
||||
|
||||
this.setState((state) => ({ muted, volume: Math.max(state.volume || 0.5, 0.05) }), () => { |
||||
if (this.gainNode) { |
||||
this.gainNode.gain.value = this.state.muted ? 0 : this.state.volume; |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
toggleReveal = () => { |
||||
if (this.props.onToggleVisibility) { |
||||
this.props.onToggleVisibility(); |
||||
} else { |
||||
this.setState({ revealed: !this.state.revealed }); |
||||
} |
||||
}; |
||||
|
||||
handleVolumeMouseDown = e => { |
||||
document.addEventListener('mousemove', this.handleMouseVolSlide, true); |
||||
document.addEventListener('mouseup', this.handleVolumeMouseUp, true); |
||||
document.addEventListener('touchmove', this.handleMouseVolSlide, true); |
||||
document.addEventListener('touchend', this.handleVolumeMouseUp, true); |
||||
|
||||
this.handleMouseVolSlide(e); |
||||
|
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
}; |
||||
|
||||
handleVolumeMouseUp = () => { |
||||
document.removeEventListener('mousemove', this.handleMouseVolSlide, true); |
||||
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true); |
||||
document.removeEventListener('touchmove', this.handleMouseVolSlide, true); |
||||
document.removeEventListener('touchend', this.handleVolumeMouseUp, true); |
||||
}; |
||||
|
||||
handleMouseDown = e => { |
||||
document.addEventListener('mousemove', this.handleMouseMove, true); |
||||
document.addEventListener('mouseup', this.handleMouseUp, true); |
||||
document.addEventListener('touchmove', this.handleMouseMove, true); |
||||
document.addEventListener('touchend', this.handleMouseUp, true); |
||||
|
||||
this.setState({ dragging: true }); |
||||
this.audio.pause(); |
||||
this.handleMouseMove(e); |
||||
|
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
}; |
||||
|
||||
handleMouseUp = () => { |
||||
document.removeEventListener('mousemove', this.handleMouseMove, true); |
||||
document.removeEventListener('mouseup', this.handleMouseUp, true); |
||||
document.removeEventListener('touchmove', this.handleMouseMove, true); |
||||
document.removeEventListener('touchend', this.handleMouseUp, true); |
||||
|
||||
this.setState({ dragging: false }); |
||||
this.audio.play(); |
||||
}; |
||||
|
||||
handleMouseMove = throttle(e => { |
||||
const { x } = getPointerPosition(this.seek, e); |
||||
const currentTime = this.audio.duration * x; |
||||
|
||||
if (!isNaN(currentTime)) { |
||||
this.setState({ currentTime }, () => { |
||||
this.audio.currentTime = currentTime; |
||||
}); |
||||
} |
||||
}, 15); |
||||
|
||||
handleTimeUpdate = () => { |
||||
this.setState({ |
||||
currentTime: this.audio.currentTime, |
||||
duration: this.audio.duration, |
||||
}); |
||||
}; |
||||
|
||||
handleMouseVolSlide = throttle(e => { |
||||
const { x } = getPointerPosition(this.volume, e); |
||||
|
||||
if(!isNaN(x)) { |
||||
this.setState((state) => ({ volume: x, muted: state.muted && x === 0 }), () => { |
||||
if (this.gainNode) { |
||||
this.gainNode.gain.value = this.state.muted ? 0 : x; |
||||
} |
||||
}); |
||||
} |
||||
}, 15); |
||||
|
||||
handleScroll = throttle(() => { |
||||
if (!this.canvas || !this.audio) { |
||||
return; |
||||
} |
||||
|
||||
const { top, height } = this.canvas.getBoundingClientRect(); |
||||
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0); |
||||
|
||||
if (!this.state.paused && !inView) { |
||||
this.audio.pause(); |
||||
|
||||
if (this.props.deployPictureInPicture) { |
||||
this.props.deployPictureInPicture('audio', this._pack()); |
||||
} |
||||
|
||||
this.setState({ paused: true }); |
||||
} |
||||
}, 150, { trailing: true }); |
||||
|
||||
handleMouseEnter = () => { |
||||
this.setState({ hovered: true }); |
||||
}; |
||||
|
||||
handleMouseLeave = () => { |
||||
this.setState({ hovered: false }); |
||||
}; |
||||
|
||||
handleLoadedData = () => { |
||||
const { autoPlay, currentTime } = this.props; |
||||
|
||||
if (currentTime) { |
||||
this.audio.currentTime = currentTime; |
||||
} |
||||
|
||||
if (autoPlay) { |
||||
this.togglePlay(); |
||||
} |
||||
}; |
||||
|
||||
_initAudioContext () { |
||||
const AudioContext = window.AudioContext || window.webkitAudioContext; |
||||
const context = new AudioContext(); |
||||
const source = context.createMediaElementSource(this.audio); |
||||
const gainNode = context.createGain(); |
||||
|
||||
gainNode.gain.value = this.state.muted ? 0 : this.state.volume; |
||||
|
||||
this.visualizer.setAudioContext(context, source); |
||||
source.connect(gainNode); |
||||
gainNode.connect(context.destination); |
||||
|
||||
this.audioContext = context; |
||||
this.gainNode = gainNode; |
||||
} |
||||
|
||||
handleDownload = () => { |
||||
fetch(this.props.src).then(res => res.blob()).then(blob => { |
||||
const element = document.createElement('a'); |
||||
const objectURL = URL.createObjectURL(blob); |
||||
|
||||
element.setAttribute('href', objectURL); |
||||
element.setAttribute('download', fileNameFromURL(this.props.src)); |
||||
|
||||
document.body.appendChild(element); |
||||
element.click(); |
||||
document.body.removeChild(element); |
||||
|
||||
URL.revokeObjectURL(objectURL); |
||||
}).catch(err => { |
||||
console.error(err); |
||||
}); |
||||
}; |
||||
|
||||
_renderCanvas () { |
||||
requestAnimationFrame(() => { |
||||
if (!this.audio) return; |
||||
|
||||
this.handleTimeUpdate(); |
||||
this._clear(); |
||||
this._draw(); |
||||
|
||||
if (!this.state.paused) { |
||||
this._renderCanvas(); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
_clear() { |
||||
this.visualizer.clear(this.state.width, this.state.height); |
||||
} |
||||
|
||||
_draw() { |
||||
this.visualizer.draw(this._getCX(), this._getCY(), this._getAccentColor(), this._getRadius(), this._getScaleCoefficient()); |
||||
} |
||||
|
||||
_getRadius () { |
||||
return parseInt((this.state.height || this.props.height) / 2 - PADDING * this._getScaleCoefficient()); |
||||
} |
||||
|
||||
_getScaleCoefficient () { |
||||
return (this.state.height || this.props.height) / 982; |
||||
} |
||||
|
||||
_getCX() { |
||||
return Math.floor(this.state.width / 2); |
||||
} |
||||
|
||||
_getCY() { |
||||
return Math.floor((this.state.height || this.props.height) / 2); |
||||
} |
||||
|
||||
_getAccentColor () { |
||||
return this.props.accentColor || '#ffffff'; |
||||
} |
||||
|
||||
_getBackgroundColor () { |
||||
return this.props.backgroundColor || '#000000'; |
||||
} |
||||
|
||||
_getForegroundColor () { |
||||
return this.props.foregroundColor || '#ffffff'; |
||||
} |
||||
|
||||
seekBy (time) { |
||||
const currentTime = this.audio.currentTime + time; |
||||
|
||||
if (!isNaN(currentTime)) { |
||||
this.setState({ currentTime }, () => { |
||||
this.audio.currentTime = currentTime; |
||||
}); |
||||
} |
||||
} |
||||
|
||||
handleAudioKeyDown = e => { |
||||
// On the audio element or the seek bar, we can safely use the space bar |
||||
// for playback control because there are no buttons to press |
||||
|
||||
if (e.key === ' ') { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
this.togglePlay(); |
||||
} |
||||
}; |
||||
|
||||
handleKeyDown = e => { |
||||
switch(e.key) { |
||||
case 'k': |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
this.togglePlay(); |
||||
break; |
||||
case 'm': |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
this.toggleMute(); |
||||
break; |
||||
case 'j': |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
this.seekBy(-10); |
||||
break; |
||||
case 'l': |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
this.seekBy(10); |
||||
break; |
||||
} |
||||
}; |
||||
|
||||
render () { |
||||
const { src, intl, alt, lang, editable, autoPlay, sensitive, blurhash } = this.props; |
||||
const { paused, volume, currentTime, duration, buffer, dragging, revealed } = this.state; |
||||
const progress = Math.min((currentTime / duration) * 100, 100); |
||||
const muted = this.state.muted || volume === 0; |
||||
|
||||
let warning; |
||||
|
||||
if (sensitive) { |
||||
warning = <FormattedMessage id='status.sensitive_warning' defaultMessage='Sensitive content' />; |
||||
} else { |
||||
warning = <FormattedMessage id='status.media_hidden' defaultMessage='Media hidden' />; |
||||
} |
||||
|
||||
return ( |
||||
<div className={classNames('audio-player', { editable, inactive: !revealed, 'media-missing-description': !alt })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), aspectRatio: '16 / 9' }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} tabIndex={0} onKeyDown={this.handleKeyDown}> |
||||
|
||||
<Blurhash |
||||
hash={blurhash} |
||||
className={classNames('media-gallery__preview', { |
||||
'media-gallery__preview--hidden': revealed, |
||||
})} |
||||
dummy={!useBlurhash} |
||||
/> |
||||
|
||||
{(revealed || editable) && <audio |
||||
src={src} |
||||
ref={this.setAudioRef} |
||||
preload={autoPlay ? 'auto' : 'none'} |
||||
onPlay={this.handlePlay} |
||||
onPause={this.handlePause} |
||||
onProgress={this.handleProgress} |
||||
onLoadedData={this.handleLoadedData} |
||||
crossOrigin='anonymous' |
||||
/>} |
||||
|
||||
<canvas |
||||
role='button' |
||||
tabIndex={0} |
||||
className='audio-player__canvas' |
||||
width={this.state.width} |
||||
height={this.state.height} |
||||
style={{ width: '100%', position: 'absolute', top: 0, left: 0 }} |
||||
ref={this.setCanvasRef} |
||||
onClick={this.togglePlay} |
||||
onKeyDown={this.handleAudioKeyDown} |
||||
title={alt} |
||||
aria-label={alt} |
||||
lang={lang} |
||||
/> |
||||
|
||||
<div className={classNames('spoiler-button', { 'spoiler-button--hidden': revealed || editable })}> |
||||
<button type='button' className='spoiler-button__overlay' onClick={this.toggleReveal}> |
||||
<span className='spoiler-button__overlay__label'> |
||||
{warning} |
||||
<span className='spoiler-button__overlay__action'><FormattedMessage id='status.media.show' defaultMessage='Click to show' /></span> |
||||
</span> |
||||
</button> |
||||
</div> |
||||
|
||||
{(revealed || editable) && <img |
||||
src={this.props.poster} |
||||
alt='' |
||||
style={{ |
||||
position: 'absolute', |
||||
left: '50%', |
||||
top: '50%', |
||||
height: `calc(${(100 - 2 * 100 * PADDING / 982)}% - ${TICK_SIZE * 2}px)`, |
||||
aspectRatio: '1', |
||||
transform: 'translate(-50%, -50%)', |
||||
borderRadius: '50%', |
||||
pointerEvents: 'none', |
||||
}} |
||||
/>} |
||||
|
||||
<div className='video-player__seek' onMouseDown={this.handleMouseDown} ref={this.setSeekRef}> |
||||
<div className='video-player__seek__buffer' style={{ width: `${buffer}%` }} /> |
||||
<div className='video-player__seek__progress' style={{ width: `${progress}%`, backgroundColor: this._getAccentColor() }} /> |
||||
|
||||
<span |
||||
className={classNames('video-player__seek__handle', { active: dragging })} |
||||
tabIndex={0} |
||||
style={{ left: `${progress}%`, backgroundColor: this._getAccentColor() }} |
||||
onKeyDown={this.handleAudioKeyDown} |
||||
/> |
||||
</div> |
||||
|
||||
<div className='video-player__controls active'> |
||||
<div className='video-player__buttons-bar'> |
||||
<div className='video-player__buttons left'> |
||||
<button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay}><Icon id={paused ? 'play' : 'pause'} icon={paused ? PlayArrowIcon : PauseIcon} /></button> |
||||
<button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} icon={muted ? VolumeOffIcon : VolumeUpIcon} /></button> |
||||
|
||||
<div className={classNames('video-player__volume', { active: this.state.hovered })} ref={this.setVolumeRef} onMouseDown={this.handleVolumeMouseDown}> |
||||
<div className='video-player__volume__current' style={{ width: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} /> |
||||
|
||||
<span |
||||
className='video-player__volume__handle' |
||||
tabIndex={0} |
||||
style={{ left: `${muted ? 0 : volume * 100}%`, backgroundColor: this._getAccentColor() }} |
||||
/> |
||||
</div> |
||||
|
||||
<span className='video-player__time'> |
||||
<span className='video-player__time-current'>{formatTime(Math.floor(currentTime))}</span> |
||||
<span className='video-player__time-sep'>/</span> |
||||
<span className='video-player__time-total'>{formatTime(Math.floor(this.state.duration || this.props.duration))}</span> |
||||
</span> |
||||
</div> |
||||
|
||||
<div className='video-player__buttons right'> |
||||
{alt && <button type='button' title={intl.formatMessage(messages.no_descriptive_text)} aria-label={intl.formatMessage(messages.no_descriptive_text)} className='player-button no-action media__no-description-icon' ><AltTextBadge key='alt' description={alt} /></button>} |
||||
{!alt && <button type='button' title={intl.formatMessage(messages.no_descriptive_text)} aria-label={intl.formatMessage(messages.no_descriptive_text)} className='player-button no-action media__no-description-icon' ><NoAltTextBadge key='no-alt' /></button>} |
||||
{!editable && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} className='player-button' onClick={this.toggleReveal}><Icon id='eye-slash' icon={VisibilityOffIcon} /></button>} |
||||
<a title={intl.formatMessage(messages.download)} aria-label={intl.formatMessage(messages.download)} className='video-player__download__icon player-button' href={this.props.src} download> |
||||
<Icon id={'download'} icon={DownloadIcon} /> |
||||
</a> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default injectIntl(Audio); |
||||
@ -1,159 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
import { PureComponent } from 'react'; |
||||
|
||||
import { injectIntl, defineMessages } from 'react-intl'; |
||||
|
||||
import { Helmet } from 'react-helmet'; |
||||
import { Link } from 'react-router-dom'; |
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import spring from 'react-motion/lib/spring'; |
||||
|
||||
import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; |
||||
import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; |
||||
import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; |
||||
import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; |
||||
import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; |
||||
import PublicIcon from '@/material-icons/400-24px/public.svg?react'; |
||||
import SettingsIcon from '@/material-icons/400-24px/settings-fill.svg?react'; |
||||
import { openModal } from 'mastodon/actions/modal'; |
||||
import Column from 'mastodon/components/column'; |
||||
import ColumnHeader from 'mastodon/components/column_header'; |
||||
import { Icon } from 'mastodon/components/icon'; |
||||
|
||||
import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; |
||||
import { changeComposing, mountCompose, unmountCompose } from '../../actions/compose'; |
||||
import { mascot } from '../../initial_state'; |
||||
import { isMobile } from '../../is_mobile'; |
||||
import Motion from '../ui/util/optional_motion'; |
||||
|
||||
import { SearchResults } from './components/search_results'; |
||||
import ComposeFormContainer from './containers/compose_form_container'; |
||||
import SearchContainer from './containers/search_container'; |
||||
|
||||
const messages = defineMessages({ |
||||
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, |
||||
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, |
||||
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, |
||||
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, |
||||
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, |
||||
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, |
||||
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, |
||||
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, |
||||
}); |
||||
|
||||
const mapStateToProps = (state, ownProps) => ({ |
||||
columns: state.getIn(['settings', 'columns']), |
||||
showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false, |
||||
}); |
||||
|
||||
class Compose extends PureComponent { |
||||
|
||||
static propTypes = { |
||||
dispatch: PropTypes.func.isRequired, |
||||
columns: ImmutablePropTypes.list.isRequired, |
||||
multiColumn: PropTypes.bool, |
||||
showSearch: PropTypes.bool, |
||||
intl: PropTypes.object.isRequired, |
||||
}; |
||||
|
||||
componentDidMount () { |
||||
const { dispatch } = this.props; |
||||
dispatch(mountCompose()); |
||||
} |
||||
|
||||
componentWillUnmount () { |
||||
const { dispatch } = this.props; |
||||
dispatch(unmountCompose()); |
||||
} |
||||
|
||||
handleLogoutClick = e => { |
||||
const { dispatch } = this.props; |
||||
|
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
|
||||
dispatch(openModal({ modalType: 'CONFIRM_LOG_OUT' })); |
||||
|
||||
return false; |
||||
}; |
||||
|
||||
onFocus = () => { |
||||
this.props.dispatch(changeComposing(true)); |
||||
}; |
||||
|
||||
onBlur = () => { |
||||
this.props.dispatch(changeComposing(false)); |
||||
}; |
||||
|
||||
render () { |
||||
const { multiColumn, showSearch, intl } = this.props; |
||||
|
||||
if (multiColumn) { |
||||
const { columns } = this.props; |
||||
|
||||
return ( |
||||
<div className='drawer' role='region' aria-label={intl.formatMessage(messages.compose)}> |
||||
<nav className='drawer__header'> |
||||
<Link to='/getting-started' className='drawer__tab' title={intl.formatMessage(messages.start)} aria-label={intl.formatMessage(messages.start)}><Icon id='bars' icon={MenuIcon} /></Link> |
||||
{!columns.some(column => column.get('id') === 'HOME') && ( |
||||
<Link to='/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' icon={HomeIcon} /></Link> |
||||
)} |
||||
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( |
||||
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' icon={NotificationsIcon} /></Link> |
||||
)} |
||||
{!columns.some(column => column.get('id') === 'COMMUNITY') && ( |
||||
<Link to='/public/local' className='drawer__tab' title={intl.formatMessage(messages.community)} aria-label={intl.formatMessage(messages.community)}><Icon id='users' icon={PeopleIcon} /></Link> |
||||
)} |
||||
{!columns.some(column => column.get('id') === 'PUBLIC') && ( |
||||
<Link to='/public' className='drawer__tab' title={intl.formatMessage(messages.public)} aria-label={intl.formatMessage(messages.public)}><Icon id='globe' icon={PublicIcon} /></Link> |
||||
)} |
||||
<a href='/settings/preferences' className='drawer__tab' title={intl.formatMessage(messages.preferences)} aria-label={intl.formatMessage(messages.preferences)}><Icon id='cog' icon={SettingsIcon} /></a> |
||||
<a href='/auth/sign_out' className='drawer__tab' title={intl.formatMessage(messages.logout)} aria-label={intl.formatMessage(messages.logout)} onClick={this.handleLogoutClick}><Icon id='sign-out' icon={LogoutIcon} /></a> |
||||
</nav> |
||||
|
||||
{multiColumn && <SearchContainer /> } |
||||
|
||||
<div className='drawer__pager'> |
||||
<div className='drawer__inner' onFocus={this.onFocus}> |
||||
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} /> |
||||
|
||||
<div className='drawer__inner__mastodon'> |
||||
<img alt='' draggable='false' src={mascot || elephantUIPlane} /> |
||||
</div> |
||||
</div> |
||||
|
||||
<Motion defaultStyle={{ x: -100 }} style={{ x: spring(showSearch ? 0 : -100, { stiffness: 210, damping: 20 }) }}> |
||||
{({ x }) => ( |
||||
<div className='drawer__inner darker' style={{ transform: `translateX(${x}%)`, visibility: x === -100 ? 'hidden' : 'visible' }}> |
||||
<SearchResults /> |
||||
</div> |
||||
)} |
||||
</Motion> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<Column onFocus={this.onFocus}> |
||||
<ColumnHeader |
||||
icon='pencil' |
||||
title={intl.formatMessage(messages.compose)} |
||||
onClick={this.handleHeaderClick} |
||||
multiColumn={multiColumn} |
||||
/> |
||||
<ComposeFormContainer /> |
||||
|
||||
<Helmet> |
||||
<meta name='robots' content='noindex' /> |
||||
</Helmet> |
||||
</Column> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default connect(mapStateToProps)(injectIntl(Compose)); |
||||
@ -1,114 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
import { PureComponent } from 'react'; |
||||
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; |
||||
|
||||
import { Helmet } from 'react-helmet'; |
||||
import { NavLink, Switch, Route } from 'react-router-dom'; |
||||
|
||||
import { connect } from 'react-redux'; |
||||
|
||||
import ExploreIcon from '@/material-icons/400-24px/explore.svg?react'; |
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react'; |
||||
import Column from 'mastodon/components/column'; |
||||
import ColumnHeader from 'mastodon/components/column_header'; |
||||
import Search from 'mastodon/features/compose/containers/search_container'; |
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; |
||||
import { trendsEnabled } from 'mastodon/initial_state'; |
||||
|
||||
import Links from './links'; |
||||
import SearchResults from './results'; |
||||
import Statuses from './statuses'; |
||||
import Suggestions from './suggestions'; |
||||
import Tags from './tags'; |
||||
|
||||
const messages = defineMessages({ |
||||
title: { id: 'explore.title', defaultMessage: 'Explore' }, |
||||
searchResults: { id: 'explore.search_results', defaultMessage: 'Search results' }, |
||||
}); |
||||
|
||||
const mapStateToProps = state => ({ |
||||
layout: state.getIn(['meta', 'layout']), |
||||
isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled, |
||||
}); |
||||
|
||||
class Explore extends PureComponent { |
||||
static propTypes = { |
||||
identity: identityContextPropShape, |
||||
intl: PropTypes.object.isRequired, |
||||
multiColumn: PropTypes.bool, |
||||
isSearching: PropTypes.bool, |
||||
}; |
||||
|
||||
handleHeaderClick = () => { |
||||
this.column.scrollTop(); |
||||
}; |
||||
|
||||
setRef = c => { |
||||
this.column = c; |
||||
}; |
||||
|
||||
render() { |
||||
const { intl, multiColumn, isSearching } = this.props; |
||||
const { signedIn } = this.props.identity; |
||||
|
||||
return ( |
||||
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.title)}> |
||||
<ColumnHeader |
||||
icon='search' |
||||
iconComponent={SearchIcon} |
||||
title={intl.formatMessage(isSearching ? messages.searchResults : messages.title)} |
||||
onClick={this.handleHeaderClick} |
||||
multiColumn={multiColumn} |
||||
/> |
||||
|
||||
<div className='explore__search-header'> |
||||
<Search /> |
||||
</div> |
||||
|
||||
{isSearching ? ( |
||||
<SearchResults /> |
||||
) : ( |
||||
<> |
||||
<div className='account__section-headline'> |
||||
<NavLink exact to='/explore'> |
||||
<FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' /> |
||||
</NavLink> |
||||
|
||||
<NavLink exact to='/explore/tags'> |
||||
<FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' /> |
||||
</NavLink> |
||||
|
||||
{signedIn && ( |
||||
<NavLink exact to='/explore/suggestions'> |
||||
<FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='People' /> |
||||
</NavLink> |
||||
)} |
||||
|
||||
<NavLink exact to='/explore/links'> |
||||
<FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' /> |
||||
</NavLink> |
||||
</div> |
||||
|
||||
<Switch> |
||||
<Route path='/explore/tags' component={Tags} /> |
||||
<Route path='/explore/links' component={Links} /> |
||||
<Route path='/explore/suggestions' component={Suggestions} /> |
||||
<Route exact path={['/explore', '/explore/posts', '/search']}> |
||||
<Statuses multiColumn={multiColumn} /> |
||||
</Route> |
||||
</Switch> |
||||
|
||||
<Helmet> |
||||
<title>{intl.formatMessage(messages.title)}</title> |
||||
<meta name='robots' content={isSearching ? 'noindex' : 'all'} /> |
||||
</Helmet> |
||||
</> |
||||
)} |
||||
</Column> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default withIdentity(connect(mapStateToProps)(injectIntl(Explore))); |
||||
@ -1,186 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl'; |
||||
|
||||
import classNames from 'classnames'; |
||||
import { withRouter } from 'react-router-dom'; |
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes'; |
||||
import ImmutablePureComponent from 'react-immutable-pure-component'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import OpenInNewIcon from '@/material-icons/400-24px/open_in_new.svg?react'; |
||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; |
||||
import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; |
||||
import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; |
||||
import StarIcon from '@/material-icons/400-24px/star.svg?react'; |
||||
import { replyCompose } from 'mastodon/actions/compose'; |
||||
import { toggleReblog, toggleFavourite } from 'mastodon/actions/interactions'; |
||||
import { openModal } from 'mastodon/actions/modal'; |
||||
import { IconButton } from 'mastodon/components/icon_button'; |
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context'; |
||||
import { me } from 'mastodon/initial_state'; |
||||
import { makeGetStatus } from 'mastodon/selectors'; |
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router'; |
||||
|
||||
const messages = defineMessages({ |
||||
reply: { id: 'status.reply', defaultMessage: 'Reply' }, |
||||
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, |
||||
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, |
||||
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, |
||||
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, |
||||
cannot_reblog: { id: 'status.cannot_reblog', defaultMessage: 'This post cannot be boosted' }, |
||||
favourite: { id: 'status.favourite', defaultMessage: 'Favorite' }, |
||||
open: { id: 'status.open', defaultMessage: 'Expand this status' }, |
||||
}); |
||||
|
||||
const makeMapStateToProps = () => { |
||||
const getStatus = makeGetStatus(); |
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({ |
||||
status: getStatus(state, { id: statusId }), |
||||
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, |
||||
}); |
||||
|
||||
return mapStateToProps; |
||||
}; |
||||
|
||||
class Footer extends ImmutablePureComponent { |
||||
static propTypes = { |
||||
identity: identityContextPropShape, |
||||
statusId: PropTypes.string.isRequired, |
||||
status: ImmutablePropTypes.map.isRequired, |
||||
intl: PropTypes.object.isRequired, |
||||
dispatch: PropTypes.func.isRequired, |
||||
askReplyConfirmation: PropTypes.bool, |
||||
withOpenButton: PropTypes.bool, |
||||
onClose: PropTypes.func, |
||||
...WithRouterPropTypes, |
||||
}; |
||||
|
||||
_performReply = () => { |
||||
const { dispatch, status, onClose } = this.props; |
||||
|
||||
if (onClose) { |
||||
onClose(true); |
||||
} |
||||
|
||||
dispatch(replyCompose(status)); |
||||
}; |
||||
|
||||
handleReplyClick = () => { |
||||
const { dispatch, askReplyConfirmation, status, onClose } = this.props; |
||||
const { signedIn } = this.props.identity; |
||||
|
||||
if (signedIn) { |
||||
if (askReplyConfirmation) { |
||||
onClose(true); |
||||
dispatch(openModal({ modalType: 'CONFIRM_REPLY', modalProps: { status } })); |
||||
} else { |
||||
this._performReply(); |
||||
} |
||||
} else { |
||||
dispatch(openModal({ |
||||
modalType: 'INTERACTION', |
||||
modalProps: { |
||||
type: 'reply', |
||||
accountId: status.getIn(['account', 'id']), |
||||
url: status.get('uri'), |
||||
}, |
||||
})); |
||||
} |
||||
}; |
||||
|
||||
handleFavouriteClick = () => { |
||||
const { dispatch, status } = this.props; |
||||
const { signedIn } = this.props.identity; |
||||
|
||||
if (signedIn) { |
||||
dispatch(toggleFavourite(status.get('id'))); |
||||
} else { |
||||
dispatch(openModal({ |
||||
modalType: 'INTERACTION', |
||||
modalProps: { |
||||
type: 'favourite', |
||||
accountId: status.getIn(['account', 'id']), |
||||
url: status.get('uri'), |
||||
}, |
||||
})); |
||||
} |
||||
}; |
||||
|
||||
handleReblogClick = e => { |
||||
const { dispatch, status } = this.props; |
||||
const { signedIn } = this.props.identity; |
||||
|
||||
if (signedIn) { |
||||
dispatch(toggleReblog(status.get('id'), e && e.shiftKey)); |
||||
} else { |
||||
dispatch(openModal({ |
||||
modalType: 'INTERACTION', |
||||
modalProps: { |
||||
type: 'reblog', |
||||
accountId: status.getIn(['account', 'id']), |
||||
url: status.get('uri'), |
||||
}, |
||||
})); |
||||
} |
||||
}; |
||||
|
||||
handleOpenClick = e => { |
||||
if (e.button !== 0 || !history) { |
||||
return; |
||||
} |
||||
|
||||
const { status, onClose } = this.props; |
||||
|
||||
if (onClose) { |
||||
onClose(); |
||||
} |
||||
|
||||
this.props.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`); |
||||
}; |
||||
|
||||
render () { |
||||
const { status, intl, withOpenButton } = this.props; |
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); |
||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; |
||||
|
||||
let replyIcon, replyIconComponent, replyTitle; |
||||
|
||||
if (status.get('in_reply_to_id', null) === null) { |
||||
replyIcon = 'reply'; |
||||
replyIconComponent = ReplyIcon; |
||||
replyTitle = intl.formatMessage(messages.reply); |
||||
} else { |
||||
replyIcon = 'reply-all'; |
||||
replyIconComponent = ReplyAllIcon; |
||||
replyTitle = intl.formatMessage(messages.replyAll); |
||||
} |
||||
|
||||
let reblogTitle = ''; |
||||
|
||||
if (status.get('reblogged')) { |
||||
reblogTitle = intl.formatMessage(messages.cancel_reblog_private); |
||||
} else if (publicStatus) { |
||||
reblogTitle = intl.formatMessage(messages.reblog); |
||||
} else if (reblogPrivate) { |
||||
reblogTitle = intl.formatMessage(messages.reblog_private); |
||||
} else { |
||||
reblogTitle = intl.formatMessage(messages.cannot_reblog); |
||||
} |
||||
|
||||
return ( |
||||
<div className='picture-in-picture__footer'> |
||||
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} iconComponent={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? ReplyIcon : replyIconComponent} onClick={this.handleReplyClick} counter={status.get('replies_count')} /> |
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' iconComponent={RepeatIcon} onClick={this.handleReblogClick} counter={status.get('reblogs_count')} /> |
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' iconComponent={StarIcon} onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} /> |
||||
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' iconComponent={OpenInNewIcon} onClick={this.handleOpenClick} href={status.get('url')} />} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
} |
||||
|
||||
export default connect(makeMapStateToProps)(withIdentity(withRouter(injectIntl(Footer)))); |
||||
@ -1,59 +0,0 @@
|
||||
import PropTypes from 'prop-types'; |
||||
|
||||
import classNames from 'classnames'; |
||||
import { useRouteMatch, NavLink } from 'react-router-dom'; |
||||
|
||||
import { Icon } from 'mastodon/components/icon'; |
||||
|
||||
const ColumnLink = ({ icon, activeIcon, iconComponent, activeIconComponent, text, to, href, method, badge, transparent, button, onClick, optional, ...other }) => { |
||||
const match = useRouteMatch(to); |
||||
const className = classNames('column-link', { 'column-link--transparent': transparent, 'column-link--button': button, 'column-link--optional': optional }); |
||||
const badgeElement = typeof badge !== 'undefined' ? <span className='column-link__badge'>{badge}</span> : null; |
||||
const iconElement = (typeof icon === 'string' || iconComponent) ? <Icon id={icon} icon={iconComponent} className='column-link__icon' /> : icon; |
||||
const activeIconElement = activeIcon ?? (activeIconComponent ? <Icon id={icon} icon={activeIconComponent} className='column-link__icon' /> : iconElement); |
||||
const active = match?.isExact; |
||||
|
||||
if (href) { |
||||
return ( |
||||
<a href={href} className={className} data-method={method} title={text} {...other}> |
||||
{active ? activeIconElement : iconElement} |
||||
<span>{text}</span> |
||||
{badgeElement} |
||||
</a> |
||||
); |
||||
} else if (button) { |
||||
return ( |
||||
<button className={className} onClick={onClick} title={text} {...other}> |
||||
{iconElement} |
||||
<span>{text}</span> |
||||
{badgeElement} |
||||
</button> |
||||
); |
||||
} else { |
||||
return ( |
||||
<NavLink to={to} className={className} title={text} exact {...other}> |
||||
{active ? activeIconElement : iconElement} |
||||
<span>{text}</span> |
||||
{badgeElement} |
||||
</NavLink> |
||||
); |
||||
} |
||||
}; |
||||
|
||||
ColumnLink.propTypes = { |
||||
icon: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, |
||||
iconComponent: PropTypes.func, |
||||
activeIcon: PropTypes.node, |
||||
activeIconComponent: PropTypes.func, |
||||
text: PropTypes.string.isRequired, |
||||
to: PropTypes.string, |
||||
href: PropTypes.string, |
||||
method: PropTypes.string, |
||||
badge: PropTypes.node, |
||||
transparent: PropTypes.bool, |
||||
button: PropTypes.bool, |
||||
onClick: PropTypes.func, |
||||
optional: PropTypes.bool, |
||||
}; |
||||
|
||||
export default ColumnLink; |
||||
Loading…
Reference in new issue