gearheads
/
mastodon
Archived
2
0
Fork 0

Fix regressions from ()

* Fix regressions from 

Properly format spoiler text HTML, while keeping old logic for blankness intact
Process hashtags and mentions in spoiler text
Format spoiler text for Atom
Change "show more" toggle into a button instead of anchor
Fix style regression on dropdowns for detailed statuses

* Fix lint issue

* Convert spoiler text to plaintext in desktop notifications
gh/stable
Eugen Rochko 2017-05-11 00:28:10 +02:00 committed by GitHub
parent 65027657ec
commit 72698bc3b4
14 changed files with 88 additions and 49 deletions

View File

@ -45,7 +45,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
// Desktop notifications
if (typeof window.Notification !== 'undefined' && showAlert) {
const title = new IntlMessageFormat(intlMessages[`notification.${notification.type}`], intlLocale).format({ name: notification.account.display_name.length > 0 ? notification.account.display_name : notification.account.username });
const body = (notification.status && notification.status.spoiler_text.length > 0) ? notification.status.spoiler_text : unescapeHTML(notification.status ? notification.status.content : '');
const body = (notification.status && notification.status.spoiler_text.length > 0) ? unescapeHTML(notification.status.spoiler_text) : unescapeHTML(notification.status ? notification.status.content : '');
new Notification(title, { body, icon: notification.account.avatar, tag: notification.id });
}

View File

