Commit c56a8914 authored by 2458773093's avatar 2458773093

Merge remote-tracking branch 'origin/styling/add-groups-link' into groups-updates

* origin/styling/add-groups-link: (31 commits)
  Comment out the "groups" button until ready to reveal.
  Changed the method of adding main navigation icons. Created a png sprite sized @2x based on largest usage (for retina). This will fix some rendering issues caused by using svg images. It will allow adding depth and more color / shading if we choose later.
  intents fix
  federation fix
  two more federation fixes
  Removed unused imports
  Removed unused PublicTimeline component
  Updated CommunityTimeline to add option for "all federated" content
  Removed unused import in unauthorized_modal
  Updated registration legal links
  Updated compose_form to account for if compose modal open
  Added empty message to pinned statuses page
  Updated nextProps withReplies for account timeline
  Added empty message to account_gallery media page
  Updated timeline/notification dequeue to be in componentDidMount
  Added TimelineQueueButtonHeader to status_list
  Added queue functionality status_list_container for status timelines
  Updated all Redis.current.publish, PushUpdateWorker.perform_async to work again
  Added timeline dequeue functionality to onSubmitCompose action
  Added redux functionality for queueing/dequeueing timelines
  ...
parents b1cd12c9 062353cf
......@@ -5,7 +5,7 @@ class IntentsController < ApplicationController
rescue_from Addressable::URI::InvalidURIError, with: :handle_invalid_uri
def show
if uri.scheme == 'web+gabsocial'
if uri.scheme == 'web+mastodon'
case uri.host
when 'follow'
return redirect_to authorize_interaction_path(uri: uri.query_values['uri'].gsub(/\Aacct:/, ''))
......
import { saveSettings } from './settings';
export const COLUMN_PARAMS_CHANGE = 'COLUMN_PARAMS_CHANGE';
export function changeColumnParams(uuid, path, value) {
return dispatch => {
dispatch({
type: COLUMN_PARAMS_CHANGE,
uuid,
path,
value,
});
dispatch(saveSettings());
};
}
......@@ -6,7 +6,7 @@ import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
import resizeImage from '../utils/resize_image';
import { importFetchedAccounts } from './importer';
import { updateTimeline } from './timelines';
import { updateTimeline, dequeueTimeline } from './timelines';
import { showAlertForError } from './alerts';
import { showAlert } from './alerts';
import { defineMessages } from 'react-intl';
......@@ -169,6 +169,10 @@ export function submitCompose(routerHistory, group) {
const timeline = getState().getIn(['timelines', timelineId]);
if (timeline && timeline.get('items').size > 0 && timeline.getIn(['items', 0]) !== null && timeline.get('online')) {
let dequeueArgs = {};
if (timelineId === 'community') dequeueArgs.onlyMedia = getState().getIn(['settings', 'community', 'other', 'onlyMedia']),
dispatch(dequeueTimeline(timelineId, null, dequeueArgs));
dispatch(updateTimeline(timelineId, { ...response.data }));
}
};
......
......@@ -16,6 +16,8 @@ import { me } from 'gabsocial/initial_state';
export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE';
export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP';
export const NOTIFICATIONS_UPDATE_QUEUE = 'NOTIFICATIONS_UPDATE_QUEUE';
export const NOTIFICATIONS_DEQUEUE = 'NOTIFICATIONS_DEQUEUE';
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
......@@ -26,6 +28,8 @@ export const NOTIFICATIONS_FILTER_SET = 'NOTIFICATIONS_FILTER_SET';
export const NOTIFICATIONS_CLEAR = 'NOTIFICATIONS_CLEAR';
export const NOTIFICATIONS_SCROLL_TOP = 'NOTIFICATIONS_SCROLL_TOP';
export const MAX_QUEUED_NOTIFICATIONS = 40;
defineMessages({
mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' },
group: { id: 'notifications.group', defaultMessage: '{count} notifications' },
......@@ -42,18 +46,6 @@ const fetchRelatedRelationships = (dispatch, notifications) => {
export function updateNotifications(notification, intlMessages, intlLocale) {
return (dispatch, getState) => {
const showInColumn = getState().getIn(['settings', 'notifications', 'shows', notification.type], true);
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
const filters = getFilters(getState(), { contextType: 'notifications' });
let filtered = false;
if (notification.type === 'mention') {
const regex = regexFromFilters(filters);
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
filtered = regex && regex.test(searchIndex);
}
if (showInColumn) {
dispatch(importFetchedAccount(notification.account));
......@@ -65,21 +57,33 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
dispatch({
type: NOTIFICATIONS_UPDATE,
notification,
meta: (playSound && !filtered) ? { sound: 'ribbit' } : undefined,
});
fetchRelatedRelationships(dispatch, [notification]);
} else if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'ribbit' },
});
}
};
};
export function updateNotificationsQueue(notification, intlMessages, intlLocale, curPath) {
return (dispatch, getState) => {
const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true);
const filters = getFilters(getState(), { contextType: 'notifications' });
const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true);
let filtered = false;
const isOnNotificationsPage = curPath === '/notifications';
if (notification.type === 'mention') {
const regex = regexFromFilters(filters);
const searchIndex = notification.status.spoiler_text + '\n' + unescapeHTML(notification.status.content);
filtered = regex && regex.test(searchIndex);
}
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert && !filtered) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const notify = new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
......@@ -88,7 +92,49 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
notify.close();
});
}
};
if (playSound && !filtered) {
dispatch({
type: NOTIFICATIONS_UPDATE_NOOP,
meta: { sound: 'ribbit' },
});
}
if (isOnNotificationsPage) {
dispatch({
type: NOTIFICATIONS_UPDATE_QUEUE,
notification,
intlMessages,
intlLocale,
});
}
else {
dispatch(updateNotifications(notification, intlMessages, intlLocale));
}
}
};
export function dequeueNotifications() {
return (dispatch, getState) => {
const queuedNotifications = getState().getIn(['notifications', 'queuedNotifications'], ImmutableList());
const totalQueuedNotificationsCount = getState().getIn(['notifications', 'totalQueuedNotificationsCount'], 0);
if (totalQueuedNotificationsCount == 0) {
return;
}
else if (totalQueuedNotificationsCount > 0 && totalQueuedNotificationsCount <= MAX_QUEUED_NOTIFICATIONS) {
queuedNotifications.forEach(block => {
dispatch(updateNotifications(block.notification, block.intlMessages, block.intlLocale));
});
}
else {
dispatch(expandNotifications());
}
dispatch({
type: NOTIFICATIONS_DEQUEUE,
});
}
};
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
......@@ -169,7 +215,7 @@ export function expandNotificationsFail(error, isLoadingMore) {
export function clearNotifications() {
return (dispatch, getState) => {
if (!me) return;
dispatch({
type: NOTIFICATIONS_CLEAR,
});
......
import { connectStream } from '../stream';
import {
updateTimeline,
deleteFromTimelines,
expandHomeTimeline,
connectTimeline,
disconnectTimeline,
updateTimelineQueue,
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateNotificationsQueue, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
import { fetchFilters } from './filters';
import { getLocale } from '../locales';
......@@ -30,13 +30,13 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
onReceive (data) {
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), accept));
dispatch(updateTimelineQueue(timelineId, JSON.parse(data.payload), accept));
break;
case 'delete':
dispatch(deleteFromTimelines(data.payload));
break;
case 'notification':
dispatch(updateNotifications(JSON.parse(data.payload), messages, locale));
dispatch(updateNotificationsQueue(JSON.parse(data.payload), messages, locale, window.location.pathname));
break;
case 'conversation':
dispatch(updateConversations(JSON.parse(data.payload)));
......
import { importFetchedStatus, importFetchedStatuses } from './importer';
import api, { getLinks } from '../api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import { Map as ImmutableMap, List as ImmutableList, toJS } from 'immutable';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
export const TIMELINE_UPDATE_QUEUE = 'TIMELINE_UPDATE_QUEUE';
export const TIMELINE_DEQUEUE = 'TIMELINE_DEQUEUE';
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
......@@ -13,6 +15,8 @@ export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const MAX_QUEUED_ITEMS = 40;
export function updateTimeline(timeline, status, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
......@@ -29,6 +33,64 @@ export function updateTimeline(timeline, status, accept) {
};
};
export function updateTimelineQueue(timeline, status, accept) {
return dispatch => {
if (typeof accept === 'function' && !accept(status)) {
return;
}
dispatch({
type: TIMELINE_UPDATE_QUEUE,
timeline,
status,
});
}
};
export function dequeueTimeline(timeline, expandFunc, optionalExpandArgs) {
return (dispatch, getState) => {
const queuedItems = getState().getIn(['timelines', timeline, 'queuedItems'], ImmutableList());
const totalQueuedItemsCount = getState().getIn(['timelines', timeline, 'totalQueuedItemsCount'], 0);
let shouldDispatchDequeue = true;
if (totalQueuedItemsCount == 0) {
return;
}
else if (totalQueuedItemsCount > 0 && totalQueuedItemsCount <= MAX_QUEUED_ITEMS) {
queuedItems.forEach(status => {
dispatch(updateTimeline(timeline, status.toJS(), null));
});
}
else {
if (typeof expandFunc === 'function') {
dispatch(clearTimeline(timeline));
expandFunc();
}
else {
if (timeline === 'home') {
dispatch(clearTimeline(timeline));
dispatch(expandHomeTimeline(optionalExpandArgs));
}
else if (timeline === 'community') {
dispatch(clearTimeline(timeline));
dispatch(expandCommunityTimeline(optionalExpandArgs));
}
else {
shouldDispatchDequeue = false;
}
}
}
if (!shouldDispatchDequeue) return;
dispatch({
type: TIMELINE_DEQUEUE,
timeline,
});
}
};
export function deleteFromTimelines(id) {
return (dispatch, getState) => {
const accountId = getState().getIn(['statuses', id, 'account']);
......
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
......
......@@ -7,6 +7,7 @@ import StatusContainer from '../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import LoadGap from './load_gap';
import ScrollableList from './scrollable_list';
import TimelineQueueButtonHeader from './timeline_queue_button_header';
export default class StatusList extends ImmutablePureComponent {
......@@ -22,6 +23,12 @@ export default class StatusList extends ImmutablePureComponent {
emptyMessage: PropTypes.node,
alwaysPrepend: PropTypes.bool,
timelineId: PropTypes.string,
queuedItemSize: PropTypes.number,
onDequeueTimeline: PropTypes.func,
};
componentDidMount() {
this.handleDequeueTimeline();
};
getFeaturedStatusCount = () => {
......@@ -64,13 +71,18 @@ export default class StatusList extends ImmutablePureComponent {
}
}
handleDequeueTimeline = () => {
const { onDequeueTimeline, timelineId } = this.props;
if (!onDequeueTimeline || !timelineId) return;
onDequeueTimeline(timelineId);
}
setRef = c => {
this.node = c;
}
render () {
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other;
const { statusIds, featuredStatusIds, onLoadMore, timelineId, totalQueuedItemsCount, isLoading, isPartial, ...other } = this.props;
if (isPartial) {
return (
......@@ -119,11 +131,12 @@ export default class StatusList extends ImmutablePureComponent {
)).concat(scrollableContent);
}
return (
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
return [
<TimelineQueueButtonHeader key='timeline-queue-button-header' onClick={this.handleDequeueTimeline} count={totalQueuedItemsCount} itemType='gab' />,
<ScrollableList key='scrollable-list' {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
{scrollableContent}
</ScrollableList>
);
];
}
}
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { shortNumberFormat } from '../utils/numbers';
export default class TimelineQueueButtonHeader extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func.isRequired,
count: PropTypes.number,
itemType: PropTypes.string,
};
static defaultProps = {
count: 0,
itemType: 'item',
};
render () {
const { count, itemType, onClick } = this.props;
if (count <= 0) return null;
return (
<div className='timeline-queue-header'>
<a className='timeline-queue-header__btn' onClick={onClick}>
<FormattedMessage
id='timeline_queue.label'
defaultMessage='Click to see {count} new {type}'
values={{
count: shortNumberFormat(count),
type: count == 1 ? itemType : `${itemType}s`,
}}
/>
</a>
</div>
);
}
}
......@@ -24,7 +24,7 @@ const messages = defineMessages({
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
linkVerifiedOn: { id: 'account.link_verified_on', defaultMessage: 'Ownership of this link was checked on {date}' },
account_locked: { id: 'account.locked_info', defaultMessage: 'This account privacy status is set to locked. The owner manually reviews who can follow them.' },
mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' },
mention: { id: 'account.mention', defaultMessage: 'Mention' },
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
......@@ -311,7 +311,7 @@ class Header extends ImmutablePureComponent {
{actionBtn}
{account.get('id') !== me &&
<Button className='button button-alternative-2' onClick={this.props.onMention}>
<FormattedMessage id='account.mention' defaultMessage='Mention @{name}' values={{
<FormattedMessage id='account.mention' defaultMessage='Mention' values={{
name: account.get('acct')
}} />
</Button>
......
......@@ -212,6 +212,13 @@ class AccountGallery extends ImmutablePureComponent {
<MediaItem key={attachment.get('id')} attachment={attachment} displayWidth={width} onOpenMedia={this.handleOpenMedia} />
))}
{
attachments.size == 0 &&
<div className='empty-column-indicator'>
<FormattedMessage id='account_gallery.none' defaultMessage='No media to show.'/>
</div>
}
{loadOlder}
</div>
......
......@@ -93,7 +93,7 @@ class AccountTimeline extends ImmutablePureComponent {
this.props.dispatch(expandAccountFeaturedTimeline(nextProps.accountId));
}
this.props.dispatch(expandAccountTimeline(nextProps.accountId, { withReplies: nextProps.params.withReplies }));
this.props.dispatch(expandAccountTimeline(nextProps.accountId, { withReplies: nextProps.withReplies }));
}
}
......
......@@ -11,7 +11,6 @@ class ColumnSettings extends React.PureComponent {
settings: ImmutablePropTypes.map.isRequired,
onChange: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
};
render () {
......@@ -21,6 +20,7 @@ class ColumnSettings extends React.PureComponent {
<div>
<div className='column-settings__row'>
<SettingToggle settings={settings} settingPath={['other', 'onlyMedia']} onChange={onChange} label={<FormattedMessage id='community.column_settings.media_only' defaultMessage='Media Only' />} />
<SettingToggle settings={settings} settingPath={['other', 'allFediverse']} onChange={onChange} label={<FormattedMessage id='community.column_settings.all_fediverse' defaultMessage='All Fediverse' />} />
</div>
</div>
);
......
import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from '../../../actions/settings';
import { changeColumnParams } from '../../../actions/columns';
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const mapStateToProps = state => ({
settings: state.getIn(['settings', 'community']),
});
return {
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
};
};
const mapDispatchToProps = (dispatch, { columnId }) => {
const mapDispatchToProps = (dispatch) => {
return {
onChange (key, checked) {
if (columnId) {
dispatch(changeColumnParams(columnId, key, checked));
} else {
dispatch(changeSetting(['community', ...key], checked));
}
dispatch(changeSetting(['community', ...key], checked));
},
};
};
......
......@@ -4,23 +4,32 @@ import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import StatusListContainer from '../ui/containers/status_list_container';
import Column from '../../components/column';
import { expandCommunityTimeline } from '../../actions/timelines';
import ColumnSettingsContainer from './containers/column_settings_container';
import { connectCommunityStream } from '../../actions/streaming';
import HomeColumnHeader from '../../components/home_column_header';
import {
expandCommunityTimeline,
expandPublicTimeline,
} from '../../actions/timelines';
import {
connectCommunityStream,
connectPublicStream,
} from '../../actions/streaming';
const messages = defineMessages({
title: { id: 'column.community', defaultMessage: 'Local timeline' },
title: { id: 'column.community', defaultMessage: 'Community timeline' },
});
const mapStateToProps = (state, { onlyMedia, columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);
const index = columns.findIndex(c => c.get('uuid') === uuid);
const mapStateToProps = state => {
const allFediverse = state.getIn(['settings', 'community', 'other', 'allFediverse']);
const onlyMedia = state.getIn(['settings', 'community', 'other', 'onlyMedia']);
const timelineId = allFediverse ? 'public' : 'community';
return {
hasUnread: state.getIn(['timelines', `community${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
onlyMedia: (columnId && index >= 0) ? columns.get(index).getIn(['params', 'other', 'onlyMedia']) : state.getIn(['settings', 'community', 'other', 'onlyMedia']),
timelineId,
allFediverse,
onlyMedia,
hasUnread: state.getIn(['timelines', `${timelineId}${onlyMedia ? ':media' : ''}`, 'unread']) > 0,
};
};
......@@ -34,30 +43,45 @@ class CommunityTimeline extends React.PureComponent {
static defaultProps = {
onlyMedia: false,
allFediverse: false,
};
static propTypes = {
dispatch: PropTypes.func.isRequired,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
onlyMedia: PropTypes.bool,
allFediverse: PropTypes.bool,
timelineId: PropTypes.string,
};
componentDidMount () {
const { dispatch, onlyMedia } = this.props;
const { dispatch, onlyMedia, allFediverse } = this.props;
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
if (allFediverse) {
dispatch(expandPublicTimeline({ onlyMedia }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
else {
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
}
componentDidUpdate (prevProps) {
if (prevProps.onlyMedia !== this.props.onlyMedia) {
const { dispatch, onlyMedia } = this.props;
if (prevProps.onlyMedia !== this.props.onlyMedia || prevProps.allFediverse !== this.props.allFediverse) {
const { dispatch, onlyMedia, allFediverse } = this.props;
this.disconnect();
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
if (allFediverse) {
dispatch(expandPublicTimeline({ onlyMedia }));
this.disconnect = dispatch(connectPublicStream({ onlyMedia }));
}
else {
dispatch(expandCommunityTimeline({ onlyMedia }));
this.disconnect = dispatch(connectCommunityStream({ onlyMedia }));
}
}
}
......@@ -69,27 +93,29 @@ class CommunityTimeline extends React.PureComponent {
}
handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props;
const { dispatch, onlyMedia, allFediverse } = this.props;
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
if (allFediverse) {
dispatch(expandPublicTimeline({ maxId, onlyMedia }));
}
else {
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
}
}
render () {
const { intl, hasUnread, columnId, onlyMedia } = this.props;
const { intl, hasUnread, onlyMedia, timelineId, allFediverse } = this.props;
return (
<Column label={intl.formatMessage(messages.title)}>
<HomeColumnHeader
activeItem='all'
active={hasUnread}
>
<HomeColumnHeader activeItem='all' active={hasUnread} >
<ColumnSettingsContainer />
</HomeColumnHeader>
<StatusListContainer
scrollKey={`community_timeline-${columnId}`}
timelineId={`community${onlyMedia ? ':media' : ''}`}