From 81ef21a0c802f1d905f37a2a818544a8b400793c Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sat, 25 Feb 2023 14:34:32 +0100 Subject: [Glitch] Rename JSX files with proper `.jsx` extension Port 44a7d87cb1f5df953b6c14c16c59e2e4ead1bcb9 to glitch-soc Signed-off-by: Claire --- .../features/status/components/action_bar.js | 230 ------- .../features/status/components/action_bar.jsx | 230 +++++++ .../glitch/features/status/components/card.js | 281 -------- .../glitch/features/status/components/card.jsx | 281 ++++++++ .../features/status/components/detailed_status.js | 335 ---------- .../features/status/components/detailed_status.jsx | 335 ++++++++++ .../flavours/glitch/features/status/index.js | 726 --------------------- .../flavours/glitch/features/status/index.jsx | 726 +++++++++++++++++++++ 8 files changed, 1572 insertions(+), 1572 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/status/components/action_bar.js create mode 100644 app/javascript/flavours/glitch/features/status/components/action_bar.jsx delete mode 100644 app/javascript/flavours/glitch/features/status/components/card.js create mode 100644 app/javascript/flavours/glitch/features/status/components/card.jsx delete mode 100644 app/javascript/flavours/glitch/features/status/components/detailed_status.js create mode 100644 app/javascript/flavours/glitch/features/status/components/detailed_status.jsx delete mode 100644 app/javascript/flavours/glitch/features/status/index.js create mode 100644 app/javascript/flavours/glitch/features/status/index.jsx (limited to 'app/javascript/flavours/glitch/features/status') diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js deleted file mode 100644 index 4901fc4cc..000000000 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ /dev/null @@ -1,230 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import IconButton from 'flavours/glitch/components/icon_button'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; -import { defineMessages, injectIntl } from 'react-intl'; -import { me } from 'flavours/glitch/initial_state'; -import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; -import classNames from 'classnames'; -import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; - -const messages = defineMessages({ - delete: { id: 'status.delete', defaultMessage: 'Delete' }, - redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, - edit: { id: 'status.edit', defaultMessage: 'Edit' }, - direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, - mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, - reply: { id: 'status.reply', defaultMessage: 'Reply' }, - 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' }, - bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, - more: { id: 'status.more', defaultMessage: 'More' }, - mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, - muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, - unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, - block: { id: 'status.block', defaultMessage: 'Block @{name}' }, - report: { id: 'status.report', defaultMessage: 'Report @{name}' }, - share: { id: 'status.share', defaultMessage: 'Share' }, - pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, - unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, - embed: { id: 'status.embed', defaultMessage: 'Embed' }, - admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, - admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, - admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, - copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, - openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, -}); - -export default @injectIntl -class ActionBar extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object, - identity: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReply: PropTypes.func.isRequired, - onReblog: PropTypes.func.isRequired, - onFavourite: PropTypes.func.isRequired, - onBookmark: PropTypes.func.isRequired, - onMute: PropTypes.func, - onMuteConversation: PropTypes.func, - onBlock: PropTypes.func, - onDelete: PropTypes.func.isRequired, - onEdit: PropTypes.func.isRequired, - onDirect: PropTypes.func.isRequired, - onMention: PropTypes.func.isRequired, - onReport: PropTypes.func, - onPin: PropTypes.func, - onEmbed: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - handleReplyClick = () => { - this.props.onReply(this.props.status); - }; - - handleReblogClick = (e) => { - this.props.onReblog(this.props.status, e); - }; - - handleFavouriteClick = (e) => { - this.props.onFavourite(this.props.status, e); - }; - - handleBookmarkClick = (e) => { - this.props.onBookmark(this.props.status, e); - }; - - handleDeleteClick = () => { - this.props.onDelete(this.props.status, this.context.router.history); - }; - - handleRedraftClick = () => { - this.props.onDelete(this.props.status, this.context.router.history, true); - }; - - handleEditClick = () => { - this.props.onEdit(this.props.status, this.context.router.history); - }; - - handleDirectClick = () => { - this.props.onDirect(this.props.status.get('account'), this.context.router.history); - }; - - handleMentionClick = () => { - this.props.onMention(this.props.status.get('account'), this.context.router.history); - }; - - handleMuteClick = () => { - this.props.onMute(this.props.status.get('account')); - }; - - handleConversationMuteClick = () => { - this.props.onMuteConversation(this.props.status); - }; - - handleBlockClick = () => { - this.props.onBlock(this.props.status); - }; - - handleReport = () => { - this.props.onReport(this.props.status); - }; - - handlePinClick = () => { - this.props.onPin(this.props.status); - }; - - handleShare = () => { - navigator.share({ - text: this.props.status.get('search_index'), - url: this.props.status.get('url'), - }); - }; - - handleEmbed = () => { - this.props.onEmbed(this.props.status); - }; - - handleCopy = () => { - const url = this.props.status.get('url'); - navigator.clipboard.writeText(url); - }; - - render () { - const { status, intl } = this.props; - const { signedIn, permissions } = this.context.identity; - - const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); - const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); - const mutingConversation = status.get('muted'); - const writtenByMe = status.getIn(['account', 'id']) === me; - const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); - - let menu = []; - - if (publicStatus) { - if (isRemote) { - menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); - } - - menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); - menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); - menu.push(null); - } - - if (writtenByMe) { - if (pinnableStatus) { - menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); - menu.push(null); - } - - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); - menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); - menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); - } else { - menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); - menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); - menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); - menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { - menu.push(null); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { - if (accountAdminLink !== undefined) { - menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) }); - } - if (statusAdminLink !== undefined) { - menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) }); - } - } - if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { - const domain = status.getIn(['account', 'acct']).split('@')[1]; - menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); - } - } - } - - const shareButton = ('share' in navigator) && publicStatus && ( -
- ); - - const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; - - 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 ( -
-
-
-
- {shareButton} -
- -
- -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.jsx b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx new file mode 100644 index 000000000..4901fc4cc --- /dev/null +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.jsx @@ -0,0 +1,230 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import IconButton from 'flavours/glitch/components/icon_button'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import { defineMessages, injectIntl } from 'react-intl'; +import { me } from 'flavours/glitch/initial_state'; +import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; +import classNames from 'classnames'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; + +const messages = defineMessages({ + delete: { id: 'status.delete', defaultMessage: 'Delete' }, + redraft: { id: 'status.redraft', defaultMessage: 'Delete & re-draft' }, + edit: { id: 'status.edit', defaultMessage: 'Edit' }, + direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, + mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, + reply: { id: 'status.reply', defaultMessage: 'Reply' }, + 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' }, + bookmark: { id: 'status.bookmark', defaultMessage: 'Bookmark' }, + more: { id: 'status.more', defaultMessage: 'More' }, + mute: { id: 'status.mute', defaultMessage: 'Mute @{name}' }, + muteConversation: { id: 'status.mute_conversation', defaultMessage: 'Mute conversation' }, + unmuteConversation: { id: 'status.unmute_conversation', defaultMessage: 'Unmute conversation' }, + block: { id: 'status.block', defaultMessage: 'Block @{name}' }, + report: { id: 'status.report', defaultMessage: 'Report @{name}' }, + share: { id: 'status.share', defaultMessage: 'Share' }, + pin: { id: 'status.pin', defaultMessage: 'Pin on profile' }, + unpin: { id: 'status.unpin', defaultMessage: 'Unpin from profile' }, + embed: { id: 'status.embed', defaultMessage: 'Embed' }, + admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, + admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, + copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, + openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, +}); + +export default @injectIntl +class ActionBar extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object, + identity: PropTypes.object, + }; + + static propTypes = { + status: ImmutablePropTypes.map.isRequired, + onReply: PropTypes.func.isRequired, + onReblog: PropTypes.func.isRequired, + onFavourite: PropTypes.func.isRequired, + onBookmark: PropTypes.func.isRequired, + onMute: PropTypes.func, + onMuteConversation: PropTypes.func, + onBlock: PropTypes.func, + onDelete: PropTypes.func.isRequired, + onEdit: PropTypes.func.isRequired, + onDirect: PropTypes.func.isRequired, + onMention: PropTypes.func.isRequired, + onReport: PropTypes.func, + onPin: PropTypes.func, + onEmbed: PropTypes.func, + intl: PropTypes.object.isRequired, + }; + + handleReplyClick = () => { + this.props.onReply(this.props.status); + }; + + handleReblogClick = (e) => { + this.props.onReblog(this.props.status, e); + }; + + handleFavouriteClick = (e) => { + this.props.onFavourite(this.props.status, e); + }; + + handleBookmarkClick = (e) => { + this.props.onBookmark(this.props.status, e); + }; + + handleDeleteClick = () => { + this.props.onDelete(this.props.status, this.context.router.history); + }; + + handleRedraftClick = () => { + this.props.onDelete(this.props.status, this.context.router.history, true); + }; + + handleEditClick = () => { + this.props.onEdit(this.props.status, this.context.router.history); + }; + + handleDirectClick = () => { + this.props.onDirect(this.props.status.get('account'), this.context.router.history); + }; + + handleMentionClick = () => { + this.props.onMention(this.props.status.get('account'), this.context.router.history); + }; + + handleMuteClick = () => { + this.props.onMute(this.props.status.get('account')); + }; + + handleConversationMuteClick = () => { + this.props.onMuteConversation(this.props.status); + }; + + handleBlockClick = () => { + this.props.onBlock(this.props.status); + }; + + handleReport = () => { + this.props.onReport(this.props.status); + }; + + handlePinClick = () => { + this.props.onPin(this.props.status); + }; + + handleShare = () => { + navigator.share({ + text: this.props.status.get('search_index'), + url: this.props.status.get('url'), + }); + }; + + handleEmbed = () => { + this.props.onEmbed(this.props.status); + }; + + handleCopy = () => { + const url = this.props.status.get('url'); + navigator.clipboard.writeText(url); + }; + + render () { + const { status, intl } = this.props; + const { signedIn, permissions } = this.context.identity; + + const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); + const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); + const mutingConversation = status.get('muted'); + const writtenByMe = status.getIn(['account', 'id']) === me; + const isRemote = status.getIn(['account', 'username']) !== status.getIn(['account', 'acct']); + + let menu = []; + + if (publicStatus) { + if (isRemote) { + menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') }); + } + + menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy }); + menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed }); + menu.push(null); + } + + if (writtenByMe) { + if (pinnableStatus) { + menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick }); + menu.push(null); + } + + menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick }); + menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick }); + menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick }); + } else { + menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); + menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); + menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); + menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); + if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { + menu.push(null); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if (accountAdminLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) }); + } + if (statusAdminLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) }); + } + } + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + const domain = status.getIn(['account', 'acct']).split('@')[1]; + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); + } + } + } + + const shareButton = ('share' in navigator) && publicStatus && ( +
+ ); + + const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; + + 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 ( +
+
+
+
+ {shareButton} +
+ +
+ +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js deleted file mode 100644 index 6a306ed14..000000000 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ /dev/null @@ -1,281 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Immutable from 'immutable'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage } from 'react-intl'; -import punycode from 'punycode'; -import classnames from 'classnames'; -import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; -import Icon from 'flavours/glitch/components/icon'; -import { useBlurhash } from 'flavours/glitch/initial_state'; -import Blurhash from 'flavours/glitch/components/blurhash'; -import { debounce } from 'lodash'; - -const getHostname = url => { - const parser = document.createElement('a'); - parser.href = url; - return parser.hostname; -}; - -const trim = (text, len) => { - const cut = text.indexOf(' ', len); - - if (cut === -1) { - return text; - } - - return text.slice(0, cut) + (text.length > len ? '…' : ''); -}; - -const domParser = new DOMParser(); - -const addAutoPlay = html => { - const document = domParser.parseFromString(html, 'text/html').documentElement; - const iframe = document.querySelector('iframe'); - - if (iframe) { - if (iframe.src.indexOf('?') !== -1) { - iframe.src += '&'; - } else { - iframe.src += '?'; - } - - iframe.src += 'autoplay=1&auto_play=1'; - - // DOM parser creates html/body elements around original HTML fragment, - // so we need to get innerHTML out of the body and not the entire document - return document.querySelector('body').innerHTML; - } - - return html; -}; - -export default class Card extends React.PureComponent { - - static propTypes = { - card: ImmutablePropTypes.map, - maxDescription: PropTypes.number, - onOpenMedia: PropTypes.func.isRequired, - compact: PropTypes.bool, - defaultWidth: PropTypes.number, - cacheWidth: PropTypes.func, - sensitive: PropTypes.bool, - }; - - static defaultProps = { - maxDescription: 50, - compact: false, - }; - - state = { - width: this.props.defaultWidth || 280, - previewLoaded: false, - embedded: false, - revealed: !this.props.sensitive, - }; - - componentWillReceiveProps (nextProps) { - if (!Immutable.is(this.props.card, nextProps.card)) { - this.setState({ embedded: false, previewLoaded: false }); - } - if (this.props.sensitive !== nextProps.sensitive) { - this.setState({ revealed: !nextProps.sensitive }); - } - } - - componentDidMount () { - window.addEventListener('resize', this.handleResize, { passive: true }); - } - - componentWillUnmount () { - window.removeEventListener('resize', this.handleResize); - } - - _setDimensions () { - const width = this.node.offsetWidth; - - if (this.props.cacheWidth) { - this.props.cacheWidth(width); - } - - this.setState({ width }); - } - - handleResize = debounce(() => { - if (this.node) { - this._setDimensions(); - } - }, 250, { - trailing: true, - }); - - handlePhotoClick = () => { - const { card, onOpenMedia } = this.props; - - onOpenMedia( - Immutable.fromJS([ - { - type: 'image', - url: card.get('embed_url'), - description: card.get('title'), - meta: { - original: { - width: card.get('width'), - height: card.get('height'), - }, - }, - }, - ]), - 0, - ); - }; - - handleEmbedClick = () => { - const { card } = this.props; - - if (card.get('type') === 'photo') { - this.handlePhotoClick(); - } else { - this.setState({ embedded: true }); - } - }; - - setRef = c => { - this.node = c; - - if (this.node) { - this._setDimensions(); - } - }; - - handleImageLoad = () => { - this.setState({ previewLoaded: true }); - }; - - handleReveal = e => { - e.preventDefault(); - e.stopPropagation(); - this.setState({ revealed: true }); - }; - - renderVideo () { - const { card } = this.props; - const content = { __html: addAutoPlay(card.get('html')) }; - const { width } = this.state; - const ratio = card.get('width') / card.get('height'); - const height = width / ratio; - - return ( -
- ); - } - - render () { - const { card, maxDescription, compact, defaultWidth } = this.props; - const { width, embedded, revealed } = this.state; - - if (card === null) { - return null; - } - - const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); - const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; - const interactive = card.get('type') !== 'link'; - const className = classnames('status-card', { horizontal, compact, interactive }); - const title = interactive ? {card.get('title')} : {card.get('title')}; - const ratio = card.get('width') / card.get('height'); - const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); - - const description = ( -
- {title} - {!(horizontal || compact) &&

{trim(card.get('description') || '', maxDescription)}

} - {provider} -
- ); - - let embed = ''; - let canvas = ( - - ); - let thumbnail = ; - let spoilerButton = ( - - ); - spoilerButton = ( -
- {spoilerButton} -
- ); - - if (interactive) { - if (embedded) { - embed = this.renderVideo(); - } else { - let iconVariant = 'play'; - - if (card.get('type') === 'photo') { - iconVariant = 'search-plus'; - } - - embed = ( -
- {canvas} - {thumbnail} - - {revealed && ( -
-
- - {horizontal && } -
-
- )} - {!revealed && spoilerButton} -
- ); - } - - return ( -
- {embed} - {!compact && description} -
- ); - } else if (card.get('image')) { - embed = ( -
- {canvas} - {thumbnail} -
- ); - } else { - embed = ( -
- -
- ); - } - - return ( - - {embed} - {description} - - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/status/components/card.jsx b/app/javascript/flavours/glitch/features/status/components/card.jsx new file mode 100644 index 000000000..6a306ed14 --- /dev/null +++ b/app/javascript/flavours/glitch/features/status/components/card.jsx @@ -0,0 +1,281 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Immutable from 'immutable'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import punycode from 'punycode'; +import classnames from 'classnames'; +import { decode as decodeIDNA } from 'flavours/glitch/utils/idna'; +import Icon from 'flavours/glitch/components/icon'; +import { useBlurhash } from 'flavours/glitch/initial_state'; +import Blurhash from 'flavours/glitch/components/blurhash'; +import { debounce } from 'lodash'; + +const getHostname = url => { + const parser = document.createElement('a'); + parser.href = url; + return parser.hostname; +}; + +const trim = (text, len) => { + const cut = text.indexOf(' ', len); + + if (cut === -1) { + return text; + } + + return text.slice(0, cut) + (text.length > len ? '…' : ''); +}; + +const domParser = new DOMParser(); + +const addAutoPlay = html => { + const document = domParser.parseFromString(html, 'text/html').documentElement; + const iframe = document.querySelector('iframe'); + + if (iframe) { + if (iframe.src.indexOf('?') !== -1) { + iframe.src += '&'; + } else { + iframe.src += '?'; + } + + iframe.src += 'autoplay=1&auto_play=1'; + + // DOM parser creates html/body elements around original HTML fragment, + // so we need to get innerHTML out of the body and not the entire document + return document.querySelector('body').innerHTML; + } + + return html; +}; + +export default class Card extends React.PureComponent { + + static propTypes = { + card: ImmutablePropTypes.map, + maxDescription: PropTypes.number, + onOpenMedia: PropTypes.func.isRequired, + compact: PropTypes.bool, + defaultWidth: PropTypes.number, + cacheWidth: PropTypes.func, + sensitive: PropTypes.bool, + }; + + static defaultProps = { + maxDescription: 50, + compact: false, + }; + + state = { + width: this.props.defaultWidth || 280, + previewLoaded: false, + embedded: false, + revealed: !this.props.sensitive, + }; + + componentWillReceiveProps (nextProps) { + if (!Immutable.is(this.props.card, nextProps.card)) { + this.setState({ embedded: false, previewLoaded: false }); + } + if (this.props.sensitive !== nextProps.sensitive) { + this.setState({ revealed: !nextProps.sensitive }); + } + } + + componentDidMount () { + window.addEventListener('resize', this.handleResize, { passive: true }); + } + + componentWillUnmount () { + window.removeEventListener('resize', this.handleResize); + } + + _setDimensions () { + const width = this.node.offsetWidth; + + if (this.props.cacheWidth) { + this.props.cacheWidth(width); + } + + this.setState({ width }); + } + + handleResize = debounce(() => { + if (this.node) { + this._setDimensions(); + } + }, 250, { + trailing: true, + }); + + handlePhotoClick = () => { + const { card, onOpenMedia } = this.props; + + onOpenMedia( + Immutable.fromJS([ + { + type: 'image', + url: card.get('embed_url'), + description: card.get('title'), + meta: { + original: { + width: card.get('width'), + height: card.get('height'), + }, + }, + }, + ]), + 0, + ); + }; + + handleEmbedClick = () => { + const { card } = this.props; + + if (card.get('type') === 'photo') { + this.handlePhotoClick(); + } else { + this.setState({ embedded: true }); + } + }; + + setRef = c => { + this.node = c; + + if (this.node) { + this._setDimensions(); + } + }; + + handleImageLoad = () => { + this.setState({ previewLoaded: true }); + }; + + handleReveal = e => { + e.preventDefault(); + e.stopPropagation(); + this.setState({ revealed: true }); + }; + + renderVideo () { + const { card } = this.props; + const content = { __html: addAutoPlay(card.get('html')) }; + const { width } = this.state; + const ratio = card.get('width') / card.get('height'); + const height = width / ratio; + + return ( +
+ ); + } + + render () { + const { card, maxDescription, compact, defaultWidth } = this.props; + const { width, embedded, revealed } = this.state; + + if (card === null) { + return null; + } + + const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); + const horizontal = (!compact && card.get('width') > card.get('height') && (card.get('width') + 100 >= width)) || card.get('type') !== 'link' || embedded; + const interactive = card.get('type') !== 'link'; + const className = classnames('status-card', { horizontal, compact, interactive }); + const title = interactive ? {card.get('title')} : {card.get('title')}; + const ratio = card.get('width') / card.get('height'); + const height = (compact && !embedded) ? (width / (16 / 9)) : (width / ratio); + + const description = ( +
+ {title} + {!(horizontal || compact) &&

{trim(card.get('description') || '', maxDescription)}

} + {provider} +
+ ); + + let embed = ''; + let canvas = ( + + ); + let thumbnail = ; + let spoilerButton = ( + + ); + spoilerButton = ( +
+ {spoilerButton} +
+ ); + + if (interactive) { + if (embedded) { + embed = this.renderVideo(); + } else { + let iconVariant = 'play'; + + if (card.get('type') === 'photo') { + iconVariant = 'search-plus'; + } + + embed = ( +
+ {canvas} + {thumbnail} + + {revealed && ( +
+
+ + {horizontal && } +
+
+ )} + {!revealed && spoilerButton} +
+ ); + } + + return ( +
+ {embed} + {!compact && description} +
+ ); + } else if (card.get('image')) { + embed = ( +
+ {canvas} + {thumbnail} +
+ ); + } else { + embed = ( +
+ +
+ ); + } + + return ( + + {embed} + {description} + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js deleted file mode 100644 index 644881fa5..000000000 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ /dev/null @@ -1,335 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import Avatar from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; -import StatusContent from 'flavours/glitch/components/status_content'; -import MediaGallery from 'flavours/glitch/components/media_gallery'; -import AttachmentList from 'flavours/glitch/components/attachment_list'; -import { Link } from 'react-router-dom'; -import { injectIntl, FormattedDate } from 'react-intl'; -import Card from './card'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import Video from 'flavours/glitch/features/video'; -import Audio from 'flavours/glitch/features/audio'; -import VisibilityIcon from 'flavours/glitch/components/status_visibility_icon'; -import scheduleIdleTask from '../../ui/util/schedule_idle_task'; -import classNames from 'classnames'; -import PollContainer from 'flavours/glitch/containers/poll_container'; -import Icon from 'flavours/glitch/components/icon'; -import AnimatedNumber from 'flavours/glitch/components/animated_number'; -import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; -import EditedTimestamp from 'flavours/glitch/components/edited_timestamp'; - -export default @injectIntl -class DetailedStatus extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - status: ImmutablePropTypes.map, - settings: ImmutablePropTypes.map.isRequired, - onOpenMedia: PropTypes.func.isRequired, - onOpenVideo: PropTypes.func.isRequired, - onToggleHidden: PropTypes.func, - onTranslate: PropTypes.func.isRequired, - expanded: PropTypes.bool, - measureHeight: PropTypes.bool, - onHeightChange: PropTypes.func, - domain: PropTypes.string.isRequired, - compact: PropTypes.bool, - showMedia: PropTypes.bool, - pictureInPicture: ImmutablePropTypes.contains({ - inUse: PropTypes.bool, - available: PropTypes.bool, - }), - onToggleMediaVisibility: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - state = { - height: null, - }; - - handleAccountClick = (e) => { - if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.context.router) { - e.preventDefault(); - let state = { ...this.context.router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state); - } - - e.stopPropagation(); - }; - - parseClick = (e, destination) => { - if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.context.router) { - e.preventDefault(); - let state = { ...this.context.router.history.location.state }; - state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1; - this.context.router.history.push(destination, state); - } - - e.stopPropagation(); - }; - - handleOpenVideo = (options) => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options); - }; - - _measureHeight (heightJustChanged) { - if (this.props.measureHeight && this.node) { - scheduleIdleTask(() => this.node && this.setState({ height: Math.ceil(this.node.scrollHeight) + 1 })); - - if (this.props.onHeightChange && heightJustChanged) { - this.props.onHeightChange(); - } - } - } - - setRef = c => { - this.node = c; - this._measureHeight(); - }; - - componentDidUpdate (prevProps, prevState) { - this._measureHeight(prevState.height !== this.state.height); - } - - handleChildUpdate = () => { - this._measureHeight(); - }; - - handleModalLink = e => { - e.preventDefault(); - - let href; - - if (e.target.nodeName !== 'A') { - href = e.target.parentNode.href; - } else { - href = e.target.href; - } - - window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes'); - }; - - handleTranslate = () => { - const { onTranslate, status } = this.props; - onTranslate(status); - }; - - render () { - const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; - const { expanded, onToggleHidden, settings, pictureInPicture, intl } = this.props; - const outerStyle = { boxSizing: 'border-box' }; - const { compact } = this.props; - - if (!status) { - return null; - } - - let applicationLink = ''; - let reblogLink = ''; - let reblogIcon = 'retweet'; - let favouriteLink = ''; - let edited = ''; - - // Depending on user settings, some media are considered as parts of the - // contents (affected by CW) while other will be displayed outside of the - // CW. - let contentMedia = []; - let contentMediaIcons = []; - let extraMedia = []; - let extraMediaIcons = []; - let media = contentMedia; - let mediaIcons = contentMediaIcons; - - if (settings.getIn(['content_warnings', 'media_outside'])) { - media = extraMedia; - mediaIcons = extraMediaIcons; - } - - if (this.props.measureHeight) { - outerStyle.height = `${this.state.height}px`; - } - - if (pictureInPicture.get('inUse')) { - media.push(); - mediaIcons.push('video-camera'); - } else if (status.get('media_attachments').size > 0) { - if (status.get('media_attachments').some(item => item.get('type') === 'unknown')) { - media.push(); - } else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') { - const attachment = status.getIn(['media_attachments', 0]); - - media.push( -