[Glitch] Profile redesign: Show full join date

Port e2be688389f618684b3d6d735d22d0b4424172a6 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo 2026-04-23 14:47:01 +02:00 committed by Claire
parent 66298d8567
commit 77955109ca
12 changed files with 430 additions and 48 deletions

View File

@ -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);

View File

@ -24,9 +24,14 @@ export const NumberFieldsItem: React.FC<ItemProps> = ({
link,
children,
className,
...restProps
}) => {
return (
<li className={classNames(classes.item, className)} title={hint}>
<li
{...restProps}
className={classNames(classes.item, className)}
title={hint}
>
{label}
{link ? (
<NavLink exact to={link}>

View File

@ -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;
}
}

View File

@ -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 ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
<button type='button' onClick={showJoinModal}>
{createdThisYear ? (
<FormattedDateWrapper
value={account.created_at}
month='short'
day='2-digit'
/>
) : (
<FormattedDateWrapper value={account.created_at} year='numeric' />
)}
</button>
</NumberFieldsItem>
</NumberFields>
);

View File

@ -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;

View File

@ -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 (
<ModalShell className={classes.joinShell}>
<ModalShellBody className={classes.joinWrapper}>
<AccountAnniversaryImage anniversary={anniversary} />
<div>
<AccountJoinMessage
name={<DisplayNameSimple account={account} />}
isMe={isMe}
serverName={domain}
anniversary={anniversary}
/>
<h1>
<FormattedDateWrapper
value={account?.created_at}
month='short'
day='numeric'
year='numeric'
/>
</h1>
</div>
<AccountAnniversaryShare
anniversary={anniversary}
onShare={handleShare}
isMe={isMe}
/>
<IconButton
iconComponent={CloseIcon}
icon='times'
onClick={onClose}
title={intl.formatMessage(closeMessage)}
className={classes.joinClose}
/>
</ModalShellBody>
</ModalShell>
);
};
const AccountJoinMessage: FC<{
name: React.JSX.Element;
isMe: boolean;
serverName?: string;
anniversary: number | null;
}> = ({ name, isMe, serverName, anniversary }) => {
if (anniversary === 0) {
if (isMe) {
return (
<FormattedMessage
id='account.join_modal.me_today'
defaultMessage='Its your first day on {server}!'
tagName='p'
values={{
server: serverName,
}}
/>
);
}
return (
<FormattedMessage
id='account.join_modal.other_today'
defaultMessage='Its {name}s first day on {server}!'
tagName='p'
values={{
name,
server: serverName,
}}
/>
);
}
if (isMe) {
if (anniversary !== null && anniversary > 0) {
return (
<FormattedMessage
id='account.join_modal.me_anniversary'
defaultMessage='Happy Fediversary! You joined {server} on'
tagName='p'
values={{
server: serverName,
}}
/>
);
}
return (
<FormattedMessage
id='account.join_modal.me'
defaultMessage='You joined {server} on'
tagName='p'
values={{
server: serverName,
}}
/>
);
}
return (
<FormattedMessage
id='account.join_modal.other'
defaultMessage='{name} joined {server} on'
tagName='p'
values={{
name,
server: serverName,
}}
/>
);
};
const AccountAnniversaryImage: FC<{ anniversary: number | null }> = ({
anniversary,
}) => {
if (anniversary === null) {
return null;
}
return (
<div className={classes.joinBanner}>
<AnniversaryImage role='presentation' />
<h2>{anniversary || 1}</h2>
{anniversary === 0 && (
<FormattedMessage
id='account.join_modal.day'
defaultMessage='Day'
tagName='h3'
/>
)}
{anniversary > 0 && (
<FormattedMessage
id='account.join_modal.years'
defaultMessage='{number, plural, one {year} other {years}}'
values={{ number: anniversary }}
tagName='h3'
/>
)}
</div>
);
};
const AccountAnniversaryShare: FC<{
anniversary: number | null;
onShare: () => void;
isMe: boolean;
}> = ({ anniversary, onShare, isMe }) => {
if (anniversary === null) {
return null;
}
return (
<Button onClick={onShare}>
{anniversary === 0 && isMe && (
<FormattedMessage
id='account.join_modal.share.intro'
defaultMessage='Share an intro post'
/>
)}
{anniversary === 0 && !isMe && (
<FormattedMessage
id='account.join_modal.share.welcome'
defaultMessage='Share a welcome post'
/>
)}
{anniversary > 0 && (
<FormattedMessage
id='account.join_modal.share.celebrate'
defaultMessage='Share a celebratory post'
/>
)}
</Button>
);
};

View File

@ -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: {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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'),

View File

@ -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);

View File

@ -6614,7 +6614,7 @@ a.status-card {
&__top {
border-radius: 16px 16px 0 0;
border-bottom: 0;
border-bottom-width: 0;
gap: 16px;
}