Merge remote-tracking branch 'parent/main' into kb_development
This commit is contained in:
commit
991c0dfddf
63 changed files with 547 additions and 357 deletions
|
@ -33,7 +33,7 @@ module Admin
|
|||
|
||||
# Disallow accidentally downgrading a domain block
|
||||
if existing_domain_block.present? && !@domain_block.stricter_than?(existing_domain_block)
|
||||
@domain_block.save
|
||||
@domain_block.validate
|
||||
flash.now[:alert] = I18n.t('admin.domain_blocks.existing_domain_block_html', name: existing_domain_block.domain, unblock_url: admin_domain_block_path(existing_domain_block)).html_safe
|
||||
@domain_block.errors.delete(:domain)
|
||||
return render :new
|
||||
|
|
|
@ -92,18 +92,10 @@ module CacheConcern
|
|||
arguments
|
||||
end
|
||||
|
||||
if Rails.gem_version >= Gem::Version.new('7.0')
|
||||
def attributes_for_database(record)
|
||||
attributes = record.attributes_for_database
|
||||
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
|
||||
attributes
|
||||
end
|
||||
else
|
||||
def attributes_for_database(record)
|
||||
attributes = record.instance_variable_get(:@attributes).send(:attributes).transform_values(&:value_for_database)
|
||||
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
|
||||
attributes
|
||||
end
|
||||
def attributes_for_database(record)
|
||||
attributes = record.attributes_for_database
|
||||
attributes.transform_values! { |attr| attr.is_a?(::ActiveModel::Type::Binary::Data) ? attr.to_s : attr }
|
||||
attributes
|
||||
end
|
||||
|
||||
def deserialize_record(class_name, attributes_from_database, new_record = false) # rubocop:disable Style/OptionalBooleanParameter
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { useCallback, useRef, useState, useEffect, forwardRef } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
||||
|
@ -37,54 +37,46 @@ const textAtCursorMatchesToken = (str, caretPosition) => {
|
|||
}
|
||||
};
|
||||
|
||||
export default class AutosuggestTextarea extends ImmutablePureComponent {
|
||||
const AutosuggestTextarea = forwardRef(({
|
||||
value,
|
||||
suggestions,
|
||||
disabled,
|
||||
placeholder,
|
||||
onSuggestionSelected,
|
||||
onSuggestionsClearRequested,
|
||||
onSuggestionsFetchRequested,
|
||||
onChange,
|
||||
onKeyUp,
|
||||
onKeyDown,
|
||||
onPaste,
|
||||
onFocus,
|
||||
autoFocus = true,
|
||||
lang,
|
||||
children,
|
||||
}, textareaRef) => {
|
||||
|
||||
static propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
const [suggestionsHidden, setSuggestionsHidden] = useState(true);
|
||||
const [selectedSuggestion, setSelectedSuggestion] = useState(0);
|
||||
const lastTokenRef = useRef(null);
|
||||
const tokenStartRef = useRef(0);
|
||||
|
||||
static defaultProps = {
|
||||
autoFocus: true,
|
||||
};
|
||||
|
||||
state = {
|
||||
suggestionsHidden: true,
|
||||
focused: false,
|
||||
selectedSuggestion: 0,
|
||||
lastToken: null,
|
||||
tokenStart: 0,
|
||||
};
|
||||
|
||||
onChange = (e) => {
|
||||
const handleChange = useCallback((e) => {
|
||||
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
|
||||
|
||||
if (token !== null && this.state.lastToken !== token) {
|
||||
this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
|
||||
this.props.onSuggestionsFetchRequested(token);
|
||||
if (token !== null && lastTokenRef.current !== token) {
|
||||
tokenStartRef.current = tokenStart;
|
||||
lastTokenRef.current = token;
|
||||
setSelectedSuggestion(0);
|
||||
onSuggestionsFetchRequested(token);
|
||||
} else if (token === null) {
|
||||
this.setState({ lastToken: null });
|
||||
this.props.onSuggestionsClearRequested();
|
||||
lastTokenRef.current = null;
|
||||
onSuggestionsClearRequested();
|
||||
}
|
||||
|
||||
this.props.onChange(e);
|
||||
};
|
||||
|
||||
onKeyDown = (e) => {
|
||||
const { suggestions, disabled } = this.props;
|
||||
const { selectedSuggestion, suggestionsHidden } = this.state;
|
||||
onChange(e);
|
||||
}, [onSuggestionsFetchRequested, onSuggestionsClearRequested, onChange, setSelectedSuggestion]);
|
||||
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
|
@ -102,80 +94,75 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
document.querySelector('.ui').parentElement.focus();
|
||||
} else {
|
||||
e.preventDefault();
|
||||
this.setState({ suggestionsHidden: true });
|
||||
setSuggestionsHidden(true);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
|
||||
setSelectedSuggestion(Math.min(selectedSuggestion + 1, suggestions.size - 1));
|
||||
}
|
||||
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
|
||||
setSelectedSuggestion(Math.max(selectedSuggestion - 1, 0));
|
||||
}
|
||||
|
||||
break;
|
||||
case 'Enter':
|
||||
case 'Tab':
|
||||
// Select suggestion
|
||||
if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
if (lastTokenRef.current !== null && suggestions.size > 0 && !suggestionsHidden) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
|
||||
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestions.get(selectedSuggestion));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (e.defaultPrevented || !this.props.onKeyDown) {
|
||||
if (e.defaultPrevented || !onKeyDown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onKeyDown(e);
|
||||
};
|
||||
onKeyDown(e);
|
||||
}, [disabled, suggestions, suggestionsHidden, selectedSuggestion, setSelectedSuggestion, setSuggestionsHidden, onSuggestionSelected, onKeyDown]);
|
||||
|
||||
onBlur = () => {
|
||||
this.setState({ suggestionsHidden: true, focused: false });
|
||||
};
|
||||
const handleBlur = useCallback(() => {
|
||||
setSuggestionsHidden(true);
|
||||
}, [setSuggestionsHidden]);
|
||||
|
||||
onFocus = (e) => {
|
||||
this.setState({ focused: true });
|
||||
if (this.props.onFocus) {
|
||||
this.props.onFocus(e);
|
||||
const handleFocus = useCallback((e) => {
|
||||
if (onFocus) {
|
||||
onFocus(e);
|
||||
}
|
||||
};
|
||||
}, [onFocus]);
|
||||
|
||||
onSuggestionClick = (e) => {
|
||||
const suggestion = this.props.suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||
const handleSuggestionClick = useCallback((e) => {
|
||||
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index'));
|
||||
e.preventDefault();
|
||||
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestion);
|
||||
this.textarea.focus();
|
||||
};
|
||||
onSuggestionSelected(tokenStartRef.current, lastTokenRef.current, suggestion);
|
||||
textareaRef.current?.focus();
|
||||
}, [suggestions, onSuggestionSelected, textareaRef]);
|
||||
|
||||
UNSAFE_componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.suggestions !== this.props.suggestions && nextProps.suggestions.size > 0 && this.state.suggestionsHidden && this.state.focused) {
|
||||
this.setState({ suggestionsHidden: false });
|
||||
}
|
||||
}
|
||||
|
||||
setTextarea = (c) => {
|
||||
this.textarea = c;
|
||||
};
|
||||
|
||||
onPaste = (e) => {
|
||||
const handlePaste = useCallback((e) => {
|
||||
if (e.clipboardData && e.clipboardData.files.length === 1) {
|
||||
this.props.onPaste(e.clipboardData.files);
|
||||
onPaste(e.clipboardData.files);
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
}, [onPaste]);
|
||||
|
||||
renderSuggestion = (suggestion, i) => {
|
||||
const { selectedSuggestion } = this.state;
|
||||
// Show the suggestions again whenever they change and the textarea is focused
|
||||
useEffect(() => {
|
||||
if (suggestions.size > 0 && textareaRef.current === document.activeElement) {
|
||||
setSuggestionsHidden(false);
|
||||
}
|
||||
}, [suggestions, textareaRef, setSuggestionsHidden]);
|
||||
|
||||
const renderSuggestion = (suggestion, i) => {
|
||||
let inner, key;
|
||||
|
||||
if (suggestion.type === 'emoji') {
|
||||
|
@ -190,50 +177,64 @@ export default class AutosuggestTextarea extends ImmutablePureComponent {
|
|||
}
|
||||
|
||||
return (
|
||||
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={this.onSuggestionClick}>
|
||||
<div role='button' tabIndex={0} key={key} data-index={i} className={classNames('autosuggest-textarea__suggestions__item', { selected: i === selectedSuggestion })} onMouseDown={handleSuggestionClick}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, lang, children } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
return [
|
||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||
<div className='autosuggest-textarea'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
|
||||
return [
|
||||
<div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'>
|
||||
<div className='autosuggest-textarea'>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{placeholder}</span>
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={handleKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onPaste={handlePaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{children}
|
||||
</div>,
|
||||
|
||||
<Textarea
|
||||
ref={this.setTextarea}
|
||||
className='autosuggest-textarea__textarea'
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
onKeyUp={onKeyUp}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
onPaste={this.onPaste}
|
||||
dir='auto'
|
||||
aria-autocomplete='list'
|
||||
lang={lang}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
{children}
|
||||
</div>,
|
||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(renderSuggestion)}
|
||||
</div>
|
||||
</div>,
|
||||
];
|
||||
});
|
||||
|
||||
<div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'>
|
||||
<div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}>
|
||||
{suggestions.map(this.renderSuggestion)}
|
||||
</div>
|
||||
</div>,
|
||||
];
|
||||
}
|
||||
AutosuggestTextarea.propTypes = {
|
||||
value: PropTypes.string,
|
||||
suggestions: ImmutablePropTypes.list,
|
||||
disabled: PropTypes.bool,
|
||||
placeholder: PropTypes.string,
|
||||
onSuggestionSelected: PropTypes.func.isRequired,
|
||||
onSuggestionsClearRequested: PropTypes.func.isRequired,
|
||||
onSuggestionsFetchRequested: PropTypes.func.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
onKeyUp: PropTypes.func,
|
||||
onKeyDown: PropTypes.func,
|
||||
onPaste: PropTypes.func.isRequired,
|
||||
onFocus:PropTypes.func,
|
||||
children: PropTypes.node,
|
||||
autoFocus: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
};
|
||||
|
||||
}
|
||||
export default AutosuggestTextarea;
|
||||
|
|
|
@ -2,6 +2,8 @@ import classNames from 'classnames';
|
|||
|
||||
import { ReactComponent as CheckBoxOutlineBlankIcon } from '@material-symbols/svg-600/outlined/check_box_outline_blank.svg';
|
||||
|
||||
import { isProduction } from 'mastodon/utils/environment';
|
||||
|
||||
interface SVGPropsWithTitle extends React.SVGProps<SVGSVGElement> {
|
||||
title?: string;
|
||||
}
|
||||
|
@ -24,7 +26,7 @@ export const Icon: React.FC<Props> = ({
|
|||
}) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!IconComponent) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (!isProduction()) {
|
||||
throw new Error(
|
||||
`<Icon id="${id}" className="${className}"> is missing an "icon" prop.`,
|
||||
);
|
||||
|
|
|
@ -11,6 +11,7 @@ import type {
|
|||
import { createBrowserHistory } from 'history';
|
||||
|
||||
import { layoutFromWindow } from 'mastodon/is_mobile';
|
||||
import { isDevelopment } from 'mastodon/utils/environment';
|
||||
|
||||
interface MastodonLocationState {
|
||||
fromMastodon?: boolean;
|
||||
|
@ -40,7 +41,7 @@ function normalizePath(
|
|||
} else if (
|
||||
location.state !== undefined &&
|
||||
state !== undefined &&
|
||||
process.env.NODE_ENV === 'development'
|
||||
isDevelopment()
|
||||
) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
|
|
|
@ -19,8 +19,9 @@ import UI from 'mastodon/features/ui';
|
|||
import initialState, { title as siteTitle } from 'mastodon/initial_state';
|
||||
import { IntlProvider } from 'mastodon/locales';
|
||||
import { store } from 'mastodon/store';
|
||||
import { isProduction } from 'mastodon/utils/environment';
|
||||
|
||||
const title = process.env.NODE_ENV === 'production' ? siteTitle : `${siteTitle} (Dev)`;
|
||||
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
|
||||
|
||||
const hydrateAction = hydrateStore(initialState);
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { createRef } from 'react';
|
||||
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
|
||||
|
@ -86,6 +87,11 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
highlighted: false,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.textareaRef = createRef(null);
|
||||
}
|
||||
|
||||
handleChange = (e) => {
|
||||
this.props.onChange(e.target.value);
|
||||
};
|
||||
|
@ -109,10 +115,10 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
};
|
||||
|
||||
handleSubmit = (e) => {
|
||||
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
|
||||
if (this.props.text !== this.textareaRef.current.value) {
|
||||
// Something changed the text inside the textarea (e.g. browser extensions like Grammarly)
|
||||
// Update the state to match the current text
|
||||
this.props.onChange(this.autosuggestTextarea.textarea.value);
|
||||
this.props.onChange(this.textareaRef.current.value);
|
||||
}
|
||||
|
||||
if (!this.canSubmit()) {
|
||||
|
@ -191,26 +197,22 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
// immediately selectable, we have to wait for observers to run, as
|
||||
// described in https://github.com/WICG/inert#performance-and-gotchas
|
||||
Promise.resolve().then(() => {
|
||||
this.autosuggestTextarea.textarea.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd);
|
||||
this.textareaRef.current.focus();
|
||||
this.setState({ highlighted: true });
|
||||
this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700);
|
||||
}).catch(console.error);
|
||||
} else if(prevProps.isSubmitting && !this.props.isSubmitting) {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
this.textareaRef.current.focus();
|
||||
} else if (this.props.spoiler !== prevProps.spoiler) {
|
||||
if (this.props.spoiler) {
|
||||
this.spoilerText.input.focus();
|
||||
} else if (prevProps.spoiler) {
|
||||
this.autosuggestTextarea.textarea.focus();
|
||||
this.textareaRef.current.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
setAutosuggestTextarea = (c) => {
|
||||
this.autosuggestTextarea = c;
|
||||
};
|
||||
|
||||
setSpoilerText = (c) => {
|
||||
this.spoilerText = c;
|
||||
};
|
||||
|
@ -221,7 +223,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
handleEmojiPick = (data) => {
|
||||
const { text } = this.props;
|
||||
const position = this.autosuggestTextarea.textarea.selectionStart;
|
||||
const position = this.textareaRef.current.selectionStart;
|
||||
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
|
||||
|
||||
this.props.onPickEmoji(position, data, needsSpace);
|
||||
|
@ -276,7 +278,7 @@ class ComposeForm extends ImmutablePureComponent {
|
|||
|
||||
<div className={classNames('compose-form__highlightable', { active: highlighted })}>
|
||||
<AutosuggestTextarea
|
||||
ref={this.setAutosuggestTextarea}
|
||||
ref={this.textareaRef}
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
disabled={disabled}
|
||||
value={this.props.text}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
export const unicodeToFilename = (str: string) => {
|
||||
let result = '';
|
||||
let charCode = 0;
|
||||
let p = 0;
|
||||
let i = 0;
|
||||
while (i < str.length) {
|
||||
charCode = str.charCodeAt(i++);
|
||||
if (p) {
|
||||
if (result.length > 0) {
|
||||
result += '-';
|
||||
}
|
||||
result += (0x10000 + ((p - 0xd800) << 10) + (charCode - 0xdc00)).toString(
|
||||
16,
|
||||
);
|
||||
p = 0;
|
||||
} else if (0xd800 <= charCode && charCode <= 0xdbff) {
|
||||
p = charCode;
|
||||
} else {
|
||||
if (result.length > 0) {
|
||||
result += '-';
|
||||
}
|
||||
result += charCode.toString(16);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
|
@ -0,0 +1,21 @@
|
|||
function padLeft(str: string, num: number) {
|
||||
while (str.length < num) {
|
||||
str = '0' + str;
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
export const unicodeToUnifiedName = (str: string) => {
|
||||
let output = '';
|
||||
|
||||
for (let i = 0; i < str.length; i += 2) {
|
||||
if (i > 0) {
|
||||
output += '-';
|
||||
}
|
||||
|
||||
output += padLeft((str.codePointAt(i) ?? 0).toString(16).toUpperCase(), 4);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import { isDevelopment } from 'mastodon/utils/environment';
|
||||
|
||||
export interface LocaleData {
|
||||
locale: string;
|
||||
messages: Record<string, string>;
|
||||
|
@ -11,7 +13,7 @@ export function setLocale(locale: LocaleData) {
|
|||
|
||||
export function getLocale(): LocaleData {
|
||||
if (!loadedLocale) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDevelopment()) {
|
||||
throw new Error('getLocale() called before any locale has been set');
|
||||
} else {
|
||||
return { locale: 'unknown', messages: {} };
|
||||
|
|
|
@ -202,7 +202,7 @@
|
|||
"dismissable_banner.community_timeline": "אלו הם החצרוצים הציבוריים האחרונים מהמשתמשים על שרת {domain}.",
|
||||
"dismissable_banner.dismiss": "בטל",
|
||||
"dismissable_banner.explore_links": "אלו הקישורים האחרונים ששותפו על ידי משתמשים ששרת זה רואה ברשת המבוזרת כרגע.",
|
||||
"dismissable_banner.explore_statuses": "ההודעות האלו, משרת זה ואחרים ברשת המבוזרת, צוברים חשיפה היום. הודעות חדשות יותר עם יותר הדהודים וחיבובים מדורגים יותר לגובה.",
|
||||
"dismissable_banner.explore_statuses": "ההודעות האלו, משרת זה ואחרים ברשת המבוזרת, צוברים חשיפה היום. הודעות חדשות יותר עם יותר הדהודים וחיבובים מדורגות גבוה יותר.",
|
||||
"dismissable_banner.explore_tags": "התגיות האלו, משרת זה ואחרים ברשת המבוזרת, צוברות חשיפה כעת.",
|
||||
"dismissable_banner.public_timeline": "אלו ההודעות האחרונות שהתקבלו מהמשתמשים שנעקבים על ידי משתמשים מ־{domain}.",
|
||||
"embed.instructions": "ניתן להטמיע את ההודעה הזו באתרך ע\"י העתקת הקוד שלהלן.",
|
||||
|
@ -315,7 +315,7 @@
|
|||
"home.pending_critical_update.title": "יצא עדכון אבטחה חשוב!",
|
||||
"home.show_announcements": "הצג הכרזות",
|
||||
"interaction_modal.description.favourite": "עם חשבון מסטודון, ניתן לחבב את ההודעה כדי לומר למחבר/ת שהערכת את תוכנו או כדי לשמור אותו לקריאה בעתיד.",
|
||||
"interaction_modal.description.follow": "עם חשבון מסטודון, ניתן לעקוב אחרי {name} כדי לקבל את הםוסטים שלו/ה בפיד הבית.",
|
||||
"interaction_modal.description.follow": "עם חשבון מסטודון, ניתן לעקוב אחרי {name} כדי לקבל את הפוסטים שלו/ה בפיד הבית.",
|
||||
"interaction_modal.description.reblog": "עם חשבון מסטודון, ניתן להדהד את החצרוץ ולשתף עם עוקבים.",
|
||||
"interaction_modal.description.reply": "עם חשבון מסטודון, ניתן לענות לחצרוץ.",
|
||||
"interaction_modal.login.action": "קח אותי לדף הבית",
|
||||
|
@ -349,7 +349,7 @@
|
|||
"keyboard_shortcuts.hotkey": "מקש קיצור",
|
||||
"keyboard_shortcuts.legend": "הצגת מקרא",
|
||||
"keyboard_shortcuts.local": "פתיחת ציר זמן קהילתי",
|
||||
"keyboard_shortcuts.mention": "לאזכר את המחבר(ת)",
|
||||
"keyboard_shortcuts.mention": "לאזכר את המחבר.ת",
|
||||
"keyboard_shortcuts.muted": "פתיחת רשימת משתמשים מושתקים",
|
||||
"keyboard_shortcuts.my_profile": "פתיחת הפרופיל שלך",
|
||||
"keyboard_shortcuts.notifications": "פתיחת טור התראות",
|
||||
|
@ -493,7 +493,7 @@
|
|||
"onboarding.steps.setup_profile.title": "התאמה אישית של הפרופיל",
|
||||
"onboarding.steps.share_profile.body": "ספרו לחברים איך למצוא אתכם במסטודון!",
|
||||
"onboarding.steps.share_profile.title": "לשתף פרופיל",
|
||||
"onboarding.tips.2fa": "<strong>הידעת?</strong> ניתן לאבטח את החשבון ע\"י הקמת אימות בשני צעדים במסך מאפייני החשבון. השיטה תעבוד עם כל יישומון תואם TOTP על המגשיר שלך, אין צורך לתת לנו את מספר הטלפון!",
|
||||
"onboarding.tips.2fa": "<strong>הידעת?</strong> ניתן לאבטח את החשבון ע\"י הקמת אימות דו-שלבי במסך מאפייני החשבון. השיטה תעבוד עם כל יישומון תואם TOTP על המכשיר שלך, ללא צורך במספר טלפון!",
|
||||
"onboarding.tips.accounts_from_other_servers": "<strong>הידעת?</strong> כיוון שמסטודון פועל ברשת מבוזרת, חלק מהפרופילים שתתקלו בהם פועלים משרתים אחרים משרת הבית שלכם. ניתן להיות איתם בקשר בצורה זהה לכל חשבון אחר! שם השרת שלהם הוא החלק השני של שם המשתמש שלהם!",
|
||||
"onboarding.tips.migration": "<strong>הידעת?</strong> אם תחליטו כי {domain} איננו שרת שמתאים לכם בעתיד, ניתן לעבור לשרת אחר מבלי לאבד עוקבים. תוכלו אפילו להקים שרת משלכן!",
|
||||
"onboarding.tips.verification": "<strong>הידעת?</strong> ניתן לאשרר את החשבון ע\"י קישור הפרופיל אל האתר שלכם ומהאתר חזרה לפרופיל. לא נדרשים תשלומים ומסמכים!",
|
||||
|
@ -575,7 +575,7 @@
|
|||
"report.thanks.title": "לא מעוניין/ת לראות את זה?",
|
||||
"report.thanks.title_actionable": "תודה על הדיווח, נבדוק את העניין.",
|
||||
"report.unfollow": "הפסיקו לעקוב אחרי @{name}",
|
||||
"report.unfollow_explanation": "אתם עוקבים אחרי החשבון הזה. כדי להפסיק לראות את הפרסומים שלו בפיד הבית שלכם, הפסיקו לעקוב אחריהם.",
|
||||
"report.unfollow_explanation": "אתם עוקבים אחרי החשבון הזה. כדי להפסיק לראות את הפרסומים שלו בפיד הבית שלכם, הפסיקו לעקוב אחריו.",
|
||||
"report_notification.attached_statuses": "{count, plural, one {הודעה מצורפת} two {הודעותיים מצורפות} many {{count} הודעות מצורפות} other {{count} הודעות מצורפות}}",
|
||||
"report_notification.categories.legal": "חוקי",
|
||||
"report_notification.categories.other": "שונות",
|
||||
|
@ -707,7 +707,7 @@
|
|||
"upload_modal.apply": "החל",
|
||||
"upload_modal.applying": "מחיל…",
|
||||
"upload_modal.choose_image": "בחר/י תמונה",
|
||||
"upload_modal.description_placeholder": "דג סקרן שט בים מאוכזב ולפתע מצא חברה",
|
||||
"upload_modal.description_placeholder": "עטלף אבק נס דרך מזגן שהתפוצץ כי חם",
|
||||
"upload_modal.detect_text": "זהה טקסט מתמונה",
|
||||
"upload_modal.edit_media": "עריכת מדיה",
|
||||
"upload_modal.hint": "הקליקי או גררי את המעגל על גבי התצוגה המקדימה על מנת לבחור בנקודת המוקד שתראה תמיד בכל התמונות הממוזערות.",
|
||||
|
|
|
@ -2,12 +2,14 @@ import { useEffect, useState } from 'react';
|
|||
|
||||
import { IntlProvider as BaseIntlProvider } from 'react-intl';
|
||||
|
||||
import { isProduction } from 'mastodon/utils/environment';
|
||||
|
||||
import { getLocale, isLocaleLoaded } from './global_locale';
|
||||
import { loadLocale } from './load_locale';
|
||||
|
||||
function onProviderError(error: unknown) {
|
||||
// Silent the error, like upstream does
|
||||
if (process.env.NODE_ENV === 'production') return;
|
||||
if (isProduction()) return;
|
||||
|
||||
// This browser does not advertise Intl support for this locale, we only print a warning
|
||||
// As-per the spec, the browser should select the best matching locale
|
||||
|
|
|
@ -137,7 +137,7 @@
|
|||
"compose.language.search": "Cari bahasa...",
|
||||
"compose.published.body": "Pos telah diterbitkan.",
|
||||
"compose.published.open": "Buka",
|
||||
"compose.saved.body": "Pos disimpan.",
|
||||
"compose.saved.body": "Kiriman disimpan.",
|
||||
"compose_form.direct_message_warning_learn_more": "Ketahui lebih lanjut",
|
||||
"compose_form.encryption_warning": "Hantaran pada Mastodon tidak disulitkan hujung ke hujung. Jangan berkongsi sebarang maklumat sensitif melalui Mastodon.",
|
||||
"compose_form.hashtag_warning": "Hantaran ini tidak akan disenaraikan di bawah mana-mana tanda pagar kerana ia tidak tersenarai. Hanya hantaran awam sahaja boleh dicari menggunakan tanda pagar.",
|
||||
|
@ -307,6 +307,9 @@
|
|||
"home.explore_prompt.body": "Suapan rumah anda akan mempunyai gabungan pos daripada hashtag yang telah anda pilih untuk diikuti, orang yang telah anda pilih untuk diikuti dan pos yang mereka tingkatkan. Jika itu terasa terlalu senyap, anda mungkin mahu:",
|
||||
"home.explore_prompt.title": "Ini adalah pusat operasi anda dalam Mastodon.",
|
||||
"home.hide_announcements": "Sembunyikan pengumuman",
|
||||
"home.pending_critical_update.body": "Sila kemas kini pelayan Mastodon anda secepat yang mungkin!",
|
||||
"home.pending_critical_update.link": "Lihat pengemaskinian",
|
||||
"home.pending_critical_update.title": "Kemas kini keselamatan kritikal tersedia!",
|
||||
"home.show_announcements": "Tunjukkan pengumuman",
|
||||
"interaction_modal.description.favourite": "Dengan akaun di Mastodon, anda boleh menggemari pos ini untuk memberitahu pengarang anda menghargainya dan menyimpannya untuk kemudian.",
|
||||
"interaction_modal.description.follow": "Dengan akaun pada Mastodon, anda boleh mengikut {name} untuk menerima hantaran mereka di suapan rumah anda.",
|
||||
|
@ -408,6 +411,7 @@
|
|||
"navigation_bar.lists": "Senarai",
|
||||
"navigation_bar.logout": "Log keluar",
|
||||
"navigation_bar.mutes": "Pengguna yang dibisukan",
|
||||
"navigation_bar.opened_in_classic_interface": "Kiriman, akaun dan halaman tertentu yang lain dibuka secara lalai di antara muka web klasik.",
|
||||
"navigation_bar.personal": "Peribadi",
|
||||
"navigation_bar.pins": "Hantaran disemat",
|
||||
"navigation_bar.preferences": "Keutamaan",
|
||||
|
@ -583,6 +587,7 @@
|
|||
"search.quick_action.open_url": "Buka URL dalam Mastadon",
|
||||
"search.quick_action.status_search": "Pos sepadan {x}",
|
||||
"search.search_or_paste": "Cari atau tampal URL",
|
||||
"search_popout.full_text_search_disabled_message": "Tidak tersedia di {domain}.",
|
||||
"search_popout.language_code": "Kod bahasa ISO",
|
||||
"search_popout.options": "Pilihan carian",
|
||||
"search_popout.quick_actions": "Tindakan pantas",
|
||||
|
|
|
@ -7,6 +7,8 @@ import * as perf from 'mastodon/performance';
|
|||
import ready from 'mastodon/ready';
|
||||
import { store } from 'mastodon/store';
|
||||
|
||||
import { isProduction } from './utils/environment';
|
||||
|
||||
/**
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
|
@ -21,7 +23,7 @@ function main() {
|
|||
root.render(<Mastodon {...props} />);
|
||||
store.dispatch(setupBrowserNotifications());
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && me && 'serviceWorker' in navigator) {
|
||||
if (isProduction() && me && 'serviceWorker' in navigator) {
|
||||
const { Workbox } = await import('workbox-window');
|
||||
const wb = new Workbox('/sw.js');
|
||||
/** @type {ServiceWorkerRegistration} */
|
||||
|
|
|
@ -5,7 +5,9 @@
|
|||
|
||||
import * as marky from 'marky';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
import { isDevelopment } from './utils/environment';
|
||||
|
||||
if (isDevelopment()) {
|
||||
if (typeof performance !== 'undefined' && performance.setResourceTimingBufferSize) {
|
||||
// Increase Firefox's performance entry limit; otherwise it's capped to 150.
|
||||
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1331135
|
||||
|
@ -18,13 +20,13 @@ if (process.env.NODE_ENV === 'development') {
|
|||
}
|
||||
|
||||
export function start(name) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDevelopment()) {
|
||||
marky.mark(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function stop(name) {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
if (isDevelopment()) {
|
||||
marky.stop(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import 'core-js/features/object/assign';
|
||||
import 'core-js/features/object/values';
|
||||
import 'core-js/features/symbol';
|
||||
import 'core-js/features/promise/finally';
|
||||
import { decode as decodeBase64 } from '../utils/base64';
|
||||
|
||||
if (!Object.hasOwn(HTMLCanvasElement.prototype, 'toBlob')) {
|
||||
const BASE64_MARKER = ';base64,';
|
||||
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
|
||||
value: function (
|
||||
this: HTMLCanvasElement,
|
||||
callback: BlobCallback,
|
||||
type = 'image/png',
|
||||
quality: unknown,
|
||||
) {
|
||||
const dataURL: string = this.toDataURL(type, quality);
|
||||
let data;
|
||||
|
||||
if (dataURL.includes(BASE64_MARKER)) {
|
||||
const [, base64] = dataURL.split(BASE64_MARKER);
|
||||
data = decodeBase64(base64);
|
||||
} else {
|
||||
[, data] = dataURL.split(',');
|
||||
}
|
||||
|
||||
callback(new Blob([data], { type }));
|
||||
},
|
||||
});
|
||||
}
|
|
@ -1,2 +1 @@
|
|||
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only';
|
||||
import 'requestidlecallback';
|
||||
|
|
|
@ -4,39 +4,18 @@
|
|||
|
||||
import { loadIntlPolyfills } from './intl';
|
||||
|
||||
function importBasePolyfills() {
|
||||
return import(/* webpackChunkName: "base_polyfills" */ './base_polyfills');
|
||||
}
|
||||
|
||||
function importExtraPolyfills() {
|
||||
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills');
|
||||
}
|
||||
|
||||
export function loadPolyfills() {
|
||||
const needsBasePolyfills = !(
|
||||
'toBlob' in HTMLCanvasElement.prototype &&
|
||||
'assign' in Object &&
|
||||
'values' in Object &&
|
||||
'Symbol' in window &&
|
||||
'finally' in Promise.prototype
|
||||
);
|
||||
|
||||
// Latest version of Firefox and Safari do not have IntersectionObserver.
|
||||
// Edge does not have requestIdleCallback.
|
||||
// Safari does not have requestIdleCallback.
|
||||
// This avoids shipping them all the polyfills.
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types */
|
||||
const needsExtraPolyfills = !(
|
||||
window.AbortController &&
|
||||
window.IntersectionObserver &&
|
||||
window.IntersectionObserverEntry &&
|
||||
'isIntersecting' in IntersectionObserverEntry.prototype &&
|
||||
window.requestIdleCallback
|
||||
);
|
||||
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
|
||||
const needsExtraPolyfills = !window.requestIdleCallback;
|
||||
|
||||
return Promise.all([
|
||||
loadIntlPolyfills(),
|
||||
needsBasePolyfills && importBasePolyfills(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- those properties might not exist in old browsers, even if they are always here in types
|
||||
needsExtraPolyfills && importExtraPolyfills(),
|
||||
]);
|
||||
}
|
||||
|
|
7
app/javascript/mastodon/utils/environment.ts
Normal file
7
app/javascript/mastodon/utils/environment.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function isDevelopment() {
|
||||
return process.env.NODE_ENV === 'development';
|
||||
}
|
||||
|
||||
export function isProduction() {
|
||||
return process.env.NODE_ENV === 'production';
|
||||
}
|
|
@ -108,7 +108,8 @@ class Trends::Statuses < Trends::Base
|
|||
def eligible?(status)
|
||||
(status.searchability.nil? || status.compute_searchability == 'public') &&
|
||||
(status.public_visibility? || status.public_unlisted_visibility?) &&
|
||||
status.account.discoverable? && !status.account.silenced? && status.spoiler_text.blank? && (!status.sensitive? || status.media_attachments.none?) &&
|
||||
status.account.discoverable? && !status.account.silenced? && !status.account.sensitized? &&
|
||||
status.spoiler_text.blank? && (!status.sensitive? || status.media_attachments.none?) &&
|
||||
!status.reply? && valid_locale?(status.language)
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue