diff options
Diffstat (limited to 'app')
39 files changed, 251 insertions, 114 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index b3c2db02b..0b40fb05b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -43,6 +43,10 @@ class ApplicationController < ActionController::Base forbidden if current_user.account.suspended? end + def after_sign_out_path_for(_resource_or_scope) + new_user_session_path + end + protected def forbidden diff --git a/app/controllers/auth/passwords_controller.rb b/app/controllers/auth/passwords_controller.rb index 54ee1c39c..171b997dc 100644 --- a/app/controllers/auth/passwords_controller.rb +++ b/app/controllers/auth/passwords_controller.rb @@ -1,5 +1,20 @@ # frozen_string_literal: true class Auth::PasswordsController < Devise::PasswordsController + before_action :check_validity_of_reset_password_token, only: :edit + layout 'auth' + + private + + def check_validity_of_reset_password_token + unless reset_password_token_is_valid? + flash[:error] = I18n.t('auth.invalid_reset_password_token') + redirect_to new_password_path(resource_name) + end + end + + def reset_password_token_is_valid? + resource_class.with_reset_password_token(params[:reset_password_token]).present? + end end diff --git a/app/controllers/authorize_follows_controller.rb b/app/controllers/authorize_follows_controller.rb index dccd1c209..78b564183 100644 --- a/app/controllers/authorize_follows_controller.rb +++ b/app/controllers/authorize_follows_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class AuthorizeFollowsController < ApplicationController - layout 'public' + layout 'modal' before_action :authenticate_user! diff --git a/app/controllers/remote_follow_controller.rb b/app/controllers/remote_follow_controller.rb index 2988231b1..48b026aa5 100644 --- a/app/controllers/remote_follow_controller.rb +++ b/app/controllers/remote_follow_controller.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class RemoteFollowController < ApplicationController - layout 'public' + layout 'modal' before_action :set_account before_action :gone, if: :suspended_account? diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index 2204e0b14..0b5e72c17 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -23,6 +23,9 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; +export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT'; +export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT'; + export function fetchStatusRequest(id, skipLoading) { return { type: STATUS_FETCH_REQUEST, @@ -215,3 +218,17 @@ export function unmuteStatusFail(id, error) { error, }; }; + +export function setStatusHeight (id, height) { + return { + type: STATUS_SET_HEIGHT, + id, + height, + }; +}; + +export function clearStatusesHeight () { + return { + type: STATUSES_CLEAR_HEIGHT, + }; +}; diff --git a/app/javascript/mastodon/components/column.js b/app/javascript/mastodon/components/column.js index 29c8f4389..103fcd495 100644 --- a/app/javascript/mastodon/components/column.js +++ b/app/javascript/mastodon/components/column.js @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import detectPassiveEvents from 'detect-passive-events'; -import scrollTop from '../scroll'; +import { scrollTop } from '../scroll'; export default class Column extends React.PureComponent { diff --git a/app/javascript/mastodon/components/display_name.js b/app/javascript/mastodon/components/display_name.js index dc3665a2b..2cf84f8f4 100644 --- a/app/javascript/mastodon/components/display_name.js +++ b/app/javascript/mastodon/components/display_name.js @@ -1,7 +1,5 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; -import emojify from '../emoji'; export default class DisplayName extends React.PureComponent { @@ -10,12 +8,11 @@ export default class DisplayName extends React.PureComponent { }; render () { - const displayName = this.props.account.get('display_name').length === 0 ? this.props.account.get('username') : this.props.account.get('display_name'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const displayNameHtml = { __html: this.props.account.get('display_name_html') }; return ( <span className='display-name'> - <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHTML} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> + <strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /> <span className='display-name__account'>@{this.props.account.get('acct')}</span> </span> ); } diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 9bc3523c8..7468957d3 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -11,8 +11,6 @@ import DisplayName from './display_name'; import StatusContent from './status_content'; import StatusActionBar from './status_action_bar'; import { FormattedMessage } from 'react-intl'; -import emojify from '../emoji'; -import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; import scheduleIdleTask from '../features/ui/util/schedule_idle_task'; import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components'; @@ -39,6 +37,7 @@ export default class Status extends ImmutablePureComponent { onOpenMedia: PropTypes.func, onOpenVideo: PropTypes.func, onBlock: PropTypes.func, + onHeightChange: PropTypes.func, me: PropTypes.number, boostModal: PropTypes.bool, autoPlayGif: PropTypes.bool, @@ -50,7 +49,6 @@ export default class Status extends ImmutablePureComponent { state = { isExpanded: false, - isIntersecting: true, // assume intersecting until told otherwise isHidden: false, // set to true in requestIdleCallback to trigger un-render } @@ -111,6 +109,10 @@ export default class Status extends ImmutablePureComponent { if (this.node && this.node.children.length !== 0) { // save the height of the fully-rendered element this.height = getRectFromEntry(entry).height; + + if (this.props.onHeightChange) { + this.props.onHeightChange(this.props.status, this.height); + } } this.setState((prevState) => { @@ -182,9 +184,13 @@ export default class Status extends ImmutablePureComponent { return null; } - if (!isIntersecting && isHidden) { + const hasIntersectionObserverWrapper = !!this.props.intersectionObserverWrapper; + const isHiddenForSure = isIntersecting === false && isHidden; + const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status.has('height'); + + if (hasIntersectionObserverWrapper && (isHiddenForSure || visibilityUnknownButHeightIsCached)) { return ( - <article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }}> + <article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height || status.get('height')}px`, opacity: 0, overflow: 'hidden' }}> {status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])} {status.get('content')} </article> @@ -192,19 +198,13 @@ export default class Status extends ImmutablePureComponent { } if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { - let displayName = status.getIn(['account', 'display_name']); - - if (displayName.length === 0) { - displayName = status.getIn(['account', 'username']); - } - - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; return ( <article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'> <div className='status__prepend'> <div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div> - <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={displayNameHTML} /></a> }} /> + <FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} /> </div> <Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} /> diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 5f02e3261..d1381f176 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -3,9 +3,7 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import escapeTextContentForBrowser from 'escape-html'; import PropTypes from 'prop-types'; -import emojify from '../emoji'; import { isRtl } from '../rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; @@ -122,8 +120,8 @@ export default class StatusContent extends React.PureComponent { const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; - const content = { __html: emojify(status.get('content')) }; - const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) }; + const content = { __html: status.get('contentHtml') }; + const spoilerContent = { __html: status.get('spoilerHtml') }; const directionStyle = { direction: 'ltr' }; const classNames = classnames('status__content', { 'status__content--with-action': this.props.onClick && this.context.router, diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index 639c8b4e7..271cf33b7 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -105,7 +105,7 @@ export default class StatusList extends ImmutablePureComponent { } handleKeyDown = (e) => { - if (['PageDown', 'PageUp', 'End', 'Home'].includes(e.key)) { + if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) { const article = (() => { switch (e.key) { case 'PageDown': diff --git a/app/javascript/mastodon/containers/status_container.js b/app/javascript/mastodon/containers/status_container.js index 9b7f984e0..d71584267 100644 --- a/app/javascript/mastodon/containers/status_container.js +++ b/app/javascript/mastodon/containers/status_container.js @@ -19,7 +19,7 @@ import { blockAccount, muteAccount, } from '../actions/accounts'; -import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses'; +import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses'; import { initReport } from '../actions/reports'; import { openModal } from '../actions/modal'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -127,6 +127,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ } }, + onHeightChange (status, height) { + dispatch(setStatusHeight(status.get('id'), height)); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); diff --git a/app/javascript/mastodon/emoji.js b/app/javascript/mastodon/emoji.js index 5695c86dd..a41dfdd1d 100644 --- a/app/javascript/mastodon/emoji.js +++ b/app/javascript/mastodon/emoji.js @@ -3,34 +3,28 @@ import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); -const excluded = ['™', '©', '®']; - -function emojify(str) { - // This walks through the string from start to end, ignoring any tags (<p>, <br>, etc.) - // and replacing valid unicode strings - // that _aren't_ within tags with an <img> version. - // The goal is to be the same as an emojione.regUnicode replacement, but faster. - let i = -1; - let insideTag = false; - let match; - while (++i < str.length) { - const char = str.charAt(i); - if (insideTag && char === '>') { - insideTag = false; - } else if (char === '<') { - insideTag = true; - } else if (!insideTag && (match = trie.search(str.substring(i)))) { - const unicodeStr = match; - if (unicodeStr in unicodeMapping && excluded.indexOf(unicodeStr) === -1) { - const [filename, shortCode] = unicodeMapping[unicodeStr]; - const alt = unicodeStr; - const replacement = `<img draggable="false" class="emojione" alt="${alt}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; - str = str.substring(0, i) + replacement + str.substring(i + unicodeStr.length); - i += (replacement.length - unicodeStr.length); // jump ahead the length we've added to the string - } +const emojify = str => { + let rtn = ''; + for (;;) { + let match, i = 0; + while (i < str.length && str[i] !== '<' && !(match = trie.search(str.slice(i)))) { + i += str.codePointAt(i) < 65536 ? 1 : 2; + } + if (i === str.length) + break; + else if (str[i] === '<') { + let tagend = str.indexOf('>', i + 1) + 1; + if (!tagend) + break; + rtn += str.slice(0, tagend); + str = str.slice(tagend); + } else { + const [filename, shortCode] = unicodeMapping[match]; + rtn += str.slice(0, i) + `<img draggable="false" class="emojione" alt="${match}" title=":${shortCode}:" src="/emoji/${filename}.svg" />`; + str = str.slice(i + match.length); } } - return str; -} + return rtn + str; +}; export default emojify; diff --git a/app/javascript/mastodon/emojione_light.js b/app/javascript/mastodon/emojione_light.js index 985e9dbcb..0d07d012f 100644 --- a/app/javascript/mastodon/emojione_light.js +++ b/app/javascript/mastodon/emojione_light.js @@ -4,8 +4,10 @@ const emojione = require('emojione'); const mappedUnicode = emojione.mapUnicodeToShort(); +const excluded = ['®', '©', '™']; module.exports.unicodeMapping = Object.keys(emojione.jsEscapeMap) + .filter(c => !excluded.includes(c)) .map(unicodeStr => [unicodeStr, mappedUnicode[emojione.jsEscapeMap[unicodeStr]]]) .map(([unicodeStr, shortCode]) => ({ [unicodeStr]: [emojione.emojioneList[shortCode].fname, shortCode.slice(1, shortCode.length - 1)] })) .reduce((x, y) => Object.assign(x, y), { }); diff --git a/app/javascript/mastodon/features/account/components/header.js b/app/javascript/mastodon/features/account/components/header.js index 9d7bc82c0..320e669a2 100644 --- a/app/javascript/mastodon/features/account/components/header.js +++ b/app/javascript/mastodon/features/account/components/header.js @@ -4,8 +4,6 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import emojify from '../../../emoji'; -import escapeTextContentForBrowser from 'escape-html'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import IconButton from '../../../components/icon_button'; import Motion from 'react-motion/lib/Motion'; @@ -95,15 +93,10 @@ export default class Header extends ImmutablePureComponent { return null; } - let displayName = account.get('display_name'); let info = ''; let actionBtn = ''; let lockedIcon = ''; - if (displayName.length === 0) { - displayName = account.get('username'); - } - if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { info = <span className='account--follows-info'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>; } @@ -128,15 +121,15 @@ export default class Header extends ImmutablePureComponent { lockedIcon = <i className='fa fa-lock' />; } - const content = { __html: emojify(account.get('note')) }; - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; + const content = { __html: account.get('note_emojified') }; + const displayNameHtml = { __html: account.get('display_name_html') }; return ( <div className='account__header' style={{ backgroundImage: `url(${account.get('header')})` }}> <div> <Avatar account={account} autoPlayGif={this.props.autoPlayGif} /> - <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHTML} /> + <span className='account__header__display-name' dangerouslySetInnerHTML={displayNameHtml} /> <span className='account__header__username'>@{account.get('acct')} {lockedIcon}</span> <div className='account__header__content' dangerouslySetInnerHTML={content} /> diff --git a/app/javascript/mastodon/features/compose/components/reply_indicator.js b/app/javascript/mastodon/features/compose/components/reply_indicator.js index 35a9b4b1b..7672440b4 100644 --- a/app/javascript/mastodon/features/compose/components/reply_indicator.js +++ b/app/javascript/mastodon/features/compose/components/reply_indicator.js @@ -4,7 +4,6 @@ import PropTypes from 'prop-types'; import Avatar from '../../../components/avatar'; import IconButton from '../../../components/icon_button'; import DisplayName from '../../../components/display_name'; -import emojify from '../../../emoji'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -43,7 +42,7 @@ export default class ReplyIndicator extends ImmutablePureComponent { return null; } - const content = { __html: emojify(status.get('content')) }; + const content = { __html: status.get('contentHtml') }; return ( <div className='reply-indicator'> diff --git a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js index 66fa5c235..4fc5638d9 100644 --- a/app/javascript/mastodon/features/follow_requests/components/account_authorize.js +++ b/app/javascript/mastodon/features/follow_requests/components/account_authorize.js @@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import Permalink from '../../../components/permalink'; import Avatar from '../../../components/avatar'; import DisplayName from '../../../components/display_name'; -import emojify from '../../../emoji'; import IconButton from '../../../components/icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -26,7 +25,7 @@ export default class AccountAuthorize extends ImmutablePureComponent { render () { const { intl, account, onAuthorize, onReject } = this.props; - const content = { __html: emojify(account.get('note')) }; + const content = { __html: account.get('note_emojified') }; return ( <div className='account-authorize__wrapper'> diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index 0771849c2..7d521e4b6 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -7,8 +7,6 @@ import StatusContainer from '../../../containers/status_container'; import AccountContainer from '../../../containers/account_container'; import { FormattedMessage } from 'react-intl'; import Permalink from '../../../components/permalink'; -import emojify from '../../../emoji'; -import escapeTextContentForBrowser from 'escape-html'; import ImmutablePureComponent from 'react-immutable-pure-component'; export default class Notification extends ImmutablePureComponent { @@ -70,9 +68,8 @@ export default class Notification extends ImmutablePureComponent { render () { const { notification } = this.props; const account = notification.get('account'); - const displayName = account.get('display_name').length > 0 ? account.get('display_name') : account.get('username'); - const displayNameHTML = { __html: emojify(escapeTextContentForBrowser(displayName)) }; - const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHTML} />; + const displayNameHtml = { __html: account.get('display_name_html') }; + const link = <Permalink className='notification__display-name' href={account.get('url')} title={account.get('acct')} to={`/accounts/${account.get('id')}`} dangerouslySetInnerHTML={displayNameHtml} />; switch(notification.get('type')) { case 'follow': diff --git a/app/javascript/mastodon/features/report/components/status_check_box.js b/app/javascript/mastodon/features/report/components/status_check_box.js index 6a1a84c28..cc9232201 100644 --- a/app/javascript/mastodon/features/report/components/status_check_box.js +++ b/app/javascript/mastodon/features/report/components/status_check_box.js @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; -import emojify from '../../../emoji'; import Toggle from 'react-toggle'; export default class StatusCheckBox extends React.PureComponent { @@ -15,7 +14,7 @@ export default class StatusCheckBox extends React.PureComponent { render () { const { status, checked, onToggle, disabled } = this.props; - const content = { __html: emojify(status.get('content')) }; + const content = { __html: status.get('contentHtml') }; if (status.get('reblog')) { return null; diff --git a/app/javascript/mastodon/features/ui/components/column.js b/app/javascript/mastodon/features/ui/components/column.js index aea102aac..9031c16fc 100644 --- a/app/javascript/mastodon/features/ui/components/column.js +++ b/app/javascript/mastodon/features/ui/components/column.js @@ -2,7 +2,7 @@ import React from 'react'; import ColumnHeader from './column_header'; import PropTypes from 'prop-types'; import { debounce } from 'lodash'; -import scrollTop from '../../../scroll'; +import { scrollTop } from '../../../scroll'; import { isMobile } from '../../../is_mobile'; export default class Column extends React.PureComponent { diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 63bd1b021..47d5a2e20 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -12,6 +12,8 @@ import ColumnLoading from './column_loading'; import BundleColumnError from './bundle_column_error'; import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, FavouritedStatuses } from '../../ui/util/async-components'; +import { scrollRight } from '../../../scroll'; + const componentMap = { 'COMPOSE': Compose, 'HOME': HomeTimeline, @@ -49,9 +51,13 @@ export default class ColumnsArea extends ImmutablePureComponent { this.setState({ shouldAnimate: true }); } - componentDidUpdate() { + componentDidUpdate(prevProps) { this.lastIndex = getIndex(this.context.router.history.location.pathname); this.setState({ shouldAnimate: true }); + + if (this.props.children !== prevProps.children && !this.props.singleColumn) { + scrollRight(this.node); + } } handleSwipe = (index) => { @@ -74,6 +80,10 @@ export default class ColumnsArea extends ImmutablePureComponent { } } + setRef = (node) => { + this.node = node; + } + renderView = (link, index) => { const columnIndex = getIndex(this.context.router.history.location.pathname); const title = this.props.intl.formatMessage({ id: link.props['data-preview-title-id'] }); @@ -114,7 +124,7 @@ export default class ColumnsArea extends ImmutablePureComponent { } return ( - <div className='columns-area'> + <div className='columns-area' ref={this.setRef}> {columns.map(column => { const params = column.get('params', null) === null ? null : column.get('params').toJS(); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index f7a6eb319..6d53f474d 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -12,6 +12,7 @@ import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; +import { clearStatusesHeight } from '../../actions/statuses'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -72,6 +73,9 @@ export default class UI extends React.PureComponent { }; handleResize = debounce(() => { + // The cached heights are no longer accurate, invalidate + this.props.dispatch(clearStatusesHeight()); + this.setState({ width: window.innerWidth }); }, 500, { trailing: true, diff --git a/app/javascript/mastodon/locales/fa.json b/app/javascript/mastodon/locales/fa.json index d2682ef12..5ada62f93 100644 --- a/app/javascript/mastodon/locales/fa.json +++ b/app/javascript/mastodon/locales/fa.json @@ -1,7 +1,7 @@ { "account.block": "مسدودسازی @{name}", "account.block_domain": "پنهانسازی همه چیز از سرور {domain}", - "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", + "account.disclaimer_full": "اطلاعات زیر ممکن است نمایهٔ این کاربر را به تمامی نشان ندهد.", "account.edit_profile": "ویرایش نمایه", "account.follow": "پی بگیرید", "account.followers": "پیگیران", @@ -13,7 +13,7 @@ "account.posts": "نوشتهها", "account.report": "گزارش @{name}", "account.requested": "در انتظار پذیرش", - "account.share": "Share @{name}'s profile", + "account.share": "همرسانی نمایهٔ @{name}", "account.unblock": "رفع انسداد @{name}", "account.unblock_domain": "رفع پنهانسازی از {domain}", "account.unfollow": "پایان پیگیری", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index ad9060d25..f3f0d0463 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -13,7 +13,7 @@ "account.posts": "Statuts", "account.report": "Signaler", "account.requested": "Invitation envoyée", - "account.share": "Share @{name}'s profile", + "account.share": "Partager le profil de @{name}", "account.unblock": "Débloquer", "account.unblock_domain": "Ne plus masquer {domain}", "account.unfollow": "Ne plus suivre", @@ -35,11 +35,11 @@ "column.notifications": "Notifications", "column.public": "Fil public global", "column_back_button.label": "Retour", - "column_header.hide_settings": "Hide settings", - "column_header.moveLeft_settings": "Move column to the left", - "column_header.moveRight_settings": "Move column to the right", + "column_header.hide_settings": "Masquer les paramètres", + "column_header.moveLeft_settings": "Déplacer la colonne vers la gauche", + "column_header.moveRight_settings": "Déplacer la colonne vers la droite", "column_header.pin": "Épingler", - "column_header.show_settings": "Show settings", + "column_header.show_settings": "Afficher les paramètres", "column_header.unpin": "Retirer", "column_subheading.navigation": "Navigation", "column_subheading.settings": "Paramètres", @@ -94,8 +94,8 @@ "home.column_settings.show_replies": "Afficher les réponses", "home.settings": "Paramètres de la colonne", "lightbox.close": "Fermer", - "lightbox.next": "Next", - "lightbox.previous": "Previous", + "lightbox.next": "Suivant", + "lightbox.previous": "Précédent", "loading_indicator.label": "Chargement…", "media_gallery.toggle_visible": "Modifier la visibilité", "missing_indicator.label": "Non trouvé", @@ -175,7 +175,7 @@ "status.report": "Signaler @{name}", "status.sensitive_toggle": "Cliquer pour afficher", "status.sensitive_warning": "Contenu sensible", - "status.share": "Share", + "status.share": "Partager", "status.show_less": "Replier", "status.show_more": "Déplier", "status.unmute_conversation": "Ne plus masquer la conversation", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index c42721f64..542230f11 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -73,7 +73,7 @@ "emoji_button.search": "Szukaj...", "emoji_button.symbols": "Symbole", "emoji_button.travel": "Podróże i miejsca", - "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby odbić piłeczkę!", + "empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!", "empty_column.hashtag": "Nie ma postów oznaczonych tym hashtagiem. Możesz napisać pierwszy!", "empty_column.home": "Nie śledzisz nikogo. Odwiedź publiczną oś czasu lub użyj wyszukiwarki, aby znaleźć interesujące Cię profile.", "empty_column.home.inactivity": "Strumień jest pusty. Jeżeli nie było Cię tu ostatnio, zostanie on wypełniony wkrótce.", @@ -159,7 +159,7 @@ "report.target": "Zgłaszanie {target}", "search.placeholder": "Szukaj", "search_results.total": "{count, number} {count, plural, one {wynik} more {wyniki}}", - "standalone.public_title": "Spojrzenie wgłąb…", + "standalone.public_title": "Spojrzenie w głąb…", "status.cannot_reblog": "Ten post nie może zostać podbity", "status.delete": "Usuń", "status.favourite": "Ulubione", @@ -178,7 +178,7 @@ "status.share": "Udostępnij", "status.show_less": "Pokaż mniej", "status.show_more": "Pokaż więcej", - "status.unmute_conversation": "Cofnij wyciezenie konwersacji", + "status.unmute_conversation": "Cofnij wyciszenie konwersacji", "tabs_bar.compose": "Napisz", "tabs_bar.federated_timeline": "Globalne", "tabs_bar.home": "Strona główna", diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index 4d7c3adc9..6442d13be 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -44,7 +44,9 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { STORE_HYDRATE } from '../actions/store'; +import emojify from '../emoji'; import { Map as ImmutableMap, fromJS } from 'immutable'; +import escapeTextContentForBrowser from 'escape-html'; const normalizeAccount = (state, account) => { account = { ...account }; @@ -53,6 +55,10 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; + const displayName = account.display_name.length === 0 ? account.username : account.display_name; + account.display_name_html = emojify(escapeTextContentForBrowser(displayName)); + account.note_emojified = emojify(account.note); + return state.set(account.id, fromJS(account)); }; diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index b1b1d0988..3e40b0b42 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -13,6 +13,8 @@ import { CONTEXT_FETCH_SUCCESS, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, + STATUS_SET_HEIGHT, + STATUSES_CLEAR_HEIGHT, } from '../actions/statuses'; import { TIMELINE_REFRESH_SUCCESS, @@ -33,7 +35,11 @@ import { FAVOURITED_STATUSES_EXPAND_SUCCESS, } from '../actions/favourites'; import { SEARCH_FETCH_SUCCESS } from '../actions/search'; +import emojify from '../emoji'; import { Map as ImmutableMap, fromJS } from 'immutable'; +import escapeTextContentForBrowser from 'escape-html'; + +const domParser = new DOMParser(); const normalizeStatus = (state, status) => { if (!status) { @@ -49,7 +55,9 @@ const normalizeStatus = (state, status) => { } const searchContent = [status.spoiler_text, status.content].join(' ').replace(/<br \/>/g, '\n').replace(/<\/p><p>/g, '\n\n'); - normalStatus.search_index = new DOMParser().parseFromString(searchContent, 'text/html').documentElement.textContent; + normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent; + normalStatus.contentHtml = emojify(normalStatus.content); + normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || '')); return state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); }; @@ -82,6 +90,18 @@ const filterStatuses = (state, relationship) => { return state; }; +const setHeight = (state, id, height) => { + return state.update(id, ImmutableMap(), map => map.set('height', height)); +}; + +const clearHeights = (state) => { + state.forEach(status => { + state = state.deleteIn([status.get('id'), 'height']); + }); + + return state; +}; + const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { @@ -120,6 +140,10 @@ export default function statuses(state = initialState, action) { return deleteStatus(state, action.id, action.references); case ACCOUNT_BLOCK_SUCCESS: return filterStatuses(state, action.relationship); + case STATUS_SET_HEIGHT: + return setHeight(state, action.id, action.height); + case STATUSES_CLEAR_HEIGHT: + return clearHeights(state); default: return state; } diff --git a/app/javascript/mastodon/scroll.js b/app/javascript/mastodon/scroll.js index c089d37db..44f95b17f 100644 --- a/app/javascript/mastodon/scroll.js +++ b/app/javascript/mastodon/scroll.js @@ -1,9 +1,9 @@ const easingOutQuint = (x, t, b, c, d) => c * ((t = t / d - 1) * t * t * t * t + 1) + b; -const scrollTop = (node) => { +const scroll = (node, key, target) => { const startTime = Date.now(); - const offset = node.scrollTop; - const targetY = -offset; + const offset = node[key]; + const gap = target - offset; const duration = 1000; let interrupt = false; @@ -15,7 +15,7 @@ const scrollTop = (node) => { return; } - node.scrollTop = easingOutQuint(0, elapsed, offset, targetY, duration); + node[key] = easingOutQuint(0, elapsed, offset, gap, duration); requestAnimationFrame(step); }; @@ -26,4 +26,5 @@ const scrollTop = (node) => { }; }; -export default scrollTop; +export const scrollRight = (node) => scroll(node, 'scrollLeft', node.scrollWidth); +export const scrollTop = (node) => scroll(node, 'scrollTop', 0); diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss index 3d5c1a692..5a9105109 100644 --- a/app/javascript/styles/accounts.scss +++ b/app/javascript/styles/accounts.scss @@ -7,7 +7,7 @@ box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); overflow: hidden; - @media screen and (max-width: 700px) { + @media screen and (max-width: 740px) { border-radius: 0; box-shadow: none; } @@ -298,7 +298,7 @@ display: flex; flex-wrap: wrap; - @media screen and (max-width: 700px) { + @media screen and (max-width: 740px) { border-radius: 0; box-shadow: none; } diff --git a/app/javascript/styles/basics.scss b/app/javascript/styles/basics.scss index 182ea36a4..4e51b555c 100644 --- a/app/javascript/styles/basics.scss +++ b/app/javascript/styles/basics.scss @@ -47,7 +47,7 @@ body { padding: 0; } - @media screen and (max-width: 360px) { + @media screen and (max-width: 400px) { padding-bottom: 0; } } diff --git a/app/javascript/styles/compact_header.scss b/app/javascript/styles/compact_header.scss index 27a67135f..cf12fcfec 100644 --- a/app/javascript/styles/compact_header.scss +++ b/app/javascript/styles/compact_header.scss @@ -3,9 +3,15 @@ font-size: 24px; line-height: 28px; color: $ui-primary-color; - overflow: hidden; font-weight: 500; margin-bottom: 20px; + padding: 0 10px; + overflow-wrap: break-word; + + @media screen and (max-width: 740px) { + text-align: center; + padding: 20px 10px 0; + } a { color: inherit; diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 41735c7a4..b5efd560f 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1835,7 +1835,6 @@ overflow-y: scroll; overflow-x: hidden; flex: 1 1 auto; - backface-visibility: hidden; -webkit-overflow-scrolling: touch; @supports(display: grid) { // hack to fix Chrome <57 contain: strict; @@ -1853,8 +1852,9 @@ flex: 0 0 auto; font-size: 16px; border: 0; - text-align: start; + text-align: unset; padding: 15px; + margin: 0; z-index: 3; &:hover { diff --git a/app/javascript/styles/containers.scss b/app/javascript/styles/containers.scss index 7dcf2c006..536f4e5a1 100644 --- a/app/javascript/styles/containers.scss +++ b/app/javascript/styles/containers.scss @@ -3,7 +3,7 @@ margin: 0 auto; margin-top: 40px; - @media screen and (max-width: 700px) { + @media screen and (max-width: 740px) { width: 100%; margin: 0; } @@ -13,8 +13,9 @@ margin: 100px auto; margin-bottom: 50px; - @media screen and (max-width: 360px) { + @media screen and (max-width: 400px) { margin: 30px auto; + margin-bottom: 20px; } h1 { @@ -42,3 +43,54 @@ } } } + +.account-header { + width: 400px; + margin: 0 auto; + display: flex; + font-size: 13px; + line-height: 18px; + box-sizing: border-box; + padding: 20px 0; + padding-bottom: 0; + margin-bottom: -30px; + margin-top: 40px; + + @media screen and (max-width: 400px) { + width: 100%; + margin: 0; + margin-bottom: 10px; + padding: 20px; + padding-bottom: 0; + } + + .avatar { + width: 40px; + height: 40px; + margin-right: 8px; + + img { + width: 100%; + height: 100%; + display: block; + margin: 0; + border-radius: 4px; + } + } + + .name { + flex: 1 1 auto; + color: $ui-secondary-color; + + .username { + display: block; + font-weight: 500; + } + } + + .logout-link { + display: block; + font-size: 32px; + line-height: 40px; + } +} diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index cffb6f197..62094e98e 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -317,7 +317,7 @@ code { } .flash-message { - background: $ui-base-color; + background: lighten($ui-base-color, 8%); color: $ui-primary-color; border-radius: 4px; padding: 15px 10px; diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 7b89305ac..cacc0364f 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -104,7 +104,7 @@ class Formatter html_attrs = { target: '_blank', rel: 'nofollow noopener' } Twitter::Autolink.send(:link_to_text, entity, link_html(entity[:url]), normalized_url, html_attrs) - rescue Addressable::URI::InvalidURIError + rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError encode(entity[:url]) end diff --git a/app/models/concerns/account_avatar.rb b/app/models/concerns/account_avatar.rb index a6527a85b..b0ec689a7 100644 --- a/app/models/concerns/account_avatar.rb +++ b/app/models/concerns/account_avatar.rb @@ -8,7 +8,7 @@ module AccountAvatar class_methods do def avatar_styles(file) styles = { original: '120x120#' } - styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles[:static] = { animated: false } if file.content_type == 'image/gif' styles end diff --git a/app/models/concerns/account_header.rb b/app/models/concerns/account_header.rb index 4ba9212a2..542e25abe 100644 --- a/app/models/concerns/account_header.rb +++ b/app/models/concerns/account_header.rb @@ -8,7 +8,7 @@ module AccountHeader class_methods do def header_styles(file) styles = { original: '700x335#' } - styles[:static] = { format: 'png' } if file.content_type == 'image/gif' + styles[:static] = { animated: false } if file.content_type == 'image/gif' styles end diff --git a/app/views/authorize_follows/show.html.haml b/app/views/authorize_follows/show.html.haml index 3b60df058..f7a8f72d2 100644 --- a/app/views/authorize_follows/show.html.haml +++ b/app/views/authorize_follows/show.html.haml @@ -3,10 +3,9 @@ .form-container .follow-prompt - %h2= t('authorize_follow.prompt_html', self: current_account.username) - = render 'card', account: @account - = form_tag authorize_follow_path, method: :post, class: 'simple_form' do - = hidden_field_tag :acct, @account.acct - = button_tag t('authorize_follow.follow'), type: :submit + - unless current_account.following?(@account) + = form_tag authorize_follow_path, method: :post, class: 'simple_form' do + = hidden_field_tag :acct, @account.acct + = button_tag t('authorize_follow.follow'), type: :submit diff --git a/app/views/layouts/modal.html.haml b/app/views/layouts/modal.html.haml new file mode 100644 index 000000000..a819e098d --- /dev/null +++ b/app/views/layouts/modal.html.haml @@ -0,0 +1,16 @@ +- content_for :header_tags do + = javascript_pack_tag 'public', integrity: true, crossorigin: 'anonymous' + +- content_for :content do + - if user_signed_in? + .account-header + .avatar= image_tag current_account.avatar.url(:original) + .name + = t 'users.signed_in_as' + %span.username @#{current_account.local_username_and_domain} + = link_to destroy_user_session_path, method: :delete, class: 'logout-link icon-button' do + = fa_icon 'sign-out' + + .container= yield + += render template: 'layouts/application' diff --git a/app/views/tags/show.html.haml b/app/views/tags/show.html.haml index 15bf714c2..8cd2f1825 100644 --- a/app/views/tags/show.html.haml +++ b/app/views/tags/show.html.haml @@ -4,6 +4,7 @@ .compact-header %h1< = link_to site_title, root_path + %br %small ##{@tag.name} - if @statuses.empty? |