From bfc509f44ac5506d1d675eb01aeda1324d4cdb6b Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 12 May 2019 20:14:35 +0200 Subject: Add database row to hold status content type --- app/models/status.rb | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/models/status.rb b/app/models/status.rb index 8736e65e3..e0dc74790 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -24,6 +24,7 @@ # local_only :boolean # full_status_text :text default(""), not null # poll_id :bigint(8) +# content_type :string # class Status < ApplicationRecord @@ -74,6 +75,7 @@ class Status < ApplicationRecord validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? + validates :content_type, inclusion: { in: %w(text/plain text/markdown) }, allow_nil: true accepts_nested_attributes_for :poll -- cgit From 94aef563b9abbc449028f44c4aac84ef2e072d89 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 12 May 2019 20:20:05 +0200 Subject: Add support for markdown-formatted toots --- Gemfile | 2 ++ Gemfile.lock | 2 ++ app/lib/formatter.rb | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 6 deletions(-) (limited to 'app') diff --git a/Gemfile b/Gemfile index b9a9159de..31cf743a4 100644 --- a/Gemfile +++ b/Gemfile @@ -95,6 +95,8 @@ gem 'json-ld', '~> 3.0' gem 'json-ld-preloaded', '~> 3.0' gem 'rdf-normalize', '~> 0.3' +gem 'redcarpet', '~> 3.4' + group :development, :test do gem 'fabrication', '~> 2.20' gem 'fuubar', '~> 2.3' diff --git a/Gemfile.lock b/Gemfile.lock index 98cd2103d..8e3f1faf7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -480,6 +480,7 @@ GEM link_header (~> 0.0, >= 0.0.8) rdf-normalize (0.3.3) rdf (>= 2.2, < 4.0) + redcarpet (3.4.0) redis (4.1.1) redis-actionpack (5.0.2) actionpack (>= 4.0, < 6) @@ -745,6 +746,7 @@ DEPENDENCIES rails-i18n (~> 5.1) rails-settings-cached (~> 0.6) rdf-normalize (~> 0.3) + redcarpet (~> 3.4) redis (~> 4.1) redis-namespace (~> 1.5) redis-rails (~> 5.0) diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 8a1aad41a..fe5b5b7b7 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -36,14 +36,52 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog - html = encode_and_link_urls(html, linkable_accounts) + html = format_markdown(html) if status.content_type == 'text/markdown' + html = encode_and_link_urls(html, linkable_accounts, keep_html: status.content_type == 'text/markdown') html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - html = simple_format(html, {}, sanitize: false) + html = simple_format(html, {}, sanitize: false) unless status.content_type == 'text/markdown' html = html.delete("\n") html.html_safe # rubocop:disable Rails/OutputSafety end + def format_markdown(html) + extensions = { + autolink: false, + no_intra_emphasis: true, + fenced_code_blocks: true, + disable_indented_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + underline: true, + highlight: true, + footnotes: true + } + + renderer = Redcarpet::Render::HTML.new({ + filter_html: false, + no_images: true, + no_styles: true, + safe_links_only: true, + hard_wrap: true, + link_attributes: { target: '_blank', rel: 'nofollow noopener' }, + }) + + markdown = Redcarpet::Markdown.new(renderer, extensions) + + html = reformat(markdown.render(html)) + html = html.gsub("\r\n", "\n").gsub("\r", "\n") + code_safe_strip(html) + end + + def code_safe_strip(html, char="\n") + html = html.split(/(].*?\/code>)/m) + html.each_slice(2) { |part| part[0].delete!(char) } + html.join + end + def reformat(html) sanitize(html, Sanitize::Config::MASTODON_STRICT) end @@ -116,7 +154,7 @@ class Formatter accounts = nil end - rewrite(html.dup, entities) do |entity| + rewrite(html.dup, entities, options[:keep_html]) do |entity| if entity[:url] link_to_url(entity, options) elsif entity[:hashtag] @@ -186,7 +224,7 @@ class Formatter html end - def rewrite(text, entities) + def rewrite(text, entities, keep_html = false) text = text.to_s # Sort by start index @@ -199,12 +237,12 @@ class Formatter last_index = entities.reduce(0) do |index, entity| indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices] - result << encode(text[index...indices.first]) + result << (keep_html ? text[index...indices.first] : encode(text[index...indices.first])) result << yield(entity) indices.last end - result << encode(text[last_index..-1]) + result << (keep_html ? text[last_index..-1] : encode(text[last_index..-1])) result.flatten.join end -- cgit From d7520f81759e9db58a5b5ca61b9114e9ef44d92f Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 12 May 2019 21:33:37 +0200 Subject: Add support for HTML-formatted toots --- app/lib/formatter.rb | 4 ++-- app/models/status.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index fe5b5b7b7..eaece8797 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -37,9 +37,9 @@ class Formatter html = raw_content html = "RT @#{prepend_reblog} #{html}" if prepend_reblog html = format_markdown(html) if status.content_type == 'text/markdown' - html = encode_and_link_urls(html, linkable_accounts, keep_html: status.content_type == 'text/markdown') + html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type)) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - html = simple_format(html, {}, sanitize: false) unless status.content_type == 'text/markdown' + html = simple_format(html, {}, sanitize: false) unless %w(text/markdown text/html).include?(status.content_type) html = html.delete("\n") html.html_safe # rubocop:disable Rails/OutputSafety diff --git a/app/models/status.rb b/app/models/status.rb index e0dc74790..6f3ba4cc3 100644 --- a/app/models/status.rb +++ b/app/models/status.rb @@ -75,7 +75,7 @@ class Status < ApplicationRecord validates_with DisallowedHashtagsValidator validates :reblog, uniqueness: { scope: :account }, if: :reblog? validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog? - validates :content_type, inclusion: { in: %w(text/plain text/markdown) }, allow_nil: true + validates :content_type, inclusion: { in: %w(text/plain text/markdown text/html) }, allow_nil: true accepts_nested_attributes_for :poll -- cgit From b3e68de4d2093112db976d6f3a83817142264ee0 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 12 May 2019 20:34:22 +0200 Subject: Serialize content-type on Delete & Redraft --- app/serializers/rest/status_serializer.rb | 1 + 1 file changed, 1 insertion(+) (limited to 'app') diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 906f489db..b07937014 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -15,6 +15,7 @@ class REST::StatusSerializer < ActiveModel::Serializer attribute :content, unless: :source_requested? attribute :text, if: :source_requested? + attribute :content_type, if: :source_requested? belongs_to :reblog, serializer: REST::StatusSerializer belongs_to :application, if: :show_application? -- cgit From 14d7ddcb1c1000a0f43da59df07f60c93620b53b Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 12 May 2019 20:15:39 +0200 Subject: Add API support for setting status content-type --- app/controllers/api/v1/statuses_controller.rb | 2 ++ app/services/post_status_service.rb | 1 + 2 files changed, 3 insertions(+) (limited to 'app') diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index b0e134554..26a0ab457 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -54,6 +54,7 @@ class Api::V1::StatusesController < Api::BaseController scheduled_at: status_params[:scheduled_at], application: doorkeeper_token.application, poll: status_params[:poll], + content_type: status_params[:content_type], idempotency: request.headers['Idempotency-Key']) render json: @status, serializer: @status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer @@ -85,6 +86,7 @@ class Api::V1::StatusesController < Api::BaseController :spoiler_text, :visibility, :scheduled_at, + :content_type, media_ids: [], poll: [ :multiple, diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index d2cca145b..25aa6629c 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -168,6 +168,7 @@ class PostStatusService < BaseService visibility: @visibility, language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account), application: @options[:application], + content_type: @options[:content_type], }.compact end -- cgit From f34a402a94bd5a0c9860165ff50c93b300fd1756 Mon Sep 17 00:00:00 2001 From: KokaKiwi Date: Sun, 17 Feb 2019 23:53:51 +0100 Subject: Add status content type dropdown to compose box. Signed-off-by: Thibaut Girka --- app/javascript/flavours/glitch/actions/compose.js | 9 ++++ .../features/compose/components/compose_form.js | 7 +++ .../glitch/features/compose/components/options.js | 51 ++++++++++++++++++++++ .../compose/containers/compose_form_container.js | 6 +++ .../flavours/glitch/features/compose/index.js | 2 +- app/javascript/flavours/glitch/reducers/compose.js | 6 +++ 6 files changed, 80 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index f117ce771..2fb97fa17 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -46,6 +46,7 @@ export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; +export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE'; export const COMPOSE_EMOJI_INSERT = 'COMPOSE_EMOJI_INSERT'; @@ -147,6 +148,7 @@ export function submitCompose(routerHistory) { } api(getState).post('/api/v1/statuses', { status, + content_type: getState().getIn(['compose', 'content_type']), in_reply_to_id: getState().getIn(['compose', 'in_reply_to'], null), media_ids: media.map(item => item.get('id')), sensitive: getState().getIn(['compose', 'sensitive']) || (spoilerText.length > 0 && media.size !== 0), @@ -517,6 +519,13 @@ export function changeComposeVisibility(value) { }; }; +export function changeComposeContentType(value) { + return { + type: COMPOSE_CONTENT_TYPE_CHANGE, + value, + }; +}; + export function insertEmojiCompose(position, emoji) { return { type: COMPOSE_EMOJI_INSERT, diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index e8f000b1e..4e93e2d84 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -38,6 +38,7 @@ class ComposeForm extends ImmutablePureComponent { suggestions: ImmutablePropTypes.list, spoiler: PropTypes.bool, privacy: PropTypes.string, + contentType: PropTypes.string, spoilerText: PropTypes.string, focusDate: PropTypes.instanceOf(Date), caretPosition: PropTypes.number, @@ -66,6 +67,7 @@ class ComposeForm extends ImmutablePureComponent { preselectOnReply: PropTypes.bool, onChangeSpoilerness: PropTypes.func, onChangeVisibility: PropTypes.func, + onChangeContentType: PropTypes.func, onMount: PropTypes.func, onUnmount: PropTypes.func, onPaste: PropTypes.func, @@ -285,10 +287,12 @@ class ComposeForm extends ImmutablePureComponent { media, onChangeSpoilerness, onChangeVisibility, + onChangeContentType, onClearSuggestions, onFetchSuggestions, onPaste, privacy, + contentType, sensitive, showSearch, sideArm, @@ -356,9 +360,11 @@ class ComposeForm extends ImmutablePureComponent { advancedOptions={advancedOptions} disabled={isSubmitting} onChangeVisibility={onChangeVisibility} + onChangeContentType={onChangeContentType} onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness} onUpload={onPaste} privacy={privacy} + contentType={contentType} sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} /> @@ -369,6 +375,7 @@ class ComposeForm extends ImmutablePureComponent { onSecondarySubmit={handleSecondarySubmit} onSubmit={handleSubmit} privacy={privacy} + contentType={contentType} sideArm={sideArm} /> diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index ee9730961..908126c6f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -29,6 +29,10 @@ const messages = defineMessages({ defaultMessage: 'Adjust status privacy', id: 'privacy.change', }, + content_type: { + defaultMessage: 'Content type', + id: 'content-type.change', + }, direct_long: { defaultMessage: 'Post to mentioned users only', id: 'privacy.direct.long', @@ -41,6 +45,10 @@ const messages = defineMessages({ defaultMessage: 'Draw something', id: 'compose.attach.doodle', }, + html: { + defaultMessage: 'HTML', + id: 'compose.content-type.html', + }, local_only_long: { defaultMessage: 'Do not post to other instances', id: 'advanced_options.local-only.long', @@ -49,6 +57,14 @@ const messages = defineMessages({ defaultMessage: 'Local-only', id: 'advanced_options.local-only.short', }, + markdown: { + defaultMessage: 'Markdown', + id: 'compose.content-type.markdown', + }, + plain: { + defaultMessage: 'Plain text', + id: 'compose.content-type.plain', + }, private_long: { defaultMessage: 'Post to followers only', id: 'privacy.private.long', @@ -113,6 +129,7 @@ class ComposerOptions extends ImmutablePureComponent { intl: PropTypes.object.isRequired, onChangeAdvancedOption: PropTypes.func, onChangeVisibility: PropTypes.func, + onChangeContentType: PropTypes.func, onTogglePoll: PropTypes.func, onDoodleOpen: PropTypes.func, onModalClose: PropTypes.func, @@ -120,6 +137,7 @@ class ComposerOptions extends ImmutablePureComponent { onToggleSpoiler: PropTypes.func, onUpload: PropTypes.func, privacy: PropTypes.string, + contentType: PropTypes.string, resetFileKey: PropTypes.number, spoiler: PropTypes.bool, }; @@ -162,6 +180,7 @@ class ComposerOptions extends ImmutablePureComponent { const { acceptContentTypes, advancedOptions, + contentType, disabled, allowMedia, hasMedia, @@ -169,6 +188,7 @@ class ComposerOptions extends ImmutablePureComponent { hasPoll, intl, onChangeAdvancedOption, + onChangeContentType, onChangeVisibility, onTogglePoll, onModalClose, @@ -208,6 +228,24 @@ class ComposerOptions extends ImmutablePureComponent { }, }; + const contentTypeItems = { + plain: { + icon: 'file', + name: 'text/plain', + text: , + }, + html: { + icon: 'file-text', + name: 'text/html', + text: , + }, + markdown: { + icon: 'file-text', + name: 'text/markdown', + text: , + }, + }; + // The result. return (
@@ -272,6 +310,19 @@ class ComposerOptions extends ImmutablePureComponent { title={intl.formatMessage(messages.change_privacy)} value={privacy} /> + {onToggleSpoiler && ( ({ dispatch(changeComposeSpoilerText(text)); }, + onChangeContentType(value) { + dispatch(changeComposeContentType(value)); + }, + onPaste(files) { dispatch(uploadCompose(files)); }, diff --git a/app/javascript/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js index a7795a04d..e60eedfd9 100644 --- a/app/javascript/flavours/glitch/features/compose/index.js +++ b/app/javascript/flavours/glitch/features/compose/index.js @@ -29,7 +29,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ }, }); -export default @connect(mapStateToProps, mapDispatchToProps) +export default @connect(mapStateToProps) @injectIntl class Compose extends React.PureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index bc1785a48..516da859a 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -25,6 +25,7 @@ import { COMPOSE_SPOILERNESS_CHANGE, COMPOSE_SPOILER_TEXT_CHANGE, COMPOSE_VISIBILITY_CHANGE, + COMPOSE_CONTENT_TYPE_CHANGE, COMPOSE_EMOJI_INSERT, COMPOSE_UPLOAD_CHANGE_REQUEST, COMPOSE_UPLOAD_CHANGE_SUCCESS, @@ -66,6 +67,7 @@ const initialState = ImmutableMap({ spoiler: false, spoiler_text: '', privacy: null, + content_type: 'text/plain', text: '', focusDate: null, caretPosition: null, @@ -310,6 +312,10 @@ export default function compose(state = initialState, action) { return state .set('privacy', action.value) .set('idempotencyKey', uuid()); + case COMPOSE_CONTENT_TYPE_CHANGE: + return state + .set('content_type', action.value) + .set('idempotencyKey', uuid()); case COMPOSE_CHANGE: return state .set('text', action.text) -- cgit From 93d98f62a8b35a01e11a41750dc39c6a287410d9 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 12 May 2019 20:35:58 +0200 Subject: Support proper content-type in Delete & Redraft --- app/javascript/flavours/glitch/actions/statuses.js | 5 +++-- app/javascript/flavours/glitch/reducers/compose.js | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/actions/statuses.js b/app/javascript/flavours/glitch/actions/statuses.js index 550fe510f..7e22a7f98 100644 --- a/app/javascript/flavours/glitch/actions/statuses.js +++ b/app/javascript/flavours/glitch/actions/statuses.js @@ -71,11 +71,12 @@ export function fetchStatusFail(id, error, skipLoading) { }; }; -export function redraft(status, raw_text) { +export function redraft(status, raw_text, content_type) { return { type: REDRAFT, status, raw_text, + content_type, }; }; @@ -94,7 +95,7 @@ export function deleteStatus(id, router, withRedraft = false) { dispatch(deleteFromTimelines(id)); if (withRedraft) { - dispatch(redraft(status, response.data.text)); + dispatch(redraft(status, response.data.text, response.data.content_type)); if (!getState().getIn(['compose', 'mounted'])) { router.push('/statuses/new'); diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 516da859a..82facb1e2 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -433,6 +433,7 @@ export default function compose(state = initialState, action) { case REDRAFT: return state.withMutations(map => { map.set('text', action.raw_text || unescapeHTML(expandMentions(action.status))); + map.set('content_type', action.content_type || 'text/plain'); map.set('in_reply_to', action.status.get('in_reply_to_id')); map.set('privacy', action.status.get('visibility')); map.set('media_attachments', action.status.get('media_attachments')); -- cgit From c2fa36bbaeb8c71adc14f1b5d4c23c7a5adfda6c Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 1 Mar 2019 11:45:16 +0100 Subject: Content-Type Dropdown: use the selected option icon for the menu Signed-off-by: Thibaut Girka --- .../flavours/glitch/features/compose/components/options.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 908126c6f..9bec36a7d 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -230,17 +230,17 @@ class ComposerOptions extends ImmutablePureComponent { const contentTypeItems = { plain: { - icon: 'file', + icon: 'align-left', name: 'text/plain', text: , }, html: { - icon: 'file-text', + icon: 'code', name: 'text/html', text: , }, markdown: { - icon: 'file-text', + icon: 'arrow-circle-down', name: 'text/markdown', text: , }, @@ -311,7 +311,8 @@ class ComposerOptions extends ImmutablePureComponent { value={privacy} /> Date: Sun, 12 May 2019 21:55:44 +0200 Subject: Hide content-type dropdown by default --- .../glitch/features/compose/components/options.js | 32 ++++++++++++---------- .../compose/containers/options_container.js | 1 + .../glitch/features/local_settings/page/index.js | 8 ++++++ .../flavours/glitch/reducers/local_settings.js | 1 + 4 files changed, 28 insertions(+), 14 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js index 9bec36a7d..0c94f5514 100644 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ b/app/javascript/flavours/glitch/features/compose/components/options.js @@ -140,6 +140,7 @@ class ComposerOptions extends ImmutablePureComponent { contentType: PropTypes.string, resetFileKey: PropTypes.number, spoiler: PropTypes.bool, + showContentTypeChoice: PropTypes.bool, }; // Handles file selection. @@ -197,6 +198,7 @@ class ComposerOptions extends ImmutablePureComponent { privacy, resetFileKey, spoiler, + showContentTypeChoice, } = this.props; // We predefine our privacy items so that we can easily pick the @@ -310,20 +312,22 @@ class ComposerOptions extends ImmutablePureComponent { title={intl.formatMessage(messages.change_privacy)} value={privacy} /> - + {showContentTypeChoice && ( + + )} {onToggleSpoiler && ( item.get('type') === 'video') : true), hasMedia: media && !!media.size, allowPoll: !(media && !!media.size), + showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']), }; }; diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.js b/app/javascript/flavours/glitch/features/local_settings/page/index.js index a13bffa3a..cd2d86713 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.js @@ -151,6 +151,14 @@ export default class LocalSettingsPage extends React.PureComponent { > + + + Date: Sun, 12 May 2019 22:13:36 +0200 Subject: Fix autolinking, and newlines in code blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Autolinking is now performed *after* the Markdown pass, by replacing HTML tags with zero-width spaces and running the twitter-text extractor as usual, except it does not auto-link URLs to avoid links in links… --- app/lib/formatter.rb | 65 ++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 15 deletions(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index eaece8797..2c509ef19 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -3,6 +3,17 @@ require 'singleton' require_relative './sanitize_config' +class HTMLRenderer < Redcarpet::Render::HTML + def block_code(code, language) + "
#{code.gsub("\n", "
")}
" + end + + def autolink(link, link_type) + return link if link_type == :email + Formatter.instance.link_url(link) + end +end + class Formatter include Singleton include RoutingHelper @@ -39,15 +50,18 @@ class Formatter html = format_markdown(html) if status.content_type == 'text/markdown' html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type)) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - html = simple_format(html, {}, sanitize: false) unless %w(text/markdown text/html).include?(status.content_type) - html = html.delete("\n") + + unless %w(text/markdown text/html).include?(status.content_type) + html = simple_format(html, {}, sanitize: false) + html = html.delete("\n") + end html.html_safe # rubocop:disable Rails/OutputSafety end def format_markdown(html) extensions = { - autolink: false, + autolink: true, no_intra_emphasis: true, fenced_code_blocks: true, disable_indented_code_blocks: true, @@ -57,11 +71,12 @@ class Formatter superscript: true, underline: true, highlight: true, - footnotes: true + footnotes: false, } - renderer = Redcarpet::Render::HTML.new({ + renderer = HTMLRenderer.new({ filter_html: false, + escape_html: false, no_images: true, no_styles: true, safe_links_only: true, @@ -72,14 +87,7 @@ class Formatter markdown = Redcarpet::Markdown.new(renderer, extensions) html = reformat(markdown.render(html)) - html = html.gsub("\r\n", "\n").gsub("\r", "\n") - code_safe_strip(html) - end - - def code_safe_strip(html, char="\n") - html = html.split(/(].*?\/code>)/m) - html.each_slice(2) { |part| part[0].delete!(char) } - html.join + html.delete("\r").delete("\n") end def reformat(html) @@ -136,6 +144,10 @@ class Formatter html.html_safe # rubocop:disable Rails/OutputSafety end + def link_url(url) + "#{link_html(url)}" + end + private def html_entities @@ -147,13 +159,13 @@ class Formatter end def encode_and_link_urls(html, accounts = nil, options = {}) - entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) - if accounts.is_a?(Hash) options = accounts accounts = nil end + entities = options[:keep_html] ? html_friendly_extractor(html) : utf8_friendly_extractor(html, extract_url_without_protocol: false) + rewrite(html.dup, entities, options[:keep_html]) do |entity| if entity[:url] link_to_url(entity, options) @@ -285,6 +297,29 @@ class Formatter Extractor.remove_overlapping_entities(special + standard) end + def html_friendly_extractor(html, options = {}) + gaps = [] + total_offset = 0 + + escaped = html.gsub(/<[^>]*>/) do |match| + total_offset += match.length - 1 + end_offset = Regexp.last_match.end(0) + gaps << [end_offset - total_offset, total_offset] + "\u200b" + end + + entities = Extractor.extract_hashtags_with_indices(escaped, :check_url_overlap => false) + + Extractor.extract_mentions_or_lists_with_indices(escaped) + Extractor.remove_overlapping_entities(entities).map do |extract| + pos = extract[:indices].first + offset_idx = gaps.rindex { |gap| gap.first <= pos } + offset = offset_idx.nil? ? 0 : gaps[offset_idx].last + next extract.merge( + :indices => [extract[:indices].first + offset, extract[:indices].last + offset] + ) + end + end + def link_to_url(entity, options = {}) url = Addressable::URI.parse(entity[:url]) html_attrs = { target: '_blank', rel: 'nofollow noopener' } -- cgit From a6b7c23f6fd33c209f83562fffb46211e062312e Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 13 May 2019 14:42:39 +0200 Subject: Add option for default toot content-type --- app/controllers/settings/preferences_controller.rb | 1 + app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 2 +- app/services/post_status_service.rb | 2 +- app/views/settings/preferences/show.html.haml | 2 ++ config/locales/simple_form.en.yml | 7 +++++++ config/settings.yml | 1 + 7 files changed, 18 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index eb7a0eb4a..3d98d583c 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -46,6 +46,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_hide_followers_count, :setting_aggregate_reblogs, :setting_show_application, + :setting_default_content_type, notification_emails: %i(follow follow_request reblog favourite mention digest report pending_account), interactions: %i(must_be_follower must_be_following) ) diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index 367ba9a83..802ca71fe 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -36,6 +36,7 @@ class UserSettingsDecorator user.settings['hide_network'] = hide_network_preference if change?('setting_hide_network') user.settings['aggregate_reblogs'] = aggregate_reblogs_preference if change?('setting_aggregate_reblogs') user.settings['show_application'] = show_application_preference if change?('setting_show_application') + user.settings['default_content_type']= default_content_type_preference if change?('setting_default_content_type') end def merged_notification_emails @@ -122,6 +123,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_aggregate_reblogs' end + def default_content_type_preference + settings['setting_default_content_type'] + end + def boolean_cast_setting(key) ActiveModel::Type::Boolean.new.cast(settings[key]) end diff --git a/app/models/user.rb b/app/models/user.rb index 8985ebf53..a245d3eb2 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -104,7 +104,7 @@ class User < ApplicationRecord delegate :auto_play_gif, :default_sensitive, :unfollow_modal, :boost_modal, :favourite_modal, :delete_modal, :reduce_motion, :system_font_ui, :noindex, :flavour, :skin, :display_media, :hide_network, :hide_followers_count, - :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, to: :settings, prefix: :setting, allow_nil: false + :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :default_content_type, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code attr_writer :external diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index 25aa6629c..c2584e090 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -168,7 +168,7 @@ class PostStatusService < BaseService visibility: @visibility, language: language_from_option(@options[:language]) || @account.user&.setting_default_language&.presence || LanguageDetector.instance.detect(@text, @account), application: @options[:application], - content_type: @options[:content_type], + content_type: @options[:content_type] || @account.user&.setting_default_content_type, }.compact end diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index a50f33517..cd5bf9be2 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -25,6 +25,8 @@ .fields-group = f.input :setting_default_privacy, collection: Status.selectable_visibilities, wrapper: :with_floating_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + = f.input :setting_default_content_type, collection: ['text/plain', 'text/markdown', 'text/html'], wrapper: :with_label, include_blank: false, label_method: lambda { |item| safe_join([t("simple_form.labels.defaults.setting_default_content_type_#{item.split('/')[1]}"), content_tag(:span, t("simple_form.hints.defaults.setting_default_content_type_#{item.split('/')[1]}"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label %hr#settings_other/ diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index ba0e403e4..6fad7f73a 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -27,6 +27,9 @@ en: phrase: Will be matched regardless of casing in text or content warning of a toot scopes: Which APIs the application will be allowed to access. If you select a top-level scope, you don't need to select individual ones. setting_aggregate_reblogs: Do not show new boosts for toots that have been recently boosted (only affects newly-received boosts) + setting_default_content_type_html: When writing toots, assume they are written in raw HTML, unless specified otherwise + setting_default_content_type_markdown: When writing toots, assume they are using Markdown for rich text formatting, unless specified otherwise + setting_default_content_type_plain: When writing toots, assume they are plain text with no special formatting, unless specified otherwise (default Mastodon behavior) setting_default_language: The language of your toots can be detected automatically, but it's not always accurate setting_display_media_default: Hide media marked as sensitive setting_display_media_hide_all: Always hide all media @@ -93,6 +96,10 @@ en: setting_aggregate_reblogs: Group boosts in timelines setting_auto_play_gif: Auto-play animated GIFs setting_boost_modal: Show confirmation dialog before boosting + setting_default_content_type: Default format for toots + setting_default_content_type_html: HTML + setting_default_content_type_markdown: Markdown + setting_default_content_type_plain: Plain text setting_default_language: Posting language setting_default_privacy: Post privacy setting_default_sensitive: Always mark media as sensitive diff --git a/config/settings.yml b/config/settings.yml index c3aeab551..69996af25 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -64,6 +64,7 @@ defaults: &defaults show_known_fediverse_at_about_page: true show_reblogs_in_public_timelines: false show_replies_in_public_timelines: false + default_content_type: 'text/plain' development: <<: *defaults -- cgit From dd5bf40b97d42daae855cd05ac13c6efa6cda4f6 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 17 May 2019 10:43:17 +0200 Subject: Properly escape HTML in code blocks --- app/lib/formatter.rb | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 2c509ef19..ccebf4353 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -5,13 +5,23 @@ require_relative './sanitize_config' class HTMLRenderer < Redcarpet::Render::HTML def block_code(code, language) - "
#{code.gsub("\n", "
")}
" + "
#{encode(code).gsub("\n", "
")}
" end def autolink(link, link_type) return link if link_type == :email Formatter.instance.link_url(link) end + + private + + def html_entities + @html_entities ||= HTMLEntities.new + end + + def encode(html) + html_entities.encode(html) + end end class Formatter -- cgit From 56245a2a7204bb63840e633e923bfd1679c793d9 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 17 May 2019 10:52:27 +0200 Subject: Export fallback content type and use it as default in WebUI --- app/javascript/flavours/glitch/reducers/compose.js | 6 ++++-- app/javascript/flavours/glitch/util/initial_state.js | 1 + app/serializers/initial_state_serializer.rb | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 82facb1e2..c0c2fc547 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -45,7 +45,7 @@ import { REDRAFT } from 'flavours/glitch/actions/statuses'; import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; import uuid from 'flavours/glitch/util/uuid'; import { privacyPreference } from 'flavours/glitch/util/privacy_preference'; -import { me } from 'flavours/glitch/util/initial_state'; +import { me, defaultContentType } from 'flavours/glitch/util/initial_state'; import { overwrite } from 'flavours/glitch/util/js_helpers'; import { unescapeHTML } from 'flavours/glitch/util/html'; import { recoverHashtags } from 'flavours/glitch/util/hashtag'; @@ -67,7 +67,7 @@ const initialState = ImmutableMap({ spoiler: false, spoiler_text: '', privacy: null, - content_type: 'text/plain', + content_type: defaultContentType || 'text/plain', text: '', focusDate: null, caretPosition: null, @@ -143,6 +143,7 @@ function apiStatusToTextHashtags (state, status) { function clearAll(state) { return state.withMutations(map => { map.set('text', ''); + if (defaultContentType) map.set('content_type', defaultContentType); map.set('spoiler', false); map.set('spoiler_text', ''); map.set('is_submitting', false); @@ -354,6 +355,7 @@ export default function compose(state = initialState, action) { case COMPOSE_RESET: return state.withMutations(map => { map.set('in_reply_to', null); + if (defaultContentType) map.set('content_type', defaultContentType); map.set('text', ''); map.set('spoiler', false); map.set('spoiler_text', ''); diff --git a/app/javascript/flavours/glitch/util/initial_state.js b/app/javascript/flavours/glitch/util/initial_state.js index 62588eeaa..99d8a4dbc 100644 --- a/app/javascript/flavours/glitch/util/initial_state.js +++ b/app/javascript/flavours/glitch/util/initial_state.js @@ -27,5 +27,6 @@ export const invitesEnabled = getMeta('invites_enabled'); export const version = getMeta('version'); export const mascot = getMeta('mascot'); export const isStaff = getMeta('is_staff'); +export const defaultContentType = getMeta('default_content_type'); export default initialState; diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 850169af8..d74e56ebc 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -47,6 +47,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers store[:reduce_motion] = object.current_account.user.setting_reduce_motion store[:is_staff] = object.current_account.user.staff? + store[:default_content_type] = object.current_account.user.setting_default_content_type end store -- cgit From 9ca21e93cc1506d0a3c0cfe450636933c3d2388a Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Fri, 17 May 2019 11:14:09 +0200 Subject: Minor optimization --- app/lib/formatter.rb | 58 ++++++++++++++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 27 deletions(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index ccebf4353..78a0e9f25 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -70,33 +70,7 @@ class Formatter end def format_markdown(html) - extensions = { - autolink: true, - no_intra_emphasis: true, - fenced_code_blocks: true, - disable_indented_code_blocks: true, - strikethrough: true, - lax_spacing: true, - space_after_headers: true, - superscript: true, - underline: true, - highlight: true, - footnotes: false, - } - - renderer = HTMLRenderer.new({ - filter_html: false, - escape_html: false, - no_images: true, - no_styles: true, - safe_links_only: true, - hard_wrap: true, - link_attributes: { target: '_blank', rel: 'nofollow noopener' }, - }) - - markdown = Redcarpet::Markdown.new(renderer, extensions) - - html = reformat(markdown.render(html)) + html = reformat(markdown_formatter.render(html)) html.delete("\r").delete("\n") end @@ -160,6 +134,36 @@ class Formatter private + def markdown_formatter + return @markdown_formatter if defined?(@markdown_formatter) + + extensions = { + autolink: true, + no_intra_emphasis: true, + fenced_code_blocks: true, + disable_indented_code_blocks: true, + strikethrough: true, + lax_spacing: true, + space_after_headers: true, + superscript: true, + underline: true, + highlight: true, + footnotes: false, + } + + renderer = HTMLRenderer.new({ + filter_html: false, + escape_html: false, + no_images: true, + no_styles: true, + safe_links_only: true, + hard_wrap: true, + link_attributes: { target: '_blank', rel: 'nofollow noopener' }, + }) + + @markdown_formatter = Redcarpet::Markdown.new(renderer, extensions) + end + def html_entities @html_entities ||= HTMLEntities.new end -- cgit From a1519a8ef564ed3773f3a0d1613cbe1c5d6f8459 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sat, 18 May 2019 00:28:51 +0200 Subject: Prevent from publicly boosting one's own private toots (#10775) --- app/services/reblog_service.rb | 4 +++- spec/services/reblog_service_spec.rb | 12 ++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/services/reblog_service.rb b/app/services/reblog_service.rb index ff48d9c75..1710640c8 100644 --- a/app/services/reblog_service.rb +++ b/app/services/reblog_service.rb @@ -18,7 +18,9 @@ class ReblogService < BaseService return reblog unless reblog.nil? - reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: options[:visibility] || account.user&.setting_default_privacy) + visibility = options[:visibility] || account.user&.setting_default_privacy + visibility = reblogged_status.visibility if reblogged_status.hidden? + reblog = account.statuses.create!(reblog: reblogged_status, text: '', visibility: visibility) DistributionWorker.perform_async(reblog.id) Pubsubhubbub::DistributionWorker.perform_async(reblog.stream_entry.id) diff --git a/spec/services/reblog_service_spec.rb b/spec/services/reblog_service_spec.rb index 9e66c6643..9d84c41d5 100644 --- a/spec/services/reblog_service_spec.rb +++ b/spec/services/reblog_service_spec.rb @@ -4,10 +4,9 @@ RSpec.describe ReblogService, type: :service do let(:alice) { Fabricate(:account, username: 'alice') } context 'creates a reblog with appropriate visibility' do - let(:bob) { Fabricate(:account, username: 'bob') } let(:visibility) { :public } let(:reblog_visibility) { :public } - let(:status) { Fabricate(:status, account: bob, visibility: visibility) } + let(:status) { Fabricate(:status, account: alice, visibility: visibility) } subject { ReblogService.new } @@ -22,6 +21,15 @@ RSpec.describe ReblogService, type: :service do expect(status.reblogs.first.visibility).to eq 'private' end end + + describe 'public reblogs of private toots should remain private' do + let(:visibility) { :private } + let(:reblog_visibility) { :public } + + it 'reblogs privately' do + expect(status.reblogs.first.visibility).to eq 'private' + end + end end context 'OStatus' do -- cgit From 6fe474837c6454ebd03ba7fa0c845a2dde5734d1 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 18 May 2019 14:41:16 +0200 Subject: Change poll options to alphabetic letters when status text is hidden (#10685) Fix #10569 --- app/javascript/mastodon/components/poll.js | 14 ++++++++------ app/javascript/mastodon/components/status.js | 2 +- .../mastodon/features/status/components/detailed_status.js | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) (limited to 'app') diff --git a/app/javascript/mastodon/components/poll.js b/app/javascript/mastodon/components/poll.js index 690f9ae5a..acab107a1 100644 --- a/app/javascript/mastodon/components/poll.js +++ b/app/javascript/mastodon/components/poll.js @@ -28,6 +28,7 @@ class Poll extends ImmutablePureComponent { intl: PropTypes.object.isRequired, dispatch: PropTypes.func, disabled: PropTypes.bool, + visible: PropTypes.bool, }; state = { @@ -69,13 +70,14 @@ class Poll extends ImmutablePureComponent { }; renderOption (option, optionIndex) { - const { poll, disabled } = this.props; - const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; - const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); - const active = !!this.state.selected[`${optionIndex}`]; - const showResults = poll.get('voted') || poll.get('expired'); + const { poll, disabled, visible } = this.props; + const percent = poll.get('votes_count') === 0 ? 0 : (option.get('votes_count') / poll.get('votes_count')) * 100; + const leading = poll.get('options').filterNot(other => other.get('title') === option.get('title')).every(other => option.get('votes_count') > other.get('votes_count')); + const active = !!this.state.selected[`${optionIndex}`]; + const showResults = poll.get('voted') || poll.get('expired'); let titleEmojified = option.get('title_emojified'); + if (!titleEmojified) { const emojiMap = makeEmojiMap(poll); titleEmojified = emojify(escapeTextContentForBrowser(option.get('title')), emojiMap); @@ -104,7 +106,7 @@ class Poll extends ImmutablePureComponent { {!showResults && } {showResults && {Math.round(percent)}%} - + {visible ? : {String.fromCharCode(64 + optionIndex + 1)}} ); diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 42535ea68..6f66a4260 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -272,7 +272,7 @@ class Status extends ImmutablePureComponent { } if (status.get('poll')) { - media = ; + media = ; } else if (status.get('media_attachments').size > 0) { if (this.props.muted) { media = ( diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js index 84471f9a3..059ecd979 100644 --- a/app/javascript/mastodon/features/status/components/detailed_status.js +++ b/app/javascript/mastodon/features/status/components/detailed_status.js @@ -106,7 +106,7 @@ export default class DetailedStatus extends ImmutablePureComponent { } if (status.get('poll')) { - media = ; + media = ; } else if (status.get('media_attachments').size > 0) { if (status.getIn(['media_attachments', 0, 'type']) === 'video') { const video = status.getIn(['media_attachments', 0]); -- cgit From 2f3e4a64be4b98167a5690fdd2f969f1b7f7820c Mon Sep 17 00:00:00 2001 From: Ben Lubar Date: Sat, 18 May 2019 13:57:45 -0500 Subject: add og:image:alt for media attachments in embeds (#10779) --- app/views/stream_entries/_og_image.html.haml | 2 ++ 1 file changed, 2 insertions(+) (limited to 'app') diff --git a/app/views/stream_entries/_og_image.html.haml b/app/views/stream_entries/_og_image.html.haml index e1b977da3..67f9274b6 100644 --- a/app/views/stream_entries/_og_image.html.haml +++ b/app/views/stream_entries/_og_image.html.haml @@ -7,6 +7,8 @@ - unless media.file.meta.nil? = opengraph 'og:image:width', media.file.meta.dig('original', 'width') = opengraph 'og:image:height', media.file.meta.dig('original', 'height') + - if media.description.present? + = opengraph 'og:image:alt', media.description - elsif media.video? || media.gifv? - player_card = true = opengraph 'og:image', full_asset_url(media.file.url(:small)) -- cgit From bb9d7fad9f8d5b77144d4d0a21a6c7cdca243f6b Mon Sep 17 00:00:00 2001 From: Hinaloe Date: Mon, 20 May 2019 01:41:41 +0900 Subject: fix `isSubmitting` prop case (#10785) --- .../mastodon/features/compose/containers/compose_form_container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/mastodon/features/compose/containers/compose_form_container.js b/app/javascript/mastodon/features/compose/containers/compose_form_container.js index 93a468388..37a0e8845 100644 --- a/app/javascript/mastodon/features/compose/containers/compose_form_container.js +++ b/app/javascript/mastodon/features/compose/containers/compose_form_container.js @@ -20,7 +20,7 @@ const mapStateToProps = state => ({ focusDate: state.getIn(['compose', 'focusDate']), caretPosition: state.getIn(['compose', 'caretPosition']), preselectDate: state.getIn(['compose', 'preselectDate']), - is_submitting: state.getIn(['compose', 'is_submitting']), + isSubmitting: state.getIn(['compose', 'is_submitting']), isChangingUpload: state.getIn(['compose', 'is_changing_upload']), isUploading: state.getIn(['compose', 'is_uploading']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), -- cgit From ae1838655876363065dd062a21064d385a90eb33 Mon Sep 17 00:00:00 2001 From: ThibG Date: Sun, 19 May 2019 21:40:36 +0200 Subject: Fix “invited by” not showing up for invited accounts in admin interface (#10791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/user.rb | 6 +++++- app/validators/blacklisted_email_validator.rb | 2 +- spec/validators/blacklisted_email_validator_spec.rb | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) (limited to 'app') diff --git a/app/models/user.rb b/app/models/user.rb index 6941b040d..3d1eb5f20 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -114,6 +114,10 @@ class User < ApplicationRecord end def invited? + invite_id.present? + end + + def valid_invitation? invite_id.present? && invite.valid_for_use? end @@ -274,7 +278,7 @@ class User < ApplicationRecord private def set_approved - self.approved = open_registrations? || invited? || external? + self.approved = open_registrations? || valid_invitation? || external? end def open_registrations? diff --git a/app/validators/blacklisted_email_validator.rb b/app/validators/blacklisted_email_validator.rb index a288c20ef..0d01a1c47 100644 --- a/app/validators/blacklisted_email_validator.rb +++ b/app/validators/blacklisted_email_validator.rb @@ -2,7 +2,7 @@ class BlacklistedEmailValidator < ActiveModel::Validator def validate(user) - return if user.invited? + return if user.valid_invitation? @email = user.email diff --git a/spec/validators/blacklisted_email_validator_spec.rb b/spec/validators/blacklisted_email_validator_spec.rb index 84b0107dd..ccc5dc0f4 100644 --- a/spec/validators/blacklisted_email_validator_spec.rb +++ b/spec/validators/blacklisted_email_validator_spec.rb @@ -8,7 +8,7 @@ RSpec.describe BlacklistedEmailValidator, type: :validator do let(:errors) { double(add: nil) } before do - allow(user).to receive(:invited?) { false } + allow(user).to receive(:valid_invitation?) { false } allow_any_instance_of(described_class).to receive(:blocked_email?) { blocked_email } described_class.new.validate(user) end -- cgit From 2cd7bfac239561ab7ad119d558a6508e66e4ad22 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sun, 19 May 2019 23:02:32 +0200 Subject: Use glitch-soc's poll component instead of upstream's --- app/javascript/flavours/glitch/containers/poll_container.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/containers/poll_container.js b/app/javascript/flavours/glitch/containers/poll_container.js index cd7216de7..da93cc905 100644 --- a/app/javascript/flavours/glitch/containers/poll_container.js +++ b/app/javascript/flavours/glitch/containers/poll_container.js @@ -1,5 +1,5 @@ import { connect } from 'react-redux'; -import Poll from 'mastodon/components/poll'; +import Poll from 'flavours/glitch/components/poll'; const mapStateToProps = (state, { pollId }) => ({ poll: state.getIn(['polls', pollId]), -- cgit From 4fbce23992c48314f581e07804360a5e14f915b8 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Mon, 20 May 2019 10:05:11 +0200 Subject: Refactor contentType selection in glitch composer --- .../flavours/glitch/features/compose/components/compose_form.js | 7 ------- .../glitch/features/compose/containers/compose_form_container.js | 6 ------ .../glitch/features/compose/containers/options_container.js | 9 ++++++++- 3 files changed, 8 insertions(+), 14 deletions(-) (limited to 'app') diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index 4e93e2d84..e8f000b1e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -38,7 +38,6 @@ class ComposeForm extends ImmutablePureComponent { suggestions: ImmutablePropTypes.list, spoiler: PropTypes.bool, privacy: PropTypes.string, - contentType: PropTypes.string, spoilerText: PropTypes.string, focusDate: PropTypes.instanceOf(Date), caretPosition: PropTypes.number, @@ -67,7 +66,6 @@ class ComposeForm extends ImmutablePureComponent { preselectOnReply: PropTypes.bool, onChangeSpoilerness: PropTypes.func, onChangeVisibility: PropTypes.func, - onChangeContentType: PropTypes.func, onMount: PropTypes.func, onUnmount: PropTypes.func, onPaste: PropTypes.func, @@ -287,12 +285,10 @@ class ComposeForm extends ImmutablePureComponent { media, onChangeSpoilerness, onChangeVisibility, - onChangeContentType, onClearSuggestions, onFetchSuggestions, onPaste, privacy, - contentType, sensitive, showSearch, sideArm, @@ -360,11 +356,9 @@ class ComposeForm extends ImmutablePureComponent { advancedOptions={advancedOptions} disabled={isSubmitting} onChangeVisibility={onChangeVisibility} - onChangeContentType={onChangeContentType} onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness} onUpload={onPaste} privacy={privacy} - contentType={contentType} sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} /> @@ -375,7 +369,6 @@ class ComposeForm extends ImmutablePureComponent { onSecondarySubmit={handleSecondarySubmit} onSubmit={handleSubmit} privacy={privacy} - contentType={contentType} sideArm={sideArm} />
diff --git a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js index ce5c3afb3..814f9a97a 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/compose_form_container.js @@ -6,7 +6,6 @@ import { changeComposeSpoilerText, changeComposeSpoilerness, changeComposeVisibility, - changeComposeContentType, clearComposeSuggestions, fetchComposeSuggestions, insertEmojiCompose, @@ -58,7 +57,6 @@ function mapStateToProps (state) { media: state.getIn(['compose', 'media_attachments']), preselectDate: state.getIn(['compose', 'preselectDate']), privacy: state.getIn(['compose', 'privacy']), - contentType: state.getIn(['compose', 'content_type']), sideArm: sideArmPrivacy, sensitive: state.getIn(['compose', 'sensitive']), showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), @@ -100,10 +98,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ dispatch(changeComposeSpoilerText(text)); }, - onChangeContentType(value) { - dispatch(changeComposeContentType(value)); - }, - onPaste(files) { dispatch(uploadCompose(files)); }, diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index a9ad3a5e8..c8c7ecd43 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -2,8 +2,10 @@ import { connect } from 'react-redux'; import Options from '../components/options'; import { changeComposeAdvancedOption, + changeComposeContentType, + addPoll, + removePoll, } from 'flavours/glitch/actions/compose'; -import { addPoll, removePoll } from 'flavours/glitch/actions/compose'; import { closeModal, openModal } from 'flavours/glitch/actions/modal'; function mapStateToProps (state) { @@ -18,6 +20,7 @@ function mapStateToProps (state) { hasMedia: media && !!media.size, allowPoll: !(media && !!media.size), showContentTypeChoice: state.getIn(['local_settings', 'show_content_type_choice']), + contentType: state.getIn(['compose', 'content_type']), }; }; @@ -27,6 +30,10 @@ const mapDispatchToProps = (dispatch) => ({ dispatch(changeComposeAdvancedOption(option, value)); }, + onChangeContentType(value) { + dispatch(changeComposeContentType(value)); + }, + onTogglePoll() { dispatch((_, getState) => { if (getState().getIn(['compose', 'poll'])) { -- cgit From 2332b3f146b0d879daba8a99bd35c8bf425edea3 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Tue, 21 May 2019 22:57:59 +0200 Subject: Fix local text/html toots not being sanitized --- app/lib/formatter.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'app') diff --git a/app/lib/formatter.rb b/app/lib/formatter.rb index 78a0e9f25..a099ff728 100644 --- a/app/lib/formatter.rb +++ b/app/lib/formatter.rb @@ -61,7 +61,9 @@ class Formatter html = encode_and_link_urls(html, linkable_accounts, keep_html: %w(text/markdown text/html).include?(status.content_type)) html = encode_custom_emojis(html, status.emojis, options[:autoplay]) if options[:custom_emojify] - unless %w(text/markdown text/html).include?(status.content_type) + if %w(text/markdown text/html).include?(status.content_type) + html = reformat(html) + else html = simple_format(html, {}, sanitize: false) html = html.delete("\n") end @@ -70,7 +72,7 @@ class Formatter end def format_markdown(html) - html = reformat(markdown_formatter.render(html)) + html = markdown_formatter.render(html) html.delete("\r").delete("\n") end -- cgit