diff --git a/.env.production.sample b/.env.production.sample index 7bcce0f7e59b98..4d058f0119180d 100644 --- a/.env.production.sample +++ b/.env.production.sample @@ -294,6 +294,9 @@ MAX_POLL_OPTION_CHARS=100 # HCAPTCHA_SECRET_KEY= # HCAPTCHA_SITE_KEY= +# New registrations will automatically follow these accounts (separated by commas) +AUTOFOLLOW= + # IP and session retention # ----------------------- # Make sure to modify the scheduling of ip_cleanup_scheduler in config/sidekiq.yml diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 343dc36ca16d10..888a0943665ddd 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -138,10 +138,10 @@ jobs: run: sudo apt-get update - name: Install native Ruby dependencies - run: sudo apt-get install -y libicu-dev libidn11-dev + run: sudo apt-get update && sudo apt-get install -y libicu-dev libidn11-dev - name: Install additional system dependencies - run: sudo apt-get install -y ffmpeg imagemagick libpam-dev + run: sudo apt-get update && sudo apt-get install -y ffmpeg imagemagick libpam-dev - name: Set up bundler cache uses: ruby/setup-ruby@v1 diff --git a/.gitignore b/.gitignore index 2bc8b18c8f0346..9751d21060ed6d 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,5 @@ yarn-debug.log # Ignore Docker option files docker-compose.override.yml + +public/MathJax diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 9e0b12370484e3..036c33882f31f7 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -14,6 +14,7 @@ import { useEmoji } from './emojis'; import { importFetchedAccounts, importFetchedStatus } from './importer'; import { openModal } from './modal'; import { updateTimeline } from './timelines'; +import { tex_to_unicode } from '../features/compose/util/autolatex/autolatex.js'; /** @type {AbortController | undefined} */ let fetchComposeSuggestionsAccountsController; @@ -64,6 +65,7 @@ export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE'; export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; +export const COMPOSE_START_LATEX = 'COMPOSE_START_LATEX'; export const COMPOSE_UPLOAD_CHANGE_REQUEST = 'COMPOSE_UPLOAD_UPDATE_REQUEST'; export const COMPOSE_UPLOAD_CHANGE_SUCCESS = 'COMPOSE_UPLOAD_UPDATE_SUCCESS'; @@ -565,6 +567,36 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) => }); }, 200, { leading: true, trailing: true }); +const fetchComposeSuggestionsLatex = (dispatch, getState, token) => { + const start_delimiter = token.slice(0,2); + const end_delimiter = {'\\(': '\\)', '\\[': '\\]'}[start_delimiter]; + let expression = token.slice(2).replace(/\\[\)\]]?$/,''); + let brace = 0; + for(let i=0;i0;brace--) { + expression += '}'; + } + const results = [ + { start_delimiter, end_delimiter, expression } + ]; + dispatch(readyComposeSuggestionsLatex(token, results)); +}; + const fetchComposeSuggestionsEmojis = (dispatch, getState, token) => { const results = emojiSearch(token.replace(':', ''), { maxResults: 5 }); dispatch(readyComposeSuggestionsEmojis(token, results)); @@ -608,6 +640,9 @@ export function fetchComposeSuggestions(token) { case '#': fetchComposeSuggestionsTags(dispatch, getState, token); break; + case '\\': + fetchComposeSuggestionsLatex(dispatch, getState, token); + break; default: fetchComposeSuggestionsAccounts(dispatch, getState, token); break; @@ -615,6 +650,14 @@ export function fetchComposeSuggestions(token) { }; } +export function readyComposeSuggestionsLatex(token, latex) { + return { + type: COMPOSE_SUGGESTIONS_READY, + token, + latex, + }; +}; + export function readyComposeSuggestionsEmojis(token, emojis) { return { type: COMPOSE_SUGGESTIONS_READY, @@ -647,6 +690,10 @@ export function selectComposeSuggestion(position, token, suggestion, path) { completion = `#${suggestion.name}`; } else if (suggestion.type === 'account') { completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']); + } else if (suggestion.type === 'latex') { + const unicode = tex_to_unicode(suggestion.expression); + completion = unicode || `${suggestion.start_delimiter}${suggestion.expression}${suggestion.end_delimiter}`; + position -= 1; } // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that @@ -779,6 +826,14 @@ export function insertEmojiCompose(position, emoji) { }; } +export function startLaTeXCompose(position, latex_style) { + return { + type: COMPOSE_START_LATEX, + position, + latex_style, + }; +}; + export function addPoll() { return { type: COMPOSE_POLL_ADD, diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index f0833c8c6bad8c..d3f54ceb0b7137 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -9,12 +9,28 @@ import AutosuggestAccountContainer from 'flavours/glitch/features/compose/contai import AutosuggestEmoji from './autosuggest_emoji'; import { AutosuggestHashtag } from './autosuggest_hashtag'; +import AutosuggestLatex from './autosuggest_latex'; const textAtCursorMatchesToken = (str, caretPosition, searchTokens) => { let word; + let left; + let right; + + left = str.slice(0, caretPosition).search(/\\\((?:(?!\\\)).)*$/); + if (left >= 0) { + right = str.slice(caretPosition).search(/\\\)/); + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition); + } + if (word.trim().length >= 3) { + return [left + 1, word]; + } + } - let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); - let right = str.slice(caretPosition).search(/[\s\u200B]/); + left = str.slice(0, caretPosition).search(/\S+$/); + right = str.slice(caretPosition).search(/\s/); if (right < 0) { word = str.slice(left); @@ -180,6 +196,9 @@ export default class AutosuggestInput extends ImmutablePureComponent { } else if (suggestion.type === 'account') { inner = ; key = suggestion.id; + } else if (suggestion.type === 'latex') { + inner = ; + key = 'latex' + suggestion.expression; } return ( diff --git a/app/javascript/flavours/glitch/components/autosuggest_latex.js b/app/javascript/flavours/glitch/components/autosuggest_latex.js new file mode 100644 index 00000000000000..02fe0605ef749d --- /dev/null +++ b/app/javascript/flavours/glitch/components/autosuggest_latex.js @@ -0,0 +1,37 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const assetHost = process.env.CDN_HOST || ''; + +export default class AutosuggestLatex extends React.PureComponent { + + static propTypes = { + latex: PropTypes.object.isRequired, + }; + + setRef = (c) => { + this.node = c; + } + + componentDidMount() { + try { + MathJax.typeset([this.node]); + } catch(e) { + console.error(e); + } + + } + + render () { + const { latex } = this.props; + + return ( +
+ \({latex.expression}\) +
+ Convert to unicode +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index 25ca3fefa56252..c9f977b92836bd 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -11,12 +11,28 @@ import AutosuggestAccountContainer from 'flavours/glitch/features/compose/contai import AutosuggestEmoji from './autosuggest_emoji'; import { AutosuggestHashtag } from './autosuggest_hashtag'; +import AutosuggestLatex from './autosuggest_latex'; const textAtCursorMatchesToken = (str, caretPosition) => { let word; + let left; + let right; + + left = str.slice(0, caretPosition).search(/\\[\(\[](?:(?!\\[\)\]]).)*(?:\\[\)\]])?$/); + if (left >= 0) { + right = str.slice(caretPosition).search(/\\[\)\]]/); + if (right < 0) { + word = str.slice(left); + } else { + word = str.slice(left, right + caretPosition + 2); + } + if (word.trim().length >= 3) { + return [left + 1, word]; + } + } - let left = str.slice(0, caretPosition).search(/[^\s\u200B]+$/); - let right = str.slice(caretPosition).search(/[\s\u200B]/); + left = str.slice(0, caretPosition).search(/\S+$/); + right = str.slice(caretPosition).search(/\s/); if (right < 0) { word = str.slice(left); @@ -187,6 +203,9 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } else if (suggestion.type === 'account') { inner = ; key = suggestion.id; + } else if (suggestion.type === 'latex') { + inner = ; + key = suggestion.expression; } return ( @@ -215,6 +234,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { value={value} onChange={this.onChange} onKeyDown={this.onKeyDown} + onInput={this.onInput} onKeyUp={onKeyUp} onFocus={this.onFocus} onBlur={this.onBlur} diff --git a/app/javascript/flavours/glitch/components/status.jsx b/app/javascript/flavours/glitch/components/status.jsx index 36abc699304bd6..ccd62f20de3d9a 100644 --- a/app/javascript/flavours/glitch/components/status.jsx +++ b/app/javascript/flavours/glitch/components/status.jsx @@ -7,13 +7,12 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { HotKeys } from 'react-hotkeys'; - import PictureInPicturePlaceholder from 'flavours/glitch/components/picture_in_picture_placeholder'; import PollContainer from 'flavours/glitch/containers/poll_container'; import NotificationOverlayContainer from 'flavours/glitch/features/notifications/containers/overlay_container'; import { displayMedia } from 'flavours/glitch/initial_state'; import { autoUnfoldCW } from 'flavours/glitch/utils/content_warning'; +import { HotKeys } from 'react-hotkeys'; import Card from '../features/status/components/card'; import Bundle from '../features/ui/components/bundle'; @@ -22,9 +21,9 @@ import { MediaGallery, Video, Audio } from '../features/ui/util/async-components import AttachmentList from './attachment_list'; import StatusActionBar from './status_action_bar'; import StatusContent from './status_content'; +import StatusExpandButton from './status_expand_button'; import StatusHeader from './status_header'; import StatusIcons from './status_icons'; -import StatusPrepend from './status_prepend'; const domParser = new DOMParser(); @@ -832,6 +831,14 @@ class Status extends ImmutablePureComponent { tagLinks={settings.get('tag_misleading_links')} rewriteMentions={settings.get('rewrite_mentions')} /> + {/* Only show expand button if collapsed and no spoiler tag is present */} + {isCollapsed && status.get('spoiler_text').length===0 ? ( +