diff --git a/app/javascript/flavours/glitch/components/copy_icon_button.jsx b/app/javascript/flavours/glitch/components/copy_icon_button.jsx new file mode 100644 index 0000000000..9c8d5fb8a0 --- /dev/null +++ b/app/javascript/flavours/glitch/components/copy_icon_button.jsx @@ -0,0 +1,43 @@ +import PropTypes from 'prop-types'; +import { useState, useCallback } from 'react'; + +import { defineMessages } from 'react-intl'; + +import classNames from 'classnames'; + +import { useDispatch } from 'react-redux'; + +import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react'; +import { showAlert } from 'flavours/glitch/actions/alerts'; +import { IconButton } from 'flavours/glitch/components/icon_button'; + +const messages = defineMessages({ + copied: { id: 'copy_icon_button.copied', defaultMessage: 'Copied to clipboard' }, +}); + +export const CopyIconButton = ({ title, value, className }) => { + const [copied, setCopied] = useState(false); + const dispatch = useDispatch(); + + const handleClick = useCallback(() => { + navigator.clipboard.writeText(value); + setCopied(true); + dispatch(showAlert({ message: messages.copied })); + setTimeout(() => setCopied(false), 700); + }, [setCopied, value, dispatch]); + + return ( + <IconButton + className={classNames(className, copied ? 'copied' : 'copyable')} + title={title} + onClick={handleClick} + iconComponent={ContentCopyIcon} + /> + ); +}; + +CopyIconButton.propTypes = { + title: PropTypes.string, + value: PropTypes.string, + className: PropTypes.string, +}; diff --git a/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx index 37d1508528..1e71ed1dcb 100644 --- a/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx +++ b/app/javascript/flavours/glitch/features/account/components/follow_request_note.jsx @@ -7,7 +7,6 @@ import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import { Icon } from 'flavours/glitch/components/icon'; - export default class FollowRequestNote extends ImmutablePureComponent { static propTypes = { diff --git a/app/javascript/flavours/glitch/features/account/components/header.jsx b/app/javascript/flavours/glitch/features/account/components/header.jsx index 81ce989e99..29e3aa32f7 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.jsx +++ b/app/javascript/flavours/glitch/features/account/components/header.jsx @@ -9,15 +9,16 @@ import { withRouter } from 'react-router-dom'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; - import CheckIcon from '@/material-icons/400-24px/check.svg?react'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; +import ShareIcon from '@/material-icons/400-24px/share.svg?react'; import { Avatar } from 'flavours/glitch/components/avatar'; import { Badge, AutomatedBadge, GroupBadge } from 'flavours/glitch/components/badge'; import { Button } from 'flavours/glitch/components/button'; +import { CopyIconButton } from 'flavours/glitch/components/copy_icon_button'; import { Icon } from 'flavours/glitch/components/icon'; import { IconButton } from 'flavours/glitch/components/icon_button'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; @@ -45,6 +46,7 @@ const messages = defineMessages({ mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, report: { id: 'account.report', defaultMessage: 'Report @{name}' }, share: { id: 'account.share', defaultMessage: 'Share @{name}\'s profile' }, + copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' }, media: { id: 'account.media', defaultMessage: 'Media' }, blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' }, unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' }, @@ -176,11 +178,10 @@ class Header extends ImmutablePureComponent { const isRemote = account.get('acct') !== account.get('username'); const remoteDomain = isRemote ? account.get('acct').split('@')[1] : null; - let info = []; - let actionBtn = ''; - let bellBtn = ''; - let lockedIcon = ''; - let menu = []; + let actionBtn, bellBtn, lockedIcon, shareBtn; + + let info = []; + let menu = []; if (me !== account.get('id') && account.getIn(['relationship', 'followed_by'])) { info.push(<span className='relationship-tag'><FormattedMessage id='account.follows_you' defaultMessage='Follows you' /></span>); @@ -198,6 +199,12 @@ class Header extends ImmutablePureComponent { bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} iconComponent={account.getIn(['relationship', 'notifying']) ? NotificationsActiveIcon : NotificationsIcon} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />; } + if ('share' in navigator) { + shareBtn = <IconButton className='optional' iconComponent={ShareIcon} title={intl.formatMessage(messages.share, { name: account.get('username') })} onClick={this.handleShare} />; + } else { + shareBtn = <CopyIconButton className='optional' title={intl.formatMessage(messages.copy)} value={account.get('url')} />; + } + if (me !== account.get('id')) { if (signedIn && !account.get('relationship')) { // Wait until the relationship is loaded actionBtn = ''; @@ -235,11 +242,6 @@ class Header extends ImmutablePureComponent { menu.push(null); } - if ('share' in navigator && !account.get('suspended')) { - menu.push({ text: intl.formatMessage(messages.share, { name: account.get('username') }), action: this.handleShare }); - menu.push(null); - } - if (account.get('id') === me) { if (profileLink) menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); if (preferencesLink) menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); @@ -308,7 +310,7 @@ class Header extends ImmutablePureComponent { } } - const content = { __html: account.get('note_emojified') }; + const content = { __html: account.get('note_emojified') }; const displayNameHtml = { __html: account.get('display_name_html') }; const fields = account.get('fields'); const isLocal = account.get('acct').indexOf('@') === -1; @@ -350,6 +352,7 @@ class Header extends ImmutablePureComponent { <> {actionBtn} {bellBtn} + {shareBtn} </> )} @@ -372,28 +375,29 @@ class Header extends ImmutablePureComponent { </div> )} - {signedIn && <AccountNoteContainer account={account} />} - {!(suspended || hidden) && ( <div className='account__header__extra'> <div className='account__header__bio'> - { fields.size > 0 && ( - <div className='account__header__fields'> - {fields.map((pair, i) => ( - <dl key={i}> - <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} /> - - <dd className={pair.get('verified_at') && 'verified'} title={pair.get('value_plain')}> - {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} className='translate' /> - </dd> - </dl> - ))} - </div> - )} + {(account.get('id') !== me && signedIn) && <AccountNoteContainer account={account} />} {account.get('note').length > 0 && account.get('note') !== '<p></p>' && <div className='account__header__content translate' dangerouslySetInnerHTML={content} />} - <div className='account__header__joined'><FormattedMessage id='account.joined' defaultMessage='Joined {date}' values={{ date: intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' }) }} /></div> + <div className='account__header__fields'> + <dl> + <dt><FormattedMessage id='account.joined_short' defaultMessage='Joined' /></dt> + <dd>{intl.formatDate(account.get('created_at'), { year: 'numeric', month: 'short', day: '2-digit' })}</dd> + </dl> + + {fields.map((pair, i) => ( + <dl key={i} className={classNames({ verified: pair.get('verified_at') })}> + <dt dangerouslySetInnerHTML={{ __html: pair.get('name_emojified') }} title={pair.get('name')} className='translate' /> + + <dd className='translate' title={pair.get('value_plain')}> + {pair.get('verified_at') && <span title={intl.formatMessage(messages.linkVerifiedOn, { date: intl.formatDate(pair.get('verified_at'), dateFormatOptions) })}><Icon id='check' icon={CheckIcon} className='verified__mark' /></span>} <span dangerouslySetInnerHTML={{ __html: pair.get('value_emojified') }} /> + </dd> + </dl> + ))} + </div> </div> </div> )} diff --git a/app/javascript/flavours/glitch/locales/en.json b/app/javascript/flavours/glitch/locales/en.json index f20d20b8a9..5d522f2571 100644 --- a/app/javascript/flavours/glitch/locales/en.json +++ b/app/javascript/flavours/glitch/locales/en.json @@ -3,7 +3,6 @@ "account.disclaimer_full": "Information below may reflect the user's profile incompletely.", "account.follows": "Follows", "account.follows_you": "Follows you", - "account.joined": "Joined {date}", "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", diff --git a/app/javascript/flavours/glitch/styles/components.scss b/app/javascript/flavours/glitch/styles/components.scss index 553c4346f5..a7ba7c1ca8 100644 --- a/app/javascript/flavours/glitch/styles/components.scss +++ b/app/javascript/flavours/glitch/styles/components.scss @@ -1244,6 +1244,17 @@ body > [data-popper-placement] { font-size: 14px; font-weight: 500; } + + &.copyable { + transition: all 300ms linear; + } + + &.copied { + border-color: $valid-value-color; + color: $valid-value-color; + transition: none; + background-color: rgba($valid-value-color, 0.15); + } } .domain__wrapper { @@ -5330,7 +5341,7 @@ a.status-card.compact:hover { border-bottom: 1px solid lighten($ui-base-color, 8%); display: flex; flex-direction: row; - padding: 10px 0; + padding: 8px 0; } .language-dropdown { @@ -7979,6 +7990,7 @@ noscript { .account__header { overflow: hidden; + container: account-header / inline-size; &.inactive { opacity: 0.5; @@ -8000,6 +8012,7 @@ noscript { height: 145px; position: relative; background: darken($ui-base-color, 4%); + border-bottom: 1px solid lighten($ui-base-color, 8%); img { object-fit: cover; @@ -8012,9 +8025,9 @@ noscript { &__bar { position: relative; - background: lighten($ui-base-color, 4%); - padding: 5px; - border-bottom: 1px solid lighten($ui-base-color, 12%); + padding: 0 20px; + padding-bottom: 16px; // glitch-soc addition for the different tabs design + border-bottom: 1px solid lighten($ui-base-color, 8%); .avatar { display: block; @@ -8023,7 +8036,7 @@ noscript { .account__avatar { background: darken($ui-base-color, 8%); - border: 2px solid lighten($ui-base-color, 4%); + border: 2px solid $ui-base-color; } } } @@ -8042,8 +8055,8 @@ noscript { display: flex; align-items: flex-start; justify-content: space-between; - padding: 7px 10px; margin-top: -55px; + padding-top: 10px; gap: 8px; overflow: hidden; margin-inline-start: -2px; // aligns the pfp with content below @@ -8074,11 +8087,22 @@ noscript { width: 24px; height: 24px; } + + &.copied { + border-color: $valid-value-color; + } + } + + @container account-header (max-width: 372px) { + .optional { + display: none; + } } } &__name { - padding: 5px 10px; + margin-top: 16px; + margin-bottom: 16px; .emojione { width: 22px; @@ -8086,17 +8110,17 @@ noscript { } h1 { - font-size: 16px; - line-height: 24px; + font-size: 17px; + line-height: 22px; color: $primary-text-color; - font-weight: 500; + font-weight: 700; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; small { display: block; - font-size: 14px; + font-size: 15px; color: $darker-text-color; font-weight: 400; overflow: hidden; @@ -8122,63 +8146,97 @@ noscript { } &__bio { - overflow: hidden; - margin: 0 -5px; - .account__header__content { - padding: 20px 15px; - padding-bottom: 5px; color: $primary-text-color; } - .account__header__joined { - font-size: 14px; - padding: 5px 15px; - color: $darker-text-color; - - .columns-area--mobile & { - padding-inline-start: 20px; - padding-inline-end: 20px; - } - } - .account__header__fields { margin: 0; - border-top: 1px solid lighten($ui-base-color, 12%); + margin-top: 16px; + border-radius: 4px; + background: darken($ui-base-color, 4%); + border: 0; + + dl { + display: block; + padding: 8px 16px; // glitch-soc: padding purposefuly reduced + border-bottom-color: lighten($ui-base-color, 4%); + } + + dd, + dt { + font-size: 13px; + line-height: 18px; + padding: 0; + text-align: initial; + } + + dt { + width: auto; + background: transparent; + text-transform: uppercase; + color: $dark-text-color; + } + + dd { + color: $darker-text-color; + } a { color: lighten($ui-highlight-color, 8%); } - dl:first-child .verified { - border-radius: 0 4px 0 0; - } - .icon { width: 18px; height: 18px; - vertical-align: middle; } - dd { - display: flex; - align-items: center; - gap: 4px; - } + .verified { + border: 1px solid rgba($valid-value-color, 0.5); + margin-top: -1px; - .verified a { - color: $valid-value-color; + &:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + margin-top: 0; + } + + &:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + } + + dt, + dd { + color: $valid-value-color; + } + + dd { + display: flex; + align-items: center; + gap: 4px; + + span { + display: flex; + } + } + + a { + color: $valid-value-color; + } } } } &__extra { - margin-top: 4px; + margin-top: 16px; &__links { font-size: 14px; color: $darker-text-color; - padding: 10px 0; + margin: 0 -10px; + padding-top: 16px; + padding-bottom: 10px; a { display: inline-block; @@ -8196,14 +8254,10 @@ noscript { } &__account-note { - margin: 0 -5px; - padding: 10px 15px; - display: flex; - flex-direction: column; + color: $primary-text-color; font-size: 14px; font-weight: 400; - border-top: 1px solid lighten($ui-base-color, 12%); - border-bottom: 1px solid lighten($ui-base-color, 12%); + margin-bottom: 10px; label { display: block; @@ -8214,23 +8268,12 @@ noscript { margin-bottom: 5px; } - &__content { - white-space: pre-wrap; - padding: 10px 0; - } - - strong { - font-size: 12px; - font-weight: 500; - text-transform: uppercase; - } - textarea { display: block; box-sizing: border-box; width: calc(100% + 20px); color: $secondary-text-color; - background: $ui-base-color; + background: transparent; padding: 10px; margin: 0 -10px; font-family: inherit; @@ -8244,6 +8287,10 @@ noscript { color: $dark-text-color; opacity: 1; } + + &:focus { + background: $ui-base-color; + } } } }