diff options
Diffstat (limited to 'app/javascript/mastodon')
9 files changed, 227 insertions, 8 deletions
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js index dca44917a..d0a55538f 100644 --- a/app/javascript/mastodon/actions/importer/normalizer.js +++ b/app/javascript/mastodon/actions/importer/normalizer.js @@ -53,9 +53,12 @@ export function normalizeStatus(status, normalOldStatus) { normalStatus.poll = status.poll.id; } + const oldUpdatedAt = normalOldStatus ? normalOldStatus.updated_at || normalOldStatus.created_at : null; + const newUpdatedAt = normalStatus ? normalStatus.updated_at || normalStatus.created_at : null; + // Only calculate these values when status first encountered // Otherwise keep the ones already in the reducer - if (normalOldStatus) { + if (normalOldStatus && oldUpdatedAt === newUpdatedAt) { normalStatus.search_index = normalOldStatus.get('search_index'); normalStatus.contentHtml = normalOldStatus.get('contentHtml'); normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml'); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index beb5c6a4a..1adc1b815 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -18,6 +18,7 @@ import { } from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; +import { resetCompose } from '../actions/compose'; const { messages } = getLocale(); @@ -96,6 +97,10 @@ export const connectTimelineStream = (timelineId, channelName, params = {}, opti case 'announcement.delete': dispatch(deleteAnnouncement(data.payload)); break; + case 'refresh': + dispatch(resetCompose()); + window.location.reload(); + break; } }, }; diff --git a/app/javascript/mastodon/components/status_action_bar.js b/app/javascript/mastodon/components/status_action_bar.js index 66b5a17ac..adcdb8a4e 100644 --- a/app/javascript/mastodon/components/status_action_bar.js +++ b/app/javascript/mastodon/components/status_action_bar.js @@ -240,10 +240,8 @@ class StatusActionBar extends ImmutablePureComponent { menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick }); menu.push(null); - if (status.getIn(['account', 'id']) === me || withDismiss) { - menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); - menu.push(null); - } + menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick }); + menu.push(null); if (status.getIn(['account', 'id']) === me) { if (publicStatus) { diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js index 3200f2d82..df05d8515 100644 --- a/app/javascript/mastodon/components/status_content.js +++ b/app/javascript/mastodon/components/status_content.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { isRtl } from '../rtl'; import { FormattedMessage } from 'react-intl'; import Permalink from './permalink'; +import RelativeTimestamp from './relative_timestamp'; import classnames from 'classnames'; import PollContainer from 'mastodon/containers/poll_container'; import Icon from 'mastodon/components/icon'; @@ -180,6 +181,20 @@ export default class StatusContent extends React.PureComponent { return null; } + const edited = (status.get('edited') === 0) ? null : ( + <div className='status__edit-notice'> + <FormattedMessage + id='status.edited' + defaultMessage='{count, plural, one {# edit} other {# edits}} · last update: {updated_at}' + key={`edit-${status.get('id')}`} + values={{ + count: status.get('edited'), + updated_at: <RelativeTimestamp timestamp={status.get('updated_at')} />, + }} + /> + </div> + ); + const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const renderReadMore = this.props.onClick && status.get('collapsed'); const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']); @@ -232,6 +247,7 @@ export default class StatusContent extends React.PureComponent { <button tabIndex='0' className={`status__content__spoiler-link ${hidden ? 'status__content__spoiler-link--show-more' : 'status__content__spoiler-link--show-less'}`} onClick={this.handleSpoilerClick}>{toggleText}</button> </p> + {edited} {mentionsPlaceholder} <div tabIndex={!hidden ? 0 : null} className={`status__content__text ${!hidden ? 'status__content__text--visible' : ''}`} style={directionStyle} dangerouslySetInnerHTML={content} /> @@ -244,6 +260,8 @@ export default class StatusContent extends React.PureComponent { } else if (this.props.onClick) { const output = [ <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle} onMouseDown={this.handleMouseDown} onMouseUp={this.handleMouseUp} key='status-content'> + {edited} + <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} @@ -260,6 +278,8 @@ export default class StatusContent extends React.PureComponent { } else { return ( <div className={classNames} ref={this.setRef} tabIndex='0' style={directionStyle}> + {edited} + <div className='status__content__text status__content__text--visible' style={directionStyle} dangerouslySetInnerHTML={content} /> {!!status.get('poll') && <PollContainer pollId={status.get('poll')} />} diff --git a/app/javascript/mastodon/features/compose/components/poll_form.js b/app/javascript/mastodon/features/compose/components/poll_form.js index 88894ae59..72ffeff09 100644 --- a/app/javascript/mastodon/features/compose/components/poll_form.js +++ b/app/javascript/mastodon/features/compose/components/poll_form.js @@ -89,7 +89,7 @@ class Option extends React.PureComponent { <AutosuggestInput placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} - maxLength={100} + maxlength={202} value={title} onChange={this.handleOptionTitleChange} suggestions={this.props.suggestions} @@ -157,7 +157,7 @@ class PollForm extends ImmutablePureComponent { </ul> <div className='poll__footer'> - <button disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> + <button disabled={options.size >= 33} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button> {/* eslint-disable-next-line jsx-a11y/no-onchange */} <select value={expiresIn} onChange={this.handleSelectDuration}> diff --git a/app/javascript/mastodon/locales/en-MP.json b/app/javascript/mastodon/locales/en-MP.json new file mode 100644 index 000000000..3e021a5c1 --- /dev/null +++ b/app/javascript/mastodon/locales/en-MP.json @@ -0,0 +1,181 @@ +{ + "account.add_account_note": "Add note for @{name}", + "account.disclaimer_full": "You're viewing the cached version of a profile from another server.", + "account.followers.empty": "No one follows this creature yet.", + "account.follows.empty": "This creature doesn't follow anyone yet.", + "account.follows": "Follows", + "account.locked_info": "This creature manually reviews who can follow them.", + "account.media": "Media", + "account.mentions": "Mentions", + "account.posts_with_replies": "Replies", + "account.posts": "Blog", + "account.reblogs": "Boosts", + "account.statuses_counter": "{count, plural, one {{counter} Roar} other {{counter} Roars}}", + "account.threads": "Threads", + "account.view_full_profile": "View the original", + "advanced_options.local-only.long": "Do not post to other servers", + "column_header.profile": "Creature", + "column.blocks": "Blocked creatures", + "column.community": "Monsterpit", + "column.directory": "Creature directory", + "column.favourites": "Admirations", + "column.mutes": "Muted creatures", + "column.pins": "Pins", + "column.public": "Tavern", + "column.toot": "Roars & Growls", + "community.column_settings.local_only": "Monsterpit only", + "community.column_settings.remote_only": "Rowdy Tavern mode", + "compose_form.clear": "Double-click to clear", + "compose_form.direct_message_warning": "This roar will only be sent to the mentioned creatures.", + "compose_form.hashtag_warning": "This roar won't be listed under any hashtag as it is unlisted. Only public roars can be searched by hashtag.", + "compose_form.lock_disclaimer": "Your account is not {locked}. Anyone can follow you to view your follower-only posts.", + "compose_form.placeholder": "Roar shamelessly!", + "compose_form.publish": "Roar", + "compose_form.spoiler_placeholder": "Enter content notes here", + "compose_form.spoiler.marked": "Text is hidden behind content notes", + "compose_form.spoiler": "Enter content notes here", + "confirmations.delete.message": "Are you sure you want to delete this roar?", + "confirmations.mute.explanation": "This will hide roars from them and roars mentioning them, but it will still allow them to see your roars and follow you.", + "confirmations.publish.confirm": "Publish", + "confirmations.publish.message": "Are you ready to publish your roar?", + "confirmations.redraft.message": "Are you sure you want to delete and redraft this roar? Admirations and boosts will be lost, and replies to the original roar will be orphaned.", + "confirmations.domain_block.message": "You are about to block and defederate from the {domain} server. If you proceed, Monsterpit will ask this server to permanently delete any data associated with your account.", + "content-type.change": "Content type", + "directory.federated": "From Tavern", + "directory.local": "From Monsterpit", + "embed.instructions": "Embed this roar on your website by copying the code below.", + "empty_column.account_timeline": "No roars here!", + "empty_column.blocks": "You haven't blocked any creatures yet.", + "empty_column.bookmarked_statuses": "You don't have any bookmarked roars yet. When you bookmark one, it will show up here.", + "empty_column.community": "The Monsterpit timeline is empty. Write something publicly to get the ball rolling!", + "empty_column.favourited_statuses": "You don't have any admired roars yet. When you admire one, it will show up here.", + "empty_column.favourites": "No one has admired this roar yet. When someone does, they will show up here.", + "empty_column.home": "Your home timeline is empty! Visit {public} or use search to get started and meet other creatures.", + "empty_column.list": "There is nothing in this list yet. When members of this list post new roars, they will appear here.", + "empty_column.mutes": "You haven't muted any creatures yet.", + "empty_column.public": "There is nothing here! Write something publicly, or manually follow creatures from other servers to fill it up", + "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Monsterpit through a different browser or native app.", + "follow_request.authorize": "Accept", + "getting_started.directory": "Creature directory", + "getting_started.invite": "Invite creatures", + "getting_started.open_source_notice": "Monsterfork is open source software. If you'd like to explore its code, you may visit the repository on {monsterware}.", + "introduction.federation.federated.headline": "Tavern", + "introduction.federation.federated.text": "Public roars from other servers will appear in the Tavern timeline.", + "introduction.federation.home.text": "Roars from creatures you follow will appear in your home feed.", + "introduction.federation.local.headline": "Monsterpit", + "introduction.federation.local.text": "Public roars from people on Monsterpit will appear in the Monsterpit timeline.", + "introduction.interactions.action": "Finish tutorial", + "introduction.interactions.favourite.headline": "Admire", + "introduction.interactions.favourite.text": "You can save a roar for later, and let the author know that you liked it, by admiring it.", + "introduction.interactions.reblog.text": "You can share other creature's roars with your followers by boosting them.", + "introduction.interactions.reply.text": "You can reply to other creature's and your own roars, which will chain them together in a conversation.", + "keyboard_shortcuts.blocked": "to open blocked creatures list", + "keyboard_shortcuts.column": "to focus a roar in one of the columns", + "keyboard_shortcuts.enter": "to open roar", + "keyboard_shortcuts.favourite": "to admire", + "keyboard_shortcuts.favourites": "to open admirations list", + "keyboard_shortcuts.federated": "to open Tavern timeline", + "keyboard_shortcuts.local": "to open Monsterpit timeline", + "keyboard_shortcuts.muted": "to open muted creatures list", + "keyboard_shortcuts.pinned": "to open pinned roars list", + "keyboard_shortcuts.spoilers": "to show/hide content note field", + "keyboard_shortcuts.toggle_hidden": "to show/hide text behind content notes", + "keyboard_shortcuts.toot": "to start a new roar", + "lists.search": "Search among creatures you follow", + "mute_modal.hide_notifications": "Hide notifications from this creature?", + "navigation_bar.blocks": "Blocked creatures", + "navigation_bar.community_timeline": "Monsterpit", + "navigation_bar.compose": "Compose new roar", + "navigation_bar.favourites": "Admirations", + "navigation_bar.logout": "Sleep", + "navigation_bar.mutes": "Muted creatures", + "navigation_bar.pins": "Pins", + "navigation_bar.public_timeline": "Tavern", + "notification_purge.start": "Enter notification cleaning mode", + "notification.favourite": "{name} admired your roar", + "notification.follow_request": "{name} wants to follow you", + "notification.follow": "{name} followed you", + "notification.mention": "{name} mentioned you", + "notification.own_poll": "Your poll has ended", + "notification.poll": "A poll you have voted in has ended", + "notification.reblog": "{name} boosted your roar", + "notifications.clear": "Clear notifications", + "notifications.column_settings.favourite": "Admirations:", + "notifications.filter.favourites": "Admirations", + "notifications_permission_banner.title": "You have desktop notifications enabled.", + "notifications_permission_banner.enable": "Request", + "notifications_permission_banner.disable": "Disable", + "notifications_permission_banner.how_to_control": "Permission is needed from your Web browser to use this feature.", + "poll.total_people": "{count, plural, one {# creature} other {# creatures}}", + "privacy.change": "Adjust roar privacy", + "privacy.direct.long": "Visible for mentioned creatures only", + "report.forward_hint": "The creature is from another server. Send an anonymized copy of the report there as well?", + "report.hint": "The report will be sent to your server moderators. You can provide an explanation of why you are reporting this creature below:", + "search_popout.tips.full_text": "Simple text returns roars you have written, admired, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.", + "search_popout.tips.status": "roar", + "search_popout.tips.text": "Simple text returns matching display names, usernames and hashtags", + "search_popout.tips.user": "creature", + "search_results.accounts": "Creatures", + "search_results.statuses_fts_disabled": "Searching roars by their content is not enabled on this Mastodon server.", + "search_results.statuses": "Roars", + "settings.always_show_spoilers_field": "Always show content notes field", + "settings.auto_collapse_lengthy": "Lengthy roars", + "settings.auto_collapse_media": "Media", + "settings.collapsed_statuses": "Collapsed roars", + "settings.confirm_boost_missing_media_description": "Show confirmation dialog before boosting roars lacking media descriptions", + "settings.confirm_missing_media_description": "Show confirmation dialog before sending roars lacking media descriptions", + "settings.content_warnings_filter": "Avoid expanding roars with content notes containing:", + "settings.content_warnings": "Content notes", + "settings.enable_collapsed": "Enable collapsed roars", + "settings.enable_content_warnings_auto_unfold": "Auto-expand roars with content notes", + "settings.filtering_behavior.cw": "Add the filtered phrase to the roar's content notes", + "settings.image_backgrounds_media": "Preview collapsed media", + "settings.image_backgrounds_users": "Give collapsed roars an image background", + "settings.prepend_cw_re": "Prepend \"re:\" to content notes when replying", + "settings.rewrite_mentions": "Rewrite mentions in roars:", + "settings.show_action_bar": "Show action buttons in collapsed roars", + "settings.show_content_type_choice": "Show content-type choice when authoring roars", + "settings.side_arm_reply_mode.copy": "Copy privacy setting of the roar being replied to", + "settings.side_arm_reply_mode.keep": "Keep secondary roar button to set privacy", + "settings.side_arm_reply_mode.restrict": "Restrict privacy setting to that of the roar being replied to", + "settings.side_arm_reply_mode": "When replying to a roar:", + "settings.side_arm": "Secondary roar button:", + "status.admin_account": "Moderate @{name}", + "status.admin_status": "Moderate roar", + "status.article": "Article", + "status.cannot_reblog": "This roar cannot be boosted", + "status.copy": "Copy link to roar", + "status.edit": "Edit", + "status.edited": "{count, plural, one {# edit} other {# edits}} · last update: {updated_at}", + "status.favourite": "Admire", + "status.has_pictures": "Features attached pictures", + "status.in_reply_to": "This roar is a reply", + "status.is_poll": "This roar is a poll", + "status.local_only": "Monsterpit-only", + "status.media.description": "Attachment #{index}: ", + "status.media.descriptions": "Attachments {list}: ", + "status.open": "Open this roar", + "status.permissions.title": "Show extended permissions...", + "status.permissions.visibility.account": "{visibility} 🡲 {domain}", + "status.permissions.visibility.status": "{visibility} 🡲 {domain}", + "status.pinned": "Pinned", + "status.publish": "Publish", + "status.reblogged_by": "{name}", + "status.reblogs.empty": "No one has boosted this roar yet. When someone does, they will show up here.", + "status.show_article": "Show article", + "status.show_less_all": "Hide all", + "status.show_less": "Hide", + "status.show_more_all": "Reveal all", + "status.show_more": "Reveal", + "status.show_thread": "Reveal thread", + "status.tags": "Show all tags...", + "status.unpublished": "Unpublished", + "tabs_bar.federated_timeline": "Tavern", + "tabs_bar.local_timeline": "Monsterpit", + "timeline_hint.resources.statuses": "Older roars", + "trends.counter_by_accounts": "{count, plural, one {{counter} creature} other {{counter} creatures}} talking", + "ui.beforeunload": "Your draft will be lost if you leave the web page.", + "upload_form.edit": "Add description text", + "upload_modal.edit_media": "Add description text", + "video.expand": "Open video" +} diff --git a/app/javascript/mastodon/locales/locale-data/en-MP.js b/app/javascript/mastodon/locales/locale-data/en-MP.js new file mode 100644 index 000000000..a2defe09a --- /dev/null +++ b/app/javascript/mastodon/locales/locale-data/en-MP.js @@ -0,0 +1,8 @@ +/*eslint eqeqeq: "off"*/ +/*eslint no-nested-ternary: "off"*/ +/*eslint quotes: "off"*/ + +export default [{ + locale: 'en-MP', + parentLocale: 'en', +}]; \ No newline at end of file diff --git a/app/javascript/mastodon/locales/whitelist_en-MP.json b/app/javascript/mastodon/locales/whitelist_en-MP.json new file mode 100644 index 000000000..32960f8ce --- /dev/null +++ b/app/javascript/mastodon/locales/whitelist_en-MP.json @@ -0,0 +1,2 @@ +[ +] \ No newline at end of file diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 4c0ba1c36..67ce96feb 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -205,7 +205,9 @@ const expandMentions = status => { const fragment = domParser.parseFromString(status.get('content'), 'text/html').documentElement; status.get('mentions').forEach(mention => { - fragment.querySelector(`a[href="${mention.get('url')}"]`).textContent = `@${mention.get('acct')}`; + const selection = fragment.querySelector(`a[href="${mention.get('url')}"]`); + if (!selection) return; + selection.textContent = `@${mention.get('acct')}`; }); return fragment.innerHTML; |