mirror of
https://git.bsd.gay/fef/nyastodon.git
synced 2024-12-28 04:13:44 +01:00
Merge pull request #2300 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes
This commit is contained in:
commit
2f6ac6b432
45 changed files with 459 additions and 301 deletions
7
.github/renovate.json5
vendored
7
.github/renovate.json5
vendored
|
@ -15,7 +15,6 @@
|
||||||
// Ignore major version bumps for these node packages
|
// Ignore major version bumps for these node packages
|
||||||
matchManagers: ['npm'],
|
matchManagers: ['npm'],
|
||||||
matchPackageNames: [
|
matchPackageNames: [
|
||||||
'@rails/ujs', // Needs to match the major Rails version
|
|
||||||
'tesseract.js', // Requires code changes
|
'tesseract.js', // Requires code changes
|
||||||
'react-hotkeys', // Requires code changes
|
'react-hotkeys', // Requires code changes
|
||||||
|
|
||||||
|
@ -51,12 +50,6 @@
|
||||||
'sidekiq', // Requires manual upgrade
|
'sidekiq', // Requires manual upgrade
|
||||||
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
|
'sidekiq-unique-jobs', // Requires manual upgrades and sync with Sidekiq version
|
||||||
'redis', // Requires manual upgrade and sync with Sidekiq version
|
'redis', // Requires manual upgrade and sync with Sidekiq version
|
||||||
'fog-openstack', // TODO: was ignored in https://github.com/mastodon/mastodon/pull/13964
|
|
||||||
|
|
||||||
// Needs major Rails version bump
|
|
||||||
'rack',
|
|
||||||
'rails',
|
|
||||||
'rails-i18n',
|
|
||||||
],
|
],
|
||||||
matchUpdateTypes: ['major'],
|
matchUpdateTypes: ['major'],
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|
|
@ -1,25 +1,36 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Api::Web::EmbedsController < Api::Web::BaseController
|
class Api::Web::EmbedsController < Api::Web::BaseController
|
||||||
before_action :require_user!
|
include Authorization
|
||||||
|
|
||||||
def create
|
before_action :set_status
|
||||||
status = StatusFinder.new(params[:url]).status
|
|
||||||
|
|
||||||
return not_found if status.hidden?
|
def show
|
||||||
|
return not_found if @status.hidden?
|
||||||
|
|
||||||
render json: status, serializer: OEmbedSerializer, width: 400
|
if @status.local?
|
||||||
rescue ActiveRecord::RecordNotFound
|
render json: @status, serializer: OEmbedSerializer, width: 400
|
||||||
oembed = FetchOEmbedService.new.call(params[:url])
|
else
|
||||||
|
return not_found unless user_signed_in?
|
||||||
|
|
||||||
return not_found if oembed.nil?
|
url = ActivityPub::TagManager.instance.url_for(@status)
|
||||||
|
oembed = FetchOEmbedService.new.call(url)
|
||||||
|
return not_found if oembed.nil?
|
||||||
|
|
||||||
begin
|
begin
|
||||||
oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
|
oembed[:html] = Sanitize.fragment(oembed[:html], Sanitize::Config::MASTODON_OEMBED)
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return not_found
|
return not_found
|
||||||
|
end
|
||||||
|
|
||||||
|
render json: oembed
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
render json: oembed
|
def set_status
|
||||||
|
@status = Status.find(params[:id])
|
||||||
|
authorize @status, :show?
|
||||||
|
rescue Mastodon::NotPermittedError
|
||||||
|
not_found
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
23
app/javascript/flavours/glitch/components/router.tsx
Normal file
23
app/javascript/flavours/glitch/components/router.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type { History } from 'history';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { Router as OriginalRouter } from 'react-router';
|
||||||
|
|
||||||
|
import { layoutFromWindow } from 'flavours/glitch/is_mobile';
|
||||||
|
|
||||||
|
const browserHistory = createBrowserHistory();
|
||||||
|
const originalPush = browserHistory.push.bind(browserHistory);
|
||||||
|
|
||||||
|
browserHistory.push = (path: string, state: History.LocationState) => {
|
||||||
|
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
||||||
|
originalPush(`/deck${path}`, state);
|
||||||
|
} else {
|
||||||
|
originalPush(path, state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
|
||||||
|
};
|
|
@ -199,9 +199,8 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
|
const { status, intl, withDismiss, withCounters, showReplyCount, scrollKey } = this.props;
|
||||||
const { permissions } = this.context.identity;
|
const { permissions, signedIn } = this.context.identity;
|
||||||
|
|
||||||
const anonymousAccess = !me;
|
|
||||||
const mutingConversation = status.get('muted');
|
const mutingConversation = status.get('muted');
|
||||||
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
|
||||||
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
|
||||||
|
@ -225,53 +224,55 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus && (signedIn || !isRemote)) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push(null);
|
if (signedIn) {
|
||||||
|
|
||||||
if (writtenByMe && pinnableStatus) {
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writtenByMe || withDismiss) {
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (writtenByMe) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
|
|
||||||
} else {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
|
|
||||||
if (!this.props.onFilter) {
|
if (writtenByMe && pinnableStatus) {
|
||||||
menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
|
if (writtenByMe || withDismiss) {
|
||||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
|
|
||||||
|
|
||||||
if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
|
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
|
}
|
||||||
if (accountAdminLink !== undefined) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) });
|
if (writtenByMe) {
|
||||||
}
|
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
if (statusAdminLink !== undefined) {
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) });
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
|
||||||
}
|
} else {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||||
|
menu.push(null);
|
||||||
|
|
||||||
|
if (!this.props.onFilter) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
|
||||||
|
menu.push(null);
|
||||||
}
|
}
|
||||||
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
|
|
||||||
const domain = status.getIn(['account', 'acct']).split('@')[1];
|
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
|
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
|
||||||
|
|
||||||
|
if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
|
||||||
|
menu.push(null);
|
||||||
|
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
|
||||||
|
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')) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
|
||||||
|
const domain = status.getIn(['account', 'acct']).split('@')[1];
|
||||||
|
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,14 +314,13 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
/>
|
/>
|
||||||
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon={reblogIcon} onClick={this.handleReblogClick} counter={withCounters ? status.get('reblogs_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={withCounters ? status.get('favourites_count') : undefined} />
|
||||||
<IconButton className='status__action-bar-button bookmark-icon' disabled={anonymousAccess} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
<IconButton className='status__action-bar-button bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} />
|
||||||
|
|
||||||
{filterButton}
|
{filterButton}
|
||||||
|
|
||||||
<div className='status__action-bar-dropdown'>
|
<div className='status__action-bar-dropdown'>
|
||||||
<DropdownMenuContainer
|
<DropdownMenuContainer
|
||||||
scrollKey={scrollKey}
|
scrollKey={scrollKey}
|
||||||
disabled={anonymousAccess}
|
|
||||||
status={status}
|
status={status}
|
||||||
items={menu}
|
items={menu}
|
||||||
icon='ellipsis-h'
|
icon='ellipsis-h'
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { BrowserRouter, Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
|
|
||||||
import { Provider as ReduxProvider } from 'react-redux';
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ import { checkDeprecatedLocalSettings } from 'flavours/glitch/actions/local_sett
|
||||||
import { hydrateStore } from 'flavours/glitch/actions/store';
|
import { hydrateStore } from 'flavours/glitch/actions/store';
|
||||||
import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
import { connectUserStream } from 'flavours/glitch/actions/streaming';
|
||||||
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
|
import ErrorBoundary from 'flavours/glitch/components/error_boundary';
|
||||||
|
import { Router } from 'flavours/glitch/components/router';
|
||||||
import UI from 'flavours/glitch/features/ui';
|
import UI from 'flavours/glitch/features/ui';
|
||||||
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
|
import initialState, { title as siteTitle } from 'flavours/glitch/initial_state';
|
||||||
import { IntlProvider } from 'flavours/glitch/locales';
|
import { IntlProvider } from 'flavours/glitch/locales';
|
||||||
|
@ -79,11 +80,11 @@ export default class Mastodon extends PureComponent {
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<BrowserRouter>
|
<Router>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
<Route path='/' component={UI} />
|
<Route path='/' component={UI} />
|
||||||
</ScrollContext>
|
</ScrollContext>
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
|
|
||||||
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -177,7 +177,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
url: status.get('url'),
|
id: status.get('id'),
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -192,7 +192,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<LinkFooter />
|
<LinkFooter multiColumn />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(multiColumn && showTrends) && <TrendsContainer />}
|
{(multiColumn && showTrends) && <TrendsContainer />}
|
||||||
|
|
|
@ -17,12 +17,6 @@ import LocalSettingsPageItem from './item';
|
||||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
layout_auto: { id: 'layout.auto', defaultMessage: 'Auto' },
|
|
||||||
layout_auto_hint: { id: 'layout.hint.auto', defaultMessage: 'Automatically chose layout based on “Enable advanced web interface” setting and screen size.' },
|
|
||||||
layout_desktop: { id: 'layout.desktop', defaultMessage: 'Desktop' },
|
|
||||||
layout_desktop_hint: { id: 'layout.hint.desktop', defaultMessage: 'Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.' },
|
|
||||||
layout_mobile: { id: 'layout.single', defaultMessage: 'Mobile' },
|
|
||||||
layout_mobile_hint: { id: 'layout.hint.single', defaultMessage: 'Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.' },
|
|
||||||
side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' },
|
side_arm_none: { id: 'settings.side_arm.none', defaultMessage: 'None' },
|
||||||
side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep its set privacy' },
|
side_arm_keep: { id: 'settings.side_arm_reply_mode.keep', defaultMessage: 'Keep its set privacy' },
|
||||||
side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' },
|
side_arm_copy: { id: 'settings.side_arm_reply_mode.copy', defaultMessage: 'Copy privacy setting of the toot being replied to' },
|
||||||
|
@ -166,19 +160,6 @@ class LocalSettingsPage extends PureComponent {
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
|
<h2><FormattedMessage id='settings.layout_opts' defaultMessage='Layout options' /></h2>
|
||||||
<LocalSettingsPageItem
|
|
||||||
settings={settings}
|
|
||||||
item={['layout']}
|
|
||||||
id='mastodon-settings--layout'
|
|
||||||
options={[
|
|
||||||
{ value: 'auto', message: intl.formatMessage(messages.layout_auto), hint: intl.formatMessage(messages.layout_auto_hint) },
|
|
||||||
{ value: 'multiple', message: intl.formatMessage(messages.layout_desktop), hint: intl.formatMessage(messages.layout_desktop_hint) },
|
|
||||||
{ value: 'single', message: intl.formatMessage(messages.layout_mobile), hint: intl.formatMessage(messages.layout_mobile_hint) },
|
|
||||||
]}
|
|
||||||
onChange={onChange}
|
|
||||||
>
|
|
||||||
<FormattedMessage id='settings.layout' defaultMessage='Layout:' />
|
|
||||||
</LocalSettingsPageItem>
|
|
||||||
<LocalSettingsPageItem
|
<LocalSettingsPageItem
|
||||||
settings={settings}
|
settings={settings}
|
||||||
item={['stretch']}
|
item={['stretch']}
|
||||||
|
|
|
@ -152,52 +152,55 @@ class ActionBar extends PureComponent {
|
||||||
|
|
||||||
let menu = [];
|
let menu = [];
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus && isRemote) {
|
||||||
if (isRemote) {
|
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
||||||
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
|
||||||
|
|
||||||
if ('share' in navigator) {
|
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (writtenByMe) {
|
menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
|
||||||
if (pinnableStatus) {
|
|
||||||
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
|
||||||
menu.push(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
if (publicStatus && 'share' in navigator) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (publicStatus && (signedIn || !isRemote)) {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (signedIn) {
|
||||||
menu.push(null);
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
if (writtenByMe) {
|
||||||
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
|
if (pinnableStatus) {
|
||||||
} else {
|
menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
menu.push(null);
|
||||||
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
|
||||||
menu.push(null);
|
|
||||||
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
|
|
||||||
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
|
|
||||||
if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
|
|
||||||
menu.push(null);
|
|
||||||
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
|
|
||||||
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')) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
|
|
||||||
const domain = status.getIn(['account', 'acct']).split('@')[1];
|
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
|
||||||
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
|
||||||
|
} else {
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.direct, { name: status.getIn(['account', 'username']) }), action: this.handleDirectClick });
|
||||||
|
menu.push(null);
|
||||||
|
menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick, dangerous: true });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick, dangerous: true });
|
||||||
|
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
|
||||||
|
if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
|
||||||
|
menu.push(null);
|
||||||
|
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
|
||||||
|
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')) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
|
||||||
|
const domain = status.getIn(['account', 'acct']).split('@')[1];
|
||||||
|
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
url: status.get('url'),
|
id: status.get('id'),
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -500,7 +500,7 @@ class Status extends ImmutablePureComponent {
|
||||||
handleEmbed = (status) => {
|
handleEmbed = (status) => {
|
||||||
this.props.dispatch(openModal({
|
this.props.dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: { url: status.get('url') },
|
modalProps: { id: status.get('id') },
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const messages = defineMessages({
|
||||||
class EmbedModal extends ImmutablePureComponent {
|
class EmbedModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
url: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
onError: PropTypes.func.isRequired,
|
onError: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -26,11 +26,11 @@ class EmbedModal extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { url } = this.props;
|
const { id } = this.props;
|
||||||
|
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
api().post('/api/web/embed', { url }).then(res => {
|
api().get(`/api/web/embeds/${id}`).then(res => {
|
||||||
this.setState({ loading: false, oembed: res.data });
|
this.setState({ loading: false, oembed: res.data });
|
||||||
|
|
||||||
const iframeDocument = this.iframe.contentWindow.document;
|
const iframeDocument = this.iframe.contentWindow.document;
|
||||||
|
|
|
@ -38,6 +38,7 @@ class LinkFooter extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -53,6 +54,7 @@ class LinkFooter extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn, permissions } = this.context.identity;
|
const { signedIn, permissions } = this.context.identity;
|
||||||
|
const { multiColumn } = this.props;
|
||||||
|
|
||||||
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
||||||
const canProfileDirectory = profileDirectory;
|
const canProfileDirectory = profileDirectory;
|
||||||
|
@ -64,7 +66,7 @@ class LinkFooter extends PureComponent {
|
||||||
<p>
|
<p>
|
||||||
<strong>{domain}</strong>:
|
<strong>{domain}</strong>:
|
||||||
{' '}
|
{' '}
|
||||||
<Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
<Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
||||||
{statusPageUrl && (
|
{statusPageUrl && (
|
||||||
<>
|
<>
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
|
@ -84,7 +86,7 @@ class LinkFooter extends PureComponent {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
<Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
<Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { defineMessages, injectIntl } from 'react-intl';
|
||||||
|
|
||||||
import NavigationPortal from 'flavours/glitch/components/navigation_portal';
|
import NavigationPortal from 'flavours/glitch/components/navigation_portal';
|
||||||
import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
|
import { timelinePreview, trendsEnabled } from 'flavours/glitch/initial_state';
|
||||||
|
import { transientSingleColumn } from 'flavours/glitch/is_mobile';
|
||||||
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
|
import { preferencesLink } from 'flavours/glitch/utils/backend_links';
|
||||||
|
|
||||||
import ColumnLink from './column_link';
|
import ColumnLink from './column_link';
|
||||||
|
@ -27,6 +28,7 @@ const messages = defineMessages({
|
||||||
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
||||||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
||||||
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
app_settings: { id: 'navigation_bar.app_settings', defaultMessage: 'App settings' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,6 +54,15 @@ class NavigationPanel extends Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
|
{transientSingleColumn && (
|
||||||
|
<>
|
||||||
|
<a href={`/deck${location.pathname}`} className='button button--block'>
|
||||||
|
{intl.formatMessage(messages.advancedInterface)}
|
||||||
|
</a>
|
||||||
|
<hr />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{signedIn && (
|
{signedIn && (
|
||||||
<>
|
<>
|
||||||
<ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />
|
<ColumnLink transparent to='/home' icon='home' text={intl.formatMessage(messages.home)} />
|
||||||
|
|
|
@ -79,7 +79,6 @@ const mapStateToProps = state => ({
|
||||||
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
hasComposingText: state.getIn(['compose', 'text']).trim().length !== 0,
|
||||||
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
|
||||||
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < 4,
|
||||||
layout_local_setting: state.getIn(['local_settings', 'layout']),
|
|
||||||
isWide: state.getIn(['local_settings', 'stretch']),
|
isWide: state.getIn(['local_settings', 'stretch']),
|
||||||
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
dropdownMenuIsOpen: state.getIn(['dropdown_menu', 'openId']) !== null,
|
||||||
unreadNotifications: state.getIn(['notifications', 'unread']),
|
unreadNotifications: state.getIn(['notifications', 'unread']),
|
||||||
|
@ -134,11 +133,11 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
mobile: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
UNSAFE_componentWillMount () {
|
||||||
if (this.props.mobile) {
|
if (this.props.singleColumn) {
|
||||||
document.body.classList.toggle('layout-single-column', true);
|
document.body.classList.toggle('layout-single-column', true);
|
||||||
document.body.classList.toggle('layout-multiple-columns', false);
|
document.body.classList.toggle('layout-multiple-columns', false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -152,9 +151,9 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
this.node.handleChildrenContentChange();
|
this.node.handleChildrenContentChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.mobile !== this.props.mobile) {
|
if (prevProps.singleColumn !== this.props.singleColumn) {
|
||||||
document.body.classList.toggle('layout-single-column', this.props.mobile);
|
document.body.classList.toggle('layout-single-column', this.props.singleColumn);
|
||||||
document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
|
document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,16 +164,17 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, mobile } = this.props;
|
const { children, singleColumn } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
const pathName = this.props.location.pathname;
|
||||||
|
|
||||||
let redirect;
|
let redirect;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
if (mobile) {
|
if (singleColumn) {
|
||||||
redirect = <Redirect from='/' to='/home' exact />;
|
redirect = <Redirect from='/' to='/home' exact />;
|
||||||
} else {
|
} else {
|
||||||
redirect = <Redirect from='/' to='/getting-started' exact />;
|
redirect = <Redirect from='/' to='/deck/getting-started' exact />;
|
||||||
}
|
}
|
||||||
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
||||||
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
||||||
|
@ -185,10 +185,13 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
|
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
{redirect}
|
{redirect}
|
||||||
|
|
||||||
|
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
||||||
|
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
|
||||||
|
|
||||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
<WrappedRoute path='/about' component={About} content={children} />
|
<WrappedRoute path='/about' component={About} content={children} />
|
||||||
|
@ -256,7 +259,6 @@ class UI extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
layout_local_setting: PropTypes.string,
|
|
||||||
isWide: PropTypes.bool,
|
isWide: PropTypes.bool,
|
||||||
systemFontUi: PropTypes.bool,
|
systemFontUi: PropTypes.bool,
|
||||||
isComposing: PropTypes.bool,
|
isComposing: PropTypes.bool,
|
||||||
|
@ -381,7 +383,7 @@ class UI extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
handleResize = () => {
|
handleResize = () => {
|
||||||
const layout = layoutFromWindow(this.props.layout_local_setting);
|
const layout = layoutFromWindow();
|
||||||
|
|
||||||
if (layout !== this.props.layout) {
|
if (layout !== this.props.layout) {
|
||||||
this.handleLayoutChange.cancel();
|
this.handleLayoutChange.cancel();
|
||||||
|
@ -445,19 +447,6 @@ class UI extends Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
|
||||||
if (nextProps.layout_local_setting !== this.props.layout_local_setting) {
|
|
||||||
const layout = layoutFromWindow(nextProps.layout_local_setting);
|
|
||||||
|
|
||||||
if (layout !== this.props.layout) {
|
|
||||||
this.handleLayoutChange.cancel();
|
|
||||||
this.props.dispatch(changeLayout(layout));
|
|
||||||
} else {
|
|
||||||
this.handleLayoutChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
componentDidUpdate (prevProps) {
|
||||||
if (this.props.unreadNotifications !== prevProps.unreadNotifications ||
|
if (this.props.unreadNotifications !== prevProps.unreadNotifications ||
|
||||||
this.props.showFaviconBadge !== prevProps.showFaviconBadge) {
|
this.props.showFaviconBadge !== prevProps.showFaviconBadge) {
|
||||||
|
@ -667,7 +656,7 @@ class UI extends Component {
|
||||||
|
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
|
<SwitchingColumnsArea location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
|
||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as React from 'react';
|
import { Component, PureComponent, cloneElement, Children } from 'react';
|
||||||
|
|
||||||
import { Switch, Route } from 'react-router-dom';
|
import { Switch, Route } from 'react-router-dom';
|
||||||
|
|
||||||
|
@ -10,14 +10,22 @@ import ColumnLoading from 'flavours/glitch/features/ui/components/column_loading
|
||||||
import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container';
|
import BundleContainer from 'flavours/glitch/features/ui/containers/bundle_container';
|
||||||
|
|
||||||
// Small wrapper to pass multiColumn to the route components
|
// Small wrapper to pass multiColumn to the route components
|
||||||
export class WrappedSwitch extends React.PureComponent {
|
export class WrappedSwitch extends PureComponent {
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { multiColumn, children } = this.props;
|
const { multiColumn, children } = this.props;
|
||||||
|
const { location } = this.context.router.route;
|
||||||
|
|
||||||
|
const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
|
||||||
|
? {...location, pathname: location.pathname.slice(5)}
|
||||||
|
: location;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch location={decklessLocation}>
|
||||||
{React.Children.map(children, child => React.cloneElement(child, { multiColumn }))}
|
{Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -32,7 +40,7 @@ WrappedSwitch.propTypes = {
|
||||||
// Small Wraper to extract the params from the route and pass
|
// Small Wraper to extract the params from the route and pass
|
||||||
// them to the rendered component, together with the content to
|
// them to the rendered component, together with the content to
|
||||||
// be rendered inside (the children)
|
// be rendered inside (the children)
|
||||||
export class WrappedRoute extends React.Component {
|
export class WrappedRoute extends Component {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
component: PropTypes.func.isRequired,
|
component: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -88,6 +88,13 @@
|
||||||
* @property {string} default_content_type
|
* @property {string} default_content_type
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
|
||||||
|
/** @type {boolean} */
|
||||||
|
export const hasMultiColumnPath = initialPath === '/'
|
||||||
|
|| initialPath === '/getting-started'
|
||||||
|
|| initialPath.startsWith('/deck');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef InitialState
|
* @typedef InitialState
|
||||||
* @property {Record<string, Account>} accounts
|
* @property {Record<string, Account>} accounts
|
||||||
|
|
|
@ -1,30 +1,21 @@
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
|
||||||
import { forceSingleColumn } from 'flavours/glitch/initial_state';
|
import { forceSingleColumn, hasMultiColumnPath } from './initial_state';
|
||||||
|
|
||||||
const LAYOUT_BREAKPOINT = 630;
|
const LAYOUT_BREAKPOINT = 630;
|
||||||
|
|
||||||
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
||||||
|
|
||||||
|
export const transientSingleColumn = !forceSingleColumn && !hasMultiColumnPath;
|
||||||
|
|
||||||
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
||||||
export const layoutFromWindow = (layout_local_setting: string): LayoutType => {
|
export const layoutFromWindow = (): LayoutType => {
|
||||||
switch (layout_local_setting) {
|
if (isMobile(window.innerWidth)) {
|
||||||
case 'multiple':
|
return 'mobile';
|
||||||
return 'multi-column';
|
} else if (!forceSingleColumn && !transientSingleColumn) {
|
||||||
case 'single':
|
return 'multi-column';
|
||||||
if (isMobile(window.innerWidth)) {
|
} else {
|
||||||
return 'mobile';
|
return 'single-column';
|
||||||
} else {
|
|
||||||
return 'single-column';
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if (isMobile(window.innerWidth)) {
|
|
||||||
return 'mobile';
|
|
||||||
} else if (forceSingleColumn) {
|
|
||||||
return 'single-column';
|
|
||||||
} else {
|
|
||||||
return 'multi-column';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -64,12 +64,6 @@
|
||||||
"keyboard_shortcuts.bookmark": "to bookmark",
|
"keyboard_shortcuts.bookmark": "to bookmark",
|
||||||
"keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting",
|
"keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting",
|
||||||
"keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots",
|
"keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots",
|
||||||
"layout.auto": "Auto",
|
|
||||||
"layout.desktop": "Desktop",
|
|
||||||
"layout.hint.auto": "Automatically chose layout based on “Enable advanced web interface” setting and screen size.",
|
|
||||||
"layout.hint.desktop": "Use multiple-column layout regardless of the “Enable advanced web interface” setting or screen size.",
|
|
||||||
"layout.hint.single": "Use single-column layout regardless of the “Enable advanced web interface” setting or screen size.",
|
|
||||||
"layout.single": "Mobile",
|
|
||||||
"media_gallery.sensitive": "Sensitive",
|
"media_gallery.sensitive": "Sensitive",
|
||||||
"moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.",
|
"moved_to_warning": "This account is marked as moved to {moved_to_link}, and may thus not accept new follows.",
|
||||||
"navigation_bar.app_settings": "App settings",
|
"navigation_bar.app_settings": "App settings",
|
||||||
|
@ -145,7 +139,6 @@
|
||||||
"settings.image_backgrounds_media_hint": "If the post has any media attachment, use the first one as a background",
|
"settings.image_backgrounds_media_hint": "If the post has any media attachment, use the first one as a background",
|
||||||
"settings.image_backgrounds_users": "Give collapsed toots an image background",
|
"settings.image_backgrounds_users": "Give collapsed toots an image background",
|
||||||
"settings.inline_preview_cards": "Inline preview cards for external links",
|
"settings.inline_preview_cards": "Inline preview cards for external links",
|
||||||
"settings.layout": "Layout:",
|
|
||||||
"settings.layout_opts": "Layout options",
|
"settings.layout_opts": "Layout options",
|
||||||
"settings.media": "Media",
|
"settings.media": "Media",
|
||||||
"settings.media_fullwidth": "Full-width media previews",
|
"settings.media_fullwidth": "Full-width media previews",
|
||||||
|
|
|
@ -6,7 +6,6 @@ import { LOCAL_SETTING_CHANGE, LOCAL_SETTING_DELETE } from 'flavours/glitch/acti
|
||||||
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
import { STORE_HYDRATE } from 'flavours/glitch/actions/store';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
layout : 'auto',
|
|
||||||
stretch : true,
|
stretch : true,
|
||||||
side_arm : 'none',
|
side_arm : 'none',
|
||||||
side_arm_reply_mode : 'keep',
|
side_arm_reply_mode : 'keep',
|
||||||
|
|
23
app/javascript/mastodon/components/router.tsx
Normal file
23
app/javascript/mastodon/components/router.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import type { History } from 'history';
|
||||||
|
import { createBrowserHistory } from 'history';
|
||||||
|
import { Router as OriginalRouter } from 'react-router';
|
||||||
|
|
||||||
|
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||||
|
|
||||||
|
const browserHistory = createBrowserHistory();
|
||||||
|
const originalPush = browserHistory.push.bind(browserHistory);
|
||||||
|
|
||||||
|
browserHistory.push = (path: string, state: History.LocationState) => {
|
||||||
|
if (layoutFromWindow() === 'multi-column' && !path.startsWith('/deck')) {
|
||||||
|
originalPush(`/deck${path}`, state);
|
||||||
|
} else {
|
||||||
|
originalPush(path, state);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Router: React.FC<PropsWithChildren> = ({ children }) => {
|
||||||
|
return <OriginalRouter history={browserHistory}>{children}</OriginalRouter>;
|
||||||
|
};
|
|
@ -258,7 +258,7 @@ class StatusActionBar extends ImmutablePureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShareClick });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus && (signedIn || !isRemote)) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||||
import { PureComponent } from 'react';
|
import { PureComponent } from 'react';
|
||||||
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { BrowserRouter, Route } from 'react-router-dom';
|
import { Route } from 'react-router-dom';
|
||||||
|
|
||||||
import { Provider as ReduxProvider } from 'react-redux';
|
import { Provider as ReduxProvider } from 'react-redux';
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||||
import { hydrateStore } from 'mastodon/actions/store';
|
import { hydrateStore } from 'mastodon/actions/store';
|
||||||
import { connectUserStream } from 'mastodon/actions/streaming';
|
import { connectUserStream } from 'mastodon/actions/streaming';
|
||||||
import ErrorBoundary from 'mastodon/components/error_boundary';
|
import ErrorBoundary from 'mastodon/components/error_boundary';
|
||||||
|
import { Router } from 'mastodon/components/router';
|
||||||
import UI from 'mastodon/features/ui';
|
import UI from 'mastodon/features/ui';
|
||||||
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
||||||
import { IntlProvider } from 'mastodon/locales';
|
import { IntlProvider } from 'mastodon/locales';
|
||||||
|
@ -75,11 +76,11 @@ export default class Mastodon extends PureComponent {
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<ReduxProvider store={store}>
|
<ReduxProvider store={store}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<BrowserRouter>
|
<Router>
|
||||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||||
<Route path='/' component={UI} />
|
<Route path='/' component={UI} />
|
||||||
</ScrollContext>
|
</ScrollContext>
|
||||||
</BrowserRouter>
|
</Router>
|
||||||
|
|
||||||
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
<Helmet defaultTitle={title} titleTemplate={`%s - ${title}`} />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
|
|
@ -139,7 +139,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
url: status.get('url'),
|
id: status.get('id'),
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -142,7 +142,7 @@ class GettingStarted extends ImmutablePureComponent {
|
||||||
|
|
||||||
{!multiColumn && <div className='flex-spacer' />}
|
{!multiColumn && <div className='flex-spacer' />}
|
||||||
|
|
||||||
<LinkFooter />
|
<LinkFooter multiColumn />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(multiColumn && showTrends) && <TrendsContainer />}
|
{(multiColumn && showTrends) && <TrendsContainer />}
|
||||||
|
|
|
@ -205,7 +205,7 @@ class ActionBar extends PureComponent {
|
||||||
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicStatus) {
|
if (publicStatus && (signedIn || !isRemote)) {
|
||||||
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: {
|
modalProps: {
|
||||||
url: status.get('url'),
|
id: status.get('id'),
|
||||||
onError: error => dispatch(showAlertForError(error)),
|
onError: error => dispatch(showAlertForError(error)),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -449,7 +449,7 @@ class Status extends ImmutablePureComponent {
|
||||||
handleEmbed = (status) => {
|
handleEmbed = (status) => {
|
||||||
this.props.dispatch(openModal({
|
this.props.dispatch(openModal({
|
||||||
modalType: 'EMBED',
|
modalType: 'EMBED',
|
||||||
modalProps: { url: status.get('url') },
|
modalProps: { id: status.get('id') },
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const messages = defineMessages({
|
||||||
class EmbedModal extends ImmutablePureComponent {
|
class EmbedModal extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
url: PropTypes.string.isRequired,
|
id: PropTypes.string.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
onError: PropTypes.func.isRequired,
|
onError: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
@ -26,11 +26,11 @@ class EmbedModal extends ImmutablePureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
const { url } = this.props;
|
const { id } = this.props;
|
||||||
|
|
||||||
this.setState({ loading: true });
|
this.setState({ loading: true });
|
||||||
|
|
||||||
api().post('/api/web/embed', { url }).then(res => {
|
api().get(`/api/web/embeds/${id}`).then(res => {
|
||||||
this.setState({ loading: false, oembed: res.data });
|
this.setState({ loading: false, oembed: res.data });
|
||||||
|
|
||||||
const iframeDocument = this.iframe.contentWindow.document;
|
const iframeDocument = this.iframe.contentWindow.document;
|
||||||
|
|
|
@ -38,6 +38,7 @@ class LinkFooter extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
multiColumn: PropTypes.bool,
|
||||||
onLogout: PropTypes.func.isRequired,
|
onLogout: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
@ -53,6 +54,7 @@ class LinkFooter extends PureComponent {
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { signedIn, permissions } = this.context.identity;
|
const { signedIn, permissions } = this.context.identity;
|
||||||
|
const { multiColumn } = this.props;
|
||||||
|
|
||||||
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS);
|
||||||
const canProfileDirectory = profileDirectory;
|
const canProfileDirectory = profileDirectory;
|
||||||
|
@ -64,7 +66,7 @@ class LinkFooter extends PureComponent {
|
||||||
<p>
|
<p>
|
||||||
<strong>{domain}</strong>:
|
<strong>{domain}</strong>:
|
||||||
{' '}
|
{' '}
|
||||||
<Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
<Link to='/about' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
|
||||||
{statusPageUrl && (
|
{statusPageUrl && (
|
||||||
<>
|
<>
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
|
@ -84,7 +86,7 @@ class LinkFooter extends PureComponent {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{DividingCircle}
|
{DividingCircle}
|
||||||
<Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
<Link to='/privacy-policy' target={multiColumn ? '_blank' : undefined}><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Link } from 'react-router-dom';
|
||||||
import { WordmarkLogo } from 'mastodon/components/logo';
|
import { WordmarkLogo } from 'mastodon/components/logo';
|
||||||
import NavigationPortal from 'mastodon/components/navigation_portal';
|
import NavigationPortal from 'mastodon/components/navigation_portal';
|
||||||
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
|
import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
|
||||||
|
import { transientSingleColumn } from 'mastodon/is_mobile';
|
||||||
|
|
||||||
import ColumnLink from './column_link';
|
import ColumnLink from './column_link';
|
||||||
import DisabledAccountBanner from './disabled_account_banner';
|
import DisabledAccountBanner from './disabled_account_banner';
|
||||||
|
@ -29,6 +30,7 @@ const messages = defineMessages({
|
||||||
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
followsAndFollowers: { id: 'navigation_bar.follows_and_followers', defaultMessage: 'Follows and followers' },
|
||||||
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
about: { id: 'navigation_bar.about', defaultMessage: 'About' },
|
||||||
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
search: { id: 'navigation_bar.search', defaultMessage: 'Search' },
|
||||||
|
advancedInterface: { id: 'navigation_bar.advanced_interface', defaultMessage: 'Open in advanced web interface' },
|
||||||
});
|
});
|
||||||
|
|
||||||
class NavigationPanel extends Component {
|
class NavigationPanel extends Component {
|
||||||
|
@ -54,6 +56,12 @@ class NavigationPanel extends Component {
|
||||||
<div className='navigation-panel'>
|
<div className='navigation-panel'>
|
||||||
<div className='navigation-panel__logo'>
|
<div className='navigation-panel__logo'>
|
||||||
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
<Link to='/' className='column-link column-link--logo'><WordmarkLogo /></Link>
|
||||||
|
|
||||||
|
{transientSingleColumn && (
|
||||||
|
<a href={`/deck${location.pathname}`} className='button button--block'>
|
||||||
|
{intl.formatMessage(messages.advancedInterface)}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<hr />
|
<hr />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -126,11 +126,11 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
location: PropTypes.object,
|
location: PropTypes.object,
|
||||||
mobile: PropTypes.bool,
|
singleColumn: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
UNSAFE_componentWillMount () {
|
UNSAFE_componentWillMount () {
|
||||||
if (this.props.mobile) {
|
if (this.props.singleColumn) {
|
||||||
document.body.classList.toggle('layout-single-column', true);
|
document.body.classList.toggle('layout-single-column', true);
|
||||||
document.body.classList.toggle('layout-multiple-columns', false);
|
document.body.classList.toggle('layout-multiple-columns', false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -144,9 +144,9 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
this.node.handleChildrenContentChange();
|
this.node.handleChildrenContentChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prevProps.mobile !== this.props.mobile) {
|
if (prevProps.singleColumn !== this.props.singleColumn) {
|
||||||
document.body.classList.toggle('layout-single-column', this.props.mobile);
|
document.body.classList.toggle('layout-single-column', this.props.singleColumn);
|
||||||
document.body.classList.toggle('layout-multiple-columns', !this.props.mobile);
|
document.body.classList.toggle('layout-multiple-columns', !this.props.singleColumn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -157,16 +157,17 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { children, mobile } = this.props;
|
const { children, singleColumn } = this.props;
|
||||||
const { signedIn } = this.context.identity;
|
const { signedIn } = this.context.identity;
|
||||||
|
const pathName = this.props.location.pathname;
|
||||||
|
|
||||||
let redirect;
|
let redirect;
|
||||||
|
|
||||||
if (signedIn) {
|
if (signedIn) {
|
||||||
if (mobile) {
|
if (singleColumn) {
|
||||||
redirect = <Redirect from='/' to='/home' exact />;
|
redirect = <Redirect from='/' to='/home' exact />;
|
||||||
} else {
|
} else {
|
||||||
redirect = <Redirect from='/' to='/getting-started' exact />;
|
redirect = <Redirect from='/' to='/deck/getting-started' exact />;
|
||||||
}
|
}
|
||||||
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
|
||||||
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
|
||||||
|
@ -177,10 +178,13 @@ class SwitchingColumnsArea extends PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ColumnsAreaContainer ref={this.setRef} singleColumn={mobile}>
|
<ColumnsAreaContainer ref={this.setRef} singleColumn={singleColumn}>
|
||||||
<WrappedSwitch>
|
<WrappedSwitch>
|
||||||
{redirect}
|
{redirect}
|
||||||
|
|
||||||
|
{singleColumn ? <Redirect from='/deck' to='/home' exact /> : null}
|
||||||
|
{singleColumn && pathName.startsWith('/deck/') ? <Redirect from={pathName} to={pathName.slice(5)} /> : null}
|
||||||
|
|
||||||
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
<WrappedRoute path='/getting-started' component={GettingStarted} content={children} />
|
||||||
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
<WrappedRoute path='/keyboard-shortcuts' component={KeyboardShortcuts} content={children} />
|
||||||
<WrappedRoute path='/about' component={About} content={children} />
|
<WrappedRoute path='/about' component={About} content={children} />
|
||||||
|
@ -573,7 +577,7 @@ class UI extends PureComponent {
|
||||||
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef} style={{ pointerEvents: dropdownMenuIsOpen ? 'none' : null }}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|
||||||
<SwitchingColumnsArea location={location} mobile={layout === 'mobile' || layout === 'single-column'}>
|
<SwitchingColumnsArea location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
|
||||||
{children}
|
{children}
|
||||||
</SwitchingColumnsArea>
|
</SwitchingColumnsArea>
|
||||||
|
|
||||||
|
|
|
@ -11,13 +11,21 @@ import BundleContainer from '../containers/bundle_container';
|
||||||
|
|
||||||
// Small wrapper to pass multiColumn to the route components
|
// Small wrapper to pass multiColumn to the route components
|
||||||
export class WrappedSwitch extends PureComponent {
|
export class WrappedSwitch extends PureComponent {
|
||||||
|
static contextTypes = {
|
||||||
|
router: PropTypes.object,
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { multiColumn, children } = this.props;
|
const { multiColumn, children } = this.props;
|
||||||
|
const { location } = this.context.router.route;
|
||||||
|
|
||||||
|
const decklessLocation = multiColumn && location.pathname.startsWith('/deck')
|
||||||
|
? {...location, pathname: location.pathname.slice(5)}
|
||||||
|
: location;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch location={decklessLocation}>
|
||||||
{Children.map(children, child => cloneElement(child, { multiColumn }))}
|
{Children.map(children, child => child ? cloneElement(child, { multiColumn }) : null)}
|
||||||
</Switch>
|
</Switch>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,6 +95,13 @@ const element = document.getElementById('initial-state');
|
||||||
/** @type {InitialState | undefined} */
|
/** @type {InitialState | undefined} */
|
||||||
const initialState = element?.textContent && JSON.parse(element.textContent);
|
const initialState = element?.textContent && JSON.parse(element.textContent);
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
const initialPath = document.querySelector("head meta[name=initialPath]")?.getAttribute("content") ?? '';
|
||||||
|
/** @type {boolean} */
|
||||||
|
export const hasMultiColumnPath = initialPath === '/'
|
||||||
|
|| initialPath === '/getting-started'
|
||||||
|
|| initialPath.startsWith('/deck');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template {keyof InitialStateMeta} K
|
* @template {keyof InitialStateMeta} K
|
||||||
* @param {K} prop
|
* @param {K} prop
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||||
|
|
||||||
import { forceSingleColumn } from './initial_state';
|
import { forceSingleColumn, hasMultiColumnPath } from './initial_state';
|
||||||
|
|
||||||
const LAYOUT_BREAKPOINT = 630;
|
const LAYOUT_BREAKPOINT = 630;
|
||||||
|
|
||||||
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
export const isMobile = (width: number) => width <= LAYOUT_BREAKPOINT;
|
||||||
|
|
||||||
|
export const transientSingleColumn = !forceSingleColumn && !hasMultiColumnPath;
|
||||||
|
|
||||||
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
export type LayoutType = 'mobile' | 'single-column' | 'multi-column';
|
||||||
export const layoutFromWindow = (): LayoutType => {
|
export const layoutFromWindow = (): LayoutType => {
|
||||||
if (isMobile(window.innerWidth)) {
|
if (isMobile(window.innerWidth)) {
|
||||||
return 'mobile';
|
return 'mobile';
|
||||||
} else if (forceSingleColumn) {
|
} else if (!forceSingleColumn && !transientSingleColumn) {
|
||||||
return 'single-column';
|
|
||||||
} else {
|
|
||||||
return 'multi-column';
|
return 'multi-column';
|
||||||
|
} else {
|
||||||
|
return 'single-column';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -385,6 +385,7 @@
|
||||||
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
"mute_modal.hide_notifications": "Hide notifications from this user?",
|
||||||
"mute_modal.indefinite": "Indefinite",
|
"mute_modal.indefinite": "Indefinite",
|
||||||
"navigation_bar.about": "About",
|
"navigation_bar.about": "About",
|
||||||
|
"navigation_bar.advanced_interface": "Open in advanced web interface",
|
||||||
"navigation_bar.blocks": "Blocked users",
|
"navigation_bar.blocks": "Blocked users",
|
||||||
"navigation_bar.bookmarks": "Bookmarks",
|
"navigation_bar.bookmarks": "Bookmarks",
|
||||||
"navigation_bar.community_timeline": "Local timeline",
|
"navigation_bar.community_timeline": "Local timeline",
|
||||||
|
|
|
@ -368,6 +368,7 @@
|
||||||
"mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
|
"mute_modal.hide_notifications": "Masquer les notifications de cette personne ?",
|
||||||
"mute_modal.indefinite": "Indéfinie",
|
"mute_modal.indefinite": "Indéfinie",
|
||||||
"navigation_bar.about": "À propos",
|
"navigation_bar.about": "À propos",
|
||||||
|
"navigation_bar.advanced_interface": "Ouvrir dans l’interface avancée",
|
||||||
"navigation_bar.blocks": "Comptes bloqués",
|
"navigation_bar.blocks": "Comptes bloqués",
|
||||||
"navigation_bar.bookmarks": "Marque-pages",
|
"navigation_bar.bookmarks": "Marque-pages",
|
||||||
"navigation_bar.community_timeline": "Fil public local",
|
"navigation_bar.community_timeline": "Fil public local",
|
||||||
|
|
|
@ -106,7 +106,9 @@ class Status < ApplicationRecord
|
||||||
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
|
||||||
scope :tagged_with_all, lambda { |tag_ids|
|
scope :tagged_with_all, lambda { |tag_ids|
|
||||||
Array(tag_ids).map(&:to_i).reduce(self) do |result, id|
|
Array(tag_ids).map(&:to_i).reduce(self) do |result, id|
|
||||||
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
|
result.where(<<~SQL.squish, tag_id: id)
|
||||||
|
EXISTS(SELECT 1 FROM statuses_tags WHERE statuses_tags.status_id = statuses.id AND statuses_tags.tag_id = :tag_id)
|
||||||
|
SQL
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
scope :tagged_with_none, lambda { |tag_ids|
|
scope :tagged_with_none, lambda { |tag_ids|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
= preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
|
= preload_pack_asset 'features/compose.js', crossorigin: 'anonymous'
|
||||||
= preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
|
= preload_pack_asset 'features/home_timeline.js', crossorigin: 'anonymous'
|
||||||
= preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
|
= preload_pack_asset 'features/notifications.js', crossorigin: 'anonymous'
|
||||||
|
%meta{ name: 'initialPath', content: request.path }
|
||||||
|
|
||||||
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
||||||
|
|
||||||
|
|
|
@ -1,28 +1,5 @@
|
||||||
{
|
{
|
||||||
"ignored_warnings": [
|
"ignored_warnings": [
|
||||||
{
|
|
||||||
"warning_type": "SQL Injection",
|
|
||||||
"warning_code": 0,
|
|
||||||
"fingerprint": "19df3740b8d02a9fe0eb52c939b4b87d3a2a591162a6adfa8d64e9c26aeebe6d",
|
|
||||||
"check_name": "SQL",
|
|
||||||
"message": "Possible SQL injection",
|
|
||||||
"file": "app/models/status.rb",
|
|
||||||
"line": 106,
|
|
||||||
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
|
|
||||||
"code": "result.joins(\"INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
|
|
||||||
"render_path": null,
|
|
||||||
"location": {
|
|
||||||
"type": "method",
|
|
||||||
"class": "Status",
|
|
||||||
"method": null
|
|
||||||
},
|
|
||||||
"user_input": "id",
|
|
||||||
"confidence": "Weak",
|
|
||||||
"cwe_id": [
|
|
||||||
89
|
|
||||||
],
|
|
||||||
"note": ""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"warning_type": "Cross-Site Scripting",
|
"warning_type": "Cross-Site Scripting",
|
||||||
"warning_code": 2,
|
"warning_code": 2,
|
||||||
|
@ -206,6 +183,6 @@
|
||||||
"note": ""
|
"note": ""
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"updated": "2023-07-11 16:08:58 +0200",
|
"updated": "2023-07-12 11:20:51 -0400",
|
||||||
"brakeman_version": "6.0.0"
|
"brakeman_version": "6.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ Rails.application.routes.draw do
|
||||||
/mutes
|
/mutes
|
||||||
/followed_tags
|
/followed_tags
|
||||||
/statuses/(*any)
|
/statuses/(*any)
|
||||||
|
/deck/(*any)
|
||||||
).freeze
|
).freeze
|
||||||
|
|
||||||
root 'home#index'
|
root 'home#index'
|
||||||
|
|
|
@ -298,7 +298,7 @@ namespace :api, format: false do
|
||||||
|
|
||||||
namespace :web do
|
namespace :web do
|
||||||
resource :settings, only: [:update]
|
resource :settings, only: [:update]
|
||||||
resource :embed, only: [:create]
|
resources :embeds, only: [:show]
|
||||||
resources :push_subscriptions, only: [:create] do
|
resources :push_subscriptions, only: [:create] do
|
||||||
member do
|
member do
|
||||||
put :update
|
put :update
|
||||||
|
|
|
@ -109,6 +109,7 @@
|
||||||
"react-overlays": "^5.2.1",
|
"react-overlays": "^5.2.1",
|
||||||
"react-redux": "^8.0.4",
|
"react-redux": "^8.0.4",
|
||||||
"react-redux-loading-bar": "^5.0.4",
|
"react-redux-loading-bar": "^5.0.4",
|
||||||
|
"react-router": "^4.3.1",
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
"react-router-scroll-4": "^1.0.0-beta.1",
|
"react-router-scroll-4": "^1.0.0-beta.1",
|
||||||
"react-select": "^5.7.3",
|
"react-select": "^5.7.3",
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe Api::Web::EmbedsController do
|
|
||||||
render_views
|
|
||||||
|
|
||||||
let(:user) { Fabricate(:user) }
|
|
||||||
|
|
||||||
before { sign_in user }
|
|
||||||
|
|
||||||
describe 'POST #create' do
|
|
||||||
subject(:body) { JSON.parse(response.body, symbolize_names: true) }
|
|
||||||
|
|
||||||
let(:response) { post :create, params: { url: url } }
|
|
||||||
|
|
||||||
context 'when successfully finds status' do
|
|
||||||
let(:status) { Fabricate(:status) }
|
|
||||||
let(:url) { "http://#{Rails.configuration.x.web_domain}/@#{status.account.username}/#{status.id}" }
|
|
||||||
|
|
||||||
it 'returns a right response' do
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
expect(body[:author_name]).to eq status.account.username
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when fails to find status' do
|
|
||||||
let(:url) { 'https://host.test/oembed.html' }
|
|
||||||
let(:service_instance) { instance_double(FetchOEmbedService) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
allow(FetchOEmbedService).to receive(:new) { service_instance }
|
|
||||||
allow(service_instance).to receive(:call) { call_result }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when successfully fetching oembed' do
|
|
||||||
let(:call_result) { { result: :ok } }
|
|
||||||
|
|
||||||
it 'returns a right response' do
|
|
||||||
expect(response).to have_http_status 200
|
|
||||||
expect(body[:result]).to eq 'ok'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when fails to fetch oembed' do
|
|
||||||
let(:call_result) { nil }
|
|
||||||
|
|
||||||
it 'returns a right response' do
|
|
||||||
expect(response).to have_http_status 404
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
161
spec/requests/api/web/embeds_spec.rb
Normal file
161
spec/requests/api/web/embeds_spec.rb
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe '/api/web/embed' do
|
||||||
|
subject { get "/api/web/embeds/#{id}", headers: headers }
|
||||||
|
|
||||||
|
context 'when accessed anonymously' do
|
||||||
|
let(:headers) { {} }
|
||||||
|
|
||||||
|
context 'when the requested status is local' do
|
||||||
|
let(:id) { status.id }
|
||||||
|
|
||||||
|
context 'when the requested status is public' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :public) }
|
||||||
|
|
||||||
|
it 'returns JSON with an html attribute' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json[:html]).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requested status is private' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requested status is remote' do
|
||||||
|
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:status) { Fabricate(:status, visibility: :public, account: remote_account, url: 'https://example.com/statuses/1') }
|
||||||
|
let(:id) { status.id }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requested status does not exist' do
|
||||||
|
let(:id) { -1 }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an API token' do
|
||||||
|
let(:user) { Fabricate(:user) }
|
||||||
|
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: 'read') }
|
||||||
|
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||||
|
|
||||||
|
context 'when the requested status is local' do
|
||||||
|
let(:id) { status.id }
|
||||||
|
|
||||||
|
context 'when the requested status is public' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :public) }
|
||||||
|
|
||||||
|
it 'returns JSON with an html attribute' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json[:html]).to be_present
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requesting user is blocked' do
|
||||||
|
before do
|
||||||
|
status.account.block!(user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requested status is private' do
|
||||||
|
let(:status) { Fabricate(:status, visibility: :private) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
user.account.follow!(status.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requested status is remote' do
|
||||||
|
let(:remote_account) { Fabricate(:account, domain: 'example.com') }
|
||||||
|
let(:status) { Fabricate(:status, visibility: :public, account: remote_account, url: 'https://example.com/statuses/1') }
|
||||||
|
let(:id) { status.id }
|
||||||
|
|
||||||
|
let(:service_instance) { instance_double(FetchOEmbedService) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(FetchOEmbedService).to receive(:new) { service_instance }
|
||||||
|
allow(service_instance).to receive(:call) { call_result }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requesting user is blocked' do
|
||||||
|
before do
|
||||||
|
status.account.block!(user.account)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when successfully fetching OEmbed' do
|
||||||
|
let(:call_result) { { html: 'ok' } }
|
||||||
|
|
||||||
|
it 'returns JSON with an html attribute' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(200)
|
||||||
|
expect(body_as_json[:html]).to be_present
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when failing to fetch OEmbed' do
|
||||||
|
let(:call_result) { nil }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the requested status does not exist' do
|
||||||
|
let(:id) { -1 }
|
||||||
|
|
||||||
|
it 'returns http not found' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(response).to have_http_status(404)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue