diff --git a/app/javascript/flavours/glitch/components/account.jsx b/app/javascript/flavours/glitch/components/account.jsx index fd7caea6df..7e5209653e 100644 --- a/app/javascript/flavours/glitch/components/account.jsx +++ b/app/javascript/flavours/glitch/components/account.jsx @@ -37,10 +37,10 @@ class Account extends ImmutablePureComponent { static propTypes = { size: PropTypes.number, account: ImmutablePropTypes.record, - onFollow: PropTypes.func.isRequired, - onBlock: PropTypes.func.isRequired, - onMute: PropTypes.func.isRequired, - onMuteNotifications: PropTypes.func.isRequired, + onFollow: PropTypes.func, + onBlock: PropTypes.func, + onMute: PropTypes.func, + onMuteNotifications: PropTypes.func, intl: PropTypes.object.isRequired, hidden: PropTypes.bool, minimal: PropTypes.bool, diff --git a/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx index eb25f5a643..892d068b31 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_emoji.jsx @@ -35,7 +35,7 @@ export default class AutosuggestEmoji extends PureComponent { alt={emoji.native || emoji.colons} /> - {emoji.colons} + <div className='autosuggest-emoji__name'>{emoji.colons}</div> </div> ); } diff --git a/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx index 6da6200142..808f303754 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx +++ b/app/javascript/flavours/glitch/components/autosuggest_hashtag.tsx @@ -1,5 +1,3 @@ -import { FormattedMessage } from 'react-intl'; - import { ShortNumber } from 'flavours/glitch/components/short_number'; interface Props { @@ -16,27 +14,18 @@ interface Props { }; } -export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => { - const weeklyUses = tag.history && ( - <ShortNumber - value={tag.history.reduce((total, day) => total + day.uses * 1, 0)} - /> - ); - - return ( - <div className='autosuggest-hashtag'> - <div className='autosuggest-hashtag__name'> - #<strong>{tag.name}</strong> - </div> - {tag.history !== undefined && ( - <div className='autosuggest-hashtag__uses'> - <FormattedMessage - id='autosuggest_hashtag.per_week' - defaultMessage='{count} per week' - values={{ count: weeklyUses }} - /> - </div> - )} +export const AutosuggestHashtag: React.FC<Props> = ({ tag }) => ( + <div className='autosuggest-hashtag'> + <div className='autosuggest-hashtag__name'> + #<strong>{tag.name}</strong> </div> - ); -}; + + {tag.history !== undefined && ( + <div className='autosuggest-hashtag__uses'> + <ShortNumber + value={tag.history.reduce((total, day) => total + day.uses * 1, 0)} + /> + </div> + )} + </div> +); diff --git a/app/javascript/flavours/glitch/components/autosuggest_input.jsx b/app/javascript/flavours/glitch/components/autosuggest_input.jsx index 6d2474b442..97da532ebb 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_input.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_input.jsx @@ -5,6 +5,8 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; +import Overlay from 'react-overlays/Overlay'; + import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; import AutosuggestEmoji from './autosuggest_emoji'; @@ -195,34 +197,37 @@ export default class AutosuggestInput extends ImmutablePureComponent { return ( <div className='autosuggest-input'> - <label> - <span style={{ display: 'none' }}>{placeholder}</span> + <input + type='text' + ref={this.setInput} + disabled={disabled} + placeholder={placeholder} + autoFocus={autoFocus} + value={value} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyUp={onKeyUp} + onFocus={this.onFocus} + onBlur={this.onBlur} + dir='auto' + aria-autocomplete='list' + aria-label={placeholder} + id={id} + className={className} + maxLength={maxLength} + lang={lang} + spellCheck={spellCheck} + /> - <input - type='text' - ref={this.setInput} - disabled={disabled} - placeholder={placeholder} - autoFocus={autoFocus} - value={value} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={onKeyUp} - onFocus={this.onFocus} - onBlur={this.onBlur} - dir='auto' - aria-autocomplete='list' - id={id} - className={className} - maxLength={maxLength} - lang={lang} - spellCheck={spellCheck} - /> - </label> - - <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> - {suggestions.map(this.renderSuggestion)} - </div> + <Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={this.input} popperConfig={{ strategy: 'fixed' }}> + {({ props }) => ( + <div {...props}> + <div className='autosuggest-textarea__suggestions' style={{ width: this.input?.clientWidth }}> + {suggestions.map(this.renderSuggestion)} + </div> + </div> + )} + </Overlay> </div> ); } diff --git a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx index 28384075c3..9fd199a21c 100644 --- a/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx +++ b/app/javascript/flavours/glitch/components/autosuggest_textarea.jsx @@ -5,6 +5,7 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; +import Overlay from 'react-overlays/Overlay'; import Textarea from 'react-textarea-autosize'; import AutosuggestAccountContainer from '../features/compose/containers/autosuggest_account_container'; @@ -52,7 +53,6 @@ const AutosuggestTextarea = forwardRef(({ onFocus, autoFocus = true, lang, - children, }, textareaRef) => { const [suggestionsHidden, setSuggestionsHidden] = useState(true); @@ -183,40 +183,38 @@ const AutosuggestTextarea = forwardRef(({ ); }; - return [ - <div className='compose-form__autosuggest-wrapper' key='autosuggest-wrapper'> - <div className='autosuggest-textarea'> - <label> - <span style={{ display: 'none' }}>{placeholder}</span> + return ( + <div className='autosuggest-textarea'> + <Textarea + ref={textareaRef} + className='autosuggest-textarea__textarea' + disabled={disabled} + placeholder={placeholder} + autoFocus={autoFocus} + value={value} + onChange={handleChange} + onKeyDown={handleKeyDown} + onKeyUp={onKeyUp} + onFocus={handleFocus} + onBlur={handleBlur} + onPaste={handlePaste} + dir='auto' + aria-autocomplete='list' + aria-label={placeholder} + lang={lang} + /> - <Textarea - ref={textareaRef} - className='autosuggest-textarea__textarea' - disabled={disabled} - placeholder={placeholder} - autoFocus={autoFocus} - value={value} - onChange={handleChange} - onKeyDown={handleKeyDown} - onKeyUp={onKeyUp} - onFocus={handleFocus} - onBlur={handleBlur} - onPaste={handlePaste} - dir='auto' - aria-autocomplete='list' - lang={lang} - /> - </label> - </div> - {children} - </div>, - - <div className='autosuggest-textarea__suggestions-wrapper' key='suggestions-wrapper'> - <div className={`autosuggest-textarea__suggestions ${suggestionsHidden || suggestions.isEmpty() ? '' : 'autosuggest-textarea__suggestions--visible'}`}> - {suggestions.map(renderSuggestion)} - </div> - </div>, - ]; + <Overlay show={!(suggestionsHidden || suggestions.isEmpty())} offset={[0, 0]} placement='bottom' target={textareaRef} popperConfig={{ strategy: 'fixed' }}> + {({ props }) => ( + <div {...props}> + <div className='autosuggest-textarea__suggestions' style={{ width: textareaRef.current?.clientWidth }}> + {suggestions.map(renderSuggestion)} + </div> + </div> + )} + </Overlay> + </div> + ); }); AutosuggestTextarea.propTypes = { @@ -232,7 +230,6 @@ AutosuggestTextarea.propTypes = { onKeyDown: PropTypes.func, onPaste: PropTypes.func.isRequired, onFocus:PropTypes.func, - children: PropTypes.node, autoFocus: PropTypes.bool, lang: PropTypes.string, }; diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/components/dropdown_menu.jsx index bfaa53f6e5..4d9c34a762 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.jsx +++ b/app/javascript/flavours/glitch/components/dropdown_menu.jsx @@ -165,7 +165,7 @@ class Dropdown extends PureComponent { children: PropTypes.node, icon: PropTypes.string, iconComponent: PropTypes.func, - items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]).isRequired, + items: PropTypes.oneOfType([PropTypes.array, ImmutablePropTypes.list]), loading: PropTypes.bool, size: PropTypes.number, title: PropTypes.string, diff --git a/app/javascript/flavours/glitch/components/visibility_icon.tsx b/app/javascript/flavours/glitch/components/visibility_icon.tsx index baf134c0ae..dd24c5c927 100644 --- a/app/javascript/flavours/glitch/components/visibility_icon.tsx +++ b/app/javascript/flavours/glitch/components/visibility_icon.tsx @@ -1,9 +1,9 @@ import { defineMessages, useIntl } from 'react-intl'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; -import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react'; import MailIcon from '@/material-icons/400-24px/mail.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; +import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import { Icon } from './icon'; @@ -11,14 +11,17 @@ type Visibility = 'public' | 'unlisted' | 'private' | 'direct'; const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_short: { + id: 'privacy.unlisted.short', + defaultMessage: 'Quiet public', + }, private_short: { id: 'privacy.private.short', - defaultMessage: 'Followers only', + defaultMessage: 'Followers', }, direct_short: { id: 'privacy.direct.short', - defaultMessage: 'Mentioned people only', + defaultMessage: 'Specific people', }, }); @@ -35,7 +38,7 @@ export const VisibilityIcon: React.FC<{ visibility: Visibility }> = ({ }, unlisted: { icon: 'unlock', - iconComponent: LockOpenIcon, + iconComponent: QuietTimeIcon, text: intl.formatMessage(messages.unlisted_short), }, private: { diff --git a/app/javascript/flavours/glitch/containers/compose_container.jsx b/app/javascript/flavours/glitch/containers/compose_container.jsx index f76550678e..772ac5fa6b 100644 --- a/app/javascript/flavours/glitch/containers/compose_container.jsx +++ b/app/javascript/flavours/glitch/containers/compose_container.jsx @@ -1,14 +1,12 @@ -import { PureComponent } from 'react'; - import { Provider } from 'react-redux'; -import { fetchCustomEmojis } from '../actions/custom_emojis'; -import { hydrateStore } from '../actions/store'; -import Compose from '../features/standalone/compose'; -import initialState from '../initial_state'; -import { IntlProvider } from '../locales'; -import { store } from '../store'; - +import { fetchCustomEmojis } from 'flavours/glitch/actions/custom_emojis'; +import { hydrateStore } from 'flavours/glitch/actions/store'; +import { Router } from 'flavours/glitch/components/router'; +import Compose from 'flavours/glitch/features/standalone/compose'; +import initialState from 'flavours/glitch/initial_state'; +import { IntlProvider } from 'flavours/glitch/locales'; +import { store } from 'flavours/glitch/store'; if (initialState) { store.dispatch(hydrateStore(initialState)); @@ -16,16 +14,14 @@ if (initialState) { store.dispatch(fetchCustomEmojis()); -export default class ComposeContainer extends PureComponent { +const ComposeContainer = () => ( + <IntlProvider> + <Provider store={store}> + <Router> + <Compose /> + </Router> + </Provider> + </IntlProvider> +); - render () { - return ( - <IntlProvider> - <Provider store={store}> - <Compose /> - </Provider> - </IntlProvider> - ); - } - -} +export default ComposeContainer; diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index 411ad77c79..7fda25ded5 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -1,13 +1,13 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useCallback } from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; -import ImmutablePropTypes from 'react-immutable-proptypes'; +import { useDispatch } from 'react-redux'; -import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; - -import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; +import { openModal } from 'flavours/glitch/actions/modal'; +import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; +import { logOut } from 'flavours/glitch/utils/log_out'; const messages = defineMessages({ edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, @@ -23,51 +23,52 @@ const messages = defineMessages({ filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, + logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, + logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, }); -class ActionBar extends PureComponent { +export const ActionBar = () => { + const dispatch = useDispatch(); + const intl = useIntl(); - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - onLogout: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; + const handleLogoutClick = useCallback(() => { + dispatch(openModal({ + modalType: 'CONFIRM', + modalProps: { + message: intl.formatMessage(messages.logoutMessage), + confirm: intl.formatMessage(messages.logoutConfirm), + closeWhenConfirm: false, + onConfirm: () => logOut(), + }, + })); + }, [dispatch, intl]); - handleLogout = () => { - this.props.onLogout(); - }; + let menu = []; - render () { - const { intl } = this.props; + menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); + menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); + menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); + menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); + menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); + menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); + menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); + menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); + menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.logout), action: handleLogoutClick }); - let menu = []; - - menu.push({ text: intl.formatMessage(messages.edit_profile), href: '/settings/profile' }); - menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); - menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); - menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); - menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); - menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); - menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); - menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); - menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); - menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout }); - - return ( - <div className='compose__action-bar'> - <div className='compose__action-bar-dropdown'> - <DropdownMenuContainer items={menu} icon='bars' iconComponent={MenuIcon} size={24} direction='right' /> - </div> - </div> - ); - } - -} - -export default injectIntl(ActionBar); + return ( + <DropdownMenuContainer + items={menu} + icon='bars' + iconComponent={MoreHorizIcon} + size={24} + direction='right' + /> + ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index 0a73bc1020..fe684c1483 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -15,7 +15,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent { return ( <div className='autosuggest-account' title={account.get('acct')}> - <div className='autosuggest-account-icon'><Avatar account={account} size={18} /></div> + <Avatar account={account} size={24} /> <DisplayName account={account} /> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx index 42452b30f6..f400d1fe2f 100644 --- a/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx @@ -1,26 +1,18 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; import { length } from 'stringz'; -export default class CharacterCounter extends PureComponent { +export const CharacterCounter = ({ text, max }) => { + const diff = max - length(text); - static propTypes = { - text: PropTypes.string.isRequired, - max: PropTypes.number.isRequired, - }; - - checkRemainingText (diff) { - if (diff < 0) { - return <span className='character-counter character-counter--over'>{diff}</span>; - } - - return <span className='character-counter'>{diff}</span>; + if (diff < 0) { + return <span className='character-counter character-counter--over'>{diff}</span>; } - render () { - const diff = this.props.max - length(this.props.text); - return this.checkRemainingText(diff); - } + return <span className='character-counter'>{diff}</span>; +}; -} +CharacterCounter.propTypes = { + text: PropTypes.string.isRequired, + max: PropTypes.number.isRequired, +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 1f24828c66..737bbfc126 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -10,8 +10,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; -import LockIcon from '@/material-icons/400-24px/lock.svg?react'; -import { Icon } from 'flavours/glitch/components/icon'; import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router'; import AutosuggestInput from '../../../components/autosuggest_input'; @@ -21,25 +19,27 @@ import { maxChars } from '../../../initial_state'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; import LanguageDropdown from '../containers/language_dropdown_container'; import PollButtonContainer from '../containers/poll_button_container'; -import PollFormContainer from '../containers/poll_form_container'; import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; import SpoilerButtonContainer from '../containers/spoiler_button_container'; import UploadButtonContainer from '../containers/upload_button_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; import { countableText } from '../util/counter'; -import CharacterCounter from './character_counter'; +import { CharacterCounter } from './character_counter'; +import { EditIndicator } from './edit_indicator'; +import { NavigationBar } from './navigation_bar'; +import { PollForm } from "./poll_form"; +import { ReplyIndicator } from './reply_indicator'; const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; const messages = defineMessages({ placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, - spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, - publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }, - publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, - saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, + spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Content warning (optional)' }, + publish: { id: 'compose_form.publish', defaultMessage: 'Post' }, + saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Update' }, + reply: { id: 'compose_form.reply', defaultMessage: 'Reply' }, }); class ComposeForm extends ImmutablePureComponent { @@ -66,6 +66,7 @@ class ComposeForm extends ImmutablePureComponent { onPaste: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired, autoFocus: PropTypes.bool, + withoutNavigation: PropTypes.bool, anyMedia: PropTypes.bool, isInReply: PropTypes.bool, singleColumn: PropTypes.bool, @@ -224,93 +225,90 @@ class ComposeForm extends ImmutablePureComponent { }; render () { - const { intl, onPaste, autoFocus } = this.props; + const { intl, onPaste, autoFocus, withoutNavigation } = this.props; const { highlighted } = this.state; const disabled = this.props.isSubmitting; - let publishText = ''; - - if (this.props.isEditing) { - publishText = intl.formatMessage(messages.saveChanges); - } else if (this.props.privacy === 'private' || this.props.privacy === 'direct') { - publishText = <><Icon id='lock' icon={LockIcon} /> {intl.formatMessage(messages.publish)}</>; - } else { - publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); - } - return ( <form className='compose-form' onSubmit={this.handleSubmit}> + <ReplyIndicator /> + {!withoutNavigation && <NavigationBar />} <WarningContainer /> - <ReplyIndicatorContainer /> + <div className={classNames('compose-form__highlightable', { active: highlighted })} ref={this.setRef}> + <div className='compose-form__scrollable'> + <EditIndicator /> - <div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}> - <AutosuggestInput - placeholder={intl.formatMessage(messages.spoiler_placeholder)} - value={this.props.spoilerText} - onChange={this.handleChangeSpoilerText} - onKeyDown={this.handleKeyDown} - disabled={!this.props.spoiler} - ref={this.setSpoilerText} - suggestions={this.props.suggestions} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSpoilerSuggestionSelected} - searchTokens={[':']} - id='cw-spoiler-input' - className='spoiler-input__input' - lang={this.props.lang} - spellCheck - /> - </div> + {this.props.spoiler && ( + <div className='spoiler-input'> + <div className='spoiler-input__border' /> - <div className={classNames('compose-form__highlightable', { active: highlighted })}> - <AutosuggestTextarea - ref={this.textareaRef} - placeholder={intl.formatMessage(messages.placeholder)} - disabled={disabled} - value={this.props.text} - onChange={this.handleChange} - suggestions={this.props.suggestions} - onFocus={this.handleFocus} - onKeyDown={this.handleKeyDown} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSuggestionSelected} - onPaste={onPaste} - autoFocus={autoFocus} - lang={this.props.lang} - > - <div className='compose-form__modifiers'> - <UploadFormContainer /> - <PollFormContainer /> - </div> - </AutosuggestTextarea> - <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> + <AutosuggestInput + placeholder={intl.formatMessage(messages.spoiler_placeholder)} + value={this.props.spoilerText} + disabled={disabled} + onChange={this.handleChangeSpoilerText} + onKeyDown={this.handleKeyDown} + ref={this.setSpoilerText} + suggestions={this.props.suggestions} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSpoilerSuggestionSelected} + searchTokens={[':']} + id='cw-spoiler-input' + className='spoiler-input__input' + lang={this.props.lang} + spellCheck + /> - <div className='compose-form__buttons-wrapper'> - <div className='compose-form__buttons'> - <UploadButtonContainer /> - <PollButtonContainer /> + <div className='spoiler-input__border' /> + </div> + )} + + <AutosuggestTextarea + ref={this.textareaRef} + placeholder={intl.formatMessage(messages.placeholder)} + disabled={disabled} + value={this.props.text} + onChange={this.handleChange} + suggestions={this.props.suggestions} + onFocus={this.handleFocus} + onKeyDown={this.handleKeyDown} + onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} + onSuggestionsClearRequested={this.onSuggestionsClearRequested} + onSuggestionSelected={this.onSuggestionSelected} + onPaste={onPaste} + autoFocus={autoFocus} + lang={this.props.lang} + /> + </div> + + <UploadFormContainer /> + <PollForm /> + + <div className='compose-form__footer'> + <div className='compose-form__dropdowns'> <PrivacyDropdownContainer disabled={this.props.isEditing} /> - <SpoilerButtonContainer /> <LanguageDropdown /> </div> - <div className='character-counter__wrapper'> - <CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} /> - </div> - </div> - </div> + <div className='compose-form__actions'> + <div className='compose-form__buttons'> + <UploadButtonContainer /> + <PollButtonContainer /> + <SpoilerButtonContainer /> + <EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} /> + <CharacterCounter max={maxChars} text={this.getFulltextForCharacterCounting()} /> + </div> - <div className='compose-form__publish'> - <div className='compose-form__publish-button-wrapper'> - <Button - type='submit' - text={publishText} - disabled={!this.canSubmit()} - block - /> + <div className='compose-form__submit'> + <Button + type='submit' + text={intl.formatMessage(this.props.isEditing ? messages.saveChanges : (this.props.isInReply ? messages.reply : messages.publish))} + disabled={!this.canSubmit()} + /> + </div> + </div> </div> </div> </form> diff --git a/app/javascript/flavours/glitch/features/compose/components/edit_indicator.jsx b/app/javascript/flavours/glitch/features/compose/components/edit_indicator.jsx new file mode 100644 index 0000000000..4e3d909833 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/edit_indicator.jsx @@ -0,0 +1,62 @@ +import { useCallback } from 'react'; + +import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; + +import { Link } from 'react-router-dom'; + +import { useDispatch, useSelector } from 'react-redux'; + +import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'; +import { cancelReplyCompose } from 'flavours/glitch/actions/compose'; +import { Icon } from 'flavours/glitch/components/icon'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import { RelativeTimestamp } from 'flavours/glitch/components/relative_timestamp'; + +const messages = defineMessages({ + cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, +}); + +export const EditIndicator = () => { + const intl = useIntl(); + const dispatch = useDispatch(); + const id = useSelector(state => state.getIn(['compose', 'id'])); + const status = useSelector(state => state.getIn(['statuses', id])); + const account = useSelector(state => state.getIn(['accounts', status?.get('account')])); + + const handleCancelClick = useCallback(() => { + dispatch(cancelReplyCompose()); + }, [dispatch]); + + if (!status) { + return null; + } + + const content = { __html: status.get('contentHtml') }; + + return ( + <div className='edit-indicator'> + <div className='edit-indicator__header'> + <div className='edit-indicator__display-name'> + <Link to={`/@${account.get('acct')}`}>@{account.get('acct')}</Link> + ยท + <Link to={`/@${account.get('acct')}/${status.get('id')}`}><RelativeTimestamp timestamp={status.get('created_at')} /></Link> + </div> + + <div className='edit-indicator__cancel'> + <IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={handleCancelClick} inverted /> + </div> + </div> + + <div className='edit-indicator__content translate' dangerouslySetInnerHTML={content} /> + + {(status.get('poll') || status.get('media_attachments').size > 0) && ( + <div className='edit-indicator__attachments'> + {status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>} + {status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>} + </div> + )} + </div> + ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx index 4195316794..0846468c4e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -10,6 +10,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import { supportsPassiveEvents } from 'detect-passive-events'; import Overlay from 'react-overlays/Overlay'; +import MoodIcon from '@/material-icons/400-24px/mood.svg?react'; +import { IconButton } from 'flavours/glitch/components/icon_button'; import { useSystemEmojiFont } from 'flavours/glitch/initial_state'; import { assetHost } from 'flavours/glitch/utils/config'; @@ -323,7 +325,6 @@ class EmojiPickerDropdown extends PureComponent { onPickEmoji: PropTypes.func.isRequired, onSkinTone: PropTypes.func.isRequired, skinTone: PropTypes.number.isRequired, - button: PropTypes.node, }; state = { @@ -381,23 +382,24 @@ class EmojiPickerDropdown extends PureComponent { }; render () { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; + const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis } = this.props; const title = intl.formatMessage(messages.emoji); const { active, loading } = this.state; return ( - <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> - <div ref={this.setTargetRef} className='emoji-button' title={title} aria-label={title} aria-expanded={active} role='button' onClick={this.onToggle} onKeyDown={this.onToggle} tabIndex={0}> - {button || <img - className={classNames('emojione', { 'pulse-loading': active && loading })} - alt='๐' - src={`${assetHost}/emoji/1f642.svg`} - />} - </div> + <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown} ref={this.setTargetRef}> + <IconButton + title={title} + aria-expanded={active} + active={active} + iconComponent={MoodIcon} + onClick={this.onToggle} + inverted + /> <Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> {({ props, placement })=> ( - <div {...props} style={{ ...props.style, width: 299 }}> + <div {...props} style={{ ...props.style }}> <div className={`dropdown-animation ${placement}`}> <EmojiPickerMenu custom_emojis={this.props.custom_emojis} diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx index a067f686e1..db1ce9cece 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx @@ -9,10 +9,11 @@ import { supportsPassiveEvents } from 'detect-passive-events'; import fuzzysort from 'fuzzysort'; import Overlay from 'react-overlays/Overlay'; +import CancelIcon from '@/material-icons/400-24px/cancel-fill.svg?react'; +import SearchIcon from '@/material-icons/400-24px/search.svg?react'; +import TranslateIcon from '@/material-icons/400-24px/translate.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; import { languages as preloadedLanguages } from 'flavours/glitch/initial_state'; -import { loupeIcon, deleteIcon } from 'flavours/glitch/utils/icons'; - -import TextIconButton from './text_icon_button'; const messages = defineMessages({ changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, @@ -231,7 +232,7 @@ class LanguageDropdownMenu extends PureComponent { <div ref={this.setRef}> <div className='emoji-mart-search'> <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> - <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button> + <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}><Icon icon={!isSearching ? SearchIcon : CancelIcon} /></button> </div> <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> @@ -297,20 +298,24 @@ class LanguageDropdown extends PureComponent { render () { const { value, intl, frequentlyUsedLanguages } = this.props; const { open, placement } = this.state; + const current = preloadedLanguages.find(lang => lang[0] === value) ?? []; return ( - <div className={classNames('privacy-dropdown', placement, { active: open })}> - <div className='privacy-dropdown__value' ref={this.setTargetRef} > - <TextIconButton - className='privacy-dropdown__value-icon' - label={value && value.toUpperCase()} - title={intl.formatMessage(messages.changeLanguage)} - active={open} - onClick={this.handleToggle} - /> - </div> + <div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}> + <button + type='button' + title={intl.formatMessage(messages.changeLanguage)} + aria-expanded={open} + onClick={this.handleToggle} + onMouseDown={this.handleMouseDown} + onKeyDown={this.handleButtonKeyDown} + className={classNames('dropdown-button', { active: open })} + > + <Icon icon={TranslateIcon} /> + <span className='dropdown-button__label'>{current[2] ?? value}</span> + </button> - <Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> + <Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> {({ props, placement }) => ( <div {...props}> <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} > diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx index 2f0bb79f89..ef9d01bd20 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx @@ -1,50 +1,36 @@ -import PropTypes from 'prop-types'; +import { useCallback } from 'react'; -import { FormattedMessage } from 'react-intl'; +import { useIntl, defineMessages } from 'react-intl'; -import { Link } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { cancelReplyCompose } from 'flavours/glitch/actions/compose'; +import Account from 'flavours/glitch/components/account'; +import { IconButton } from 'flavours/glitch/components/icon_button'; +import { me } from 'flavours/glitch/initial_state'; -import { Avatar } from '../../../components/avatar'; +import { ActionBar } from './action_bar'; -import ActionBar from './action_bar'; -export default class NavigationBar extends ImmutablePureComponent { +const messages = defineMessages({ + cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, +}); - static propTypes = { - account: ImmutablePropTypes.record.isRequired, - onLogout: PropTypes.func.isRequired, - onClose: PropTypes.func, - }; +export const NavigationBar = () => { + const dispatch = useDispatch(); + const intl = useIntl(); + const account = useSelector(state => state.getIn(['accounts', me])); + const isReplying = useSelector(state => !!state.getIn(['compose', 'in_reply_to'])); - render () { - const username = this.props.account.get('acct'); - return ( - <div className='navigation-bar'> - <Link to={`/@${username}`}> - <span style={{ display: 'none' }}>{username}</span> - <Avatar account={this.props.account} size={46} /> - </Link> + const handleCancelClick = useCallback(() => { + dispatch(cancelReplyCompose()); + }, [dispatch]); - <div className='navigation-bar__profile'> - <span> - <Link to={`/@${username}`}> - <strong className='navigation-bar__profile-account'>@{username}</strong> - </Link> - </span> - - <span> - <a href='/settings/profile' className='navigation-bar__profile-edit'><FormattedMessage id='navigation_bar.edit_profile' defaultMessage='Edit profile' /></a> - </span> - </div> - - <div className='navigation-bar__actions'> - <ActionBar account={this.props.account} onLogout={this.props.onLogout} /> - </div> - </div> - ); - } - -} + return ( + <div className='navigation-bar'> + <Account account={account} minimal /> + {isReplying ? <IconButton title={intl.formatMessage(messages.cancel)} iconComponent={CloseIcon} onClick={handleCancelClick} /> : <ActionBar />} + </div> + ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx b/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx index 4900d38119..345497abd3 100644 --- a/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx @@ -3,11 +3,10 @@ import { PureComponent } from 'react'; import { defineMessages, injectIntl } from 'react-intl'; -import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; +import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react'; import { IconButton } from '../../../components/icon_button'; - const messages = defineMessages({ add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' }, remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' }, @@ -22,7 +21,6 @@ class PollButton extends PureComponent { static propTypes = { disabled: PropTypes.bool, - unavailable: PropTypes.bool, active: PropTypes.bool, onClick: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, @@ -33,17 +31,13 @@ class PollButton extends PureComponent { }; render () { - const { intl, active, unavailable, disabled } = this.props; - - if (unavailable) { - return null; - } + const { intl, active, disabled } = this.props; return ( <div className='compose-form__poll-button'> <IconButton icon='tasks' - iconComponent={InsertChartIcon} + iconComponent={BarChart4BarsIcon} title={intl.formatMessage(active ? messages.remove_poll : messages.add_poll)} disabled={disabled} onClick={this.handleClick} diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx index 1ee0a06c62..8964121c57 100644 --- a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx @@ -1,189 +1,162 @@ import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; +import { useCallback } from 'react'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import classNames from 'classnames'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useDispatch, useSelector } from 'react-redux'; -import AddIcon from '@/material-icons/400-24px/add.svg?react'; -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; +import { + changePollSettings, + changePollOption, + clearComposeSuggestions, + fetchComposeSuggestions, + selectComposeSuggestion, +} from 'flavours/glitch/actions/compose'; import AutosuggestInput from 'flavours/glitch/components/autosuggest_input'; -import { Icon } from 'flavours/glitch/components/icon'; -import { IconButton } from 'flavours/glitch/components/icon_button'; const messages = defineMessages({ - option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, - add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, - remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, - poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, + option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Option {number}' }, + add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add option' }, + remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this option' }, + duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll length' }, + type: { id: 'compose_form.poll.type', defaultMessage: 'Style' }, switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' }, switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' }, 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}}' }, + singleChoice: { id: 'compose_form.poll.single', defaultMessage: 'Pick one' }, + multipleChoice: { id: 'compose_form.poll.multiple', defaultMessage: 'Multiple choice' }, }); -class OptionIntl extends PureComponent { +const Select = ({ label, options, value, onChange }) => { + return ( + <label className='compose-form__poll__select'> + <span className='compose-form__poll__select__label'>{label}</span> - static propTypes = { - title: PropTypes.string.isRequired, - lang: PropTypes.string, - index: PropTypes.number.isRequired, - isPollMultiple: PropTypes.bool, - autoFocus: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - onToggleMultiple: PropTypes.func.isRequired, - suggestions: ImmutablePropTypes.list, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onSuggestionSelected: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; + <select className='compose-form__poll__select__value' value={value} onChange={onChange}> + {options.map((option, i) => ( + <option key={i} value={option.value}>{option.label}</option> + ))} + </select> + </label> + ); +}; - handleOptionTitleChange = e => { - this.props.onChange(this.props.index, e.target.value); - }; +Select.propTypes = { + label: PropTypes.node, + value: PropTypes.any, + onChange: PropTypes.func, + options: PropTypes.arrayOf(PropTypes.shape({ + label: PropTypes.node, + value: PropTypes.any, + })), +}; - handleOptionRemove = () => { - this.props.onRemove(this.props.index); - }; +const Option = ({ multipleChoice, index, title, autoFocus }) => { + const intl = useIntl(); + const dispatch = useDispatch(); + const suggestions = useSelector(state => state.getIn(['compose', 'suggestions'])); + const lang = useSelector(state => state.getIn(['compose', 'language'])); + const handleChange = useCallback(({ target: { value } }) => { + dispatch(changePollOption(index, value)); + }, [dispatch, index]); - handleToggleMultiple = e => { - this.props.onToggleMultiple(); - e.preventDefault(); - e.stopPropagation(); - }; + const handleSuggestionsFetchRequested = useCallback(token => { + dispatch(fetchComposeSuggestions(token)); + }, [dispatch]); - handleCheckboxKeypress = e => { - if (e.key === 'Enter' || e.key === ' ') { - this.handleToggleMultiple(e); - } - }; + const handleSuggestionsClearRequested = useCallback(() => { + dispatch(clearComposeSuggestions()); + }, [dispatch]); - onSuggestionsClearRequested = () => { - this.props.onClearSuggestions(); - }; + const handleSuggestionSelected = useCallback((tokenStart, token, value) => { + dispatch(selectComposeSuggestion(tokenStart, token, value, ['poll', 'options', index])); + }, [dispatch, index]); - onSuggestionsFetchRequested = (token) => { - this.props.onFetchSuggestions(token); - }; + return ( + <label className={classNames('poll__option editable', { empty: index > 1 && title.length === 0 })}> + <span className={classNames('poll__input', { checkbox: multipleChoice })} /> - onSuggestionSelected = (tokenStart, token, value) => { - this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); - }; + <AutosuggestInput + placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} + maxLength={100} + value={title} + lang={lang} + spellCheck + onChange={handleChange} + suggestions={suggestions} + onSuggestionsFetchRequested={handleSuggestionsFetchRequested} + onSuggestionsClearRequested={handleSuggestionsClearRequested} + onSuggestionSelected={handleSuggestionSelected} + searchTokens={[':']} + autoFocus={autoFocus} + /> + </label> + ); +}; - render () { - const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props; +Option.propTypes = { + title: PropTypes.string.isRequired, + index: PropTypes.number.isRequired, + multipleChoice: PropTypes.bool, + autoFocus: PropTypes.bool, +}; - return ( - <li> - <label className='poll__option editable'> - <span - className={classNames('poll__input', { checkbox: isPollMultiple })} - onClick={this.handleToggleMultiple} - onKeyPress={this.handleCheckboxKeypress} - role='button' - tabIndex={0} - title={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)} - aria-label={intl.formatMessage(isPollMultiple ? messages.switchToSingle : messages.switchToMultiple)} - /> +export const PollForm = () => { + const intl = useIntl(); + const dispatch = useDispatch(); + const poll = useSelector(state => state.getIn(['compose', 'poll'])); + const options = poll?.get('options'); + const expiresIn = poll?.get('expires_in'); + const isMultiple = poll?.get('multiple'); - <AutosuggestInput - placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} - maxLength={100} - value={title} - lang={lang} - spellCheck - onChange={this.handleOptionTitleChange} - suggestions={this.props.suggestions} - onSuggestionsFetchRequested={this.onSuggestionsFetchRequested} - onSuggestionsClearRequested={this.onSuggestionsClearRequested} - onSuggestionSelected={this.onSuggestionSelected} - searchTokens={[':']} - autoFocus={autoFocus} - /> - </label> + const handleDurationChange = useCallback(({ target: { value } }) => { + dispatch(changePollSettings(value, isMultiple)); + }, [dispatch, isMultiple]); - <div className='poll__cancel'> - <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' iconComponent={CloseIcon} onClick={this.handleOptionRemove} /> - </div> - </li> - ); + const handleTypeChange = useCallback(({ target: { value } }) => { + dispatch(changePollSettings(expiresIn, value === 'true')); + }, [dispatch, expiresIn]); + + if (poll === null) { + return null; } -} + return ( + <div className='compose-form__poll'> + {options.map((title, i) => ( + <Option + title={title} + key={i} + index={i} + multipleChoice={isMultiple} + autoFocus={i === 0} + /> + ))} -const Option = injectIntl(OptionIntl); + <div className='compose-form__poll__footer'> + <Select label={intl.formatMessage(messages.duration)} options={[ + { value: 300, label: intl.formatMessage(messages.minutes, { number: 5 })}, + { value: 1800, label: intl.formatMessage(messages.minutes, { number: 30 })}, + { value: 3600, label: intl.formatMessage(messages.hours, { number: 1 })}, + { value: 21600, label: intl.formatMessage(messages.hours, { number: 6 })}, + { value: 43200, label: intl.formatMessage(messages.hours, { number: 12 })}, + { value: 86400, label: intl.formatMessage(messages.days, { number: 1 })}, + { value: 259200, label: intl.formatMessage(messages.days, { number: 3 })}, + { value: 604800, label: intl.formatMessage(messages.days, { number: 7 })}, + ]} value={expiresIn} onChange={handleDurationChange} /> -class PollForm extends ImmutablePureComponent { + <div className='compose-form__poll__footer__sep' /> - static propTypes = { - options: ImmutablePropTypes.list, - lang: PropTypes.string, - expiresIn: PropTypes.number, - isMultiple: PropTypes.bool, - onChangeOption: PropTypes.func.isRequired, - onAddOption: PropTypes.func.isRequired, - onRemoveOption: PropTypes.func.isRequired, - onChangeSettings: PropTypes.func.isRequired, - suggestions: ImmutablePropTypes.list, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onSuggestionSelected: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleAddOption = () => { - this.props.onAddOption(''); - }; - - handleSelectDuration = e => { - this.props.onChangeSettings(e.target.value, this.props.isMultiple); - }; - - handleToggleMultiple = () => { - this.props.onChangeSettings(this.props.expiresIn, !this.props.isMultiple); - }; - - render () { - const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; - - if (!options) { - return null; - } - - const autoFocusIndex = options.indexOf(''); - - return ( - <div className='compose-form__poll-wrapper'> - <ul> - {options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)} - </ul> - - <div className='poll__footer'> - <button type='button' disabled={options.size >= 5} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' icon={AddIcon} /> <FormattedMessage {...messages.add_option} /></button> - - {/* eslint-disable-next-line jsx-a11y/no-onchange */} - <select value={expiresIn} onChange={this.handleSelectDuration}> - <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> - <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> - <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> - <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option> - <option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option> - <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option> - <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option> - <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option> - </select> - </div> + <Select label={intl.formatMessage(messages.type)} options={[ + { value: false, label: intl.formatMessage(messages.singleChoice) }, + { value: true, label: intl.formatMessage(messages.multipleChoice) }, + ]} value={isMultiple} onChange={handleTypeChange} /> </div> - ); - } - -} - -export default injectIntl(PollForm); + </div> + ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx index f7c893f28a..8a49f71511 100644 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx @@ -5,28 +5,27 @@ import { injectIntl, defineMessages } from 'react-intl'; import classNames from 'classnames'; - import { supportsPassiveEvents } from 'detect-passive-events'; import Overlay from 'react-overlays/Overlay'; import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; -import LockOpenIcon from '@/material-icons/400-24px/lock_open.svg?react'; import PublicIcon from '@/material-icons/400-24px/public.svg?react'; +import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; -import { IconButton } from '../../../components/icon_button'; - const messages = defineMessages({ public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Anyone on and off Mastodon' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Fewer algorithmic fanfares' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Only your followers' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Specific people' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Everyone mentioned in the post' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Change post privacy' }, + unlisted_extra: { id: 'privacy.unlisted.additional', defaultMessage: 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' }, }); const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; @@ -135,6 +134,12 @@ class PrivacyDropdownMenu extends PureComponent { <strong>{item.text}</strong> {item.meta} </div> + + {item.extra && ( + <div className='privacy-dropdown__option__additional' title={item.extra}> + <Icon id='info-circle' icon={InfoIcon} /> + </div> + )} </div> ))} </div> @@ -163,30 +168,11 @@ class PrivacyDropdown extends PureComponent { }; handleToggle = () => { - if (this.props.isUserTouching && this.props.isUserTouching()) { - if (this.state.open) { - this.props.onModalClose(); - } else { - this.props.onModalOpen({ - actions: this.options.map(option => ({ ...option, active: option.value === this.props.value })), - onClick: this.handleModalActionClick, - }); - } - } else { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - this.setState({ open: !this.state.open }); + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); } - }; - handleModalActionClick = (e) => { - e.preventDefault(); - - const { value } = this.options[e.currentTarget.getAttribute('data-index')]; - - this.props.onModalClose(); - this.props.onChange(value); + this.setState({ open: !this.state.open }); }; handleKeyDown = e => { @@ -228,7 +214,7 @@ class PrivacyDropdown extends PureComponent { this.options = [ { icon: 'globe', iconComponent: PublicIcon, value: 'public', text: formatMessage(messages.public_short), meta: formatMessage(messages.public_long) }, - { icon: 'unlock', iconComponent: LockOpenIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long) }, + { icon: 'unlock', iconComponent: QuietTimeIcon, value: 'unlisted', text: formatMessage(messages.unlisted_short), meta: formatMessage(messages.unlisted_long), extra: formatMessage(messages.unlisted_extra) }, { icon: 'lock', iconComponent: LockIcon, value: 'private', text: formatMessage(messages.private_short), meta: formatMessage(messages.private_long) }, ]; @@ -259,23 +245,21 @@ class PrivacyDropdown extends PureComponent { return ( <div ref={this.setTargetRef} onKeyDown={this.handleKeyDown}> - <IconButton - className='privacy-dropdown__value-icon' - icon={valueOption.icon} - iconComponent={valueOption.iconComponent} + <button + type='button' title={intl.formatMessage(messages.change_privacy)} - size={18} - expanded={open} - active={open} - inverted + aria-expanded={open} onClick={this.handleToggle} onMouseDown={this.handleMouseDown} onKeyDown={this.handleButtonKeyDown} - style={{ height: null, lineHeight: '27px' }} disabled={disabled} - /> + className={classNames('dropdown-button', { active: open })} + > + <Icon id={valueOption.icon} icon={valueOption.iconComponent} /> + <span className='dropdown-button__label'>{valueOption.text}</span> + </button> - <Overlay show={open} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> + <Overlay show={open} offset={[5, 5]} placement={placement} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> {({ props, placement }) => ( <div {...props}> <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}> diff --git a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx index e3721b6fa9..5c895195a0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx @@ -1,74 +1,48 @@ -import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; -import { defineMessages, injectIntl } from 'react-intl'; +import { Link } from 'react-router-dom'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; +import { useSelector } from 'react-redux'; -import CloseIcon from '@/material-icons/400-24px/close.svg?react'; -import AttachmentList from 'flavours/glitch/components/attachment_list'; -import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router'; +import BarChart4BarsIcon from '@/material-icons/400-24px/bar_chart_4_bars.svg?react'; +import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'; +import { Avatar } from 'flavours/glitch/components/avatar'; +import { DisplayName } from 'flavours/glitch/components/display_name'; +import { Icon } from 'flavours/glitch/components/icon'; -import { Avatar } from '../../../components/avatar'; -import { DisplayName } from '../../../components/display_name'; -import { IconButton } from '../../../components/icon_button'; +export const ReplyIndicator = () => { + const inReplyToId = useSelector(state => state.getIn(['compose', 'in_reply_to'])); + const status = useSelector(state => state.getIn(['statuses', inReplyToId])); + const account = useSelector(state => state.getIn(['accounts', status?.get('account')])); -const messages = defineMessages({ - cancel: { id: 'reply_indicator.cancel', defaultMessage: 'Cancel' }, -}); + if (!status) { + return null; + } -class ReplyIndicator extends ImmutablePureComponent { + const content = { __html: status.get('contentHtml') }; - static propTypes = { - status: ImmutablePropTypes.map, - onCancel: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - ...WithOptionalRouterPropTypes, - }; + return ( + <div className='reply-indicator'> + <div className='reply-indicator__line' /> - handleClick = () => { - this.props.onCancel(); - }; + <Link to={`/@${account.get('acct')}`} className='detailed-status__display-avatar'> + <Avatar account={account} size={46} /> + </Link> - handleAccountClick = (e) => { - if (e.button === 0 && !(e.ctrlKey || e.metaKey)) { - e.preventDefault(); - this.props.history?.push(`/@${this.props.status.getIn(['account', 'acct'])}`); - } - }; - - render () { - const { status, intl } = this.props; - - if (!status) { - return null; - } - - const content = { __html: status.get('contentHtml') }; - - return ( - <div className='reply-indicator'> - <div className='reply-indicator__header'> - <div className='reply-indicator__cancel'><IconButton title={intl.formatMessage(messages.cancel)} icon='times' iconComponent={CloseIcon} onClick={this.handleClick} inverted /></div> - - <a href={`/@${status.getIn(['account', 'acct'])}`} onClick={this.handleAccountClick} className='reply-indicator__display-name'> - <div className='reply-indicator__display-avatar'><Avatar account={status.get('account')} size={24} /></div> - <DisplayName account={status.get('account')} /> - </a> - </div> + <div className='reply-indicator__main'> + <Link to={`/@${account.get('acct')}`} className='detailed-status__display-name'> + <DisplayName account={account} /> + </Link> <div className='reply-indicator__content translate' dangerouslySetInnerHTML={content} /> - {status.get('media_attachments').size > 0 && ( - <AttachmentList - compact - media={status.get('media_attachments')} - /> + {(status.get('poll') || status.get('media_attachments').size > 0) && ( + <div className='reply-indicator__attachments'> + {status.get('poll') && <><Icon icon={BarChart4BarsIcon} /><FormattedMessage id='reply_indicator.poll' defaultMessage='Poll' /></>} + {status.get('media_attachments').size > 0 && <><Icon icon={PhotoLibraryIcon} /><FormattedMessage id='reply_indicator.attachments' defaultMessage='{count, plural, one {# attachment} other {# attachments}}' values={{ count: status.get('media_attachments').size }} /></>} + </div> )} </div> - ); - } - -} - -export default withOptionalRouter(injectIntl(ReplyIndicator)); + </div> + ); +}; diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.jsx b/app/javascript/flavours/glitch/features/compose/components/upload.jsx index fd2d80e2f2..2568aacb54 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/upload.jsx @@ -2,6 +2,8 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; @@ -9,7 +11,8 @@ import spring from 'react-motion/lib/spring'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react'; -import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; +import { Blurhash } from 'flavours/glitch/components/blurhash'; import { Icon } from 'flavours/glitch/components/icon'; import Motion from '../../ui/util/optional_motion'; @@ -18,6 +21,7 @@ export default class Upload extends ImmutablePureComponent { static propTypes = { media: ImmutablePropTypes.map.isRequired, + sensitive: PropTypes.bool, onUndo: PropTypes.func.isRequired, onOpenFocalPoint: PropTypes.func.isRequired, }; @@ -33,7 +37,7 @@ export default class Upload extends ImmutablePureComponent { }; render () { - const { media } = this.props; + const { media, sensitive } = this.props; if (!media) { return null; @@ -43,22 +47,26 @@ export default class Upload extends ImmutablePureComponent { const focusY = media.getIn(['meta', 'focus', 'y']); const x = ((focusX / 2) + .5) * 100; const y = ((focusY / -2) + .5) * 100; + const missingDescription = (media.get('description') || '').length === 0; return ( <div className='compose-form__upload'> <Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}> {({ scale }) => ( - <div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}> + <div className='compose-form__upload__thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: !sensitive ? `url(${media.get('preview_url')})` : null, backgroundPosition: `${x}% ${y}%` }}> + {sensitive && <Blurhash + hash={media.get('blurhash')} + className='compose-form__upload__preview' + />} + <div className='compose-form__upload__actions'> - <button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' icon={CloseIcon} /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button> - <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button> + <button type='button' className='icon-button compose-form__upload__delete' onClick={this.handleUndoClick}><Icon icon={CloseIcon} /></button> + <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon icon={EditIcon} /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button> </div> - {(media.get('description') || '').length === 0 && ( - <div className='compose-form__upload__warning'> - <button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' icon={InfoIcon} /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button> - </div> - )} + <div className='compose-form__upload__warning'> + <button type='button' className={classNames('icon-button', { active: missingDescription })} onClick={this.handleFocalPointClick}>{missingDescription && <Icon icon={WarningIcon} />} ALT</button> + </div> </div> )} </Motion> diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx index 923d6a3c47..4b8f7213b7 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/upload_button.jsx @@ -6,9 +6,8 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; import { connect } from 'react-redux'; -import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate.svg?react'; - -import { IconButton } from '../../../components/icon_button'; +import PhotoLibraryIcon from '@/material-icons/400-24px/photo_library.svg?react'; +import { IconButton } from 'flavours/glitch/components/icon_button'; const messages = defineMessages({ upload: { id: 'upload_button.label', defaultMessage: 'Add images, a video or an audio file' }, @@ -31,7 +30,6 @@ class UploadButton extends ImmutablePureComponent { static propTypes = { disabled: PropTypes.bool, - unavailable: PropTypes.bool, onSelectFile: PropTypes.func.isRequired, style: PropTypes.object, resetFileKey: PropTypes.number, @@ -54,17 +52,13 @@ class UploadButton extends ImmutablePureComponent { }; render () { - const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props; - - if (unavailable) { - return null; - } + const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; const message = intl.formatMessage(messages.upload); return ( <div className='compose-form__upload-button'> - <IconButton icon='paperclip' iconComponent={AddPhotoAlternateIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> + <IconButton icon='paperclip' iconComponent={PhotoLibraryIcon} title={message} disabled={disabled} onClick={this.handleClick} className='compose-form__upload-button-icon' size={18} inverted style={iconStyle} /> <label> <span style={{ display: 'none' }}>{message}</span> <input diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx index cf2e53ad90..46bac7823b 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx @@ -1,7 +1,6 @@ import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import SensitiveButtonContainer from '../containers/sensitive_button_container'; import UploadContainer from '../containers/upload_container'; import UploadProgressContainer from '../containers/upload_progress_container'; @@ -15,17 +14,17 @@ export default class UploadForm extends ImmutablePureComponent { const { mediaIds } = this.props; return ( - <div className='compose-form__upload-wrapper'> + <> <UploadProgressContainer /> - <div className='compose-form__uploads-wrapper'> - {mediaIds.map(id => ( - <UploadContainer id={id} key={id} /> - ))} - </div> - - {!mediaIds.isEmpty() && <SensitiveButtonContainer />} - </div> + {mediaIds.size > 0 && ( + <div className='compose-form__uploads'> + {mediaIds.map(id => ( + <UploadContainer id={id} key={id} /> + ))} + </div> + )} + </> ); } diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx index f4408b5460..7af5d9090e 100644 --- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx @@ -35,9 +35,7 @@ export default class UploadProgress extends PureComponent { return ( <div className='upload-progress'> - <div className='upload-progress__icon'> - <Icon id='upload' icon={UploadFileIcon} /> - </div> + <Icon id='upload' icon={UploadFileIcon} /> <div className='upload-progress__message'> {message} diff --git a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js b/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js deleted file mode 100644 index 459fffd910..0000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/navigation_container.js +++ /dev/null @@ -1,36 +0,0 @@ -import { defineMessages, injectIntl } from 'react-intl'; - -import { connect } from 'react-redux'; - -import { openModal } from 'flavours/glitch/actions/modal'; -import { logOut } from 'flavours/glitch/utils/log_out'; - -import { me } from '../../../initial_state'; -import NavigationBar from '../components/navigation_bar'; - -const messages = defineMessages({ - logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' }, - logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' }, -}); - -const mapStateToProps = state => { - return { - account: state.getIn(['accounts', me]), - }; -}; - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onLogout () { - dispatch(openModal({ - modalType: 'CONFIRM', - modalProps: { - message: intl.formatMessage(messages.logoutMessage), - confirm: intl.formatMessage(messages.logoutConfirm), - closeWhenConfirm: false, - onConfirm: () => logOut(), - }, - })); - }, -}); - -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar)); diff --git a/app/javascript/flavours/glitch/features/compose/containers/poll_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/poll_button_container.js index b7785ecbb4..9de388f64a 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/poll_button_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/poll_button_container.js @@ -4,7 +4,7 @@ import { addPoll, removePoll } from '../../../actions/compose'; import PollButton from '../components/poll_button'; const mapStateToProps = state => ({ - unavailable: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0), + disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 0), active: state.getIn(['compose', 'poll']) !== null, }); diff --git a/app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js b/app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js deleted file mode 100644 index 177ffcea6a..0000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/poll_form_container.js +++ /dev/null @@ -1,53 +0,0 @@ -import { connect } from 'react-redux'; - -import { - addPollOption, - removePollOption, - changePollOption, - changePollSettings, - clearComposeSuggestions, - fetchComposeSuggestions, - selectComposeSuggestion, -} from '../../../actions/compose'; -import PollForm from '../components/poll_form'; - -const mapStateToProps = state => ({ - suggestions: state.getIn(['compose', 'suggestions']), - options: state.getIn(['compose', 'poll', 'options']), - lang: state.getIn(['compose', 'language']), - expiresIn: state.getIn(['compose', 'poll', 'expires_in']), - isMultiple: state.getIn(['compose', 'poll', 'multiple']), -}); - -const mapDispatchToProps = dispatch => ({ - onAddOption(title) { - dispatch(addPollOption(title)); - }, - - onRemoveOption(index) { - dispatch(removePollOption(index)); - }, - - onChangeOption(index, title) { - dispatch(changePollOption(index, title)); - }, - - onChangeSettings(expiresIn, isMultiple) { - dispatch(changePollSettings(expiresIn, isMultiple)); - }, - - onClearSuggestions () { - dispatch(clearComposeSuggestions()); - }, - - onFetchSuggestions (token) { - dispatch(fetchComposeSuggestions(token)); - }, - - onSuggestionSelected (position, token, accountId, path) { - dispatch(selectComposeSuggestion(position, token, accountId, path)); - }, - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(PollForm); diff --git a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js b/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js deleted file mode 100644 index 1147e448af..0000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/reply_indicator_container.js +++ /dev/null @@ -1,36 +0,0 @@ -import { connect } from 'react-redux'; - -import { cancelReplyCompose } from '../../../actions/compose'; -import { makeGetStatus } from '../../../selectors'; -import ReplyIndicator from '../components/reply_indicator'; - -const makeMapStateToProps = () => { - const getStatus = makeGetStatus(); - - const mapStateToProps = state => { - let statusId = state.getIn(['compose', 'id'], null); - let editing = true; - - if (statusId === null) { - statusId = state.getIn(['compose', 'in_reply_to']); - editing = false; - } - - return { - status: getStatus(state, { id: statusId }), - editing, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = dispatch => ({ - - onCancel () { - dispatch(cancelReplyCompose()); - }, - -}); - -export default connect(makeMapStateToProps, mapDispatchToProps)(ReplyIndicator); diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx deleted file mode 100644 index ad56ff1b92..0000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -import classNames from 'classnames'; - -import { connect } from 'react-redux'; - -import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; - -const messages = defineMessages({ - marked: { - id: 'compose_form.sensitive.marked', - defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}', - }, - unmarked: { - id: 'compose_form.sensitive.unmarked', - defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}', - }, -}); - -const mapStateToProps = state => ({ - active: state.getIn(['compose', 'sensitive']), - disabled: state.getIn(['compose', 'spoiler']), - mediaCount: state.getIn(['compose', 'media_attachments']).size, -}); - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSensitivity()); - }, - -}); - -class SensitiveButton extends PureComponent { - - static propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - mediaCount: PropTypes.number, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { active, disabled, mediaCount, onClick, intl } = this.props; - - return ( - <div className='compose-form__sensitive-button'> - <label className={classNames('icon-button', { active })} title={intl.formatMessage(active ? messages.marked : messages.unmarked, { count: mediaCount })}> - <input - name='mark-sensitive' - type='checkbox' - checked={active} - onChange={onClick} - disabled={disabled} - /> - - <FormattedMessage - id='compose_form.sensitive.hide' - defaultMessage='{count, plural, one {Mark media as sensitive} other {Mark media as sensitive}}' - values={{ count: mediaCount }} - /> - </label> - </div> - ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js index b3d7a71335..fce1816fe8 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/spoiler_button_container.js @@ -2,8 +2,10 @@ import { injectIntl, defineMessages } from 'react-intl'; import { connect } from 'react-redux'; +import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + import { changeComposeSpoilerness } from '../../../actions/compose'; -import TextIconButton from '../components/text_icon_button'; const messages = defineMessages({ marked: { id: 'compose_form.spoiler.marked', defaultMessage: 'Text is hidden behind warning' }, @@ -11,10 +13,12 @@ const messages = defineMessages({ }); const mapStateToProps = (state, { intl }) => ({ - label: 'CW', + iconComponent: WarningIcon, title: intl.formatMessage(state.getIn(['compose', 'spoiler']) ? messages.marked : messages.unmarked), active: state.getIn(['compose', 'spoiler']), ariaControls: 'cw-spoiler-input', + size: 18, + inverted: true, }); const mapDispatchToProps = dispatch => ({ @@ -25,4 +29,4 @@ const mapDispatchToProps = dispatch => ({ }); -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TextIconButton)); +export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(IconButton)); diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js index 42195da617..7c4757b6c3 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_button_container.js @@ -4,8 +4,7 @@ import { uploadCompose } from '../../../actions/compose'; import UploadButton from '../components/upload_button'; const mapStateToProps = state => ({ - disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))), - unavailable: state.getIn(['compose', 'poll']) !== null, + disabled: state.getIn(['compose', 'poll']) !== null || state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size + state.getIn(['compose', 'pending_media_attachments']) > 3 || state.getIn(['compose', 'media_attachments']).some(m => ['video', 'audio'].includes(m.get('type')))), resetFileKey: state.getIn(['compose', 'resetFileKey']), }); diff --git a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js index 77bb90db87..a17a691444 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/upload_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/upload_container.js @@ -5,6 +5,7 @@ import Upload from '../components/upload'; const mapStateToProps = (state, { id }) => ({ media: state.getIn(['compose', 'media_attachments']).find(item => item.get('id') === id), + sensitive: state.getIn(['compose', 'spoiler']), }); const mapDispatchToProps = dispatch => ({ diff --git a/app/javascript/flavours/glitch/features/compose/index.jsx b/app/javascript/flavours/glitch/features/compose/index.jsx index 5bbb7fab96..8d0c194227 100644 --- a/app/javascript/flavours/glitch/features/compose/index.jsx +++ b/app/javascript/flavours/glitch/features/compose/index.jsx @@ -30,7 +30,6 @@ import { isMobile } from '../../is_mobile'; import Motion from '../ui/util/optional_motion'; import ComposeFormContainer from './containers/compose_form_container'; -import NavigationContainer from './containers/navigation_container'; import SearchContainer from './containers/search_container'; import SearchResultsContainer from './containers/search_results_container'; @@ -129,8 +128,6 @@ class Compose extends PureComponent { <div className='drawer__pager'> <div className='drawer__inner' onFocus={this.onFocus}> - <NavigationContainer onClose={this.onBlur} /> - <ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} /> <div className='drawer__inner__mastodon'> @@ -152,7 +149,6 @@ class Compose extends PureComponent { return ( <Column onFocus={this.onFocus}> - <NavigationContainer onClose={this.onBlur} /> <ComposeFormContainer /> <Helmet> diff --git a/app/javascript/flavours/glitch/features/getting_started/index.jsx b/app/javascript/flavours/glitch/features/getting_started/index.jsx index 866904eafb..a3464f146a 100644 --- a/app/javascript/flavours/glitch/features/getting_started/index.jsx +++ b/app/javascript/flavours/glitch/features/getting_started/index.jsx @@ -32,7 +32,7 @@ import { preferencesLink } from 'flavours/glitch/utils/backend_links'; import { me, showTrends } from '../../initial_state'; -import NavigationBar from '../compose/components/navigation_bar'; +import { NavigationBar } from '../compose/components/navigation_bar'; import ColumnLink from '../ui/components/column_link'; import ColumnSubheading from '../ui/components/column_subheading'; diff --git a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx index dab63c4ce6..849a553e06 100644 --- a/app/javascript/flavours/glitch/features/local_settings/page/index.jsx +++ b/app/javascript/flavours/glitch/features/local_settings/page/index.jsx @@ -28,9 +28,9 @@ const messages = defineMessages({ pop_in_left: { id: 'settings.pop_in_left', defaultMessage: 'Left' }, pop_in_right: { id: 'settings.pop_in_right', defaultMessage: 'Right' }, public: { id: 'privacy.public.short', defaultMessage: 'Public' }, - unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - private: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, - direct: { id: 'privacy.direct.short', defaultMessage: 'Mentioned people only' }, + unlisted: { id: 'privacy.unlisted.short', defaultMessage: 'Quiet public' }, + private: { id: 'privacy.private.short', defaultMessage: 'Followers' }, + direct: { id: 'privacy.direct.short', defaultMessage: 'Specific people' }, }); class LocalSettingsPage extends PureComponent { diff --git a/app/javascript/flavours/glitch/features/standalone/compose/index.jsx b/app/javascript/flavours/glitch/features/standalone/compose/index.jsx index c36e843f5a..a0482c3ce5 100644 --- a/app/javascript/flavours/glitch/features/standalone/compose/index.jsx +++ b/app/javascript/flavours/glitch/features/standalone/compose/index.jsx @@ -1,21 +1,15 @@ -import { PureComponent } from 'react'; +import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container'; +import LoadingBarContainer from 'flavours/glitch/features/ui/containers/loading_bar_container'; +import ModalContainer from 'flavours/glitch/features/ui/containers/modal_container'; +import NotificationsContainer from 'flavours/glitch/features/ui/containers/notifications_container'; -import ComposeFormContainer from '../../compose/containers/compose_form_container'; -import LoadingBarContainer from '../../ui/containers/loading_bar_container'; -import ModalContainer from '../../ui/containers/modal_container'; -import NotificationsContainer from '../../ui/containers/notifications_container'; +const Compose = () => ( + <> + <ComposeFormContainer autoFocus withoutNavigation /> + <NotificationsContainer /> + <ModalContainer /> + <LoadingBarContainer className='loading-bar' /> + </> +); -export default class Compose extends PureComponent { - - render () { - return ( - <div> - <ComposeFormContainer autoFocus /> - <NotificationsContainer /> - <ModalContainer /> - <LoadingBarContainer className='loading-bar' /> - </div> - ); - } - -} +export default Compose; diff --git a/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx b/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx index 3f9ba99e4f..a99d76c1a4 100644 --- a/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/compose_panel.jsx @@ -6,7 +6,6 @@ import { connect } from 'react-redux'; import { mountCompose, unmountCompose } from 'flavours/glitch/actions/compose'; import ServerBanner from 'flavours/glitch/components/server_banner'; import ComposeFormContainer from 'flavours/glitch/features/compose/containers/compose_form_container'; -import NavigationContainer from 'flavours/glitch/features/compose/containers/navigation_container'; import SearchContainer from 'flavours/glitch/features/compose/containers/search_container'; import LinkFooter from './link_footer'; @@ -46,10 +45,7 @@ class ComposePanel extends PureComponent { )} {signedIn && ( - <> - <NavigationContainer /> - <ComposeFormContainer singleColumn /> - </> + <ComposeFormContainer singleColumn /> )} <LinkFooter /> diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx index caed29c252..d584277dc3 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.jsx @@ -21,7 +21,7 @@ import { Button } from 'flavours/glitch/components/button'; import { GIFV } from 'flavours/glitch/components/gifv'; import { IconButton } from 'flavours/glitch/components/icon_button'; import Audio from 'flavours/glitch/features/audio'; -import CharacterCounter from 'flavours/glitch/features/compose/components/character_counter'; +import { CharacterCounter } from 'flavours/glitch/features/compose/components/character_counter'; import UploadProgress from 'flavours/glitch/features/compose/components/upload_progress'; import { Tesseract as fetchTesseract } from 'flavours/glitch/features/ui/util/async-components'; import { me } from 'flavours/glitch/initial_state'; diff --git a/app/javascript/flavours/glitch/features/ui/components/mute_modal.jsx b/app/javascript/flavours/glitch/features/ui/components/mute_modal.jsx index 2d95cabef8..fa81ea81a3 100644 --- a/app/javascript/flavours/glitch/features/ui/components/mute_modal.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/mute_modal.jsx @@ -108,7 +108,6 @@ class MuteModal extends PureComponent { <div> <span><FormattedMessage id='mute_modal.duration' defaultMessage='Duration' />: </span> - {/* eslint-disable-next-line jsx-a11y/no-onchange */} <select value={muteDuration} onChange={this.changeMuteDuration}> <option value={0}>{intl.formatMessage(messages.indefinite)}</option> <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index 94e7415b03..7321f5ac3d 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -5,13 +5,6 @@ "account.follows_you": "Follows you", "account.suspended_disclaimer_full": "This user has been suspended by a moderator.", "account.view_full_profile": "View full profile", - "advanced_options.icon_title": "Advanced options", - "advanced_options.local-only.long": "Do not post to other instances", - "advanced_options.local-only.short": "Local-only", - "advanced_options.local-only.tooltip": "This post is local-only", - "advanced_options.threaded_mode.long": "Automatically opens a reply on posting", - "advanced_options.threaded_mode.short": "Threaded mode", - "advanced_options.threaded_mode.tooltip": "Threaded mode enabled", "boost_modal.missing_description": "This toot contains some media without description", "column.favourited_by": "Favourited by", "column.heading": "Misc", @@ -21,26 +14,13 @@ "column_subheading.lists": "Lists", "column_subheading.navigation": "Navigation", "community.column_settings.allow_local_only": "Show local-only toots", - "compose.attach": "Attach...", - "compose.attach.doodle": "Draw something", - "compose.attach.upload": "Upload a file", - "compose.content-type.html": "HTML", - "compose.content-type.markdown": "Markdown", - "compose.content-type.plain": "Plain text", - "compose_form.poll.multiple_choices": "Allow multiple choices", - "compose_form.poll.single_choice": "Allow one choice", - "compose_form.spoiler": "Hide text behind warning", "confirmation_modal.do_not_ask_again": "Do not ask for confirmation again", "confirmations.deprecated_settings.confirm": "Use Mastodon preferences", "confirmations.deprecated_settings.message": "Some of the glitch-soc device-specific {app_settings} you are using have been replaced by Mastodon {preferences} and will be overriden:", - "confirmations.missing_media_description.confirm": "Send anyway", - "confirmations.missing_media_description.edit": "Edit media", - "confirmations.missing_media_description.message": "At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.", "confirmations.unfilter.author": "Author", "confirmations.unfilter.confirm": "Show", "confirmations.unfilter.edit_filter": "Edit filter", "confirmations.unfilter.filters": "Matching {count, plural, one {filter} other {filters}}", - "content-type.change": "Content type", "direct.group_by_conversations": "Group by conversation", "endorsed_accounts_editor.endorsed_accounts": "Featured accounts", "favourite_modal.combo": "You can press {combo} to skip this next time", diff --git a/app/javascript/flavours/glitch/packs/share.jsx b/app/javascript/flavours/glitch/packs/share.jsx index 21f1da638d..7a4e333653 100644 --- a/app/javascript/flavours/glitch/packs/share.jsx +++ b/app/javascript/flavours/glitch/packs/share.jsx @@ -10,10 +10,12 @@ function loaded() { if (mountNode) { const attr = mountNode.getAttribute('data-props'); - if(!attr) return; + + if (!attr) return; const props = JSON.parse(attr); const root = createRoot(mountNode); + root.render(<ComposeContainer {...props} />); } } diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 8a221e6406..e11a103026 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -44,9 +44,7 @@ import { COMPOSE_RESET, COMPOSE_POLL_ADD, COMPOSE_POLL_REMOVE, - COMPOSE_POLL_OPTION_ADD, COMPOSE_POLL_OPTION_CHANGE, - COMPOSE_POLL_OPTION_REMOVE, COMPOSE_POLL_SETTINGS_CHANGE, INIT_MEDIA_EDIT_MODAL, COMPOSE_CHANGE_MEDIA_DESCRIPTION, @@ -366,6 +364,18 @@ const updateSuggestionTags = (state, token) => { }); }; +const updatePoll = (state, index, value) => state.updateIn(['poll', 'options'], options => { + const tmp = options.set(index, value).filterNot(x => x.trim().length === 0); + + if (tmp.size === 0) { + return tmp.push('').push(''); + } else if (tmp.size < 4) { + return tmp.push(''); + } + + return tmp; +}); + export default function compose(state = initialState, action) { switch(action.type) { case STORE_HYDRATE: @@ -649,12 +659,8 @@ export default function compose(state = initialState, action) { return state.set('poll', initialPoll); case COMPOSE_POLL_REMOVE: return state.set('poll', null); - case COMPOSE_POLL_OPTION_ADD: - return state.updateIn(['poll', 'options'], options => options.push(action.title)); case COMPOSE_POLL_OPTION_CHANGE: - return state.setIn(['poll', 'options', action.index], action.title); - case COMPOSE_POLL_OPTION_REMOVE: - return state.updateIn(['poll', 'options'], options => options.delete(action.index)); + return updatePoll(state, action.index, action.title); case COMPOSE_POLL_SETTINGS_CHANGE: return state.update('poll', poll => poll.set('expires_in', action.expiresIn).set('multiple', action.isMultiple)); case COMPOSE_LANGUAGE_CHANGE: diff --git a/app/javascript/flavours/glitch/styles/_mixins.scss b/app/javascript/flavours/glitch/styles/_mixins.scss index ec2a7e7ced..a59bb2d441 100644 --- a/app/javascript/flavours/glitch/styles/_mixins.scss +++ b/app/javascript/flavours/glitch/styles/_mixins.scss @@ -24,13 +24,14 @@ outline: 0; box-sizing: border-box; width: 100%; - border: 0; box-shadow: none; font-family: inherit; background: $ui-base-color; color: $darker-text-color; border-radius: 4px; - font-size: 14px; + border: 1px solid lighten($ui-base-color, 8%); + font-size: 17px; + line-height: normal; margin: 0; } diff --git a/app/javascript/flavours/glitch/styles/admin.scss b/app/javascript/flavours/glitch/styles/admin.scss index 2f4027b03f..5963f3ee6e 100644 --- a/app/javascript/flavours/glitch/styles/admin.scss +++ b/app/javascript/flavours/glitch/styles/admin.scss @@ -1336,6 +1336,9 @@ a.sparkline { &__label { padding: 15px; + display: flex; + gap: 8px; + align-items: center; } &__rules { @@ -1346,6 +1349,9 @@ a.sparkline { &__rule { cursor: pointer; padding: 15px; + display: flex; + gap: 8px; + align-items: center; } } diff --git a/app/javascript/flavours/glitch/styles/basics.scss b/app/javascript/flavours/glitch/styles/basics.scss index a796bce674..7dc8e340c3 100644 --- a/app/javascript/flavours/glitch/styles/basics.scss +++ b/app/javascript/flavours/glitch/styles/basics.scss @@ -8,7 +8,7 @@ body { font-family: $font-sans-serif, sans-serif; - background: darken($ui-base-color, 7%); + background: darken($ui-base-color, 8%); font-size: 13px; line-height: 18px; font-weight: 400; diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 54a119ded7..5be0e85606 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -373,46 +373,155 @@ body > [data-popper-placement] { } } -.compose-form { - padding: 10px; // glitch: reduced padding +.autosuggest-textarea { + &__textarea { + background: transparent; + min-height: 100px; + padding-bottom: 0; + resize: none; + scrollbar-color: initial; - &__sensitive-button { - padding: 10px; - padding-top: 0; - font-size: 14px; - font-weight: 500; - - &.active { - color: $highlight-text-color; - } - - input[type='checkbox'] { - appearance: none; - display: inline-block; - position: relative; - border: 1px solid $ui-primary-color; - box-sizing: border-box; - width: 18px; - height: 18px; - flex: 0 0 auto; - margin-inline-end: 10px; - top: -1px; - border-radius: 4px; - vertical-align: middle; - cursor: inherit; - - &:checked { - border-color: $highlight-text-color; - background: $highlight-text-color - url("data:image/svg+xml;utf8,<svg width='18' height='18' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M4.5 8.5L8 12l6-6' stroke='white' stroke-width='1.5'/></svg>") - center center no-repeat; - } + &::-webkit-scrollbar { + all: unset; } } - .compose-form__warning { + &__suggestions { + box-shadow: var(--dropdown-shadow); + background: $ui-base-color; + border: 1px solid lighten($ui-base-color, 14%); + border-radius: 0 0 4px 4px; + color: $secondary-text-color; + font-size: 14px; + padding: 0; + + &__item { + box-sizing: border-box; + display: flex; + align-items: center; + height: 48px; + cursor: pointer; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + color: $secondary-text-color; + + &:last-child { + border-radius: 0 0 4px 4px; + } + + &:hover, + &:focus, + &:active, + &.selected { + background: $ui-highlight-color; + color: $primary-text-color; + + .autosuggest-account .display-name__account { + color: inherit; + } + } + } + } +} + +.autosuggest-account, +.autosuggest-emoji, +.autosuggest-hashtag { + flex: 1 0 0; + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 12px; + padding: 8px 12px; + overflow: hidden; + text-overflow: ellipsis; +} + +.autosuggest-account { + .display-name { + font-weight: 400; + display: flex; + flex-direction: column; + flex: 1 0 0; + } + + .display-name__account { + display: block; + line-height: 16px; + font-size: 12px; + color: $dark-text-color; + } +} + +.autosuggest-hashtag { + justify-content: space-between; + + &__name { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + &__uses { + flex: 0 0 auto; + text-align: end; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.autosuggest-emoji { + &__name { + flex: 1 0 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.autosuggest-account .account__avatar, +.autosuggest-emoji img { + display: block; + width: 24px; + height: 24px; + flex: 0 0 auto; +} + +.compose-form { + display: flex; + flex-direction: column; + gap: 32px; + + .layout-multiple-columns &, + .column & { + padding: 15px; + } + + &__highlightable { + display: flex; + flex-direction: column; + gap: 16px; + flex: 0 1 auto; + border-radius: 4px; + border: 1px solid lighten($ui-base-color, 8%); + transition: border-color 300ms linear; + min-height: 0; + position: relative; + background: $ui-base-color; + overflow-y: auto; + + &.active { + transition: none; + border-color: $ui-highlight-color; + } + } + + &__warning { color: $inverted-text-color; - margin-bottom: 10px; background: $ui-primary-color; box-shadow: 0 2px 6px rgba($base-shadow-color, 0.3); padding: 8px 10px; @@ -444,32 +553,27 @@ body > [data-popper-placement] { } } - .emoji-picker-dropdown { - position: absolute; - top: 0; - inset-inline-end: 0; - } - - .compose-form__autosuggest-wrapper { - position: relative; - } - - .autosuggest-textarea, - .autosuggest-input, .spoiler-input { - position: relative; - width: 100%; - } + display: flex; + align-items: stretch; - .spoiler-input { - height: 0; - transform-origin: bottom; - opacity: 0; + &__border { + background: url('~images/warning-stripes.svg') repeat-y; + width: 5px; + flex: 0 0 auto; - &.spoiler-input--visible { - height: 36px; - margin-bottom: 11px; - opacity: 1; + &:first-child { + border-start-start-radius: 4px; + } + + &:last-child { + border-start-end-radius: 4px; + } + } + + .autosuggest-input { + flex: 1 1 auto; + border-bottom: 1px solid lighten($ui-base-color, 8%); } } @@ -479,269 +583,303 @@ body > [data-popper-placement] { box-sizing: border-box; width: 100%; margin: 0; - color: $inverted-text-color; - background: $simple-background-color; - padding: 10px; + color: $secondary-text-color; + background: $ui-base-color; font-family: inherit; font-size: 14px; - resize: vertical; + padding: 12px; + line-height: normal; border: 0; outline: 0; - &::placeholder { - color: $dark-text-color; - } - &:focus { outline: 0; } - - @media screen and (width <= 600px) { - font-size: 16px; - } } .spoiler-input__input { - border-radius: 4px; + padding: 12px 12px - 5px; + background: mix($ui-base-color, $ui-highlight-color, 85%); + color: $highlight-text-color; } - .autosuggest-textarea__textarea { - min-height: 100px; - border-radius: 4px 4px 0 0; - padding-bottom: 0; - padding-right: 10px + 22px; // Cannot use inline-end because of dir=auto - resize: none; - scrollbar-color: initial; - - &::-webkit-scrollbar { - all: unset; - } - - @media screen and (width <= 600px) { - height: 100px !important; // Prevent auto-resize textarea - resize: vertical; - } - } - - .autosuggest-textarea__suggestions-wrapper { - position: relative; - height: 0; - } - - .autosuggest-textarea__suggestions { - box-sizing: border-box; - display: none; - position: absolute; - top: 100%; - width: 100%; - z-index: 99; - box-shadow: 4px 4px 6px rgba($base-shadow-color, 0.4); - background: $ui-secondary-color; - border-radius: 0 0 4px 4px; - color: $inverted-text-color; - font-size: 14px; - padding: 6px; - - &.autosuggest-textarea__suggestions--visible { - display: block; - } - } - - .autosuggest-textarea__suggestions__item { - padding: 10px; - cursor: pointer; - border-radius: 4px; - - &:hover, - &:focus, - &:active, - &.selected { - background: darken($ui-secondary-color, 10%); - } - } - - .autosuggest-account, - .autosuggest-emoji, - .autosuggest-hashtag { + &__dropdowns { display: flex; - flex-direction: row; align-items: center; - justify-content: flex-start; - line-height: 18px; - font-size: 14px; - } + gap: 8px; - .autosuggest-hashtag { - justify-content: space-between; - - &__name { - flex: 1 1 auto; + & > div { overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - strong { - font-weight: 500; - } - - &__uses { - flex: 0 0 auto; - text-align: end; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - } - - .autosuggest-account-icon, - .autosuggest-emoji img { - display: block; - margin-inline-end: 8px; - width: 16px; - height: 16px; - } - - .autosuggest-account .display-name__account { - color: $lighter-text-color; - } - - .compose-form__modifiers { - color: $inverted-text-color; - font-family: inherit; - font-size: 14px; - background: $simple-background-color; - - .compose-form__upload-wrapper { - overflow: hidden; - } - - .compose-form__uploads-wrapper { display: flex; - flex-direction: row; - padding: 5px; - flex-wrap: wrap; + } + } + + &__uploads { + display: flex; + gap: 8px; + padding: 0 12px; + flex-wrap: wrap; + align-self: stretch; + align-items: flex-start; + align-content: flex-start; + justify-content: center; + } + + &__upload { + flex: 1 1 0; + min-width: calc(50% - 8px); + + &__actions { + display: flex; + align-items: flex-start; + justify-content: space-between; + padding: 8px; } - .compose-form__upload { - flex: 1 1 0; - min-width: 40%; - margin: 5px; - - &__actions { - background: linear-gradient( - 180deg, - rgba($base-shadow-color, 0.8) 0, - rgba($base-shadow-color, 0.35) 80%, - transparent - ); - display: flex; - align-items: flex-start; - justify-content: space-between; - } - - .icon-button { - flex: 0 1 auto; - color: $secondary-text-color; - font-size: 14px; - font-weight: 500; - padding: 10px; - font-family: inherit; - - .icon { - width: 15px; - height: 15px; - } - - &:hover, - &:focus, - &:active { - color: lighten($secondary-text-color, 7%); - } - } - - &__warning { - position: absolute; - z-index: 2; - bottom: 0; - inset-inline-start: 0; - inset-inline-end: 0; - box-sizing: border-box; - background: linear-gradient( - 0deg, - rgba($base-shadow-color, 0.8) 0, - rgba($base-shadow-color, 0.35) 80%, - transparent - ); - } + &__preview { + position: absolute; + width: 100%; + height: 100%; + border-radius: 6px; + z-index: -1; + top: 0; + inset-inline-start: 0; } - .compose-form__upload-thumbnail { - border-radius: 4px; - background-color: $base-shadow-color; + &__thumbnail { + width: 100%; + height: 144px; + border-radius: 6px; background-position: center; background-size: cover; background-repeat: no-repeat; - height: 140px; - width: 100%; overflow: hidden; } + + .icon-button { + flex: 0 0 auto; + color: $white; + background: rgba(0, 0, 0, 75%); + border-radius: 6px; + font-size: 12px; + line-height: 16px; + font-weight: 500; + padding: 4px 8px; + font-family: inherit; + + .icon { + width: 15px; + height: 15px; + } + } + + .icon-button.compose-form__upload__delete { + padding: 3px; + border-radius: 50%; + + .icon { + width: 18px; + height: 18px; + } + } + + &__warning { + position: absolute; + z-index: 2; + bottom: 0; + inset-inline-start: 0; + inset-inline-end: 0; + padding: 8px; + + .icon-button.active { + color: #ffbe2e; + background: rgba(0, 0, 0, 75%); + } + } } - .compose-form__buttons-wrapper { - padding: 10px; - background: darken($simple-background-color, 8%); - border-radius: 0 0 4px 4px; + &__footer { display: flex; - justify-content: space-between; - flex: 0 0 auto; + flex-direction: column; + gap: 12px; + padding: 12px; + padding-top: 0; + } - .compose-form__buttons { + &__submit { + display: flex; + align-items: center; + flex: 1 1 auto; + max-width: 100%; + overflow: hidden; + } + + &__buttons { + display: flex; + gap: 8px; + align-items: center; + flex: 1 1 auto; + + & > div { display: flex; - gap: 2px; - - .icon-button { - height: 100%; - } - - .compose-form__upload-button-icon { - line-height: 27px; - } - - .compose-form__sensitive-button { - display: none; - - &.compose-form__sensitive-button--visible { - display: block; - } - - .compose-form__sensitive-button__icon { - line-height: 27px; - } - } } - .icon-button, - .text-icon-button { - box-sizing: content-box; - padding: 0 3px; + .icon-button { + padding: 3px; } - .character-counter__wrapper { - align-self: center; - margin-inline-end: 4px; + .icon-button .icon { + width: 18px; + height: 18px; } } - .compose-form__publish { + &__actions { display: flex; - justify-content: flex-end; - min-width: 0; + align-items: center; flex: 0 0 auto; + gap: 12px; + flex-wrap: wrap; - .compose-form__publish-button-wrapper { - padding-top: 15px; + .button { + display: block; // Otherwise text-ellipsis doesn't work + font-size: 14px; + line-height: normal; + font-weight: 700; + flex: 1 1 auto; + padding: 5px 12px; + border-radius: 4px; } + + .icon-button { + box-sizing: content-box; + color: $highlight-text-color; + + &:hover, + &:focus, + &:active { + color: $highlight-text-color; + } + + &.disabled { + color: $highlight-text-color; + opacity: 0.5; + } + + &.active { + background: $ui-highlight-color; + color: $primary-text-color; + } + } + } + + &__poll { + display: flex; + flex-direction: column; + align-self: stretch; + gap: 8px; + + .poll__option { + padding: 0 12px; + gap: 8px; + + &.empty:not(:focus-within) { + opacity: 0.5; + } + } + + .poll__input { + width: 17px; + height: 17px; + border-color: $darker-text-color; + } + + &__footer { + display: flex; + align-items: center; + gap: 16px; + padding-inline-start: 37px; + padding-inline-end: 40px; + + &__sep { + width: 1px; + height: 22px; + background: lighten($ui-base-color, 8%); + flex: 0 0 auto; + } + } + + &__select { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1 1 auto; + min-width: 0; + + &__label { + flex: 0 0 auto; + font-size: 11px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.5px; + color: $darker-text-color; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + &__value { + flex: 0 0 auto; + appearance: none; + background: transparent; + border: none; + padding: 0; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.1px; + color: $highlight-text-color; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } +} + +.dropdown-button { + display: flex; + align-items: center; + gap: 4px; + background: transparent; + color: $highlight-text-color; + border-radius: 6px; + border: 1px solid $highlight-text-color; + padding: 4px 8px; + font-size: 13px; + line-height: normal; + font-weight: 400; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .icon { + width: 15px; + height: 15px; + flex: 0 0 auto; + } + + &__label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1 1 auto; + } + + &.active { + background: $ui-highlight-color; + border-color: $ui-highlight-color; + color: $primary-text-color; } } @@ -749,11 +887,14 @@ body > [data-popper-placement] { cursor: default; font-family: $font-sans-serif, sans-serif; font-size: 14px; - font-weight: 600; - color: $lighter-text-color; + font-weight: 400; + line-height: normal; + color: $darker-text-color; + flex: 1 0 auto; + text-align: end; &.character-counter--over { - color: $warning-red; + color: $error-red; } } @@ -799,41 +940,6 @@ body > [data-popper-placement] { } } -.reply-indicator { - border-radius: 4px; - margin-bottom: 10px; - background: $ui-primary-color; - padding: 10px; - min-height: 23px; - overflow-y: auto; - flex: 0 2 auto; -} - -.reply-indicator__header { - margin-bottom: 5px; - overflow: hidden; -} - -.reply-indicator__cancel { - float: right; - line-height: 24px; -} - -.reply-indicator__display-name { - color: $inverted-text-color; - display: block; - max-width: 100%; - line-height: 24px; - overflow: hidden; - padding-inline-end: 25px; - text-decoration: none; -} - -.reply-indicator__display-avatar { - float: left; - margin-inline-end: 5px; -} - .status__content--with-action { cursor: pointer; } @@ -843,14 +949,15 @@ body > [data-popper-placement] { } .status__content, +.edit-indicator__content, .reply-indicator__content { position: relative; - font-size: 15px; - line-height: 20px; word-wrap: break-word; font-weight: 400; overflow: hidden; text-overflow: ellipsis; + font-size: 15px; + line-height: 20px; padding-top: 2px; color: $primary-text-color; @@ -938,6 +1045,174 @@ body > [data-popper-placement] { overflow: visible; } +.reply-indicator { + display: grid; + grid-template-columns: 46px minmax(0, 1fr); + grid-template-rows: 46px max-content; + gap: 0 10px; + + .detailed-status__display-name { + margin-bottom: 4px; + } + + .detailed-status__display-avatar { + grid-column-start: 1; + grid-row-start: 1; + grid-row-end: span 1; + } + + &__main { + grid-column-start: 2; + grid-row-start: 1; + grid-row-end: span 2; + } + + .display-name { + font-size: 14px; + line-height: 16px; + + &__account { + display: none; + } + } + + &__line { + grid-column-start: 1; + grid-row-start: 2; + grid-row-end: span 1; + position: relative; + + &::before { + display: block; + content: ''; + position: absolute; + inset-inline-start: 50%; + top: 4px; + transform: translateX(-50%); + background: lighten($ui-base-color, 8%); + width: 2px; + height: calc(100% + 32px - 8px); // Account for gap to next element + } + } + + &__content { + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + padding: 0; + max-height: 4 * 20px; + overflow: hidden; + color: $darker-text-color; + } + + &__attachments { + margin-top: 4px; + color: $darker-text-color; + font-size: 12px; + line-height: 16px; + display: flex; + align-items: center; + gap: 4px; + + .icon { + width: 18px; + height: 18px; + } + } +} + +.edit-indicator { + border-radius: 4px 4px 0 0; + background: lighten($ui-base-color, 4%); + padding: 12px; + overflow-y: auto; + flex: 0 0 auto; + border-bottom: 0.5px solid lighten($ui-base-color, 8%); + display: flex; + flex-direction: column; + gap: 4px; + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + color: $darker-text-color; + font-size: 12px; + line-height: 16px; + overflow: hidden; + text-overflow: ellipsis; + } + + &__cancel { + display: flex; + + .icon { + width: 18px; + height: 18px; + } + } + + &__display-name { + display: flex; + gap: 4px; + + a { + color: inherit; + text-decoration: none; + + &:hover, + &:focus, + &:active { + text-decoration: underline; + } + } + } + + &__content { + color: $secondary-text-color; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + padding-top: 0 !important; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + max-height: 4 * 20px; + overflow: hidden; + + a { + color: $highlight-text-color; + } + } + + &__attachments { + color: $darker-text-color; + font-size: 12px; + line-height: 16px; + opacity: 0.75; + display: flex; + align-items: center; + gap: 4px; + + .icon { + width: 18px; + height: 18px; + } + } +} + +.edit-indicator__content, +.reply-indicator__content { + .emojione { + width: 18px; + height: 18px; + margin: -3px 0 0; + } +} + .announcements__item__content { word-wrap: break-word; overflow-y: auto; @@ -1623,15 +1898,6 @@ body > [data-popper-placement] { line-height: 18px; } -.reply-indicator__content { - color: $inverted-text-color; - font-size: 14px; - - a { - color: $lighter-text-color; - } -} - .domain { padding: 10px; border-bottom: 1px solid lighten($ui-base-color, 8%); @@ -1845,7 +2111,6 @@ a .account__avatar { } .status__display-name, -.reply-indicator__display-name, .detailed-status__display-name, a.account__display-name { &:hover .display-name strong { @@ -2104,57 +2369,45 @@ a.account__display-name { } .navigation-bar { - padding: 10px; // glitch: reduced padding display: flex; align-items: center; flex-shrink: 0; cursor: default; gap: 10px; - color: $darker-text-color; - strong { - color: $secondary-text-color; + .column > & { + padding: 15px; } - a { - color: inherit; - text-decoration: none; - } + .account { + border-bottom: 0; + padding: 0; + flex: 1 1 auto; + min-width: 0; - .navigation-bar__actions { - position: relative; + &__display-name { + font-size: 16px; + line-height: 24px; + letter-spacing: 0.15px; + font-weight: 500; - .compose__action-bar .icon-button { - pointer-events: auto; - transform: scale(1, 1) translate(0, 0); - opacity: 1; - - .icon { - width: 24px; - height: 24px; + .display-name__account { + font-size: 14px; + line-height: 20px; + letter-spacing: 0.1px; } } } -} -.navigation-bar__profile { - display: flex; - flex-direction: column; - flex: 1 1 auto; - line-height: 20px; -} + .icon-button { + padding: 8px; + color: $secondary-text-color; + } -.navigation-bar__profile-account { - display: inline; - font-weight: 500; - overflow: hidden; - text-overflow: ellipsis; -} - -.navigation-bar__profile-edit { - display: inline; - color: inherit; - text-decoration: none; + .icon-button .icon { + width: 24px; + height: 24px; + } } .dropdown-animation { @@ -2328,6 +2581,7 @@ a.account__display-name { &__panels { display: flex; justify-content: center; + gap: 16px; width: 100%; height: 100%; min-height: 100vh; @@ -2360,7 +2614,6 @@ a.account__display-name { flex-direction: column; @media screen and (min-width: $no-gap-breakpoint) { - padding: 0 10px; max-width: 600px; } } @@ -2610,6 +2863,7 @@ $ui-header-height: 55px; .columns-area__panels { min-height: calc(100vh - $ui-header-height); + gap: 0; } .columns-area__panels__pane--navigational { @@ -3009,21 +3263,6 @@ $ui-header-height: 55px; } } -.compose-form__highlightable { - display: flex; - flex-direction: column; - flex: 0 1 auto; - border-radius: 4px; - transition: box-shadow 300ms linear; - min-height: 0; - position: relative; - - &.active { - transition: none; - box-shadow: 0 0 0 6px rgba(lighten($highlight-text-color, 8%), 0.7); - } -} - .compose-panel { width: 285px; margin-top: 10px; @@ -3052,32 +3291,9 @@ $ui-header-height: 55px; } } - .navigation-bar { - flex: 0 1 48px; - } - .compose-form { - flex: 1; - display: flex; - flex-direction: column; - min-height: 310px; - padding-bottom: 71px; - margin-bottom: -71px; - } - - .compose-form__autosuggest-wrapper { - overflow-y: auto; - background-color: $white; - border-radius: 4px 4px 0 0; - flex: 0 1 auto; - } - - .autosuggest-textarea__textarea { - overflow-y: hidden; - } - - .compose-form__upload-thumbnail { - height: 80px; + flex: 1 1 auto; + min-height: 0; } } @@ -3097,6 +3313,10 @@ $ui-header-height: 55px; height: 30px; width: auto; } + + &__logo { + margin-bottom: 12px; + } } .navigation-panel, @@ -3128,7 +3348,7 @@ $ui-header-height: 55px; position: absolute; top: 0; inset-inline-start: 0; - background: $ui-base-color; + background: darken($ui-base-color, 4%); box-sizing: border-box; padding: 0; display: flex; @@ -3144,7 +3364,7 @@ $ui-header-height: 55px; } .drawer__inner__mastodon { - background: $ui-base-color + background: darken($ui-base-color, 4%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-color)}"/></svg>') no-repeat bottom / 100% auto; flex: 1; @@ -3174,24 +3394,20 @@ $ui-header-height: 55px; } } -.pseudo-drawer { - background: lighten($ui-base-color, 13%); - font-size: 13px; - text-align: start; -} - .drawer__header { flex: 0 0 auto; font-size: 16px; - background: $ui-base-color; + background: darken($ui-base-color, 4%); margin-bottom: 10px; display: flex; flex-direction: row; border-radius: 4px; overflow: hidden; - a:hover { - background: lighten($ui-base-color, 3%); + a:hover, + a:focus, + a:active { + background: $ui-base-color; } } @@ -3437,7 +3653,7 @@ $ui-header-height: 55px; &--transparent { background: transparent; - color: $ui-secondary-color; + color: $secondary-text-color; &:hover, &:focus, @@ -4915,10 +5131,7 @@ a.status-card { } .emoji-picker-dropdown__menu { - background: $simple-background-color; position: relative; - box-shadow: var(--dropdown-shadow); - border-radius: 4px; margin-top: 5px; z-index: 2; @@ -4941,11 +5154,12 @@ a.status-card { .emoji-picker-dropdown__modifiers__menu { position: absolute; z-index: 4; - top: -4px; - inset-inline-start: -8px; - background: $simple-background-color; + top: -5px; + inset-inline-start: -9px; + background: var(--dropdown-background-color); + border: 1px solid var(--dropdown-border-color); border-radius: 4px; - box-shadow: 1px 2px 6px rgba($base-shadow-color, 0.2); + box-shadow: var(--dropdown-shadow); overflow: hidden; button { @@ -4958,7 +5172,7 @@ a.status-card { &:hover, &:focus, &:active { - background: rgba($ui-secondary-color, 0.4); + background: var(--dropdown-border-color); } } @@ -5027,15 +5241,17 @@ a.status-card { } .upload-progress { - padding: 10px; - color: $lighter-text-color; + color: $darker-text-color; overflow: hidden; display: flex; - gap: 10px; + gap: 8px; + align-items: center; + padding: 0 12px; .icon { width: 24px; height: 24px; + color: $ui-highlight-color; } span { @@ -5054,7 +5270,7 @@ a.status-card { width: 100%; height: 6px; border-radius: 6px; - background: darken($simple-background-color, 8%); + background: darken($ui-base-color, 8%); position: relative; margin-top: 5px; } @@ -5108,12 +5324,16 @@ a.status-card { filter: none; } -.privacy-dropdown__dropdown { - background: $simple-background-color; +.privacy-dropdown__dropdown, +.language-dropdown__dropdown { box-shadow: var(--dropdown-shadow); + background: var(--dropdown-background-color); + border: 1px solid var(--dropdown-border-color); + padding: 4px; border-radius: 4px; overflow: hidden; z-index: 2; + width: 300px; &.top { transform-origin: 50% 100%; @@ -5134,29 +5354,41 @@ a.status-card { } .privacy-dropdown__option { - color: $inverted-text-color; - padding: 10px; - align-items: center; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; + padding: 8px 12px; cursor: pointer; display: flex; + align-items: center; + gap: 12px; + border-radius: 4px; + color: $primary-text-color; &:hover, + &:active { + background: var(--dropdown-border-color); + } + + &:focus, &.active { background: $ui-highlight-color; color: $primary-text-color; outline: 0; - .privacy-dropdown__option__content { + .privacy-dropdown__option__content, + .privacy-dropdown__option__content strong, + .privacy-dropdown__option__additional { color: $primary-text-color; - - strong { - color: $primary-text-color; - } } } - &.active:hover { - background: lighten($ui-highlight-color, 4%); + &__additional { + display: flex; + align-items: center; + justify-content: center; + color: $darker-text-color; + cursor: help; } } @@ -5164,17 +5396,16 @@ a.status-card { display: flex; align-items: center; justify-content: center; - margin-inline-end: 10px; } .privacy-dropdown__option__content { flex: 1 1 auto; - color: $lighter-text-color; + color: $darker-text-color; strong { + color: $primary-text-color; font-weight: 500; display: block; - color: $inverted-text-color; @each $lang in $cjk-langs { &:lang(#{$lang}) { @@ -5298,64 +5529,78 @@ a.status-card { .language-dropdown { &__dropdown { - background: $simple-background-color; - box-shadow: var(--dropdown-shadow); - border-radius: 4px; - overflow: hidden; - z-index: 2; - - &.top { - transform-origin: 50% 100%; - } - - &.bottom { - transform-origin: 50% 0; - } + padding: 0; .emoji-mart-search { - padding-inline-end: 10px; + padding: 10px; + background: var(--dropdown-background-color); + + input { + padding: 8px 12px; + background: $ui-base-color; + border: 1px solid lighten($ui-base-color, 8%); + color: $darker-text-color; + + @media screen and (width <= 600px) { + font-size: 16px; + line-height: 24px; + letter-spacing: 0.5px; + } + } } .emoji-mart-search-icon { - inset-inline-end: 10px + 5px; + inset-inline-end: 15px; + opacity: 1; + color: $darker-text-color; + + .icon { + width: 18px; + height: 18px; + } + + &:disabled { + opacity: 0.38; + } } .emoji-mart-scroll { padding: 0 10px 10px; + background: var(--dropdown-background-color); } &__results { &__item { cursor: pointer; - color: $inverted-text-color; + color: $primary-text-color; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.25px; font-weight: 500; - padding: 10px; + padding: 8px 12px; border-radius: 4px; - display: flex; - gap: 6px; - align-items: center; - - &:focus, - &:active, - &:hover { - background: $ui-secondary-color; - } + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; &__common-name { color: $darker-text-color; + font-weight: 400; } + &:active, + &:hover { + background: var(--dropdown-border-color); + } + + &:focus, &.active { background: $ui-highlight-color; color: $primary-text-color; outline: 0; .language-dropdown__dropdown__results__item__common-name { - color: $secondary-text-color; - } - - &:hover { - background: lighten($ui-highlight-color, 4%); + color: $primary-text-color; } } } @@ -5364,9 +5609,13 @@ a.status-card { } .search { - margin-bottom: 10px; + margin-bottom: 32px; position: relative; + .layout-multiple-columns & { + margin-bottom: 10px; + } + &__popout { box-sizing: border-box; display: none; @@ -5375,6 +5624,7 @@ a.status-card { margin-top: -2px; width: 100%; background: $ui-base-color; + border: 1px solid lighten($ui-base-color, 8%); border-radius: 0 0 4px 4px; box-shadow: var(--dropdown-shadow); z-index: 99; @@ -5383,7 +5633,7 @@ a.status-card { h4 { text-transform: uppercase; - color: $dark-text-color; + color: $darker-text-color; font-weight: 500; padding: 0 10px; margin-bottom: 10px; @@ -5391,6 +5641,7 @@ a.status-card { .icon-button { padding: 0; + color: $darker-text-color; } .icon { @@ -5406,7 +5657,7 @@ a.status-card { } &__message { - color: $dark-text-color; + color: $darker-text-color; padding: 0 10px; } @@ -5462,6 +5713,10 @@ a.status-card { } &.active { + .search__input { + border-radius: 4px 4px 0 0; + } + .search__popout { display: block; } @@ -5472,14 +5727,9 @@ a.status-card { @include search-input; display: block; - padding: 15px; - padding-inline-end: 30px; - line-height: 18px; - font-size: 16px; - - &::placeholder { - color: lighten($darker-text-color, 4%); - } + padding: 12px 16px; + padding-inline-start: 16px + 15px + 8px; + line-height: normal; &::-moz-focus-inner { border: 0; @@ -5490,10 +5740,6 @@ a.status-card { &:active { outline: 0 !important; } - - &:focus { - background: lighten($ui-base-color, 4%); - } } .search__icon { @@ -5508,21 +5754,21 @@ a.status-card { .icon { position: absolute; - top: 13px; - inset-inline-end: 10px; + top: 12px + 2px; + inset-inline-start: 16px - 2px; display: inline-block; opacity: 0; transition: all 100ms linear; transition-property: transform, opacity; - width: 24px; - height: 24px; - color: $secondary-text-color; + width: 20px; + height: 20px; + color: $darker-text-color; cursor: default; pointer-events: none; &.active { pointer-events: auto; - opacity: 0.3; + opacity: 1; } } @@ -5537,16 +5783,10 @@ a.status-card { .icon-times-circle { transform: rotate(0deg); - color: $action-button-color; cursor: pointer; &.active { transform: rotate(90deg); - opacity: 1; - } - - &:hover { - color: lighten($action-button-color, 7%); } } } @@ -6167,6 +6407,11 @@ a.status-card { } } + .dialog-option { + align-items: center; + gap: 12px; + } + .dialog-option .poll__input { border-color: $inverted-text-color; color: $ui-secondary-color; @@ -6175,8 +6420,8 @@ a.status-card { justify-content: center; svg { - width: 8px; - height: auto; + width: 15px; + height: 15px; } &:active, @@ -7446,90 +7691,6 @@ noscript { } } -@media screen and (width <= 630px) and (height <= 400px) { - $duration: 400ms; - $delay: 100ms; - - .search { - will-change: margin-top; - transition: margin-top $duration $delay; - } - - .navigation-bar { - will-change: padding-bottom; - transition: padding-bottom $duration $delay; - } - - .navigation-bar { - & > a:first-child { - will-change: margin-top, margin-inline-start, margin-inline-end, width; - transition: - margin-top $duration $delay, - margin-inline-start $duration ($duration + $delay), - margin-inline-end $duration ($duration + $delay); - } - - & > .navigation-bar__profile-edit { - will-change: margin-top; - transition: margin-top $duration $delay; - } - - .navigation-bar__actions { - & > .icon-button.close { - will-change: opacity transform; - transition: - opacity $duration * 0.5 $delay, - transform $duration $delay; - } - - & > .compose__action-bar .icon-button { - will-change: opacity transform; - transition: - opacity $duration * 0.5 $delay + $duration * 0.5, - transform $duration $delay; - } - } - } - - .is-composing { - .search { - margin-top: -50px; - } - - .navigation-bar { - padding-bottom: 0; - - & > a:first-child { - margin: -100px 10px 0 -50px; - } - - .navigation-bar__profile { - padding-top: 2px; - } - - .navigation-bar__profile-edit { - position: absolute; - margin-top: -60px; - } - - .navigation-bar__actions { - .icon-button.close { - pointer-events: auto; - opacity: 1; - transform: scale(1, 1) translate(0, 0); - bottom: 5px; - } - - .compose__action-bar .icon-button { - pointer-events: none; - opacity: 0; - transform: scale(0, 1) translate(100%, 0); - } - } - } - } -} - .embed-modal { width: auto; max-width: 80vw; @@ -9498,11 +9659,14 @@ noscript { .link-footer { flex: 0 0 auto; - padding: 10px; padding-top: 20px; z-index: 1; font-size: 13px; + .column & { + padding: 15px; + } + p { color: $dark-text-color; margin-bottom: 20px; diff --git a/app/javascript/flavours/glitch/styles/containers.scss b/app/javascript/flavours/glitch/styles/containers.scss index 6d72e43924..632f1da8c7 100644 --- a/app/javascript/flavours/glitch/styles/containers.scss +++ b/app/javascript/flavours/glitch/styles/containers.scss @@ -40,13 +40,12 @@ .compose-form { width: 400px; margin: 0 auto; - padding: 20px 0; - margin-top: 40px; + padding: 10px 0; + padding-bottom: 20px; box-sizing: border-box; @media screen and (width <= 400px) { width: 100%; - margin-top: 0; padding: 20px; } } @@ -56,13 +55,15 @@ width: 400px; margin: 0 auto; display: flex; - font-size: 13px; - line-height: 18px; + align-items: center; + gap: 10px; + font-size: 14px; + line-height: 20px; box-sizing: border-box; padding: 20px 0; margin-top: 40px; margin-bottom: 10px; - border-bottom: 1px solid $ui-base-color; + border-bottom: 1px solid lighten($ui-base-color, 8%); @media screen and (width <= 440px) { width: 100%; @@ -73,6 +74,7 @@ .avatar { width: 40px; height: 40px; + flex: 0 0 auto; @include avatar-size(40px); margin-inline-end: 10px; @@ -90,13 +92,14 @@ .name { flex: 1 1 auto; color: $secondary-text-color; - width: calc(100% - 90px); .username { display: block; - font-weight: 500; + font-size: 16px; + line-height: 24px; text-overflow: ellipsis; overflow: hidden; + color: $primary-text-color; } } @@ -104,7 +107,7 @@ display: block; font-size: 32px; line-height: 40px; - margin-inline-start: 10px; + flex: 0 0 auto; } } diff --git a/app/javascript/flavours/glitch/styles/contrast/diff.scss b/app/javascript/flavours/glitch/styles/contrast/diff.scss index 1c2386f02d..ae607f484a 100644 --- a/app/javascript/flavours/glitch/styles/contrast/diff.scss +++ b/app/javascript/flavours/glitch/styles/contrast/diff.scss @@ -1,20 +1,7 @@ -.compose-form { - .compose-form__modifiers { - .compose-form__upload { - &-description { - input { - &::placeholder { - opacity: 1; - } - } - } - } - } -} - .status__content a, -.link-footer a, .reply-indicator__content a, +.edit-indicator__content a, +.link-footer a, .status__content__read-more-button, .status__content__translate-button { text-decoration: underline; @@ -42,7 +29,9 @@ } } -.status__content a { +.status__content a, +.reply-indicator__content a, +.edit-indicator__content a { color: $highlight-text-color; } @@ -50,24 +39,10 @@ color: $darker-text-color; } -.compose-form__poll-wrapper .button.button-secondary, -.compose-form .autosuggest-textarea__textarea::placeholder, -.compose-form .spoiler-input__input::placeholder, -.report-dialog-modal__textarea::placeholder, -.language-dropdown__dropdown__results__item__common-name, -.compose-form .icon-button { +.report-dialog-modal__textarea::placeholder { color: $inverted-text-color; } -.text-icon-button.active { - color: $ui-highlight-color; -} - -.language-dropdown__dropdown__results__item.active { - background: $ui-highlight-color; - font-weight: 500; -} - .link-button:disabled { cursor: not-allowed; diff --git a/app/javascript/flavours/glitch/styles/emoji_picker.scss b/app/javascript/flavours/glitch/styles/emoji_picker.scss index c7247c3a57..fec0c10ddb 100644 --- a/app/javascript/flavours/glitch/styles/emoji_picker.scss +++ b/app/javascript/flavours/glitch/styles/emoji_picker.scss @@ -1,7 +1,6 @@ .emoji-mart { font-size: 13px; display: inline-block; - color: $inverted-text-color; &, * { @@ -15,13 +14,13 @@ } .emoji-mart-bar { - border: 0 solid darken($ui-secondary-color, 8%); + border: 0 solid var(--dropdown-border-color); &:first-child { border-bottom-width: 1px; border-top-left-radius: 5px; border-top-right-radius: 5px; - background: $ui-secondary-color; + background: var(--dropdown-border-color); } &:last-child { @@ -36,7 +35,6 @@ display: flex; justify-content: space-between; padding: 0 6px; - color: $lighter-text-color; line-height: 0; } @@ -50,9 +48,10 @@ cursor: pointer; background: transparent; border: 0; + color: $darker-text-color; &:hover { - color: darken($lighter-text-color, 4%); + color: lighten($darker-text-color, 4%); } } @@ -60,7 +59,7 @@ color: $highlight-text-color; &:hover { - color: darken($highlight-text-color, 4%); + color: lighten($highlight-text-color, 4%); } .emoji-mart-anchor-bar { @@ -95,7 +94,7 @@ height: 270px; max-height: 35vh; padding: 0 6px 6px; - background: $simple-background-color; + background: var(--dropdown-background-color); will-change: transform; &::-webkit-scrollbar-track:hover, @@ -107,7 +106,7 @@ .emoji-mart-search { padding: 10px; padding-inline-end: 45px; - background: $simple-background-color; + background: var(--dropdown-background-color); position: relative; input { @@ -118,9 +117,9 @@ font-family: inherit; display: block; width: 100%; - background: rgba($ui-secondary-color, 0.3); - color: $inverted-text-color; - border: 1px solid $ui-secondary-color; + background: $ui-base-color; + color: $darker-text-color; + border: 1px solid lighten($ui-base-color, 8%); border-radius: 4px; &::-moz-focus-inner { @@ -155,11 +154,10 @@ &:disabled { cursor: default; pointer-events: none; - opacity: 0.3; } svg { - fill: $action-button-color; + fill: $darker-text-color; } } @@ -180,7 +178,7 @@ inset-inline-start: 0; width: 100%; height: 100%; - background-color: rgba($ui-secondary-color, 0.7); + background-color: var(--dropdown-border-color); border-radius: 100%; } } @@ -197,7 +195,7 @@ width: 100%; font-weight: 500; padding: 5px 6px; - background: $simple-background-color; + background: var(--dropdown-background-color); } } @@ -241,7 +239,7 @@ .emoji-mart-no-results { font-size: 14px; - color: $light-text-color; + color: $dark-text-color; text-align: center; padding: 5px 6px; padding-top: 70px; diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss index 0f3a6fa32f..1025a1bbaa 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/diff.scss @@ -145,10 +145,6 @@ html { } } -.compose-form__autosuggest-wrapper, -.poll__option input[type='text'], -.compose-form .spoiler-input__input, -.compose-form__poll-wrapper select, .search__input, .setting-text, .report-dialog-modal__textarea, @@ -172,28 +168,11 @@ html { border-bottom: 0; } -.compose-form__poll-wrapper select { - background: $simple-background-color - url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(lighten($ui-base-color, 8%))}'/></svg>") - no-repeat right 8px center / auto 16px; -} - -.compose-form__poll-wrapper, -.compose-form__poll-wrapper .poll__footer { - border-top-color: lighten($ui-base-color, 8%); -} - .notification__filter-bar { border: 1px solid lighten($ui-base-color, 8%); border-top: 0; } -.compose-form .compose-form__buttons-wrapper { - background: $ui-base-color; - border: 1px solid lighten($ui-base-color, 8%); - border-top: 0; -} - .drawer__header, .drawer__inner { background: $white; @@ -206,52 +185,6 @@ html { no-repeat bottom / 100% auto; } -// Change the colors used in compose-form -.compose-form { - .compose-form__modifiers { - .compose-form__upload__actions .icon-button, - .compose-form__upload__warning .icon-button { - color: lighten($white, 7%); - - &:active, - &:focus, - &:hover { - color: $white; - } - } - } - - .compose-form__buttons-wrapper { - background: darken($ui-base-color, 6%); - } - - .autosuggest-textarea__suggestions { - background: darken($ui-base-color, 6%); - } - - .autosuggest-textarea__suggestions__item { - &:hover, - &:focus, - &:active, - &.selected { - background: lighten($ui-base-color, 4%); - } - } -} - -.emoji-mart-bar { - border-color: lighten($ui-base-color, 4%); - - &:first-child { - background: darken($ui-base-color, 6%); - } -} - -.emoji-mart-search input { - background: rgba($ui-base-color, 0.3); - border-color: $ui-base-color; -} - .upload-progress__backdrop { background: $ui-base-color; } @@ -283,46 +216,11 @@ html { background: $ui-base-color; } -.privacy-dropdown.active .privacy-dropdown__value.active .icon-button { - color: $white; -} - .account-gallery__item a { background-color: $ui-base-color; } -// Change the colors used in the dropdown menu -.dropdown-menu { - background: $white; - - &__arrow::before { - background-color: $white; - } - - &__item { - color: $darker-text-color; - - &--dangerous { - color: $error-value-color; - } - - a, - button { - background: $white; - } - } -} - // Change the text colors on inverted background -.privacy-dropdown__option.active, -.privacy-dropdown__option:hover, -.privacy-dropdown__option.active .privacy-dropdown__option__content, -.privacy-dropdown__option.active .privacy-dropdown__option__content strong, -.privacy-dropdown__option:hover .privacy-dropdown__option__content, -.privacy-dropdown__option:hover .privacy-dropdown__option__content strong, -.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:active, -.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:focus, -.dropdown-menu__item:not(.dropdown-menu__item--dangerous) a:hover, .actions-modal ul li:not(:empty) a.active, .actions-modal ul li:not(:empty) a.active button, .actions-modal ul li:not(:empty) a:active, @@ -331,7 +229,6 @@ html { .actions-modal ul li:not(:empty) a:focus button, .actions-modal ul li:not(:empty) a:hover, .actions-modal ul li:not(:empty) a:hover button, -.language-dropdown__dropdown__results__item.active, .admin-wrapper .sidebar ul .simple-navigation-active-leaf a, .simple_form .block-button, .simple_form .button, @@ -339,19 +236,6 @@ html { color: $white; } -.language-dropdown__dropdown__results__item - .language-dropdown__dropdown__results__item__common-name { - color: lighten($ui-base-color, 8%); -} - -.language-dropdown__dropdown__results__item.active - .language-dropdown__dropdown__results__item__common-name { - color: darken($ui-base-color, 12%); -} - -.dropdown-menu__separator, -.dropdown-menu__item.edited-timestamp__history__item, -.dropdown-menu__container__header, .compare-history-modal .report-modal__target, .report-dialog-modal .poll__option.dialog-option { border-bottom-color: lighten($ui-base-color, 4%); @@ -385,10 +269,7 @@ html { .reactions-bar__item:hover, .reactions-bar__item:focus, -.reactions-bar__item:active, -.language-dropdown__dropdown__results__item:hover, -.language-dropdown__dropdown__results__item:focus, -.language-dropdown__dropdown__results__item:active { +.reactions-bar__item:active { background-color: $ui-base-color; } @@ -631,11 +512,6 @@ html { } } -.reply-indicator { - background: transparent; - border: 1px solid lighten($ui-base-color, 8%); -} - .status__content, .reply-indicator__content { a { @@ -745,3 +621,30 @@ html { filter: contrast(75%) brightness(75%) !important; } } + +.compose-form__actions .icon-button.active, +.dropdown-button.active, +.privacy-dropdown__option.active, +.privacy-dropdown__option:focus, +.language-dropdown__dropdown__results__item:focus, +.language-dropdown__dropdown__results__item.active, +.privacy-dropdown__option:focus .privacy-dropdown__option__content, +.privacy-dropdown__option:focus .privacy-dropdown__option__content strong, +.privacy-dropdown__option.active .privacy-dropdown__option__content, +.privacy-dropdown__option.active .privacy-dropdown__option__content strong, +.language-dropdown__dropdown__results__item:focus + .language-dropdown__dropdown__results__item__common-name, +.language-dropdown__dropdown__results__item.active + .language-dropdown__dropdown__results__item__common-name { + color: $white; +} + +.compose-form .spoiler-input__input { + color: lighten($ui-highlight-color, 8%); +} + +.compose-form .autosuggest-textarea__textarea, +.compose-form__highlightable, +.poll__option input[type='text'] { + background: darken($ui-base-color, 10%); +} diff --git a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss index 250e200fc6..3cf5561ca3 100644 --- a/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss +++ b/app/javascript/flavours/glitch/styles/mastodon-light/variables.scss @@ -34,7 +34,7 @@ $ui-button-tertiary-border-color: $blurple-500 !default; $primary-text-color: $black !default; $darker-text-color: $classic-base-color !default; -$highlight-text-color: darken($ui-highlight-color, 8%) !default; +$highlight-text-color: $ui-highlight-color !default; $dark-text-color: #444b5d; $action-button-color: #606984; @@ -55,3 +55,8 @@ $account-background-color: $white !default; } $emojis-requiring-inversion: 'chains'; + +.theme-mastodon-light { + --dropdown-border-color: #d9e1e8; + --dropdown-background-color: #fff; +} diff --git a/app/javascript/flavours/glitch/styles/modal.scss b/app/javascript/flavours/glitch/styles/modal.scss index 0b7220b21d..60e7d62245 100644 --- a/app/javascript/flavours/glitch/styles/modal.scss +++ b/app/javascript/flavours/glitch/styles/modal.scss @@ -1,5 +1,5 @@ .modal-layout { - background: $ui-base-color + background: darken($ui-base-color, 4%) url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234.80078 31.757813" width="234.80078" height="31.757812"><path d="M19.599609 0c-1.05 0-2.10039.375-2.90039 1.125L0 16.925781v14.832031h234.80078V17.025391l-16.5-15.900391c-1.6-1.5-4.20078-1.5-5.80078 0l-13.80078 13.099609c-1.6 1.5-4.19883 1.5-5.79883 0L179.09961 1.125c-1.6-1.5-4.19883-1.5-5.79883 0L159.5 14.224609c-1.6 1.5-4.20078 1.5-5.80078 0L139.90039 1.125c-1.6-1.5-4.20078-1.5-5.80078 0l-13.79883 13.099609c-1.6 1.5-4.20078 1.5-5.80078 0L100.69922 1.125c-1.600001-1.5-4.198829-1.5-5.798829 0l-13.59961 13.099609c-1.6 1.5-4.200781 1.5-5.800781 0L61.699219 1.125c-1.6-1.5-4.198828-1.5-5.798828 0L42.099609 14.224609c-1.6 1.5-4.198828 1.5-5.798828 0L22.5 1.125C21.7.375 20.649609 0 19.599609 0z" fill="#{hex-color($ui-base-lighter-color)}33"/></svg>') repeat-x bottom fixed; display: flex; diff --git a/app/javascript/flavours/glitch/styles/polls.scss b/app/javascript/flavours/glitch/styles/polls.scss index 4566a013a6..f12fd827de 100644 --- a/app/javascript/flavours/glitch/styles/polls.scss +++ b/app/javascript/flavours/glitch/styles/polls.scss @@ -58,6 +58,8 @@ &__option { position: relative; display: flex; + align-items: flex-start; + gap: 8px; padding: 6px 0; line-height: 18px; cursor: default; @@ -84,16 +86,22 @@ box-sizing: border-box; width: 100%; font-size: 14px; - color: $inverted-text-color; + color: $secondary-text-color; outline: 0; font-family: inherit; - background: $simple-background-color; - border: 1px solid darken($simple-background-color, 14%); + background: $ui-base-color; + border: 1px solid $darker-text-color; border-radius: 4px; - padding: 6px 10px; + padding: 8px 12px; &:focus { - border-color: $highlight-text-color; + border-color: $ui-highlight-color; + } + + @media screen and (width <= 600px) { + font-size: 16px; + line-height: 24px; + letter-spacing: 0.5px; } } @@ -102,26 +110,20 @@ } &.editable { - display: flex; align-items: center; overflow: visible; } } &__input { - display: inline-block; + display: block; position: relative; border: 1px solid $ui-primary-color; box-sizing: border-box; - width: 18px; - height: 18px; - margin-inline-end: 10px; - top: -1px; + width: 17px; + height: 17px; border-radius: 50%; - vertical-align: middle; - margin-top: auto; - margin-bottom: auto; - flex: 0 0 18px; + flex: 0 0 auto; &.checkbox { border-radius: 4px; @@ -165,6 +167,15 @@ } } + &__option.editable &__input { + &:active, + &:focus, + &:hover { + border-color: $ui-primary-color; + border-width: 1px; + } + } + &__number { display: inline-block; width: 45px; @@ -215,92 +226,6 @@ } } -.compose-form__poll-wrapper { - border-top: 1px solid darken($simple-background-color, 8%); - overflow-x: hidden; - - ul { - padding: 10px; - } - - .poll__input { - &:active, - &:focus, - &:hover { - border-color: $ui-button-focus-background-color; - } - } - - .poll__footer { - border-top: 1px solid darken($simple-background-color, 8%); - padding: 10px; - display: flex; - align-items: center; - - button, - select { - width: 100%; - flex: 1 1 50%; - - &:focus { - border-color: $highlight-text-color; - } - } - } - - .button.button-secondary { - font-size: 14px; - font-weight: 400; - padding: 6px 10px; - height: auto; - line-height: inherit; - color: $action-button-color; - border-color: $action-button-color; - margin-inline-end: 5px; - - &:hover, - &:focus, - &.active { - border-color: $action-button-color; - background-color: $action-button-color; - color: $ui-button-color; - } - } - - li { - display: flex; - align-items: center; - - .poll__option { - flex: 0 0 auto; - width: calc(100% - (23px + 6px)); - margin-inline-end: 6px; - } - } - - 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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14.933 18.467' height='19.698' width='15.929'><path d='M3.467 14.967l-3.393-3.5H14.86l-3.392 3.5c-1.866 1.925-3.666 3.5-4 3.5-.335 0-2.135-1.575-4-3.5zm.266-11.234L7.467 0 11.2 3.733l3.733 3.734H0l3.733-3.734z' fill='#{hex-color(darken($simple-background-color, 14%))}'/></svg>") - no-repeat right 8px center / auto 16px; - border: 1px solid darken($simple-background-color, 14%); - border-radius: 4px; - padding: 6px 10px; - padding-inline-end: 30px; - } - - .icon-button.disabled { - color: darken($simple-background-color, 14%); - } -} - .muted .poll { color: $dark-text-color;