From 4ec1771165ab8dd40e52804fd087eacfab25290b Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Thu, 28 Sep 2017 15:31:31 +0200 Subject: Add ability to specify alternative text for media attachments (#5123) * Fix #117 - Add ability to specify alternative text for media attachments - POST /api/v1/media accepts `description` straight away - PUT /api/v1/media/:id to update `description` (only for unattached ones) - Serialized as `name` of Document object in ActivityPub - Uploads form adjusted for better performance and description input * Add tests * Change undo button blend mode to difference --- app/javascript/styles/components.scss | 47 +++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 7 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index da479347b..631cd7a13 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -335,12 +335,52 @@ .compose-form__uploads-wrapper { display: flex; + flex-direction: row; padding: 5px; + flex-wrap: wrap; } .compose-form__upload { flex: 1 1 0; + min-width: 40%; margin: 5px; + + &-description { + position: absolute; + z-index: 2; + bottom: 0; + left: 0; + right: 0; + box-sizing: border-box; + background: linear-gradient(0deg, rgba($base-shadow-color, 0.8) 0, rgba($base-shadow-color, 0.35) 80%, transparent); + padding: 10px; + opacity: 0; + transition: opacity .1s ease; + + input { + background: transparent; + color: $ui-secondary-color; + border: 0; + padding: 0; + margin: 0; + width: 100%; + font-family: inherit; + font-size: 14px; + font-weight: 500; + + &:focus { + color: $white; + } + } + + &.active { + opacity: 1; + } + } + + .icon-button { + mix-blend-mode: difference; + } } .compose-form__upload-thumbnail { @@ -352,13 +392,6 @@ width: 100%; } -.compose-form__upload-cancel { - background-size: cover; - border-radius: 4px; - height: 100px; - width: 100px; -} - .compose-form__label { display: block; line-height: 24px; -- cgit From 6e0659c838bacfea54bbab5a4dd3501fbdf8b668 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Thu, 28 Sep 2017 11:43:18 -0700 Subject: Improve performance of modal and swipe animations (#5135) * Improve performance of modal and swipe animations * Fix eslint issues --- .../mastodon/features/ui/components/media_modal.js | 7 ++- .../mastodon/features/ui/components/modal_root.js | 65 ++++++++++------------ app/javascript/styles/components.scss | 9 ++- 3 files changed, 40 insertions(+), 41 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index da2ceecb1..705645b40 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -84,14 +84,17 @@ export default class MediaModal extends ImmutablePureComponent { return null; }).toArray(); + const containerStyle = { + alignItems: 'center', // center vertically + }; + return (
{leftNav}
- - + {content}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index a09c9d9b3..f420f0abf 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,7 +1,5 @@ import React from 'react'; import PropTypes from 'prop-types'; -import TransitionMotion from 'react-motion/lib/TransitionMotion'; -import spring from 'react-motion/lib/spring'; import BundleContainer from '../containers/bundle_container'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; @@ -35,6 +33,10 @@ export default class ModalRoot extends React.PureComponent { onClose: PropTypes.func.isRequired, }; + state = { + revealed: false, + }; + handleKeyUp = (e) => { if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) && !!this.props.type) { @@ -51,6 +53,8 @@ export default class ModalRoot extends React.PureComponent { this.activeElement = document.activeElement; this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); + } else if (!nextProps.type) { + this.setState({ revealed: false }); } } @@ -60,6 +64,11 @@ export default class ModalRoot extends React.PureComponent { this.activeElement.focus(); this.activeElement = null; } + if (this.props.type) { + requestAnimationFrame(() => { + this.setState({ revealed: true }); + }); + } } componentWillUnmount () { @@ -74,14 +83,6 @@ export default class ModalRoot extends React.PureComponent { this.node = ref; } - willEnter () { - return { opacity: 0, scale: 0.98 }; - } - - willLeave () { - return { opacity: spring(0), scale: spring(0.98) }; - } - renderLoading = modalId => () => { return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? : null; } @@ -94,38 +95,30 @@ export default class ModalRoot extends React.PureComponent { render () { const { type, props, onClose } = this.props; + const { revealed } = this.state; const visible = !!type; - const items = []; - if (visible) { - items.push({ - key: type, - data: { type, props }, - style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) }, - }); + if (!visible) { + return ( +
+ ); } return ( - - {interpolatedStyles => -
- {interpolatedStyles.map(({ key, data: { type, props }, style }) => ( -
-
-
- - {(SpecificComponent) => } - -
-
- ))} +
+
+
+
+ { + visible ? + ( + {(SpecificComponent) => } + ) : + null + }
- } - +
+
); } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 631cd7a13..5ea0d134e 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -2983,14 +2983,18 @@ button.icon-button.active i.fa-retweet { } } +.modal-root { + transition: opacity 0.3s linear; + will-change: opacity; + z-index: 9999; +} + .modal-root__overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; - z-index: 9999; - opacity: 0; background: rgba($base-overlay-background, 0.7); transform: translateZ(0); } @@ -3007,7 +3011,6 @@ button.icon-button.active i.fa-retweet { justify-content: center; align-content: space-around; z-index: 9999; - opacity: 0; pointer-events: none; user-select: none; } -- cgit From 0b3f1ec62a08ab2aad2b7c1ab8f88bdac5e8a3c6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 1 Oct 2017 10:52:39 +0200 Subject: Reorganize preferences page (#5161) --- app/javascript/styles/forms.scss | 11 ++++++++ app/views/settings/preferences/show.html.haml | 38 ++++++++++++++------------- config/locales/de.yml | 1 - config/locales/devise.de.yml | 4 +-- config/locales/en.yml | 6 +++++ config/locales/ja.yml | 2 +- config/locales/oc.yml | 2 +- config/locales/pl.yml | 2 +- config/locales/simple_form.en.yml | 5 ++-- 9 files changed, 45 insertions(+), 26 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index 0526f174c..d241c9d26 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -22,6 +22,16 @@ code { margin-top: 4px; } + h4 { + text-transform: uppercase; + font-size: 13px; + font-weight: 500; + color: $ui-primary-color; + padding-bottom: 8px; + margin-bottom: 8px; + border-bottom: 1px solid lighten($ui-base-color, 8%); + } + p.hint { margin-bottom: 15px; color: $ui-primary-color; @@ -316,6 +326,7 @@ code { select { font-size: 16px; + max-height: 29px; } .input-with-append { diff --git a/app/views/settings/preferences/show.html.haml b/app/views/settings/preferences/show.html.haml index 5efd538e4..ffb1bbf6a 100644 --- a/app/views/settings/preferences/show.html.haml +++ b/app/views/settings/preferences/show.html.haml @@ -4,30 +4,22 @@ = simple_form_for current_user, url: settings_preferences_path, html: { method: :put } do |f| = render 'shared/error_messages', object: current_user + %h4= t 'preferences.languages' + .fields-group - = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| safe_join([I18n.t("themes.#{theme}", default: theme)])}, wrapper: :with_label, include_blank: false - - = f.input :locale, - collection: I18n.available_locales, - wrapper: :with_label, - include_blank: false, - label_method: lambda { |locale| human_locale(locale) }, - selected: I18n.locale - - = f.input :filtered_languages, - collection: filterable_languages, - wrapper: :with_block_label, - include_blank: false, - label_method: lambda { |locale| human_locale(locale) }, - required: false, - as: :check_boxes, - collection_wrapper_tag: 'ul', - item_wrapper_tag: 'li' + = f.input :locale, collection: I18n.available_locales, wrapper: :with_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, selected: I18n.locale + + = f.input :filtered_languages, collection: filterable_languages, wrapper: :with_block_label, include_blank: false, label_method: lambda { |locale| human_locale(locale) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' + + %h4= t 'preferences.publishing' + .fields-group = f.input :setting_default_privacy, collection: Status.visibilities.keys - ['direct'], wrapper: :with_label, include_blank: false, label_method: lambda { |visibility| safe_join([I18n.t("statuses.visibilities.#{visibility}"), content_tag(:span, I18n.t("statuses.visibilities.#{visibility}_long"), class: 'hint')]) }, required: false, as: :radio_buttons, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :setting_default_sensitive, as: :boolean, wrapper: :with_label + %h4= t 'preferences.notifications' + .fields-group = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :follow, as: :boolean, wrapper: :with_label @@ -35,6 +27,9 @@ = ff.input :reblog, as: :boolean, wrapper: :with_label = ff.input :favourite, as: :boolean, wrapper: :with_label = ff.input :mention, as: :boolean, wrapper: :with_label + + .fields-group + = f.simple_fields_for :notification_emails, hash_to_object(current_user.settings.notification_emails) do |ff| = ff.input :digest, as: :boolean, wrapper: :with_label .fields-group @@ -42,10 +37,17 @@ = ff.input :must_be_follower, as: :boolean, wrapper: :with_label = ff.input :must_be_following, as: :boolean, wrapper: :with_label + %h4= t 'preferences.other' + .fields-group = f.input :setting_noindex, as: :boolean, wrapper: :with_label + %h4= t 'preferences.web' + .fields-group + - if Themes.instance.names.size > 1 + = f.input :setting_theme, collection: Themes.instance.names, label_method: lambda { |theme| I18n.t("themes.#{theme}", default: theme) }, wrapper: :with_label, include_blank: false + = f.input :setting_unfollow_modal, as: :boolean, wrapper: :with_label = f.input :setting_boost_modal, as: :boolean, wrapper: :with_label = f.input :setting_delete_modal, as: :boolean, wrapper: :with_label diff --git a/config/locales/de.yml b/config/locales/de.yml index 06a535ba6..1192a7b10 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -388,7 +388,6 @@ de: private_long: Nur für Folgende sichtbar public: Öffentlich public_long: Für alle sichtbar - unlisted: Nicht gelistet unlisted: Für alle sichtbar, aber nicht in öffentlichen Zeitleisten aufgelistet stream_entries: click_to_show: Klicken, um zu zeigen diff --git a/config/locales/devise.de.yml b/config/locales/devise.de.yml index b1e26f1e5..0db946b9f 100644 --- a/config/locales/devise.de.yml +++ b/config/locales/devise.de.yml @@ -8,10 +8,10 @@ de: failure: already_authenticated: Du bist bereits angemeldet. inactive: Dein Konto wurde noch nicht aktiviert. - invalid: '%{authentication_keys} oder Passwort ungültig.' + invalid: "%{authentication_keys} oder Passwort ungültig." last_attempt: Du hast noch einen Versuch, bevor dein Konto gesperrt wird. locked: Dein Konto ist gesperrt. - not_found_in_database: '%{authentication_keys} oder Passwort ungültig.' + not_found_in_database: "%{authentication_keys} oder Passwort ungültig." timeout: Deine Sitzung ist abgelaufen. Bitte melde dich erneut an. unauthenticated: Du musst dich anmelden oder registrieren, bevor du fortfahren kannst. unconfirmed: Du musst deine E-Mail-Adresse bestätigen, bevor du fortfahren kannst. diff --git a/config/locales/en.yml b/config/locales/en.yml index f87d8532c..3049e0365 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -393,6 +393,12 @@ en: next: Next prev: Prev truncate: "…" + preferences: + languages: Languages + notifications: Notifications + other: Other + publishing: Publishing + web: Web push_notifications: favourite: title: "%{name} favourited your status" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 2ccc827da..364bfcfd6 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -247,7 +247,7 @@ ja: salutation: "%{name} さん" settings: 'メール設定の変更: %{link}' signature: Mastodon %{instance} インスタンスからの通知 - view: 'リンク' + view: リンク applications: created: アプリが作成されました destroyed: アプリが削除されました diff --git a/config/locales/oc.yml b/config/locales/oc.yml index 406de36f0..3100e6265 100644 --- a/config/locales/oc.yml +++ b/config/locales/oc.yml @@ -93,8 +93,8 @@ oc: reset_password: Reïnicializar lo senhal resubscribe: Se tornar abonar salmon_url: URL Salmon - shared_inbox_url: URL de recepcion partejada search: Cercar + shared_inbox_url: URL de recepcion partejada show: created_reports: Rapòrts creat per aqueste compte report: rapòrt diff --git a/config/locales/pl.yml b/config/locales/pl.yml index 9bf57e38b..f52883123 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -62,7 +62,7 @@ pl: followers: Śledzący followers_url: Adres śledzących follows: Śledzeni - inbox: Adres skrzynki + inbox_url: Adres skrzynki ip: Adres IP location: all: Wszystkie diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml index 6f8b88898..86c80290c 100644 --- a/config/locales/simple_form.en.yml +++ b/config/locales/simple_form.en.yml @@ -4,6 +4,7 @@ en: hints: defaults: avatar: PNG, GIF or JPG. At most 2MB. Will be downscaled to 120x120px + digest: Sent after a long period of inactivity with a summary of mentions you've received in your absence display_name: one: 1 character left other: %{count} characters left @@ -19,7 +20,7 @@ en: sessions: otp: Enter the Two-factor code from your phone or use one of your recovery codes. user: - filtered_languages: Selected languages will be removed from your public timelines. + filtered_languages: Checked languages will be filtered from public timelines for you labels: defaults: avatar: Avatar @@ -44,7 +45,7 @@ en: setting_delete_modal: Show confirmation dialog before deleting a toot setting_noindex: Opt-out of search engine indexing setting_system_font_ui: Use system's default font - setting_theme: Site theme + setting_theme: Site theme setting_unfollow_modal: Show confirmation dialog before unfollowing someone severity: Severity type: Import type -- cgit From cdad7977fc94cd6a1a97841ed0f25e8504cb80d6 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sun, 1 Oct 2017 12:20:00 +0200 Subject: Improve privacy dropdown, remove react-simple-dropdown dependency (#5140) * Improve privacy dropdown, remove react-simple-dropdown dependency * Animate privacy warning * Fix react-router-scroll --- .../compose/components/privacy_dropdown.js | 154 +++++++++++++++------ .../features/compose/components/warning.js | 11 +- app/javascript/styles/components.scss | 27 ++-- app/javascript/styles/rtl.scss | 18 +-- 4 files changed, 138 insertions(+), 72 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js index 0474dfb4e..d5bb58712 100644 --- a/app/javascript/mastodon/features/compose/components/privacy_dropdown.js +++ b/app/javascript/mastodon/features/compose/components/privacy_dropdown.js @@ -2,7 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import { injectIntl, defineMessages } from 'react-intl'; import IconButton from '../../../components/icon_button'; +import { Overlay } from 'react-overlays'; +import { Motion, spring } from 'react-motion'; import detectPassiveEvents from 'detect-passive-events'; +import classNames from 'classnames'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, @@ -16,10 +19,77 @@ const messages = defineMessages({ change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, }); -const iconStyle = { - height: null, - lineHeight: '27px', -}; +const listenerOptions = detectPassiveEvents.hasSupport ? { passive: true } : false; + +class PrivacyDropdownMenu extends React.PureComponent { + + static propTypes = { + style: PropTypes.object, + items: PropTypes.array.isRequired, + value: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + } + + handleClick = e => { + if (e.key === 'Escape') { + this.props.onClose(); + } else if (!e.key || e.key === 'Enter') { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + } + } + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + } + + render () { + const { style, items, value } = this.props; + + return ( + + {({ opacity, scaleX, scaleY }) => ( +
+ {items.map(item => +
+
+ +
+ +
+ {item.text} + {item.meta} +
+
+ )} +
+ )} +
+ ); + } + +} @injectIntl export default class PrivacyDropdown extends React.PureComponent { @@ -55,26 +125,30 @@ export default class PrivacyDropdown extends React.PureComponent { handleModalActionClick = (e) => { e.preventDefault(); + const { value } = this.options[e.currentTarget.getAttribute('data-index')]; + this.props.onModalClose(); this.props.onChange(value); } - handleClick = (e) => { - if (e.key === 'Escape') { - this.setState({ open: false }); - } else if (!e.key || e.key === 'Enter') { - const value = e.currentTarget.getAttribute('data-index'); - e.preventDefault(); - this.setState({ open: false }); - this.props.onChange(value); + handleKeyDown = e => { + switch(e.key) { + case 'Enter': + this.handleToggle(); + break; + case 'Escape': + this.handleClose(); + break; } } - onGlobalClick = (e) => { - if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { - this.setState({ open: false }); - } + handleClose = () => { + this.setState({ open: false }); + } + + handleChange = value => { + this.props.onChange(value); } componentWillMount () { @@ -88,20 +162,6 @@ export default class PrivacyDropdown extends React.PureComponent { ]; } - componentDidMount () { - window.addEventListener('click', this.onGlobalClick); - window.addEventListener('touchstart', this.onGlobalClick, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - - componentWillUnmount () { - window.removeEventListener('click', this.onGlobalClick); - window.removeEventListener('touchstart', this.onGlobalClick, detectPassiveEvents.hasSupport ? { passive: true } : false); - } - - setRef = (c) => { - this.node = c; - } - render () { const { value, intl } = this.props; const { open } = this.state; @@ -109,19 +169,29 @@ export default class PrivacyDropdown extends React.PureComponent { const valueOption = this.options.find(item => item.value === value); return ( -
-
-
- {open && this.options.map(item => -
-
-
- {item.text} - {item.meta} -
-
- )} +
+
+
+ + + +
); } diff --git a/app/javascript/mastodon/features/compose/components/warning.js b/app/javascript/mastodon/features/compose/components/warning.js index 75f36b840..dc902f33b 100644 --- a/app/javascript/mastodon/features/compose/components/warning.js +++ b/app/javascript/mastodon/features/compose/components/warning.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import { Motion, spring } from 'react-motion'; export default class Warning extends React.PureComponent { @@ -11,9 +12,13 @@ export default class Warning extends React.PureComponent { const { message } = this.props; return ( -
- {message} -
+ + {({ opacity, scaleX, scaleY }) => ( +
+ {message} +
+ )} +
); } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 5ea0d134e..caa7c0787 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1275,7 +1275,7 @@ background: $ui-secondary-color; padding: 4px 0; border-radius: 4px; - box-shadow: 0 0 15px rgba($base-shadow-color, 0.4); + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); ul { list-style: none; @@ -2805,19 +2805,12 @@ button.icon-button.active i.fa-retweet { filter: none; } -.privacy-dropdown { - position: relative; -} - .privacy-dropdown__dropdown { - display: none; position: absolute; - left: 0; - top: 27px; - width: 230px; background: $simple-background-color; - border-radius: 0 4px 4px; - z-index: 2; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + border-radius: 4px; + margin-left: 40px; overflow: hidden; } @@ -2869,6 +2862,18 @@ button.icon-button.active i.fa-retweet { background: $simple-background-color; border-radius: 4px 4px 0 0; box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); + + .icon-button { + transition: none; + } + + &.active { + background: $ui-highlight-color; + + .icon-button { + color: $primary-text-color; + } + } } .privacy-dropdown__dropdown { diff --git a/app/javascript/styles/rtl.scss b/app/javascript/styles/rtl.scss index 0fdeccd9c..67bfa8a38 100644 --- a/app/javascript/styles/rtl.scss +++ b/app/javascript/styles/rtl.scss @@ -128,22 +128,8 @@ body.rtl { } .privacy-dropdown__dropdown { - left: auto; - right: 0; - } - - .dropdown--active .dropdown__content { - text-align: right; - } - - .dropdown--active .dropdown__content::before { - left: auto; - right: 8px; - } - - .dropdown--active .dropdown__content > ul { - left: auto; - right: -10px; + margin-left: 0; + margin-right: 40px; } .privacy-dropdown__option__icon { -- cgit From c567c874537b733a3e15625ad01eb0ae0ced8f4e Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Sun, 1 Oct 2017 18:01:01 -0700 Subject: Toggle contain:strict on fullscreen (#5159) * Toggle contain:strict on fullscreen * Fix scss lint issue * fix scss whitespace lint issue --- .../mastodon/components/scrollable_list.js | 11 +++++- .../mastodon/features/ui/util/fullscreen.js | 46 ++++++++++++++++++++++ app/javascript/mastodon/features/video/index.js | 30 +------------- app/javascript/styles/components.scss | 10 +++++ 4 files changed, 67 insertions(+), 30 deletions(-) create mode 100644 app/javascript/mastodon/features/ui/util/fullscreen.js (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index ff0540e5d..c6b588765 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -6,6 +6,8 @@ import LoadMore from './load_more'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import { throttle } from 'lodash'; import { List as ImmutableList } from 'immutable'; +import classNames from 'classnames'; +import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen'; export default class ScrollableList extends PureComponent { @@ -66,6 +68,7 @@ export default class ScrollableList extends PureComponent { componentDidMount () { this.attachScrollListener(); this.attachIntersectionObserver(); + attachFullscreenListener(this.onFullScreenChange); // Handle initial scroll posiiton this.handleScroll(); @@ -92,6 +95,11 @@ export default class ScrollableList extends PureComponent { componentWillUnmount () { this.detachScrollListener(); this.detachIntersectionObserver(); + detachFullscreenListener(this.onFullScreenChange); + } + + onFullScreenChange = () => { + this.setState({ fullscreen: isFullscreen() }); } attachIntersectionObserver () { @@ -165,6 +173,7 @@ export default class ScrollableList extends PureComponent { render () { const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; + const { fullscreen } = this.state; const childrenCount = React.Children.count(children); const loadMore = (hasMore && childrenCount > 0) ? : null; @@ -172,7 +181,7 @@ export default class ScrollableList extends PureComponent { if (isLoading || childrenCount > 0 || !emptyMessage) { scrollableArea = ( -
+
{prepend} diff --git a/app/javascript/mastodon/features/ui/util/fullscreen.js b/app/javascript/mastodon/features/ui/util/fullscreen.js new file mode 100644 index 000000000..cf5d0cf98 --- /dev/null +++ b/app/javascript/mastodon/features/ui/util/fullscreen.js @@ -0,0 +1,46 @@ +// APIs for normalizing fullscreen operations. Note that Edge uses +// the WebKit-prefixed APIs currently (as of Edge 16). + +export const isFullscreen = () => document.fullscreenElement || + document.webkitFullscreenElement || + document.mozFullScreenElement; + +export const exitFullscreen = () => { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } +}; + +export const requestFullscreen = el => { + if (el.requestFullscreen) { + el.requestFullscreen(); + } else if (el.webkitRequestFullscreen) { + el.webkitRequestFullscreen(); + } else if (el.mozRequestFullScreen) { + el.mozRequestFullScreen(); + } +}; + +export const attachFullscreenListener = (listener) => { + if ('onfullscreenchange' in document) { + document.addEventListener('fullscreenchange', listener); + } else if ('onwebkitfullscreenchange' in document) { + document.addEventListener('webkitfullscreenchange', listener); + } else if ('onmozfullscreenchange' in document) { + document.addEventListener('mozfullscreenchange', listener); + } +}; + +export const detachFullscreenListener = (listener) => { + if ('onfullscreenchange' in document) { + document.removeEventListener('fullscreenchange', listener); + } else if ('onwebkitfullscreenchange' in document) { + document.removeEventListener('webkitfullscreenchange', listener); + } else if ('onmozfullscreenchange' in document) { + document.removeEventListener('mozfullscreenchange', listener); + } +}; diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js index 069264ef5..7502dda8b 100644 --- a/app/javascript/mastodon/features/video/index.js +++ b/app/javascript/mastodon/features/video/index.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { throttle } from 'lodash'; import classNames from 'classnames'; +import { isFullscreen, requestFullscreen, exitFullscreen } from '../ui/util/fullscreen'; const messages = defineMessages({ play: { id: 'video.play', defaultMessage: 'Play' }, @@ -69,35 +70,6 @@ const getPointerPosition = (el, event) => { return position; }; -const isFullscreen = () => document.fullscreenElement || - document.webkitFullscreenElement || - document.mozFullScreenElement || - document.msFullscreenElement; - -const exitFullscreen = () => { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } -}; - -const requestFullscreen = el => { - if (el.requestFullscreen) { - el.requestFullscreen(); - } else if (el.webkitRequestFullscreen) { - el.webkitRequestFullscreen(); - } else if (el.mozRequestFullScreen) { - el.mozRequestFullScreen(); - } else if (el.msRequestFullscreen) { - el.msRequestFullscreen(); - } -}; - @injectIntl export default class Video extends React.PureComponent { diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index caa7c0787..48d6e0c4d 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -1674,6 +1674,16 @@ &.optionally-scrollable { overflow-y: auto; } + + @supports(display: grid) { // hack to fix Chrome <57 + contain: strict; + } +} + +.scrollable.fullscreen { + @supports(display: grid) { // hack to fix Chrome <57 + contain: none; + } } .column-back-button { -- cgit From 4453c9a9f59f818a746dd6aa3cdf06566d8c0d32 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 2 Oct 2017 18:24:05 +0200 Subject: Search popout (#5170) --- .../mastodon/features/compose/components/search.js | 52 +++++++++++++++++++++- app/javascript/styles/components.scss | 31 +++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 85ef767ab..79abffad8 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -1,11 +1,46 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { Overlay } from 'react-overlays'; +import { Motion, spring } from 'react-motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, }); +class SearchPopout extends React.PureComponent { + + static propTypes = { + style: PropTypes.object, + }; + + render () { + const { style } = this.props; + + return ( +
+ + {({ opacity, scaleX, scaleY }) => ( +
+

+ +
    +
  • #example
  • +
  • @username@domain
  • +
  • URL
  • +
  • URL
  • +
+ + +
+ )} +
+
+ ); + } + +} + @injectIntl export default class Search extends React.PureComponent { @@ -19,6 +54,10 @@ export default class Search extends React.PureComponent { intl: PropTypes.object.isRequired, }; + state = { + expanded: false, + }; + handleChange = (e) => { this.props.onChange(e.target.value); } @@ -43,11 +82,17 @@ export default class Search extends React.PureComponent { } handleFocus = () => { + this.setState({ expanded: true }); this.props.onShow(); } + handleBlur = () => { + this.setState({ expanded: false }); + } + render () { const { intl, value, submitted } = this.props; + const { expanded } = this.state; const hasValue = value.length > 0 || submitted; return ( @@ -62,6 +107,7 @@ export default class Search extends React.PureComponent { onChange={this.handleChange} onKeyUp={this.handleKeyDown} onFocus={this.handleFocus} + onBlur={this.handleBlur} /> @@ -69,6 +115,10 @@ export default class Search extends React.PureComponent {
+ + + +
); } diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 48d6e0c4d..e83a22e00 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -4063,6 +4063,37 @@ button.icon-button.active i.fa-retweet { border-radius: 0; } +.search-popout { + background: $simple-background-color; + border-radius: 4px; + padding: 10px 14px; + padding-bottom: 14px; + margin-top: 10px; + color: $ui-primary-color; + box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); + + h4 { + text-transform: uppercase; + color: $ui-primary-color; + font-size: 13px; + font-weight: 500; + margin-bottom: 10px; + } + + li { + padding: 4px 0; + } + + ul { + margin-bottom: 10px; + } + + em { + font-weight: 500; + color: $ui-base-color; + } +} + noscript { text-align: center; -- cgit From ecacb15cd50609fb3d749ecac89835a43255fb34 Mon Sep 17 00:00:00 2001 From: Yamagishi Kazutoshi Date: Tue, 3 Oct 2017 20:10:57 +0900 Subject: Add placeholder text color to form of media attachments (#5196) --- app/javascript/styles/components.scss | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index e83a22e00..3e1b08e9f 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -371,6 +371,11 @@ &:focus { color: $white; } + + &::placeholder { + opacity: 0.54; + color: $ui-secondary-color; + } } &.active { -- cgit From 875d943c189afe9887200f357d916a9f8fd19fe8 Mon Sep 17 00:00:00 2001 From: PFM Date: Wed, 4 Oct 2017 00:11:22 +0900 Subject: Add pagination in media modal (#4343) * Add pagination in media modal * Change array name * Add an element class * Avoid nested class * Pull out the active class * Use map instead of forEach * Remove parentheses --- .../mastodon/features/ui/components/media_modal.js | 21 ++++++++++++++++- app/javascript/styles/components.scss | 27 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/features/ui/components/media_modal.js b/app/javascript/mastodon/features/ui/components/media_modal.js index 705645b40..f41a83089 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.js +++ b/app/javascript/mastodon/features/ui/components/media_modal.js @@ -29,7 +29,7 @@ export default class MediaModal extends ImmutablePureComponent { }; handleSwipe = (index) => { - this.setState({ index: (index) % this.props.media.size }); + this.setState({ index: index % this.props.media.size }); } handleNextClick = () => { @@ -40,6 +40,11 @@ export default class MediaModal extends ImmutablePureComponent { this.setState({ index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size }); } + handleChangeIndex = (e) => { + const index = Number(e.currentTarget.getAttribute('data-index')); + this.setState({ index: index % this.props.media.size }); + } + handleKeyUp = (e) => { switch(e.key) { case 'ArrowLeft': @@ -67,10 +72,21 @@ export default class MediaModal extends ImmutablePureComponent { const { media, intl, onClose } = this.props; const index = this.getIndex(); + let pagination = []; const leftNav = media.size > 1 && ; const rightNav = media.size > 1 && ; + if (media.size > 1) { + pagination = media.map((item, i) => { + const classes = ['media-modal__button']; + if (i === index) { + classes.push('media-modal__button--active'); + } + return (
  • ); + }); + } + const content = media.map((image) => { const width = image.getIn(['meta', 'original', 'width']) || null; const height = image.getIn(['meta', 'original', 'height']) || null; @@ -98,6 +114,9 @@ export default class MediaModal extends ImmutablePureComponent { {content}
    +
      + {pagination} +
    {rightNav}
    diff --git a/app/javascript/styles/components.scss b/app/javascript/styles/components.scss index 3e1b08e9f..6ef4e3866 100644 --- a/app/javascript/styles/components.scss +++ b/app/javascript/styles/components.scss @@ -3080,6 +3080,33 @@ button.icon-button.active i.fa-retweet { background: $base-overlay-background; } +.media-modal__pagination { + width: 100%; + text-align: center; + position: absolute; + left: 0; + bottom: -40px; +} + +.media-modal__page-dot { + display: inline-block; +} + +.media-modal__button { + background-color: $white; + height: 12px; + width: 12px; + border-radius: 6px; + margin: 10px; + padding: 0; + border: 0; + font-size: 0; +} + +.media-modal__button--active { + background-color: $ui-highlight-color; +} + .media-modal__close { position: absolute; right: 4px; -- cgit From 32e8a87830f2b054f2a32ded4c41d91003503d14 Mon Sep 17 00:00:00 2001 From: Lynx Kotoura Date: Thu, 5 Oct 2017 05:49:36 +0900 Subject: adjust public profile pages 2 (#5223) --- app/javascript/styles/accounts.scss | 17 ++++++++--------- app/javascript/styles/forms.scss | 1 + config/initializers/kaminari_config.rb | 3 +-- 3 files changed, 10 insertions(+), 11 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/styles/accounts.scss b/app/javascript/styles/accounts.scss index 744650554..30adf8cdc 100644 --- a/app/javascript/styles/accounts.scss +++ b/app/javascript/styles/accounts.scss @@ -69,12 +69,16 @@ position: relative; z-index: 2; margin-bottom: 30px; + overflow: hidden; + text-overflow: ellipsis; small { display: block; font-size: 14px; color: $ui-highlight-color; font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; } } @@ -284,21 +288,15 @@ color: lighten($ui-base-color, 10%); } - @media screen and (max-width: 360px) { + @media screen and (max-width: 700px) { padding: 30px 20px; - a, - .current, - .next, - .prev, - .gap { + .page { display: none; } .next, - .prev, - .next a, - .prev a { + .prev { display: inline-block; } } @@ -375,6 +373,7 @@ height: 80px; border-radius: 80px; border: 2px solid $simple-background-color; + background: $simple-background-color; } } diff --git a/app/javascript/styles/forms.scss b/app/javascript/styles/forms.scss index d241c9d26..61fcf286f 100644 --- a/app/javascript/styles/forms.scss +++ b/app/javascript/styles/forms.scss @@ -515,6 +515,7 @@ code { .action-pagination { display: flex; + flex-wrap: wrap; align-items: center; .actions, diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb index 27b183eeb..aa1517256 100644 --- a/config/initializers/kaminari_config.rb +++ b/config/initializers/kaminari_config.rb @@ -3,6 +3,5 @@ Kaminari.configure do |config| config.default_per_page = 40 config.window = 1 - config.left = 3 - config.right = 1 + config.outer_window = 1 end -- cgit From 7db0f8dcb2110b4ec8815bedc965cfbd01a59798 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 6 Oct 2017 01:07:59 +0200 Subject: Implement hotkeys for web UI (#5164) * Fix #2102 - Implement hotkeys Hotkeys on status list: - r to reply - m to mention author - f to favourite - b to boost - enter to open status - p to open author's profile - up or k to move up in the list - down or j to move down in the list - 1-9 to focus a status in one of the columns - n to focus the compose textarea - alt+n to start a brand new toot - backspace to navigate back * Add navigational hotkeys The key g followed by: - s: start - h: home - n: notifications - l: local timeline - t: federated timeline - f: favourites - u: own profile - p: pinned toots - b: blocked users - m: muted users * Add hotkey for focusing search, make escape un-focus compose/search * Fix focusing notifications column, fix hotkeys in compose textarea --- app/javascript/mastodon/actions/compose.js | 7 + .../mastodon/components/autosuggest_textarea.js | 14 +- .../mastodon/components/scrollable_list.js | 28 +-- app/javascript/mastodon/components/status.js | 116 ++++++++--- app/javascript/mastodon/components/status_list.js | 31 ++- .../mastodon/features/compose/components/search.js | 2 + .../notifications/components/notification.js | 115 ++++++++--- .../containers/notification_container.js | 9 +- .../mastodon/features/notifications/index.js | 28 ++- app/javascript/mastodon/features/status/index.js | 151 ++++++++++++-- app/javascript/mastodon/features/ui/index.js | 223 +++++++++++++++++---- app/javascript/mastodon/reducers/compose.js | 2 + app/javascript/styles/basics.scss | 13 +- app/javascript/styles/components.scss | 24 ++- package.json | 1 + yarn.lock | 21 ++ 16 files changed, 631 insertions(+), 154 deletions(-) (limited to 'app/javascript/styles') diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index 7ac33bdd0..ed4837ebd 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -16,6 +16,7 @@ export const COMPOSE_SUBMIT_FAIL = 'COMPOSE_SUBMIT_FAIL'; export const COMPOSE_REPLY = 'COMPOSE_REPLY'; export const COMPOSE_REPLY_CANCEL = 'COMPOSE_REPLY_CANCEL'; export const COMPOSE_MENTION = 'COMPOSE_MENTION'; +export const COMPOSE_RESET = 'COMPOSE_RESET'; export const COMPOSE_UPLOAD_REQUEST = 'COMPOSE_UPLOAD_REQUEST'; export const COMPOSE_UPLOAD_SUCCESS = 'COMPOSE_UPLOAD_SUCCESS'; export const COMPOSE_UPLOAD_FAIL = 'COMPOSE_UPLOAD_FAIL'; @@ -68,6 +69,12 @@ export function cancelReplyCompose() { }; }; +export function resetCompose() { + return { + type: COMPOSE_RESET, + }; +}; + export function mentionCompose(account, router) { return (dispatch, getState) => { dispatch({ diff --git a/app/javascript/mastodon/components/autosuggest_textarea.js b/app/javascript/mastodon/components/autosuggest_textarea.js index 6f725885d..14a8d4c38 100644 --- a/app/javascript/mastodon/components/autosuggest_textarea.js +++ b/app/javascript/mastodon/components/autosuggest_textarea.js @@ -125,6 +125,16 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { this.props.onKeyDown(e); } + onKeyUp = e => { + if (e.key === 'Escape' && this.state.suggestionsHidden) { + document.querySelector('.ui').parentElement.focus(); + } + + if (this.props.onKeyUp) { + this.props.onKeyUp(e); + } + } + onBlur = () => { this.setState({ suggestionsHidden: true }); } @@ -173,7 +183,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { } render () { - const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus } = this.props; + const { value, suggestions, disabled, placeholder, autoFocus } = this.props; const { suggestionsHidden } = this.state; const style = { direction: 'ltr' }; @@ -195,7 +205,7 @@ export default class AutosuggestTextarea extends ImmutablePureComponent { value={value} onChange={this.onChange} onKeyDown={this.onKeyDown} - onKeyUp={onKeyUp} + onKeyUp={this.onKeyUp} onBlur={this.onBlur} onPaste={this.onPaste} style={style} diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js index c6b588765..ab9d48510 100644 --- a/app/javascript/mastodon/components/scrollable_list.js +++ b/app/javascript/mastodon/components/scrollable_list.js @@ -145,32 +145,6 @@ export default class ScrollableList extends PureComponent { return this._lastMouseMove !== null && ((new Date()) - this._lastMouseMove < 600); } - handleKeyDown = (e) => { - if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) { - const article = (() => { - switch (e.key) { - case 'PageDown': - return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling; - case 'PageUp': - return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling; - case 'End': - return this.node.querySelector('[role="feed"] > article:last-of-type'); - case 'Home': - return this.node.querySelector('[role="feed"] > article:first-of-type'); - default: - return null; - } - })(); - - - if (article) { - e.preventDefault(); - article.focus(); - article.scrollIntoView(); - } - } - } - render () { const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props; const { fullscreen } = this.state; @@ -182,7 +156,7 @@ export default class ScrollableList extends PureComponent { if (isLoading || childrenCount > 0 || !emptyMessage) { scrollableArea = (
    -
    +
    {prepend} {React.Children.map(this.props.children, (child, index) => ( diff --git a/app/javascript/mastodon/components/status.js b/app/javascript/mastodon/components/status.js index 17482e57a..70005436b 100644 --- a/app/javascript/mastodon/components/status.js +++ b/app/javascript/mastodon/components/status.js @@ -10,6 +10,8 @@ import StatusActionBar from './status_action_bar'; import { FormattedMessage } from 'react-intl'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { MediaGallery, Video } from '../features/ui/util/async-components'; +import { HotKeys } from 'react-hotkeys'; +import classNames from 'classnames'; // We use the component (and not the container) since we do not want // to use the progress bar to show download progress @@ -39,6 +41,8 @@ export default class Status extends ImmutablePureComponent { autoPlayGif: PropTypes.bool, muted: PropTypes.bool, hidden: PropTypes.bool, + onMoveUp: PropTypes.func, + onMoveDown: PropTypes.func, }; state = { @@ -89,16 +93,62 @@ export default class Status extends ImmutablePureComponent { } handleOpenVideo = startTime => { - this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), startTime); + this.props.onOpenVideo(this._properStatus().getIn(['media_attachments', 0]), startTime); + } + + handleHotkeyReply = e => { + e.preventDefault(); + this.props.onReply(this._properStatus(), this.context.router.history); + } + + handleHotkeyFavourite = () => { + this.props.onFavourite(this._properStatus()); + } + + handleHotkeyBoost = e => { + this.props.onReblog(this._properStatus(), e); + } + + handleHotkeyMention = e => { + e.preventDefault(); + this.props.onMention(this._properStatus().get('account'), this.context.router.history); + } + + handleHotkeyOpen = () => { + this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`); + } + + handleHotkeyOpenProfile = () => { + this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`); + } + + handleHotkeyMoveUp = () => { + this.props.onMoveUp(this.props.status.get('id')); + } + + handleHotkeyMoveDown = () => { + this.props.onMoveDown(this.props.status.get('id')); + } + + _properStatus () { + const { status } = this.props; + + if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { + return status.get('reblog'); + } else { + return status; + } } render () { let media = null; - let statusAvatar; + let statusAvatar, prepend; - const { status, account, hidden, ...other } = this.props; + const { hidden } = this.props; const { isExpanded } = this.state; + let { status, account, ...other } = this.props; + if (status === null) { return null; } @@ -115,16 +165,15 @@ export default class Status extends ImmutablePureComponent { if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') { const display_name_html = { __html: status.getIn(['account', 'display_name_html']) }; - return ( -
    -
    -
    - }} /> -
    - - + prepend = ( +
    +
    + }} />
    ); + + account = status.get('account'); + status = status.get('reblog'); } if (status.get('media_attachments').size > 0 && !this.props.muted) { @@ -160,26 +209,43 @@ export default class Status extends ImmutablePureComponent { statusAvatar = ; } + const handlers = this.props.muted ? {} : { + reply: this.handleHotkeyReply, + favourite: this.handleHotkeyFavourite, + boost: this.handleHotkeyBoost, + mention: this.handleHotkeyMention, + open: this.handleHotkeyOpen, + openProfile: this.handleHotkeyOpenProfile, + moveUp: this.handleHotkeyMoveUp, + moveDown: this.handleHotkeyMoveDown, + }; + return ( -
    -
    - + +
    + {prepend} - -
    - {statusAvatar} -
    +
    + + - {media} + {media} - -
    + +
    +
    + ); } diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js index cbae28afe..58a7b228a 100644 --- a/app/javascript/mastodon/components/status_list.js +++ b/app/javascript/mastodon/components/status_list.js @@ -25,18 +25,45 @@ export default class StatusList extends ImmutablePureComponent { trackScroll: true, }; + handleMoveUp = id => { + const elementIndex = this.props.statusIds.indexOf(id) - 1; + this._selectChild(elementIndex); + } + + handleMoveDown = id => { + const elementIndex = this.props.statusIds.indexOf(id) + 1; + this._selectChild(elementIndex); + } + + _selectChild (index) { + const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`); + + if (element) { + element.focus(); + } + } + + setRef = c => { + this.node = c; + } + render () { const { statusIds, ...other } = this.props; const { isLoading } = other; const scrollableContent = (isLoading || statusIds.size > 0) ? ( statusIds.map((statusId) => ( - + )) ) : null; return ( - + {scrollableContent} ); diff --git a/app/javascript/mastodon/features/compose/components/search.js b/app/javascript/mastodon/features/compose/components/search.js index 79abffad8..4c3f0dcb5 100644 --- a/app/javascript/mastodon/features/compose/components/search.js +++ b/app/javascript/mastodon/features/compose/components/search.js @@ -74,6 +74,8 @@ export default class Search extends React.PureComponent { if (e.key === 'Enter') { e.preventDefault(); this.props.onSubmit(); + } else if (e.key === 'Escape') { + document.querySelector('.ui').parentElement.focus(); } } diff --git a/app/javascript/mastodon/features/notifications/components/notification.js b/app/javascript/mastodon/features/notifications/components/notification.js index a608a5223..9d170cad5 100644 --- a/app/javascript/mastodon/features/notifications/components/notification.js +++ b/app/javascript/mastodon/features/notifications/components/notification.js @@ -6,61 +6,126 @@ import AccountContainer from '../../../containers/account_container'; import { FormattedMessage } from 'react-intl'; import Permalink from '../../../components/permalink'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import { HotKeys } from 'react-hotkeys'; export default class Notification extends ImmutablePureComponent { + static contextTypes = { + router: PropTypes.object, + }; + static propTypes = { notification: ImmutablePropTypes.map.isRequired, hidden: PropTypes.bool, + onMoveUp: PropTypes.func.isRequired, + onMoveDown: PropTypes.func.isRequired, + onMention: PropTypes.func.isRequired, }; + handleMoveUp = () => { + const { notification, onMoveUp } = this.props; + onMoveUp(notification.get('id')); + } + + handleMoveDown = () => { + const { notification, onMoveDown } = this.props; + onMoveDown(notification.get('id')); + } + + handleOpen = () => { + const { notification } = this.props; + + if (notification.get('status')) { + this.context.router.history.push(`/statuses/${notification.get('status')}`); + } else { + this.handleOpenProfile(); + } + } + + handleOpenProfile = () => { + const { notification } = this.props; + this.context.router.history.push(`/accounts/${notification.getIn(['account', 'id'])}`); + } + + handleMention = e => { + e.preventDefault(); + + const { notification, onMention } = this.props; + onMention(notification.get('account'), this.context.router.history); + } + + getHandlers () { + return { + moveUp: this.handleMoveUp, + moveDown: this.handleMoveDown, + open: this.handleOpen, + openProfile: this.handleOpenProfile, + mention: this.handleMention, + reply: this.handleMention, + }; + } + renderFollow (account, link) { return ( -
    -
    -
    - + +
    +
    +
    + +
    + +
    - +
    - -
    + ); } renderMention (notification) { - return