diff --git a/Gemfile.lock b/Gemfile.lock index 4d36cf4e9f..5464dbef70 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -194,7 +194,7 @@ GEM database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) date (3.3.4) - debug (1.9.1) + debug (1.9.2) irb (~> 1.10) reline (>= 0.3.8) debug_inspector (1.2.0) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index dba10a1646..69712836fe 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -167,8 +167,11 @@ module ApplicationHelper if theme == 'system' concat stylesheet_pack_tag("skins/#{flavour}/mastodon-light", media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') concat stylesheet_pack_tag("skins/#{flavour}/default", media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') + concat tag.meta name: 'theme-color', content: Themes::MASTODON_DARK_THEME_COLOR, media: '(prefers-color-scheme: dark)' + concat tag.meta name: 'theme-color', content: Themes::MASTODON_LIGHT_THEME_COLOR, media: '(prefers-color-scheme: light)' else - stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous' + concat stylesheet_pack_tag "skins/#{flavour}/#{theme}", media: 'all', crossorigin: 'anonymous' + concat tag.meta name: 'theme-color', content: theme == 'mastodon-light' ? Themes::MASTODON_LIGHT_THEME_COLOR : Themes::MASTODON_DARK_THEME_COLOR end end diff --git a/app/javascript/mastodon/actions/boosts.js b/app/javascript/mastodon/actions/boosts.js deleted file mode 100644 index 1fc2e391e2..0000000000 --- a/app/javascript/mastodon/actions/boosts.js +++ /dev/null @@ -1,32 +0,0 @@ -import { openModal } from './modal'; - -export const BOOSTS_INIT_MODAL = 'BOOSTS_INIT_MODAL'; -export const BOOSTS_CHANGE_PRIVACY = 'BOOSTS_CHANGE_PRIVACY'; - -export function initBoostModal(props) { - return (dispatch, getState) => { - const default_privacy = getState().getIn(['compose', 'default_privacy']); - - const privacy = props.status.get('visibility') === 'private' ? 'private' : default_privacy; - - dispatch({ - type: BOOSTS_INIT_MODAL, - privacy, - }); - - dispatch(openModal({ - modalType: 'BOOST', - modalProps: props, - })); - }; -} - - -export function changeBoostPrivacy(privacy) { - return dispatch => { - dispatch({ - type: BOOSTS_CHANGE_PRIVACY, - privacy, - }); - }; -} diff --git a/app/javascript/mastodon/components/visibility_icon.tsx b/app/javascript/mastodon/components/visibility_icon.tsx index 753dc02737..3a310cbae2 100644 --- a/app/javascript/mastodon/components/visibility_icon.tsx +++ b/app/javascript/mastodon/components/visibility_icon.tsx @@ -4,11 +4,10 @@ import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?re import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; +import type { StatusVisibility } from 'mastodon/models/status'; import { Icon } from './icon'; -type Visibility = 'public' | 'unlisted' | 'private' | 'direct'; - const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, unlisted_short: { @@ -25,7 +24,7 @@ const messages = defineMessages({ }, }); -export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({ +export const VisibilityIcon: React.FC<{ visibility: StatusVisibility }> = ({ visibility, }) => { const intl = useIntl(); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index da93a16b08..c6842e8df8 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -8,7 +8,6 @@ import { } from '../actions/accounts'; import { showAlertForError } from '../actions/alerts'; import { initBlockModal } from '../actions/blocks'; -import { initBoostModal } from '../actions/boosts'; import { replyCompose, mentionCompose, @@ -107,7 +106,7 @@ const mapDispatchToProps = (dispatch, { intl, contextType }) => ({ if ((e && e.shiftKey) || !boostModal) { this.onModalReblog(status); } else { - dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); + dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } })); } }, diff --git a/app/javascript/mastodon/features/notifications/containers/notification_container.js b/app/javascript/mastodon/features/notifications/containers/notification_container.js index 4458fd7bc3..de450cd1ab 100644 --- a/app/javascript/mastodon/features/notifications/containers/notification_container.js +++ b/app/javascript/mastodon/features/notifications/containers/notification_container.js @@ -1,6 +1,5 @@ import { connect } from 'react-redux'; -import { initBoostModal } from '../../../actions/boosts'; import { mentionCompose } from '../../../actions/compose'; import { reblog, @@ -8,6 +7,7 @@ import { unreblog, unfavourite, } from '../../../actions/interactions'; +import { openModal } from '../../../actions/modal'; import { hideStatus, revealStatus, @@ -49,7 +49,7 @@ const mapDispatchToProps = dispatch => ({ if (e.shiftKey || !boostModal) { this.onModalReblog(status); } else { - dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); + dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } })); } } }, diff --git a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx index a7d8356be7..7a163a8825 100644 --- a/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx +++ b/app/javascript/mastodon/features/picture_in_picture/components/footer.jsx @@ -14,7 +14,6 @@ import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; import ReplyIcon from '@/material-icons/400-24px/reply.svg?react'; import ReplyAllIcon from '@/material-icons/400-24px/reply_all.svg?react'; import StarIcon from '@/material-icons/400-24px/star.svg?react'; -import { initBoostModal } from 'mastodon/actions/boosts'; import { replyCompose } from 'mastodon/actions/compose'; import { reblog, favourite, unreblog, unfavourite } from 'mastodon/actions/interactions'; import { openModal } from 'mastodon/actions/modal'; @@ -140,7 +139,7 @@ class Footer extends ImmutablePureComponent { } else if ((e && e.shiftKey) || !boostModal) { this._performReblog(status); } else { - dispatch(initBoostModal({ status, onReblog: this._performReblog })); + dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this._performReblog } })); } } else { dispatch(openModal({ diff --git a/app/javascript/mastodon/features/status/containers/detailed_status_container.js b/app/javascript/mastodon/features/status/containers/detailed_status_container.js index 3e1f8d4d29..1c650f544f 100644 --- a/app/javascript/mastodon/features/status/containers/detailed_status_container.js +++ b/app/javascript/mastodon/features/status/containers/detailed_status_container.js @@ -4,7 +4,6 @@ import { connect } from 'react-redux'; import { showAlertForError } from '../../../actions/alerts'; import { initBlockModal } from '../../../actions/blocks'; -import { initBoostModal } from '../../../actions/boosts'; import { replyCompose, mentionCompose, @@ -85,7 +84,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({ if (e.shiftKey || !boostModal) { this.onModalReblog(status); } else { - dispatch(initBoostModal({ status, onReblog: this.onModalReblog })); + dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.onModalReblog } })); } } }, diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 44db9d9c3f..3914759725 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -27,7 +27,6 @@ import { unmuteAccount, } from '../../actions/accounts'; import { initBlockModal } from '../../actions/blocks'; -import { initBoostModal } from '../../actions/boosts'; import { replyCompose, mentionCompose, @@ -317,7 +316,7 @@ class Status extends ImmutablePureComponent { if ((e && e.shiftKey) || !boostModal) { this.handleModalReblog(status); } else { - dispatch(initBoostModal({ status, onReblog: this.handleModalReblog })); + dispatch(openModal({ modalType: 'BOOST', modalProps: { status, onReblog: this.handleModalReblog } })); } } } else { diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.jsx b/app/javascript/mastodon/features/ui/components/boost_modal.jsx deleted file mode 100644 index 3b3e1e3f97..0000000000 --- a/app/javascript/mastodon/features/ui/components/boost_modal.jsx +++ /dev/null @@ -1,125 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; -import { withRouter } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; -import { changeBoostPrivacy } from 'mastodon/actions/boosts'; -import AttachmentList from 'mastodon/components/attachment_list'; -import { Icon } from 'mastodon/components/icon'; -import { VisibilityIcon } from 'mastodon/components/visibility_icon'; -import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown'; -import { WithRouterPropTypes } from 'mastodon/utils/react_router'; - -import { Avatar } from '../../../components/avatar'; -import { Button } from '../../../components/button'; -import { DisplayName } from '../../../components/display_name'; -import { RelativeTimestamp } from '../../../components/relative_timestamp'; -import StatusContent from '../../../components/status_content'; - -const messages = defineMessages({ - cancel_reblog: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, - reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, -}); - -const mapStateToProps = state => { - return { - privacy: state.getIn(['boosts', 'new', 'privacy']), - }; -}; - -const mapDispatchToProps = dispatch => { - return { - onChangeBoostPrivacy(value) { - dispatch(changeBoostPrivacy(value)); - }, - }; -}; - -class BoostModal extends ImmutablePureComponent { - static propTypes = { - status: ImmutablePropTypes.map.isRequired, - onReblog: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - onChangeBoostPrivacy: PropTypes.func.isRequired, - privacy: PropTypes.string.isRequired, - intl: PropTypes.object.isRequired, - ...WithRouterPropTypes, - }; - - handleReblog = () => { - this.props.onReblog(this.props.status, this.props.privacy); - this.props.onClose(); - }; - - handleAccountClick = (e) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.onClose(); - this.props.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`); - } - }; - - _findContainer = () => { - return document.getElementsByClassName('modal-root__container')[0]; - }; - - render () { - const { status, privacy, intl } = this.props; - const buttonText = status.get('reblogged') ? messages.cancel_reblog : messages.reblog; - - return ( -
-
-
-
- - - - - - -
- -
- - -
-
- - - - {status.get('media_attachments').size > 0 && ( - - )} -
-
- -
-
Shift + }} />
- {status.get('visibility') !== 'private' && !status.get('reblogged') && ( - - )} -
-
- ); - } - -} - -export default withRouter(connect(mapStateToProps, mapDispatchToProps)(injectIntl(BoostModal))); diff --git a/app/javascript/mastodon/features/ui/components/boost_modal.tsx b/app/javascript/mastodon/features/ui/components/boost_modal.tsx new file mode 100644 index 0000000000..40b0c81833 --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/boost_modal.tsx @@ -0,0 +1,162 @@ +import type { MouseEventHandler } from 'react'; +import { useCallback, useState } from 'react'; + +import { defineMessages, FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; +import { useHistory } from 'react-router'; + +import type Immutable from 'immutable'; + +import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react'; +import AttachmentList from 'mastodon/components/attachment_list'; +import { Icon } from 'mastodon/components/icon'; +import { VisibilityIcon } from 'mastodon/components/visibility_icon'; +import PrivacyDropdown from 'mastodon/features/compose/components/privacy_dropdown'; +import type { Account } from 'mastodon/models/account'; +import type { Status, StatusVisibility } from 'mastodon/models/status'; +import { useAppSelector } from 'mastodon/store'; + +import { Avatar } from '../../../components/avatar'; +import { Button } from '../../../components/button'; +import { DisplayName } from '../../../components/display_name'; +import { RelativeTimestamp } from '../../../components/relative_timestamp'; +import StatusContent from '../../../components/status_content'; + +const messages = defineMessages({ + cancel_reblog: { + id: 'status.cancel_reblog_private', + defaultMessage: 'Unboost', + }, + reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, +}); + +export const BoostModal: React.FC<{ + status: Status; + onClose: () => void; + onReblog: (status: Status, privacy: StatusVisibility) => void; +}> = ({ status, onReblog, onClose }) => { + const intl = useIntl(); + const history = useHistory(); + + const default_privacy = useAppSelector( + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + (state) => state.compose.get('default_privacy') as StatusVisibility, + ); + + const account = status.get('account') as Account; + const statusVisibility = status.get('visibility') as StatusVisibility; + + const [privacy, setPrivacy] = useState( + statusVisibility === 'private' ? 'private' : default_privacy, + ); + + const onPrivacyChange = useCallback((value: StatusVisibility) => { + setPrivacy(value); + }, []); + + const handleReblog = useCallback(() => { + onReblog(status, privacy); + onClose(); + }, [onClose, onReblog, status, privacy]); + + const handleAccountClick = useCallback( + (e) => { + if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { + e.preventDefault(); + onClose(); + history.push(`/@${account.acct}`); + } + }, + [history, onClose, account], + ); + + const buttonText = status.get('reblogged') + ? messages.cancel_reblog + : messages.reblog; + + const findContainer = useCallback( + () => document.getElementsByClassName('modal-root__container')[0], + [], + ); + + return ( +
+
+
+ + + {/* @ts-expect-error Expected until StatusContent is typed */} + + + {(status.get('media_attachments') as Immutable.List).size > + 0 && ( + + )} +
+
+ +
+
+ + Shift + + + ), + }} + /> +
+ {statusVisibility !== 'private' && !status.get('reblogged') && ( + + )} +
+
+ ); +}; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 97d7706da4..404b53c742 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -24,7 +24,7 @@ import BundleContainer from '../containers/bundle_container'; import ActionsModal from './actions_modal'; import AudioModal from './audio_modal'; -import BoostModal from './boost_modal'; +import { BoostModal } from './boost_modal'; import BundleModalError from './bundle_modal_error'; import ConfirmationModal from './confirmation_modal'; import FocalPointModal from './focal_point_modal'; diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts new file mode 100644 index 0000000000..83e9f6b885 --- /dev/null +++ b/app/javascript/mastodon/models/status.ts @@ -0,0 +1,4 @@ +export type StatusVisibility = 'public' | 'unlisted' | 'private' | 'direct'; + +// Temporary until we type it correctly +export type Status = Immutable.Map; diff --git a/app/javascript/mastodon/reducers/boosts.js b/app/javascript/mastodon/reducers/boosts.js deleted file mode 100644 index d0d825057c..0000000000 --- a/app/javascript/mastodon/reducers/boosts.js +++ /dev/null @@ -1,25 +0,0 @@ -import Immutable from 'immutable'; - -import { - BOOSTS_INIT_MODAL, - BOOSTS_CHANGE_PRIVACY, -} from 'mastodon/actions/boosts'; - -const initialState = Immutable.Map({ - new: Immutable.Map({ - privacy: 'public', - }), -}); - -export default function mutes(state = initialState, action) { - switch (action.type) { - case BOOSTS_INIT_MODAL: - return state.withMutations((state) => { - state.setIn(['new', 'privacy'], action.privacy); - }); - case BOOSTS_CHANGE_PRIVACY: - return state.setIn(['new', 'privacy'], action.privacy); - default: - return state; - } -} diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts index db5e68a70c..6296ef2026 100644 --- a/app/javascript/mastodon/reducers/index.ts +++ b/app/javascript/mastodon/reducers/index.ts @@ -7,7 +7,6 @@ import { accountsReducer } from './accounts'; import accounts_map from './accounts_map'; import alerts from './alerts'; import announcements from './announcements'; -import boosts from './boosts'; import compose from './compose'; import contexts from './contexts'; import conversations from './conversations'; @@ -60,7 +59,6 @@ const reducers = { relationships: relationshipsReducer, settings, push_notifications, - boosts, server, contexts, compose, diff --git a/app/lib/themes.rb b/app/lib/themes.rb index c24f0641a9..265ef00255 100644 --- a/app/lib/themes.rb +++ b/app/lib/themes.rb @@ -6,6 +6,9 @@ require 'yaml' class Themes include Singleton + MASTODON_DARK_THEME_COLOR = '#191b22' + MASTODON_LIGHT_THEME_COLOR = '#f3f5f7' + def initialize @flavours = {} diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index a1d6e75000..4fb6f2f405 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -21,7 +21,6 @@ %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ - %meta{ name: 'theme-color', content: '#191b22' }/ %meta{ name: 'apple-mobile-web-app-capable', content: 'yes' }/ %title= html_title diff --git a/yarn.lock b/yarn.lock index 8904bdbc61..a8e2b24472 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8887,17 +8887,17 @@ __metadata: linkType: hard "glob@npm:^10.2.2, glob@npm:^10.2.6, glob@npm:^10.3.10, glob@npm:^10.3.7": - version: 10.3.10 - resolution: "glob@npm:10.3.10" + version: 10.3.12 + resolution: "glob@npm:10.3.12" dependencies: foreground-child: "npm:^3.1.0" - jackspeak: "npm:^2.3.5" + jackspeak: "npm:^2.3.6" minimatch: "npm:^9.0.1" - minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry: "npm:^1.10.1" + minipass: "npm:^7.0.4" + path-scurry: "npm:^1.10.2" bin: glob: dist/esm/bin.mjs - checksum: 10c0/13d8a1feb7eac7945f8c8480e11cd4a44b24d26503d99a8d8ac8d5aefbf3e9802a2b6087318a829fad04cb4e829f25c5f4f1110c68966c498720dd261c7e344d + checksum: 10c0/f60cefdc1cf3f958b2bb5823e1b233727f04916d489dc4641d76914f016e6704421e06a83cbb68b0cb1cb9382298b7a88075b844ad2127fc9727ea22b18b0711 languageName: node linkType: hard @@ -10358,7 +10358,7 @@ __metadata: languageName: node linkType: hard -"jackspeak@npm:^2.3.5": +"jackspeak@npm:^2.3.6": version: 2.3.6 resolution: "jackspeak@npm:2.3.6" dependencies: @@ -11471,10 +11471,10 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": - version: 10.0.1 - resolution: "lru-cache@npm:10.0.1" - checksum: 10c0/982dabfb227b9a2daf56d712ae0e72e01115a28c0a2068cd71277bca04568f3417bbf741c6c7941abc5c620fd8059e34f15607f90ebccbfa0a17533322d27a8e +"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0": + version: 10.2.0 + resolution: "lru-cache@npm:10.2.0" + checksum: 10c0/c9847612aa2daaef102d30542a8d6d9b2c2bb36581c1bf0dc3ebf5e5f3352c772a749e604afae2e46873b930a9e9523743faac4e5b937c576ab29196774712ee languageName: node linkType: hard @@ -11940,7 +11940,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3, minipass@npm:^7.0.4": version: 7.0.4 resolution: "minipass@npm:7.0.4" checksum: 10c0/6c7370a6dfd257bf18222da581ba89a5eaedca10e158781232a8b5542a90547540b4b9b7e7f490e4cda43acfbd12e086f0453728ecf8c19e0ef6921bc5958ac5 @@ -12773,13 +12773,13 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.10.1": - version: 1.10.1 - resolution: "path-scurry@npm:1.10.1" +"path-scurry@npm:^1.10.2": + version: 1.10.2 + resolution: "path-scurry@npm:1.10.2" dependencies: - lru-cache: "npm:^9.1.1 || ^10.0.0" + lru-cache: "npm:^10.2.0" minipass: "npm:^5.0.0 || ^6.0.2 || ^7.0.0" - checksum: 10c0/e5dc78a7348d25eec61ab166317e9e9c7b46818aa2c2b9006c507a6ff48c672d011292d9662527213e558f5652ce0afcc788663a061d8b59ab495681840c0c1e + checksum: 10c0/d723777fbf9627f201e64656680f66ebd940957eebacf780e6cce1c2919c29c116678b2d7dbf8821b3a2caa758d125f4444005ccec886a25c8f324504e48e601 languageName: node linkType: hard