@ -73,8 +73,12 @@ class StatusActionBar extends React.PureComponent {
render () {
const { status, me, intl } = this.props;
const reblog_disabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
const reblogDisabled = status.get('visibility') === 'private' || status.get('visibility') === 'direct';
let menu = [];
let reblogIcon = 'retweet';
let replyIcon;
let replyTitle;
menu.push({ text: intl.formatMessage(messages.open), action: this.handleOpen });
menu.push(null);
@ -89,23 +93,24 @@ class StatusActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport });
}
let reblogIcon = 'retweet';
if (status.get('visibility') === 'direct') reblogIcon = 'envelope';
else if (status.get('visibility') === 'private') reblogIcon = 'lock';
let reply_icon;
let reply_title;
if (status.get('visibility') === 'direct') {
reblogIcon = 'envelope';
} else if (status.get('visibility') === 'private') {
reblogIcon = 'lock';
}
if (status.get('in_reply_to_id', null) === null) {
reply_icon = "reply";
reply_title = intl.formatMessage(messages.reply);
replyIcon = "reply";
replyTitle = intl.formatMessage(messages.reply);
} else {
reply_icon = "reply-all";
reply_title = intl.formatMessage(messages.replyAll);
replyIcon = "reply-all";
replyTitle = intl.formatMessage(messages.replyAll);
}
return (
<div className='status__action-bar'>
<div className='status__action-bar-button-wrapper'><IconButton title={reply_title} icon={reply_icon} onClick={this.handleReplyClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton title={replyTitle} icon={replyIcon} onClick={this.handleReplyClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton disabled={reblogDisabled} active={status.get('reblogged')} title={reblogDisabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='status__action-bar-button-wrapper'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} className='star-icon' /></div>
<div className='status__action-bar-dropdown'>

View File

@ -11,9 +11,11 @@ class StatusContent extends React.PureComponent {
constructor (props, context) {
super(props, context);
this.state = {
hidden: true
};
this.onMentionClick = this.onMentionClick.bind(this);
this.onHashtagClick = this.onHashtagClick.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this)
@ -36,8 +38,6 @@ class StatusContent extends React.PureComponent {
link.setAttribute('title', mention.get('acct'));
} else if (link.textContent[0] === '#' || (link.previousSibling && link.previousSibling.textContent && link.previousSibling.textContent[link.previousSibling.textContent.length - 1] === '#')) {
link.addEventListener('click', this.onHashtagClick.bind(this, link.text), false);
} else if (media) {
link.innerHTML = '<i class="fa fa-fw fa-photo"></i>';
} else {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
@ -70,11 +70,11 @@ class StatusContent extends React.PureComponent {
const [ startX, startY ] = this.startXY;
const [ deltaX, deltaY ] = [Math.abs(e.clientX - startX), Math.abs(e.clientY - startY)];
if (e.target.localName === 'a' || (e.target.parentNode && e.target.parentNode.localName === 'a')) {
if (e.target.localName === 'button' || e.target.localName === 'a' || (e.target.parentNode && e.target.parentNode.localName === 'a')) {
return;
}
if (deltaX + deltaY < 5 && e.button === 0) {
if (deltaX + deltaY < 5 && e.button === 0 && this.props.onClick) {
this.props.onClick();
}
@ -95,7 +95,7 @@ class StatusContent extends React.PureComponent {
const { hidden } = this.state;
const content = { __html: emojify(status.get('content')) };
const spoilerContent = { __html: emojify(escapeTextContentForBrowser(status.get('spoiler_text', ''))) };
const spoilerContent = { __html: emojify(status.get('spoiler_text', '')) };
const directionStyle = { direction: 'ltr' };
if (isRtl(status.get('content'))) {
@ -118,14 +118,19 @@ class StatusContent extends React.PureComponent {
}
return (
<div className='status__content' onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp}>
<div
ref={this.setRef}
className='status__content'
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
>
<p style={{ marginBottom: hidden && status.get('mentions').size === 0 ? '0px' : '' }} >
<span dangerouslySetInnerHTML={spoilerContent} /> <a tabIndex='0' className='status__content__spoiler-link' role='button' onClick={this.handleSpoilerClick}>{toggleText}</a>
<span dangerouslySetInnerHTML={spoilerContent} /> <button tabIndex='0' className='status__content__spoiler-link' onClick={this.handleSpoilerClick}>{toggleText}</button>
</p>
{mentionsPlaceholder}
<div ref={this.setRef} style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
<div style={{ display: hidden ? 'none' : 'block', ...directionStyle }} dangerouslySetInnerHTML={content} />
</div>
);
} else if (this.props.onClick) {

View File

@ -76,7 +76,10 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_id', null) === null ? 'reply' : 'reply-all'} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton disabled={reblog_disabled} active={status.get('reblogged')} title={reblog_disabled ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} icon={reblogIcon} onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton animate={true} active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} activeStyle={{ color: '#ca8f04' }} /></div>
<div className='detailed-status__button'><DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" ariaLabel="More" /></div>
<div className='detailed-status__action-bar-dropdown'>
<DropdownMenu size={18} icon='ellipsis-h' items={menu} direction="left" ariaLabel="More" />
</div>
</div>
);
}

View File

@ -3,9 +3,12 @@ import { length } from 'stringz';
import { default as dateFormat } from 'date-fns/format';
import distanceInWordsStrict from 'date-fns/distance_in_words_strict';
import { delegate } from 'rails-ujs';
import Rails from 'rails-ujs';
require.context('../images/', true);
Rails.start();
const parseFormat = (format) => format.replace(/%(\w)/g, (_, modifier) => {
switch (modifier) {
case '%':

View File

@ -474,15 +474,18 @@
}
}
a.status__content__spoiler-link {
.status__content__spoiler-link {
display: inline-block;
border-radius: 2px;
background: transparent;
border: 0;
color: lighten($ui-base-color, 8%);
font-weight: 500;
font-size: 11px;
padding: 0 6px;
text-transform: uppercase;
line-height: inherit;
cursor: pointer;
}
.status__prepend-icon-wrapper {
@ -608,6 +611,34 @@ a.status__content__spoiler-link {
width: 18px;
}
.detailed-status__action-bar-dropdown {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.dropdown {
display: block;
width: 18px;
height: 18px;
}
.dropdown--active {
.dropdown__content.dropdown__left {
left: 20px;
right: initial;
}
&::after {
bottom: initial;
margin-left: 7px;
margin-top: -7px;
right: initial;
}
}
}
.detailed-status {
background: lighten($ui-base-color, 4%);
padding: 14px 10px;
@ -2165,6 +2196,7 @@ button.icon-button.active i.fa-retweet {
display: flex;
flex: 1 1 auto;
align-items: center;
justify-content: center;
a {
color: $ui-highlight-color;

View File

@ -332,7 +332,7 @@ class AtomSerializer
end
def serialize_status_attributes(entry, status)
append_element(entry, 'summary', status.spoiler_text, 'xml:lang': status.language) if status.spoiler_text?
append_element(entry, 'summary', Formatter.instance.format(status.proper, :spoiler_text, false).to_str, 'xml:lang': status.language, type: 'html') if status.spoiler_text?
append_element(entry, 'content', Formatter.instance.format(status.proper).to_str, type: 'html', 'xml:lang': status.language)
status.mentions.each do |mentioned|

View File

@ -9,13 +9,15 @@ class Formatter
include ActionView::Helpers::TextHelper
def format(status)
return reformat(status.content) unless status.local?
def format(status, attribute = :text, paragraphize = true)
raw_content = status.public_send(attribute)
html = status.text
return '' if raw_content.blank?
return reformat(raw_content) unless status.local?
html = raw_content
html = encode_and_link_urls(html, status.mentions)
html = simple_format(html, {}, sanitize: false)
html = simple_format(html, {}, sanitize: false) if paragraphize
html = html.delete("\n")
html.html_safe # rubocop:disable Rails/OutputSafety
@ -25,18 +27,6 @@ class Formatter
sanitize(html, Sanitize::Config::MASTODON_STRICT).html_safe # rubocop:disable Rails/OutputSafety
end
def format_spoiler(status)
return reformat(status.spoiler_text) unless status.local?
html = status.spoiler_text
html = encode_and_link_urls(html)
html = simple_format(html, {}, sanitize: false)
html = html.delete("\n")
html.html_safe # rubocop:disable Rails/OutputSafety
end
def plaintext(status)
return status.text if status.local?
strip_tags(status.text)

View File

@ -2,7 +2,7 @@
class ProcessHashtagsService < BaseService
def call(status, tags = [])
text = [status.text, status.spoiler_text].reject(&:empty?).join(' ')
text = [status.text, status.spoiler_text].reject(&:blank?).join(' ')
tags = text.scan(Tag::HASHTAG_RE).map(&:first) if status.local?
tags.map { |str| str.mb_chars.downcase }.uniq(&:to_s).each do |tag|

View File

@ -10,7 +10,9 @@ class ProcessMentionsService < BaseService
def call(status)
return unless status.local?
status.text.scan(Account::MENTION_RE).each do |match|
text = [status.text, status.spoiler_text].reject(&:blank?).join(' ')
text.scan(Account::MENTION_RE).each do |match|
username, domain = match.first.split('@')
mentioned_account = Account.find_remote(username, domain)

View File

@ -1,7 +1,8 @@
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :spoiler_text, :visibility
attributes :id, :created_at, :in_reply_to_id, :in_reply_to_account_id, :sensitive, :visibility
node(:uri) { |status| TagManager.instance.uri_for(status) }
node(:content) { |status| Formatter.instance.format(status) }
node(:spoiler_text) { |status| Formatter.instance.format(status, :spoiler_text, false) }
node(:url) { |status| TagManager.instance.url_for(status) }
node(:reblogs_count) { |status| defined?(@reblogs_counts_map) ? (@reblogs_counts_map[status.id] || 0) : status.reblogs_count }
node(:favourites_count) { |status| defined?(@favourites_counts_map) ? (@favourites_counts_map[status.id] || 0) : status.favourites_count }

View File

@ -10,7 +10,7 @@
.status__content.p-name.emojify<
- if status.spoiler_text?
%p{ style: 'margin-bottom: 0' }<
%span.p-summary> #{Formatter.instance.format_spoiler(status)}&nbsp;
%span.p-summary> #{Formatter.instance.format(status, :spoiler_text, false)}&nbsp;
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)

View File

@ -16,7 +16,7 @@
.status__content.p-name.emojify<
- if status.spoiler_text?
%p{ style: 'margin-bottom: 0' }<
%span.p-summary> #{Formatter.instance.format_spoiler(status)}&nbsp;
%span.p-summary> #{Formatter.instance.format(status, :spoiler_text, false)}&nbsp;
%a.status__content__spoiler-link{ href: '#' }= t('statuses.show_more')
.e-content{ lang: status.language, style: "display: #{status.spoiler_text? ? 'none' : 'block'}; direction: #{rtl?(status.content) ? 'rtl' : 'ltr'}" }= Formatter.instance.format(status)

View File

@ -69,11 +69,9 @@ RSpec.describe Formatter do
end
end
=begin
it 'matches a URL without closing paranthesis' do
xit 'matches a URL without closing paranthesis' do
expect(subject.match('(http://google.com/)')[0]).to eq 'http://google.com'
end
=end
context 'matches a URL without exclamation point' do
let(:local_text) { 'http://www.google.com!' }