diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 742f6467c7..7e4b48a857 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -161,10 +161,11 @@ export function resetCompose() { }; } -export const focusCompose = (defaultText = '') => (dispatch, getState) => { +export const focusCompose = (defaultText = '', caretStart = false) => (dispatch, getState) => { dispatch({ type: COMPOSE_FOCUS, defaultText, + caretStart, }); ensureComposeIsVisible(getState); diff --git a/app/javascript/flavours/glitch/components/number_fields/index.tsx b/app/javascript/flavours/glitch/components/number_fields/index.tsx index 71ee954888..a10147afbb 100644 --- a/app/javascript/flavours/glitch/components/number_fields/index.tsx +++ b/app/javascript/flavours/glitch/components/number_fields/index.tsx @@ -24,9 +24,14 @@ export const NumberFieldsItem: React.FC = ({ link, children, className, + ...restProps }) => { return ( -
  • +
  • {label} {link ? ( diff --git a/app/javascript/flavours/glitch/components/number_fields/styles.module.scss b/app/javascript/flavours/glitch/components/number_fields/styles.module.scss index f8739f7c10..f71f690e9c 100644 --- a/app/javascript/flavours/glitch/components/number_fields/styles.module.scss +++ b/app/javascript/flavours/glitch/components/number_fields/styles.module.scss @@ -14,6 +14,7 @@ } a, + button, strong { display: block; font-weight: 600; @@ -25,10 +26,21 @@ a { padding: 0; text-decoration: none; + } + a, + button { &:hover, &:focus { text-decoration: underline; } } + + button { + appearance: none; + background: none; + border: none; + display: block; + padding: 0; + } } diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx index 9f8330b078..15842da458 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx @@ -1,8 +1,9 @@ -import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react'; import type { FC } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; +import { openModal } from '@/flavours/glitch/actions/modal'; import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date'; import { NumberFields, @@ -10,6 +11,7 @@ import { } from '@/flavours/glitch/components/number_fields'; import { ShortNumber } from '@/flavours/glitch/components/short_number'; import { useAccount } from '@/flavours/glitch/hooks/useAccount'; +import { useAppDispatch } from '@/flavours/glitch/store'; export const AccountNumberFields: FC<{ accountId: string }> = ({ accountId, @@ -21,6 +23,13 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({ [account?.created_at], ); + const dispatch = useAppDispatch(); + const showJoinModal = useCallback(() => { + dispatch( + openModal({ modalType: 'ACCOUNT_JOIN_DATE', modalProps: { accountId } }), + ); + }, [accountId, dispatch]); + if (!account) { return null; } @@ -60,15 +69,17 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({ } hint={intl.formatDate(account.created_at)} > - {createdThisYear ? ( - - ) : ( - - )} + ); diff --git a/app/javascript/flavours/glitch/features/account_timeline/modals/field_modal.tsx b/app/javascript/flavours/glitch/features/account_timeline/modals/field_modal.tsx index d13ab5bfaf..52dc571e81 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/modals/field_modal.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/modals/field_modal.tsx @@ -13,7 +13,7 @@ import { import type { AccountField } from '../common'; import { useFieldHtml } from '../hooks/useFieldHtml'; -import classes from './styles.module.css'; +import classes from './styles.module.scss'; export const AccountFieldModal: FC<{ onClose: () => void; diff --git a/app/javascript/flavours/glitch/features/account_timeline/modals/join_modal.tsx b/app/javascript/flavours/glitch/features/account_timeline/modals/join_modal.tsx new file mode 100644 index 0000000000..1b126f6f72 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/modals/join_modal.tsx @@ -0,0 +1,273 @@ +import { useCallback, useMemo } from 'react'; +import type { FC } from 'react'; + +import { defineMessage, FormattedMessage, useIntl } from 'react-intl'; + +import { focusCompose, resetCompose } from '@/flavours/glitch/actions/compose'; +import { closeModal } from '@/flavours/glitch/actions/modal'; +import { Button } from '@/flavours/glitch/components/button'; +import { DisplayNameSimple } from '@/flavours/glitch/components/display_name/simple'; +import { FormattedDateWrapper } from '@/flavours/glitch/components/formatted_date'; +import { IconButton } from '@/flavours/glitch/components/icon_button'; +import { + ModalShell, + ModalShellBody, +} from '@/flavours/glitch/components/modal_shell'; +import { useAccount } from '@/flavours/glitch/hooks/useAccount'; +import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId'; +import { + createAppSelector, + useAppDispatch, + useAppSelector, +} from '@/flavours/glitch/store'; +import AnniversaryImage from '@/images/anniversary.svg?react'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; + +import classes from './styles.module.scss'; + +const closeMessage = defineMessage({ + id: 'lightbox.close', + defaultMessage: 'Close', +}); + +const selectServerName = createAppSelector( + [ + (state) => state.accounts, + (_, accountId: string) => accountId, + (state) => state.server.getIn(['server', 'domain']) as string | undefined, + ], + (accounts, accountId, serverDomain) => { + const acct = accounts.getIn([accountId, 'acct']) as string | undefined; + if (!acct) { + return undefined; + } + + const domain = acct.split('@').at(1); + if (domain) { + return domain; + } + + return serverDomain; + }, +); + +export const AccountJoinModal: FC<{ + accountId: string; + onClose: () => void; +}> = ({ accountId, onClose }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const currentId = useCurrentAccountId(); + const isMe = accountId === currentId; + + const createdAtStr = account?.created_at; + const anniversary = useMemo(() => { + if (!createdAtStr) { + return null; + } + const now = new Date(); + const createdAt = new Date(createdAtStr); + if ( + now.getMonth() === createdAt.getMonth() && + now.getDate() === createdAt.getDate() + ) { + return now.getFullYear() - createdAt.getFullYear(); + } + return null; + }, [createdAtStr]); + + const domain = useAppSelector((state) => selectServerName(state, accountId)); + + const dispatch = useAppDispatch(); + const handle = account?.acct; + const handleShare = useCallback(() => { + if (anniversary === null) { + return; + } + + let shareText = '#Fediversary'; + if (anniversary === 0) { + shareText = isMe ? '#firstday' : '#welcome'; + } + + if (!isMe && handle) { + shareText = `@${handle} ${shareText}`; + } + + dispatch(resetCompose()); + dispatch(focusCompose(`\n\n${shareText}`, true)); + dispatch(closeModal({ modalType: 'ACCOUNT_JOIN_DATE', ignoreFocus: true })); + }, [anniversary, handle, dispatch, isMe]); + + return ( + + + + +
    + } + isMe={isMe} + serverName={domain} + anniversary={anniversary} + /> +

    + +

    +
    + + + + +
    +
    + ); +}; + +const AccountJoinMessage: FC<{ + name: React.JSX.Element; + isMe: boolean; + serverName?: string; + anniversary: number | null; +}> = ({ name, isMe, serverName, anniversary }) => { + if (anniversary === 0) { + if (isMe) { + return ( + + ); + } + return ( + + ); + } + + if (isMe) { + if (anniversary !== null && anniversary > 0) { + return ( + + ); + } + return ( + + ); + } + + return ( + + ); +}; + +const AccountAnniversaryImage: FC<{ anniversary: number | null }> = ({ + anniversary, +}) => { + if (anniversary === null) { + return null; + } + + return ( +
    + +

    {anniversary || 1}

    + {anniversary === 0 && ( + + )} + {anniversary > 0 && ( + + )} +
    + ); +}; + +const AccountAnniversaryShare: FC<{ + anniversary: number | null; + onShare: () => void; + isMe: boolean; +}> = ({ anniversary, onShare, isMe }) => { + if (anniversary === null) { + return null; + } + + return ( + + ); +}; diff --git a/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx b/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx index 2a45d5fab4..09c8102045 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/modals/note_modal.tsx @@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store'; import { ConfirmationModal } from '../../ui/components/confirmation_modals'; -import classes from './styles.module.css'; +import classes from './styles.module.scss'; const messages = defineMessages({ newTitle: { diff --git a/app/javascript/flavours/glitch/features/account_timeline/modals/styles.module.css b/app/javascript/flavours/glitch/features/account_timeline/modals/styles.module.css deleted file mode 100644 index 6266551e2d..0000000000 --- a/app/javascript/flavours/glitch/features/account_timeline/modals/styles.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.noteCallout { - margin-bottom: 16px; -} - -.noteInput { - min-height: 70px; - width: 100%; - padding: 8px; - border-radius: 8px; - box-sizing: border-box; - background: var(--color-bg-primary); - border: 1px solid var(--color-border-primary); - appearance: none; - resize: none; - margin-top: 4px; -} - -.noteInput:focus-visible { - outline: var(--outline-focus-default); - outline-offset: 2px; -} - -.fieldName, -.fieldValue { - word-break: break-all; -} - -.fieldValue { - color: var(--color-text-primary); - font-weight: 600; - margin-top: 4px; -} diff --git a/app/javascript/flavours/glitch/features/account_timeline/modals/styles.module.scss b/app/javascript/flavours/glitch/features/account_timeline/modals/styles.module.scss new file mode 100644 index 0000000000..67a4f43d44 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/modals/styles.module.scss @@ -0,0 +1,108 @@ +@use '@/styles/mastodon/variables' as *; + +.noteCallout { + margin-bottom: 16px; +} + +.noteInput { + min-height: 70px; + width: 100%; + padding: 8px; + border-radius: 8px; + box-sizing: border-box; + background: var(--color-bg-primary); + border: 1px solid var(--color-border-primary); + appearance: none; + resize: none; + margin-top: 4px; +} + +.noteInput:focus-visible { + outline: var(--outline-focus-default); + outline-offset: 2px; +} + +.fieldName, +.fieldValue { + word-break: break-all; +} + +.fieldValue { + color: var(--color-text-primary); + font-weight: 600; + margin-top: 4px; +} + +@media screen and (min-width: ($mobile-breakpoint + 1)) { + .joinShell { + > :global(.safety-action-modal__top) { + border-bottom-left-radius: 16px; + border-bottom-right-radius: 16px; + border-bottom-width: 1px; + } + } +} + +.joinWrapper { + display: flex; + flex-direction: column; + color: var(--color-text-primary); + align-items: center; + justify-content: center; + position: relative; + min-height: 120px; + gap: 16px; + + p, + h1 { + text-align: center; + } + + p { + font-size: 13px; + } + + h1 { + margin-top: 8px; + margin-bottom: 0; + font-size: 17px; + font-weight: 600; + } +} + +.joinBanner { + width: 120px; + height: 110px; + position: relative; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + svg { + width: 100%; + position: absolute; + top: 0; + left: 0; + } + + h2 { + font-size: 40px; + font-weight: 600; + line-height: 40px; + margin-top: 8px; + } + + h3 { + font-size: 13px; + color: var(--color-text-secondary); + text-transform: uppercase; + } +} + +.joinClose { + position: absolute; + top: 0; + right: 0; + z-index: 1; +} diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx index 77c559ba0f..79f41da604 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.jsx @@ -101,6 +101,7 @@ export const MODAL_COMPONENTS = { 'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }), 'ACCOUNT_NOTE': () => import('@/flavours/glitch/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })), 'ACCOUNT_FIELD_OVERFLOW': () => import('@/flavours/glitch/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })), + 'ACCOUNT_JOIN_DATE': () => import('@/flavours/glitch/features/account_timeline/modals/join_modal').then(module => ({ default: module.AccountJoinModal })), 'ACCOUNT_EDIT_NAME': accountEditModal('NameModal'), 'ACCOUNT_EDIT_BIO': accountEditModal('BioModal'), 'ACCOUNT_EDIT_PROFILE_DISPLAY': accountEditModal('ProfileDisplayModal'), diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index eefd8a1281..15a9370066 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -732,7 +732,10 @@ export const composeReducer = (state = initialState, action) => { case COMPOSE_LANGUAGE_CHANGE: return state.set('language', action.language); case COMPOSE_FOCUS: - return state.set('focusDate', new Date()).update('text', text => text.length > 0 ? text : action.defaultText); + return state + .set('focusDate', new Date()) + .update('text', text => text.length > 0 ? text : action.defaultText) + .update('caretPosition', position => action.caretStart ? 0 : position); case COMPOSE_CHANGE_MEDIA_ORDER: return state.update('media_attachments', list => { const indexA = list.findIndex(x => x.get('id') === action.a); diff --git a/app/javascript/flavours/glitch/styles/mastodon/components.scss b/app/javascript/flavours/glitch/styles/mastodon/components.scss index 2cd931f151..cd4351b63f 100644 --- a/app/javascript/flavours/glitch/styles/mastodon/components.scss +++ b/app/javascript/flavours/glitch/styles/mastodon/components.scss @@ -6614,7 +6614,7 @@ a.status-card { &__top { border-radius: 16px 16px 0 0; - border-bottom: 0; + border-bottom-width: 0; gap: 16px; }