Merge branch 'master' into glitch-soc/merge-upstream
Conflicts: - `Gemfile.lock`: Not a real conflict, upstream updated dependencies that were too close to glitch-soc-only ones in the file. - `app/controllers/oauth/authorized_applications_controller.rb`: Upstream changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's theming system. Ported upstream changes. - `app/controllers/settings/base_controller.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's theming system. Ported upstream changes. - `app/controllers/settings/sessions_controller.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's theming system. Ported upstream changes. - `app/models/user.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc not preventing moved accounts from logging in. Ported upstream changes while keeping the ability for moved accounts to log in. - `app/policies/status_policy.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's local-only toots. Ported upstream changes. - `app/serializers/rest/account_serializer.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's ability to hide followers count. Ported upstream changes. - `app/services/process_mentions_service.rb`: Upstream refactored and changed the logic surrounding suspended accounts. Minor conflict due to glitch-soc's local-only toots. Ported upstream changes. - `package.json`: Not a real conflict, upstream updated dependencies that were too close to glitch-soc-only ones in the file.
This commit is contained in:
@ -7,6 +7,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { autoPlayGif, me, isStaff } from 'mastodon/initial_state';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import { counterRenderer } from 'mastodon/components/common_counter';
|
||||
import ShortNumber from 'mastodon/components/short_number';
|
||||
@ -35,6 +36,8 @@ const messages = defineMessages({
|
||||
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' },
|
||||
@ -68,8 +71,9 @@ class Header extends ImmutablePureComponent {
|
||||
onBlock: PropTypes.func.isRequired,
|
||||
onMention: PropTypes.func.isRequired,
|
||||
onDirect: PropTypes.func.isRequired,
|
||||
onReport: 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,
|
||||
@ -140,8 +144,11 @@ class Header extends ImmutablePureComponent {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suspended = account.get('suspended');
|
||||
|
||||
let info = [];
|
||||
let actionBtn = '';
|
||||
let bellBtn = '';
|
||||
let lockedIcon = '';
|
||||
let menu = [];
|
||||
|
||||
@ -171,6 +178,10 @@ class Header extends ImmutablePureComponent {
|
||||
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 = '';
|
||||
}
|
||||
@ -268,7 +279,7 @@ class Header extends ImmutablePureComponent {
|
||||
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
|
||||
<div className='account__header__image'>
|
||||
<div className='account__header__info'>
|
||||
{info}
|
||||
{!suspended && info}
|
||||
</div>
|
||||
|
||||
<img src={autoPlayGif ? account.get('header') : account.get('header_static')} alt='' className='parallax' />
|
||||
@ -282,11 +293,14 @@ class Header extends ImmutablePureComponent {
|
||||
|
||||
<div className='spacer' />
|
||||
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{actionBtn}
|
||||
{!suspended && (
|
||||
<div className='account__header__tabs__buttons'>
|
||||
{actionBtn}
|
||||
{bellBtn}
|
||||
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
<DropdownMenuContainer items={menu} icon='ellipsis-v' size={24} direction='right' />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className='account__header__tabs__name'>
|
||||
@ -298,7 +312,7 @@ class Header extends ImmutablePureComponent {
|
||||
|
||||
<div className='account__header__extra'>
|
||||
<div className='account__header__bio'>
|
||||
{ (fields.size > 0 || identity_proofs.size > 0) && (
|
||||
{(fields.size > 0 || identity_proofs.size > 0) && (
|
||||
<div className='account__header__fields'>
|
||||
{identity_proofs.map((proof, i) => (
|
||||
<dl key={i}>
|
||||
@ -324,33 +338,35 @@ class Header extends ImmutablePureComponent {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{account.get('id') !== me && <AccountNoteContainer account={account} />}
|
||||
{account.get('id') !== me && !suspended && <AccountNoteContainer account={account} />}
|
||||
|
||||
{account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content' dangerouslySetInnerHTML={content} />}
|
||||
</div>
|
||||
|
||||
<div className='account__header__extra__links'>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('statuses_count')}
|
||||
renderer={counterRenderer('statuses')}
|
||||
/>
|
||||
</NavLink>
|
||||
{!suspended && (
|
||||
<div className='account__header__extra__links'>
|
||||
<NavLink isActive={this.isStatusesPageActive} activeClassName='active' to={`/accounts/${account.get('id')}`} title={intl.formatNumber(account.get('statuses_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('statuses_count')}
|
||||
renderer={counterRenderer('statuses')}
|
||||
/>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('following_count')}
|
||||
renderer={counterRenderer('following')}
|
||||
/>
|
||||
</NavLink>
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/following`} title={intl.formatNumber(account.get('following_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('following_count')}
|
||||
renderer={counterRenderer('following')}
|
||||
/>
|
||||
</NavLink>
|
||||
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('followers_count')}
|
||||
renderer={counterRenderer('followers')}
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
<NavLink exact activeClassName='active' to={`/accounts/${account.get('id')}/followers`} title={intl.formatNumber(account.get('followers_count'))}>
|
||||
<ShortNumber
|
||||
value={account.get('followers_count')}
|
||||
renderer={counterRenderer('followers')}
|
||||
/>
|
||||
</NavLink>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -15,12 +15,15 @@ import { ScrollContainer } from 'react-router-scroll-4';
|
||||
import LoadMore from 'mastodon/components/load_more';
|
||||
import MissingIndicator from 'mastodon/components/missing_indicator';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const mapStateToProps = (state, props) => ({
|
||||
isAccount: !!state.getIn(['accounts', props.params.accountId]),
|
||||
attachments: getAccountGallery(state, props.params.accountId),
|
||||
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', props.params.accountId, 'suspended'], false),
|
||||
blockedBy: state.getIn(['relationships', props.params.accountId, 'blocked_by'], false),
|
||||
});
|
||||
|
||||
class LoadMoreMedia extends ImmutablePureComponent {
|
||||
@ -56,6 +59,8 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
isLoading: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
};
|
||||
|
||||
@ -119,7 +124,7 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn } = this.props;
|
||||
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
|
||||
const { width } = this.state;
|
||||
|
||||
if (!isAccount) {
|
||||
@ -152,15 +157,21 @@ class AccountGallery extends ImmutablePureComponent {
|
||||
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
|
||||
<HeaderContainer accountId={this.props.params.accountId} />
|
||||
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
{(suspended || blockedBy) ? (
|
||||
<div className='empty-column-indicator'>
|
||||
<FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />
|
||||
</div>
|
||||
) : (
|
||||
<div role='feed' className='account-gallery__container' ref={this.handleRef}>
|
||||
{attachments.map((attachment, index) => attachment === null ? (
|
||||
<LoadMoreMedia key={'more:' + attachments.getIn(index + 1, 'id')} maxId={index > 0 ? attachments.getIn(index - 1, 'id') : null} onLoadMore={this.handleLoadMore} />
|
||||
) : (
|
||||
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
|
||||
))}
|
||||
|
||||
{loadOlder}
|
||||
</div>
|
||||
{loadOlder}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading && attachments.size === 0 && (
|
||||
<div className='scrollable__append'>
|
||||
|
@ -55,6 +55,10 @@ export default class Header extends ImmutablePureComponent {
|
||||
this.props.onReblogToggle(this.props.account);
|
||||
}
|
||||
|
||||
handleNotifyToggle = () => {
|
||||
this.props.onNotifyToggle(this.props.account);
|
||||
}
|
||||
|
||||
handleMute = () => {
|
||||
this.props.onMute(this.props.account);
|
||||
}
|
||||
@ -106,6 +110,7 @@ export default class Header extends ImmutablePureComponent {
|
||||
onMention={this.handleMention}
|
||||
onDirect={this.handleDirect}
|
||||
onReblogToggle={this.handleReblogToggle}
|
||||
onNotifyToggle={this.handleNotifyToggle}
|
||||
onReport={this.handleReport}
|
||||
onMute={this.handleMute}
|
||||
onBlockDomain={this.handleBlockDomain}
|
||||
|
@ -76,9 +76,9 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
|
||||
onReblogToggle (account) {
|
||||
if (account.getIn(['relationship', 'showing_reblogs'])) {
|
||||
dispatch(followAccount(account.get('id'), false));
|
||||
dispatch(followAccount(account.get('id'), { reblogs: false }));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id'), true));
|
||||
dispatch(followAccount(account.get('id'), { reblogs: true }));
|
||||
}
|
||||
},
|
||||
|
||||
@ -90,6 +90,14 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||
}
|
||||
},
|
||||
|
||||
onNotifyToggle (account) {
|
||||
if (account.getIn(['relationship', 'notifying'])) {
|
||||
dispatch(followAccount(account.get('id'), { notify: false }));
|
||||
} else {
|
||||
dispatch(followAccount(account.get('id'), { notify: true }));
|
||||
}
|
||||
},
|
||||
|
||||
onReport (account) {
|
||||
dispatch(initReport(account));
|
||||
},
|
||||
|
@ -31,6 +31,7 @@ const mapStateToProps = (state, { params: { accountId }, withReplies = false })
|
||||
featuredStatusIds: withReplies ? ImmutableList() : state.getIn(['timelines', `account:${accountId}:pinned`, 'items'], emptyList),
|
||||
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
|
||||
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
|
||||
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
|
||||
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
|
||||
};
|
||||
};
|
||||
@ -57,6 +58,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
withReplies: PropTypes.bool,
|
||||
blockedBy: PropTypes.bool,
|
||||
isAccount: PropTypes.bool,
|
||||
suspended: PropTypes.bool,
|
||||
remote: PropTypes.bool,
|
||||
remoteUrl: PropTypes.string,
|
||||
multiColumn: PropTypes.bool,
|
||||
@ -113,7 +115,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
||||
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
|
||||
|
||||
if (!isAccount) {
|
||||
return (
|
||||
@ -134,7 +136,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
|
||||
let emptyMessage;
|
||||
|
||||
if (blockedBy) {
|
||||
if (suspended || blockedBy) {
|
||||
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
|
||||
} else if (remote && statusIds.isEmpty()) {
|
||||
emptyMessage = <RemoteHint url={remoteUrl} />;
|
||||
@ -153,7 +155,7 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||
alwaysPrepend
|
||||
append={remoteMessage}
|
||||
scrollKey='account_timeline'
|
||||
statusIds={blockedBy ? emptyList : statusIds}
|
||||
statusIds={(suspended || blockedBy) ? emptyList : statusIds}
|
||||
featuredStatusIds={featuredStatusIds}
|
||||
isLoading={isLoading}
|
||||
hasMore={hasMore}
|
||||
|
@ -37,7 +37,11 @@ class Audio extends React.PureComponent {
|
||||
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 = {
|
||||
@ -64,6 +68,19 @@ class Audio extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
_pack() {
|
||||
return {
|
||||
src: this.props.src,
|
||||
volume: this.audio.volume,
|
||||
muted: this.audio.muted,
|
||||
currentTime: this.audio.currentTime,
|
||||
poster: this.props.poster,
|
||||
backgroundColor: this.props.backgroundColor,
|
||||
foregroundColor: this.props.foregroundColor,
|
||||
accentColor: this.props.accentColor,
|
||||
};
|
||||
}
|
||||
|
||||
_setDimensions () {
|
||||
const width = this.player.offsetWidth;
|
||||
const height = this.props.fullscreen ? this.player.offsetHeight : (width / (16/9));
|
||||
@ -112,6 +129,10 @@ class Audio extends React.PureComponent {
|
||||
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 = () => {
|
||||
@ -248,7 +269,13 @@ class Audio extends React.PureComponent {
|
||||
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
||||
|
||||
if (!this.state.paused && !inView) {
|
||||
this.setState({ paused: true }, () => this.audio.pause());
|
||||
this.audio.pause();
|
||||
|
||||
if (this.props.deployPictureInPicture) {
|
||||
this.props.deployPictureInPicture('audio', this._pack());
|
||||
}
|
||||
|
||||
this.setState({ paused: true });
|
||||
}
|
||||
}, 150, { trailing: true });
|
||||
|
||||
@ -261,10 +288,22 @@ class Audio extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
const { autoPlay } = this.props;
|
||||
const { autoPlay, currentTime, volume, muted } = this.props;
|
||||
|
||||
if (currentTime) {
|
||||
this.audio.currentTime = currentTime;
|
||||
}
|
||||
|
||||
if (volume !== undefined) {
|
||||
this.audio.volume = volume;
|
||||
}
|
||||
|
||||
if (muted !== undefined) {
|
||||
this.audio.muted = muted;
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
this.audio.play();
|
||||
this.togglePlay();
|
||||
}
|
||||
}
|
||||
|
||||
@ -350,7 +389,7 @@ class Audio extends React.PureComponent {
|
||||
render () {
|
||||
const { src, intl, alt, editable, autoPlay } = this.props;
|
||||
const { paused, muted, volume, currentTime, duration, buffer, dragging } = this.state;
|
||||
const progress = (currentTime / duration) * 100;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
|
||||
return (
|
||||
<div className={classNames('audio-player', { editable })} ref={this.setPlayerRef} style={{ backgroundColor: this._getBackgroundColor(), color: this._getForegroundColor(), width: '100%', height: this.props.fullscreen ? '100%' : (this.state.height || this.props.height) }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}>
|
||||
|
@ -12,7 +12,7 @@ const emojiFilenames = (emojis) => {
|
||||
};
|
||||
|
||||
// Emoji requiring extra borders depending on theme
|
||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞']);
|
||||
const darkEmoji = emojiFilenames(['🎱', '🐜', '⚫', '🖤', '⬛', '◼️', '◾', '◼️', '✒️', '▪️', '💣', '🎳', '📷', '📸', '♣️', '🕶️', '✴️', '🔌', '💂♀️', '📽️', '🍳', '🦍', '💂', '🔪', '🕳️', '🕹️', '🕋', '🖊️', '🖋️', '💂♂️', '🎤', '🎓', '🎥', '🎼', '♠️', '🎩', '🦃', '📼', '📹', '🎮', '🐃', '🏴', '🐞', '🕺']);
|
||||
const lightEmoji = emojiFilenames(['👽', '⚾', '🐔', '☁️', '💨', '🕊️', '👀', '🍥', '👻', '🐐', '❕', '❔', '⛸️', '🌩️', '🔊', '🔇', '📃', '🌧️', '🐏', '🍚', '🍙', '🐓', '🐑', '💀', '☠️', '🌨️', '🔉', '🔈', '💬', '💭', '🏐', '🏳️', '⚪', '⬜', '◽', '◻️', '▫️']);
|
||||
|
||||
const emojiFilename = (filename) => {
|
||||
|
@ -9,6 +9,7 @@ const tooltips = defineMessages({
|
||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||
statuses: { id: 'notifications.filter.statuses', defaultMessage: 'Updates from people you follow' },
|
||||
});
|
||||
|
||||
export default @injectIntl
|
||||
@ -87,6 +88,13 @@ class FilterBar extends React.PureComponent {
|
||||
>
|
||||
<Icon id='tasks' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'status' ? 'active' : ''}
|
||||
onClick={this.onClick('status')}
|
||||
title={intl.formatMessage(tooltips.statuses)}
|
||||
>
|
||||
<Icon id='home' fixedWidth />
|
||||
</button>
|
||||
<button
|
||||
className={selectedFilter === 'follow' ? 'active' : ''}
|
||||
onClick={this.onClick('follow')}
|
||||
|
@ -10,6 +10,7 @@ import AccountContainer from 'mastodon/containers/account_container';
|
||||
import FollowRequestContainer from '../containers/follow_request_container';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import Permalink from 'mastodon/components/permalink';
|
||||
import classNames from 'classnames';
|
||||
|
||||
const messages = defineMessages({
|
||||
favourite: { id: 'notification.favourite', defaultMessage: '{name} favourited your status' },
|
||||
@ -17,6 +18,7 @@ const messages = defineMessages({
|
||||
ownPoll: { id: 'notification.own_poll', defaultMessage: 'Your poll has ended' },
|
||||
poll: { id: 'notification.poll', defaultMessage: 'A poll you have voted in has ended' },
|
||||
reblog: { id: 'notification.reblog', defaultMessage: '{name} boosted your status' },
|
||||
status: { id: 'notification.status', defaultMessage: '{name} just posted' },
|
||||
});
|
||||
|
||||
const notificationForScreenReader = (intl, message, timestamp) => {
|
||||
@ -49,6 +51,7 @@ class Notification extends ImmutablePureComponent {
|
||||
updateScrollBottom: PropTypes.func,
|
||||
cacheMediaWidth: PropTypes.func,
|
||||
cachedMediaWidth: PropTypes.number,
|
||||
unread: PropTypes.bool,
|
||||
};
|
||||
|
||||
handleMoveUp = () => {
|
||||
@ -113,11 +116,11 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
renderFollow (notification, account, link) {
|
||||
const { intl } = this.props;
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-follow focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className={classNames('notification notification-follow focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.follow, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='user-plus' fixedWidth />
|
||||
@ -135,11 +138,11 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
renderFollowRequest (notification, account, link) {
|
||||
const { intl } = this.props;
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-follow-request focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className={classNames('notification notification-follow-request focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage({ id: 'notification.follow_request', defaultMessage: '{name} has requested to follow you' }, { name: account.get('acct') }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='user' fixedWidth />
|
||||
@ -169,16 +172,17 @@ class Notification extends ImmutablePureComponent {
|
||||
updateScrollBottom={this.props.updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
unread={this.props.unread}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderFavourite (notification, link) {
|
||||
const { intl } = this.props;
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-favourite focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='star' className='star-icon' fixedWidth />
|
||||
@ -206,11 +210,11 @@ class Notification extends ImmutablePureComponent {
|
||||
}
|
||||
|
||||
renderReblog (notification, link) {
|
||||
const { intl } = this.props;
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-reblog focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className={classNames('notification notification-reblog focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.reblog, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='retweet' fixedWidth />
|
||||
@ -237,14 +241,46 @@ class Notification extends ImmutablePureComponent {
|
||||
);
|
||||
}
|
||||
|
||||
renderStatus (notification, link) {
|
||||
const { intl, unread } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className={classNames('notification notification-status focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.status, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='home' fixedWidth />
|
||||
</div>
|
||||
|
||||
<span title={notification.get('created_at')}>
|
||||
<FormattedMessage id='notification.status' defaultMessage='{name} just posted' values={{ name: link }} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<StatusContainer
|
||||
id={notification.get('status')}
|
||||
account={notification.get('account')}
|
||||
muted
|
||||
withDismiss
|
||||
hidden={this.props.hidden}
|
||||
getScrollPosition={this.props.getScrollPosition}
|
||||
updateScrollBottom={this.props.updateScrollBottom}
|
||||
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||
/>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
renderPoll (notification, account) {
|
||||
const { intl } = this.props;
|
||||
const { intl, unread } = this.props;
|
||||
const ownPoll = me === account.get('id');
|
||||
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.getHandlers()}>
|
||||
<div className='notification notification-poll focusable' tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
||||
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
|
||||
<div className='notification__message'>
|
||||
<div className='notification__favourite-icon-wrapper'>
|
||||
<Icon id='tasks' fixedWidth />
|
||||
@ -292,6 +328,8 @@ class Notification extends ImmutablePureComponent {
|
||||
return this.renderFavourite(notification, link);
|
||||
case 'reblog':
|
||||
return this.renderReblog(notification, link);
|
||||
case 'status':
|
||||
return this.renderStatus(notification, link);
|
||||
case 'poll':
|
||||
return this.renderPoll(notification, account);
|
||||
}
|
||||
|
@ -4,7 +4,14 @@ import PropTypes from 'prop-types';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import Column from '../../components/column';
|
||||
import ColumnHeader from '../../components/column_header';
|
||||
import { expandNotifications, scrollTopNotifications, loadPending, mountNotifications, unmountNotifications } from '../../actions/notifications';
|
||||
import {
|
||||
expandNotifications,
|
||||
scrollTopNotifications,
|
||||
loadPending,
|
||||
mountNotifications,
|
||||
unmountNotifications,
|
||||
markNotificationsAsRead,
|
||||
} from '../../actions/notifications';
|
||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||
import NotificationContainer from './containers/notification_container';
|
||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||
@ -15,9 +22,12 @@ import { List as ImmutableList } from 'immutable';
|
||||
import { debounce } from 'lodash';
|
||||
import ScrollableList from '../../components/scrollable_list';
|
||||
import LoadGap from '../../components/load_gap';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import compareId from 'mastodon/compare_id';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||
markAsRead : { id: 'notifications.mark_as_read', defaultMessage: 'Mark every notification as read' },
|
||||
});
|
||||
|
||||
const getNotifications = createSelector([
|
||||
@ -32,7 +42,7 @@ const getNotifications = createSelector([
|
||||
// we need to turn it off for FilterBar in order not to block ourselves from seeing a specific category
|
||||
return notifications.filterNot(item => item !== null && excludedTypes.includes(item.get('type')));
|
||||
}
|
||||
return notifications.filter(item => item !== null && allowedType === item.get('type'));
|
||||
return notifications.filter(item => item === null || allowedType === item.get('type'));
|
||||
});
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
@ -42,6 +52,8 @@ const mapStateToProps = state => ({
|
||||
isUnread: state.getIn(['notifications', 'unread']) > 0 || state.getIn(['notifications', 'pendingItems']).size > 0,
|
||||
hasMore: state.getIn(['notifications', 'hasMore']),
|
||||
numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size,
|
||||
lastReadId: state.getIn(['notifications', 'readMarkerId']),
|
||||
canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
@ -60,6 +72,8 @@ class Notifications extends React.PureComponent {
|
||||
multiColumn: PropTypes.bool,
|
||||
hasMore: PropTypes.bool,
|
||||
numPending: PropTypes.number,
|
||||
lastReadId: PropTypes.string,
|
||||
canMarkAsRead: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
@ -146,8 +160,12 @@ class Notifications extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
handleMarkAsRead = () => {
|
||||
this.props.dispatch(markNotificationsAsRead());
|
||||
};
|
||||
|
||||
render () {
|
||||
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar } = this.props;
|
||||
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead } = this.props;
|
||||
const pinned = !!columnId;
|
||||
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
|
||||
|
||||
@ -174,6 +192,7 @@ class Notifications extends React.PureComponent {
|
||||
accountId={item.get('account')}
|
||||
onMoveUp={this.handleMoveUp}
|
||||
onMoveDown={this.handleMoveDown}
|
||||
unread={lastReadId !== '0' && compareId(item.get('id'), lastReadId) > 0}
|
||||
/>
|
||||
));
|
||||
} else {
|
||||
@ -202,6 +221,21 @@ class Notifications extends React.PureComponent {
|
||||
</ScrollableList>
|
||||
);
|
||||
|
||||
let extraButton = null;
|
||||
|
||||
if (canMarkAsRead) {
|
||||
extraButton = (
|
||||
<button
|
||||
aria-label={intl.formatMessage(messages.markAsRead)}
|
||||
title={intl.formatMessage(messages.markAsRead)}
|
||||
onClick={this.handleMarkAsRead}
|
||||
className='column-header__button'
|
||||
>
|
||||
<Icon id='check' />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Column bindToDocument={!multiColumn} ref={this.setColumnRef} label={intl.formatMessage(messages.title)}>
|
||||
<ColumnHeader
|
||||
@ -213,6 +247,7 @@ class Notifications extends React.PureComponent {
|
||||
onClick={this.handleHeaderClick}
|
||||
pinned={pinned}
|
||||
multiColumn={multiColumn}
|
||||
extraButton={extraButton}
|
||||
>
|
||||
<ColumnSettingsContainer />
|
||||
</ColumnHeader>
|
||||
|
@ -0,0 +1,137 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import classNames from 'classnames';
|
||||
import { me, boostModal } from 'mastodon/initial_state';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import { replyCompose } from 'mastodon/actions/compose';
|
||||
import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
import { openModal } from 'mastodon/actions/modal';
|
||||
|
||||
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: 'Favourite' },
|
||||
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
|
||||
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
|
||||
});
|
||||
|
||||
const makeMapStateToProps = () => {
|
||||
const getStatus = makeGetStatus();
|
||||
|
||||
const mapStateToProps = (state, { statusId }) => ({
|
||||
status: getStatus(state, { id: statusId }),
|
||||
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
});
|
||||
|
||||
return mapStateToProps;
|
||||
};
|
||||
|
||||
export default @connect(makeMapStateToProps)
|
||||
@injectIntl
|
||||
class Footer extends ImmutablePureComponent {
|
||||
|
||||
static contextTypes = {
|
||||
router: PropTypes.object,
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
statusId: PropTypes.string.isRequired,
|
||||
status: ImmutablePropTypes.map.isRequired,
|
||||
intl: PropTypes.object.isRequired,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
askReplyConfirmation: PropTypes.bool,
|
||||
};
|
||||
|
||||
_performReply = () => {
|
||||
const { dispatch, status } = this.props;
|
||||
dispatch(replyCompose(status, this.context.router.history));
|
||||
};
|
||||
|
||||
handleReplyClick = () => {
|
||||
const { dispatch, askReplyConfirmation, intl } = this.props;
|
||||
|
||||
if (askReplyConfirmation) {
|
||||
dispatch(openModal('CONFIRM', {
|
||||
message: intl.formatMessage(messages.replyMessage),
|
||||
confirm: intl.formatMessage(messages.replyConfirm),
|
||||
onConfirm: this._performReply,
|
||||
}));
|
||||
} else {
|
||||
this._performReply();
|
||||
}
|
||||
};
|
||||
|
||||
handleFavouriteClick = () => {
|
||||
const { dispatch, status } = this.props;
|
||||
|
||||
if (status.get('favourited')) {
|
||||
dispatch(unfavourite(status));
|
||||
} else {
|
||||
dispatch(favourite(status));
|
||||
}
|
||||
};
|
||||
|
||||
_performReblog = () => {
|
||||
const { dispatch, status } = this.props;
|
||||
dispatch(reblog(status));
|
||||
}
|
||||
|
||||
handleReblogClick = e => {
|
||||
const { dispatch, status } = this.props;
|
||||
|
||||
if (status.get('reblogged')) {
|
||||
dispatch(unreblog(status));
|
||||
} else if ((e && e.shiftKey) || !boostModal) {
|
||||
this._performReblog();
|
||||
} else {
|
||||
dispatch(openModal('BOOST', { status, onReblog: this._performReblog }));
|
||||
}
|
||||
};
|
||||
|
||||
render () {
|
||||
const { status, intl } = this.props;
|
||||
|
||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
|
||||
|
||||
let replyIcon, replyTitle;
|
||||
|
||||
if (status.get('in_reply_to_id', null) === null) {
|
||||
replyIcon = 'reply';
|
||||
replyTitle = intl.formatMessage(messages.reply);
|
||||
} else {
|
||||
replyIcon = 'reply-all';
|
||||
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} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
|
||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
|
||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import PropTypes from 'prop-types';
|
||||
import IconButton from 'mastodon/components/icon_button';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Avatar from 'mastodon/components/avatar';
|
||||
import DisplayName from 'mastodon/components/display_name';
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
account: state.getIn(['accounts', accountId]),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class Header extends ImmutablePureComponent {
|
||||
|
||||
static propTypes = {
|
||||
accountId: PropTypes.string.isRequired,
|
||||
statusId: PropTypes.string.isRequired,
|
||||
account: ImmutablePropTypes.map.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { account, statusId, onClose } = this.props;
|
||||
|
||||
return (
|
||||
<div className='picture-in-picture__header'>
|
||||
<Link to={`/statuses/${statusId}`} className='picture-in-picture__header__account'>
|
||||
<Avatar account={account} size={36} />
|
||||
<DisplayName account={account} />
|
||||
</Link>
|
||||
|
||||
<IconButton icon='times' onClick={onClose} title='Close' />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
85
app/javascript/mastodon/features/picture_in_picture/index.js
Normal file
85
app/javascript/mastodon/features/picture_in_picture/index.js
Normal file
@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import Video from 'mastodon/features/video';
|
||||
import Audio from 'mastodon/features/audio';
|
||||
import { removePictureInPicture } from 'mastodon/actions/picture_in_picture';
|
||||
import Header from './components/header';
|
||||
import Footer from './components/footer';
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
...state.get('picture_in_picture'),
|
||||
});
|
||||
|
||||
export default @connect(mapStateToProps)
|
||||
class PictureInPicture extends React.Component {
|
||||
|
||||
static propTypes = {
|
||||
statusId: PropTypes.string,
|
||||
accountId: PropTypes.string,
|
||||
type: PropTypes.string,
|
||||
src: PropTypes.string,
|
||||
muted: PropTypes.bool,
|
||||
volume: PropTypes.number,
|
||||
currentTime: PropTypes.number,
|
||||
poster: PropTypes.string,
|
||||
backgroundColor: PropTypes.string,
|
||||
foregroundColor: PropTypes.string,
|
||||
accentColor: PropTypes.string,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
handleClose = () => {
|
||||
const { dispatch } = this.props;
|
||||
dispatch(removePictureInPicture());
|
||||
}
|
||||
|
||||
render () {
|
||||
const { type, src, currentTime, accountId, statusId } = this.props;
|
||||
|
||||
if (!currentTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let player;
|
||||
|
||||
if (type === 'video') {
|
||||
player = (
|
||||
<Video
|
||||
src={src}
|
||||
currentTime={this.props.currentTime}
|
||||
volume={this.props.volume}
|
||||
muted={this.props.muted}
|
||||
autoPlay
|
||||
inline
|
||||
alwaysVisible
|
||||
/>
|
||||
);
|
||||
} else if (type === 'audio') {
|
||||
player = (
|
||||
<Audio
|
||||
src={src}
|
||||
currentTime={this.props.currentTime}
|
||||
volume={this.props.volume}
|
||||
muted={this.props.muted}
|
||||
poster={this.props.poster}
|
||||
backgroundColor={this.props.backgroundColor}
|
||||
foregroundColor={this.props.foregroundColor}
|
||||
accentColor={this.props.accentColor}
|
||||
autoPlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='picture-in-picture'>
|
||||
<Header accountId={accountId} statusId={statusId} onClose={this.handleClose} />
|
||||
|
||||
{player}
|
||||
|
||||
<Footer statusId={statusId} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@ -15,6 +15,7 @@ import scheduleIdleTask from '../../ui/util/schedule_idle_task';
|
||||
import classNames from 'classnames';
|
||||
import Icon from 'mastodon/components/icon';
|
||||
import AnimatedNumber from 'mastodon/components/animated_number';
|
||||
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
|
||||
|
||||
const messages = defineMessages({
|
||||
public_short: { id: 'privacy.public.short', defaultMessage: 'Public' },
|
||||
@ -40,6 +41,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
domain: PropTypes.string.isRequired,
|
||||
compact: PropTypes.bool,
|
||||
showMedia: PropTypes.bool,
|
||||
usingPiP: PropTypes.bool,
|
||||
onToggleMediaVisibility: PropTypes.func,
|
||||
};
|
||||
|
||||
@ -100,7 +102,7 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
render () {
|
||||
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
|
||||
const outerStyle = { boxSizing: 'border-box' };
|
||||
const { intl, compact } = this.props;
|
||||
const { intl, compact, usingPiP } = this.props;
|
||||
|
||||
if (!status) {
|
||||
return null;
|
||||
@ -116,7 +118,9 @@ class DetailedStatus extends ImmutablePureComponent {
|
||||
outerStyle.height = `${this.state.height}px`;
|
||||
}
|
||||
|
||||
if (status.get('media_attachments').size > 0) {
|
||||
if (usingPiP) {
|
||||
media = <PictureInPicturePlaceholder />;
|
||||
} else if (status.get('media_attachments').size > 0) {
|
||||
if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
|
||||
const attachment = status.getIn(['media_attachments', 0]);
|
||||
|
||||
|
@ -143,6 +143,7 @@ const makeMapStateToProps = () => {
|
||||
descendantsIds,
|
||||
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||
domain: state.getIn(['meta', 'domain']),
|
||||
usingPiP: state.get('picture_in_picture').statusId === props.params.statusId,
|
||||
};
|
||||
};
|
||||
|
||||
@ -167,6 +168,7 @@ class Status extends ImmutablePureComponent {
|
||||
askReplyConfirmation: PropTypes.bool,
|
||||
multiColumn: PropTypes.bool,
|
||||
domain: PropTypes.string.isRequired,
|
||||
usingPiP: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -492,7 +494,7 @@ class Status extends ImmutablePureComponent {
|
||||
|
||||
render () {
|
||||
let ancestors, descendants;
|
||||
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn } = this.props;
|
||||
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props;
|
||||
const { fullscreen } = this.state;
|
||||
|
||||
if (status === null) {
|
||||
@ -550,6 +552,7 @@ class Status extends ImmutablePureComponent {
|
||||
domain={domain}
|
||||
showMedia={this.state.showMedia}
|
||||
onToggleMediaVisibility={this.handleToggleMediaVisibility}
|
||||
usingPiP={usingPiP}
|
||||
/>
|
||||
|
||||
<ActionBar
|
||||
|
@ -160,7 +160,7 @@ class MediaModal extends ImmutablePureComponent {
|
||||
src={image.get('url')}
|
||||
width={image.get('width')}
|
||||
height={image.get('height')}
|
||||
startTime={time || 0}
|
||||
currentTime={time || 0}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={image.get('description')}
|
||||
|
@ -66,9 +66,9 @@ export default class VideoModal extends ImmutablePureComponent {
|
||||
preview={media.get('preview_url')}
|
||||
blurhash={media.get('blurhash')}
|
||||
src={media.get('url')}
|
||||
startTime={options.startTime}
|
||||
currentTime={options.startTime}
|
||||
autoPlay={options.autoPlay}
|
||||
defaultVolume={options.defaultVolume}
|
||||
volume={options.defaultVolume}
|
||||
onCloseVideo={onClose}
|
||||
detailed
|
||||
alt={media.get('description')}
|
||||
|
@ -16,11 +16,12 @@ import { expandNotifications } from '../../actions/notifications';
|
||||
import { fetchFilters } from '../../actions/filters';
|
||||
import { clearHeight } from '../../actions/height_cache';
|
||||
import { focusApp, unfocusApp } from 'mastodon/actions/app';
|
||||
import { synchronouslySubmitMarkers } from 'mastodon/actions/markers';
|
||||
import { synchronouslySubmitMarkers, submitMarkers, fetchMarkers } from 'mastodon/actions/markers';
|
||||
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
|
||||
import UploadArea from './components/upload_area';
|
||||
import ColumnsAreaContainer from './containers/columns_area_container';
|
||||
import DocumentTitle from './components/document_title';
|
||||
import PictureInPicture from 'mastodon/features/picture_in_picture';
|
||||
import {
|
||||
Compose,
|
||||
Status,
|
||||
@ -265,6 +266,7 @@ class UI extends React.PureComponent {
|
||||
|
||||
handleWindowFocus = () => {
|
||||
this.props.dispatch(focusApp());
|
||||
this.props.dispatch(submitMarkers());
|
||||
}
|
||||
|
||||
handleWindowBlur = () => {
|
||||
@ -368,6 +370,7 @@ class UI extends React.PureComponent {
|
||||
window.setTimeout(() => Notification.requestPermission(), 120 * 1000);
|
||||
}
|
||||
|
||||
this.props.dispatch(fetchMarkers());
|
||||
this.props.dispatch(expandHomeTimeline());
|
||||
this.props.dispatch(expandNotifications());
|
||||
|
||||
@ -545,6 +548,7 @@ class UI extends React.PureComponent {
|
||||
{children}
|
||||
</SwitchingColumnsArea>
|
||||
|
||||
<PictureInPicture />
|
||||
<NotificationsContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
<ModalContainer />
|
||||
|
@ -104,20 +104,23 @@ class Video extends React.PureComponent {
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number,
|
||||
sensitive: PropTypes.bool,
|
||||
startTime: PropTypes.number,
|
||||
currentTime: PropTypes.number,
|
||||
onOpenVideo: PropTypes.func,
|
||||
onCloseVideo: PropTypes.func,
|
||||
detailed: PropTypes.bool,
|
||||
inline: PropTypes.bool,
|
||||
editable: PropTypes.bool,
|
||||
alwaysVisible: PropTypes.bool,
|
||||
cacheWidth: PropTypes.func,
|
||||
visible: PropTypes.bool,
|
||||
onToggleVisibility: PropTypes.func,
|
||||
deployPictureInPicture: PropTypes.func,
|
||||
intl: PropTypes.object.isRequired,
|
||||
blurhash: PropTypes.string,
|
||||
link: PropTypes.node,
|
||||
autoPlay: PropTypes.bool,
|
||||
defaultVolume: PropTypes.number,
|
||||
volume: PropTypes.number,
|
||||
muted: PropTypes.bool,
|
||||
};
|
||||
|
||||
state = {
|
||||
@ -297,6 +300,15 @@ class Video extends React.PureComponent {
|
||||
document.removeEventListener('webkitfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('mozfullscreenchange', this.handleFullscreenChange, true);
|
||||
document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true);
|
||||
|
||||
if (!this.state.paused && this.video && this.props.deployPictureInPicture) {
|
||||
this.props.deployPictureInPicture('video', {
|
||||
src: this.props.src,
|
||||
currentTime: this.video.currentTime,
|
||||
muted: this.video.muted,
|
||||
volume: this.video.volume,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps (nextProps) {
|
||||
@ -328,7 +340,18 @@ class Video extends React.PureComponent {
|
||||
const inView = (top <= (window.innerHeight || document.documentElement.clientHeight)) && (top + height >= 0);
|
||||
|
||||
if (!this.state.paused && !inView) {
|
||||
this.setState({ paused: true }, () => this.video.pause());
|
||||
this.video.pause();
|
||||
|
||||
if (this.props.deployPictureInPicture) {
|
||||
this.props.deployPictureInPicture('video', {
|
||||
src: this.props.src,
|
||||
currentTime: this.video.currentTime,
|
||||
muted: this.video.muted,
|
||||
volume: this.video.volume,
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ paused: true });
|
||||
}
|
||||
}, 150, { trailing: true })
|
||||
|
||||
@ -361,15 +384,21 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
handleLoadedData = () => {
|
||||
if (this.props.startTime) {
|
||||
this.video.currentTime = this.props.startTime;
|
||||
const { currentTime, volume, muted, autoPlay } = this.props;
|
||||
|
||||
if (currentTime) {
|
||||
this.video.currentTime = currentTime;
|
||||
}
|
||||
|
||||
if (this.props.defaultVolume !== undefined) {
|
||||
this.video.volume = this.props.defaultVolume;
|
||||
if (volume !== undefined) {
|
||||
this.video.volume = volume;
|
||||
}
|
||||
|
||||
if (this.props.autoPlay) {
|
||||
if (muted !== undefined) {
|
||||
this.video.muted = muted;
|
||||
}
|
||||
|
||||
if (autoPlay) {
|
||||
this.video.play();
|
||||
}
|
||||
}
|
||||
@ -414,9 +443,9 @@ class Video extends React.PureComponent {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { preview, src, inline, startTime, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
|
||||
const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, detailed, sensitive, link, editable, blurhash } = this.props;
|
||||
const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state;
|
||||
const progress = (currentTime / duration) * 100;
|
||||
const progress = Math.min((currentTime / duration) * 100, 100);
|
||||
const playerStyle = {};
|
||||
|
||||
let { width, height } = this.props;
|
||||
@ -430,7 +459,7 @@ class Video extends React.PureComponent {
|
||||
|
||||
let preload;
|
||||
|
||||
if (startTime || fullscreen || dragging) {
|
||||
if (this.props.currentTime || fullscreen || dragging) {
|
||||
preload = 'auto';
|
||||
} else if (detailed) {
|
||||
preload = 'metadata';
|
||||
@ -530,7 +559,7 @@ class Video extends React.PureComponent {
|
||||
</div>
|
||||
|
||||
<div className='video-player__buttons right'>
|
||||
{(!onCloseVideo && !editable && !fullscreen) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{(!onCloseVideo && !editable && !fullscreen && !this.props.alwaysVisible) && <button type='button' title={intl.formatMessage(messages.hide)} aria-label={intl.formatMessage(messages.hide)} onClick={this.toggleReveal}><Icon id='eye-slash' fixedWidth /></button>}
|
||||
{(!fullscreen && onOpenVideo) && <button type='button' title={intl.formatMessage(messages.expand)} aria-label={intl.formatMessage(messages.expand)} onClick={this.handleOpenVideo}><Icon id='expand' fixedWidth /></button>}
|
||||
{onCloseVideo && <button type='button' title={intl.formatMessage(messages.close)} aria-label={intl.formatMessage(messages.close)} onClick={this.handleCloseVideo}><Icon id='compress' fixedWidth /></button>}
|
||||
<button type='button' title={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} aria-label={intl.formatMessage(fullscreen ? messages.exit_fullscreen : messages.fullscreen)} onClick={this.toggleFullscreen}><Icon id={fullscreen ? 'compress' : 'arrows-alt'} fixedWidth /></button>
|
||||
|
Reference in New Issue
Block a user