diff options
Diffstat (limited to 'app/javascript/flavours')
74 files changed, 1598 insertions, 662 deletions
diff --git a/app/javascript/flavours/glitch/actions/dropdown_menu.js b/app/javascript/flavours/glitch/actions/dropdown_menu.js index 217ba4e74..14f2939c7 100644 --- a/app/javascript/flavours/glitch/actions/dropdown_menu.js +++ b/app/javascript/flavours/glitch/actions/dropdown_menu.js @@ -1,8 +1,8 @@ export const DROPDOWN_MENU_OPEN = 'DROPDOWN_MENU_OPEN'; export const DROPDOWN_MENU_CLOSE = 'DROPDOWN_MENU_CLOSE'; -export function openDropdownMenu(id, placement) { - return { type: DROPDOWN_MENU_OPEN, id, placement }; +export function openDropdownMenu(id, placement, keyboard) { + return { type: DROPDOWN_MENU_OPEN, id, placement, keyboard }; } export function closeDropdownMenu(id) { diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index e88eda78f..fb84cd01e 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -25,6 +25,11 @@ export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL'; export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR'; export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP'; +export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; +export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; + +export const NOTIFICATIONS_SET_VISIBILITY = 'NOTIFICATIONS_SET_VISIBILITY'; + defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, }); @@ -216,3 +221,22 @@ export function deleteMarkedNotificationsSuccess() { type: NOTIFICATIONS_DELETE_MARKED_SUCCESS, }; }; + +export function mountNotifications() { + return { + type: NOTIFICATIONS_MOUNT, + }; +}; + +export function unmountNotifications() { + return { + type: NOTIFICATIONS_UNMOUNT, + }; +}; + +export function notificationsSetVisibility(visibility) { + return { + type: NOTIFICATIONS_SET_VISIBILITY, + visibility: visibility, + }; +}; diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 1c2b0bf25..05611c135 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -23,6 +23,7 @@ class DropdownMenu extends React.PureComponent { placement: PropTypes.string, arrowOffsetLeft: PropTypes.string, arrowOffsetTop: PropTypes.string, + openedViaKeyboard: PropTypes.bool, }; static defaultProps = { @@ -42,13 +43,15 @@ class DropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('keydown', this.handleKeyDown, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - if (this.focusedItem) this.focusedItem.focus(); + if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus(); this.setState({ mounted: true }); } componentWillUnmount () { document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('keydown', this.handleKeyDown, false); document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); } @@ -62,13 +65,10 @@ class DropdownMenu extends React.PureComponent { handleKeyDown = e => { const items = Array.from(this.node.getElementsByTagName('a')); - const index = items.indexOf(e.currentTarget); + const index = items.indexOf(document.activeElement); let element; switch(e.key) { - case 'Enter': - this.handleClick(e); - break; case 'ArrowDown': element = items[index+1]; if (element) { @@ -96,6 +96,12 @@ class DropdownMenu extends React.PureComponent { } } + handleItemKeyDown = e => { + if (e.key === 'Enter') { + this.handleClick(e); + } + } + handleClick = e => { const i = Number(e.currentTarget.getAttribute('data-index')); const { action, to } = this.props.items[i]; @@ -120,7 +126,7 @@ class DropdownMenu extends React.PureComponent { return ( <li className='dropdown-menu__item' key={`${text}-${i}`}> - <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}> + <a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}> {text} </a> </li> @@ -170,6 +176,7 @@ export default class Dropdown extends React.PureComponent { onClose: PropTypes.func.isRequired, dropdownPlacement: PropTypes.string, openDropdownId: PropTypes.number, + openedViaKeyboard: PropTypes.bool, }; static defaultProps = { @@ -180,14 +187,14 @@ export default class Dropdown extends React.PureComponent { id: id++, }; - handleClick = ({ target }) => { + handleClick = ({ target, type }) => { if (this.state.id === this.props.openDropdownId) { this.handleClose(); } else { const { top } = target.getBoundingClientRect(); const placement = top * 2 < innerHeight ? 'bottom' : 'top'; - this.props.onOpen(this.state.id, this.handleItemClick, placement); + this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click'); } } @@ -197,6 +204,11 @@ export default class Dropdown extends React.PureComponent { handleKeyDown = e => { switch(e.key) { + case ' ': + case 'Enter': + this.handleClick(e); + e.preventDefault(); + break; case 'Escape': this.handleClose(); break; @@ -232,7 +244,7 @@ export default class Dropdown extends React.PureComponent { } render () { - const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId } = this.props; + const { icon, items, size, ariaLabel, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props; const open = this.state.id === openDropdownId; return ( @@ -248,7 +260,7 @@ export default class Dropdown extends React.PureComponent { /> <Overlay show={open} placement={dropdownPlacement} target={this.findTarget}> - <DropdownMenu items={items} onClose={this.handleClose} /> + <DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} /> </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index 3faf0b453..605a2862b 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -70,7 +70,7 @@ class Item extends React.PureComponent { handleClick = (e) => { const { index, onClick } = this.props; - if (e.button === 0) { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { e.preventDefault(); onClick(index); } @@ -78,6 +78,11 @@ class Item extends React.PureComponent { e.stopPropagation(); } + handleMouseDown = (e) => { + e.preventDefault(); + e.stopPropagation(); + } + render () { const { attachment, index, size, standalone, letterbox, displayWidth } = this.props; @@ -163,7 +168,8 @@ class Item extends React.PureComponent { sizes={sizes} alt={attachment.get('description')} title={attachment.get('description')} - style={{ objectPosition: `${x}% ${y}%` }} /> + style={{ objectPosition: letterbox ? null : `${x}% ${y}%` }} + /> </a> ); } else if (attachment.get('type') === 'gifv') { @@ -180,6 +186,7 @@ class Item extends React.PureComponent { onClick={this.handleClick} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} + onMouseDown={this.handleMouseDown} autoPlay={autoPlay} loop muted @@ -191,7 +198,7 @@ class Item extends React.PureComponent { } return ( - <div className={classNames('media-gallery__item', { standalone })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> + <div className={classNames('media-gallery__item', { standalone, letterbox })} key={attachment.get('id')} style={{ left: left, top: top, right: right, bottom: bottom, width: `${width}%`, height: `${height}%` }}> {thumbnail} </div> ); @@ -261,6 +268,8 @@ export default class MediaGallery extends React.PureComponent { if (this.isStandaloneEligible() && width) { style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); + } else if (width) { + style.height = width / (16/9); } if (!visible) { @@ -280,7 +289,7 @@ export default class MediaGallery extends React.PureComponent { } } - const computedClass = classNames('media-gallery', `size-${size}`, { 'full-width': fullwidth }); + const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); return ( <div className={computedClass} style={style} ref={this.handleRef}> diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index b96b4dd98..a677cbf5b 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -149,6 +149,10 @@ export default class ScrollableList extends PureComponent { this.props.onLoadMore(); } + defaultShouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; @@ -190,7 +194,7 @@ export default class ScrollableList extends PureComponent { if (trackScroll) { return ( - <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> + <ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll || this.defaultShouldUpdateScroll}> {scrollableArea} </ScrollContainer> ); diff --git a/app/javascript/flavours/glitch/components/status.js b/app/javascript/flavours/glitch/components/status.js index 13ff781fe..4d47660c2 100644 --- a/app/javascript/flavours/glitch/components/status.js +++ b/app/javascript/flavours/glitch/components/status.js @@ -450,6 +450,7 @@ export default class Status extends ImmutablePureComponent { sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} fullwidth={settings.getIn(['media', 'fullwidth'])} + preventPlayback={isCollapsed || !isExpanded} onOpenVideo={this.handleOpenVideo} />)} </Bundle> diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index e26bdb717..f7e741d2d 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -5,7 +5,7 @@ import IconButton from './icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, isStaff } from 'flavours/glitch/util/initial_state'; import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ @@ -31,6 +31,8 @@ const messages = defineMessages({ 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' }, }); const obfuscatedCount = count => { @@ -186,6 +188,11 @@ export default class StatusActionBar extends ImmutablePureComponent { 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 (isStaff) { + menu.push(null); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + } } if (status.get('in_reply_to_id', null) === null) { diff --git a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js index dccd93dab..b2419a0fd 100644 --- a/app/javascript/flavours/glitch/containers/dropdown_menu_container.js +++ b/app/javascript/flavours/glitch/containers/dropdown_menu_container.js @@ -8,20 +8,21 @@ const mapStateToProps = state => ({ isModalOpen: state.get('modal').modalType === 'ACTIONS', dropdownPlacement: state.getIn(['dropdown_menu', 'placement']), openDropdownId: state.getIn(['dropdown_menu', 'openId']), + openedViaKeyboard: state.getIn(['dropdown_menu', 'keyboard']), }); const mapDispatchToProps = (dispatch, { status, items }) => ({ - onOpen(id, onItemClick, dropdownPlacement) { + onOpen(id, onItemClick, dropdownPlacement, keyboard) { dispatch(isUserTouching() ? openModal('ACTIONS', { status, actions: items.map( (item, i) => item ? { ...item, name: `${item.text}-${i}`, - onClick: (e) => { return onItemClick(i, e) }, + onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null, } : null ), - }) : openDropdownMenu(id, dropdownPlacement)); + }) : openDropdownMenu(id, dropdownPlacement, keyboard)); }, onClose(id) { dispatch(closeModal()); diff --git a/app/javascript/flavours/glitch/features/account/components/action_bar.js b/app/javascript/flavours/glitch/features/account/components/action_bar.js index 9c80a470b..3d6eeb06a 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -2,9 +2,9 @@ import React from 'react'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; -import { Link } from 'react-router-dom'; +import { NavLink } from 'react-router-dom'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, isStaff } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, @@ -25,6 +25,7 @@ const messages = defineMessages({ showReblogs: { id: 'account.show_reblogs', defaultMessage: 'Show boosts from @{name}' }, endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, + admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, }); @injectIntl @@ -51,6 +52,13 @@ export default class ActionBar extends React.PureComponent { }); } + isStatusesPageActive = (match, location) => { + if (!match) { + return false; + } + return !location.pathname.match(/\/(followers|following)\/?$/); + } + render () { const { account, intl } = this.props; @@ -120,6 +128,11 @@ export default class ActionBar extends React.PureComponent { } } + if (account.get('id') !== me && isStaff) { + menu.push(null); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + } + return ( <div> {extraInfo} @@ -130,20 +143,20 @@ export default class ActionBar extends React.PureComponent { </div> <div className='account__action-bar-links'> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> + <NavLink isActive={this.isStatusesPageActive} activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}`}> <FormattedMessage id='account.posts' defaultMessage='Posts' /> <strong><FormattedNumber value={account.get('statuses_count')} /></strong> - </Link> + </NavLink> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> + <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/following`}> <FormattedMessage id='account.follows' defaultMessage='Follows' /> <strong><FormattedNumber value={account.get('following_count')} /></strong> - </Link> + </NavLink> - <Link className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> + <NavLink exact activeClassName='active' className='account__action-bar__tab' to={`/accounts/${account.get('id')}/followers`}> <FormattedMessage id='account.followers' defaultMessage='Followers' /> <strong><FormattedNumber value={account.get('followers_count')} /></strong> - </Link> + </NavLink> </div> </div> </div> diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index eda0d637e..f0d36947d 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -15,8 +15,19 @@ const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' }, unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + link_verified_on: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' }, }); +const dateFormatOptions = { + month: 'short', + day: 'numeric', + year: 'numeric', + hour12: false, + hour: '2-digit', + minute: '2-digit', +}; + @injectIntl export default class Header extends ImmutablePureComponent { @@ -27,6 +38,10 @@ export default class Header extends ImmutablePureComponent { intl: PropTypes.object.isRequired, }; + openEditProfile = () => { + window.open('/settings/profile', '_blank'); + } + render () { const { account, intl } = this.props; @@ -77,6 +92,12 @@ export default class Header extends ImmutablePureComponent { </div> ); } + } else { + actionBtn = ( + <div className='account--action-button'> + <IconButton size={26} icon='pencil' title={intl.formatMessage(messages.edit_profile)} onClick={this.openEditProfile} /> + </div> + ); } if (account.get('moved') && !account.getIn(['relationship', 'following'])) { @@ -111,7 +132,9 @@ export default class Header extends ImmutablePureComponent { {fields.map((pair, i) => ( <dl key={i}> <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> - <dd dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} title={pair.get('value_plain')} /> + <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> + {pair.get('verified_at') && <span title={intl.formatMessage(messages.link_verified_on, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><i className='fa fa-check verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> + </dd> </dl> ))} </div> diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 20ba0a1b1..2216f9153 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -60,10 +60,6 @@ export default class AccountTimeline extends ImmutablePureComponent { this.props.dispatch(expandAccountTimeline(this.props.params.accountId, { maxId, withReplies: this.props.withReplies })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { statusIds, featuredStatusIds, isLoading, hasMore } = this.props; @@ -87,7 +83,6 @@ export default class AccountTimeline extends ImmutablePureComponent { isLoading={isLoading} hasMore={hasMore} onLoadMore={this.handleLoadMore} - shouldUpdateScroll={this.shouldUpdateScroll} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js index f1b4f947e..9468ad81d 100644 --- a/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js +++ b/app/javascript/flavours/glitch/features/bookmarked_statuses/index.js @@ -66,10 +66,6 @@ export default class Bookmarks extends ImmutablePureComponent { this.props.dispatch(expandBookmarkedStatuses()); }, 300, { leading: true }) - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; @@ -91,7 +87,6 @@ export default class Bookmarks extends ImmutablePureComponent { trackScroll={!pinned} statusIds={statusIds} scrollKey={`bookmarked_statuses-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/flavours/glitch/features/community_timeline/index.js b/app/javascript/flavours/glitch/features/community_timeline/index.js index 5bd9d037c..ddcca2dc0 100644 --- a/app/javascript/flavours/glitch/features/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/community_timeline/index.js @@ -71,10 +71,6 @@ export default class CommunityTimeline extends React.PureComponent { this.props.dispatch(expandCommunityTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, hasUnread, columnId, multiColumn } = this.props; const pinned = !!columnId; @@ -97,7 +93,6 @@ export default class CommunityTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} timelineId='community' onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index bc409f0a3..257797047 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -102,6 +102,7 @@ function mapStateToProps (state) { anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, spoilersAlwaysOn: spoilersAlwaysOn, mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']), + preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']), }; }; @@ -146,7 +147,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ onMount() { dispatch(mountCompose()); }, - onOpenActionModal(props) { + onOpenActionsModal(props) { dispatch(openModal('ACTIONS', props)); }, onOpenDoodleModal() { @@ -242,7 +243,7 @@ const handlers = { } // Submit disabled: - if (isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia)) { + if (isSubmitting || isUploading || (!text.trim().length && !anyMedia)) { return; } @@ -328,13 +329,14 @@ class Composer extends React.Component { isSubmitting, preselectDate, text, + preselectOnReply, } = this.props; let selectionEnd, selectionStart; // Caret/selection handling. if (focusDate !== prevProps.focusDate) { switch (true) { - case preselectDate !== prevProps.preselectDate: + case preselectDate !== prevProps.preselectDate && preselectOnReply: selectionStart = text.search(/\s/) + 1; selectionEnd = text.length; break; @@ -347,6 +349,7 @@ class Composer extends React.Component { if (textarea) { textarea.setSelectionRange(selectionStart, selectionEnd); textarea.focus(); + textarea.scrollIntoView(); } // Refocuses the textarea after submitting. @@ -415,7 +418,7 @@ class Composer extends React.Component { spoilersAlwaysOn, } = this.props; - let disabledButton = isSubmitting || isUploading || (!!text.length && !text.trim().length && !anyMedia); + let disabledButton = isSubmitting || isUploading || (!text.trim().length && !anyMedia); return ( <div className='composer'> @@ -485,8 +488,8 @@ class Composer extends React.Component { onUpload={onUpload} privacy={privacy} resetFileKey={resetFileKey} - sensitive={sensitive} - spoiler={spoiler} + sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} + spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} /> <ComposerPublisher countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`} @@ -533,6 +536,7 @@ Composer.propTypes = { anyMedia: PropTypes.bool, spoilersAlwaysOn: PropTypes.bool, mediaDescriptionConfirmation: PropTypes.bool, + preselectOnReply: PropTypes.bool, // Dispatch props. onCancelReply: PropTypes.func, diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js index 331692398..1b7ae8904 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js @@ -11,7 +11,7 @@ import { unicodeMapping } from 'flavours/glitch/util/emoji'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; // Gets our asset host from the environment, if available. -const assetHost = ((process || {}).env || {}).CDN_HOST || ''; +const assetHost = process.env.CDN_HOST || ''; // Handlers. const handlers = { diff --git a/app/javascript/flavours/glitch/features/direct_timeline/index.js b/app/javascript/flavours/glitch/features/direct_timeline/index.js index 949c85dce..dc7e0534d 100644 --- a/app/javascript/flavours/glitch/features/direct_timeline/index.js +++ b/app/javascript/flavours/glitch/features/direct_timeline/index.js @@ -71,10 +71,6 @@ export default class DirectTimeline extends React.PureComponent { this.props.dispatch(expandDirectTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, hasUnread, columnId, multiColumn } = this.props; const pinned = !!columnId; @@ -97,7 +93,6 @@ export default class DirectTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`direct_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} timelineId='direct' onLoadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />} diff --git a/app/javascript/flavours/glitch/features/domain_blocks/index.js b/app/javascript/flavours/glitch/features/domain_blocks/index.js index 8b023e0bc..3b29e2a26 100644 --- a/app/javascript/flavours/glitch/features/domain_blocks/index.js +++ b/app/javascript/flavours/glitch/features/domain_blocks/index.js @@ -40,10 +40,6 @@ export default class Blocks extends ImmutablePureComponent { this.props.dispatch(expandDomainBlocks()); }, 300, { leading: true }); - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, domains } = this.props; @@ -58,7 +54,7 @@ export default class Blocks extends ImmutablePureComponent { return ( <Column icon='minus-circle' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore} shouldUpdateScroll={this.shouldUpdateScroll}> + <ScrollableList scrollKey='domain_blocks' onLoadMore={this.handleLoadMore}> {domains.map(domain => <DomainContainer key={domain} domain={domain} /> )} diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js index deec42435..7fefd32c9 100644 --- a/app/javascript/flavours/glitch/features/drawer/header/index.js +++ b/app/javascript/flavours/glitch/features/drawer/header/index.js @@ -46,6 +46,8 @@ const messages = defineMessages({ // The component. export default function DrawerHeader ({ columns, + unreadNotifications, + showNotificationsBadge, intl, onSettingsClick, }) { @@ -77,7 +79,12 @@ export default function DrawerHeader ({ aria-label={intl.formatMessage(messages.notifications)} title={intl.formatMessage(messages.notifications)} to='/notifications' - ><Icon icon='bell' /></Link> + > + <span className='icon-badge-wrapper'> + <Icon icon='bell' /> + { showNotificationsBadge && unreadNotifications > 0 && <div className='icon-badge' />} + </span> + </Link> ))} {renderForColumn('COMMUNITY', ( <Link @@ -112,6 +119,8 @@ export default function DrawerHeader ({ // Props. DrawerHeader.propTypes = { columns: ImmutablePropTypes.list, + unreadNotifications: PropTypes.number, + showNotificationsBadge: PropTypes.bool, intl: PropTypes.object, onSettingsClick: PropTypes.func, }; diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js index 497b7d0e4..038a2513e 100644 --- a/app/javascript/flavours/glitch/features/drawer/index.js +++ b/app/javascript/flavours/glitch/features/drawer/index.js @@ -40,6 +40,8 @@ const mapStateToProps = state => ({ searchHidden: state.getIn(['search', 'hidden']), searchValue: state.getIn(['search', 'value']), submitted: state.getIn(['search', 'submitted']), + unreadNotifications: state.getIn(['notifications', 'unread']), + showNotificationsBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']), }); // Dispatch mapping. @@ -93,6 +95,8 @@ class Drawer extends React.Component { searchValue, submitted, isSearchPage, + unreadNotifications, + showNotificationsBadge, } = this.props; const computedClass = classNames('drawer', `mbstobon-${elefriend}`); @@ -102,6 +106,8 @@ class Drawer extends React.Component { {multiColumn ? ( <DrawerHeader columns={columns} + unreadNotifications={unreadNotifications} + showNotificationsBadge={showNotificationsBadge} intl={intl} onSettingsClick={onOpenSettings} /> @@ -145,6 +151,8 @@ Drawer.propTypes = { searchHidden: PropTypes.bool, searchValue: PropTypes.string, submitted: PropTypes.bool, + unreadNotifications: PropTypes.number, + showNotificationsBadge: PropTypes.bool, // Dispatch props. onChange: PropTypes.func, diff --git a/app/javascript/flavours/glitch/features/emoji_picker/index.js b/app/javascript/flavours/glitch/features/emoji_picker/index.js index d22a50848..a78117971 100644 --- a/app/javascript/flavours/glitch/features/emoji_picker/index.js +++ b/app/javascript/flavours/glitch/features/emoji_picker/index.js @@ -340,6 +340,7 @@ class EmojiPickerMenu extends React.PureComponent { skin={skinTone} showPreview={false} backgroundImageFn={backgroundImageFn} + autoFocus emojiTooltip /> diff --git a/app/javascript/flavours/glitch/features/favourited_statuses/index.js b/app/javascript/flavours/glitch/features/favourited_statuses/index.js index 923d4b24f..32bf4e71a 100644 --- a/app/javascript/flavours/glitch/features/favourited_statuses/index.js +++ b/app/javascript/flavours/glitch/features/favourited_statuses/index.js @@ -66,10 +66,6 @@ export default class Favourites extends ImmutablePureComponent { this.props.dispatch(expandFavouritedStatuses()); }, 300, { leading: true }) - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props; const pinned = !!columnId; @@ -91,7 +87,6 @@ export default class Favourites extends ImmutablePureComponent { trackScroll={!pinned} statusIds={statusIds} scrollKey={`favourited_statuses-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} hasMore={hasMore} isLoading={isLoading} onLoadMore={this.handleLoadMore} diff --git a/app/javascript/flavours/glitch/features/favourites/index.js b/app/javascript/flavours/glitch/features/favourites/index.js index 055a15ccb..cf8b31eb3 100644 --- a/app/javascript/flavours/glitch/features/favourites/index.js +++ b/app/javascript/flavours/glitch/features/favourites/index.js @@ -33,6 +33,10 @@ export default class Favourites extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { accountIds } = this.props; @@ -48,7 +52,7 @@ export default class Favourites extends ImmutablePureComponent { <Column> <ColumnBackButton /> - <ScrollContainer scrollKey='favourites'> + <ScrollContainer scrollKey='favourites' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable'> {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} </div> diff --git a/app/javascript/flavours/glitch/features/follow_requests/index.js b/app/javascript/flavours/glitch/features/follow_requests/index.js index 04ff3f111..1e4633984 100644 --- a/app/javascript/flavours/glitch/features/follow_requests/index.js +++ b/app/javascript/flavours/glitch/features/follow_requests/index.js @@ -42,6 +42,10 @@ export default class FollowRequests extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { intl, accountIds } = this.props; @@ -57,7 +61,7 @@ export default class FollowRequests extends ImmutablePureComponent { <Column name='follow-requests' icon='users' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='follow_requests'> + <ScrollContainer scrollKey='follow_requests' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable' onScroll={this.handleScroll}> {accountIds.map(id => <AccountAuthorizeContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/followers/index.js b/app/javascript/flavours/glitch/features/followers/index.js index c42e0386c..cdde1775c 100644 --- a/app/javascript/flavours/glitch/features/followers/index.js +++ b/app/javascript/flavours/glitch/features/followers/index.js @@ -56,6 +56,10 @@ export default class Followers extends ImmutablePureComponent { this.props.dispatch(expandFollowers(this.props.params.accountId)); } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { accountIds, hasMore } = this.props; @@ -77,7 +81,7 @@ export default class Followers extends ImmutablePureComponent { <Column> <ColumnBackButton /> - <ScrollContainer scrollKey='followers'> + <ScrollContainer scrollKey='followers' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable' onScroll={this.handleScroll}> <div className='followers'> <HeaderContainer accountId={this.props.params.accountId} hideTabs /> diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index f710456d5..311fabb63 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -82,10 +82,6 @@ export default class HashtagTimeline extends React.PureComponent { this.props.dispatch(expandHashtagTimeline(this.props.params.id, { maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { hasUnread, columnId, multiColumn } = this.props; const { id } = this.props.params; @@ -110,7 +106,6 @@ export default class HashtagTimeline extends React.PureComponent { scrollKey={`hashtag_timeline-${columnId}`} timelineId={`hashtag:${id}`} onLoadMore={this.handleLoadMore} - shouldUpdateScroll={this.shouldUpdateScroll} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> </Column> diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js index 0c1040290..a992b1ffc 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js @@ -10,6 +10,7 @@ import LocalSettingsNavigationItem from './item'; const messages = defineMessages({ general: { id: 'settings.general', defaultMessage: 'General' }, + compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box options' }, content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' }, collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, media: { id: 'settings.media', defaultMessage: 'Media' }, @@ -43,31 +44,37 @@ export default class LocalSettingsNavigation extends React.PureComponent { active={index === 1} index={1} onNavigate={onNavigate} - title={intl.formatMessage(messages.content_warnings)} + title={intl.formatMessage(messages.compose)} /> <LocalSettingsNavigationItem active={index === 2} index={2} onNavigate={onNavigate} - title={intl.formatMessage(messages.collapsed)} + title={intl.formatMessage(messages.content_warnings)} /> <LocalSettingsNavigationItem active={index === 3} index={3} onNavigate={onNavigate} - title={intl.formatMessage(messages.media)} + title={intl.formatMessage(messages.collapsed)} /> <LocalSettingsNavigationItem active={index === 4} - href='/settings/preferences' index={4} + onNavigate={onNavigate} + title={intl.formatMessage(messages.media)} + /> + <LocalSettingsNavigationItem + active={index === 5} + href='/settings/preferences' + index={5} icon='cog' title={intl.formatMessage(messages.preferences)} /> <LocalSettingsNavigationItem - active={index === 5} + active={index === 6} className='close' - index={5} + index={6} onNavigate={onClose} title={intl.formatMessage(messages.close)} /> diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index 0db49ba5d..5b4e0cb01 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -43,6 +43,25 @@ export default class LocalSettingsPage extends React.PureComponent { <FormattedMessage id='settings.show_reply_counter' defaultMessage='Display an estimate of the reply count' /> </LocalSettingsPageItem> <section> + <h2><FormattedMessage id='settings.notifications_opts' defaultMessage='Notifications options' /></h2> + <LocalSettingsPageItem + settings={settings} + item={['notifications', 'tab_badge']} + id='mastodon-settings--notifications-tab_badge' + onChange={onChange} + > + <FormattedMessage id='settings.notifications.tab_badge' defaultMessage="Display a badge for unread notifications if the notifications column isn't open" /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['notifications', 'favicon_badge']} + id='mastodon-settings--notifications-favicon_badge' + onChange={onChange} + > + <FormattedMessage id='settings.notifications.favicon_badge' defaultMessage='Display unread notifications count in the favicon' /> + </LocalSettingsPageItem> + </section> + <section> <h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2> <LocalSettingsPageItem settings={settings} @@ -74,53 +93,63 @@ export default class LocalSettingsPage extends React.PureComponent { <FormattedMessage id='settings.navbar_under' defaultMessage='Navbar at the bottom (Mobile only)' /> </LocalSettingsPageItem> </section> - <section> - <h2><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h2> - <LocalSettingsPageItem - settings={settings} - item={['always_show_spoilers_field']} - id='mastodon-settings--always_show_spoilers_field' - onChange={onChange} - > - <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['confirm_missing_media_description']} - id='mastodon-settings--confirm_missing_media_description' - onChange={onChange} - > - <FormattedMessage id='settings.confirm_missing_media_description' defaultMessage='Show confirmation dialog before sending toots lacking media descriptions' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['side_arm']} - id='mastodon-settings--side_arm' - options={[ - { value: 'none', message: intl.formatMessage(messages.side_arm_none) }, - { value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) }, - { value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) }, - { value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) }, - { value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) }, - ]} - onChange={onChange} - > - <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' /> - </LocalSettingsPageItem> - <LocalSettingsPageItem - settings={settings} - item={['side_arm_reply_mode']} - id='mastodon-settings--side_arm_reply_mode' - options={[ - { value: 'keep', message: intl.formatMessage(messages.side_arm_keep) }, - { value: 'copy', message: intl.formatMessage(messages.side_arm_copy) }, - { value: 'restrict', message: intl.formatMessage(messages.side_arm_restrict) }, - ]} - onChange={onChange} - > - <FormattedMessage id='settings.side_arm_reply_mode' defaultMessage='When replying to a toot:' /> - </LocalSettingsPageItem> - </section> + </div> + ), + ({ intl, onChange, settings }) => ( + <div className='glitch local-settings__page compose_box_opts'> + <h1><FormattedMessage id='settings.compose_box_opts' defaultMessage='Compose box options' /></h1> + <LocalSettingsPageItem + settings={settings} + item={['always_show_spoilers_field']} + id='mastodon-settings--always_show_spoilers_field' + onChange={onChange} + > + <FormattedMessage id='settings.always_show_spoilers_field' defaultMessage='Always enable the Content Warning field' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['preselect_on_reply']} + id='mastodon-settings--preselect_on_reply' + onChange={onChange} + > + <FormattedMessage id='settings.preselect_on_reply' defaultMessage='Pre-select usernames past the first when replying to a toot with multiple participants' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['confirm_missing_media_description']} + id='mastodon-settings--confirm_missing_media_description' + onChange={onChange} + > + <FormattedMessage id='settings.confirm_missing_media_description' defaultMessage='Show confirmation dialog before sending toots lacking media descriptions' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['side_arm']} + id='mastodon-settings--side_arm' + options={[ + { value: 'none', message: intl.formatMessage(messages.side_arm_none) }, + { value: 'direct', message: intl.formatMessage({ id: 'privacy.direct.short' }) }, + { value: 'private', message: intl.formatMessage({ id: 'privacy.private.short' }) }, + { value: 'unlisted', message: intl.formatMessage({ id: 'privacy.unlisted.short' }) }, + { value: 'public', message: intl.formatMessage({ id: 'privacy.public.short' }) }, + ]} + onChange={onChange} + > + <FormattedMessage id='settings.side_arm' defaultMessage='Secondary toot button:' /> + </LocalSettingsPageItem> + <LocalSettingsPageItem + settings={settings} + item={['side_arm_reply_mode']} + id='mastodon-settings--side_arm_reply_mode' + options={[ + { value: 'keep', message: intl.formatMessage(messages.side_arm_keep) }, + { value: 'copy', message: intl.formatMessage(messages.side_arm_copy) }, + { value: 'restrict', message: intl.formatMessage(messages.side_arm_restrict) }, + ]} + onChange={onChange} + > + <FormattedMessage id='settings.side_arm_reply_mode' defaultMessage='When replying to a toot:' /> + </LocalSettingsPageItem> </div> ), ({ intl, onChange, settings }) => ( diff --git a/app/javascript/flavours/glitch/features/mutes/index.js b/app/javascript/flavours/glitch/features/mutes/index.js index 87517eec9..d94c1d8ad 100644 --- a/app/javascript/flavours/glitch/features/mutes/index.js +++ b/app/javascript/flavours/glitch/features/mutes/index.js @@ -42,6 +42,10 @@ export default class Mutes extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { intl, accountIds } = this.props; @@ -56,7 +60,7 @@ export default class Mutes extends ImmutablePureComponent { return ( <Column name='mutes' icon='volume-off' heading={intl.formatMessage(messages.heading)}> <ColumnBackButtonSlim /> - <ScrollContainer scrollKey='mutes'> + <ScrollContainer scrollKey='mutes' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable mutes' onScroll={this.handleScroll}> {accountIds.map(id => <AccountContainer key={id} id={id} /> diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 4c05b24fe..13ed26865 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -8,6 +8,8 @@ import { enterNotificationClearingMode, expandNotifications, scrollTopNotifications, + mountNotifications, + unmountNotifications, } from 'flavours/glitch/actions/notifications'; import { addColumn, removeColumn, moveColumn } from 'flavours/glitch/actions/columns'; import NotificationContainer from './containers/notification_container'; @@ -42,6 +44,12 @@ const mapDispatchToProps = dispatch => ({ onEnterCleaningMode(yes) { dispatch(enterNotificationClearingMode(yes)); }, + onMount() { + dispatch(mountNotifications()); + }, + onUnmount() { + dispatch(unmountNotifications()); + }, dispatch, }); @@ -62,6 +70,8 @@ export default class Notifications extends React.PureComponent { localSettings: ImmutablePropTypes.map, notifCleaningActive: PropTypes.bool, onEnterCleaningMode: PropTypes.func, + onMount: PropTypes.func, + onUnmount: PropTypes.func, }; static defaultProps = { @@ -126,6 +136,20 @@ export default class Notifications extends React.PureComponent { } } + componentDidMount () { + const { onMount } = this.props; + if (onMount) { + onMount(); + } + } + + componentWillUnmount () { + const { onUnmount } = this.props; + if (onUnmount) { + onUnmount(); + } + } + render () { const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props; const pinned = !!columnId; diff --git a/app/javascript/flavours/glitch/features/pinned_statuses/index.js b/app/javascript/flavours/glitch/features/pinned_statuses/index.js index e7fa7ac0d..f56d70176 100644 --- a/app/javascript/flavours/glitch/features/pinned_statuses/index.js +++ b/app/javascript/flavours/glitch/features/pinned_statuses/index.js @@ -41,10 +41,6 @@ export default class PinnedStatuses extends ImmutablePureComponent { this.column = c; } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, statusIds, hasMore } = this.props; @@ -54,7 +50,6 @@ export default class PinnedStatuses extends ImmutablePureComponent { <StatusList statusIds={statusIds} scrollKey='pinned_statuses' - shouldUpdateScroll={this.shouldUpdateScroll} hasMore={hasMore} /> </Column> diff --git a/app/javascript/flavours/glitch/features/public_timeline/index.js b/app/javascript/flavours/glitch/features/public_timeline/index.js index af86b22ac..53f2836f1 100644 --- a/app/javascript/flavours/glitch/features/public_timeline/index.js +++ b/app/javascript/flavours/glitch/features/public_timeline/index.js @@ -71,10 +71,6 @@ export default class PublicTimeline extends React.PureComponent { this.props.dispatch(expandPublicTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl, columnId, hasUnread, multiColumn } = this.props; const pinned = !!columnId; @@ -99,7 +95,6 @@ export default class PublicTimeline extends React.PureComponent { onLoadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} - shouldUpdateScroll={this.shouldUpdateScroll} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} /> </Column> diff --git a/app/javascript/flavours/glitch/features/reblogs/index.js b/app/javascript/flavours/glitch/features/reblogs/index.js index 25b792b39..c0a65d1de 100644 --- a/app/javascript/flavours/glitch/features/reblogs/index.js +++ b/app/javascript/flavours/glitch/features/reblogs/index.js @@ -33,6 +33,10 @@ export default class Reblogs extends ImmutablePureComponent { } } + shouldUpdateScroll = (prevRouterProps, { location }) => { + return !(location.state && location.state.mastodonModalOpen); + } + render () { const { accountIds } = this.props; @@ -48,7 +52,7 @@ export default class Reblogs extends ImmutablePureComponent { <Column> <ColumnBackButton /> - <ScrollContainer scrollKey='reblogs'> + <ScrollContainer scrollKey='reblogs' shouldUpdateScroll={this.shouldUpdateScroll}> <div className='scrollable reblogs'> {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} </div> diff --git a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js index d4ef3ce48..2b67e836a 100644 --- a/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js +++ b/app/javascript/flavours/glitch/features/standalone/community_timeline/index.js @@ -47,10 +47,6 @@ export default class CommunityTimeline extends React.PureComponent { this.props.dispatch(expandCommunityTimeline({ maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { intl } = this.props; @@ -66,7 +62,6 @@ export default class CommunityTimeline extends React.PureComponent { timelineId='community' onLoadMore={this.handleLoadMore} scrollKey='standalone_public_timeline' - shouldUpdateScroll={this.shouldUpdateScroll} trackScroll={false} /> </Column> diff --git a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js index d2b1971ec..dc02f1c91 100644 --- a/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/standalone/hashtag_timeline/index.js @@ -41,10 +41,6 @@ export default class HashtagTimeline extends React.PureComponent { this.props.dispatch(expandHashtagTimeline(this.props.hashtag, { maxId })); } - shouldUpdateScroll = (prevRouterProps, { location }) => { - return !(location.state && location.state.mastodonModalOpen) - } - render () { const { hashtag } = this.props; @@ -59,7 +55,6 @@ export default class HashtagTimeline extends React.PureComponent { <StatusListContainer trackScroll={false} scrollKey='standalone_hashtag_timeline' - shouldUpdateScroll={this.shouldUpdateScroll} timelineId={`hashtag:${hashtag}`} onLoadMore={this.handleLoadMore} /> diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index 413833a79..009aa49eb 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -4,7 +4,7 @@ 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/util/initial_state'; +import { me, isStaff } from 'flavours/glitch/util/initial_state'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -26,6 +26,8 @@ const messages = defineMessages({ 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' }, }); @injectIntl @@ -146,6 +148,11 @@ export default class ActionBar extends React.PureComponent { 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 (isStaff) { + menu.push(null); + menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` }); + menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` }); + } } const shareButton = ('share' in navigator) && status.get('visibility') === 'public' && ( diff --git a/app/javascript/flavours/glitch/features/status/components/card.js b/app/javascript/flavours/glitch/features/status/components/card.js index 680bf63ab..b52f3c4fa 100644 --- a/app/javascript/flavours/glitch/features/status/components/card.js +++ b/app/javascript/flavours/glitch/features/status/components/card.js @@ -20,6 +20,39 @@ const getHostname = url => { return parser.hostname; }; +const trim = (text, len) => { + const cut = text.indexOf(' ', len); + + if (cut === -1) { + return text; + } + + return text.substring(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 = { @@ -33,9 +66,16 @@ export default class Card extends React.PureComponent { }; state = { - width: 0, + width: 280, + embedded: false, }; + componentWillReceiveProps (nextProps) { + if (this.props.card !== nextProps.card) { + this.setState({ embedded: false }); + } + } + handlePhotoClick = () => { const { card, onOpenMedia } = this.props; @@ -43,7 +83,7 @@ export default class Card extends React.PureComponent { Immutable.fromJS([ { type: 'image', - url: card.get('url'), + url: card.get('embed_url'), description: card.get('title'), meta: { original: { @@ -57,56 +97,14 @@ export default class Card extends React.PureComponent { ); }; - renderLink () { - const { card, maxDescription } = this.props; - - let image = ''; - let provider = card.get('provider_name'); - - if (card.get('image')) { - image = ( - <div className='status-card__image'> - <img src={card.get('image')} alt={card.get('title')} className='status-card__image-image' width={card.get('width')} height={card.get('height')} /> - </div> - ); - } - - if (provider.length < 1) { - provider = decodeIDNA(getHostname(card.get('url'))); - } - - const className = classnames('status-card', { - 'horizontal': card.get('width') > card.get('height'), - }); - - return ( - <a href={card.get('url')} className={className} target='_blank' rel='noopener'> - {image} - - <div className='status-card__content'> - <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong> - <p className='status-card__description'>{(card.get('description') || '').substring(0, maxDescription)}</p> - <span className='status-card__host'>{provider}</span> - </div> - </a> - ); - } - - renderPhoto () { + handleEmbedClick = () => { const { card } = this.props; - return ( - <img - className='status-card-photo' - onClick={this.handlePhotoClick} - role='button' - tabIndex='0' - src={card.get('url')} - alt={card.get('title')} - width={card.get('width')} - height={card.get('height')} - /> - ); + if (card.get('type') === 'photo') { + this.handlePhotoClick(); + } else { + this.setState({ embedded: true }); + } } setRef = c => { @@ -117,7 +115,7 @@ export default class Card extends React.PureComponent { renderVideo () { const { card } = this.props; - const content = { __html: card.get('html') }; + const content = { __html: addAutoPlay(card.get('html')) }; const { width } = this.state; const ratio = card.get('width') / card.get('height'); const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); @@ -125,7 +123,7 @@ export default class Card extends React.PureComponent { return ( <div ref={this.setRef} - className='status-card-video' + className='status-card__image status-card-video' dangerouslySetInnerHTML={content} style={{ height }} /> @@ -133,23 +131,76 @@ export default class Card extends React.PureComponent { } render () { - const { card } = this.props; + const { card, maxDescription } = this.props; + const { width, embedded } = this.state; if (card === null) { return null; } - switch(card.get('type')) { - case 'link': - return this.renderLink(); - case 'photo': - return this.renderPhoto(); - case 'video': - return this.renderVideo(); - case 'rich': - default: - return null; + const provider = card.get('provider_name').length === 0 ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); + const horizontal = card.get('width') > card.get('height') && (card.get('width') + 100 >= width) || card.get('type') !== 'link'; + const className = classnames('status-card', { horizontal }); + const interactive = card.get('type') !== 'link'; + const title = interactive ? <a className='status-card__title' href={card.get('url')} title={card.get('title')} rel='noopener' target='_blank'><strong>{card.get('title')}</strong></a> : <strong className='status-card__title' title={card.get('title')}>{card.get('title')}</strong>; + const ratio = card.get('width') / card.get('height'); + const height = card.get('width') > card.get('height') ? (width / ratio) : (width * ratio); + + const description = ( + <div className='status-card__content'> + {title} + {!horizontal && <p className='status-card__description'>{trim(card.get('description') || '', maxDescription)}</p>} + <span className='status-card__host'>{provider}</span> + </div> + ); + + let embed = ''; + let thumbnail = <div style={{ backgroundImage: `url(${card.get('image')})`, width: horizontal ? width : null, height: horizontal ? height : null }} className='status-card__image-image' />; + + if (interactive) { + if (embedded) { + embed = this.renderVideo(); + } else { + let iconVariant = 'play'; + + if (card.get('type') === 'photo') { + iconVariant = 'search-plus'; + } + + embed = ( + <div className='status-card__image'> + {thumbnail} + + <div className='status-card__actions'> + <div> + <button onClick={this.handleEmbedClick}><i className={`fa fa-${iconVariant}`} /></button> + <a href={card.get('url')} target='_blank' rel='noopener'><i className='fa fa-external-link' /></a> + </div> + </div> + </div> + ); + } + + return ( + <div className={className} ref={this.setRef}> + {embed} + {description} + </div> + ); + } else if (card.get('image')) { + embed = ( + <div className='status-card__image'> + {thumbnail} + </div> + ); } + + return ( + <a href={card.get('url')} className={className} target='_blank' rel='noopener' ref={this.setRef}> + {embed} + {description} + </a> + ); } } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index ee9eb02c6..2e61e6d8c 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -77,6 +77,7 @@ export default class DetailedStatus extends ImmutablePureComponent { sensitive={status.get('sensitive')} media={status.get('media_attachments')} letterbox={settings.getIn(['media', 'letterbox'])} + fullwidth={settings.getIn(['media', 'fullwidth'])} onOpenMedia={this.props.onOpenMedia} /> ); diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index 0697b49ad..5759a575c 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -105,7 +105,7 @@ export default class Status extends ImmutablePureComponent { if (nextProps.params.statusId !== this.props.params.statusId && nextProps.params.statusId) { this._scrolledIntoView = false; this.props.dispatch(fetchStatus(nextProps.params.statusId)); - this.setState({ isExpanded: autoUnfoldCW(nextProps.settings, nextProps.status) }); + this.setState({ isExpanded: autoUnfoldCW(nextProps.settings, nextProps.status), threadExpanded: undefined }); } } diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index 8fde279c7..71cb7e8c9 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { injectIntl } from 'react-intl'; +import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -30,6 +30,12 @@ const componentMap = { 'LIST': ListTimeline, }; +const shouldHideFAB = path => path.match(/^\/statuses\//); + +const messages = defineMessages({ + publish: { id: 'compose_form.publish', defaultMessage: 'Toot' }, +}); + @component => injectIntl(component, { withRef: true }) export default class ColumnsArea extends ImmutablePureComponent { @@ -147,14 +153,14 @@ export default class ColumnsArea extends ImmutablePureComponent { } render () { - const { columns, children, singleColumn } = this.props; + const { columns, children, singleColumn, intl } = this.props; const { shouldAnimate } = this.state; const columnIndex = getIndex(this.context.router.history.location.pathname); this.pendingIndex = null; if (singleColumn) { - const floatingActionButton = this.context.router.history.location.pathname === '/statuses/new' ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button'><i className='fa fa-pencil' /></Link>; + const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><i className='fa fa-pencil' /></Link>; return columnIndex !== -1 ? [ <ReactSwipeableViews key='content' index={columnIndex} onChangeIndex={this.handleSwipe} onTransitionEnd={this.handleAnimationEnd} animateTransitions={shouldAnimate} springConfig={{ duration: '400ms', delay: '0s', easeFunction: 'ease' }} style={{ height: '100%' }}> diff --git a/app/javascript/flavours/glitch/features/ui/components/media_modal.js b/app/javascript/flavours/glitch/features/ui/components/media_modal.js index d4fd45d4d..1f3ac18ea 100644 --- a/app/javascript/flavours/glitch/features/ui/components/media_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/media_modal.js @@ -129,7 +129,7 @@ export default class MediaModal extends ImmutablePureComponent { startTime={time || 0} onCloseVideo={onClose} detailed - description={image.get('description')} + alt={image.get('description')} key={image.get('url')} /> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/report_modal.js b/app/javascript/flavours/glitch/features/ui/components/report_modal.js index 81643b6c2..a139394ac 100644 --- a/app/javascript/flavours/glitch/features/ui/components/report_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/report_modal.js @@ -106,6 +106,7 @@ export default class ReportModal extends ImmutablePureComponent { onChange={this.handleCommentChange} onKeyDown={this.handleKeyDown} disabled={isSubmitting} + autoFocus /> {domain && ( diff --git a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js index b2fee21e1..b44a21a42 100644 --- a/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js +++ b/app/javascript/flavours/glitch/features/ui/components/tabs_bar.js @@ -4,10 +4,34 @@ import { NavLink, withRouter } from 'react-router-dom'; import { FormattedMessage, injectIntl } from 'react-intl'; import { debounce } from 'lodash'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; +import { connect } from 'react-redux'; + +const mapStateToProps = state => ({ + unreadNotifications: state.getIn(['notifications', 'unread']), + showBadge: state.getIn(['local_settings', 'notifications', 'tab_badge']), +}); + +@connect(mapStateToProps) +class NotificationsIcon extends React.PureComponent { + static propTypes = { + unreadNotifications: PropTypes.number, + showBadge: PropTypes.bool, + }; + + render() { + const { unreadNotifications, showBadge } = this.props; + return ( + <span className='icon-badge-wrapper'> + <i className='fa fa-fw fa-bell' /> + { showBadge && unreadNotifications > 0 && <div className='icon-badge' />} + </span> + ); + } +} export const links = [ <NavLink className='tabs-bar__link primary' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><i className='fa fa-fw fa-home' /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, - <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><i className='fa fa-fw fa-bell' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, + <NavLink className='tabs-bar__link primary' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, <NavLink className='tabs-bar__link secondary' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><i className='fa fa-fw fa-users' /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, <NavLink className='tabs-bar__link secondary' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><i className='fa fa-fw fa-globe' /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index e0cb7fc09..69e0ee46e 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -24,7 +24,7 @@ export default class VideoModal extends ImmutablePureComponent { startTime={time} onCloseVideo={onClose} detailed - description={media.get('description')} + alt={media.get('description')} /> </div> </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js b/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js index 0a0a4d41a..3f6562bc9 100644 --- a/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js +++ b/app/javascript/flavours/glitch/features/ui/components/zoomable_image.js @@ -137,6 +137,7 @@ export default class ZoomableImage extends React.PureComponent { role='presentation' ref={this.setImageRef} alt={alt} + title={alt} src={src} style={{ transform: `scale(${scale})`, diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 1cff94321..ecbac1f8f 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -10,13 +10,14 @@ import { isMobile } from 'flavours/glitch/util/is_mobile'; import { debounce } from 'lodash'; import { uploadCompose, resetCompose } from 'flavours/glitch/actions/compose'; import { expandHomeTimeline } from 'flavours/glitch/actions/timelines'; -import { expandNotifications } from 'flavours/glitch/actions/notifications'; +import { expandNotifications, notificationsSetVisibility } from 'flavours/glitch/actions/notifications'; import { fetchFilters } from 'flavours/glitch/actions/filters'; import { clearHeight } from 'flavours/glitch/actions/height_cache'; import { WrappedSwitch, WrappedRoute } from 'flavours/glitch/util/react_router_helpers'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; import classNames from 'classnames'; +import Favico from 'favico.js'; import { Drawer, Status, @@ -59,11 +60,14 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - hasComposingText: state.getIn(['compose', 'text']) !== '', + hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0, + hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0, layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), navbarUnder: state.getIn(['local_settings', 'navbar_under']), dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null, + unreadNotifications: state.getIn(['notifications', 'unread']), + showFaviconBadge: state.getIn(['local_settings', 'notifications', 'favicon_badge']), }); const keyMap = { @@ -110,11 +114,14 @@ export default class UI extends React.Component { navbarUnder: PropTypes.bool, isComposing: PropTypes.bool, hasComposingText: PropTypes.bool, + hasMediaAttachments: PropTypes.bool, match: PropTypes.object.isRequired, location: PropTypes.object.isRequired, history: PropTypes.object.isRequired, intl: PropTypes.object.isRequired, dropdownMenuIsOpen: PropTypes.bool, + unreadNotifications: PropTypes.number, + showFaviconBadge: PropTypes.bool, }; state = { @@ -123,9 +130,9 @@ export default class UI extends React.Component { }; handleBeforeUnload = (e) => { - const { intl, hasComposingText } = this.props; + const { intl, hasComposingText, hasMediaAttachments } = this.props; - if (hasComposingText) { + if (hasComposingText || hasMediaAttachments) { // Setting returnValue to any string causes confirmation dialog. // Many browsers no longer display this text to users, // but we set user-friendly message for other browsers, e.g. Edge. @@ -206,7 +213,27 @@ export default class UI extends React.Component { } } + handleVisibilityChange = () => { + const visibility = !document[this.visibilityHiddenProp]; + this.props.dispatch(notificationsSetVisibility(visibility)); + } + componentWillMount () { + if (typeof document.hidden !== 'undefined') { // Opera 12.10 and Firefox 18 and later support + this.visibilityHiddenProp = 'hidden'; + this.visibilityChange = 'visibilitychange'; + } else if (typeof document.msHidden !== 'undefined') { + this.visibilityHiddenProp = 'msHidden'; + this.visibilityChange = 'msvisibilitychange'; + } else if (typeof document.webkitHidden !== 'undefined') { + this.visibilityHiddenProp = 'webkitHidden'; + this.visibilityChange = 'webkitvisibilitychange'; + } + if (this.visibilityChange !== undefined) { + document.addEventListener(this.visibilityChange, this.handleVisibilityChange, false); + this.handleVisibilityChange(); + } + window.addEventListener('beforeunload', this.handleBeforeUnload, false); window.addEventListener('resize', this.handleResize, { passive: true }); document.addEventListener('dragenter', this.handleDragEnter, false); @@ -219,6 +246,8 @@ export default class UI extends React.Component { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); } + this.favicon = new Favico({ animation:"none" }); + this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); setTimeout(() => this.props.dispatch(fetchFilters()), 500); @@ -247,9 +276,19 @@ export default class UI extends React.Component { if (![this.props.location.pathname, '/'].includes(prevProps.location.pathname)) { this.columnsAreaNode.handleChildrenContentChange(); } + if (this.props.unreadNotifications != prevProps.unreadNotifications || + this.props.showFaviconBadge != prevProps.showFaviconBadge) { + if (this.favicon) { + this.favicon.badge(this.props.showFaviconBadge ? this.props.unreadNotifications : 0); + } + } } componentWillUnmount () { + if (this.visibilityChange !== undefined) { + document.removeEventListener(this.visibilityChange, this.handleVisibilityChange); + } + window.removeEventListener('beforeunload', this.handleBeforeUnload); window.removeEventListener('resize', this.handleResize); document.removeEventListener('dragenter', this.handleDragEnter); diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 7e284a0bc..5cbe01f26 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -101,6 +101,7 @@ export default class Video extends React.PureComponent { fullwidth: PropTypes.bool, detailed: PropTypes.bool, inline: PropTypes.bool, + preventPlayback: PropTypes.bool, intl: PropTypes.object.isRequired, }; @@ -134,7 +135,10 @@ export default class Video extends React.PureComponent { this.seek = c; } - handleClickRoot = e => e.stopPropagation(); + handleMouseDownRoot = e => { + e.preventDefault(); + e.stopPropagation(); + } handlePlay = () => { this.setState({ paused: false }); @@ -215,6 +219,12 @@ export default class Video extends React.PureComponent { document.removeEventListener('MSFullscreenChange', this.handleFullscreenChange, true); } + componentDidUpdate (prevProps) { + if (this.video && this.state.revealed && this.props.preventPlayback && !prevProps.preventPlayback) { + this.video.pause(); + } + } + handleFullscreenChange = () => { this.setState({ fullscreen: isFullscreen() }); } @@ -254,11 +264,12 @@ export default class Video extends React.PureComponent { } handleOpenVideo = () => { - const { src, preview, width, height } = this.props; + const { src, preview, width, height, alt } = this.props; const media = fromJS({ type: 'video', url: src, preview_url: preview, + description: alt, width, height, }); @@ -311,7 +322,7 @@ export default class Video extends React.PureComponent { ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} - onClick={this.handleClickRoot} + onMouseDown={this.handleMouseDownRoot} tabIndex={0} > <video diff --git a/app/javascript/flavours/glitch/locales/ja.js b/app/javascript/flavours/glitch/locales/ja.js index f558d7ab7..c323956c6 100644 --- a/app/javascript/flavours/glitch/locales/ja.js +++ b/app/javascript/flavours/glitch/locales/ja.js @@ -4,13 +4,14 @@ const messages = { 'getting_started.open_source_notice': 'Glitchsocは{Mastodon}によるフリーなオープンソースソフトウェアです。誰でもGitHub({github})から開発に參加したり、問題を報告したりできます。', 'layout.auto': '自動', 'layout.current_is': 'あなたの現在のレイアウト:', - 'layout.desktop': 'Desktop', - 'layout.mobile': 'Mobile', + 'layout.desktop': 'デスクトップ', + 'layout.single': 'モバイル', 'navigation_bar.app_settings': 'アプリ設定', 'getting_started.onboarding': '解説を表示', 'onboarding.page_one.federation': '{domain}はMastodonのインスタンスです。Mastodonとは、独立したサーバが連携して作るソーシャルネットワークです。これらのサーバーをインスタンスと呼びます。', 'onboarding.page_one.welcome': '{domain}へようこそ!', 'onboarding.page_six.github': '{domain}はGlitchsocを使用しています。Glitchsocは{Mastodon}のフレンドリーな{fork}で、どんなMastodonアプリやインスタンスとも互換性があります。Glitchsocは完全に無料で、オープンソースです。{github}でバグ報告や機能要望あるいは貢獻をすることが可能です。', + 'settings.always_show_spoilers_field': '常にコンテンツワーニング設定を表示する(指定がない場合は通常投稿)', 'settings.auto_collapse': '自動折りたたみ', 'settings.auto_collapse_all': 'すべて', 'settings.auto_collapse_lengthy': '長いトゥート', @@ -20,7 +21,12 @@ const messages = { 'settings.auto_collapse_replies': '返信', 'settings.close': '閉じる', 'settings.collapsed_statuses': 'トゥート', + 'settings.confirm_missing_media_description': '画像に対する補助記載がないときに投稿前の警告を表示する', + 'settings.content_warnings': 'コンテンツワーニング', + 'settings.content_warnings_filter': '説明に指定した文字が含まれているものを自動で展開しないようにする', + 'settings.content_warnings.regexp': '正規表現', 'settings.enable_collapsed': 'トゥート折りたたみを有効にする', + 'settings.enable_content_warnings_auto_unfold': 'コンテンツワーニング指定されている投稿を常に表示する', 'settings.general': '一般', 'settings.image_backgrounds': '画像背景', 'settings.image_backgrounds_media': '折りたまれたメディア付きトゥートをプレビュー', @@ -28,15 +34,28 @@ const messages = { 'settings.media': 'メディア', 'settings.media_letterbox': 'メディアをレターボックス式で表示', 'settings.media_fullwidth': '全幅メディアプレビュー', + 'settings.navbar_under': 'ナビを画面下部に移動させる(モバイル レイアウトのみ)', + 'settings.notifications.favicon_badge': '通知アイコンに未読件数を表示する', + 'settings.notifications_opts': '通知の設定', + 'settings.notifications.tab_badge': '未読の通知があるとき、通知アイコンにマークを表示する', 'settings.preferences': 'ユーザー設定', - 'settings.wide_view': 'ワイドビュー(Desktopレイアウトのみ)', - 'settings.navbar_under': 'ナビを画面下部に移動させる(Mobileレイアウトのみ)', + 'settings.wide_view': 'ワイドビュー(デスクトップ レイアウトのみ)', 'settings.compose_box_opts': 'コンポーズボックス設定', + 'settings.show_reply_counter': '投稿に対するリプライの数を表示する', 'settings.side_arm': 'セカンダリートゥートボタン', + 'settings.side_arm.none': '表示しない', + 'settings.side_arm_reply_mode': '返信時の投稿範囲', + 'settings.side_arm_reply_mode.copy': '返信先の投稿範囲を利用する', + 'settings.side_arm_reply_mode.keep': 'セカンダリートゥートボタンの設定を維持する', + 'settings.side_arm_reply_mode.restrict': '返信先の投稿範囲に制限する', 'settings.layout': 'レイアウト', + 'settings.layout_opts': 'レイアウトの設定', 'status.collapse': '折りたたむ', 'status.uncollapse': '折りたたみを解除', + 'confirmations.missing_media_description.message': '少なくとも1つの画像に視聴覚障害者のための画像説明が付与されていません。すべての画像に対して説明を付与することを望みます。', + 'confirmations.missing_media_description.confirm': 'このまま投稿', + 'favourite_modal.combo': '次からは {combo} を押せば、これをスキップできます。', 'home.column_settings.show_direct': 'DMを表示', @@ -68,4 +87,4 @@ const messages = { 'column.bookmarks': 'ブックマーク' }; -export default Object.assign({}, inherited, messages); +export default Object.assign({}, inherited, messages); \ No newline at end of file diff --git a/app/javascript/flavours/glitch/middleware/sounds.js b/app/javascript/flavours/glitch/middleware/sounds.js index 3d1e3eaba..9f1bc02b9 100644 --- a/app/javascript/flavours/glitch/middleware/sounds.js +++ b/app/javascript/flavours/glitch/middleware/sounds.js @@ -15,7 +15,7 @@ const play = audio => { if (typeof audio.fastSeek === 'function') { audio.fastSeek(0); } else { - audio.seek(0); + audio.currentTime = 0; } } diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 594d70ee2..0ddff707e 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -314,8 +314,12 @@ export default function compose(state = initialState, action) { map.set('idempotencyKey', uuid()); if (action.status.get('spoiler_text').length > 0) { + let spoiler_text = action.status.get('spoiler_text'); + if (!spoiler_text.match(/^re[: ]/i)) { + spoiler_text = 're: '.concat(spoiler_text); + } map.set('spoiler', true); - map.set('spoiler_text', action.status.get('spoiler_text')); + map.set('spoiler_text', spoiler_text); } else { map.set('spoiler', false); map.set('spoiler_text', ''); diff --git a/app/javascript/flavours/glitch/reducers/dropdown_menu.js b/app/javascript/flavours/glitch/reducers/dropdown_menu.js index 5449884cc..36fd4f132 100644 --- a/app/javascript/flavours/glitch/reducers/dropdown_menu.js +++ b/app/javascript/flavours/glitch/reducers/dropdown_menu.js @@ -4,12 +4,12 @@ import { DROPDOWN_MENU_CLOSE, } from '../actions/dropdown_menu'; -const initialState = Immutable.Map({ openId: null, placement: null }); +const initialState = Immutable.Map({ openId: null, placement: null, keyboard: false }); export default function dropdownMenu(state = initialState, action) { switch (action.type) { case DROPDOWN_MENU_OPEN: - return state.merge({ openId: action.id, placement: action.placement }); + return state.merge({ openId: action.id, placement: action.placement, keyboard: action.keyboard }); case DROPDOWN_MENU_CLOSE: return state.get('openId') === action.id ? state.set('openId', null) : state; default: diff --git a/app/javascript/flavours/glitch/reducers/local_settings.js b/app/javascript/flavours/glitch/reducers/local_settings.js index 063ae3943..260a9f08f 100644 --- a/app/javascript/flavours/glitch/reducers/local_settings.js +++ b/app/javascript/flavours/glitch/reducers/local_settings.js @@ -14,6 +14,7 @@ const initialState = ImmutableMap({ show_reply_count : false, always_show_spoilers_field: false, confirm_missing_media_description: false, + preselect_on_reply: true, content_warnings : ImmutableMap({ auto_unfold : false, filter : null, @@ -37,6 +38,10 @@ const initialState = ImmutableMap({ letterbox : true, fullwidth : true, }), + notifications : ImmutableMap({ + favicon_badge : false, + tab_badge : true, + }), }); const hydrate = (state, localSettings) => state.mergeDeep(localSettings); diff --git a/app/javascript/flavours/glitch/reducers/notifications.js b/app/javascript/flavours/glitch/reducers/notifications.js index dc820b476..0b816e85e 100644 --- a/app/javascript/flavours/glitch/reducers/notifications.js +++ b/app/javascript/flavours/glitch/reducers/notifications.js @@ -1,4 +1,7 @@ import { + NOTIFICATIONS_MOUNT, + NOTIFICATIONS_UNMOUNT, + NOTIFICATIONS_SET_VISIBILITY, NOTIFICATIONS_UPDATE, NOTIFICATIONS_EXPAND_SUCCESS, NOTIFICATIONS_EXPAND_REQUEST, @@ -24,9 +27,12 @@ const initialState = ImmutableMap({ items: ImmutableList(), hasMore: true, top: true, + mounted: 0, unread: 0, + lastReadId: '0', isLoading: false, cleaningMode: false, + isTabVisible: true, // notification removal mark of new notifs loaded whilst cleaningMode is true. markNewForDelete: false, }); @@ -40,9 +46,11 @@ const notificationToMap = (state, notification) => ImmutableMap({ }); const normalizeNotification = (state, notification) => { - const top = state.get('top'); + const top = !shouldCountUnreadNotifications(state); - if (!top) { + if (top) { + state = state.set('lastReadId', notification.id); + } else { state = state.update('unread', unread => unread + 1); } @@ -56,6 +64,8 @@ const normalizeNotification = (state, notification) => { }; const expandNormalizedNotifications = (state, notifications, next) => { + const top = !(shouldCountUnreadNotifications(state)); + const lastReadId = state.get('lastReadId'); let items = ImmutableList(); notifications.forEach((n, i) => { @@ -77,6 +87,14 @@ const expandNormalizedNotifications = (state, notifications, next) => { }); } + if (top) { + if (!items.isEmpty()) { + mutable.update('lastReadId', id => compareId(id, items.first().get('id')) > 0 ? id : items.first().get('id')); + } + } else { + mutable.update('unread', unread => unread + items.filter(item => compareId(item.get('id'), lastReadId) > 0).size); + } + if (!next) { mutable.set('hasMore', true); } @@ -89,15 +107,29 @@ const filterNotifications = (state, relationship) => { return state.update('items', list => list.filterNot(item => item !== null && item.get('account') === relationship.id)); }; +const clearUnread = (state) => { + state = state.set('unread', 0); + const lastNotification = state.get('items').find(item => item !== null); + return state.set('lastReadId', lastNotification ? lastNotification.get('id') : '0'); +} + const updateTop = (state, top) => { - if (top) { - state = state.set('unread', 0); + state = state.set('top', top); + + if (!shouldCountUnreadNotifications(state)) { + state = clearUnread(state); } return state.set('top', top); }; const deleteByStatus = (state, statusId) => { + const top = !(shouldCountUnreadNotifications(state)); + if (!top) { + const lastReadId = state.get('lastReadId'); + const deletedUnread = state.get('items').filter(item => item !== null && item.get('status') === statusId && compareId(item.get('id'), lastReadId) > 0); + state = state.update('unread', unread => unread - deletedUnread.size); + } return state.update('items', list => list.filterNot(item => item !== null && item.get('status') === statusId)); }; @@ -129,10 +161,36 @@ const deleteMarkedNotifs = (state) => { return state.update('items', list => list.filterNot(item => item.get('markedForDelete'))); }; +const updateMounted = (state) => { + state = state.update('mounted', count => count + 1); + if (!shouldCountUnreadNotifications(state)) { + state = clearUnread(state); + } + return state; +}; + +const updateVisibility = (state, visibility) => { + state = state.set('isTabVisible', visibility); + if (!shouldCountUnreadNotifications(state)) { + state = clearUnread(state); + } + return state; +}; + +const shouldCountUnreadNotifications = (state) => { + return !(state.get('isTabVisible') && state.get('top') && state.get('mounted') > 0); +}; + export default function notifications(state = initialState, action) { let st; switch(action.type) { + case NOTIFICATIONS_MOUNT: + return updateMounted(state); + case NOTIFICATIONS_UNMOUNT: + return state.update('mounted', count => count - 1); + case NOTIFICATIONS_SET_VISIBILITY: + return updateVisibility(state, action.visibility); case NOTIFICATIONS_EXPAND_REQUEST: case NOTIFICATIONS_DELETE_MARKED_REQUEST: return state.set('isLoading', true); diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index 102723e39..2e317c382 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -43,10 +43,11 @@ @mixin fullwidth-gallery { &.full-width { - margin-left: -22px; - margin-right: -22px; + margin-left: -14px; + margin-right: -14px; width: inherit; max-width: none; height: 250px; + border-radius: 0px; } } diff --git a/app/javascript/flavours/glitch/styles/accessibility.scss b/app/javascript/flavours/glitch/styles/accessibility.scss new file mode 100644 index 000000000..4fe5c8b1c --- /dev/null +++ b/app/javascript/flavours/glitch/styles/accessibility.scss @@ -0,0 +1,13 @@ +$emojis-requiring-outlines: '8ball' 'ant' 'back' 'black_circle' 'black_heart' 'black_large_square' 'black_medium_small_square' 'black_medium_square' 'black_nib' 'black_small_square' 'bomb' 'bowling' 'bust_in_silhouette' 'busts_in_silhouette' 'camera' 'camera_with_flash' 'clubs' 'copyright' 'curly_loop' 'currency_exchange' 'dark_sunglasses' 'eight_pointed_black_star' 'electric_plug' 'end' 'female-guard' 'film_projector' 'fried_egg' 'gorilla' 'guardsman' 'heavy_check_mark' 'heavy_division_sign' 'heavy_dollar_sign' 'heavy_minus_sign' 'heavy_multiplication_x' 'heavy_plus_sign' 'hocho' 'hole' 'joystick' 'kaaba' 'lower_left_ballpoint_pen' 'lower_left_fountain_pen' 'male-guard' 'microphone' 'mortar_board' 'movie_camera' 'musical_score' 'on' 'registered' 'soon' 'spades' 'speaking_head_in_silhouette' 'spider' 'telephone_receiver' 'tm' 'top' 'tophat' 'turkey' 'vhs' 'video_camera' 'video_game' 'water_buffalo' 'waving_black_flag' 'wavy_dash' !default; + +%emoji-outline { + filter: drop-shadow(1px 1px 0 $primary-text-color) drop-shadow(-1px 1px 0 $primary-text-color) drop-shadow(1px -1px 0 $primary-text-color) drop-shadow(-1px -1px 0 $primary-text-color); +} + +.emojione { + @each $emoji in $emojis-requiring-outlines { + &[title=':#{$emoji}:'] { + @extend %emoji-outline; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/accounts.scss b/app/javascript/flavours/glitch/styles/accounts.scss index ac1989832..9568581ec 100644 --- a/app/javascript/flavours/glitch/styles/accounts.scss +++ b/app/javascript/flavours/glitch/styles/accounts.scss @@ -267,6 +267,20 @@ } } + .verified { + border: 1px solid rgba($valid-value-color, 0.5); + background: rgba($valid-value-color, 0.25); + + a { + color: $valid-value-color; + font-weight: 500; + } + + &__mark { + color: $valid-value-color; + } + } + dl:last-child { border-bottom: 0; } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 7fe5e4a19..b8cc33039 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1,3 +1,5 @@ +$no-columns-breakpoint: 600px; + .admin-wrapper { display: flex; justify-content: center; @@ -24,12 +26,22 @@ height: 100px; } + @media screen and (max-width: $no-columns-breakpoint) { + & > a:first-child { + display: none; + } + } + ul { list-style: none; border-radius: 4px 0 0 4px; overflow: hidden; margin-bottom: 20px; + @media screen and (max-width: $no-columns-breakpoint) { + margin-bottom: 0; + } + a { display: block; padding: 15px; @@ -62,20 +74,24 @@ a { border: 0; padding: 15px 35px; + } + } - &.selected { - color: $primary-text-color; - background-color: $ui-highlight-color; - border-bottom: 0; - border-radius: 0; + .simple-navigation-active-leaf a { + color: $primary-text-color; + background-color: $ui-highlight-color; + border-bottom: 0; + border-radius: 0; - &:hover { - background-color: lighten($ui-highlight-color, 5%); - } - } + &:hover { + background-color: lighten($ui-highlight-color, 5%); } } } + + & > ul > .simple-navigation-active-leaf a { + border-radius: 4px 0 0 4px; + } } .content-wrapper { @@ -89,11 +105,19 @@ padding-top: 60px; padding-left: 25px; + @media screen and (max-width: $no-columns-breakpoint) { + max-width: none; + padding: 15px; + padding-top: 30px; + } + h2 { color: $secondary-text-color; font-size: 24px; line-height: 28px; font-weight: 400; + padding-bottom: 40px; + border-bottom: 1px solid lighten($ui-base-color, 8%); margin-bottom: 40px; } @@ -108,7 +132,7 @@ h4 { text-transform: uppercase; font-size: 13px; - font-weight: 500; + font-weight: 700; color: $darker-text-color; padding-bottom: 8px; margin-bottom: 8px; @@ -122,6 +146,11 @@ font-weight: 400; } + .fields-group h6 { + color: $primary-text-color; + font-weight: 500; + } + & > p { font-size: 14px; line-height: 18px; @@ -167,30 +196,7 @@ } } - .simple_form { - max-width: 400px; - - &.edit_user, - &.new_form_admin_settings, - &.new_form_two_factor_confirmation, - &.new_form_delete_confirmation, - &.new_import, - &.new_domain_block, - &.edit_domain_block { - max-width: none; - } - - .form_two_factor_confirmation_code, - .form_delete_confirmation_password { - max-width: 400px; - } - - .actions { - max-width: 400px; - } - } - - @media screen and (max-width: 600px) { + @media screen and (max-width: $no-columns-breakpoint) { display: block; overflow-y: auto; -webkit-overflow-scrolling: touch; @@ -204,16 +210,8 @@ .sidebar { width: 100%; - padding: 10px 0; + padding: 0; height: auto; - - .logo { - margin: 20px auto; - } - } - - .content { - padding-top: 20px; } } } @@ -578,3 +576,102 @@ a.name-tag, color: $dark-text-color; } } + +.report-card { + background: $ui-base-color; + border-radius: 4px; + margin-bottom: 20px; + + &__profile { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px; + + .account { + padding: 0; + border: 0; + + &__avatar-wrapper { + margin-left: 0; + } + } + + &__stats { + flex: 0 0 auto; + font-weight: 500; + color: $darker-text-color; + text-transform: uppercase; + text-align: right; + + a { + color: inherit; + text-decoration: none; + + &:focus, + &:hover, + &:active { + color: lighten($darker-text-color, 8%); + } + } + + .red { + color: $error-value-color; + } + } + } + + &__summary { + &__item { + display: flex; + justify-content: flex-start; + border-top: 1px solid darken($ui-base-color, 4%); + + &:hover { + background: lighten($ui-base-color, 2%); + } + + &__reported-by, + &__assigned { + padding: 15px; + flex: 0 0 auto; + box-sizing: border-box; + width: 150px; + color: $darker-text-color; + + &, + .username { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__content { + flex: 1 1 auto; + max-width: calc(100% - 300px); + + &__icon { + color: $dark-text-color; + margin-right: 4px; + font-weight: 500; + } + } + + &__content a { + display: block; + box-sizing: border-box; + width: 100%; + padding: 15px; + text-decoration: none; + color: $darker-text-color; + } + } + } +} + +.one-line { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index 11c91bbc9..9c2499ac4 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -1,3 +1,10 @@ +@function hex-color($color) { + @if type-of($color) == 'color' { + $color: str-slice(ie-hex-str($color), 4); + } + @return '%23' + unquote($color) +} + body { font-family: 'mastodon-font-sans-serif', sans-serif; background: darken($ui-base-color, 8%); diff --git a/app/javascript/flavours/glitch/styles/components/accounts.scss b/app/javascript/flavours/glitch/styles/components/accounts.scss index b2b6248ff..3eddd7fb4 100644 --- a/app/javascript/flavours/glitch/styles/components/accounts.scss +++ b/app/javascript/flavours/glitch/styles/components/accounts.scss @@ -239,6 +239,11 @@ flex: 0 1 100%; border-left: 1px solid lighten($ui-base-color, 8%); padding: 10px 0; + border-bottom: 4px solid transparent; + + &.active { + border-bottom: 4px solid $ui-highlight-color; + } & > span { display: block; diff --git a/app/javascript/flavours/glitch/styles/components/boost.scss b/app/javascript/flavours/glitch/styles/components/boost.scss index d92444042..f1ad041e9 100644 --- a/app/javascript/flavours/glitch/styles/components/boost.scss +++ b/app/javascript/flavours/glitch/styles/components/boost.scss @@ -1,10 +1,3 @@ -@function hex-color($color) { - @if type-of($color) == 'color' { - $color: str-slice(ie-hex-str($color), 4); - } - @return '%23' + unquote($color) -} - button.icon-button i.fa-retweet { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='22' height='209'><path d='M4.97 3.16c-.1.03-.17.1-.22.18L.8 8.24c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77L5.5 3.35c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.02-2.4.02H7.1l2.32 2.85.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($action-button-color)}' stroke-width='0'/><path d='M7.78 19.66c-.24.02-.44.25-.44.5v2.46h-.06c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v4.47c0 4.26-.56 3.62 3.65 3.62H8.5l-1.3-1.06c-.1-.08-.18-.2-.2-.3-.02-.17.06-.35.2-.45l1.33-1.1H7.28c-.44 0-.72-.3-.72-.7v-4.48c0-.44.28-.72.72-.72h.06v2.5c0 .38.54.63.82.38l4.9-3.93c.25-.18.25-.6 0-.78l-4.9-3.92c-.1-.1-.24-.14-.38-.12zm9.34 2.93c-.54-.02-1.3.02-2.4.02h-1.25l1.3 1.07c.1.07.18.2.2.33.02.16-.06.3-.2.4l-1.33 1.1h1.28c.42 0 .72.28.72.72v4.47c0 .42-.3.72-.72.72h-.1v-2.47c0-.3-.3-.53-.6-.47-.07 0-.14.05-.2.1l-4.9 3.93c-.26.18-.26.6 0 .78l4.9 3.92c.27.25.82 0 .8-.38v-2.5h.1c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.15.4-3.62-1.25-3.66zM10.34 38.66c-.24.02-.44.25-.43.5v2.47H7.3c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.47c0 3.66-.23 3.7 2.34 3.66l-1.34-1.1c-.1-.08-.18-.2-.2-.3 0-.17.07-.35.2-.45l1.96-1.6c-.03-.06-.04-.13-.04-.2v-4.48c0-.44.28-.72.72-.72H9.9v2.5c0 .36.5.6.8.38l4.93-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.08-.23-.13-.36-.12zm5.63 2.93l1.34 1.1c.1.07.18.2.2.33.02.16-.03.3-.16.4l-1.96 1.6c.02.07.06.13.06.22v4.47c0 .42-.3.72-.72.72h-2.66v-2.47c0-.3-.3-.53-.6-.47-.06.02-.12.05-.18.1l-4.94 3.93c-.24.18-.24.6 0 .78l4.94 3.92c.28.22.78-.02.78-.38v-2.5h2.66c4.27 0 3.65.67 3.65-3.62v-4.47c0-3.66.34-3.7-2.4-3.66zM13.06 57.66c-.23.03-.4.26-.4.5v2.47H7.28c-1.08 0-1.86-.04-2.4-.04-1.64 0-1.25.43-1.25 3.65v4.87l2.93-2.37v-2.5c0-.44.28-.72.72-.72h5.38v2.5c0 .36.5.6.78.38l4.94-3.93c.24-.18.24-.6 0-.78l-4.94-3.92c-.1-.1-.24-.14-.38-.12zm5.3 6.15l-2.92 2.4v2.52c0 .42-.3.72-.72.72h-5.4v-2.47c0-.3-.32-.53-.6-.47-.07.02-.13.05-.2.1L3.6 70.52c-.25.18-.25.6 0 .78l4.93 3.92c.28.22.78-.02.78-.38v-2.5h5.42c4.27 0 3.65.67 3.65-3.62v-4.47-.44zM19.25 78.8c-.1.03-.2.1-.28.17l-.9.9c-.44-.3-1.36-.25-3.35-.25H7.28c-1.08 0-1.86-.03-2.4-.03-1.64 0-1.25.43-1.25 3.65v.7l2.93.3v-1c0-.44.28-.72.72-.72h7.44c.2 0 .37.08.5.2l-1.8 1.8c-.25.26-.08.76.27.8l6.27.7c.28.03.56-.25.53-.53l-.7-6.25c0-.27-.3-.48-.55-.44zm-17.2 6.1c-.2.07-.36.3-.33.54l.7 6.25c.02.36.58.55.83.27l.8-.8c.02 0 .04-.02.04 0 .46.24 1.37.17 3.18.17h7.44c4.27 0 3.65.67 3.65-3.62v-.75l-2.93-.3v1.05c0 .42-.3.72-.72.72H7.28c-.15 0-.3-.03-.4-.1L8.8 86.4c.3-.24.1-.8-.27-.84l-6.28-.65h-.2zM4.88 98.6c-1.33 0-1.34.48-1.3 2.3l1.14-1.37c.08-.1.22-.17.34-.2.16 0 .34.08.44.2l1.66 2.03c.04 0 .07-.03.12-.03h7.44c.34 0 .57.2.65.5h-2.43c-.34.05-.53.52-.3.78l3.92 4.95c.18.24.6.24.78 0l3.94-4.94c.22-.27-.02-.76-.37-.77H18.4c.02-3.9.6-3.4-3.66-3.4H7.28c-1.08 0-1.86-.04-2.4-.04zm.15 2.46c-.1.03-.2.1-.28.2l-3.94 4.9c-.2.28.03.77.4.78H3.6c-.02 3.94-.45 3.4 3.66 3.4h7.44c3.65 0 3.74.3 3.7-2.25l-1.1 1.34c-.1.1-.2.17-.32.2-.16 0-.34-.08-.44-.2l-1.65-2.03c-.06.02-.1.04-.18.04H7.28c-.35 0-.57-.2-.66-.5h2.44c.37 0 .63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.23-.47-.2zM4.88 117.6c-1.16 0-1.3.3-1.3 1.56l1.14-1.38c.08-.1.22-.14.34-.16.16 0 .34.04.44.16l2.22 2.75h7c.42 0 .72.28.72.72v.53h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-.53c0-4.2.72-3.63-3.66-3.63H7.28c-1.08 0-1.86-.03-2.4-.03zm.1 1.74c-.1.03-.17.1-.23.16L.8 124.44c-.2.28.03.77.4.78H3.6v.5c0 4.26-.55 3.62 3.66 3.62h7.44c1.03 0 1.74.02 2.28 0-.16.02-.34-.03-.44-.15l-2.22-2.76H7.28c-.44 0-.72-.3-.72-.72v-.5h2.5c.37.02.63-.5.4-.78L5.5 119.5c-.12-.15-.34-.22-.53-.16zm12.02 10c1.2-.02 1.4-.25 1.4-1.53l-1.1 1.36c-.07.1-.17.17-.3.18zM5.94 136.6l2.37 2.93h6.42c.42 0 .72.28.72.72v1.25h-2.6c-.3.1-.43.54-.2.78l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.25c0-4.2.72-3.63-3.66-3.63H7.28c-.6 0-.92-.02-1.34-.03zm-1.72.06c-.4.08-.54.3-.6.75l.6-.74zm.84.93c-.12 0-.24.08-.3.18l-3.95 4.9c-.24.3 0 .83.4.82H3.6v1.22c0 4.26-.55 3.62 3.66 3.62h7.44c.63 0 .97.02 1.4.03l-2.37-2.93H7.28c-.44 0-.72-.3-.72-.72v-1.22h2.5c.4.04.67-.53.4-.8l-3.96-4.92c-.1-.13-.27-.2-.44-.2zm13.28 10.03l-.56.7c.36-.07.5-.3.56-.7zM17.13 155.6c-.55-.02-1.32.03-2.4.03h-8.2l2.38 2.9h5.82c.42 0 .72.28.72.72v1.97H12.9c-.32.06-.48.52-.28.78l3.94 4.94c.2.23.6.22.78-.03l3.94-4.9c.22-.28-.02-.77-.37-.78H18.4v-1.97c0-3.15.4-3.62-1.25-3.66zm-12.1.28c-.1.02-.2.1-.28.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v1.96c0 4.26-.55 3.62 3.66 3.62h8.24l-2.36-2.9H7.28c-.44 0-.72-.3-.72-.72v-1.97h2.5c.37.02.63-.5.4-.78l-3.96-4.9c-.1-.15-.3-.22-.47-.2zM5.13 174.5c-.15 0-.3.07-.38.2L.8 179.6c-.24.27 0 .82.4.8H3.6v2.32c0 4.26-.55 3.62 3.66 3.62h7.94l-2.35-2.9h-5.6c-.43 0-.7-.3-.7-.72v-2.3h2.5c.38.03.66-.54.4-.83l-3.97-4.9c-.1-.13-.23-.2-.38-.2zm12 .1c-.55-.02-1.32.03-2.4.03H6.83l2.35 2.9h5.52c.42 0 .72.28.72.72v2.34h-2.6c-.3.1-.43.53-.2.78l3.92 4.9c.18.24.6.24.78 0l3.94-4.9c.22-.3-.02-.78-.37-.8H18.4v-2.33c0-3.15.4-3.62-1.25-3.66zM4.97 193.16c-.1.03-.17.1-.22.18l-3.94 4.9c-.2.3.03.78.4.8H3.6v2.68c0 4.26-.55 3.62 3.66 3.62h7.66l-2.3-2.84c-.03-.02-.03-.04-.05-.06H7.27c-.44 0-.72-.3-.72-.72v-2.7h2.5c.37.03.63-.48.4-.77l-3.96-4.9c-.12-.17-.34-.25-.53-.2zm12.16.43c-.55-.02-1.32.03-2.4.03H7.1l2.32 2.84.03.06h5.25c.42 0 .72.28.72.72v2.7h-2.5c-.36.02-.56.54-.3.8l3.92 4.9c.18.25.6.25.78 0l3.94-4.9c.26-.28 0-.83-.37-.8H18.4v-2.7c0-3.15.4-3.62-1.25-3.66z' fill='#{hex-color($highlight-text-color)}' stroke-width='0'/></svg>"); diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 0432b233a..6a9af4490 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -5,7 +5,6 @@ padding: 10px 5px; width: 300px; flex: none; - contain: strict; &:first-child { padding-left: 10px; @@ -49,7 +48,6 @@ background: lighten($ui-base-color, 13%); overflow-x: hidden; overflow-y: auto; - contain: strict; & > .mastodon { flex: 1; @@ -253,7 +251,6 @@ background: $ui-base-color; overflow-x: hidden; overflow-y: auto; - contain: strict; & > header { border-bottom: 1px solid darken($ui-base-color, 4%); diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 36417cde8..cbf968ec4 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -415,14 +415,14 @@ &.top { bottom: -5px; - margin-left: -13px; + margin-left: -7px; border-width: 5px 7px 0; border-top-color: $ui-secondary-color; } &.bottom { top: -5px; - margin-left: -13px; + margin-left: -7px; border-width: 0 7px 5px; border-bottom-color: $ui-secondary-color; } @@ -571,7 +571,7 @@ } } - span { + span:last-child { margin-left: 5px; display: none; } @@ -827,6 +827,10 @@ color: $highlight-text-color; } +.reduce-motion button.icon-button.disabled i.fa-retweet { + color: darken($action-button-color, 13%); +} + .load-more { display: block; color: $dark-text-color; @@ -837,8 +841,10 @@ line-height: inherit; margin: 0; padding: 15px; + box-sizing: border-box; width: 100%; clear: both; + text-decoration: none; &:hover { background: lighten($ui-base-color, 2%); @@ -1119,6 +1125,22 @@ left: 0; } +.icon-badge-wrapper { + position: relative; +} + +.icon-badge { + position: absolute; + display: block; + right: -.25em; + top: -.25em; + background-color: $ui-highlight-color; + border-radius: 50%; + font-size: 75%; + width: 1em; + height: 1em; +} + ::-webkit-scrollbar-thumb { border-radius: 0; } diff --git a/app/javascript/flavours/glitch/styles/components/media.scss b/app/javascript/flavours/glitch/styles/components/media.scss index 5a49c07fa..40a144de4 100644 --- a/app/javascript/flavours/glitch/styles/components/media.scss +++ b/app/javascript/flavours/glitch/styles/components/media.scss @@ -78,17 +78,11 @@ box-sizing: border-box; margin-top: 8px; overflow: hidden; + border-radius: 4px; position: relative; - background: $base-shadow-color; width: 100%; height: 110px; - .detailed-status & { - margin-left: -14px; - width: calc(100% + 28px); - height: 250px; - } - @include fullwidth-gallery; } @@ -98,6 +92,12 @@ display: block; float: left; position: relative; + border-radius: 4px; + overflow: hidden; + + .full-width & { + border-radius: 0; + } &.standalone { .media-gallery__item-gifv-thumbnail { @@ -105,13 +105,17 @@ top: 0; } } + + &.letterbox { + background: $base-shadow-color; + } } .media-gallery__item-thumbnail { cursor: zoom-in; display: block; text-decoration: none; - height: 100%; + color: $secondary-text-color; line-height: 0; &, @@ -294,7 +298,6 @@ max-width: 100vw; max-height: 80vh; z-index: 1; - object-fit: cover; position: relative; } diff --git a/app/javascript/flavours/glitch/styles/components/metadata.scss b/app/javascript/flavours/glitch/styles/components/metadata.scss index 2efe6cd66..da045574a 100644 --- a/app/javascript/flavours/glitch/styles/components/metadata.scss +++ b/app/javascript/flavours/glitch/styles/components/metadata.scss @@ -4,17 +4,12 @@ overflow: hidden; margin: 20px -10px -20px; border-bottom: 0; - - a { - text-decoration: none; - - &:hover{ - text-decoration: underline; - } - } + border-top: 0; dl { - border-top: 1px solid lighten($ui-base-color, 8%); + background: $ui-base-color; + border-top: 1px solid lighten($ui-base-color, 4%); + border-bottom: 0; display: flex; } @@ -35,10 +30,6 @@ width: 120px; flex: 0 0 auto; font-weight: 500; - - a { - color: $primary-text-color; - } } dd { @@ -46,8 +37,9 @@ color: $primary-text-color; background: $ui-base-color; - a { - color: $highlight-text-color; + &.verified { + border: 1px solid rgba($valid-value-color, 0.5); + background: rgba($valid-value-color, 0.25); } } } diff --git a/app/javascript/flavours/glitch/styles/components/status.scss b/app/javascript/flavours/glitch/styles/components/status.scss index fbc26ed2a..aa49aba55 100644 --- a/app/javascript/flavours/glitch/styles/components/status.scss +++ b/app/javascript/flavours/glitch/styles/components/status.scss @@ -623,7 +623,6 @@ .status-card { display: flex; - cursor: pointer; font-size: 14px; border: 1px solid lighten($ui-base-color, 8%); border-radius: 4px; @@ -632,20 +631,62 @@ text-decoration: none; overflow: hidden; - &:hover { - background: lighten($ui-base-color, 8%); + &__actions { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0; + display: flex; + justify-content: center; + align-items: center; + + & > div { + background: rgba($base-shadow-color, 0.6); + border-radius: 4px; + padding: 12px 9px; + flex: 0 0 auto; + display: flex; + justify-content: center; + align-items: center; + } + + button, + a { + display: inline; + color: $primary-text-color; + background: transparent; + border: 0; + padding: 0 5px; + text-decoration: none; + opacity: 0.6; + font-size: 18px; + line-height: 18px; + + &:hover, + &:active, + &:focus { + opacity: 1; + } + } + + a { + font-size: 19px; + position: relative; + bottom: -1px; + } + + a .fa, a:hover .fa { + color: inherit; + } } } -.status-card-video, -.status-card-rich, -.status-card-photo { - margin-top: 14px; - overflow: hidden; +a.status-card { + cursor: pointer; - iframe { - width: 100%; - height: auto; + &:hover { + background: lighten($ui-base-color, 8%); } } @@ -673,6 +714,7 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + text-decoration: none; } .status-card__content { @@ -694,6 +736,7 @@ .status-card__image { flex: 0 0 100px; background: lighten($ui-base-color, 8%); + position: relative; } .status-card.horizontal { @@ -719,6 +762,8 @@ width: 100%; height: 100%; object-fit: cover; + background-size: cover; + background-position: center center; } .status__video-player { @@ -736,7 +781,7 @@ .status__video-player-video { height: 100%; - object-fit: cover; + object-fit: contain; position: relative; top: 50%; transform: translateY(-50%); diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index b5d79f4d7..d1b9934d7 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -347,6 +347,23 @@ margin-bottom: 10px; box-shadow: 0 0 15px rgba($base-shadow-color, 0.2); + &.inactive { + opacity: 0.5; + + .public-account-header__image, + .avatar { + filter: grayscale(100%); + } + + .logo-button { + background-color: $secondary-text-color; + + svg path:last-child { + fill: $secondary-text-color; + } + } + } + &__image { border-radius: 4px 4px 0 0; overflow: hidden; @@ -588,6 +605,10 @@ border-bottom: 4px solid $highlight-text-color; opacity: 1; } + + &.inactive::after { + border-bottom-color: $secondary-text-color; + } } &:hover { @@ -703,6 +724,14 @@ a { color: lighten($ui-highlight-color, 8%); } + + dl:first-child .verified { + border-radius: 0 4px 0 0; + } + + .verified a { + color: $valid-value-color; + } } .account__header__content { diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index f97890187..cbd3de94c 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -1,3 +1,5 @@ +$no-columns-breakpoint: 600px; + code { font-family: 'mastodon-font-monospace', monospace; font-weight: 400; @@ -13,6 +15,60 @@ code { .input { margin-bottom: 15px; overflow: hidden; + + &.hidden { + margin: 0; + } + + &.radio_buttons { + .radio { + margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } + } + + .radio > label { + position: relative; + padding-left: 28px; + + input { + position: absolute; + top: -2px; + left: 0; + } + } + } + + &.boolean { + position: relative; + margin-bottom: 0; + + .label_input > label { + font-family: inherit; + font-size: 14px; + padding-top: 5px; + color: $primary-text-color; + display: block; + width: auto; + } + + .label_input, + .hint { + padding-left: 28px; + } + + .label_input__wrapper { + position: static; + } + + label.checkbox { + position: absolute; + top: 2px; + left: 0; + } + } } .row { @@ -27,9 +83,22 @@ code { } } + .hint { + color: $darker-text-color; + + a { + color: $highlight-text-color; + } + + code { + border-radius: 3px; + padding: 0.2em 0.4em; + background: darken($ui-base-color, 12%); + } + } + span.hint { display: block; - color: $darker-text-color; font-size: 12px; margin-top: 4px; } @@ -44,11 +113,6 @@ code { line-height: 18px; margin-top: 15px; margin-bottom: 0; - color: $darker-text-color; - - a { - color: $highlight-text-color; - } } } @@ -66,81 +130,60 @@ code { } } - .label_input { - display: flex; + .input.with_floating_label { + .label_input { + display: flex; - label { - flex: 0 0 auto; + & > label { + font-family: inherit; + font-size: 14px; + color: $primary-text-color; + font-weight: 500; + min-width: 150px; + flex: 0 0 auto; + } + + input, + select { + flex: 1 1 auto; + } } - input { - flex: 1 1 auto; + &.select .hint { + margin-top: 6px; + margin-left: 150px; } } .input.with_label { - padding: 15px 0; - margin-bottom: 0; - - .label_input { - flex-wrap: wrap; - align-items: flex-start; - } - - &.select .label_input { - align-items: initial; - } - .label_input > label { font-family: inherit; - font-size: 16px; + font-size: 14px; color: $primary-text-color; display: block; - padding-top: 5px; - margin-bottom: 5px; - flex: 1; - min-width: 150px; + margin-bottom: 8px; word-wrap: break-word; + font-weight: 500; + } - &.select { - flex: 0; - } - - & ~ * { - margin-left: 10px; - } + .hint { + margin-top: 6px; } ul { flex: 390px; } - - &.boolean { - padding: initial; - margin-bottom: initial; - - .label_input > label { - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; - } - - label.checkbox { - position: relative; - padding-left: 25px; - flex: 1 1 auto; - } - } } .input.with_block_label { + max-width: none; + & > label { font-family: inherit; font-size: 16px; color: $primary-text-color; display: block; + font-weight: 500; padding-top: 5px; } @@ -148,55 +191,75 @@ code { margin-bottom: 15px; } - li { - float: left; - width: 50%; + ul { + columns: 2; } } + .required abbr { + text-decoration: none; + color: lighten($error-value-color, 12%); + } + .fields-group { margin-bottom: 25px; - } - .input.radio_buttons .radio label { - margin-bottom: 5px; - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; + .input:last-child { + margin-bottom: 0; + } } - .input.boolean { - margin-bottom: 5px; + .fields-row { + display: flex; + margin: 0 -10px; + padding-top: 5px; + margin-bottom: 25px; - label { - font-family: inherit; - font-size: 14px; - color: $primary-text-color; - display: block; - width: auto; + .input { + max-width: none; } - label.checkbox { - position: relative; - padding-left: 25px; + &__column { + box-sizing: border-box; + padding: 0 10px; flex: 1 1 auto; + min-height: 1px; + + &-6 { + max-width: 50%; + } } - input[type=checkbox] { - position: absolute; - left: 0; - top: 5px; - margin: 0; + .fields-group:last-child, + .fields-row__column.fields-group { + margin-bottom: 0; } - .hint { - padding-left: 25px; - margin-left: 0; + @media screen and (max-width: $no-columns-breakpoint) { + display: block; + margin-bottom: 0; + + &__column { + max-width: none; + } + + .fields-group:last-child, + .fields-row__column.fields-group, + .fields-row__column { + margin-bottom: 25px; + } } } + .input.radio_buttons .radio label { + margin-bottom: 5px; + font-family: inherit; + font-size: 14px; + color: $primary-text-color; + display: block; + width: auto; + } + .check_boxes { .checkbox { label { @@ -225,12 +288,7 @@ code { input[type=email], input[type=password], textarea { - background: transparent; box-sizing: border-box; - border: 0; - border-bottom: 2px solid $ui-primary-color; - border-radius: 2px 2px 0 0; - padding: 7px 4px; font-size: 16px; color: $primary-text-color; display: block; @@ -238,23 +296,31 @@ code { outline: 0; font-family: inherit; resize: vertical; + background: darken($ui-base-color, 10%); + border: 1px solid darken($ui-base-color, 14%); + border-radius: 4px; + padding: 10px; &:invalid { box-shadow: none; } &:focus:invalid { - border-bottom-color: lighten($error-red, 12%); + border-color: lighten($error-red, 12%); } &:required:valid { - border-bottom-color: $valid-value-color; + border-color: $valid-value-color; + } + + &:hover { + border-color: darken($ui-base-color, 20%); } &:active, &:focus { - border-bottom-color: $highlight-text-color; - background: rgba($base-overlay-background, 0.1); + border-color: $highlight-text-color; + background: darken($ui-base-color, 8%); } } @@ -338,28 +404,52 @@ code { } select { + appearance: none; + box-sizing: border-box; font-size: 16px; - max-height: 29px; + color: $primary-text-color; + display: block; + width: 100%; + outline: 0; + font-family: inherit; + resize: vertical; + background: darken($ui-base-color, 10%) url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 12%))}'/></svg>") no-repeat right 8px center / auto 16px; + border: 1px solid darken($ui-base-color, 14%); + border-radius: 4px; + padding: 10px; + height: 41px; } - .input-with-append { - position: relative; - - .input input { - padding-right: 127px; + .label_input { + &__wrapper { + position: relative; } - .append { + &__append { position: absolute; - right: 0; - top: 0; - padding: 7px 4px; + right: 1px; + top: 1px; + padding: 10px; padding-bottom: 9px; font-size: 16px; color: $dark-text-color; font-family: inherit; pointer-events: none; cursor: default; + max-width: 140px; + white-space: nowrap; + overflow: hidden; + + &::after { + content: ''; + display: block; + position: absolute; + top: 0; + right: 0; + bottom: 1px; + width: 5px; + background-image: linear-gradient(to right, rgba(darken($ui-base-color, 10%), 0), darken($ui-base-color, 10%)); + } } } } @@ -434,6 +524,30 @@ code { } } +.quick-nav { + list-style: none; + margin-bottom: 25px; + font-size: 14px; + + li { + display: inline-block; + margin-right: 10px; + } + + a { + color: $highlight-text-color; + text-transform: uppercase; + text-decoration: none; + font-weight: 700; + + &:hover, + &:focus, + &:active { + color: lighten($highlight-text-color, 8%); + } + } +} + .oauth-prompt, .follow-prompt { margin-bottom: 30px; @@ -592,3 +706,64 @@ code { display: block; } } + +.scope-danger { + color: $warning-red; +} + +.form_admin_settings_site_short_description, +.form_admin_settings_site_description, +.form_admin_settings_site_extended_description, +.form_admin_settings_site_terms, +.form_admin_settings_custom_css, +.form_admin_settings_closed_registrations_message { + textarea { + font-family: 'mastodon-font-monospace', monospace; + } +} + +.input-copy { + background: darken($ui-base-color, 10%); + border: 1px solid darken($ui-base-color, 14%); + border-radius: 4px; + display: flex; + align-items: center; + padding-right: 4px; + position: relative; + top: 1px; + transition: border-color 300ms linear; + + &__wrapper { + flex: 1 1 auto; + } + + input[type=text] { + background: transparent; + border: 0; + padding: 10px; + font-size: 14px; + font-family: 'mastodon-font-monospace', monospace; + } + + button { + flex: 0 0 auto; + margin: 4px; + text-transform: none; + font-weight: 400; + font-size: 14px; + padding: 7px 18px; + padding-bottom: 6px; + width: auto; + transition: background 300ms linear; + } + + &.copied { + border-color: $valid-value-color; + transition: none; + + button { + background: $valid-value-color; + transition: none; + } + } +} diff --git a/app/javascript/flavours/glitch/styles/index.scss b/app/javascript/flavours/glitch/styles/index.scss index 8e3ff43e3..3cb592499 100644 --- a/app/javascript/flavours/glitch/styles/index.scss +++ b/app/javascript/flavours/glitch/styles/index.scss @@ -19,5 +19,6 @@ @import 'about'; @import 'tables'; @import 'admin'; +@import 'accessibility'; @import 'rtl'; @import 'dashboard'; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light.scss b/app/javascript/flavours/glitch/styles/mastodon-light.scss index 029d5dde2..8fc132651 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light.scss @@ -1,218 +1,3 @@ -// Set variables -$ui-base-color: #d9e1e8; -$ui-base-lighter-color: darken($ui-base-color, 57%); -$ui-highlight-color: #2b90d9; -$ui-primary-color: darken($ui-highlight-color, 28%); -$ui-secondary-color: #282c37; - -$primary-text-color: black; -$base-overlay-background: $ui-base-color; - -$login-button-color: white; -$account-background-color: white; - -// Import defaults +@import 'mastodon-light/variables'; @import 'index'; - -// Change the color of the log in button -.button { - &.button-alternative-2 { - color: $login-button-color; - } -} - -// Change columns' default background colors -.column { - > .scrollable { - background: lighten($ui-base-color, 13%); - } -} - -.status.collapsed .status__content:after { - background: linear-gradient(rgba(lighten($ui-base-color, 13%), 0), rgba(lighten($ui-base-color, 13%), 1)); -} - -.drawer__inner { - background: $ui-base-color; -} - -.drawer > .contents { - background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color(lighten($ui-base-color, 13%))}"/></svg>') no-repeat bottom / 100% auto !important; - - .mastodon { - filter: contrast(75%) brightness(75%) !important; - } -} - -// Change the default appearance of the content warning button -.status__content { - - .status__content__spoiler-link { - - background: darken($ui-base-color, 30%); - - &:hover { - background: darken($ui-base-color, 35%); - text-decoration: none; - } - - } - -} - -// Change the default appearance of the action buttons -.icon-button { - - &:hover, - &:active, - &:focus { - color: darken($ui-base-color, 40%); - transition: color 200ms ease-out; - } - - &.disabled { - color: darken($ui-base-color, 30%); - } - -} - -.status { - &.status-direct { - .icon-button.disabled { - color: darken($ui-base-color, 30%); - } - } -} - -// Change the colors used in the dropdown menu -.dropdown-menu { - background: $ui-base-color; -} - -.dropdown-menu__arrow { - - &.left { - border-left-color: $ui-base-color; - } - - &.top { - border-top-color: $ui-base-color; - } - - &.bottom { - border-bottom-color: $ui-base-color; - } - - &.right { - border-right-color: $ui-base-color; - } - -} - -.dropdown-menu__item { - a { - background: $ui-base-color; - color: $ui-secondary-color; - } -} - -// Change the default color of several parts of the compose form -.composer { - - .composer--spoiler input, .composer--textarea textarea { - color: darken($ui-base-color, 80%); - - &:disabled { background: darken($simple-background-color, 10%) } - - &::placeholder { - color: darken($ui-base-color, 70%); - } - } - - strong { - color: lighten($ui-secondary-color, 65%); - } - - .composer--options { - background: darken($ui-base-color, 10%); - box-shadow: unset; - } - - .composer--options--dropdown--content--item { - color: $ui-primary-color; - - strong { - color: $ui-primary-color; - } - - } - -} - -// Change the default color used for the text in an empty column or on the error column -.empty-column-indicator, -.error-column { - color: darken($ui-base-color, 60%); -} - -// Change the default colors used on some parts of the profile pages -.activity-stream-tabs { - - background: $account-background-color; - - a { - &.active { - color: $ui-primary-color; - } - } - -} - -.activity-stream { - - .entry { - background: $account-background-color; - } - - .status.light { - - .status__content { - color: $primary-text-color; - } - - .display-name { - strong { - color: $primary-text-color; - } - } - - } - -} - -.accounts-grid { - .account-grid-card { - - .controls { - .icon-button { - color: $ui-secondary-color; - } - } - - .name { - a { - color: $primary-text-color; - } - } - - .username { - color: $ui-secondary-color; - } - - .account__header__content { - color: $primary-text-color; - } - - } -} - +@import 'mastodon-light/diff'; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss new file mode 100644 index 000000000..aba8baf70 --- /dev/null +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -0,0 +1,318 @@ +// Notes! +// Sass color functions, "darken" and "lighten" are automatically replaced. + +.glitch.local-settings { + background: $ui-base-color; + + &__navigation { + background: darken($ui-base-color, 8%); + } + + &__navigation__item { + background: darken($ui-base-color, 8%); + + &:hover { + background: $ui-base-color; + } + } +} + +.notification__dismiss-overlay { + .wrappy { + box-shadow: unset; + } + + .ckbox { + text-shadow: unset; + } +} + +.status.status-direct { + background: darken($ui-base-color, 8%); + + &.collapsed> .status__content:after { + background: linear-gradient(rgba(darken($ui-base-color, 8%), 0), rgba(darken($ui-base-color, 8%), 1)); + } +} + +.focusable:focus.status.status-direct { + background: darken($ui-base-color, 4%); + + &.collapsed> .status__content:after { + background: linear-gradient(rgba(darken($ui-base-color, 4%), 0), rgba(darken($ui-base-color, 4%), 1)); + } +} + +// Change columns' default background colors +.column { + > .scrollable { + background: darken($ui-base-color, 13%); + } +} + +.status.collapsed .status__content:after { + background: linear-gradient(rgba(darken($ui-base-color, 13%), 0), rgba(darken($ui-base-color, 13%), 1)); +} + +.drawer__inner { + background: $ui-base-color; +} + +.drawer > .contents { + background: $ui-base-color url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color(darken($ui-base-color, 13%))}"/></svg>') no-repeat bottom / 100% auto !important; + + .mastodon { + filter: contrast(75%) brightness(75%) !important; + } +} + +// Change the default appearance of the content warning button +.status__content { + + .status__content__spoiler-link { + + background: lighten($ui-base-color, 30%); + + &:hover { + background: lighten($ui-base-color, 35%); + text-decoration: none; + } + + } + +} + +// Change the background colors of media and video spoilers +.media-spoiler, +.video-player__spoiler { + background: $ui-base-color; +} + +// Change the colors used in the dropdown menu +.dropdown-menu { + background: $ui-base-color; +} + +.dropdown-menu__arrow { + + &.left { + border-left-color: $ui-base-color; + } + + &.top { + border-top-color: $ui-base-color; + } + + &.bottom { + border-bottom-color: $ui-base-color; + } + + &.right { + border-right-color: $ui-base-color; + } + +} + +.dropdown-menu__item { + a { + background: $ui-base-color; + color: $ui-secondary-color; + } +} + +// Change the default color of several parts of the compose form +.composer { + + .composer--spoiler input, .composer--textarea textarea { + color: lighten($ui-base-color, 80%); + + &:disabled { background: lighten($simple-background-color, 10%) } + + &::placeholder { + color: lighten($ui-base-color, 70%); + } + } + + .composer--options { + background: lighten($ui-base-color, 10%); + box-shadow: unset; + + & > hr { + display: none; + } + } + + .composer--options--dropdown--content--item { + color: $ui-primary-color; + + strong { + color: $ui-primary-color; + } + + } + +} + +.composer--upload_form--actions .icon-button { + color: lighten($white, 7%); + + &:active, + &:focus, + &:hover { + color: $white; + } +} + +.composer--upload_form--item > div input { + color: lighten($white, 7%); + + &::placeholder { + color: lighten($white, 10%); + } +} + +.dropdown-menu__separator { + border-bottom-color: lighten($ui-base-color, 12%); +} + +.status__content, +.reply-indicator__content { + a { + color: $highlight-text-color; + } +} + +.emoji-mart-bar { + border-color: darken($ui-base-color, 4%); + + &:first-child { + background: lighten($ui-base-color, 10%); + } +} + +.emoji-mart-search input { + background: rgba($ui-base-color, 0.3); + border-color: $ui-base-color; +} + +.composer--textarea--suggestions { + background: lighten($ui-base-color, 10%) +} + +.composer--textarea--suggestions--item { + &:hover, + &:focus, + &:active, + &.selected { + background: darken($ui-base-color, 4%); + } +} + +.react-toggle-track { + background: $ui-secondary-color; +} + +.react-toggle:hover:not(.react-toggle--disabled) .react-toggle-track { + background: lighten($ui-secondary-color, 10%); +} + +.react-toggle.react-toggle--checked:hover:not(.react-toggle--disabled) .react-toggle-track { + background: darken($ui-highlight-color, 10%); +} + +// Change the background colors of modals +.actions-modal, +.boost-modal, +.confirmation-modal, +.mute-modal, +.report-modal, +.embed-modal, +.error-modal, +.onboarding-modal { + background: $ui-base-color; +} + +.boost-modal__action-bar, +.confirmation-modal__action-bar, +.mute-modal__action-bar, +.onboarding-modal__paginator, +.error-modal__footer { + background: darken($ui-base-color, 6%); + + .onboarding-modal__nav, + .error-modal__nav { + &:hover, + &:focus, + &:active { + background-color: darken($ui-base-color, 12%); + } + } +} + +// Change the default color used for the text in an empty column or on the error column +.empty-column-indicator, +.error-column { + color: lighten($ui-base-color, 60%); +} + +// Change the default colors used on some parts of the profile pages +.activity-stream-tabs { + + background: $account-background-color; + + a { + &.active { + color: $ui-primary-color; + } + } + +} + +.activity-stream { + + .entry { + background: $account-background-color; + } + + .status.light { + + .status__content { + color: $primary-text-color; + } + + .display-name { + strong { + color: $primary-text-color; + } + } + + } + +} + +.accounts-grid { + .account-grid-card { + + .controls { + .icon-button { + color: $ui-secondary-color; + } + } + + .name { + a { + color: $primary-text-color; + } + } + + .username { + color: $ui-secondary-color; + } + + .account__header__content { + color: $primary-text-color; + } + + } +} + diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss new file mode 100644 index 000000000..1b060b58d --- /dev/null +++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss @@ -0,0 +1,38 @@ +// Dependent colors +$black: #000000; +$white: #ffffff; + +$classic-base-color: #282c37; +$classic-primary-color: #9baec8; +$classic-secondary-color: #d9e1e8; +$classic-highlight-color: #2b90d9; + +$ui-base-color: $classic-secondary-color !default; +$ui-base-lighter-color: darken($ui-base-color, 57%); +$ui-highlight-color: $classic-highlight-color !default; +$ui-primary-color: $classic-primary-color !default; +$ui-secondary-color: $classic-base-color !default; + +$primary-text-color: $black !default; +$darker-text-color: $classic-base-color !default; +$dark-text-color: #444b5d; +$action-button-color: #606984; + +$base-overlay-background: $white !default; + +$inverted-text-color: $black !default; +$lighter-text-color: $classic-base-color !default; +$light-text-color: #444b5d; + +$account-background-color: $white !default; + +//Invert darkened and lightened colors +@function darken($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) + $amount); +} + +@function lighten($color, $amount) { + @return hsl(hue($color), saturation($color), lightness($color) - $amount); +} + +$emojis-requiring-outlines: 'alien' 'baseball' 'chains' 'chicken' 'cloud' 'crescent_moon' 'dash' 'dove_of_peace' 'eyes' 'first_quarter_moon' 'first_quarter_moon_with_face' 'fish_cake' 'full_moon' 'full_moon_with_face' 'ghost' 'goat' 'grey_exclamation' 'grey_question' 'ice_skate' 'last_quarter_moon' 'last_quarter_moon_with_face' 'lightning' 'loud_sound' 'moon' 'mute' 'page_with_curl' 'rain_cloud' 'ram' 'rice' 'rice_ball' 'rooster' 'sheep' 'skull' 'skull_and_crossbones' 'snow_cloud' 'sound' 'speaker' 'speech_balloon' 'thought_balloon' 'volleyball' 'waning_crescent_moon' 'waning_gibbous_moon' 'waving_white_flag' 'waxing_crescent_moon' 'white_circle' 'white_large_square' 'white_medium_small_square' 'white_medium_square' 'white_small_square' 'wind_blowing_face'; diff --git a/app/javascript/flavours/glitch/styles/rtl.scss b/app/javascript/flavours/glitch/styles/rtl.scss index e9099a9e9..b8c0efad8 100644 --- a/app/javascript/flavours/glitch/styles/rtl.scss +++ b/app/javascript/flavours/glitch/styles/rtl.scss @@ -206,13 +206,19 @@ body.rtl { } .simple_form .input-with-append .input input { - padding-left: 127px; + padding-left: 142px; padding-right: 0; } .simple_form .input-with-append .append { right: auto; left: 0; + + &::after { + right: auto; + left: 0; + background-image: linear-gradient(to left, rgba($ui-base-color, 0), $ui-base-color); + } } .table th, diff --git a/app/javascript/flavours/glitch/styles/stream_entries.scss b/app/javascript/flavours/glitch/styles/stream_entries.scss index cf82be073..c458fcb79 100644 --- a/app/javascript/flavours/glitch/styles/stream_entries.scss +++ b/app/javascript/flavours/glitch/styles/stream_entries.scss @@ -3,6 +3,7 @@ border-radius: 4px; overflow: hidden; margin-bottom: 10px; + text-align: left; @media screen and (max-width: $no-gap-breakpoint) { margin-bottom: 0; @@ -36,7 +37,8 @@ &:last-child { .detailed-status, - .status { + .status, + .load-more { border-bottom: 0; border-radius: 0 0 4px 4px; } @@ -44,13 +46,15 @@ &:first-child { .detailed-status, - .status { + .status, + .load-more { border-radius: 4px 4px 0 0; } &:last-child { .detailed-status, - .status { + .status, + .load-more { border-radius: 4px; } } @@ -58,11 +62,16 @@ @media screen and (max-width: 740px) { .detailed-status, - .status { + .status, + .load-more { border-radius: 0 !important; } } } + + &--highlighted .entry { + background: lighten($ui-base-color, 8%); + } } .button.logo-button { @@ -101,6 +110,18 @@ } } + &.button--destructive { + &:active, + &:focus, + &:hover { + background: $error-red; + + svg path:last-child { + fill: $error-red; + } + } + } + @media screen and (max-width: $no-gap-breakpoint) { svg { display: none; diff --git a/app/javascript/flavours/glitch/util/emoji/index.js b/app/javascript/flavours/glitch/util/emoji/index.js index c6416db2d..82a1ef89c 100644 --- a/app/javascript/flavours/glitch/util/emoji/index.js +++ b/app/javascript/flavours/glitch/util/emoji/index.js @@ -62,6 +62,10 @@ const emojify = (str, customEmojis = {}) => { const title = shortCode ? `:${shortCode}:` : ''; replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${filename}.svg" />`; rend = i + match.length; + // If the matched character was followed by VS15 (for selecting text presentation), skip it. + if (str.codePointAt(rend) === 65038) { + rend += 1; + } } rtn += str.slice(0, i) + replacement; str = str.slice(rend); diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index 236ea1c3a..fdf004527 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -21,5 +21,6 @@ export const deleteModal = getMeta('delete_modal'); export const me = getMeta('me'); export const searchEnabled = getMeta('search_enabled'); export const maxChars = (initialState && initialState.max_toot_chars) || 500; +export const isStaff = getMeta('is_staff'); export default initialState; |