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:
parent
859db01268
commit
076f7534db
74 changed files with 456 additions and 221 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,6 +6,7 @@
|
|||
|
||||
# Ignore bundler config and downloaded libraries.
|
||||
/.bundle
|
||||
distribution
|
||||
/vendor/bundle
|
||||
|
||||
# Ignore the default SQLite database.
|
||||
|
|
30
.woodpecker.yml
Normal file
30
.woodpecker.yml
Normal 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
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -11,7 +11,7 @@ export function initBoostModal(props) {
|
|||
|
||||
dispatch({
|
||||
type: BOOSTS_INIT_MODAL,
|
||||
privacy
|
||||
privacy,
|
||||
});
|
||||
|
||||
dispatch(openModal('BOOST', props));
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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={[]}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent {
|
|||
...rest,
|
||||
active: value && name === value,
|
||||
name,
|
||||
})
|
||||
}),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -42,5 +42,4 @@ export default class NavigationBar extends ImmutablePureComponent {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -95,4 +95,5 @@ class Publisher extends ImmutablePureComponent {
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -51,9 +51,10 @@ class TextareaIcons extends ImmutablePureComponent {
|
|||
id={icon}
|
||||
/>
|
||||
</span>
|
||||
) : null
|
||||
) : null,
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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']);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|