commit
7d2e6429c2
@ -1,3 +1,9 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
[Issue text goes here].
|
[Issue text goes here].
|
||||||
|
|
||||||
* * * *
|
* * * *
|
@ -0,0 +1,56 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::Push::SubscriptionsController < Api::BaseController
|
||||||
|
before_action -> { doorkeeper_authorize! :push }
|
||||||
|
before_action :require_user!
|
||||||
|
before_action :set_web_push_subscription
|
||||||
|
|
||||||
|
def create
|
||||||
|
@web_subscription&.destroy!
|
||||||
|
|
||||||
|
@web_subscription = ::Web::PushSubscription.create!(
|
||||||
|
endpoint: subscription_params[:endpoint],
|
||||||
|
key_p256dh: subscription_params[:keys][:p256dh],
|
||||||
|
key_auth: subscription_params[:keys][:auth],
|
||||||
|
data: data_params,
|
||||||
|
user_id: current_user.id,
|
||||||
|
access_token_id: doorkeeper_token.id
|
||||||
|
)
|
||||||
|
|
||||||
|
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def show
|
||||||
|
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||||
|
|
||||||
|
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def update
|
||||||
|
raise ActiveRecord::RecordNotFound if @web_subscription.nil?
|
||||||
|
|
||||||
|
@web_subscription.update!(data: data_params)
|
||||||
|
|
||||||
|
render json: @web_subscription, serializer: REST::WebPushSubscriptionSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
def destroy
|
||||||
|
@web_subscription&.destroy!
|
||||||
|
render_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_web_push_subscription
|
||||||
|
@web_subscription = ::Web::PushSubscription.find_by(access_token_id: doorkeeper_token.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscription_params
|
||||||
|
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
|
||||||
|
end
|
||||||
|
|
||||||
|
def data_params
|
||||||
|
return {} if params[:data].blank?
|
||||||
|
params.require(:data).permit(alerts: [:follow, :favourite, :reblog, :mention])
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,17 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V1::TrendsController < Api::BaseController
|
||||||
|
before_action :set_tags
|
||||||
|
|
||||||
|
respond_to :json
|
||||||
|
|
||||||
|
def index
|
||||||
|
render json: @tags, each_serializer: REST::TagSerializer
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_tags
|
||||||
|
@tags = TrendingTags.get(limit_param(10))
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,8 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Api::V2::SearchController < Api::V1::SearchController
|
||||||
|
def index
|
||||||
|
@search = Search.new(search)
|
||||||
|
render json: @search, serializer: REST::V2::SearchSerializer
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,22 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module SessionTrackingConcern
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
UPDATE_SIGN_IN_HOURS = 24
|
||||||
|
|
||||||
|
included do
|
||||||
|
before_action :set_session_activity
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def set_session_activity
|
||||||
|
return unless session_needs_update?
|
||||||
|
current_session.touch
|
||||||
|
end
|
||||||
|
|
||||||
|
def session_needs_update?
|
||||||
|
!current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,14 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Oauth::TokensController < Doorkeeper::TokensController
|
||||||
|
def revoke
|
||||||
|
unsubscribe_for_token if authorized? && token.accessible?
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def unsubscribe_for_token
|
||||||
|
Web::PushSubscription.where(access_token_id: token.id).delete_all
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { injectIntl, defineMessages } from 'react-intl';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
|
||||||
|
});
|
||||||
|
|
||||||
|
@injectIntl
|
||||||
|
export default class LoadGap extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
disabled: PropTypes.bool,
|
||||||
|
maxId: PropTypes.string,
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClick = () => {
|
||||||
|
this.props.onClick(this.props.maxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { disabled, intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className='load-more load-gap' disabled={disabled} onClick={this.handleClick} aria-label={intl.formatMessage(messages.load_more)}>
|
||||||
|
<i className='fa fa-ellipsis-h' />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,18 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import Card from 'flavours/glitch/features/status/components/card';
|
|
||||||
import { fromJS } from 'immutable';
|
|
||||||
|
|
||||||
export default class CardContainer extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
locale: PropTypes.string,
|
|
||||||
card: PropTypes.array.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { card, ...props } = this.props;
|
|
||||||
return <Card card={fromJS(card)} {...props} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,90 @@
|
|||||||
|
import React, { PureComponent, Fragment } from 'react';
|
||||||
|
import ReactDOM from 'react-dom';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { IntlProvider, addLocaleData } from 'react-intl';
|
||||||
|
import { getLocale } from 'mastodon/locales';
|
||||||
|
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
||||||
|
import Video from 'flavours/glitch/features/video';
|
||||||
|
import Card from 'flavours/glitch/features/status/components/card';
|
||||||
|
import ModalRoot from 'flavours/glitch/components/modal_root';
|
||||||
|
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
||||||
|
import { List as ImmutableList, fromJS } from 'immutable';
|
||||||
|
|
||||||
|
const { localeData, messages } = getLocale();
|
||||||
|
addLocaleData(localeData);
|
||||||
|
|
||||||
|
const MEDIA_COMPONENTS = { MediaGallery, Video, Card };
|
||||||
|
|
||||||
|
export default class MediaContainer extends PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
locale: PropTypes.string.isRequired,
|
||||||
|
components: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
media: null,
|
||||||
|
index: null,
|
||||||
|
time: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleOpenMedia = (media, index) => {
|
||||||
|
document.body.classList.add('media-standalone__body');
|
||||||
|
this.setState({ media, index });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleOpenVideo = (video, time) => {
|
||||||
|
const media = ImmutableList([video]);
|
||||||
|
|
||||||
|
document.body.classList.add('media-standalone__body');
|
||||||
|
this.setState({ media, time });
|
||||||
|
}
|
||||||
|
|
||||||
|
handleCloseMedia = () => {
|
||||||
|
document.body.classList.remove('media-standalone__body');
|
||||||
|
this.setState({ media: null, index: null, time: null });
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { locale, components } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IntlProvider locale={locale} messages={messages}>
|
||||||
|
<Fragment>
|
||||||
|
{[].map.call(components, (component, i) => {
|
||||||
|
const componentName = component.getAttribute('data-component');
|
||||||
|
const Component = MEDIA_COMPONENTS[componentName];
|
||||||
|
const { media, card, ...props } = JSON.parse(component.getAttribute('data-props'));
|
||||||
|
|
||||||
|
Object.assign(props, {
|
||||||
|
...(media ? { media: fromJS(media) } : {}),
|
||||||
|
...(card ? { card: fromJS(card) } : {}),
|
||||||
|
|
||||||
|
...(componentName === 'Video' ? {
|
||||||
|
onOpenVideo: this.handleOpenVideo,
|
||||||
|
} : {
|
||||||
|
onOpenMedia: this.handleOpenMedia,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return ReactDOM.createPortal(
|
||||||
|
<Component {...props} key={`media-${i}`} />,
|
||||||
|
component,
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ModalRoot onClose={this.handleCloseMedia}>
|
||||||
|
{this.state.media && (
|
||||||
|
<MediaModal
|
||||||
|
media={this.state.media}
|
||||||
|
index={this.state.index || 0}
|
||||||
|
time={this.state.time}
|
||||||
|
onClose={this.handleCloseMedia}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ModalRoot>
|
||||||
|
</Fragment>
|
||||||
|
</IntlProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,68 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
|
||||||
import { getLocale } from 'mastodon/locales';
|
|
||||||
import MediaGallery from 'flavours/glitch/components/media_gallery';
|
|
||||||
import ModalRoot from 'flavours/glitch/components/modal_root';
|
|
||||||
import MediaModal from 'flavours/glitch/features/ui/components/media_modal';
|
|
||||||
import { fromJS } from 'immutable';
|
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
|
||||||
addLocaleData(localeData);
|
|
||||||
|
|
||||||
export default class MediaGalleriesContainer extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
locale: PropTypes.string.isRequired,
|
|
||||||
galleries: PropTypes.object.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
|
||||||
media: null,
|
|
||||||
index: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleOpenMedia = (media, index) => {
|
|
||||||
document.body.classList.add('media-gallery-standalone__body');
|
|
||||||
this.setState({ media, index });
|
|
||||||
}
|
|
||||||
|
|
||||||
handleCloseMedia = () => {
|
|
||||||
document.body.classList.remove('media-gallery-standalone__body');
|
|
||||||
this.setState({ media: null, index: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { locale, galleries } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
|
||||||
<React.Fragment>
|
|
||||||
{[].map.call(galleries, gallery => {
|
|
||||||
const { media, ...props } = JSON.parse(gallery.getAttribute('data-props'));
|
|
||||||
|
|
||||||
return ReactDOM.createPortal(
|
|
||||||
<MediaGallery
|
|
||||||
{...props}
|
|
||||||
media={fromJS(media)}
|
|
||||||
onOpenMedia={this.handleOpenMedia}
|
|
||||||
/>,
|
|
||||||
gallery
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<ModalRoot onClose={this.handleCloseMedia}>
|
|
||||||
{this.state.media === null || this.state.index === null ? null : (
|
|
||||||
<MediaModal
|
|
||||||
media={this.state.media}
|
|
||||||
index={this.state.index}
|
|
||||||
onClose={this.handleCloseMedia}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ModalRoot>
|
|
||||||
</React.Fragment>
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { IntlProvider, addLocaleData } from 'react-intl';
|
|
||||||
import { getLocale } from 'mastodon/locales';
|
|
||||||
import Video from 'flavours/glitch/features/video';
|
|
||||||
|
|
||||||
const { localeData, messages } = getLocale();
|
|
||||||
addLocaleData(localeData);
|
|
||||||
|
|
||||||
export default class VideoContainer extends React.PureComponent {
|
|
||||||
|
|
||||||
static propTypes = {
|
|
||||||
locale: PropTypes.string.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
render () {
|
|
||||||
const { locale, ...props } = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IntlProvider locale={locale} messages={messages}>
|
|
||||||
<Video {...props} />
|
|
||||||
</IntlProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,53 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
// This is the spring used with our motion.
|
||||||
|
const motionSpring = spring(1, { damping: 35, stiffness: 400 });
|
||||||
|
|
||||||
|
// Messages.
|
||||||
|
const messages = defineMessages({
|
||||||
|
disclaimer: {
|
||||||
|
defaultMessage: 'This toot will only be sent to all the mentioned users.',
|
||||||
|
id: 'compose_form.direct_message_warning',
|
||||||
|
},
|
||||||
|
learn_more: {
|
||||||
|
defaultMessage: 'Learn more',
|
||||||
|
id: 'compose_form.direct_message_warning_learn_more'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The component.
|
||||||
|
export default function ComposerDirectWarning () {
|
||||||
|
return (
|
||||||
|
<Motion
|
||||||
|
defaultStyle={{
|
||||||
|
opacity: 0,
|
||||||
|
scaleX: 0.85,
|
||||||
|
scaleY: 0.75,
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
opacity: motionSpring,
|
||||||
|
scaleX: motionSpring,
|
||||||
|
scaleY: motionSpring,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ opacity, scaleX, scaleY }) => (
|
||||||
|
<div
|
||||||
|
className='composer--warning'
|
||||||
|
style={{
|
||||||
|
opacity: opacity,
|
||||||
|
transform: `scale(${scaleX}, ${scaleY})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage {...messages.disclaimer} /> <a href='/terms' target='_blank'><FormattedMessage {...messages.learn_more} /></a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Motion>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposerDirectWarning.propTypes = {};
|
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import StatusListContainer from 'flavours/glitch/features/ui/containers/status_list_container';
|
||||||
|
import { expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
|
||||||
|
import Column from 'flavours/glitch/components/column';
|
||||||
|
import ColumnHeader from 'flavours/glitch/components/column_header';
|
||||||
|
import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
import { connectCommunityStream } from 'flavours/glitch/actions/streaming';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
title: { id: 'standalone.public_title', defaultMessage: 'A look inside...' },
|
||||||
|
});
|
||||||
|
|
||||||
|
@connect()
|
||||||
|
@injectIntl
|
||||||
|
export default class CommunityTimeline extends React.PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
dispatch: PropTypes.func.isRequired,
|
||||||
|
intl: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleHeaderClick = () => {
|
||||||
|
this.column.scrollTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = c => {
|
||||||
|
this.column = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
|
dispatch(expandCommunityTimeline());
|
||||||
|
this.disconnect = dispatch(connectCommunityStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (this.disconnect) {
|
||||||
|
this.disconnect();
|
||||||
|
this.disconnect = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = maxId => {
|
||||||
|
this.props.dispatch(expandCommunityTimeline({ maxId }));
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { intl } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column ref={this.setRef}>
|
||||||
|
<ColumnHeader
|
||||||
|
icon='users'
|
||||||
|
title={intl.formatMessage(messages.title)}
|
||||||
|
onClick={this.handleHeaderClick}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusListContainer
|
||||||
|
timelineId='community'
|
||||||
|
onLoadMore={this.handleLoadMore}
|
||||||
|
scrollKey='standalone_public_timeline'
|
||||||
|
trackScroll={false}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue