diff --git a/app/javascript/mastodon/api_types/accounts.ts b/app/javascript/mastodon/api_types/accounts.ts index a9464b1a47..4a82a3548d 100644 --- a/app/javascript/mastodon/api_types/accounts.ts +++ b/app/javascript/mastodon/api_types/accounts.ts @@ -29,6 +29,7 @@ export interface ApiAccountOtherSettingsJSON { | 'mutuals_only' | 'block'; subscription_policy: 'allow' | 'followers_only' | 'block'; + bluesky: boolean; } export interface ApiServerFeaturesJSON { diff --git a/app/javascript/mastodon/features/account/components/bluesky_pill.jsx b/app/javascript/mastodon/features/account/components/bluesky_pill.jsx new file mode 100644 index 0000000000..dabbf61c68 --- /dev/null +++ b/app/javascript/mastodon/features/account/components/bluesky_pill.jsx @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import { useState, useRef, useCallback } from 'react'; + +import { FormattedMessage } from 'react-intl'; + + +import classNames from 'classnames'; + +import Overlay from 'react-overlays/Overlay'; + +import BadgeIcon from '@/material-icons/400-24px/badge.svg?react'; +import { Button } from 'mastodon/components/button'; +import { Icon } from 'mastodon/components/icon'; + +export const BlueskyPill = ({ domain, username, isSelf }) => { + const [open, setOpen] = useState(false); + const triggerRef = useRef(null); + + const handleClick = useCallback(() => { + setOpen(!open); + }, [open, setOpen]); + + const handleBlueskyProfileOpenClick = useCallback(() => { + window.open(`https://bsky.app/profile/${replacedUsername}.${replacedDomain}.ap.brid.gy`, '_blank'); + }); + + const replacedUsername = username.replaceAll('_', '-'); + const replacedDomain = domain.replaceAll('_', '-'); + + return ( + <> + <button className={classNames('account__domain-pill', { active: open })} ref={triggerRef} onClick={handleClick}>bluesky</button> + + <Overlay show={open} rootClose onHide={handleClick} offset={[5, 5]} target={triggerRef}> + {({ props }) => ( + <div {...props} className='account__domain-pill__popout dropdown-animation'> + <div className='account__domain-pill__popout__header'> + <div className='account__domain-pill__popout__header__icon'><Icon icon={BadgeIcon} /></div> + <h3><FormattedMessage id='bluesky_pill.account_available' defaultMessage='Bluesky account is available' /></h3> + </div> + + <div className='account__domain-pill__popout__handle'> + <div className='account__domain-pill__popout__handle__label'>{isSelf ? <FormattedMessage id='bluesky_pill.your_handle' defaultMessage='Your bluesky account:' /> : <FormattedMessage id='bluesky_pill.their_handle' defaultMessage='Their bluesky account:' />}</div> + <div className='account__domain-pill__popout__handle__handle'>@{replacedUsername}.{replacedDomain}.ap.brid.gy</div> + </div> + + <p>{isSelf ? <FormattedMessage id='bluesky_pill.who_you_are' defaultMessage='You can share your Mastodon account from Bluesky using the above user ID. However, please note that this is actually a bridge connection and there are many restrictions, such as only public posts will be shared.' /> : <FormattedMessage id='bluesky_pill.who_they_are' defaultMessage='You can follow this Mastodon account from Bluesky using the above user ID. However, please note that this is actually a bridge connection and there are many restrictions, such as only public posts will be shared.' />}</p> + + <p><Button onClick={handleBlueskyProfileOpenClick}><FormattedMessage id='bluesky_pill.jump_bluesky_profile' defaultMessage='Jump Bluesky profile page' /></Button></p> + </div> + )} + </Overlay> + </> + ); +}; + +BlueskyPill.propTypes = { + username: PropTypes.string.isRequired, + domain: PropTypes.string.isRequired, + isSelf: PropTypes.bool, +}; diff --git a/app/javascript/mastodon/features/account/components/header.jsx b/app/javascript/mastodon/features/account/components/header.jsx index f2fb646df8..132ebb838e 100644 --- a/app/javascript/mastodon/features/account/components/header.jsx +++ b/app/javascript/mastodon/features/account/components/header.jsx @@ -34,6 +34,7 @@ import { WithRouterPropTypes } from 'mastodon/utils/react_router'; import AccountNoteContainer from '../containers/account_note_container'; import FollowRequestNoteContainer from '../containers/follow_request_note_container'; +import { BlueskyPill } from './bluesky_pill'; import { DomainPill } from './domain_pill'; const messages = defineMessages({ @@ -468,6 +469,7 @@ class Header extends ImmutablePureComponent { <small> <span>@{username}<span className='invisible'>@{domain}</span></span> <DomainPill username={username} domain={domain} isSelf={me === account.get('id')} /> + {account.getIn(['other_settings', 'bluesky']) && <BlueskyPill username={username} domain={domain} isSelf={me === account.get('id')} />} {lockedIcon} </small> </h1> diff --git a/app/javascript/mastodon/models/account.ts b/app/javascript/mastodon/models/account.ts index 6681b9df12..4a9bba4c26 100644 --- a/app/javascript/mastodon/models/account.ts +++ b/app/javascript/mastodon/models/account.ts @@ -60,6 +60,7 @@ const AccountOtherSettingsFactory = ImmutableRecord<AccountOtherSettingsShape>({ allow_quote: true, emoji_reaction_policy: 'allow', subscription_policy: 'allow', + bluesky: false, }); // ServerFeatures diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb index 979b2278dc..2566b7ea39 100644 --- a/app/models/concerns/account/interactions.rb +++ b/app/models/concerns/account/interactions.rb @@ -220,6 +220,11 @@ module Account::Interactions following?(other_account) && followed_by?(other_account) end + def bluesky_connected? + bridge_account = Account.find_by(domain: 'bsky.brid.gy', username: 'bsky.brid.gy') + bridge_account && followed_by?(bridge_account) + end + def blocking?(other_account) block_relationships.exists?(target_account: other_account) end diff --git a/app/models/concerns/account/other_settings.rb b/app/models/concerns/account/other_settings.rb index 7968f857ff..24b343ea48 100644 --- a/app/models/concerns/account/other_settings.rb +++ b/app/models/concerns/account/other_settings.rb @@ -79,6 +79,13 @@ module Account::OtherSettings show_emoji_reaction?(account) end + def bluesky_enabled? + return bluesky_connected? if local? && !suspended? + return settings['bluesky'] if settings.present? && settings.key?('bluesky') + + false + end + def public_settings # Please update `app/javascript/mastodon/api_types/accounts.ts` when making changes to the attributes { @@ -90,6 +97,7 @@ module Account::OtherSettings 'translatable_private' => translatable_private?, 'allow_quote' => allow_quote?, 'emoji_reaction_policy' => Setting.enable_emoji_reaction ? emoji_reaction_policy.to_s : 'block', + 'bluesky' => bluesky_enabled?, } end