Compare commits

..

No commits in common. "796bcc701df89592dd430cbc4469f4a6789a1034" and "3a232d962611e11813901c88e73a62ffa7cff660" have entirely different histories.

13 changed files with 104 additions and 203 deletions

View File

@ -26,12 +26,6 @@ module ContextHelper
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
attribution_domains: { 'toot' => 'http://joinmastodon.org/ns#', 'attributionDomains' => { '@id' => 'toot:attributionDomains', '@type' => '@id' } },
quote_requests: { 'QuoteRequest' => 'https://w3id.org/fep/044f#QuoteRequest' },
quotes: {
'quote' => 'https://w3id.org/fep/044f#quote',
'quoteUri' => 'http://fedibird.com/ns#quoteUri',
'_misskey_quote' => 'https://misskey-hub.net/ns#_misskey_quote',
'quoteAuthorization' => 'https://w3id.org/fep/044f#quoteAuthorization',
},
interaction_policies: {
'gts' => 'https://gotosocial.org/ns#',
'interactionPolicy' => { '@id' => 'gts:interactionPolicy', '@type' => '@id' },

View File

@ -1,30 +1,12 @@
import { useCallback } from 'react';
import { useLinks } from 'mastodon/hooks/useLinks';
interface AccountBioProps {
export const AccountBio: React.FC<{
note: string;
className: string;
dropdownAccountId?: string;
}
}> = ({ note, className }) => {
const handleClick = useLinks();
export const AccountBio: React.FC<AccountBioProps> = ({
note,
className,
dropdownAccountId,
}) => {
const handleClick = useLinks(!!dropdownAccountId);
const handleNodeChange = useCallback(
(node: HTMLDivElement | null) => {
if (!dropdownAccountId || !node || node.childNodes.length === 0) {
return;
}
addDropdownToHashtags(node, dropdownAccountId);
},
[dropdownAccountId],
);
if (note.length === 0) {
if (note.length === 0 || note === '<p></p>') {
return null;
}
@ -33,28 +15,6 @@ export const AccountBio: React.FC<AccountBioProps> = ({
className={`${className} translate`}
dangerouslySetInnerHTML={{ __html: note }}
onClickCapture={handleClick}
ref={handleNodeChange}
/>
);
};
function addDropdownToHashtags(node: HTMLElement | null, accountId: string) {
if (!node) {
return;
}
for (const childNode of node.childNodes) {
if (!(childNode instanceof HTMLElement)) {
continue;
}
if (
childNode instanceof HTMLAnchorElement &&
(childNode.classList.contains('hashtag') ||
childNode.innerText.startsWith('#')) &&
!childNode.dataset.menuHashtag
) {
childNode.dataset.menuHashtag = accountId;
} else if (childNode.childNodes.length > 0) {
addDropdownToHashtags(childNode, accountId);
}
}
}

View File

@ -6,7 +6,6 @@ import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { NavLink } from 'react-router-dom';
import { AccountBio } from '@/mastodon/components/account_bio';
import CheckIcon from '@/material-icons/400-24px/check.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
@ -774,6 +773,7 @@ export const AccountHeader: React.FC<{
);
}
const content = { __html: account.note_emojified };
const displayNameHtml = { __html: account.display_name_html };
const fields = account.fields;
const isLocal = !account.acct.includes('@');
@ -897,11 +897,12 @@ export const AccountHeader: React.FC<{
<AccountNote accountId={accountId} />
)}
<AccountBio
note={account.note_emojified}
dropdownAccountId={accountId}
className='account__header__content'
/>
{account.note.length > 0 && account.note !== '<p></p>' && (
<div
className='account__header__content translate'
dangerouslySetInnerHTML={content}
/>
)}
<div className='account__header__fields'>
<dl>

View File

@ -10,11 +10,15 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import debounce from 'lodash.debounce';
import { missingAltTextModal } from 'mastodon/initial_state';
import { changeComposeLanguage } from 'mastodon/actions/compose';
import AutosuggestInput from 'mastodon/components/autosuggest_input';
import AutosuggestTextarea from 'mastodon/components/autosuggest_textarea';
import { Button } from 'mastodon/components/button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container';
import PollButtonContainer from '../containers/poll_button_container';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
@ -31,6 +35,8 @@ import { ReplyIndicator } from './reply_indicator';
import { UploadForm } from './upload_form';
import { Warning } from './warning';
import { connect } from 'react-redux';
const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d';
const messages = defineMessages({
@ -41,6 +47,28 @@ const messages = defineMessages({
reply: { id: 'compose_form.reply', defaultMessage: 'Reply' },
});
const mapStateToProps = (state) => ({
currentLanguage: state.meta.get('locale'),
});
const languageDetectorInGlobalThis = 'LanguageDetector' in globalThis;
let supportsLanguageDetector = languageDetectorInGlobalThis && await globalThis.LanguageDetector.availability() === 'available';
let languageDetector;
// If the API is supported, but the model not loaded yet
if (languageDetectorInGlobalThis && !supportsLanguageDetector) {
// trigger the model download
LanguageDetector.create().then((_languageDetector) => {
supportsLanguageDetector = true
languageDetector = _languageDetector
})
}
function countLetters(text) {
const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' })
const letters = [...segmenter.segment(text)]
return letters.length
}
class ComposeForm extends ImmutablePureComponent {
static propTypes = {
intl: PropTypes.object.isRequired,
@ -85,6 +113,7 @@ class ComposeForm extends ImmutablePureComponent {
constructor(props) {
super(props);
this.textareaRef = createRef(null);
this.debouncedHandleKeyUp = debounce(this._handleKeyUp.bind(this), 500);
}
handleChange = (e) => {
@ -97,6 +126,34 @@ class ComposeForm extends ImmutablePureComponent {
}
};
handleKeyUp = (e) => {
this.debouncedHandleKeyUp(e);
}
_handleKeyUp = async (e) => {
if (!supportsLanguageDetector) {
return;
}
if (!languageDetector) {
languageDetector = await globalThis.LanguageDetector.create();
}
const text = this.getFulltextForCharacterCounting().trim();
const currentLanguage = this.props.currentLanguage;
if (!text || countLetters(text) <= 5) {
this.props.dispatch(changeComposeLanguage(currentLanguage));
return;
}
try {
let detectedLanguage = (await languageDetector.detect(text))[0].detectedLanguage
detectedLanguage = detectedLanguage === 'und' ? currentLanguage : detectedLanguage.substring(0, 2);
this.props.dispatch(changeComposeLanguage(detectedLanguage));
}
catch {
this.props.dispatch(changeComposeLanguage(currentLanguage));
}
}
getFulltextForCharacterCounting = () => {
return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join('');
};
@ -162,6 +219,7 @@ class ComposeForm extends ImmutablePureComponent {
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
this.debouncedHandleKeyUp.cancel();
}
componentDidUpdate (prevProps) {
@ -273,6 +331,7 @@ class ComposeForm extends ImmutablePureComponent {
suggestions={this.props.suggestions}
onFocus={this.handleFocus}
onKeyDown={this.handleKeyDown}
onKeyUp={this.handleKeyUp}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
@ -309,7 +368,7 @@ class ComposeForm extends ImmutablePureComponent {
>
{intl.formatMessage(
this.props.isEditing ?
messages.saveChanges :
messages.saveChanges :
(this.props.isInReply ? messages.reply : messages.publish)
)}
</Button>
@ -323,4 +382,4 @@ class ComposeForm extends ImmutablePureComponent {
}
export default injectIntl(ComposeForm);
export default injectIntl(connect(mapStateToProps)(ComposeForm));

View File

@ -20,7 +20,7 @@ import { languages as preloadedLanguages } from 'mastodon/initial_state';
import type { RootState } from 'mastodon/store';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { debouncedGuess, countLetters } from '../util/language_detection';
import { debouncedGuess } from '../util/language_detection';
const messages = defineMessages({
changeLanguage: {
@ -375,25 +375,12 @@ export const LanguageDropdown: React.FC = () => {
);
useEffect(() => {
let canceled = false;
if (countLetters(text) >= 5) {
debouncedGuess(text)
.then((lang) => {
if (!canceled) {
setGuess(lang ?? '');
}
})
.catch(() => {
setGuess('');
});
if (text.length > 20) {
debouncedGuess(text, setGuess);
} else {
debouncedGuess.cancel();
setGuess('');
}
return () => {
canceled = true;
};
}, [text, setGuess]);
return (

View File

@ -1,5 +0,0 @@
export declare const debouncedGuess: (
text: string,
) => Promise<string | undefined>;
export declare const countLetters: (text: string) => number;

View File

@ -1,5 +1,7 @@
const languageDetectorInGlobalThis = 'LanguageDetector' in globalThis;
let languageDetectorSupportedAndReady = languageDetectorInGlobalThis && await globalThis.LanguageDetector.availability() === 'available';
import lande from 'lande';
import { debounce } from 'lodash';
import { urlRegex } from './url_regex';
const ISO_639_MAP = {
afr: 'af', // Afrikaans
@ -54,23 +56,21 @@ const ISO_639_MAP = {
vie: 'vi', // Vietnamese
};
const countLetters = (text) => {
const segmenter = new Intl.Segmenter('und', { granularity: 'grapheme' })
const letters = [...segmenter.segment(text)]
return letters.length
const guessLanguage = (text) => {
text = text
.replace(urlRegex, '')
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '');
if (text.length > 20) {
const [lang, confidence] = lande(text)[0];
if (confidence > 0.8)
return ISO_639_MAP[lang];
}
return '';
};
let module;
// If the API is supported, but the model not loaded yet…
if (languageDetectorInGlobalThis) {
if (!languageDetectorSupportedAndReady) {
// …trigger the model download
globalThis.LanguageDetector.create();
}
module = await import('./language_detection_with_languagedetector');
} else {
module = await import('./language_detection_with_laude');
}
const debouncedGuess = module.debouncedGuess;
export { debouncedGuess, countLetters, ISO_639_MAP };
export const debouncedGuess = debounce((text, setGuess) => {
setGuess(guessLanguage(text));
}, 500, { maxWait: 1500, leading: true, trailing: true });

View File

@ -1,41 +0,0 @@
import { debounce } from 'lodash';
import { urlRegex } from './url_regex';
const guessLanguage = async (text) => {
text = text
.replace(urlRegex, '')
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '');
try {
const languageDetector = await globalThis.LanguageDetector.create();
let {detectedLanguage, confidence} = (await languageDetector.detect(text))[0];
if (confidence > 0.8) {
detectedLanguage = detectedLanguage.split('-')[0];
return detectedLanguage;
}
} catch {
return '';
}
return '';
};
const debouncedGuess = (() => {
let resolver = null;
const debounced = debounce((text) => {
const result = guessLanguage(text);
if (resolver) {
resolver(result);
resolver = null;
}
}, 500, { maxWait: 1500, leading: true, trailing: true });
return (text) => new Promise((resolve) => {
resolver = resolve;
debounced(text);
});
})();
export { debouncedGuess };

View File

@ -1,38 +0,0 @@
import lande from 'lande';
import { debounce } from 'lodash';
import { countLetters, ISO_639_MAP } from './language_detection';
import { urlRegex } from './url_regex';
const guessLanguage = (text) => {
text = text
.replace(urlRegex, '')
.replace(/(^|[^/\w])@(([a-z0-9_]+)@[a-z0-9.-]+[a-z0-9]+)/ig, '');
if (countLetters(text) > 20) {
const [lang, confidence] = lande(text)[0];
if (confidence > 0.8)
return ISO_639_MAP[lang];
}
return '';
};
const debouncedGuess = (() => {
let resolver = null;
const debounced = debounce((text) => {
const result = guessLanguage(text);
if (resolver) {
resolver(result);
resolver = null;
}
}, 500, { maxWait: 1500, leading: true, trailing: true });
return (text) => new Promise((resolve) => {
resolver = resolve;
debounced(text);
});
})();
export { debouncedGuess };

View File

@ -8,14 +8,13 @@ import { openURL } from 'mastodon/actions/search';
import { useAppDispatch } from 'mastodon/store';
const isMentionClick = (element: HTMLAnchorElement) =>
element.classList.contains('mention') &&
!element.classList.contains('hashtag');
element.classList.contains('mention');
const isHashtagClick = (element: HTMLAnchorElement) =>
element.textContent?.[0] === '#' ||
element.previousSibling?.textContent?.endsWith('#');
export const useLinks = (skipHashtags?: boolean) => {
export const useLinks = () => {
const history = useHistory();
const dispatch = useAppDispatch();
@ -62,12 +61,12 @@ export const useLinks = (skipHashtags?: boolean) => {
if (isMentionClick(target)) {
e.preventDefault();
void handleMentionClick(target);
} else if (isHashtagClick(target) && !skipHashtags) {
} else if (isHashtagClick(target)) {
e.preventDefault();
handleHashtagClick(target);
}
},
[skipHashtags, handleMentionClick, handleHashtagClick],
[handleMentionClick, handleHashtagClick],
);
return handleClick;

View File

@ -1,5 +1,6 @@
// @ts-check
/**
* @typedef {[code: string, name: string, localName: string]} InitialStateLanguage
*/
@ -63,7 +64,6 @@
* @property {boolean=} critical_updates_pending
* @property {InitialStateMeta} meta
* @property {Role?} role
* @property {string[]} features
*/
const element = document.getElementById('initial-state');
@ -140,12 +140,4 @@ export function getAccessToken() {
return getMeta('access_token');
}
/**
* @param {string} feature
* @returns {boolean}
*/
export function isFeatureEnabled(feature) {
return initialState?.features?.includes(feature) || false;
}
export default initialState;

View File

@ -126,9 +126,6 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
? accountJSON.username
: accountJSON.display_name;
const accountNote =
accountJSON.note && accountJSON.note !== '<p></p>' ? accountJSON.note : '';
return AccountFactory({
...accountJSON,
moved: moved?.id,
@ -145,8 +142,8 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
escapeTextContentForBrowser(displayName),
emojiMap,
),
note_emojified: emojify(accountNote, emojiMap),
note_plain: unescapeHTML(accountNote),
note_emojified: emojify(accountJSON.note, emojiMap),
note_plain: unescapeHTML(accountJSON.note),
url:
accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://')

View File

@ -5,7 +5,7 @@ class InitialStateSerializer < ActiveModel::Serializer
attributes :meta, :compose, :accounts,
:media_attachments, :settings,
:languages, :features
:languages
attribute :critical_updates_pending, if: -> { object&.role&.can?(:view_devops) && SoftwareUpdate.check_enabled? }
@ -85,10 +85,6 @@ class InitialStateSerializer < ActiveModel::Serializer
LanguagesHelper::SUPPORTED_LOCALES.map { |(key, value)| [key, value[0], value[1]] }
end
def features
Mastodon::Feature.enabled_features
end
private
def default_meta_store