diff options
Diffstat (limited to 'app/javascript/flavours/glitch')
38 files changed, 527 insertions, 74 deletions
diff --git a/app/javascript/flavours/glitch/actions/custom_emojis.js b/app/javascript/flavours/glitch/actions/custom_emojis.js index 0595a6da7..29ae72edb 100644 --- a/app/javascript/flavours/glitch/actions/custom_emojis.js +++ b/app/javascript/flavours/glitch/actions/custom_emojis.js @@ -19,6 +19,7 @@ export function fetchCustomEmojis() { export function fetchCustomEmojisRequest() { return { type: CUSTOM_EMOJIS_FETCH_REQUEST, + skipLoading: true, }; }; @@ -26,6 +27,7 @@ export function fetchCustomEmojisSuccess(custom_emojis) { return { type: CUSTOM_EMOJIS_FETCH_SUCCESS, custom_emojis, + skipLoading: true, }; }; @@ -33,5 +35,6 @@ export function fetchCustomEmojisFail(error) { return { type: CUSTOM_EMOJIS_FETCH_FAIL, error, + skipLoading: true, }; }; diff --git a/app/javascript/flavours/glitch/actions/favourites.js b/app/javascript/flavours/glitch/actions/favourites.js index 0c0f3af44..28eca8e5f 100644 --- a/app/javascript/flavours/glitch/actions/favourites.js +++ b/app/javascript/flavours/glitch/actions/favourites.js @@ -28,6 +28,7 @@ export function fetchFavouritedStatuses() { export function fetchFavouritedStatusesRequest() { return { type: FAVOURITED_STATUSES_FETCH_REQUEST, + skipLoading: true, }; }; @@ -36,6 +37,7 @@ export function fetchFavouritedStatusesSuccess(statuses, next) { type: FAVOURITED_STATUSES_FETCH_SUCCESS, statuses, next, + skipLoading: true, }; }; @@ -43,6 +45,7 @@ export function fetchFavouritedStatusesFail(error) { return { type: FAVOURITED_STATUSES_FETCH_FAIL, error, + skipLoading: true, }; }; diff --git a/app/javascript/flavours/glitch/actions/lists.js b/app/javascript/flavours/glitch/actions/lists.js index 3c3af5fee..3021fa5b5 100644 --- a/app/javascript/flavours/glitch/actions/lists.js +++ b/app/javascript/flavours/glitch/actions/lists.js @@ -40,6 +40,13 @@ export const LIST_EDITOR_REMOVE_REQUEST = 'LIST_EDITOR_REMOVE_REQUEST'; export const LIST_EDITOR_REMOVE_SUCCESS = 'LIST_EDITOR_REMOVE_SUCCESS'; export const LIST_EDITOR_REMOVE_FAIL = 'LIST_EDITOR_REMOVE_FAIL'; +export const LIST_ADDER_RESET = 'LIST_ADDER_RESET'; +export const LIST_ADDER_SETUP = 'LIST_ADDER_SETUP'; + +export const LIST_ADDER_LISTS_FETCH_REQUEST = 'LIST_ADDER_LISTS_FETCH_REQUEST'; +export const LIST_ADDER_LISTS_FETCH_SUCCESS = 'LIST_ADDER_LISTS_FETCH_SUCCESS'; +export const LIST_ADDER_LISTS_FETCH_FAIL = 'LIST_ADDER_LISTS_FETCH_FAIL'; + export const fetchList = id => (dispatch, getState) => { if (getState().getIn(['lists', id])) { return; @@ -311,3 +318,50 @@ export const removeFromListFail = (listId, accountId, error) => ({ accountId, error, }); + +export const resetListAdder = () => ({ + type: LIST_ADDER_RESET, +}); + +export const setupListAdder = accountId => (dispatch, getState) => { + dispatch({ + type: LIST_ADDER_SETUP, + account: getState().getIn(['accounts', accountId]), + }); + dispatch(fetchLists()); + dispatch(fetchAccountLists(accountId)); +}; + +export const fetchAccountLists = accountId => (dispatch, getState) => { + dispatch(fetchAccountListsRequest(accountId)); + + api(getState).get(`/api/v1/accounts/${accountId}/lists`) + .then(({ data }) => dispatch(fetchAccountListsSuccess(accountId, data))) + .catch(err => dispatch(fetchAccountListsFail(accountId, err))); +}; + +export const fetchAccountListsRequest = id => ({ + type:LIST_ADDER_LISTS_FETCH_REQUEST, + id, +}); + +export const fetchAccountListsSuccess = (id, lists) => ({ + type: LIST_ADDER_LISTS_FETCH_SUCCESS, + id, + lists, +}); + +export const fetchAccountListsFail = (id, err) => ({ + type: LIST_ADDER_LISTS_FETCH_FAIL, + id, + err, +}); + +export const addToListAdder = listId => (dispatch, getState) => { + dispatch(addToList(listId, getState().getIn(['listAdder', 'accountId']))); +}; + +export const removeFromListAdder = listId => (dispatch, getState) => { + dispatch(removeFromList(listId, getState().getIn(['listAdder', 'accountId']))); +}; + diff --git a/app/javascript/flavours/glitch/actions/notifications.js b/app/javascript/flavours/glitch/actions/notifications.js index fb84cd01e..ddf14f78f 100644 --- a/app/javascript/flavours/glitch/actions/notifications.js +++ b/app/javascript/flavours/glitch/actions/notifications.js @@ -89,6 +89,7 @@ const noOp = () => {}; export function expandNotifications({ maxId } = {}, done = noOp) { return (dispatch, getState) => { const notifications = getState().get('notifications'); + const isLoadingMore = !!maxId; if (notifications.get('isLoading')) { done(); @@ -104,40 +105,43 @@ export function expandNotifications({ maxId } = {}, done = noOp) { params.since_id = notifications.getIn(['items', 0]); } - dispatch(expandNotificationsRequest()); + dispatch(expandNotificationsRequest(isLoadingMore)); api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null, isLoadingMore)); fetchRelatedRelationships(dispatch, response.data); done(); }).catch(error => { - dispatch(expandNotificationsFail(error)); + dispatch(expandNotificationsFail(error, isLoadingMore)); done(); }); }; }; -export function expandNotificationsRequest() { +export function expandNotificationsRequest(isLoadingMore) { return { type: NOTIFICATIONS_EXPAND_REQUEST, + skipLoading: !isLoadingMore, }; }; -export function expandNotificationsSuccess(notifications, next) { +export function expandNotificationsSuccess(notifications, next, isLoadingMore) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, accounts: notifications.map(item => item.account), statuses: notifications.map(item => item.status).filter(status => !!status), next, + skipLoading: !isLoadingMore, }; }; -export function expandNotificationsFail(error) { +export function expandNotificationsFail(error, isLoadingMore) { return { type: NOTIFICATIONS_EXPAND_FAIL, error, + skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/flavours/glitch/actions/timelines.js b/app/javascript/flavours/glitch/actions/timelines.js index 7f1ff8e3b..27ca66e51 100644 --- a/app/javascript/flavours/glitch/actions/timelines.js +++ b/app/javascript/flavours/glitch/actions/timelines.js @@ -16,7 +16,6 @@ export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE'; export function updateTimeline(timeline, status) { return (dispatch, getState) => { - const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : []; const parents = []; if (status.in_reply_to_id) { @@ -32,7 +31,6 @@ export function updateTimeline(timeline, status) { type: TIMELINE_UPDATE, timeline, status, - references, }); if (parents.length > 0) { @@ -66,6 +64,7 @@ const noOp = () => {}; export function expandTimeline(timelineId, path, params = {}, done = noOp) { return (dispatch, getState) => { const timeline = getState().getIn(['timelines', timelineId], ImmutableMap()); + const isLoadingMore = !!params.max_id; if (timeline.get('isLoading')) { done(); @@ -76,14 +75,14 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) { params.since_id = timeline.getIn(['items', 0]); } - dispatch(expandTimelineRequest(timelineId)); + dispatch(expandTimelineRequest(timelineId, isLoadingMore)); api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206, isLoadingMore)); done(); }).catch(error => { - dispatch(expandTimelineFail(timelineId, error)); + dispatch(expandTimelineFail(timelineId, error, isLoadingMore)); done(); }); }; @@ -99,28 +98,31 @@ export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandT export const expandHashtagTimeline = (hashtag, { maxId } = {}, done = noOp) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId }, done); export const expandListTimeline = (id, { maxId } = {}, done = noOp) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId }, done); -export function expandTimelineRequest(timeline) { +export function expandTimelineRequest(timeline, isLoadingMore) { return { type: TIMELINE_EXPAND_REQUEST, timeline, + skipLoading: !isLoadingMore, }; }; -export function expandTimelineSuccess(timeline, statuses, next, partial) { +export function expandTimelineSuccess(timeline, statuses, next, partial, isLoadingMore) { return { type: TIMELINE_EXPAND_SUCCESS, timeline, statuses, next, partial, + skipLoading: !isLoadingMore, }; }; -export function expandTimelineFail(timeline, error) { +export function expandTimelineFail(timeline, error, isLoadingMore) { return { type: TIMELINE_EXPAND_FAIL, timeline, error, + skipLoading: !isLoadingMore, }; }; diff --git a/app/javascript/flavours/glitch/components/media_gallery.js b/app/javascript/flavours/glitch/components/media_gallery.js index e8dfd6f8e..10afeb2eb 100644 --- a/app/javascript/flavours/glitch/components/media_gallery.js +++ b/app/javascript/flavours/glitch/components/media_gallery.js @@ -232,12 +232,12 @@ export default class MediaGallery extends React.PureComponent { componentWillReceiveProps (nextProps) { if (!is(nextProps.media, this.props.media)) { - this.setState({ visible: !nextProps.sensitive }); + this.setState({ visible: nextProps.revealed === undefined ? (displayMedia !== 'hide_all' && !nextProps.sensitive || displayMedia === 'show_all') : nextProps.revealed }); } } componentDidUpdate (prevProps) { - if (this.node && this.node.offsetWidth) { + if (this.node && this.node.offsetWidth && this.node.offsetWidth != this.state.width) { this.setState({ width: this.node.offsetWidth, }); @@ -254,8 +254,7 @@ export default class MediaGallery extends React.PureComponent { handleRef = (node) => { this.node = node; - if (node /*&& this.isStandaloneEligible()*/) { - // offsetWidth triggers a layout, so only calculate when we need to + if (node && node.offsetWidth && node.offsetWidth != this.state.width) { this.setState({ width: node.offsetWidth, }); @@ -276,10 +275,14 @@ export default class MediaGallery extends React.PureComponent { const style = {}; + const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); + if (this.isStandaloneEligible() && width) { style.height = width / this.props.media.getIn([0, 'meta', 'small', 'aspect']); } else if (width) { style.height = width / (16/9); + } else { + return (<div className={computedClass} ref={this.handleRef}></div>); } if (!visible) { @@ -299,8 +302,6 @@ export default class MediaGallery extends React.PureComponent { } } - const computedClass = classNames('media-gallery', { 'full-width': fullwidth }); - return ( <div className={computedClass} style={style} ref={this.handleRef}> {visible ? ( diff --git a/app/javascript/flavours/glitch/components/scrollable_list.js b/app/javascript/flavours/glitch/components/scrollable_list.js index 3ee710dc9..a05d49829 100644 --- a/app/javascript/flavours/glitch/components/scrollable_list.js +++ b/app/javascript/flavours/glitch/components/scrollable_list.js @@ -8,6 +8,7 @@ import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; import classNames from 'classnames'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from 'flavours/glitch/util/fullscreen'; +import LoadingIndicator from './loading_indicator'; export default class ScrollableList extends PureComponent { @@ -23,8 +24,10 @@ export default class ScrollableList extends PureComponent { trackScroll: PropTypes.bool, shouldUpdateScroll: PropTypes.func, isLoading: PropTypes.bool, + showLoading: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, + alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, children: PropTypes.node, }; @@ -131,12 +134,14 @@ export default class ScrollableList extends PureComponent { getFirstChildKey (props) { const { children } = props; - let firstChild = children; + let firstChild = children; + if (children instanceof ImmutableList) { firstChild = children.get(0); } else if (Array.isArray(children)) { firstChild = children[0]; } + return firstChild && firstChild.key; } @@ -144,7 +149,7 @@ export default class ScrollableList extends PureComponent { this.node = c; } - handleLoadMore = (e) => { + handleLoadMore = e => { e.preventDefault(); this.props.onLoadMore(); } @@ -155,14 +160,26 @@ export default class ScrollableList extends PureComponent { } render () { - const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props; + const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, prepend, alwaysPrepend, emptyMessage, onLoadMore } = this.props; const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null; let scrollableArea = null; - if (isLoading || childrenCount > 0 || !emptyMessage) { + if (showLoading) { + scrollableArea = ( + <div className='scrollable scrollable--flex' ref={this.setRef}> + <div role='feed' className='item-list'> + {prepend} + </div> + + <div className='scrollable__append'> + <LoadingIndicator /> + </div> + </div> + ); + } else if (isLoading || childrenCount > 0 || !emptyMessage) { scrollableArea = ( <div className={classNames('scrollable', { fullscreen })} ref={this.setRef}> <div role='feed' className='item-list'> @@ -187,8 +204,12 @@ export default class ScrollableList extends PureComponent { ); } else { scrollableArea = ( - <div className='empty-column-indicator' ref={this.setRef}> - {emptyMessage} + <div className={classNames('scrollable', { fullscreen })} ref={this.setRef} style={{ flex: '1 1 auto', display: 'flex', flexDirection: 'column' }}> + {alwaysPrepend && prepend} + + <div className='empty-column-indicator'> + {emptyMessage} + </div> </div> ); } diff --git a/app/javascript/flavours/glitch/components/status_action_bar.js b/app/javascript/flavours/glitch/components/status_action_bar.js index f7e741d2d..16abcab4e 100644 --- a/app/javascript/flavours/glitch/components/status_action_bar.js +++ b/app/javascript/flavours/glitch/components/status_action_bar.js @@ -7,6 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me, isStaff } from 'flavours/glitch/util/initial_state'; import RelativeTimestamp from './relative_timestamp'; +import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_links'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -188,10 +189,20 @@ 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) { + if (isStaff && (accountAdminLink || statusAdminLink)) { 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 (accountAdminLink !== undefined) { + menu.push({ + text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), + href: accountAdminLink(status.getIn(['account', 'id'])), + }); + } + if (statusAdminLink !== undefined) { + menu.push({ + text: intl.formatMessage(messages.admin_status), + href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')), + }); + } } } diff --git a/app/javascript/flavours/glitch/components/status_list.js b/app/javascript/flavours/glitch/components/status_list.js index aa902db47..68cd608b9 100644 --- a/app/javascript/flavours/glitch/components/status_list.js +++ b/app/javascript/flavours/glitch/components/status_list.js @@ -23,8 +23,9 @@ export default class StatusList extends ImmutablePureComponent { isPartial: PropTypes.bool, hasMore: PropTypes.bool, prepend: PropTypes.node, + alwaysPrepend: PropTypes.bool, emptyMessage: PropTypes.node, - timelineId: PropTypes.string.isRequired, + timelineId: PropTypes.string, }; static defaultProps = { @@ -121,7 +122,7 @@ export default class StatusList extends ImmutablePureComponent { } return ( - <ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}> + <ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}> {scrollableContent} </ScrollableList> ); 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 3d6eeb06a..ffa5b7e5e 100644 --- a/app/javascript/flavours/glitch/features/account/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/account/components/action_bar.js @@ -5,6 +5,7 @@ import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_cont import { NavLink } from 'react-router-dom'; import { defineMessages, injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; import { me, isStaff } from 'flavours/glitch/util/initial_state'; +import { profileLink, accountAdminLink } from 'flavours/glitch/util/backend_links'; const messages = defineMessages({ mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, @@ -25,6 +26,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' }, + add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, }); @@ -43,6 +45,7 @@ export default class ActionBar extends React.PureComponent { onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, onEndorseToggle: PropTypes.func.isRequired, + onAddToList: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, }; @@ -75,7 +78,9 @@ export default class ActionBar extends React.PureComponent { menu.push(null); if (account.get('id') === me) { - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); + if (profileLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); + } } else { if (account.getIn(['relationship', 'following'])) { if (account.getIn(['relationship', 'showing_reblogs'])) { @@ -85,6 +90,7 @@ export default class ActionBar extends React.PureComponent { } menu.push({ text: intl.formatMessage(account.getIn(['relationship', 'endorsed']) ? messages.unendorse : messages.endorse), action: this.props.onEndorseToggle }); + menu.push({ text: intl.formatMessage(messages.add_or_remove_from_list), action: this.props.onAddToList }); menu.push(null); } @@ -128,9 +134,12 @@ export default class ActionBar extends React.PureComponent { } } - if (account.get('id') !== me && isStaff) { + if (account.get('id') !== me && isStaff && (accountAdminLink !== undefined)) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` }); + menu.push({ + text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), + href: accountAdminLink(account.get('id')), + }); } return ( diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/header.js b/app/javascript/flavours/glitch/features/account_timeline/components/header.js index 89b9be92b..8dc0be93e 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/header.js +++ b/app/javascript/flavours/glitch/features/account_timeline/components/header.js @@ -23,6 +23,7 @@ export default class Header extends ImmutablePureComponent { onBlockDomain: PropTypes.func.isRequired, onUnblockDomain: PropTypes.func.isRequired, onEndorseToggle: PropTypes.func.isRequired, + onAddToList: PropTypes.func.isRequired, hideTabs: PropTypes.bool, }; @@ -78,6 +79,10 @@ export default class Header extends ImmutablePureComponent { this.props.onEndorseToggle(this.props.account); } + handleAddToList = () => { + this.props.onAddToList(this.props.account); + } + render () { const { account, hideTabs } = this.props; @@ -106,6 +111,7 @@ export default class Header extends ImmutablePureComponent { onBlockDomain={this.handleBlockDomain} onUnblockDomain={this.handleUnblockDomain} onEndorseToggle={this.handleEndorseToggle} + onAddToList={this.handleAddToList} /> {!hideTabs && ( diff --git a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js index f5f56d85c..e333c31a1 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js +++ b/app/javascript/flavours/glitch/features/account_timeline/containers/header_container.js @@ -120,6 +120,12 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(unblockDomain(domain)); }, + onAddToList(account){ + dispatch(openModal('LIST_ADDER', { + accountId: account.get('id'), + })); + }, + }); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Header)); diff --git a/app/javascript/flavours/glitch/features/account_timeline/index.js b/app/javascript/flavours/glitch/features/account_timeline/index.js index 2216f9153..6f887a145 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/index.js +++ b/app/javascript/flavours/glitch/features/account_timeline/index.js @@ -11,6 +11,7 @@ import HeaderContainer from './containers/header_container'; import ColumnBackButton from '../../components/column_back_button'; import { List as ImmutableList } from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; const mapStateToProps = (state, { params: { accountId }, withReplies = false }) => { const path = withReplies ? `${accountId}:with_replies` : accountId; @@ -77,12 +78,14 @@ export default class AccountTimeline extends ImmutablePureComponent { <StatusList prepend={<HeaderContainer accountId={this.props.params.accountId} />} + alwaysPrepend scrollKey='account_timeline' statusIds={statusIds} featuredStatusIds={featuredStatusIds} isLoading={isLoading} hasMore={hasMore} onLoadMore={this.handleLoadMore} + emptyMessage={<FormattedMessage id='empty_column.account_timeline' defaultMessage='No toots here!' />} /> </Column> ); diff --git a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js b/app/javascript/flavours/glitch/features/composer/direct_warning/index.js index d1febdd1b..3b1369acd 100644 --- a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js +++ b/app/javascript/flavours/glitch/features/composer/direct_warning/index.js @@ -2,6 +2,7 @@ import React from 'react'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { defineMessages, FormattedMessage } from 'react-intl'; +import { termsLink} from 'flavours/glitch/util/backend_links'; // This is the spring used with our motion. const motionSpring = spring(1, { damping: 35, stiffness: 400 }); @@ -42,7 +43,8 @@ export default function ComposerDirectWarning () { }} > <span> - <FormattedMessage {...messages.disclaimer} /> <a href='/terms' target='_blank'><FormattedMessage {...messages.learn_more} /></a> + <FormattedMessage {...messages.disclaimer} /> + { termsLink !== undefined && <a href={termsLink} target='_blank'><FormattedMessage {...messages.learn_more} /></a> } </span> </div> )} diff --git a/app/javascript/flavours/glitch/features/composer/warning/index.js b/app/javascript/flavours/glitch/features/composer/warning/index.js index c225b50e8..8be8acbce 100644 --- a/app/javascript/flavours/glitch/features/composer/warning/index.js +++ b/app/javascript/flavours/glitch/features/composer/warning/index.js @@ -2,6 +2,7 @@ import React from 'react'; import Motion from 'flavours/glitch/util/optional_motion'; import spring from 'react-motion/lib/spring'; import { defineMessages, FormattedMessage } from 'react-intl'; +import { profileLink } from 'flavours/glitch/util/backend_links'; // This is the spring used with our motion. const motionSpring = spring(1, { damping: 35, stiffness: 400 }); @@ -20,6 +21,10 @@ const messages = defineMessages({ // The component. export default function ComposerWarning () { + let lockedLink = <FormattedMessage {...messages.locked} />; + if (profileLink !== undefined) { + lockedLink = <a href={profileLink}>{lockedLink}</a>; + } return ( <Motion defaultStyle={{ @@ -43,7 +48,7 @@ export default function ComposerWarning () { > <FormattedMessage {...messages.disclaimer} - values={{ locked: <a href='/settings/profile'><FormattedMessage {...messages.locked} /></a> }} + values={{ locked: lockedLink }} /> </div> )} diff --git a/app/javascript/flavours/glitch/features/drawer/account/index.js b/app/javascript/flavours/glitch/features/drawer/account/index.js index 168d0c2cf..552848641 100644 --- a/app/javascript/flavours/glitch/features/drawer/account/index.js +++ b/app/javascript/flavours/glitch/features/drawer/account/index.js @@ -12,6 +12,7 @@ import Permalink from 'flavours/glitch/components/permalink'; // Utils. import { hiddenComponent } from 'flavours/glitch/util/react_helpers'; +import { profileLink } from 'flavours/glitch/util/backend_links'; // Messages. const messages = defineMessages({ @@ -28,12 +29,14 @@ export default function DrawerAccount ({ account }) { if (!account) { return ( <div className='drawer--account'> - <a - className='edit' - href='/settings/profile' - > - <FormattedMessage {...messages.edit} /> - </a> + { profileLink !== undefined && ( + <a + className='edit' + href={ profileLink } + > + <FormattedMessage {...messages.edit} /> + </a> + )} </div> ); } @@ -59,10 +62,12 @@ export default function DrawerAccount ({ account }) { > <strong>@{account.get('acct')}</strong> </Permalink> - <a - className='edit' - href='/settings/profile' - ><FormattedMessage {...messages.edit} /></a> + { profileLink !== undefined && ( + <a + className='edit' + href={ profileLink } + ><FormattedMessage {...messages.edit} /></a> + )} </div> ); } diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js index 7fefd32c9..da5599732 100644 --- a/app/javascript/flavours/glitch/features/drawer/header/index.js +++ b/app/javascript/flavours/glitch/features/drawer/header/index.js @@ -10,6 +10,7 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { conditionalRender } from 'flavours/glitch/util/react_helpers'; +import { signOutLink } from 'flavours/glitch/util/backend_links'; // Messages. const messages = defineMessages({ @@ -109,7 +110,7 @@ export default function DrawerHeader ({ <a aria-label={intl.formatMessage(messages.logout)} data-method='delete' - href='/auth/sign_out' + href={ signOutLink } title={intl.formatMessage(messages.logout)} ><Icon icon='sign-out' /></a> </nav> diff --git a/app/javascript/flavours/glitch/features/getting_started/index.js b/app/javascript/flavours/glitch/features/getting_started/index.js index ce1ae50e4..c87f76c5e 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.js +++ b/app/javascript/flavours/glitch/features/getting_started/index.js @@ -13,6 +13,7 @@ import { fetchFollowRequests } from 'flavours/glitch/actions/accounts'; import { List as ImmutableList } from 'immutable'; import { createSelector } from 'reselect'; import { fetchLists } from 'flavours/glitch/actions/lists'; +import { preferencesLink, profileLink, signOutLink } from 'flavours/glitch/util/backend_links'; const messages = defineMessages({ heading: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, @@ -157,9 +158,9 @@ export default class GettingStarted extends ImmutablePureComponent { <ColumnSubheading text={intl.formatMessage(messages.lists_subheading)} /> {listItems} <ColumnSubheading text={intl.formatMessage(messages.settings_subheading)} /> - <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href='/settings/preferences' /> + { preferencesLink !== undefined && <ColumnLink icon='cog' text={intl.formatMessage(messages.preferences)} href={preferencesLink} /> } <ColumnLink icon='cogs' text={intl.formatMessage(messages.settings)} onClick={openSettings} /> - <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href='/auth/sign_out' method='delete' /> + <ColumnLink icon='sign-out' text={intl.formatMessage(messages.sign_out)} href={signOutLink} method='delete' /> </div> <div className='getting-started__footer'> diff --git a/app/javascript/flavours/glitch/features/list_adder/components/account.js b/app/javascript/flavours/glitch/features/list_adder/components/account.js new file mode 100644 index 000000000..1369aac07 --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_adder/components/account.js @@ -0,0 +1,43 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { makeGetAccount } from '../../../selectors'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import Avatar from '../../../components/avatar'; +import DisplayName from '../../../components/display_name'; +import { injectIntl } from 'react-intl'; + +const makeMapStateToProps = () => { + const getAccount = makeGetAccount(); + + const mapStateToProps = (state, { accountId }) => ({ + account: getAccount(state, accountId), + }); + + return mapStateToProps; +}; + + +export default @connect(makeMapStateToProps) +@injectIntl +class Account extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account } = this.props; + return ( + <div className='account'> + <div className='account__wrapper'> + <div className='account__display-name'> + <div className='account__avatar-wrapper'><Avatar account={account} size={36} /></div> + <DisplayName account={account} /> + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/list_adder/components/list.js b/app/javascript/flavours/glitch/features/list_adder/components/list.js new file mode 100644 index 000000000..cb8eb7d7a --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_adder/components/list.js @@ -0,0 +1,68 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import IconButton from '../../../components/icon_button'; +import { defineMessages, injectIntl } from 'react-intl'; +import { removeFromListAdder, addToListAdder } from '../../../actions/lists'; + +const messages = defineMessages({ + remove: { id: 'lists.account.remove', defaultMessage: 'Remove from list' }, + add: { id: 'lists.account.add', defaultMessage: 'Add to list' }, +}); + +const MapStateToProps = (state, { listId, added }) => ({ + list: state.get('lists').get(listId), + added: typeof added === 'undefined' ? state.getIn(['listAdder', 'lists', 'items']).includes(listId) : added, +}); + +const mapDispatchToProps = (dispatch, { listId }) => ({ + onRemove: () => dispatch(removeFromListAdder(listId)), + onAdd: () => dispatch(addToListAdder(listId)), +}); + +export default @connect(MapStateToProps, mapDispatchToProps) +@injectIntl +class List extends ImmutablePureComponent { + + static propTypes = { + list: ImmutablePropTypes.map.isRequired, + intl: PropTypes.object.isRequired, + onRemove: PropTypes.func.isRequired, + onAdd: PropTypes.func.isRequired, + added: PropTypes.bool, + }; + + static defaultProps = { + added: false, + }; + + render () { + const { list, intl, onRemove, onAdd, added } = this.props; + + let button; + + if (added) { + button = <IconButton icon='times' title={intl.formatMessage(messages.remove)} onClick={onRemove} />; + } else { + button = <IconButton icon='plus' title={intl.formatMessage(messages.add)} onClick={onAdd} />; + } + + return ( + <div className='list'> + <div className='list__wrapper'> + <div className='list__display-name'> + <i className='fa fa-fw fa-list-ul column-link__icon' /> + {list.get('title')} + </div> + + <div className='account__relationship'> + {button} + </div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/list_adder/index.js b/app/javascript/flavours/glitch/features/list_adder/index.js new file mode 100644 index 000000000..cb8a15e8c --- /dev/null +++ b/app/javascript/flavours/glitch/features/list_adder/index.js @@ -0,0 +1,73 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { injectIntl } from 'react-intl'; +import { setupListAdder, resetListAdder } from '../../actions/lists'; +import { createSelector } from 'reselect'; +import List from './components/list'; +import Account from './components/account'; +import NewListForm from '../lists/components/new_list_form'; +// hack + +const getOrderedLists = createSelector([state => state.get('lists')], lists => { + if (!lists) { + return lists; + } + + return lists.toList().filter(item => !!item).sort((a, b) => a.get('title').localeCompare(b.get('title'))); +}); + +const mapStateToProps = state => ({ + listIds: getOrderedLists(state).map(list=>list.get('id')), +}); + +const mapDispatchToProps = dispatch => ({ + onInitialize: accountId => dispatch(setupListAdder(accountId)), + onReset: () => dispatch(resetListAdder()), +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class ListAdder extends ImmutablePureComponent { + + static propTypes = { + accountId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + onInitialize: PropTypes.func.isRequired, + onReset: PropTypes.func.isRequired, + listIds: ImmutablePropTypes.list.isRequired, + }; + + componentDidMount () { + const { onInitialize, accountId } = this.props; + onInitialize(accountId); + } + + componentWillUnmount () { + const { onReset } = this.props; + onReset(); + } + + render () { + const { accountId, listIds } = this.props; + + return ( + <div className='modal-root__modal list-adder'> + <div className='list-adder__account'> + <Account accountId={accountId} /> + </div> + + <NewListForm /> + + + <div className='list-adder__lists'> + {listIds.map(ListId => <List key={ListId} listId={ListId} />)} + </div> + </div> + ); + } + +} 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 cf02101cf..ce10e3f51 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js @@ -5,6 +5,7 @@ import { injectIntl, defineMessages } from 'react-intl'; // Our imports import LocalSettingsNavigationItem from './item'; +import { preferencesLink } from 'flavours/glitch/util/backend_links'; // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * @@ -71,7 +72,7 @@ export default class LocalSettingsNavigation extends React.PureComponent { /> <LocalSettingsNavigationItem active={index === 5} - href='/settings/preferences' + href={ preferencesLink } index={5} icon='sliders' title={intl.formatMessage(messages.preferences)} diff --git a/app/javascript/flavours/glitch/features/notifications/index.js b/app/javascript/flavours/glitch/features/notifications/index.js index 13ed26865..0e73f02d8 100644 --- a/app/javascript/flavours/glitch/features/notifications/index.js +++ b/app/javascript/flavours/glitch/features/notifications/index.js @@ -187,6 +187,7 @@ export default class Notifications extends React.PureComponent { scrollKey={`notifications-${columnId}`} trackScroll={!pinned} isLoading={isLoading} + showLoading={isLoading && notifications.size === 0} hasMore={hasMore} emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} 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 009aa49eb..be82bca5b 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -5,6 +5,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import { defineMessages, injectIntl } from 'react-intl'; import { me, isStaff } from 'flavours/glitch/util/initial_state'; +import { accountAdminLink, statusAdminLink } from 'flavours/glitch/util/backend_links'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -148,10 +149,20 @@ 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) { + if (isStaff && (accountAdminLink || statusAdminLink)) { 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 (accountAdminLink !== undefined) { + menu.push({ + text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), + href: accountAdminLink(status.getIn(['account', 'id'])), + }); + } + if (statusAdminLink !== undefined) { + menu.push({ + text: intl.formatMessage(messages.admin_status), + href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')), + }); + } } } 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 1fe0c069d..436c2df0a 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -65,6 +65,7 @@ export default class DetailedStatus extends ImmutablePureComponent { sensitive={status.get('sensitive')} letterbox={settings.getIn(['media', 'letterbox'])} fullwidth={settings.getIn(['media', 'fullwidth'])} + preventPlayback={!expanded} onOpenVideo={this.handleOpenVideo} autoplay /> @@ -78,6 +79,7 @@ export default class DetailedStatus extends ImmutablePureComponent { media={status.get('media_attachments')} letterbox={settings.getIn(['media', 'letterbox'])} fullwidth={settings.getIn(['media', 'fullwidth'])} + hidden={!expanded} onOpenMedia={this.props.onOpenMedia} /> ); diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index c9f54804a..303e05db6 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -19,6 +19,7 @@ import { SettingsModal, EmbedModal, ListEditor, + ListAdder, PinnedAccountsEditor, } from 'flavours/glitch/util/async-components'; @@ -36,6 +37,7 @@ const MODAL_COMPONENTS = { 'ACTIONS': () => Promise.resolve({ default: ActionsModal }), 'EMBED': EmbedModal, 'LIST_EDITOR': ListEditor, + 'LIST_ADDER':ListAdder, 'FOCAL_POINT': () => Promise.resolve({ default: FocalPointModal }), 'PINNED_ACCOUNTS_EDITOR': PinnedAccountsEditor, }; diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index 4f95aea96..4c2e5e62b 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -120,7 +120,7 @@ export default class Video extends React.PureComponent { setPlayerRef = c => { this.player = c; - if (c) { + if (c && c.offsetWidth && c.offsetWidth != this.state.containerWidth) { this.setState({ containerWidth: c.offsetWidth, }); @@ -220,7 +220,7 @@ export default class Video extends React.PureComponent { } componentDidUpdate (prevProps) { - if (this.player && this.player.offsetWidth && !this.state.fullscreen) { + if (this.player && this.player.offsetWidth && this.player.offsetWidth != this.state.containerWidth && !this.state.fullscreen) { this.setState({ containerWidth: this.player.offsetWidth, }); @@ -294,6 +294,8 @@ export default class Video extends React.PureComponent { const progress = (currentTime / duration) * 100; const playerStyle = {}; + const computedClass = classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, letterbox, 'full-width': fullwidth }); + let { width, height } = this.props; if (inline && containerWidth) { @@ -302,6 +304,8 @@ export default class Video extends React.PureComponent { playerStyle.width = width; playerStyle.height = height; + } else if (inline) { + return (<div className={computedClass} ref={this.setPlayerRef} tabindex={0}></div>); } let warning; @@ -322,7 +326,7 @@ export default class Video extends React.PureComponent { return ( <div - className={classNames('video-player', { inactive: !revealed, detailed, inline: inline && !fullscreen, fullscreen, letterbox, 'full-width': fullwidth })} + className={computedClass} style={playerStyle} ref={this.setPlayerRef} onMouseEnter={this.handleMouseEnter} diff --git a/app/javascript/flavours/glitch/reducers/index.js b/app/javascript/flavours/glitch/reducers/index.js index ae9e2b639..5b1ec4abc 100644 --- a/app/javascript/flavours/glitch/reducers/index.js +++ b/app/javascript/flavours/glitch/reducers/index.js @@ -26,6 +26,7 @@ import height_cache from './height_cache'; import custom_emojis from './custom_emojis'; import lists from './lists'; import listEditor from './list_editor'; +import listAdder from './list_adder'; import filters from './filters'; import pinnedAccountsEditor from './pinned_accounts_editor'; @@ -57,6 +58,7 @@ const reducers = { custom_emojis, lists, listEditor, + listAdder, filters, pinnedAccountsEditor, }; diff --git a/app/javascript/flavours/glitch/reducers/list_adder.js b/app/javascript/flavours/glitch/reducers/list_adder.js new file mode 100644 index 000000000..b8c1b0e26 --- /dev/null +++ b/app/javascript/flavours/glitch/reducers/list_adder.js @@ -0,0 +1,47 @@ +import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; +import { + LIST_ADDER_RESET, + LIST_ADDER_SETUP, + LIST_ADDER_LISTS_FETCH_REQUEST, + LIST_ADDER_LISTS_FETCH_SUCCESS, + LIST_ADDER_LISTS_FETCH_FAIL, + LIST_EDITOR_ADD_SUCCESS, + LIST_EDITOR_REMOVE_SUCCESS, +} from '../actions/lists'; + +const initialState = ImmutableMap({ + accountId: null, + + lists: ImmutableMap({ + items: ImmutableList(), + loaded: false, + isLoading: false, + }), +}); + +export default function listAdderReducer(state = initialState, action) { + switch(action.type) { + case LIST_ADDER_RESET: + return initialState; + case LIST_ADDER_SETUP: + return state.withMutations(map => { + map.set('accountId', action.account.get('id')); + }); + case LIST_ADDER_LISTS_FETCH_REQUEST: + return state.setIn(['lists', 'isLoading'], true); + case LIST_ADDER_LISTS_FETCH_FAIL: + return state.setIn(['lists', 'isLoading'], false); + case LIST_ADDER_LISTS_FETCH_SUCCESS: + return state.update('lists', lists => lists.withMutations(map => { + map.set('isLoading', false); + map.set('loaded', true); + map.set('items', ImmutableList(action.lists.map(item => item.id))); + })); + case LIST_EDITOR_ADD_SUCCESS: + return state.updateIn(['lists', 'items'], list => list.unshift(action.listId)); + case LIST_EDITOR_REMOVE_SUCCESS: + return state.updateIn(['lists', 'items'], list => list.filterNot(item => item === action.listId)); + default: + return state; + } +}; diff --git a/app/javascript/flavours/glitch/reducers/statuses.js b/app/javascript/flavours/glitch/reducers/statuses.js index 617d96e5d..1beaf73e1 100644 --- a/app/javascript/flavours/glitch/reducers/statuses.js +++ b/app/javascript/flavours/glitch/reducers/statuses.js @@ -118,15 +118,15 @@ export default function statuses(state = initialState, action) { case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); case FAVOURITE_FAIL: - return state.setIn([action.status.get('id'), 'favourited'], false); + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'favourited'], false); case BOOKMARK_REQUEST: return state.setIn([action.status.get('id'), 'bookmarked'], true); case BOOKMARK_FAIL: - return state.setIn([action.status.get('id'), 'bookmarked'], false); + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'bookmarked'], false); case REBLOG_REQUEST: return state.setIn([action.status.get('id'), 'reblogged'], true); case REBLOG_FAIL: - return state.setIn([action.status.get('id'), 'reblogged'], false); + return state.get(action.status.get('id')) === undefined ? state : state.setIn([action.status.get('id'), 'reblogged'], false); case STATUS_MUTE_SUCCESS: return state.setIn([action.id, 'muted'], true); case STATUS_UNMUTE_SUCCESS: diff --git a/app/javascript/flavours/glitch/reducers/timelines.js b/app/javascript/flavours/glitch/reducers/timelines.js index 19e400b19..844a0580f 100644 --- a/app/javascript/flavours/glitch/reducers/timelines.js +++ b/app/javascript/flavours/glitch/reducers/timelines.js @@ -49,7 +49,7 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial) => })); }; -const updateTimeline = (state, timeline, status, references) => { +const updateTimeline = (state, timeline, status) => { const top = state.getIn([timeline, 'top']); const ids = state.getIn([timeline, 'items'], ImmutableList()); const includesId = ids.includes(status.get('id')); @@ -64,7 +64,6 @@ const updateTimeline = (state, timeline, status, references) => { return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { if (!top) mMap.set('unread', unread + 1); if (top && ids.size > 40) newIds = newIds.take(20); - if (status.getIn(['reblog', 'id'], null) !== null) newIds = newIds.filterNot(item => references.includes(item)); mMap.set('items', newIds.unshift(status.get('id'))); })); }; @@ -119,7 +118,7 @@ export default function timelines(state = initialState, action) { case TIMELINE_EXPAND_SUCCESS: return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial); case TIMELINE_UPDATE: - return updateTimeline(state, action.timeline, fromJS(action.status), action.references); + return updateTimeline(state, action.timeline, fromJS(action.status)); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); case ACCOUNT_BLOCK_SUCCESS: diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 6a9af4490..a5c9d0130 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -43,8 +43,7 @@ position: relative; flex-direction: column; padding: 0; - width: 100%; - height: 100%; + flex-grow: 1; background: lighten($ui-base-color, 13%); overflow-x: hidden; overflow-y: auto; @@ -327,7 +326,7 @@ position: absolute; top: 0; left: 0; - background: lighten($ui-base-color, 13%) 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($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto; + background: lighten($ui-base-color, 13%); box-sizing: border-box; padding: 0; display: flex; @@ -340,11 +339,6 @@ &.darker { background: $ui-base-color; } - - > .mastodon { - background: url('~images/elephant_ui_plane.svg') no-repeat left bottom / contain; - flex: 1; - } } .pseudo-drawer { diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 7d71f0d1d..b16b13d87 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -597,6 +597,16 @@ @supports(display: grid) { // hack to fix Chrome <57 contain: strict; } + + &--flex { + display: flex; + flex-direction: column; + } + + &__append { + flex: 1 1 auto; + position: relative; + } } .scrollable.fullscreen { diff --git a/app/javascript/flavours/glitch/styles/components/lists.scss b/app/javascript/flavours/glitch/styles/components/lists.scss index f5837c6c4..d00a1941b 100644 --- a/app/javascript/flavours/glitch/styles/components/lists.scss +++ b/app/javascript/flavours/glitch/styles/components/lists.scss @@ -51,3 +51,44 @@ margin-bottom: 0; } } + +.list-adder { + background: $ui-base-color; + flex-direction: column; + border-radius: 8px; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + width: 380px; + overflow: hidden; + + @media screen and (max-width: 420px) { + width: 90%; + } + + &__account { + background: lighten($ui-base-color, 13%); + } + + &__lists { + background: lighten($ui-base-color, 13%); + height: 50vh; + border-radius: 0 0 8px 8px; + overflow-y: auto; + } + + .list { + padding: 10px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + + .list__wrapper { + display: flex; + } + + .list__display-name { + flex: 1 1 auto; + overflow: hidden; + text-decoration: none; + font-size: 16px; + padding: 10px; + } +} diff --git a/app/javascript/flavours/glitch/styles/forms.scss b/app/javascript/flavours/glitch/styles/forms.scss index 8c4c934ea..46ef85774 100644 --- a/app/javascript/flavours/glitch/styles/forms.scss +++ b/app/javascript/flavours/glitch/styles/forms.scss @@ -330,9 +330,12 @@ code { } input[type=text], + input[type=number], input[type=email], - input[type=password] { - border-bottom-color: $valid-value-color; + input[type=password], + textarea, + select { + border-color: lighten($error-red, 12%); } .error { diff --git a/app/javascript/flavours/glitch/styles/reset.scss b/app/javascript/flavours/glitch/styles/reset.scss index cc5ba9d7c..e24ba8c1c 100644 --- a/app/javascript/flavours/glitch/styles/reset.scss +++ b/app/javascript/flavours/glitch/styles/reset.scss @@ -53,9 +53,13 @@ table { border-spacing: 0; } +html { + scrollbar-color: lighten($ui-base-color, 4%) transparent; +} + ::-webkit-scrollbar { - width: 8px; - height: 8px; + width: 12px; + height: 12px; } ::-webkit-scrollbar-thumb { diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index 557ce317e..e96af845f 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -145,3 +145,7 @@ export function EmbedModal () { export function GettingStartedMisc () { return import(/* webpackChunkName: "flavours/glitch/async/getting_started_misc" */'flavours/glitch/features/getting_started_misc'); } + +export function ListAdder () { + return import(/* webpackChunkName: "features/glitch/async/list_adder" */'flavours/glitch/features/list_adder'); +} diff --git a/app/javascript/flavours/glitch/util/backend_links.js b/app/javascript/flavours/glitch/util/backend_links.js new file mode 100644 index 000000000..4fc03f919 --- /dev/null +++ b/app/javascript/flavours/glitch/util/backend_links.js @@ -0,0 +1,6 @@ +export const preferencesLink = '/settings/preferences'; +export const profileLink = '/settings/profile'; +export const signOutLink = '/auth/sign_out'; +export const termsLink = '/terms'; +export const accountAdminLink = (id) => `/admin/accounts/${id}`; +export const statusAdminLink = (account_id, status_id) => `/admin/accounts/${account_id}/statuses/${status_id}`; |