From 82951920f7321fefbdf0628cbb70f2f490f7b716 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 28 Sep 2020 21:44:29 +0200 Subject: Fix bell button causing a brief “Cancel follow request” on locked accounts (#14896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/javascript/mastodon/reducers/relationships.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/reducers/relationships.js b/app/javascript/mastodon/reducers/relationships.js index 1d050cc63..53949258a 100644 --- a/app/javascript/mastodon/reducers/relationships.js +++ b/app/javascript/mastodon/reducers/relationships.js @@ -45,7 +45,7 @@ const initialState = ImmutableMap(); export default function relationships(state = initialState, action) { switch(action.type) { case ACCOUNT_FOLLOW_REQUEST: - return state.setIn([action.id, action.locked ? 'requested' : 'following'], true); + return state.getIn([action.id, 'following']) ? state : state.setIn([action.id, action.locked ? 'requested' : 'following'], true); case ACCOUNT_FOLLOW_FAIL: return state.setIn([action.id, action.locked ? 'requested' : 'following'], false); case ACCOUNT_UNFOLLOW_REQUEST: -- cgit From f5d08f2417a55501affc0c42108f1c14d47cbb99 Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Tue, 29 Sep 2020 22:30:56 +0900 Subject: Fix unread notification markers (#14897) --- app/javascript/mastodon/actions/notifications.js | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 552def63b..cd03a1d78 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -234,3 +234,7 @@ export const mountNotifications = () => ({ export const unmountNotifications = () => ({ type: NOTIFICATIONS_UNMOUNT, }); + +export const markNotificationsAsRead = () => ({ + type: NOTIFICATIONS_MARK_AS_READ, +}); -- cgit From 78e45a52850c52ced693420a6142cf847581f563 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 30 Sep 2020 19:31:03 +0200 Subject: Add option to disable swiping motions across the WebUI (#13885) Fixes #13882 --- app/controllers/settings/preferences_controller.rb | 1 + .../mastodon/features/getting_started/components/announcements.js | 3 ++- app/javascript/mastodon/features/introduction/index.js | 3 ++- app/javascript/mastodon/features/ui/components/columns_area.js | 4 +++- app/javascript/mastodon/features/ui/components/media_modal.js | 2 ++ app/javascript/mastodon/initial_state.js | 1 + app/lib/user_settings_decorator.rb | 5 +++++ app/models/user.rb | 1 + app/serializers/initial_state_serializer.rb | 1 + app/views/settings/preferences/appearance/show.html.haml | 1 + config/locales/simple_form.en.yml | 1 + config/settings.yml | 1 + 12 files changed, 21 insertions(+), 3 deletions(-) (limited to 'app/javascript') diff --git a/app/controllers/settings/preferences_controller.rb b/app/controllers/settings/preferences_controller.rb index be4dc904d..32b5d7948 100644 --- a/app/controllers/settings/preferences_controller.rb +++ b/app/controllers/settings/preferences_controller.rb @@ -43,6 +43,7 @@ class Settings::PreferencesController < Settings::BaseController :setting_display_media, :setting_expand_spoilers, :setting_reduce_motion, + :setting_disable_swiping, :setting_system_font_ui, :setting_noindex, :setting_theme, diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index 1896994da..8f824f140 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -6,7 +6,7 @@ import PropTypes from 'prop-types'; import IconButton from 'mastodon/components/icon_button'; import Icon from 'mastodon/components/icon'; import { defineMessages, injectIntl, FormattedMessage, FormattedDate } from 'react-intl'; -import { autoPlayGif, reduceMotion } from 'mastodon/initial_state'; +import { autoPlayGif, reduceMotion, disableSwiping } from 'mastodon/initial_state'; import elephantUIPlane from 'mastodon/../images/elephant_ui_plane.svg'; import { mascot } from 'mastodon/initial_state'; import unicodeMapping from 'mastodon/features/emoji/emoji_unicode_mapping_light'; @@ -436,6 +436,7 @@ class Announcements extends ImmutablePureComponent { removeReaction={this.props.removeReaction} intl={intl} selected={index === idx} + disabled={disableSwiping} /> ))} diff --git a/app/javascript/mastodon/features/introduction/index.js b/app/javascript/mastodon/features/introduction/index.js index 754477bb9..5820750a4 100644 --- a/app/javascript/mastodon/features/introduction/index.js +++ b/app/javascript/mastodon/features/introduction/index.js @@ -9,6 +9,7 @@ import screenHello from '../../../images/screen_hello.svg'; import screenFederation from '../../../images/screen_federation.svg'; import screenInteractions from '../../../images/screen_interactions.svg'; import logoTransparent from '../../../images/logo_transparent.svg'; +import { disableSwiping } from 'mastodon/initial_state'; const FrameWelcome = ({ domain, onNext }) => (
@@ -171,7 +172,7 @@ class Introduction extends React.PureComponent { return (
- + {pages.map((page, i) => (
{page}
))} diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js index 9b03cf26d..ecc0b8f0b 100644 --- a/app/javascript/mastodon/features/ui/components/columns_area.js +++ b/app/javascript/mastodon/features/ui/components/columns_area.js @@ -8,6 +8,8 @@ import ReactSwipeableViews from 'react-swipeable-views'; import TabsBar, { links, getIndex, getLink } from './tabs_bar'; import { Link } from 'react-router-dom'; +import { disableSwiping } from 'mastodon/initial_state'; + import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; @@ -185,7 +187,7 @@ class ColumnsArea extends ImmutablePureComponent { const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : ; const content = columnIndex !== -1 ? ( - + {links.map(this.renderView)} ) : ( diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index a02e3bbd7..54ec51fcf 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -10,6 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import ImageLoader from './image_loader'; import Icon from 'mastodon/components/icon'; import GIFV from 'mastodon/components/gifv'; +import { disableSwiping } from 'mastodon/initial_state'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -212,6 +213,7 @@ class MediaModal extends ImmutablePureComponent { containerStyle={containerStyle} onChangeIndex={this.handleSwipe} index={index} + disabled={disableSwiping} > {content} diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js index 1134c55db..80d43907d 100644 --- a/app/javascript/mastodon/initial_state.js +++ b/app/javascript/mastodon/initial_state.js @@ -25,5 +25,6 @@ export const usePendingItems = getMeta('use_pending_items'); export const showTrends = getMeta('trends'); export const title = getMeta('title'); export const cropImages = getMeta('crop_images'); +export const disableSwiping = getMeta('disable_swiping'); export default initialState; diff --git a/app/lib/user_settings_decorator.rb b/app/lib/user_settings_decorator.rb index fa8255faa..e37bc6d9f 100644 --- a/app/lib/user_settings_decorator.rb +++ b/app/lib/user_settings_decorator.rb @@ -27,6 +27,7 @@ class UserSettingsDecorator user.settings['display_media'] = display_media_preference if change?('setting_display_media') user.settings['expand_spoilers'] = expand_spoilers_preference if change?('setting_expand_spoilers') user.settings['reduce_motion'] = reduce_motion_preference if change?('setting_reduce_motion') + user.settings['disable_swiping'] = disable_swiping_preference if change?('setting_disable_swiping') user.settings['system_font_ui'] = system_font_ui_preference if change?('setting_system_font_ui') user.settings['noindex'] = noindex_preference if change?('setting_noindex') user.settings['theme'] = theme_preference if change?('setting_theme') @@ -88,6 +89,10 @@ class UserSettingsDecorator boolean_cast_setting 'setting_reduce_motion' end + def disable_swiping_preference + boolean_cast_setting 'setting_disable_swiping' + end + def noindex_preference boolean_cast_setting 'setting_noindex' end diff --git a/app/models/user.rb b/app/models/user.rb index 6b21d6ed6..2c460e1fd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -115,6 +115,7 @@ class User < ApplicationRecord :reduce_motion, :system_font_ui, :noindex, :theme, :display_media, :hide_network, :expand_spoilers, :default_language, :aggregate_reblogs, :show_application, :advanced_layout, :use_blurhash, :use_pending_items, :trends, :crop_images, + :disable_swiping, to: :settings, prefix: :setting, allow_nil: false attr_reader :invite_code, :sign_in_token_attempt diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index 2939004ef..70263e0c5 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -33,6 +33,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:display_media] = object.current_account.user.setting_display_media store[:expand_spoilers] = object.current_account.user.setting_expand_spoilers store[:reduce_motion] = object.current_account.user.setting_reduce_motion + store[:disable_swiping] = object.current_account.user.setting_disable_swiping store[:advanced_layout] = object.current_account.user.setting_advanced_layout store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_pending_items] = object.current_account.user.setting_use_pending_items diff --git a/app/views/settings/preferences/appearance/show.html.haml b/app/views/settings/preferences/appearance/show.html.haml index 10fff406e..14941d5fd 100644 --- a/app/views/settings/preferences/appearance/show.html.haml +++ b/app/views/settings/preferences/appearance/show.html.haml @@ -30,6 +30,7 @@ .fields-group = f.input :setting_auto_play_gif, as: :boolean, wrapper: :with_label, recommended: true = f.input :setting_reduce_motion, as: :boolean, wrapper: :with_label + = f.input :setting_disable_swiping, as: :boolean, wrapper: :with_label = f.input :setting_system_font_ui, as: :boolean, wrapper: :with_label %h4= t 'appearance.toot_layout' diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 910e77ec2..9b0af6d24 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -136,6 +136,7 @@ en: setting_default_privacy: Posting privacy setting_default_sensitive: Always mark media as sensitive setting_delete_modal: Show confirmation dialog before deleting a toot + setting_disable_swiping: Disable swiping motions setting_display_media: Media display setting_display_media_default: Default setting_display_media_hide_all: Hide all diff --git a/config/settings.yml b/config/settings.yml index 002473643..217745f28 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -26,6 +26,7 @@ defaults: &defaults expand_spoilers: false preview_sensitive_media: false reduce_motion: false + disable_swiping: false show_application: true system_font_ui: false noindex: false -- cgit From 65136600e36ded708dc4ea5a1ef04031db51b021 Mon Sep 17 00:00:00 2001 From: ThibG Date: Wed, 30 Sep 2020 19:31:17 +0200 Subject: Fix logging out on mobile (#14901) Fixes #14900 --- app/javascript/mastodon/features/getting_started/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js index 02e1bbba5..1b9994612 100644 --- a/app/javascript/mastodon/features/getting_started/index.js +++ b/app/javascript/mastodon/features/getting_started/index.js @@ -10,7 +10,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { me, profile_directory, showTrends } from '../../initial_state'; import { fetchFollowRequests } from 'mastodon/actions/accounts'; import { List as ImmutableList } from 'immutable'; -import NavigationBar from '../compose/components/navigation_bar'; +import NavigationContainer from '../compose/containers/navigation_container'; import Icon from 'mastodon/components/icon'; import LinkFooter from 'mastodon/features/ui/components/link_footer'; import TrendsContainer from './containers/trends_container'; @@ -168,7 +168,7 @@ class GettingStarted extends ImmutablePureComponent {
- {!multiColumn && } + {!multiColumn && } {navItems}
-- cgit From bec8b12bb52a57b54b66899b2650a5611d9bd561 Mon Sep 17 00:00:00 2001 From: Takeshi Umeda Date: Thu, 1 Oct 2020 11:17:46 +0900 Subject: Fix mark as read in notifications to be saved immediately (#14907) * Fix mark as read in notifications to be saved immediately * fix code style --- app/javascript/mastodon/actions/markers.js | 8 ++++++-- app/javascript/mastodon/features/notifications/index.js | 2 ++ app/javascript/mastodon/features/ui/index.js | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/actions/markers.js b/app/javascript/mastodon/actions/markers.js index 41a95503e..c4b61effd 100644 --- a/app/javascript/mastodon/actions/markers.js +++ b/app/javascript/mastodon/actions/markers.js @@ -100,8 +100,12 @@ export function submitMarkersSuccess({ home, notifications }) { }; }; -export function submitMarkers() { - return (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); +export function submitMarkers(params = {}) { + const result = (dispatch, getState) => debouncedSubmitMarkers(dispatch, getState); + if (params.immediate === true) { + debouncedSubmitMarkers.flush(); + } + return result; }; export const fetchMarkers = () => (dispatch, getState) => { diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 41a45b2b6..68afe593d 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -12,6 +12,7 @@ import { unmountNotifications, markNotificationsAsRead, } from '../../actions/notifications'; +import { submitMarkers } from '../../actions/markers'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import NotificationContainer from './containers/notification_container'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; @@ -162,6 +163,7 @@ class Notifications extends React.PureComponent { handleMarkAsRead = () => { this.props.dispatch(markNotificationsAsRead()); + this.props.dispatch(submitMarkers({ immediate: true })); }; render () { diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index d05133507..b53e319d2 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -266,7 +266,7 @@ class UI extends React.PureComponent { handleWindowFocus = () => { this.props.dispatch(focusApp()); - this.props.dispatch(submitMarkers()); + this.props.dispatch(submitMarkers({ immediate: true })); } handleWindowBlur = () => { -- cgit From a549415868fe23e0afaf258c17afafac117d0163 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 4 Oct 2020 15:02:36 +0200 Subject: Fix regressions in icon buttons in web UI (#14915) --- app/javascript/mastodon/components/icon_button.js | 1 + app/javascript/styles/mastodon/components.scss | 14 ++++++++++++-- app/views/statuses/_simple_status.html.haml | 13 ++++++------- 3 files changed, 19 insertions(+), 9 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/components/icon_button.js b/app/javascript/mastodon/components/icon_button.js index 7f83dc1b9..7ec39198a 100644 --- a/app/javascript/mastodon/components/icon_button.js +++ b/app/javascript/mastodon/components/icon_button.js @@ -116,6 +116,7 @@ export default class IconButton extends React.PureComponent { activate, deactivate, overlayed: overlay, + 'icon-button--with-counter': typeof counter !== 'undefined', }); if (typeof counter !== 'undefined') { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index a20265bb9..ec49ae120 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -163,8 +163,7 @@ } .icon-button { - display: inline-flex; - align-items: center; + display: inline-block; padding: 0; color: $action-button-color; border: 0; @@ -173,6 +172,7 @@ cursor: pointer; transition: all 100ms ease-in; transition-property: background-color, color; + text-decoration: none; &:hover, &:active, @@ -247,6 +247,12 @@ } } + &--with-counter { + display: inline-flex; + align-items: center; + width: auto !important; + } + &__counter { display: inline-block; width: 14px; @@ -1152,6 +1158,10 @@ .status__action-bar-button { margin-right: 18px; + + &.icon-button--with-counter { + margin-right: 14px; + } } .status__action-bar-dropdown { diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml index 0c0b57112..336c1f2c8 100644 --- a/app/views/statuses/_simple_status.html.haml +++ b/app/views/statuses/_simple_status.html.haml @@ -52,13 +52,12 @@ = t 'statuses.show_thread' .status__action-bar - .status__action-bar__counter - = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button modal-button' do - - if status.in_reply_to_id.nil? - = fa_icon 'reply fw' - - else - = fa_icon 'reply-all fw' - .status__action-bar__counter__label= obscured_counter status.replies_count + = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button icon-button--with-counter modal-button' do + - if status.in_reply_to_id.nil? + = fa_icon 'reply fw' + - else + = fa_icon 'reply-all fw' + %span.icon-button__counter= obscured_counter status.replies_count = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do - if status.distributable? = fa_icon 'retweet fw' -- cgit From 5a9ad221bf534cf52e2a81fda01b35088a3d8624 Mon Sep 17 00:00:00 2001 From: mayaeh Date: Mon, 5 Oct 2020 09:57:38 +0900 Subject: Update translation files (#14920) --- .../mastodon/locales/defaultMessages.json | 115 ++++++++++++++++++++- app/javascript/mastodon/locales/en.json | 13 +++ 2 files changed, 124 insertions(+), 4 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json index 9d6988cc7..c44f14dde 100644 --- a/app/javascript/mastodon/locales/defaultMessages.json +++ b/app/javascript/mastodon/locales/defaultMessages.json @@ -167,10 +167,18 @@ }, { "descriptors": [ + { + "defaultMessage": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.", + "id": "error.unexpected_crash.explanation_addons" + }, { "defaultMessage": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", "id": "error.unexpected_crash.explanation" }, + { + "defaultMessage": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", + "id": "error.unexpected_crash.next_steps_addons" + }, { "defaultMessage": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "id": "error.unexpected_crash.next_steps" @@ -265,6 +273,15 @@ ], "path": "app/javascript/mastodon/components/missing_indicator.json" }, + { + "descriptors": [ + { + "defaultMessage": "Put it back", + "id": "picture_in_picture.restore" + } + ], + "path": "app/javascript/mastodon/components/picture_in_picture_placeholder.json" + }, { "descriptors": [ { @@ -633,6 +650,15 @@ ], "path": "app/javascript/mastodon/containers/status_container.json" }, + { + "descriptors": [ + { + "defaultMessage": "Profile unavailable", + "id": "empty_column.account_unavailable" + } + ], + "path": "app/javascript/mastodon/features/account_gallery/index.json" + }, { "descriptors": [ { @@ -796,6 +822,14 @@ "defaultMessage": "Show boosts from @{name}", "id": "account.show_reblogs" }, + { + "defaultMessage": "Notify me when @{name} posts", + "id": "account.enable_notifications" + }, + { + "defaultMessage": "Stop notifying me when @{name} posts", + "id": "account.disable_notifications" + }, { "defaultMessage": "Pinned toots", "id": "navigation_bar.pins" @@ -2125,6 +2159,18 @@ "defaultMessage": "Delete", "id": "confirmations.delete_list.confirm" }, + { + "defaultMessage": "Any followed user", + "id": "lists.replies_policy.all_replies" + }, + { + "defaultMessage": "No one", + "id": "lists.replies_policy.no_replies" + }, + { + "defaultMessage": "Members of the list", + "id": "lists.replies_policy.list_replies" + }, { "defaultMessage": "Edit list", "id": "lists.edit" @@ -2133,6 +2179,10 @@ "defaultMessage": "Delete list", "id": "lists.delete" }, + { + "defaultMessage": "Show replies to:", + "id": "lists.replies_policy.title" + }, { "defaultMessage": "There is nothing in this list yet. When members of this list post new statuses, they will appear here.", "id": "empty_column.list" @@ -2271,6 +2321,10 @@ "defaultMessage": "Follows", "id": "notifications.filter.follows" }, + { + "defaultMessage": "Updates from people you follow", + "id": "notifications.filter.statuses" + }, { "defaultMessage": "All", "id": "notifications.filter.all" @@ -2313,6 +2367,10 @@ "defaultMessage": "{name} boosted your status", "id": "notification.reblog" }, + { + "defaultMessage": "{name} just posted", + "id": "notification.status" + }, { "defaultMessage": "{name} has requested to follow you", "id": "notification.follow_request" @@ -2339,6 +2397,10 @@ "defaultMessage": "Notifications", "id": "column.notifications" }, + { + "defaultMessage": "Mark every notification as read", + "id": "notifications.mark_as_read" + }, { "defaultMessage": "You don't have any notifications yet. Interact with others to start the conversation.", "id": "empty_column.notifications" @@ -2346,6 +2408,47 @@ ], "path": "app/javascript/mastodon/features/notifications/index.json" }, + { + "descriptors": [ + { + "defaultMessage": "Reply", + "id": "status.reply" + }, + { + "defaultMessage": "Reply to thread", + "id": "status.replyAll" + }, + { + "defaultMessage": "Boost", + "id": "status.reblog" + }, + { + "defaultMessage": "Boost with original visibility", + "id": "status.reblog_private" + }, + { + "defaultMessage": "Unboost", + "id": "status.cancel_reblog_private" + }, + { + "defaultMessage": "This post cannot be boosted", + "id": "status.cannot_reblog" + }, + { + "defaultMessage": "Favourite", + "id": "status.favourite" + }, + { + "defaultMessage": "Reply", + "id": "confirmations.reply.confirm" + }, + { + "defaultMessage": "Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?", + "id": "confirmations.reply.message" + } + ], + "path": "app/javascript/mastodon/features/picture_in_picture/components/footer.json" + }, { "descriptors": [ { @@ -2785,6 +2888,14 @@ "defaultMessage": "Describe for the visually impaired", "id": "upload_form.description" }, + { + "defaultMessage": "Analyzing picture…", + "id": "upload_modal.analyzing_picture" + }, + { + "defaultMessage": "Preparing OCR…", + "id": "upload_modal.preparing_ocr" + }, { "defaultMessage": "Edit media", "id": "upload_modal.edit_media" @@ -2797,10 +2908,6 @@ "defaultMessage": "Change thumbnail", "id": "upload_form.thumbnail" }, - { - "defaultMessage": "Analyzing picture…", - "id": "upload_modal.analyzing_picture" - }, { "defaultMessage": "Detect text from picture", "id": "upload_modal.detect_text" diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 8fb7d0449..9c8b3d11b 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -9,8 +9,10 @@ "account.browse_more_on_origin_server": "Browse more on the original profile", "account.cancel_follow_request": "Cancel follow request", "account.direct": "Direct message @{name}", + "account.disable_notifications": "Stop notifying me when @{name} posts", "account.domain_blocked": "Domain blocked", "account.edit_profile": "Edit profile", + "account.enable_notifications": "Notify me when @{name} posts", "account.endorse": "Feature on profile", "account.follow": "Follow", "account.followers": "Followers", @@ -166,7 +168,9 @@ "empty_column.notifications": "You don't have any notifications yet. Interact with others to start the conversation.", "empty_column.public": "There is nothing here! Write something publicly, or manually follow users from other servers to fill it up", "error.unexpected_crash.explanation": "Due to a bug in our code or a browser compatibility issue, this page could not be displayed correctly.", + "error.unexpected_crash.explanation_addons": "This page could not be displayed correctly. This error is likely caused by a browser add-on or automatic translation tools.", "error.unexpected_crash.next_steps": "Try refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", + "error.unexpected_crash.next_steps_addons": "Try disabling them and refreshing the page. If that does not help, you may still be able to use Mastodon through a different browser or native app.", "errors.unexpected_crash.copy_stacktrace": "Copy stacktrace to clipboard", "errors.unexpected_crash.report_issue": "Report issue", "follow_request.authorize": "Authorize", @@ -260,6 +264,10 @@ "lists.edit.submit": "Change title", "lists.new.create": "Add list", "lists.new.title_placeholder": "New list title", + "lists.replies_policy.all_replies": "Any followed user", + "lists.replies_policy.list_replies": "Members of the list", + "lists.replies_policy.no_replies": "No one", + "lists.replies_policy.title": "Show replies to:", "lists.search": "Search among people you follow", "lists.subheading": "Your lists", "load_pending": "{count, plural, one {# new item} other {# new items}}", @@ -298,6 +306,7 @@ "notification.own_poll": "Your poll has ended", "notification.poll": "A poll you have voted in has ended", "notification.reblog": "{name} boosted your toot", + "notification.status": "{name} just posted", "notifications.clear": "Clear notifications", "notifications.clear_confirmation": "Are you sure you want to permanently clear all your notifications?", "notifications.column_settings.alert": "Desktop notifications", @@ -319,7 +328,10 @@ "notifications.filter.follows": "Follows", "notifications.filter.mentions": "Mentions", "notifications.filter.polls": "Poll results", + "notifications.filter.statuses": "Updates from people you follow", "notifications.group": "{count} notifications", + "notifications.mark_as_read": "Mark every notification as read", + "picture_in_picture.restore": "Put it back", "poll.closed": "Closed", "poll.refresh": "Refresh", "poll.total_people": "{count, plural, one {# person} other {# people}}", @@ -446,6 +458,7 @@ "upload_modal.detect_text": "Detect text from picture", "upload_modal.edit_media": "Edit media", "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.", + "upload_modal.preparing_ocr": "Preparing OCR…", "upload_modal.preview_label": "Preview ({ratio})", "upload_progress.label": "Uploading...", "video.close": "Close video", -- cgit From 63770d3aac1611155213e4c903424849cc254519 Mon Sep 17 00:00:00 2001 From: ThibG Date: Mon, 5 Oct 2020 22:04:06 +0200 Subject: Ignore alt-key hotkeys in text fields (#14942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #14862 This used to be the case until #13987, which introduced a hotkey to toggle the Content Warning field. Unfortunately, MacOS relies on the “alt” key for many things, including composing text (see #14862), therefore, even if that makes the CW toggle hotkey significantly less useful, it makes sense to not interfere with composing toots. --- app/javascript/mastodon/features/ui/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index b53e319d2..91c86b505 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -379,7 +379,7 @@ class UI extends React.PureComponent { componentDidMount () { this.hotkeys.__mousetrap__.stopCallback = (e, element) => { - return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName) && !e.altKey; + return ['TEXTAREA', 'SELECT', 'INPUT'].includes(element.tagName); }; } -- cgit From dac3e362fd5c3d1be9e5d89149049a7eb2c6bbc4 Mon Sep 17 00:00:00 2001 From: ThibG Date: Thu, 8 Oct 2020 00:35:27 +0200 Subject: Fix unread notification marker not updating when mounting column (#14954) --- app/javascript/mastodon/reducers/notifications.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index b01db806f..98c91ebb2 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -151,7 +151,7 @@ const deleteByStatus = (state, statusId) => { const updateMounted = (state) => { state = state.update('mounted', count => count + 1); - if (!shouldCountUnreadNotifications(state)) { + if (!shouldCountUnreadNotifications(state, state.get('mounted') === 1)) { state = state.set('readMarkerId', state.get('lastReadId')); state = clearUnread(state); } @@ -167,14 +167,14 @@ const updateVisibility = (state, visibility) => { return state; }; -const shouldCountUnreadNotifications = (state) => { +const shouldCountUnreadNotifications = (state, ignoreScroll = false) => { const isTabVisible = state.get('isTabVisible'); const isOnTop = state.get('top'); const isMounted = state.get('mounted') > 0; const lastReadId = state.get('lastReadId'); const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0); - return !(isTabVisible && isOnTop && isMounted && lastItemReached); + return !(isTabVisible && (ignoreScroll || isOnTop) && isMounted && lastItemReached); }; const recountUnread = (state, last_read_id) => { -- cgit From dc52a778e111a67a5275dd4afecf3991e279e005 Mon Sep 17 00:00:00 2001 From: ThibG Date: Fri, 9 Oct 2020 17:12:52 +0200 Subject: Fix issue checking for last unread notification when there are gaps (#14960) --- app/javascript/mastodon/reducers/notifications.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 98c91ebb2..216876134 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -172,7 +172,8 @@ const shouldCountUnreadNotifications = (state, ignoreScroll = false) => { const isOnTop = state.get('top'); const isMounted = state.get('mounted') > 0; const lastReadId = state.get('lastReadId'); - const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (!state.get('items').isEmpty() && compareId(state.get('items').last().get('id'), lastReadId) <= 0); + const lastItem = state.get('items').findLast(item => item !== null); + const lastItemReached = !state.get('hasMore') || lastReadId === '0' || (lastItem && compareId(lastItem.get('id'), lastReadId) <= 0); return !(isTabVisible && (ignoreScroll || isOnTop) && isMounted && lastItemReached); }; -- cgit From f54ca3d08e068af07a5b7a8b139e7658b3236db8 Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 13 Oct 2020 00:37:21 +0200 Subject: Fix browser notification permission request logic (#13543) * Add notification permission handling code * Request notification permission when enabling any notification setting * Add badge to notification settings when permissions insufficient * Disable alerts by default, requesting permission and enable them on onboarding --- app/javascript/mastodon/actions/notifications.js | 45 ++++++++++++++++++++++ app/javascript/mastodon/actions/onboarding.js | 12 ++++++ .../mastodon/components/column_header.js | 18 ++++++++- .../mastodon/components/icon_with_badge.js | 4 +- .../notifications/components/column_settings.js | 39 ++++++++++++++++++- .../containers/column_settings_container.js | 35 ++++++++++++++++- .../mastodon/features/notifications/index.js | 3 ++ .../ui/components/notifications_counter_icon.js | 1 + app/javascript/mastodon/features/ui/index.js | 4 -- app/javascript/mastodon/main.js | 2 + app/javascript/mastodon/reducers/notifications.js | 8 ++++ app/javascript/mastodon/reducers/settings.js | 10 ++--- app/javascript/mastodon/utils/notifications.js | 29 ++++++++++++++ app/javascript/styles/mastodon/components.scss | 20 ++++++++++ 14 files changed, 215 insertions(+), 15 deletions(-) create mode 100644 app/javascript/mastodon/utils/notifications.js (limited to 'app/javascript') diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index cd03a1d78..c4fa66428 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -16,6 +16,7 @@ import { getFiltersRegex } from '../selectors'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import compareId from 'mastodon/compare_id'; import { searchTextFromRawStatus } from 'mastodon/actions/importer/normalizer'; +import { requestNotificationPermission } from '../utils/notifications'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; export const NOTIFICATIONS_UPDATE_NOOP = 'NOTIFICATIONS_UPDATE_NOOP'; @@ -33,8 +34,12 @@ export const NOTIFICATIONS_LOAD_PENDING = 'NOTIFICATIONS_LOAD_PENDING'; export const NOTIFICATIONS_MOUNT = 'NOTIFICATIONS_MOUNT'; export const NOTIFICATIONS_UNMOUNT = 'NOTIFICATIONS_UNMOUNT'; + export const NOTIFICATIONS_MARK_AS_READ = 'NOTIFICATIONS_MARK_AS_READ'; +export const NOTIFICATIONS_SET_BROWSER_SUPPORT = 'NOTIFICATIONS_SET_BROWSER_SUPPORT'; +export const NOTIFICATIONS_SET_BROWSER_PERMISSION = 'NOTIFICATIONS_SET_BROWSER_PERMISSION'; + defineMessages({ mention: { id: 'notification.mention', defaultMessage: '{name} mentioned you' }, group: { id: 'notifications.group', defaultMessage: '{count} notifications' }, @@ -235,6 +240,46 @@ export const unmountNotifications = () => ({ type: NOTIFICATIONS_UNMOUNT, }); + export const markNotificationsAsRead = () => ({ type: NOTIFICATIONS_MARK_AS_READ, }); + +// Browser support +export function setupBrowserNotifications() { + return dispatch => { + dispatch(setBrowserSupport('Notification' in window)); + if ('Notification' in window) { + dispatch(setBrowserPermission(Notification.permission)); + } + + if ('Notification' in window && 'permissions' in navigator) { + navigator.permissions.query({ name: 'notifications' }).then((status) => { + status.onchange = () => dispatch(setBrowserPermission(Notification.permission)); + }); + } + }; +} + +export function requestBrowserPermission(callback = noOp) { + return dispatch => { + requestNotificationPermission((permission) => { + dispatch(setBrowserPermission(permission)); + callback(permission); + }); + }; +}; + +export function setBrowserSupport (value) { + return { + type: NOTIFICATIONS_SET_BROWSER_SUPPORT, + value, + }; +} + +export function setBrowserPermission (value) { + return { + type: NOTIFICATIONS_SET_BROWSER_PERMISSION, + value, + }; +} diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js index a1dd3a731..90f1da7bd 100644 --- a/app/javascript/mastodon/actions/onboarding.js +++ b/app/javascript/mastodon/actions/onboarding.js @@ -1,8 +1,20 @@ import { changeSetting, saveSettings } from './settings'; +import { requestBrowserPermission } from './notifications'; export const INTRODUCTION_VERSION = 20181216044202; export const closeOnboarding = () => dispatch => { dispatch(changeSetting(['introductionVersion'], INTRODUCTION_VERSION)); dispatch(saveSettings()); + + dispatch(requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', 'alerts', 'follow'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'favourite'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); + dispatch(saveSettings()); + } + })); }; diff --git a/app/javascript/mastodon/components/column_header.js b/app/javascript/mastodon/components/column_header.js index 1bb583583..236e92296 100644 --- a/app/javascript/mastodon/components/column_header.js +++ b/app/javascript/mastodon/components/column_header.js @@ -34,6 +34,7 @@ class ColumnHeader extends React.PureComponent { onMove: PropTypes.func, onClick: PropTypes.func, appendContent: PropTypes.node, + collapseIssues: PropTypes.bool, }; state = { @@ -83,7 +84,7 @@ class ColumnHeader extends React.PureComponent { } render () { - const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent } = this.props; + const { title, icon, active, children, pinned, multiColumn, extraButton, showBackButton, intl: { formatMessage }, placeholder, appendContent, collapseIssues } = this.props; const { collapsed, animating } = this.state; const wrapperClassName = classNames('column-header__wrapper', { @@ -145,7 +146,20 @@ class ColumnHeader extends React.PureComponent { } if (children || (multiColumn && this.props.onPin)) { - collapseButton = ; + collapseButton = ( + + ); } const hasTitle = icon && title; diff --git a/app/javascript/mastodon/components/icon_with_badge.js b/app/javascript/mastodon/components/icon_with_badge.js index 7851eb4be..4214eccfd 100644 --- a/app/javascript/mastodon/components/icon_with_badge.js +++ b/app/javascript/mastodon/components/icon_with_badge.js @@ -4,16 +4,18 @@ import Icon from 'mastodon/components/icon'; const formatNumber = num => num > 40 ? '40+' : num; -const IconWithBadge = ({ id, count, className }) => ( +const IconWithBadge = ({ id, count, issueBadge, className }) => ( {count > 0 && {formatNumber(count)}} + {issueBadge && } ); IconWithBadge.propTypes = { id: PropTypes.string.isRequired, count: PropTypes.number.isRequired, + issueBadge: PropTypes.bool, className: PropTypes.string, }; diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index 8bd03fbda..be88df6d6 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -4,6 +4,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import ClearColumnButton from './clear_column_button'; import SettingToggle from './setting_toggle'; +import Icon from 'mastodon/components/icon'; export default class ColumnSettings extends React.PureComponent { @@ -12,6 +13,10 @@ export default class ColumnSettings extends React.PureComponent { pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, + onRequestNotificationPermission: PropTypes.func.isRequired, + alertsEnabled: PropTypes.bool, + browserSupport: PropTypes.bool, + browserPermission: PropTypes.bool, }; onPushChange = (path, checked) => { @@ -19,7 +24,7 @@ export default class ColumnSettings extends React.PureComponent { } render () { - const { settings, pushSettings, onChange, onClear } = this.props; + const { settings, pushSettings, onChange, onClear, onRequestNotificationPermission, alertsEnabled, browserSupport, browserPermission } = this.props; const filterShowStr = ; const filterAdvancedStr = ; @@ -30,8 +35,40 @@ export default class ColumnSettings extends React.PureComponent { const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); const pushStr = showPushSettings && ; + const settingsIssues = []; + + if (alertsEnabled && browserSupport && browserPermission !== 'granted') { + if (browserPermission === 'denied') { + settingsIssues.push( + + ); + } else if (browserPermission === 'default') { + settingsIssues.push( + + ); + } + } + return (
+ {settingsIssues && ( +
+ {settingsIssues} +
+ )} +
diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index a67f26295..664c37980 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -3,28 +3,55 @@ import { defineMessages, injectIntl } from 'react-intl'; import ColumnSettings from '../components/column_settings'; import { changeSetting } from '../../../actions/settings'; import { setFilter } from '../../../actions/notifications'; -import { clearNotifications } from '../../../actions/notifications'; +import { clearNotifications, requestBrowserPermission } from '../../../actions/notifications'; import { changeAlerts as changePushNotifications } from '../../../actions/push_notifications'; import { openModal } from '../../../actions/modal'; +import { showAlert } from '../../../actions/alerts'; const messages = defineMessages({ clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, + permissionDenied: { id: 'notifications.permission_denied', defaultMessage: 'Cannot enable desktop notifications as permission has been denied.' }, }); const mapStateToProps = state => ({ settings: state.getIn(['settings', 'notifications']), pushSettings: state.get('push_notifications'), + alertsEnabled: state.getIn(['settings', 'notifications', 'alerts']).includes(true), + browserSupport: state.getIn(['notifications', 'browserSupport']), + browserPermission: state.getIn(['notifications', 'browserPermission']), }); const mapDispatchToProps = (dispatch, { intl }) => ({ onChange (path, checked) { if (path[0] === 'push') { - dispatch(changePushNotifications(path.slice(1), checked)); + if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') { + dispatch(requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changePushNotifications(path.slice(1), checked)); + } else { + dispatch(showAlert(undefined, messages.permissionDenied)); + } + })); + } else { + dispatch(changePushNotifications(path.slice(1), checked)); + } } else if (path[0] === 'quickFilter') { dispatch(changeSetting(['notifications', ...path], checked)); dispatch(setFilter('all')); + } else if (path[0] === 'alerts' && checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') { + if (checked && typeof window.Notification !== 'undefined' && Notification.permission !== 'granted') { + dispatch(requestBrowserPermission((permission) => { + if (permission === 'granted') { + dispatch(changeSetting(['notifications', ...path], checked)); + } else { + dispatch(showAlert(undefined, messages.permissionDenied)); + } + })); + } else { + dispatch(changeSetting(['notifications', ...path], checked)); + } } else { dispatch(changeSetting(['notifications', ...path], checked)); } @@ -38,6 +65,10 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ })); }, + onRequestNotificationPermission () { + dispatch(requestBrowserPermission()); + }, + }); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(ColumnSettings)); diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 68afe593d..5e87faa08 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -55,6 +55,7 @@ const mapStateToProps = state => ({ numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, lastReadId: state.getIn(['notifications', 'readMarkerId']), canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), + needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) !== 'granted', }); export default @connect(mapStateToProps) @@ -75,6 +76,7 @@ class Notifications extends React.PureComponent { numPending: PropTypes.number, lastReadId: PropTypes.string, canMarkAsRead: PropTypes.bool, + needsNotificationPermission: PropTypes.bool, }; static defaultProps = { @@ -250,6 +252,7 @@ class Notifications extends React.PureComponent { pinned={pinned} multiColumn={multiColumn} extraButton={extraButton} + collapseIssues={this.props.needsNotificationPermission} > diff --git a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js index da553cd9f..b8932704b 100644 --- a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js +++ b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js @@ -3,6 +3,7 @@ import IconWithBadge from 'mastodon/components/icon_with_badge'; const mapStateToProps = state => ({ count: state.getIn(['notifications', 'unread']), + issueBadge: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) !== 'granted', id: 'bell', }); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 91c86b505..c6df49a5f 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -366,10 +366,6 @@ class UI extends React.PureComponent { navigator.serviceWorker.addEventListener('message', this.handleServiceWorkerPostMessage); } - if (typeof window.Notification !== 'undefined' && Notification.permission === 'default') { - window.setTimeout(() => Notification.requestPermission(), 120 * 1000); - } - this.props.dispatch(fetchMarkers()); this.props.dispatch(expandHomeTimeline()); this.props.dispatch(expandNotifications()); diff --git a/app/javascript/mastodon/main.js b/app/javascript/mastodon/main.js index da4884fd3..bda51f692 100644 --- a/app/javascript/mastodon/main.js +++ b/app/javascript/mastodon/main.js @@ -1,4 +1,5 @@ import * as registerPushNotifications from './actions/push_notifications'; +import { setupBrowserNotifications } from './actions/notifications'; import { default as Mastodon, store } from './containers/mastodon'; import React from 'react'; import ReactDOM from 'react-dom'; @@ -22,6 +23,7 @@ function main() { const props = JSON.parse(mountNode.getAttribute('data-props')); ReactDOM.render(, mountNode); + store.dispatch(setupBrowserNotifications()); if (process.env.NODE_ENV === 'production') { // avoid offline in dev mode because it's harder to debug require('offline-plugin/runtime').install(); diff --git a/app/javascript/mastodon/reducers/notifications.js b/app/javascript/mastodon/reducers/notifications.js index 216876134..1d4874717 100644 --- a/app/javascript/mastodon/reducers/notifications.js +++ b/app/javascript/mastodon/reducers/notifications.js @@ -10,6 +10,8 @@ import { NOTIFICATIONS_MOUNT, NOTIFICATIONS_UNMOUNT, NOTIFICATIONS_MARK_AS_READ, + NOTIFICATIONS_SET_BROWSER_SUPPORT, + NOTIFICATIONS_SET_BROWSER_PERMISSION, } from '../actions/notifications'; import { ACCOUNT_BLOCK_SUCCESS, @@ -40,6 +42,8 @@ const initialState = ImmutableMap({ readMarkerId: '0', isTabVisible: true, isLoading: false, + browserSupport: false, + browserPermission: 'default', }); const notificationToMap = notification => ImmutableMap({ @@ -242,6 +246,10 @@ export default function notifications(state = initialState, action) { case NOTIFICATIONS_MARK_AS_READ: const lastNotification = state.get('items').find(item => item !== null); return lastNotification ? recountUnread(state, lastNotification.get('id')) : state; + case NOTIFICATIONS_SET_BROWSER_SUPPORT: + return state.set('browserSupport', action.value); + case NOTIFICATIONS_SET_BROWSER_PERMISSION: + return state.set('browserPermission', action.value); default: return state; } diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index efef2ad9a..886353de3 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -29,12 +29,12 @@ const initialState = ImmutableMap({ notifications: ImmutableMap({ alerts: ImmutableMap({ - follow: true, + follow: false, follow_request: false, - favourite: true, - reblog: true, - mention: true, - poll: true, + favourite: false, + reblog: false, + mention: false, + poll: false, }), quickFilter: ImmutableMap({ diff --git a/app/javascript/mastodon/utils/notifications.js b/app/javascript/mastodon/utils/notifications.js new file mode 100644 index 000000000..ab119c2e3 --- /dev/null +++ b/app/javascript/mastodon/utils/notifications.js @@ -0,0 +1,29 @@ +// Handles browser quirks, based on +// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API + +const checkNotificationPromise = () => { + try { + Notification.requestPermission().then(); + } catch(e) { + return false; + } + + return true; +}; + +const handlePermission = (permission, callback) => { + // Whatever the user answers, we make sure Chrome stores the information + if(!('permission' in Notification)) { + Notification.permission = permission; + } + + callback(Notification.permission); +}; + +export const requestNotificationPermission = (callback) => { + if (checkNotificationPromise()) { + Notification.requestPermission().then((permission) => handlePermission(permission, callback)); + } else { + Notification.requestPermission((permission) => handlePermission(permission, callback)); + } +}; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index ec49ae120..8a8f20baa 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -2418,6 +2418,17 @@ a.account__display-name { line-height: 14px; color: $primary-text-color; } + + &__issue-badge { + position: absolute; + left: 11px; + bottom: 1px; + display: block; + background: $error-red; + border-radius: 50%; + width: 0.625rem; + height: 0.625rem; + } } .column-link--transparent .icon-with-badge__badge { @@ -3453,6 +3464,15 @@ a.status-card.compact:hover { cursor: pointer; } +.column-header__issue-btn { + color: $warning-red; + + &:hover { + color: $error-red; + text-decoration: underline; + } +} + .column-header__icon { display: inline-block; margin-right: 5px; -- cgit From 96761752eccfc0d239974a24e0cc2d74c6aee7ac Mon Sep 17 00:00:00 2001 From: OSAMU SATO Date: Tue, 13 Oct 2020 08:01:14 +0900 Subject: Add duration parameter to muting. (#13831) * Adding duration to muting. * Remove useless checks --- app/controllers/api/v1/accounts_controller.rb | 2 +- app/controllers/api/v1/mutes_controller.rb | 2 +- app/javascript/mastodon/actions/accounts.js | 4 +- app/javascript/mastodon/actions/mutes.js | 10 +++++ app/javascript/mastodon/components/account.js | 7 ++++ .../mastodon/features/ui/components/mute_modal.js | 43 +++++++++++++++++++--- app/javascript/mastodon/locales/en.json | 2 + app/javascript/mastodon/locales/ja.json | 2 + app/javascript/mastodon/reducers/mutes.js | 4 ++ app/javascript/styles/mastodon-light/diff.scss | 5 +++ app/javascript/styles/mastodon/components.scss | 16 ++++++++ app/models/concerns/account_interactions.rb | 7 +++- app/models/mute.rb | 2 + app/serializers/rest/muted_account_serializer.rb | 10 +++++ app/services/mute_service.rb | 6 ++- app/workers/delete_mute_worker.rb | 10 +++++ .../20200317021758_add_expires_at_to_mutes.rb | 5 +++ db/schema.rb | 1 + 18 files changed, 124 insertions(+), 14 deletions(-) create mode 100644 app/serializers/rest/muted_account_serializer.rb create mode 100644 app/workers/delete_mute_worker.rb create mode 100644 db/migrate/20200317021758_add_expires_at_to_mutes.rb (limited to 'app/javascript') diff --git a/app/controllers/api/v1/accounts_controller.rb b/app/controllers/api/v1/accounts_controller.rb index 4a97f0251..3e66ff212 100644 --- a/app/controllers/api/v1/accounts_controller.rb +++ b/app/controllers/api/v1/accounts_controller.rb @@ -42,7 +42,7 @@ class Api::V1::AccountsController < Api::BaseController end def mute - MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications)) + MuteService.new.call(current_user.account, @account, notifications: truthy_param?(:notifications), duration: (params[:duration] || 0)) render json: @account, serializer: REST::RelationshipSerializer, relationships: relationships end diff --git a/app/controllers/api/v1/mutes_controller.rb b/app/controllers/api/v1/mutes_controller.rb index 805d0dee2..fd52511d7 100644 --- a/app/controllers/api/v1/mutes_controller.rb +++ b/app/controllers/api/v1/mutes_controller.rb @@ -7,7 +7,7 @@ class Api::V1::MutesController < Api::BaseController def index @accounts = load_accounts - render json: @accounts, each_serializer: REST::AccountSerializer + render json: @accounts, each_serializer: REST::MutedAccountSerializer end private diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 723c04e55..58b636602 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -257,11 +257,11 @@ export function unblockAccountFail(error) { }; -export function muteAccount(id, notifications) { +export function muteAccount(id, notifications, duration=0) { return (dispatch, getState) => { dispatch(muteAccountRequest(id)); - api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications }).then(response => { + api(getState).post(`/api/v1/accounts/${id}/mute`, { notifications, duration }).then(response => { // Pass in entire statuses map so we can use it to filter stuff in different parts of the reducers dispatch(muteAccountSuccess(response.data, getState().get('statuses'))); }).catch(error => { diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js index 9f645faee..d8874f353 100644 --- a/app/javascript/mastodon/actions/mutes.js +++ b/app/javascript/mastodon/actions/mutes.js @@ -13,6 +13,7 @@ export const MUTES_EXPAND_FAIL = 'MUTES_EXPAND_FAIL'; export const MUTES_INIT_MODAL = 'MUTES_INIT_MODAL'; export const MUTES_TOGGLE_HIDE_NOTIFICATIONS = 'MUTES_TOGGLE_HIDE_NOTIFICATIONS'; +export const MUTES_CHANGE_DURATION = 'MUTES_CHANGE_DURATION'; export function fetchMutes() { return (dispatch, getState) => { @@ -104,3 +105,12 @@ export function toggleHideNotifications() { dispatch({ type: MUTES_TOGGLE_HIDE_NOTIFICATIONS }); }; } + +export function changeMuteDuration(duration) { + return dispatch => { + dispatch({ + type: MUTES_CHANGE_DURATION, + duration, + }); + }; +} diff --git a/app/javascript/mastodon/components/account.js b/app/javascript/mastodon/components/account.js index 2705a6001..0e40ee1d6 100644 --- a/app/javascript/mastodon/components/account.js +++ b/app/javascript/mastodon/components/account.js @@ -8,6 +8,7 @@ import IconButton from './icon_button'; import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { me } from '../initial_state'; +import RelativeTimestamp from './relative_timestamp'; const messages = defineMessages({ follow: { id: 'account.follow', defaultMessage: 'Follow' }, @@ -107,11 +108,17 @@ class Account extends ImmutablePureComponent { } } + let mute_expires_at; + if (account.get('mute_expires_at')) { + mute_expires_at =
; + } + return (
+ {mute_expires_at}
diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js index 852830c3c..51228b532 100644 --- a/app/javascript/mastodon/features/ui/components/mute_modal.js +++ b/app/javascript/mastodon/features/ui/components/mute_modal.js @@ -1,25 +1,31 @@ import React from 'react'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; -import { injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import Toggle from 'react-toggle'; import Button from '../../../components/button'; import { closeModal } from '../../../actions/modal'; import { muteAccount } from '../../../actions/accounts'; -import { toggleHideNotifications } from '../../../actions/mutes'; +import { toggleHideNotifications, changeMuteDuration } from '../../../actions/mutes'; +const messages = defineMessages({ + minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, + hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, + days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, +}); const mapStateToProps = state => { return { account: state.getIn(['mutes', 'new', 'account']), notifications: state.getIn(['mutes', 'new', 'notifications']), + muteDuration: state.getIn(['mutes', 'new', 'duration']), }; }; const mapDispatchToProps = dispatch => { return { - onConfirm(account, notifications) { - dispatch(muteAccount(account.get('id'), notifications)); + onConfirm(account, notifications, muteDuration) { + dispatch(muteAccount(account.get('id'), notifications, muteDuration)); }, onClose() { @@ -29,6 +35,10 @@ const mapDispatchToProps = dispatch => { onToggleNotifications() { dispatch(toggleHideNotifications()); }, + + onChangeMuteDuration(e) { + dispatch(changeMuteDuration(e.target.value)); + }, }; }; @@ -43,6 +53,8 @@ class MuteModal extends React.PureComponent { onConfirm: PropTypes.func.isRequired, onToggleNotifications: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, + muteDuration: PropTypes.number.isRequired, + onChangeMuteDuration: PropTypes.func.isRequired, }; componentDidMount() { @@ -51,7 +63,7 @@ class MuteModal extends React.PureComponent { handleClick = () => { this.props.onClose(); - this.props.onConfirm(this.props.account, this.props.notifications); + this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration); } handleCancel = () => { @@ -66,8 +78,12 @@ class MuteModal extends React.PureComponent { this.props.onToggleNotifications(); } + changeMuteDuration = (e) => { + this.props.onChangeMuteDuration(e); + } + render () { - const { account, notifications } = this.props; + const { account, notifications, muteDuration, intl } = this.props; return (
@@ -91,6 +107,21 @@ class MuteModal extends React.PureComponent {
+
+ : + + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 9c8b3d11b..b53731340 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -276,6 +276,8 @@ "missing_indicator.label": "Not found", "missing_indicator.sublabel": "This resource could not be found", "mute_modal.hide_notifications": "Hide notifications from this user?", + "mute_modal.duration": "Duration", + "mute_modal.indefinite": "Indefinite", "navigation_bar.apps": "Mobile apps", "navigation_bar.blocks": "Blocked users", "navigation_bar.bookmarks": "Bookmarks", diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json index ec3d0ee59..2a1df987c 100644 --- a/app/javascript/mastodon/locales/ja.json +++ b/app/javascript/mastodon/locales/ja.json @@ -268,6 +268,8 @@ "missing_indicator.label": "見つかりません", "missing_indicator.sublabel": "見つかりませんでした", "mute_modal.hide_notifications": "このユーザーからの通知を隠しますか?", + "mute_modal.duration": "ミュートする期間", + "mute_modal.indefinite": "無期限", "navigation_bar.apps": "アプリ", "navigation_bar.blocks": "ブロックしたユーザー", "navigation_bar.bookmarks": "ブックマーク", diff --git a/app/javascript/mastodon/reducers/mutes.js b/app/javascript/mastodon/reducers/mutes.js index 4672e5097..a9eb61ff8 100644 --- a/app/javascript/mastodon/reducers/mutes.js +++ b/app/javascript/mastodon/reducers/mutes.js @@ -3,12 +3,14 @@ import Immutable from 'immutable'; import { MUTES_INIT_MODAL, MUTES_TOGGLE_HIDE_NOTIFICATIONS, + MUTES_CHANGE_DURATION, } from '../actions/mutes'; const initialState = Immutable.Map({ new: Immutable.Map({ account: null, notifications: true, + duration: 0, }), }); @@ -21,6 +23,8 @@ export default function mutes(state = initialState, action) { }); case MUTES_TOGGLE_HIDE_NOTIFICATIONS: return state.updateIn(['new', 'notifications'], (old) => !old); + case MUTES_CHANGE_DURATION: + return state.setIn(['new', 'duration'], Number(action.duration)); default: return state; } diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss index 6b81e7623..64f4adb84 100644 --- a/app/javascript/styles/mastodon-light/diff.scss +++ b/app/javascript/styles/mastodon-light/diff.scss @@ -759,3 +759,8 @@ html { .compose-form .compose-form__warning { box-shadow: none; } + +.mute-modal select { + border: 1px solid lighten($ui-base-color, 8%); + background: $simple-background-color url("data:image/svg+xml;utf8,") no-repeat right 8px center / auto 16px; +} diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 8a8f20baa..378d1640f 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -5074,6 +5074,22 @@ a.status-card.compact:hover { } } } + + select { + appearance: none; + box-sizing: border-box; + font-size: 14px; + color: $inverted-text-color; + display: inline-block; + width: auto; + outline: 0; + font-family: inherit; + background: $simple-background-color url("data:image/svg+xml;utf8,") no-repeat right 8px center / auto 16px; + border: 1px solid darken($simple-background-color, 14%); + border-radius: 4px; + padding: 6px 10px; + padding-right: 30px; + } } .confirmation-modal__container, diff --git a/app/models/concerns/account_interactions.rb b/app/models/concerns/account_interactions.rb index 427ebdae2..6a0ad5aa9 100644 --- a/app/models/concerns/account_interactions.rb +++ b/app/models/concerns/account_interactions.rb @@ -131,9 +131,12 @@ module AccountInteractions .find_or_create_by!(target_account: other_account) end - def mute!(other_account, notifications: nil) + def mute!(other_account, notifications: nil, duration: 0) notifications = true if notifications.nil? - mute = mute_relationships.create_with(hide_notifications: notifications).find_or_create_by!(target_account: other_account) + mute = mute_relationships.create_with(hide_notifications: notifications).find_or_initialize_by(target_account: other_account) + mute.expires_in = duration.zero? ? nil : duration + mute.save! + remove_potential_friendship(other_account) # When toggling a mute between hiding and allowing notifications, the mute will already exist, so the find_or_create_by! call will return the existing Mute without updating the hide_notifications attribute. Therefore, we check that hide_notifications? is what we want and set it if it isn't. diff --git a/app/models/mute.rb b/app/models/mute.rb index 0e00c2278..578345ef6 100644 --- a/app/models/mute.rb +++ b/app/models/mute.rb @@ -9,11 +9,13 @@ # account_id :bigint(8) not null # target_account_id :bigint(8) not null # hide_notifications :boolean default(TRUE), not null +# expires_at :datetime # class Mute < ApplicationRecord include Paginable include RelationshipCacheable + include Expireable belongs_to :account belongs_to :target_account, class_name: 'Account' diff --git a/app/serializers/rest/muted_account_serializer.rb b/app/serializers/rest/muted_account_serializer.rb new file mode 100644 index 000000000..3ddd706dc --- /dev/null +++ b/app/serializers/rest/muted_account_serializer.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class REST::MutedAccountSerializer < REST::AccountSerializer + attribute :mute_expires_at + + def mute_expires_at + mute = current_user.account.mute_relationships.find_by(target_account_id: object.id) + mute && !mute.expired? ? mute.expires_at : nil + end +end diff --git a/app/services/mute_service.rb b/app/services/mute_service.rb index 676804cb9..9ae9afd62 100644 --- a/app/services/mute_service.rb +++ b/app/services/mute_service.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true class MuteService < BaseService - def call(account, target_account, notifications: nil) + def call(account, target_account, notifications: nil, duration: 0) return if account.id == target_account.id - mute = account.mute!(target_account, notifications: notifications) + mute = account.mute!(target_account, notifications: notifications, duration: duration) if mute.hide_notifications? BlockWorker.perform_async(account.id, target_account.id) @@ -12,6 +12,8 @@ class MuteService < BaseService MuteWorker.perform_async(account.id, target_account.id) end + DeleteMuteWorker.perform_at(duration.seconds, mute.id) if duration != 0 + mute end end diff --git a/app/workers/delete_mute_worker.rb b/app/workers/delete_mute_worker.rb new file mode 100644 index 000000000..eb031020e --- /dev/null +++ b/app/workers/delete_mute_worker.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +class DeleteMuteWorker + include Sidekiq::Worker + + def perform(mute_id) + mute = Mute.find_by(id: mute_id) + UnmuteService.new.call(mute.account, mute.target_account) if mute&.expired? + end +end diff --git a/db/migrate/20200317021758_add_expires_at_to_mutes.rb b/db/migrate/20200317021758_add_expires_at_to_mutes.rb new file mode 100644 index 000000000..eaae8319d --- /dev/null +++ b/db/migrate/20200317021758_add_expires_at_to_mutes.rb @@ -0,0 +1,5 @@ +class AddExpiresAtToMutes < ActiveRecord::Migration[5.2] + def change + add_column :mutes, :expires_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 5805f3105..262e25b3b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -545,6 +545,7 @@ ActiveRecord::Schema.define(version: 2020_10_08_220312) do t.boolean "hide_notifications", default: true, null: false t.bigint "account_id", null: false t.bigint "target_account_id", null: false + t.datetime "expires_at" t.index ["account_id", "target_account_id"], name: "index_mutes_on_account_id_and_target_account_id", unique: true t.index ["target_account_id"], name: "index_mutes_on_target_account_id" end -- cgit From 4c45b43cb8a3d902c130729d36d559ec9de23d3e Mon Sep 17 00:00:00 2001 From: ThibG Date: Tue, 13 Oct 2020 01:19:35 +0200 Subject: Change how CDN_HOST is passed down to make assets build reproducible (#14381) * Change how CDN_HOST is passed down to make assets build reproducible * Change webpacker/webpack configuration to dynamically load publicPath based on meta header * Fix embedded layout missing the cdn-host meta header --- .../mastodon/components/autosuggest_emoji.js | 3 +-- .../compose/components/emoji_picker_dropdown.js | 2 +- app/javascript/mastodon/features/emoji/emoji.js | 3 +-- .../getting_started/components/announcements.js | 3 +-- .../features/ui/components/focal_point_modal.js | 3 +-- app/javascript/mastodon/utils/config.js | 10 ++++++++++ app/javascript/packs/about.js | 1 + app/javascript/packs/admin.js | 1 + app/javascript/packs/application.js | 1 + app/javascript/packs/error.js | 1 + app/javascript/packs/public-path.js | 21 +++++++++++++++++++++ app/javascript/packs/public.js | 1 + app/javascript/packs/share.js | 1 + app/views/layouts/application.html.haml | 1 + app/views/layouts/embedded.html.haml | 1 + config/webpack/configuration.js | 17 ++--------------- 16 files changed, 46 insertions(+), 24 deletions(-) create mode 100644 app/javascript/mastodon/utils/config.js create mode 100644 app/javascript/packs/public-path.js (limited to 'app/javascript') diff --git a/app/javascript/mastodon/components/autosuggest_emoji.js b/app/javascript/mastodon/components/autosuggest_emoji.js index ce4383a60..4937e4d98 100644 --- a/app/javascript/mastodon/components/autosuggest_emoji.js +++ b/app/javascript/mastodon/components/autosuggest_emoji.js @@ -1,8 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import unicodeMapping from '../features/emoji/emoji_unicode_mapping_light'; - -const assetHost = process.env.CDN_HOST || ''; +import { assetHost } from 'mastodon/utils/config'; export default class AutosuggestEmoji extends React.PureComponent { diff --git a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js index e8a36a923..bac136e5e 100644 --- a/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/emoji_picker_dropdown.js @@ -7,6 +7,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import detectPassiveEvents from 'detect-passive-events'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import { assetHost } from 'mastodon/utils/config'; const messages = defineMessages({ emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, @@ -25,7 +26,6 @@ const messages = defineMessages({ flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, }); -const assetHost = process.env.CDN_HOST || ''; let EmojiPicker, Emoji; // load asynchronously const backgroundImageFn = () => `${assetHost}/emoji/sheet_10.png`; diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index 5d9dad097..4e37f3a80 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -1,11 +1,10 @@ import { autoPlayGif } from '../../initial_state'; import unicodeMapping from './emoji_unicode_mapping_light'; +import { assetHost } from 'mastodon/utils/config'; import Trie from 'substring-trie'; const trie = new Trie(Object.keys(unicodeMapping)); -const assetHost = process.env.CDN_HOST || ''; - // Convert to file names from emojis. (For different variation selector emojis) const emojiFilenames = (emojis) => { return emojis.map(v => unicodeMapping[v].filename); diff --git a/app/javascript/mastodon/features/getting_started/components/announcements.js b/app/javascript/mastodon/features/getting_started/components/announcements.js index 8f824f140..4853c3935 100644 --- a/app/javascript/mastodon/features/getting_started/components/announcements.js +++ b/app/javascript/mastodon/features/getting_started/components/announcements.js @@ -15,6 +15,7 @@ import EmojiPickerDropdown from 'mastodon/features/compose/containers/emoji_pick import AnimatedNumber from 'mastodon/components/animated_number'; import TransitionMotion from 'react-motion/lib/TransitionMotion'; import spring from 'react-motion/lib/spring'; +import { assetHost } from 'mastodon/utils/config'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -153,8 +154,6 @@ class Content extends ImmutablePureComponent { } -const assetHost = process.env.CDN_HOST || ''; - class Emoji extends React.PureComponent { static propTypes = { diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js index 926a495d1..e19f277d8 100644 --- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js +++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js @@ -20,6 +20,7 @@ import GIFV from 'mastodon/components/gifv'; import { me } from 'mastodon/initial_state'; import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js'; import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js'; +import { assetHost } from 'mastodon/utils/config'; const messages = defineMessages({ close: { id: 'lightbox.close', defaultMessage: 'Close' }, @@ -50,8 +51,6 @@ const removeExtraLineBreaks = str => str.replace(/\n\n/g, '******') .replace(/\n/g, ' ') .replace(/\*\*\*\*\*\*/g, '\n\n'); -const assetHost = process.env.CDN_HOST || ''; - class ImageLoader extends React.PureComponent { static propTypes = { diff --git a/app/javascript/mastodon/utils/config.js b/app/javascript/mastodon/utils/config.js new file mode 100644 index 000000000..932cd0cbf --- /dev/null +++ b/app/javascript/mastodon/utils/config.js @@ -0,0 +1,10 @@ +import ready from '../ready'; + +export let assetHost = ''; + +ready(() => { + const cdnHost = document.querySelector('meta[name=cdn-host]'); + if (cdnHost) { + assetHost = cdnHost.content || ''; + } +}); diff --git a/app/javascript/packs/about.js b/app/javascript/packs/about.js index 843cb2c87..892d825ec 100644 --- a/app/javascript/packs/about.js +++ b/app/javascript/packs/about.js @@ -1,3 +1,4 @@ +import './public-path'; import loadPolyfills from '../mastodon/load_polyfills'; import { start } from '../mastodon/common'; diff --git a/app/javascript/packs/admin.js b/app/javascript/packs/admin.js index 51f92de8a..65b8dc040 100644 --- a/app/javascript/packs/admin.js +++ b/app/javascript/packs/admin.js @@ -1,3 +1,4 @@ +import './public-path'; import { delegate } from '@rails/ujs'; import ready from '../mastodon/ready'; diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index c65ebed74..91240aecf 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -1,3 +1,4 @@ +import './public-path'; import loadPolyfills from '../mastodon/load_polyfills'; import { start } from '../mastodon/common'; diff --git a/app/javascript/packs/error.js b/app/javascript/packs/error.js index 685c89065..6376dc2f5 100644 --- a/app/javascript/packs/error.js +++ b/app/javascript/packs/error.js @@ -1,3 +1,4 @@ +import './public-path'; import ready from '../mastodon/ready'; ready(() => { diff --git a/app/javascript/packs/public-path.js b/app/javascript/packs/public-path.js new file mode 100644 index 000000000..f96109f4f --- /dev/null +++ b/app/javascript/packs/public-path.js @@ -0,0 +1,21 @@ +// Dynamically set webpack's loading path depending on a meta header, in order +// to share the same assets regardless of instance configuration. +// See https://webpack.js.org/guides/public-path/#on-the-fly + +function removeOuterSlashes(string) { + return string.replace(/^\/*/, '').replace(/\/*$/, ''); +} + +function formatPublicPath(host = '', path = '') { + let formattedHost = removeOuterSlashes(host); + if (formattedHost && !/^http/i.test(formattedHost)) { + formattedHost = `//${formattedHost}`; + } + const formattedPath = removeOuterSlashes(path); + return `${formattedHost}/${formattedPath}/`; +} + +const cdnHost = document.querySelector('meta[name=cdn-host]'); + +// eslint-disable-next-line camelcase, no-undef, no-unused-vars +__webpack_public_path__ = formatPublicPath(cdnHost ? cdnHost.content : '', process.env.PUBLIC_OUTPUT_PATH); diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index 551e281a8..39defa7ae 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -1,3 +1,4 @@ +import './public-path'; import escapeTextContentForBrowser from 'escape-html'; import loadPolyfills from '../mastodon/load_polyfills'; import ready from '../mastodon/ready'; diff --git a/app/javascript/packs/share.js b/app/javascript/packs/share.js index 4ef23e1b2..1225d7b52 100644 --- a/app/javascript/packs/share.js +++ b/app/javascript/packs/share.js @@ -1,3 +1,4 @@ +import './public-path'; import loadPolyfills from '../mastodon/load_polyfills'; import { start } from '../mastodon/common'; diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index e32cdcabb..1f10f40c0 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -6,6 +6,7 @@ - if cdn_host? %link{ rel: 'dns-prefetch', href: cdn_host }/ + %meta{ name: 'cdn-host', content: cdn_host }/ - if storage_host? %link{ rel: 'dns-prefetch', href: storage_host }/ diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index 4a40b8584..37051e70c 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -6,6 +6,7 @@ - if cdn_host? %link{ rel: 'dns-prefetch', href: cdn_host }/ + %meta{ name: 'cdn-host', content: cdn_host }/ - if storage_host? %link{ rel: 'dns-prefetch', href: storage_host }/ diff --git a/config/webpack/configuration.js b/config/webpack/configuration.js index 80a094c72..e4f88a9c4 100644 --- a/config/webpack/configuration.js +++ b/config/webpack/configuration.js @@ -11,30 +11,17 @@ const settings = safeLoad(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env const themePath = resolve('config', 'themes.yml'); const themes = safeLoad(readFileSync(themePath), 'utf8'); -function removeOuterSlashes(string) { - return string.replace(/^\/*/, '').replace(/\/*$/, ''); -} - -function formatPublicPath(host = '', path = '') { - let formattedHost = removeOuterSlashes(host); - if (formattedHost && !/^http/i.test(formattedHost)) { - formattedHost = `//${formattedHost}`; - } - const formattedPath = removeOuterSlashes(path); - return `${formattedHost}/${formattedPath}/`; -} - const output = { path: resolve('public', settings.public_output_path), - publicPath: formatPublicPath(env.CDN_HOST, settings.public_output_path), + publicPath: `/${settings.public_output_path}/`, }; module.exports = { settings, themes, env: { - CDN_HOST: env.CDN_HOST, NODE_ENV: env.NODE_ENV, + PUBLIC_OUTPUT_PATH: settings.public_output_path, }, output, }; -- cgit From a69ca294738dbe22bacaf9f1fc5a551d99797b35 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 15 Oct 2020 16:24:47 +0200 Subject: Change how missing desktop notifications permission is displayed (#14985) Add missing controls for new notification type --- app/javascript/mastodon/actions/onboarding.js | 1 + .../notifications/components/column_settings.js | 61 ++++++++-------------- .../components/notifications_permission_banner.js | 30 +++++++++++ .../notifications/components/setting_toggle.js | 5 +- .../containers/column_settings_container.js | 2 +- .../mastodon/features/notifications/index.js | 8 +-- .../ui/components/notifications_counter_icon.js | 1 - app/javascript/mastodon/reducers/settings.js | 3 ++ app/javascript/styles/mastodon/components.scss | 26 +++++++++ 9 files changed, 92 insertions(+), 45 deletions(-) create mode 100644 app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js (limited to 'app/javascript') diff --git a/app/javascript/mastodon/actions/onboarding.js b/app/javascript/mastodon/actions/onboarding.js index 90f1da7bd..42d8ea33f 100644 --- a/app/javascript/mastodon/actions/onboarding.js +++ b/app/javascript/mastodon/actions/onboarding.js @@ -14,6 +14,7 @@ export const closeOnboarding = () => dispatch => { dispatch(changeSetting(['notifications', 'alerts', 'reblog'], true)); dispatch(changeSetting(['notifications', 'alerts', 'mention'], true)); dispatch(changeSetting(['notifications', 'alerts', 'poll'], true)); + dispatch(changeSetting(['notifications', 'alerts', 'status'], true)); dispatch(saveSettings()); } })); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.js b/app/javascript/mastodon/features/notifications/components/column_settings.js index be88df6d6..169e4b44d 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.js +++ b/app/javascript/mastodon/features/notifications/components/column_settings.js @@ -4,7 +4,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { FormattedMessage } from 'react-intl'; import ClearColumnButton from './clear_column_button'; import SettingToggle from './setting_toggle'; -import Icon from 'mastodon/components/icon'; export default class ColumnSettings extends React.PureComponent { @@ -13,7 +12,7 @@ export default class ColumnSettings extends React.PureComponent { pushSettings: ImmutablePropTypes.map.isRequired, onChange: PropTypes.func.isRequired, onClear: PropTypes.func.isRequired, - onRequestNotificationPermission: PropTypes.func.isRequired, + onRequestNotificationPermission: PropTypes.func, alertsEnabled: PropTypes.bool, browserSupport: PropTypes.bool, browserPermission: PropTypes.bool, @@ -24,7 +23,7 @@ export default class ColumnSettings extends React.PureComponent { } render () { - const { settings, pushSettings, onChange, onClear, onRequestNotificationPermission, alertsEnabled, browserSupport, browserPermission } = this.props; + const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission } = this.props; const filterShowStr = ; const filterAdvancedStr = ; @@ -35,37 +34,11 @@ export default class ColumnSettings extends React.PureComponent { const showPushSettings = pushSettings.get('browserSupport') && pushSettings.get('isSubscribed'); const pushStr = showPushSettings && ; - const settingsIssues = []; - - if (alertsEnabled && browserSupport && browserPermission !== 'granted') { - if (browserPermission === 'denied') { - settingsIssues.push( - - ); - } else if (browserPermission === 'default') { - settingsIssues.push( - - ); - } - } - return (
- {settingsIssues && ( -
- {settingsIssues} + {alertsEnabled && browserSupport && browserPermission === 'denied' && ( +
+
)} @@ -77,6 +50,7 @@ export default class ColumnSettings extends React.PureComponent { +
@@ -87,7 +61,7 @@ export default class ColumnSettings extends React.PureComponent {
- + {showPushSettings && } @@ -98,7 +72,7 @@ export default class ColumnSettings extends React.PureComponent {
- + {showPushSettings && } @@ -109,7 +83,7 @@ export default class ColumnSettings extends React.PureComponent {
- + {showPushSettings && } @@ -120,7 +94,7 @@ export default class ColumnSettings extends React.PureComponent {
- + {showPushSettings && } @@ -131,7 +105,7 @@ export default class ColumnSettings extends React.PureComponent {
- + {showPushSettings && } @@ -142,12 +116,23 @@ export default class ColumnSettings extends React.PureComponent {
- + {showPushSettings && }
+ +
+ + +
+ + {showPushSettings && } + + +
+
); } diff --git a/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js b/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js new file mode 100644 index 000000000..766c9bb5b --- /dev/null +++ b/app/javascript/mastodon/features/notifications/components/notifications_permission_banner.js @@ -0,0 +1,30 @@ +import React from 'react'; +import Icon from 'mastodon/components/icon'; +import Button from 'mastodon/components/button'; +import { requestBrowserPermission } from 'mastodon/actions/notifications'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +export default @connect(() => {}) +class NotificationsPermissionBanner extends React.PureComponent { + + static propTypes = { + dispatch: PropTypes.func.isRequired, + }; + + handleClick = () => { + this.props.dispatch(requestBrowserPermission()); + } + + render () { + return ( +
+

+

}} />

+ +
+ ); + } + +} diff --git a/app/javascript/mastodon/features/notifications/components/setting_toggle.js b/app/javascript/mastodon/features/notifications/components/setting_toggle.js index e6f593ef8..c4c8bffbe 100644 --- a/app/javascript/mastodon/features/notifications/components/setting_toggle.js +++ b/app/javascript/mastodon/features/notifications/components/setting_toggle.js @@ -12,6 +12,7 @@ export default class SettingToggle extends React.PureComponent { label: PropTypes.node.isRequired, onChange: PropTypes.func.isRequired, defaultValue: PropTypes.bool, + disabled: PropTypes.bool, } onChange = ({ target }) => { @@ -19,12 +20,12 @@ export default class SettingToggle extends React.PureComponent { } render () { - const { prefix, settings, settingPath, label, defaultValue } = this.props; + const { prefix, settings, settingPath, label, defaultValue, disabled } = this.props; const id = ['setting-toggle', prefix, ...settingPath].filter(Boolean).join('-'); return (
- +
); diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js index 664c37980..9a70bd4f3 100644 --- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js +++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js @@ -11,7 +11,7 @@ import { showAlert } from '../../../actions/alerts'; const messages = defineMessages({ clearMessage: { id: 'notifications.clear_confirmation', defaultMessage: 'Are you sure you want to permanently clear all your notifications?' }, clearConfirm: { id: 'notifications.clear', defaultMessage: 'Clear notifications' }, - permissionDenied: { id: 'notifications.permission_denied', defaultMessage: 'Cannot enable desktop notifications as permission has been denied.' }, + permissionDenied: { id: 'notifications.permission_denied_alert', defaultMessage: 'Desktop notifications can\'t be enabled, as browser permission has been denied before' }, }); const mapStateToProps = state => ({ diff --git a/app/javascript/mastodon/features/notifications/index.js b/app/javascript/mastodon/features/notifications/index.js index 5e87faa08..73df7f49d 100644 --- a/app/javascript/mastodon/features/notifications/index.js +++ b/app/javascript/mastodon/features/notifications/index.js @@ -25,6 +25,7 @@ import ScrollableList from '../../components/scrollable_list'; import LoadGap from '../../components/load_gap'; import Icon from 'mastodon/components/icon'; import compareId from 'mastodon/compare_id'; +import NotificationsPermissionBanner from './components/notifications_permission_banner'; const messages = defineMessages({ title: { id: 'column.notifications', defaultMessage: 'Notifications' }, @@ -55,7 +56,7 @@ const mapStateToProps = state => ({ numPending: state.getIn(['notifications', 'pendingItems'], ImmutableList()).size, lastReadId: state.getIn(['notifications', 'readMarkerId']), canMarkAsRead: state.getIn(['notifications', 'readMarkerId']) !== '0' && getNotifications(state).some(item => item !== null && compareId(item.get('id'), state.getIn(['notifications', 'readMarkerId'])) > 0), - needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) !== 'granted', + needsNotificationPermission: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) === 'default', }); export default @connect(mapStateToProps) @@ -169,7 +170,7 @@ class Notifications extends React.PureComponent { }; render () { - const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead } = this.props; + const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props; const pinned = !!columnId; const emptyMessage = ; @@ -213,6 +214,8 @@ class Notifications extends React.PureComponent { showLoading={isLoading && notifications.size === 0} hasMore={hasMore} numPending={numPending} + prepend={needsNotificationPermission && } + alwaysPrepend emptyMessage={emptyMessage} onLoadMore={this.handleLoadOlder} onLoadPending={this.handleLoadPending} @@ -252,7 +255,6 @@ class Notifications extends React.PureComponent { pinned={pinned} multiColumn={multiColumn} extraButton={extraButton} - collapseIssues={this.props.needsNotificationPermission} > diff --git a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js index b8932704b..da553cd9f 100644 --- a/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js +++ b/app/javascript/mastodon/features/ui/components/notifications_counter_icon.js @@ -3,7 +3,6 @@ import IconWithBadge from 'mastodon/components/icon_with_badge'; const mapStateToProps = state => ({ count: state.getIn(['notifications', 'unread']), - issueBadge: state.getIn(['settings', 'notifications', 'alerts']).includes(true) && state.getIn(['notifications', 'browserSupport']) && state.getIn(['notifications', 'browserPermission']) !== 'granted', id: 'bell', }); diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 886353de3..057fa353a 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -35,6 +35,7 @@ const initialState = ImmutableMap({ reblog: false, mention: false, poll: false, + status: false, }), quickFilter: ImmutableMap({ @@ -50,6 +51,7 @@ const initialState = ImmutableMap({ reblog: true, mention: true, poll: true, + status: true, }), sounds: ImmutableMap({ @@ -59,6 +61,7 @@ const initialState = ImmutableMap({ reblog: true, mention: true, poll: true, + status: true, }), }), diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 378d1640f..0d32b9d6c 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3732,6 +3732,10 @@ a.status-card.compact:hover { margin-bottom: 10px; } +.column-settings__row--with-margin { + margin-bottom: 15px; +} + .column-settings__hashtags { .column-settings__row { margin-bottom: 15px; @@ -7168,3 +7172,25 @@ noscript { border-color: lighten($ui-base-color, 12%); } } + +.notifications-permission-banner { + padding: 30px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + h2 { + font-size: 16px; + font-weight: 500; + margin-bottom: 15px; + text-align: center; + } + + p { + color: $darker-text-color; + margin-bottom: 15px; + text-align: center; + } +} -- cgit From fb5f3be18f50eb5657479196e29d13a905c43ac8 Mon Sep 17 00:00:00 2001 From: mayaeh Date: Fri, 16 Oct 2020 04:24:58 +0900 Subject: Fix strings that could not be translated (#14980) --- .../mastodon/features/ui/components/mute_modal.js | 3 +- .../mastodon/locales/defaultMessages.json | 49 ++++++++++++++++++++++ app/javascript/mastodon/locales/en.json | 8 +++- 3 files changed, 58 insertions(+), 2 deletions(-) (limited to 'app/javascript') diff --git a/app/javascript/mastodon/features/ui/components/mute_modal.js b/app/javascript/mastodon/features/ui/components/mute_modal.js index 51228b532..d8d8e68c3 100644 --- a/app/javascript/mastodon/features/ui/components/mute_modal.js +++ b/app/javascript/mastodon/features/ui/components/mute_modal.js @@ -12,6 +12,7 @@ const messages = defineMessages({ minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, + indefinite: { id: 'mute_modal.indefinite', defaultMessage: 'Indefinite' }, }); const mapStateToProps = state => { @@ -112,7 +113,7 @@ class MuteModal extends React.PureComponent { {/* eslint-disable-next-line jsx-a11y/no-onchange */}