Akkoma patches

don't replace my history

re-add CI

lint stuff

upload to $build-tag

change SW path

use default SW

fix notifications streams

use akkoma hashtag schema

add local intl
This commit is contained in:
FloatingGhost 2022-12-08 14:32:45 +00:00
parent 859db01268
commit 076f7534db
74 changed files with 456 additions and 221 deletions

1
.gitignore vendored
View File

@ -6,6 +6,7 @@
# Ignore bundler config and downloaded libraries.
/.bundle
distribution
/vendor/bundle
# Ignore the default SQLite database.

30
.woodpecker.yml Normal file
View File

@ -0,0 +1,30 @@
pipeline:
build:
when:
event:
- tag
- push
image: node:16
commands:
- yarn
- TARGET=distribution ./build.sh
release:
when:
event:
- tag
- push
image: node:16
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
commands:
- apt-get update && apt-get install -y rclone wget zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- export BUILD_TAG=$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}
- zip mastofe.zip -r distribution
- rclone copyto mastofe.zip scaleway:akkoma-updates/frontend/$BUILD_TAG/masto-fe.zip

View File

@ -2,7 +2,9 @@
import 'packs/public-path';
import escapeTextContentForBrowser from 'escape-html';
const { delegate } = require('@rails/ujs');
import emojify from '../mastodon/features/emoji/emoji';
delegate(document, '#account_display_name', 'input', ({ target }) => {
@ -65,7 +67,7 @@ delegate(document, '.input-copy button', 'click', ({ target }) => {
input.blur();
target.parentNode.classList.add('copied');
setTimeout(() => {
setTimeout(() => {
target.parentNode.classList.remove('copied');
}, 700);
}

View File

@ -811,7 +811,7 @@ export function fetchPinnedAccounts() {
return (dispatch, getState) => {
dispatch(fetchPinnedAccountsRequest());
api(getState).get(`/api/v1/endorsements`, { params: { limit: 0 } }).then(response => {
api(getState).get('/api/v1/endorsements', { params: { limit: 0 } }).then(response => {
dispatch(importFetchedAccounts(response.data));
dispatch(fetchPinnedAccountsSuccess(response.data));
}).catch(err => dispatch(fetchPinnedAccountsFail(err)));
@ -873,7 +873,7 @@ export function changePinnedAccountsSuggestions(value) {
return {
type: PINNED_ACCOUNTS_EDITOR_SUGGESTIONS_CHANGE,
value,
}
};
};
export function resetPinnedAccountsEditor() {

View File

@ -11,7 +11,7 @@ export function initBoostModal(props) {
dispatch({
type: BOOSTS_INIT_MODAL,
privacy
privacy,
});
dispatch(openModal('BOOST', props));

View File

@ -18,7 +18,10 @@ const urlBase64ToUint8Array = (base64String) => {
return outputArray;
};
const getApplicationServerKey = () => document.querySelector('[name="applicationServerKey"]').getAttribute('content');
const getApplicationServerKey = () => {
const k = document.querySelector('[name="applicationServerKey"]');
return k === null ? '' : k.getAttribute('content');
};
const getRegistration = () => navigator.serviceWorker.ready;

View File

@ -18,7 +18,7 @@ const applyMigrations = (state) => {
if (state.getIn(['settings', 'notifications', 'showUnread']) !== false) {
state.setIn(['settings', 'notifications', 'showUnread'], state.getIn(['local_settings', 'notifications', 'show_unread']));
}
state.removeIn(['local_settings', 'notifications', 'show_unread'])
state.removeIn(['local_settings', 'notifications', 'show_unread']);
}
});
};

View File

@ -80,6 +80,7 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti
},
onReceive (data) {
console.log("recv", timelineId, data)
switch(data.event) {
case 'update':
dispatch(updateTimeline(timelineId, JSON.parse(data.payload), options.accept));

View File

@ -27,6 +27,7 @@ export const loadPending = timeline => ({
});
export function updateTimeline(timeline, status, accept) {
console.log("UPDATE", timeline, status);
return (dispatch, getState) => {
if (typeof accept === 'function' && !accept(status)) {
return;
@ -55,7 +56,7 @@ export function updateTimeline(timeline, status, accept) {
timeline,
status,
usePendingItems: preferPendingItems,
filtered
filtered,
});
if (timeline === 'home') {

View File

@ -74,7 +74,7 @@ export default class DisplayName extends React.PureComponent {
)).reduce((prev, cur) => [prev, ', ', cur]);
if (others.size - 2 > 0) {
displayName.push(` +${others.size - 2}`);
displayName.push(` +${others.size - 2}`);
}
suffix = (

View File

@ -55,8 +55,9 @@ export const ImmutableHashtag = ({ hashtag }) => (
name={hashtag.get('name')}
href={hashtag.get('url')}
to={`/tags/${hashtag.get('name')}`}
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
people={0}
uses={0}
history={[]}
/>
);

View File

@ -121,8 +121,9 @@ export default class IntersectionObserverArticle extends React.Component {
aria-setsize={listLength}
data-id={id}
tabIndex='0'
style={style}>
{children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })}
style={style}
>
{children && React.cloneElement(children, { hidden: !isIntersecting && (isHidden || !!cachedHeight) })}
</article>
);
}

View File

@ -323,7 +323,7 @@ class MediaGallery extends React.PureComponent {
_setDimensions () {
const width = this.node.offsetWidth;
if (width && width != this.state.width) {
// offsetWidth triggers a layout, so only calculate when we need to
if (this.props.cacheWidth) {
@ -360,7 +360,7 @@ class MediaGallery extends React.PureComponent {
} else if (width) {
style.height = width / (16/9);
} else {
return (<div className={computedClass} ref={this.handleRef}></div>);
return (<div className={computedClass} ref={this.handleRef} />);
}
if (this.isStandaloneEligible()) {

View File

@ -5,6 +5,7 @@ import { createBrowserHistory } from 'history';
import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};

View File

@ -24,7 +24,7 @@ export default class Permalink extends React.PureComponent {
if (this.context.router) {
e.preventDefault();
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(this.props.to, state);
}

View File

@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
export default
class Spoilers extends React.PureComponent {
static propTypes = {
spoilerText: PropTypes.string,
children: PropTypes.node,
@ -21,17 +22,17 @@ class Spoilers extends React.PureComponent {
const { spoilerText, children } = this.props;
const { hidden } = this.state;
const toggleText = hidden ?
<FormattedMessage
id='status.show_more'
defaultMessage='Show more'
key='0'
/> :
<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
/>;
const toggleText = hidden ?
(<FormattedMessage
id='status.show_more'
defaultMessage='Show more'
key='0'
/>) :
(<FormattedMessage
id='status.show_less'
defaultMessage='Show less'
key='0'
/>);
return ([
<p className='spoiler__text'>
@ -43,8 +44,9 @@ class Spoilers extends React.PureComponent {
</p>,
<div className={`status__content__spoiler ${!hidden ? 'status__content__spoiler--visible' : ''}`}>
{children}
</div>
</div>,
]);
}
}

View File

@ -54,7 +54,7 @@ export const defaultMediaVisibility = (status, settings) => {
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
}
};
export default @injectIntl
class Status extends ImmutablePureComponent {
@ -297,7 +297,9 @@ class Status extends ImmutablePureComponent {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => { this.props.updateScrollBottom(position.height - position.top); });
requestAnimationFrame(() => {
this.props.updateScrollBottom(position.height - position.top);
});
}
}
}
@ -356,7 +358,7 @@ class Status extends ImmutablePureComponent {
status.getIn(['reblog', 'id'], status.get('id'))
}`;
}
let state = {...router.history.location.state};
let state = { ...router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
router.history.push(destination, state);
}
@ -429,14 +431,14 @@ class Status extends ImmutablePureComponent {
}
handleHotkeyOpen = () => {
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
const status = this.props.status;
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`, state);
}
handleHotkeyOpenProfile = () => {
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
}

View File

@ -161,7 +161,7 @@ class StatusActionBar extends ImmutablePureComponent {
}
handleOpen = () => {
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
if (state.mastodonModalKey) {
this.context.router.history.replace(`/@${this.props.status.getIn(['account', 'acct'])}/${this.props.status.get('id')}`, { mastodonBackSteps: (state.mastodonBackSteps || 0) + 1 });
} else {

View File

@ -12,7 +12,7 @@ const textMatchesTarget = (text, origin, host) => {
return (text === origin || text === host
|| text.startsWith(origin + '/') || text.startsWith(host + '/')
|| 'www.' + text === host || ('www.' + text).startsWith(host + '/'));
}
};
const isLinkMisleading = (link) => {
let linkTextParts = [];

View File

@ -66,16 +66,16 @@ class StatusIcons extends React.PureComponent {
const { intl } = this.props;
switch (mediaIcon) {
case 'link':
return intl.formatMessage(messages.previewCard);
case 'picture-o':
return intl.formatMessage(messages.pictures);
case 'tasks':
return intl.formatMessage(messages.poll);
case 'video-camera':
return intl.formatMessage(messages.video);
case 'music':
return intl.formatMessage(messages.audio);
case 'link':
return intl.formatMessage(messages.previewCard);
case 'picture-o':
return intl.formatMessage(messages.pictures);
case 'tasks':
return intl.formatMessage(messages.poll);
case 'video-camera':
return intl.formatMessage(messages.video);
case 'music':
return intl.formatMessage(messages.audio);
}
}

View File

@ -10,6 +10,7 @@ const messages = defineMessages({
unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' },
private: { id: 'privacy.private.short', defaultMessage: 'Followers-only' },
direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' },
local: { id: 'privacy.local.short', defaultMessage: 'Local users only' },
});
export default @injectIntl
@ -29,6 +30,7 @@ class VisibilityIcon extends ImmutablePureComponent {
unlisted: 'unlock',
private: 'lock',
direct: 'envelope',
local: 'lock',
}[visibility];
const label = intl.formatMessage(messages[visibility]);

View File

@ -35,7 +35,7 @@ const createIdentityContext = state => ({
accountId: state.meta.me,
disabledAccountId: state.meta.disabled_account_id,
accessToken: state.meta.access_token,
permissions: state.role ? state.role.permissions : 0,
permissions: [],
});
export default class Mastodon extends React.PureComponent {

View File

@ -32,7 +32,7 @@ class ActionBar extends React.PureComponent {
<div className='account__disclaimer'>
<Icon id='info-circle' fixedWidth /> <FormattedMessage
id='account.suspended_disclaimer_full'
defaultMessage="This user has been suspended by a moderator."
defaultMessage='This user has been suspended by a moderator.'
/>
</div>
</div>

View File

@ -174,8 +174,7 @@ class Header extends ImmutablePureComponent {
if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) {
info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>);
}
else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
} else if (me !== account.get('id') && account.getIn(['relationship', 'blocking'])) {
info.push(<span className='relationship-tag'><FormattedMessage id='account.blocked' defaultMessage='Blocked' /></span>);
}
@ -359,7 +358,7 @@ class Header extends ImmutablePureComponent {
{fields.map((pair, i) => (
<dl key={i}>
<dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} />
<dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}>
{pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} className='translate' />
</dd>

View File

@ -21,7 +21,7 @@ export default class MovedNote extends ImmutablePureComponent {
handleAccountClick = e => {
if (e.button === 0) {
e.preventDefault();
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.to.get('acct')}`, state);
}

View File

@ -12,7 +12,7 @@ import {
} from 'flavours/glitch/actions/accounts';
import {
mentionCompose,
directCompose
directCompose,
} from 'flavours/glitch/actions/compose';
import { initMuteModal } from 'flavours/glitch/actions/mutes';
import { initBlockModal } from 'flavours/glitch/actions/blocks';

View File

@ -126,7 +126,7 @@ class Audio extends React.PureComponent {
this.visualizer.setCanvas(c);
}
componentDidMount () {
window.addEventListener('scroll', this.handleScroll);
window.addEventListener('resize', this.handleResize, { passive: true });

View File

@ -12,7 +12,7 @@ const mapStateToProps = (state, { columnId }) => {
settings: (uuid && index >= 0) ? columns.get(index).get('params') : state.getIn(['settings', 'community']),
};
};
const mapDispatchToProps = (dispatch, { columnId }) => {
return {
onChange (key, checked) {

View File

@ -22,9 +22,9 @@ import { length } from 'stringz';
const messages = defineMessages({
placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' },
missingDescriptionMessage: { id: 'confirmations.missing_media_description.message',
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm',
defaultMessage: 'Send anyway' },
defaultMessage: 'Send anyway' },
spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' },
});
@ -88,7 +88,7 @@ class ComposeForm extends ImmutablePureComponent {
return [
this.props.spoiler? this.props.spoilerText: '',
countableText(this.props.text),
this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : ''
this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : '',
].join('');
}
@ -217,60 +217,60 @@ class ComposeForm extends ImmutablePureComponent {
// the first; this provides a convenient shortcut to drop
// everyone else from the conversation.
_updateFocusAndSelection = (prevProps) => {
const {
textarea,
spoilerText,
} = this;
const {
focusDate,
caretPosition,
isSubmitting,
preselectDate,
text,
preselectOnReply,
singleColumn,
} = this.props;
let selectionEnd, selectionStart;
const {
textarea,
spoilerText,
} = this;
const {
focusDate,
caretPosition,
isSubmitting,
preselectDate,
text,
preselectOnReply,
singleColumn,
} = this.props;
let selectionEnd, selectionStart;
// Caret/selection handling.
if (focusDate !== prevProps.focusDate) {
switch (true) {
case preselectDate !== prevProps.preselectDate && this.props.isInReply && preselectOnReply:
selectionStart = text.search(/\s/) + 1;
selectionEnd = text.length;
break;
case !isNaN(caretPosition) && caretPosition !== null:
selectionStart = selectionEnd = caretPosition;
break;
default:
selectionStart = selectionEnd = text.length;
}
if (textarea) {
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
textarea.setSelectionRange(selectionStart, selectionEnd);
textarea.focus();
if (!singleColumn) textarea.scrollIntoView();
}).catch(console.error);
}
// Caret/selection handling.
if (focusDate !== prevProps.focusDate) {
switch (true) {
case preselectDate !== prevProps.preselectDate && this.props.isInReply && preselectOnReply:
selectionStart = text.search(/\s/) + 1;
selectionEnd = text.length;
break;
case !isNaN(caretPosition) && caretPosition !== null:
selectionStart = selectionEnd = caretPosition;
break;
default:
selectionStart = selectionEnd = text.length;
}
if (textarea) {
// Because of the wicg-inert polyfill, the activeElement may not be
// immediately selectable, we have to wait for observers to run, as
// described in https://github.com/WICG/inert#performance-and-gotchas
Promise.resolve().then(() => {
textarea.setSelectionRange(selectionStart, selectionEnd);
textarea.focus();
if (!singleColumn) textarea.scrollIntoView();
}).catch(console.error);
}
// Refocuses the textarea after submitting.
} else if (textarea && prevProps.isSubmitting && !isSubmitting) {
textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) {
if (spoilerText) {
spoilerText.focus();
}
} else {
if (textarea) {
textarea.focus();
}
}
}
}
// Refocuses the textarea after submitting.
} else if (textarea && prevProps.isSubmitting && !isSubmitting) {
textarea.focus();
} else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) {
if (spoilerText) {
spoilerText.focus();
}
} else {
if (textarea) {
textarea.focus();
}
}
}
}
render () {
@ -302,13 +302,12 @@ class ComposeForm extends ImmutablePureComponent {
isEditing,
} = this.props;
const countText = this.getFulltextForCharacterCounting();
const countText = this.getFulltextForCharacterCounting();
return (
<div className='compose-form'>
<WarningContainer />
<ReplyIndicatorContainer />
<ReplyIndicatorContainer />
<div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
<AutosuggestInput
@ -367,17 +366,17 @@ class ComposeForm extends ImmutablePureComponent {
</div>
</div>
<Publisher
countText={countText}
disabled={!this.canSubmit()}
isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
</div>
);
}
<Publisher
countText={countText}
disabled={!this.canSubmit()}
isEditing={isEditing}
onSecondarySubmit={handleSecondarySubmit}
onSubmit={handleSubmit}
privacy={privacy}
sideArm={sideArm}
/>
</div>
);
}
}

View File

@ -153,7 +153,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
...rest,
active: value && name === value,
name,
})
}),
),
};
}

View File

@ -47,6 +47,7 @@ const messages = defineMessages({
export default @injectIntl
class Header extends ImmutablePureComponent {
static propTypes = {
columns: ImmutablePropTypes.list,
unreadNotifications: PropTypes.number,
@ -71,8 +72,8 @@ class Header extends ImmutablePureComponent {
// Only renders the component if the column isn't being shown.
const renderForColumn = conditionalRender.bind(null,
columnId => !columns || !columns.some(
column => column.get('id') === columnId
)
column => column.get('id') === columnId,
),
);
// The result.
@ -125,10 +126,11 @@ class Header extends ImmutablePureComponent {
<a
aria-label={intl.formatMessage(messages.logout)}
onClick={this.handleLogoutClick}
href={ signOutLink }
href={signOutLink}
title={intl.formatMessage(messages.logout)}
><Icon id='sign-out' /></a>
</nav>
);
};
}

View File

@ -42,5 +42,4 @@ export default class NavigationBar extends ImmutablePureComponent {
</div>
);
}
}

View File

@ -95,4 +95,5 @@ class Publisher extends ImmutablePureComponent {
</div>
);
};
}

View File

@ -103,7 +103,7 @@ class SearchResults extends ImmutablePureComponent {
<section className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Posts' /></h5>
{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId}/>)}
{results.get('statuses').map(statusId => <StatusContainer id={statusId} key={statusId} />)}
{results.get('statuses').size >= 5 && <LoadMore visible onClick={this.handleLoadMoreStatuses} />}
</section>
@ -137,4 +137,5 @@ class SearchResults extends ImmutablePureComponent {
</div>
);
};
}

View File

@ -51,9 +51,10 @@ class TextareaIcons extends ImmutablePureComponent {
id={icon}
/>
</span>
) : null
) : null,
) : null}
</div>
);
}
}

View File

@ -6,6 +6,7 @@ import UploadContainer from '../containers/upload_container';
import SensitiveButtonContainer from '../containers/sensitive_button_container';
export default class UploadForm extends ImmutablePureComponent {
static propTypes = {
mediaIds: ImmutablePropTypes.list.isRequired,
};

View File

@ -22,11 +22,11 @@ import { privacyPreference } from 'flavours/glitch/utils/privacy_preference';
const messages = defineMessages({
missingDescriptionMessage: { id: 'confirmations.missing_media_description.message',
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' },
missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm',
defaultMessage: 'Send anyway' },
defaultMessage: 'Send anyway' },
missingDescriptionEdit: { id: 'confirmations.missing_media_description.edit',
defaultMessage: 'Edit media' },
defaultMessage: 'Edit media' },
});
// State mapping.
@ -38,12 +38,12 @@ function mapStateToProps (state) {
const sideArmRestrictedPrivacy = replyPrivacy ? privacyPreference(replyPrivacy, sideArmBasePrivacy) : null;
let sideArmPrivacy = null;
switch (state.getIn(['local_settings', 'side_arm_reply_mode'])) {
case 'copy':
sideArmPrivacy = replyPrivacy;
break;
case 'restrict':
sideArmPrivacy = sideArmRestrictedPrivacy;
break;
case 'copy':
sideArmPrivacy = replyPrivacy;
break;
case 'restrict':
sideArmPrivacy = sideArmRestrictedPrivacy;
break;
}
sideArmPrivacy = sideArmPrivacy || sideArmBasePrivacy;
return {

View File

@ -8,7 +8,7 @@ import { profileLink, termsLink } from 'flavours/glitch/utils/backend_links';
const buildHashtagRE = () => {
try {
const HASHTAG_SEPARATORS = "_\\u00b7\\u200c";
const HASHTAG_SEPARATORS = '_\\u00b7\\u200c';
const ALPHA = '\\p{L}\\p{M}';
const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}';
return new RegExp(
@ -22,7 +22,7 @@ const buildHashtagRE = () => {
'[' + WORD + '_]*' +
'[' + ALPHA + ']' +
'[' + WORD + '_]*' +
'))', 'iu'
'))', 'iu',
);
} catch {
return /(?:^|[^\/\)\w])#(\w*[a-zA-Z·]\w*)/i;

View File

@ -43,6 +43,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
export default @connect(mapStateToProps, mapDispatchToProps)
@injectIntl
class Compose extends React.PureComponent {
static propTypes = {
multiColumn: PropTypes.bool,
showSearch: PropTypes.bool,

View File

@ -60,7 +60,7 @@ class Conversation extends ImmutablePureComponent {
}
destination = `/statuses/${lastStatus.get('id')}`;
}
let state = {...router.history.location.state};
let state = { ...router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
router.history.push(destination, state);
e.preventDefault();

View File

@ -79,9 +79,9 @@ const badgeDisplay = (number, limit) => {
const NAVIGATION_PANEL_BREAKPOINT = 600 + (285 * 2) + (10 * 2);
export default @connect(makeMapStateToProps, mapDispatchToProps)
export default @connect(makeMapStateToProps, mapDispatchToProps)
@injectIntl
class GettingStarted extends ImmutablePureComponent {
class GettingStarted extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object.isRequired,

View File

@ -60,7 +60,7 @@ class LocalSettingsPage extends React.PureComponent {
onChange={onChange}
>
<FormattedMessage id='settings.hicolor_privacy_icons' defaultMessage='High color privacy icons' />
<span className='hint'><FormattedMessage id='settings.hicolor_privacy_icons.hint' defaultMessage="Display privacy icons in bright and easily distinguishable colors" /></span>
<span className='hint'><FormattedMessage id='settings.hicolor_privacy_icons.hint' defaultMessage='Display privacy icons in bright and easily distinguishable colors' /></span>
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
@ -77,7 +77,7 @@ class LocalSettingsPage extends React.PureComponent {
onChange={onChange}
>
<FormattedMessage id='settings.tag_misleading_links' defaultMessage='Tag misleading links' />
<span className='hint'><FormattedMessage id='settings.tag_misleading_links.hint' defaultMessage="Add a visual indication with the link target host to every link not mentioning it explicitly" /></span>
<span className='hint'><FormattedMessage id='settings.tag_misleading_links.hint' defaultMessage='Add a visual indication with the link target host to every link not mentioning it explicitly' /></span>
</LocalSettingsPageItem>
<LocalSettingsPageItem
settings={settings}
@ -100,7 +100,7 @@ class LocalSettingsPage extends React.PureComponent {
id='mastodon-settings--notifications-tab_badge'
onChange={onChange}
>
<FormattedMessage id='settings.notifications.tab_badge' defaultMessage="Unread notifications badge" />
<FormattedMessage id='settings.notifications.tab_badge' defaultMessage='Unread notifications badge' />
<span className='hint'><FormattedMessage id='settings.notifications.tab_badge.hint' defaultMessage="Display a badge for unread notifications in the column icons when the notifications column isn't open" /></span>
</LocalSettingsPageItem>
<LocalSettingsPageItem
@ -110,7 +110,7 @@ class LocalSettingsPage extends React.PureComponent {
onChange={onChange}
>
<FormattedMessage id='settings.notifications.favicon_badge' defaultMessage='Unread notifications favicon badge' />
<span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage="Add a badge for unread notifications to the favicon" /></span>
<span className='hint'><FormattedMessage id='settings.notifications.favicon_badge.hint' defaultMessage='Add a badge for unread notifications to the favicon' /></span>
</LocalSettingsPageItem>
</section>

View File

@ -54,13 +54,14 @@ export default class LocalSettingsPageItem extends React.PureComponent {
let optionId = `${id}--${opt.value}`;
return (
<label htmlFor={optionId}>
<input type='radio'
<input
type='radio'
name={id}
id={optionId}
value={opt.value}
onBlur={handleChange}
onChange={handleChange}
checked={ currentValue === opt.value }
checked={currentValue === opt.value}
disabled={!enabled}
/>
{opt.message}

View File

@ -1,7 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import classNames from 'classnames'
import classNames from 'classnames';
export default class PillBarButton extends React.PureComponent {

View File

@ -4,7 +4,7 @@ import { injectIntl } from 'react-intl';
import {
fetchPinnedAccountsSuggestions,
clearPinnedAccountsSuggestions,
changePinnedAccountsSuggestions
changePinnedAccountsSuggestions,
} from '../../../actions/accounts';
import Search from 'flavours/glitch/features/list_editor/components/search';

View File

@ -2,7 +2,7 @@ import { connect } from 'react-redux';
import ColumnSettings from '../components/column_settings';
import { changeSetting } from 'flavours/glitch/actions/settings';
import { changeColumnParams } from 'flavours/glitch/actions/columns';
const mapStateToProps = (state, { columnId }) => {
const uuid = columnId;
const columns = state.getIn(['settings', 'columns']);

View File

@ -0,0 +1,100 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { expandPublicTimeline, expandCommunityTimeline } from 'flavours/glitch/actions/timelines';
import Masonry from 'react-masonry-infinite';
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import DetailedStatusContainer from 'flavours/glitch/features/status/containers/detailed_status_container';
import { debounce } from 'lodash';
import LoadingIndicator from 'flavours/glitch/components/loading_indicator';
const mapStateToProps = (state, { local }) => {
const timeline = state.getIn(['timelines', local ? 'community' : 'public'], ImmutableMap());
return {
statusIds: timeline.get('items', ImmutableList()),
isLoading: timeline.get('isLoading', false),
hasMore: timeline.get('hasMore', false),
};
};
export default @connect(mapStateToProps)
class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list.isRequired,
isLoading: PropTypes.bool.isRequired,
hasMore: PropTypes.bool.isRequired,
local: PropTypes.bool,
};
componentDidMount () {
this._connect();
}
componentDidUpdate (prevProps) {
if (prevProps.local !== this.props.local) {
this._disconnect();
this._connect();
}
}
_connect () {
const { dispatch, local } = this.props;
dispatch(local ? expandCommunityTimeline() : expandPublicTimeline());
}
handleLoadMore = () => {
const { dispatch, statusIds, local } = this.props;
const maxId = statusIds.last();
if (maxId) {
dispatch(local ? expandCommunityTimeline({ maxId }) : expandPublicTimeline({ maxId }));
}
}
setRef = c => {
this.masonry = c;
}
handleHeightChange = debounce(() => {
if (!this.masonry) {
return;
}
this.masonry.forcePack();
}, 50)
render () {
const { statusIds, hasMore, isLoading } = this.props;
const sizes = [
{ columns: 1, gutter: 0 },
{ mq: '415px', columns: 1, gutter: 10 },
{ mq: '640px', columns: 2, gutter: 10 },
{ mq: '960px', columns: 3, gutter: 10 },
{ mq: '1255px', columns: 3, gutter: 10 },
];
const loader = (isLoading && statusIds.isEmpty()) ? <LoadingIndicator key={0} /> : undefined;
return (
<Masonry ref={this.setRef} className='statuses-grid' hasMore={hasMore} loadMore={this.handleLoadMore} sizes={sizes} loader={loader}>
{statusIds.map(statusId => (
<div className='statuses-grid__item' key={statusId}>
<DetailedStatusContainer
id={statusId}
compact
measureHeight
onHeightChange={this.handleHeightChange}
/>
</div>
)).toArray()}
</Masonry>
);
}
}

View File

@ -53,7 +53,7 @@ class DetailedStatus extends ImmutablePureComponent {
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.context.router) {
e.preventDefault();
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
}
@ -64,7 +64,7 @@ class DetailedStatus extends ImmutablePureComponent {
parseClick = (e, destination) => {
if (e.button === 0 && !(e.ctrlKey || e.altKey || e.metaKey) && this.context.router) {
e.preventDefault();
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(destination, state);
}

View File

@ -498,7 +498,7 @@ class Status extends ImmutablePureComponent {
}
handleHotkeyOpenProfile = () => {
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
}

View File

@ -64,7 +64,7 @@ class BoostModal extends ImmutablePureComponent {
if (e.button === 0) {
e.preventDefault();
this.props.onClose();
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
}
@ -116,9 +116,9 @@ class BoostModal extends ImmutablePureComponent {
<div className='boost-modal__action-bar'>
<div>
{ missingMediaDescription ?
<FormattedMessage id='boost_modal.missing_description' defaultMessage='This toot contains some media without description' />
<FormattedMessage id='boost_modal.missing_description' defaultMessage='This toot contains some media without description' />
:
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
<FormattedMessage id='boost_modal.combo' defaultMessage='You can press {combo} to skip this next time' values={{ combo: <span>Shift + <Icon id='retweet' /></span> }} />
}
</div>

View File

@ -30,7 +30,7 @@ const ColumnLink = ({ icon, text, to, onClick, href, method, badge, transparent,
e.preventDefault();
e.stopPropagation();
return onClick(e);
}
};
return (
<a href='#' onClick={onClick && handleOnClick} className={className} title={text} {...other} tabIndex='0'>
{iconElement}

View File

@ -575,7 +575,7 @@ class DoodleModal extends ImmutablePureComponent {
<div>
<select aria-label='Canvas size' onInput={this.changeSize} defaultValue={this.size}>
{ Object.values(mapValues(DOODLE_SIZES, (val, k) =>
<option key={k} value={k}>{val[2]}</option>
<option key={k} value={k}>{val[2]}</option>,
)) }
</select>
</div>
@ -602,7 +602,7 @@ class DoodleModal extends ImmutablePureComponent {
'foreground': this.fg === c[0],
'background': this.bg === c[0],
})}
/>
/>,
)
}
</div>

View File

@ -44,7 +44,7 @@ class FavouriteModal extends ImmutablePureComponent {
if (e.button === 0) {
e.preventDefault();
this.props.onClose();
let state = {...this.context.router.history.location.state};
let state = { ...this.context.router.history.location.state };
state.mastodonBackSteps = (state.mastodonBackSteps || 0) + 1;
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`, state);
}

View File

@ -63,8 +63,8 @@ class NavigationPanel extends React.Component {
{(signedIn || timelinePreview) && (
<>
<ColumnLink transparent to='/public/local' icon='users' text={intl.formatMessage(messages.local)} />
<ColumnLink transparent exact to='/public' icon='globe' text={intl.formatMessage(messages.federated)} />
<ColumnLink transparent to='/web/public/local' icon='users' text={intl.formatMessage(messages.local)} />
<ColumnLink transparent exact to='/web/public' icon='globe' text={intl.formatMessage(messages.federated)} />
</>
)}

View File

@ -49,7 +49,7 @@ const PageTwo = ({ intl, myAccount }) => (
privacy='public'
text='Awoo! #introductions'
spoilerText=''
suggestions={ [] }
suggestions={[]}
/>
</div>
</div>

View File

@ -405,7 +405,7 @@ class UI extends React.Component {
navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage);
}
this.favicon = new Favico({ animation:"none" });
this.favicon = new Favico({ animation:'none' });
// On first launch, redirect to the follow recommendations page
if (signedIn && this.props.firstLaunch) {
@ -659,7 +659,7 @@ class UI extends React.Component {
<PermaLink href={moved.get('url')} to={`/@${moved.get('acct')}`}>
@{moved.get('acct')}
</PermaLink>
)}}
) }}
/>
</div>)}

View File

@ -552,7 +552,7 @@ class Video extends React.PureComponent {
playerStyle.height = height;
} else if (inline) {
return (<div className={computedClass} ref={this.setPlayerRef} tabindex={0}></div>);
return (<div className={computedClass} ref={this.setPlayerRef} tabindex={0} />);
}
let preload;

View File

@ -67,13 +67,13 @@ const messages = {
'advanced_options.threaded_mode.short': 'Tryb wątków',
'advanced_options.threaded_mode.long': 'Przechodzi do tworzenia odpowiedzi po publikacji wpisu',
'advanced_options.threaded_mode.tooltip': 'Włączono tryb wątków',
'column.bookmarks': 'Zakładki',
'compose_form.sensitive': 'Oznacz zawartość multimedialną jako wrażliwą',
'compose_form.spoiler': 'Ukryj tekst za ostrzeżeniem',
'favourite_modal.combo': 'Możesz nacisnąć {combo}, aby pominąć to następnym razem',
'tabs_bar.compose': 'Napisz',
};
export default Object.assign({}, inherited, messages);

View File

@ -150,16 +150,16 @@ function apiStatusToTextMentions (state, status) {
}
return set.union(status.mentions.filter(
mention => mention.id !== me
mention => mention.id !== me,
).map(
mention => `@${mention.acct} `
mention => `@${mention.acct} `,
)).join('');
}
function apiStatusToTextHashtags (state, status) {
const text = unescapeHTML(status.content);
return ImmutableOrderedSet([]).union(recoverHashtags(status.tags, text).map(
(name) => `#${name} `
(name) => `#${name} `,
)).join('');
}
@ -175,7 +175,7 @@ function clearAll(state) {
map.set('in_reply_to', null);
map.update(
'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
map => map.mergeWith(overwrite, state.get('default_advanced_options')),
);
map.set('privacy', state.get('default_privacy'));
map.set('sensitive', state.get('default_sensitive'));
@ -202,7 +202,7 @@ function continueThread (state, status) {
map.set('in_reply_to', status.id);
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: status.local_only }))
map => map.merge(new ImmutableMap({ do_not_federate: status.local_only })),
);
map.set('privacy', status.visibility);
map.set('sensitive', false);
@ -414,7 +414,7 @@ export default function compose(state = initialState, action) {
map.set('privacy', privacyPreference(action.status.get('visibility'), state.get('default_privacy')));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') }))
map => map.merge(new ImmutableMap({ do_not_federate: !!action.status.get('local_only') })),
);
map.set('focusDate', new Date());
map.set('caretPosition', null);
@ -451,7 +451,7 @@ export default function compose(state = initialState, action) {
map.set('poll', null);
map.update(
'advanced_options',
map => map.mergeWith(overwrite, state.get('default_advanced_options'))
map => map.mergeWith(overwrite, state.get('default_advanced_options')),
);
map.set('idempotencyKey', uuid());
});
@ -571,7 +571,7 @@ export default function compose(state = initialState, action) {
map.set('language', action.status.get('language'));
map.update(
'advanced_options',
map => map.merge(new ImmutableMap({ do_not_federate }))
map => map.merge(new ImmutableMap({ do_not_federate })),
);
map.set('id', null);

View File

@ -67,7 +67,7 @@ const deleteFromContexts = (immutableState, ids) => immutableState.withMutations
const filterContexts = (state, relationship, statuses) => {
const ownedStatusIds = statuses.filter(status => status.get('account') === relationship.id)
.map(status => status.get('id'));
.map(status => status.get('id'));
return deleteFromContexts(state, ownedStatusIds);
};

View File

@ -54,10 +54,10 @@ export default function listEditorReducer(state = initialState, action) {
});
case LIST_CREATE_REQUEST:
case LIST_UPDATE_REQUEST:
return state.withMutations(map => {
map.set('isSubmitting', true);
map.set('isChanged', false);
});
return state.withMutations(map => {
map.set('isSubmitting', true);
map.set('isChanged', false);
});
case LIST_CREATE_FAIL:
case LIST_UPDATE_FAIL:
return state.set('isSubmitting', false);

View File

@ -0,0 +1,13 @@
import { RULES_FETCH_SUCCESS } from 'flavours/glitch/actions/rules';
import { List as ImmutableList, fromJS } from 'immutable';
const initialState = ImmutableList();
export default function rules(state = initialState, action) {
switch (action.type) {
case RULES_FETCH_SUCCESS:
return state;
default:
return state;
}
}

View File

@ -88,25 +88,7 @@ const sharedCallbacks = {
},
received (data) {
const { stream } = data;
subscriptions.filter(({ channelName, params }) => {
const streamChannelName = stream[0];
if (stream.length === 1) {
return channelName === streamChannelName;
}
const streamIdentifier = stream[1];
if (['hashtag', 'hashtag:local'].includes(channelName)) {
return channelName === streamChannelName && params.tag === streamIdentifier;
} else if (channelName === 'list') {
return channelName === streamChannelName && params.list === streamIdentifier;
}
return false;
}).forEach(subscription => {
subscriptions.forEach(subscription => {
subscription.onReceive(data);
});
},
@ -230,7 +212,7 @@ const createConnection = (streamingAPIBaseURL, accessToken, channelName, { conne
channelName = params.shift();
if (streamingAPIBaseURL.startsWith('ws')) {
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?${params.join('&')}`, accessToken);
const ws = new WebSocketClient(`${streamingAPIBaseURL}/api/v1/streaming/?stream=user&${params.join('&')}`, accessToken);
ws.onopen = connected;
ws.onmessage = e => received(JSON.parse(e.data));

View File

@ -0,0 +1,28 @@
import * as registerPushNotifications from 'flavours/glitch/actions/push_notifications';
import { setupBrowserNotifications } from 'flavours/glitch/actions/notifications';
import { default as Mastodon, store } from 'flavours/glitch/containers/mastodon';
import React from 'react';
import ReactDOM from 'react-dom';
import ready from './ready';
const perf = require('./performance');
function main() {
perf.start('main()');
ready(() => {
const mountNode = document.getElementById('mastodon');
const props = JSON.parse(mountNode.getAttribute('data-props'));
ReactDOM.render(<Mastodon {...props} />, mountNode);
store.dispatch(setupBrowserNotifications());
if (process.env.NODE_ENV === 'production') {
// avoid offline in dev mode because it's harder to debug
require('offline-plugin/runtime').install();
store.dispatch(registerPushNotifications.register());
}
perf.stop('main()');
});
}
export default main;

View File

@ -1,8 +1,8 @@
export function recoverHashtags (recognizedTags, text) {
return recognizedTags.map(tag => {
const re = new RegExp(`(?:^|[^\/\)\w])#(${tag.name})`, 'i');
const matched_hashtag = text.match(re);
return matched_hashtag ? matched_hashtag[1] : null;
}
const re = new RegExp(`(?:^|[^\/\)\w])#(${tag.name})`, 'i');
const matched_hashtag = text.match(re);
return matched_hashtag ? matched_hashtag[1] : null;
},
).filter(x => x !== null);
}

View File

@ -7,7 +7,7 @@ export function assignHandlers (target, handlers) {
// We just bind each handler to the `target`.
const handle = target.handlers = {};
Object.keys(handlers).forEach(
key => handle[key] = handlers[key].bind(target)
key => handle[key] = handlers[key].bind(target),
);
}

View File

@ -30,7 +30,7 @@ const createIdentityContext = state => ({
accountId: state.meta.me,
disabledAccountId: state.meta.disabled_account_id,
accessToken: state.meta.access_token,
permissions: state.role ? state.role.permissions : 0,
permissions: [],
});
export default class Mastodon extends React.PureComponent {

View File

@ -11,7 +11,7 @@ function openWebCache() {
}
function fetchRoot() {
return fetch('/', { credentials: 'include', redirect: 'manual' });
return fetch('/web', { credentials: 'include', redirect: 'manual' });
}
precacheAndRoute(self.__WB_MANIFEST);
@ -58,7 +58,7 @@ registerRoute(
// Cause a new version of a registered Service Worker to replace an existing one
// that is already installed, and replace the currently active worker on open pages.
self.addEventListener('install', function(event) {
event.waitUntil(Promise.all([openWebCache(), fetchRoot()]).then(([cache, root]) => cache.put('/', root)));
event.waitUntil(Promise.all([openWebCache(), fetchRoot()]).then(([cache, root]) => cache.put('/web', root)));
});
self.addEventListener('activate', function(event) {
@ -68,14 +68,13 @@ self.addEventListener('activate', function(event) {
self.addEventListener('fetch', function(event) {
const url = new URL(event.request.url);
if (url.pathname === '/auth/sign_out') {
const asyncResponse = fetch(event.request);
const asyncCache = openWebCache();
event.respondWith(asyncResponse.then(response => {
if (response.ok || response.type === 'opaqueredirect') {
return Promise.all([
asyncCache.then(cache => cache.delete('/')),
asyncCache.then(cache => cache.delete('/web')),
indexedDB.deleteDatabase('mastodon'),
]).then(() => response);
}

20
build.sh Executable file
View File

@ -0,0 +1,20 @@
#!/bin/sh
TARGET="${TARGET:-./distribution}" # Where pleromas repository is sitting
mkdir -p $TARGET/emoji
die() {
echo "Die: $@"
exit 1
}
[ -d "${TARGET}" ] || die "${TARGET} directory is missing, are you sure TARGET is set to a pleroma repository? (Info: TARGET=${TARGET} )"
yarn install -D || die "Installing dependencies via yarn failed"
rm -rf public/packs public/assets
env -i "PATH=$PATH" npm run build || die "Building the frontend failed"
cp public/assets/sw.js "${TARGET}/sw.js" || die "installing sw.js (service-worker) failed"
rm -rf "${TARGET}/packs" || die "Removing old assets in priv/static/packs failed"
cp -r public/packs "${TARGET}/packs" || die "Copying new assets in priv/static/packs failed"
rm -rf "${TARGET}/emoji/*.svg" || die "Removing the old emoji assets failed"
cp -r public/emoji/* "${TARGET}/emoji" || die "Installing the new emoji assets failed"

View File

@ -58,8 +58,7 @@ module.exports = {
entry: entries,
output: {
filename: 'js/[name]-[chunkhash].js',
chunkFilename: 'js/[name]-[chunkhash].chunk.js',
filename: 'js/[name].js',
hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
hashFunction: 'sha256',
path: output.path,
@ -102,8 +101,7 @@ module.exports = {
},
),
new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash:8].css',
chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
filename: 'css/[name].css',
}),
new AssetsManifestPlugin({
integrity: true,

18
dev.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/sh
TARGET="${TARGET:-./distribution}" # Where pleromas repository is sitting
mkdir -p $TARGET/emoji
die() {
echo "Die: $@"
exit 1
}
[ -d "${TARGET}" ] || die "${TARGET} directory is missing, are you sure TARGET is set to a pleroma repository? (Info: TARGET=${TARGET} )"
yarn install -D || die "Installing dependencies via yarn failed"
rm -rf public/packs public/assets
env -i "PATH=$PATH" npm run build:development || die "Building the frontend failed"
rm -rf "${TARGET}/packs" || die "Removing old assets in priv/static/packs failed"
cp -r public/packs "${TARGET}/packs" || die "Copying new assets in priv/static/packs failed"
rm -rf "${TARGET}/emoji/*.svg" || die "Removing the old emoji assets failed"

View File

@ -1,14 +1,17 @@
{
"name": "@mastodon/mastodon",
"description": "mastodon",
"license": "AGPL-3.0-or-later",
"engines": {
"node": ">=14"
},
"scripts": {
"postversion": "git push --tags",
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/webpack",
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/webpack",
"build:development": "cross-env NODE_ENV=development webpack --config config/webpack/development.js",
"build:production": "cross-env NODE_ENV=production webpack --config config/webpack/production.js",
"build": "cross-env NODE_ENV=production webpack --config config/webpack/production.js",
"manage:translations": "node ./config/webpack/translationRunner.js",
"dev": "cross-env NODE_ENV=development webpack-dev-server --config config/webpack/development.js --progress --color",
"start": "node ./streaming/index.js",
"test": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:jest",
"test:lint": "${npm_execpath} run test:lint:js && ${npm_execpath} run test:lint:sass",
@ -48,6 +51,7 @@
"blurhash": "^2.0.4",
"classnames": "^2.3.2",
"cocoon-js-vanilla": "^1.3.0",
"babel-runtime": "^6.26.0",
"color-blend": "^3.0.1",
"compression-webpack-plugin": "^6.1.1",
"cross-env": "^7.0.3",

8
package.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/bash
TARGET="${TARGET:-./distribution}"
rm -rf "${TARGET}/packs" || die "Removing old assets in priv/static/packs failed"
cp -r public/packs "${TARGET}/packs" || die "Copying new assets in priv/static/packs failed"
rm -rf "${TARGET}/emoji/*.svg" || die "Removing the old emoji assets failed"
cp -r public/emoji/* "${TARGET}/emoji" || die "Installing the new emoji assets failed"

View File

@ -2673,6 +2673,14 @@ babel-preset-jest@^29.2.0:
babel-plugin-jest-hoist "^29.2.0"
babel-preset-current-node-syntax "^1.0.0"
babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
integrity sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -3508,7 +3516,7 @@ core-js-pure@^3.0.0:
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
core-js@^2.5.0:
core-js@^2.4.0, core-js@^2.5.0:
version "2.6.12"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
@ -9301,6 +9309,11 @@ regenerate@^1.4.2:
resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a"
integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==
regenerator-runtime@^0.11.0:
version "0.11.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==
regenerator-runtime@^0.12.0:
version "0.12.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"