kopia lustrzana https://gitlab.com/soapbox-pub/soapbox
Merge branch 'streak-modal' into 'main'
Streak modal See merge request soapbox-pub/soapbox!3332merge-requests/3278/merge
commit
eb95f88523
|
@ -15,6 +15,7 @@ import {
|
||||||
selectComposeSuggestion,
|
selectComposeSuggestion,
|
||||||
uploadCompose,
|
uploadCompose,
|
||||||
} from 'soapbox/actions/compose.ts';
|
} from 'soapbox/actions/compose.ts';
|
||||||
|
import { openModal } from 'soapbox/actions/modals.ts';
|
||||||
import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts';
|
import { useCustomEmojis } from 'soapbox/api/hooks/useCustomEmojis.ts';
|
||||||
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx';
|
import AutosuggestInput, { AutoSuggestion } from 'soapbox/components/autosuggest-input.tsx';
|
||||||
import Button from 'soapbox/components/ui/button.tsx';
|
import Button from 'soapbox/components/ui/button.tsx';
|
||||||
|
@ -72,9 +73,10 @@ interface IComposeForm<ID extends string> {
|
||||||
event?: string;
|
event?: string;
|
||||||
group?: string;
|
group?: string;
|
||||||
extra?: React.ReactNode;
|
extra?: React.ReactNode;
|
||||||
|
streak?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, extra }: IComposeForm<ID>) => {
|
const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickableAreaRef, event, group, extra, streak }: IComposeForm<ID>) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -156,6 +158,10 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
|
||||||
dispatch(changeCompose(id, text));
|
dispatch(changeCompose(id, text));
|
||||||
dispatch(submitCompose(id, { history }));
|
dispatch(submitCompose(id, { history }));
|
||||||
|
|
||||||
|
if (streak === 0 && features.streak) {
|
||||||
|
dispatch(openModal('STREAK'));
|
||||||
|
}
|
||||||
|
|
||||||
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
editorRef.current?.dispatchCommand(CLEAR_EDITOR_COMMAND, undefined);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@ import {
|
||||||
ReblogsModal,
|
ReblogsModal,
|
||||||
ReplyMentionsModal,
|
ReplyMentionsModal,
|
||||||
ReportModal,
|
ReportModal,
|
||||||
|
StreakModal,
|
||||||
UnauthorizedModal,
|
UnauthorizedModal,
|
||||||
VideoModal,
|
VideoModal,
|
||||||
EditRuleModal,
|
EditRuleModal,
|
||||||
|
@ -92,6 +93,7 @@ const MODAL_COMPONENTS: Record<string, React.ExoticComponent<any>> = {
|
||||||
'REBLOGS': ReblogsModal,
|
'REBLOGS': ReblogsModal,
|
||||||
'REPLY_MENTIONS': ReplyMentionsModal,
|
'REPLY_MENTIONS': ReplyMentionsModal,
|
||||||
'REPORT': ReportModal,
|
'REPORT': ReportModal,
|
||||||
|
'STREAK': StreakModal,
|
||||||
'UNAUTHORIZED': UnauthorizedModal,
|
'UNAUTHORIZED': UnauthorizedModal,
|
||||||
'VIDEO': VideoModal,
|
'VIDEO': VideoModal,
|
||||||
'ZAPS': ZapsModal,
|
'ZAPS': ZapsModal,
|
||||||
|
|
|
@ -15,6 +15,7 @@ import { useAppDispatch } from 'soapbox/hooks/useAppDispatch.ts';
|
||||||
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
import { useAppSelector } from 'soapbox/hooks/useAppSelector.ts';
|
||||||
import { useCompose } from 'soapbox/hooks/useCompose.ts';
|
import { useCompose } from 'soapbox/hooks/useCompose.ts';
|
||||||
import { useDraggedFiles } from 'soapbox/hooks/useDraggedFiles.ts';
|
import { useDraggedFiles } from 'soapbox/hooks/useDraggedFiles.ts';
|
||||||
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
|
||||||
import ComposeForm from '../../../compose/components/compose-form.tsx';
|
import ComposeForm from '../../../compose/components/compose-form.tsx';
|
||||||
|
|
||||||
|
@ -33,6 +34,7 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose, composeId = 'compose-m
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const node = useRef<HTMLDivElement>(null);
|
const node = useRef<HTMLDivElement>(null);
|
||||||
const compose = useCompose(composeId);
|
const compose = useCompose(composeId);
|
||||||
|
const { account } = useOwnAccount();
|
||||||
|
|
||||||
const { id: statusId, privacy, in_reply_to: inReplyTo, quote, group_id: groupId } = compose!;
|
const { id: statusId, privacy, in_reply_to: inReplyTo, quote, group_id: groupId } = compose!;
|
||||||
|
|
||||||
|
@ -40,6 +42,8 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose, composeId = 'compose-m
|
||||||
dispatch(uploadCompose(composeId, files, intl));
|
dispatch(uploadCompose(composeId, files, intl));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userStreak = account?.ditto.streak.days;
|
||||||
|
|
||||||
const onClickClose = () => {
|
const onClickClose = () => {
|
||||||
if (checkComposeContent(compose)) {
|
if (checkComposeContent(compose)) {
|
||||||
dispatch(openModal('CONFIRM', {
|
dispatch(openModal('CONFIRM', {
|
||||||
|
@ -93,6 +97,7 @@ const ComposeModal: React.FC<IComposeModal> = ({ onClose, composeId = 'compose-m
|
||||||
id={composeId}
|
id={composeId}
|
||||||
extra={<ComposeFormGroupToggle composeId={composeId} groupId={groupId} />}
|
extra={<ComposeFormGroupToggle composeId={composeId} groupId={groupId} />}
|
||||||
autoFocus
|
autoFocus
|
||||||
|
streak={userStreak}
|
||||||
/>
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
@ -135,4 +140,4 @@ const ComposeFormGroupToggle: React.FC<IComposeFormGroupToggle> = ({ composeId,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ComposeModal;
|
export default ComposeModal;
|
|
@ -0,0 +1,135 @@
|
||||||
|
import flameIcon from '@tabler/icons/filled/flame.svg';
|
||||||
|
import React from 'react';
|
||||||
|
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import StillImage from 'soapbox/components/still-image.tsx';
|
||||||
|
import Avatar from 'soapbox/components/ui/avatar.tsx';
|
||||||
|
import { Card, CardBody } from 'soapbox/components/ui/card.tsx';
|
||||||
|
import HStack from 'soapbox/components/ui/hstack.tsx';
|
||||||
|
import Icon from 'soapbox/components/ui/icon.tsx';
|
||||||
|
import Modal from 'soapbox/components/ui/modal.tsx';
|
||||||
|
import Stack from 'soapbox/components/ui/stack.tsx';
|
||||||
|
import Text from 'soapbox/components/ui/text.tsx';
|
||||||
|
import VerificationBadge from 'soapbox/components/verification-badge.tsx';
|
||||||
|
import ActionButton from 'soapbox/features/ui/components/action-button.tsx';
|
||||||
|
import { useOwnAccount } from 'soapbox/hooks/useOwnAccount.ts';
|
||||||
|
import { emojifyText } from 'soapbox/utils/emojify.tsx';
|
||||||
|
import { shortNumberFormat } from 'soapbox/utils/numbers.tsx';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
streak: { id: 'account.streak', defaultMessage: 'Day Streak' },
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IStreakModal {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StreakModal: React.FC<IStreakModal> = ({ onClose }) => {
|
||||||
|
const { account } = useOwnAccount();
|
||||||
|
const intl = useIntl();
|
||||||
|
if (!account) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<HStack alignItems='center' justifyContent='center' space={1} className='my-6 -mr-8'>
|
||||||
|
<Text weight='bold' size='2xl' className='text-black'>
|
||||||
|
<FormattedMessage id='streak_modal.title' defaultMessage="You've unlocked a" />
|
||||||
|
</Text>
|
||||||
|
<Text theme='primary'>
|
||||||
|
<Icon src={flameIcon} className='size-6' />
|
||||||
|
</Text>
|
||||||
|
<Text weight='bold' size='2xl' className='text-black'>
|
||||||
|
<FormattedMessage id='streak_modal.sub' defaultMessage='streak!' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
}
|
||||||
|
onClose={onClose}
|
||||||
|
>
|
||||||
|
<div className='mx-auto'>
|
||||||
|
<Card className='relative isolate mx-auto my-4 w-80 overflow-hidden border border-gray-200' rounded slim>
|
||||||
|
<CardBody className='relative'>
|
||||||
|
<div className='relative h-14 overflow-hidden bg-gray-200'>
|
||||||
|
<StillImage src={account.header} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Stack space={2} className='-mt-12 px-3 pb-3'>
|
||||||
|
<HStack justifyContent='between'>
|
||||||
|
<Link to={`/@${account.acct}`} title={account.acct}>
|
||||||
|
<Avatar src={account.avatar} size={80} className='size-20 overflow-hidden bg-gray-50 ring-2 ring-white' />
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className='mt-2'>
|
||||||
|
<ActionButton account={account} small />
|
||||||
|
</div>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Stack>
|
||||||
|
<Link to={`/@${account.acct}`}>
|
||||||
|
<HStack space={1} alignItems='center'>
|
||||||
|
<Text size='lg' weight='bold' truncate>
|
||||||
|
{emojifyText(account.display_name, account.emojis)}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{account.verified && <VerificationBadge />}
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<HStack alignItems='center' space={3}>
|
||||||
|
{account.followers_count >= 0 && (
|
||||||
|
<Link to={`/@${account.acct}/followers`} title={intl.formatNumber(account.followers_count)}>
|
||||||
|
<HStack alignItems='center' space={1}>
|
||||||
|
<Text theme='primary' weight='bold' size='sm'>
|
||||||
|
{shortNumberFormat(account.followers_count)}
|
||||||
|
</Text>
|
||||||
|
<Text weight='bold' size='sm'>
|
||||||
|
<FormattedMessage id='account.followers' defaultMessage='Followers' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{account.following_count >= 0 && (
|
||||||
|
<Link to={`/@${account.acct}/following`} title={intl.formatNumber(account.following_count)}>
|
||||||
|
<HStack alignItems='center' space={1}>
|
||||||
|
<Text theme='primary' weight='bold' size='sm'>
|
||||||
|
{shortNumberFormat(account.following_count)}
|
||||||
|
</Text>
|
||||||
|
<Text weight='bold' size='sm'>
|
||||||
|
<FormattedMessage id='account.follows' defaultMessage='Following' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HStack alignItems='center'>
|
||||||
|
<Text theme='primary'>
|
||||||
|
<span role='img' aria-label={intl.formatMessage(messages.streak)}>
|
||||||
|
<Icon src={flameIcon} className='size-4' />
|
||||||
|
</span>
|
||||||
|
</Text>
|
||||||
|
<Text weight='bold' size='sm' className='text-black'>
|
||||||
|
{account.ditto?.streak?.days > 0 ?
|
||||||
|
shortNumberFormat(account.ditto.streak.days) :
|
||||||
|
<FormattedMessage id='streak_modal.streak_count' defaultMessage='1' />}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<HStack justifyContent='center'>
|
||||||
|
<Text className='my-6'>
|
||||||
|
<FormattedMessage id='streak_modal.message' defaultMessage='Post every day to keep it going.' />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StreakModal;
|
|
@ -181,3 +181,4 @@ export const ZapsModal = lazy(() => import('soapbox/features/ui/components/modal
|
||||||
export const ZapSplitModal = lazy(() => import('soapbox/features/ui/components/modals/zap-split/zap-split-modal.tsx'));
|
export const ZapSplitModal = lazy(() => import('soapbox/features/ui/components/modals/zap-split/zap-split-modal.tsx'));
|
||||||
export const CaptchaModal = lazy(() => import('soapbox/features/ui/components/modals/captcha-modal/captcha-modal.tsx'));
|
export const CaptchaModal = lazy(() => import('soapbox/features/ui/components/modals/captcha-modal/captcha-modal.tsx'));
|
||||||
export const NostrBunkerLogin = lazy(() => import('soapbox/features/nostr/nostr-bunker-login.tsx'));
|
export const NostrBunkerLogin = lazy(() => import('soapbox/features/nostr/nostr-bunker-login.tsx'));
|
||||||
|
export const StreakModal = lazy(() => import('soapbox/features/ui/components/modals/streak-modal.tsx'));
|
||||||
|
|
|
@ -1597,6 +1597,10 @@
|
||||||
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
|
"status_list.queue_label": "Click to see {count} new {count, plural, one {post} other {posts}}",
|
||||||
"statuses.quote_tombstone": "Post is unavailable.",
|
"statuses.quote_tombstone": "Post is unavailable.",
|
||||||
"statuses.tombstone": "One or more posts are unavailable.",
|
"statuses.tombstone": "One or more posts are unavailable.",
|
||||||
|
"streak_modal.message": "Post every day to keep it going.",
|
||||||
|
"streak_modal.streak_count": "1",
|
||||||
|
"streak_modal.sub": "streak!",
|
||||||
|
"streak_modal.title": "You've unlocked a",
|
||||||
"streamfield.add": "Add",
|
"streamfield.add": "Add",
|
||||||
"streamfield.remove": "Remove",
|
"streamfield.remove": "Remove",
|
||||||
"suggestions.dismiss": "Dismiss suggestion",
|
"suggestions.dismiss": "Dismiss suggestion",
|
||||||
|
|
|
@ -1017,6 +1017,12 @@ const getInstanceFeatures = (instance: InstanceV1 | InstanceV2) => {
|
||||||
*/
|
*/
|
||||||
spoilers: v.software !== TRUTHSOCIAL,
|
spoilers: v.software !== TRUTHSOCIAL,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can view user streaks.
|
||||||
|
* @see GET /api/v1/accounts/verify_credentials
|
||||||
|
*/
|
||||||
|
streak: v.software === DITTO,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can display suggested accounts.
|
* Can display suggested accounts.
|
||||||
* @see {@link https://docs.joinmastodon.org/methods/suggestions/}
|
* @see {@link https://docs.joinmastodon.org/methods/suggestions/}
|
||||||
|
|
Ładowanie…
Reference in New Issue