mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 11:11:11 +02:00
Profile redesign: Nudge to add featured tags (#38315)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { FC, ReactNode } from 'react';
|
||||
import type { FC, ReactElement, ReactNode } from 'react';
|
||||
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
@@ -19,7 +19,7 @@ import classes from './styles.module.css';
|
||||
export interface CalloutProps {
|
||||
variant?:
|
||||
| 'default'
|
||||
// | 'subtle'
|
||||
| 'subtle'
|
||||
| 'feature'
|
||||
| 'inverted'
|
||||
| 'success'
|
||||
@@ -31,9 +31,9 @@ export interface CalloutProps {
|
||||
/** Set to false to hide the icon. */
|
||||
icon?: IconProp | boolean;
|
||||
onPrimary?: () => void;
|
||||
primaryLabel?: string;
|
||||
primaryLabel?: string | ReactElement;
|
||||
onSecondary?: () => void;
|
||||
secondaryLabel?: string;
|
||||
secondaryLabel?: string | ReactElement;
|
||||
onClose?: () => void;
|
||||
id?: string;
|
||||
extraContent?: ReactNode;
|
||||
@@ -41,7 +41,7 @@ export interface CalloutProps {
|
||||
|
||||
const variantClasses = {
|
||||
default: classes.variantDefault as string,
|
||||
// subtle: classes.variantSubtle as string,
|
||||
subtle: classes.variantSubtle as string,
|
||||
feature: classes.variantFeature as string,
|
||||
inverted: classes.variantInverted as string,
|
||||
success: classes.variantSuccess as string,
|
||||
|
||||
@@ -32,6 +32,10 @@
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
margin-bottom: 5px;
|
||||
@@ -51,6 +55,7 @@
|
||||
color: inherit;
|
||||
font-weight: 500;
|
||||
padding: 0;
|
||||
text-wrap: nowrap;
|
||||
text-decoration: underline;
|
||||
transition: color 0.1s ease-in-out;
|
||||
|
||||
@@ -80,14 +85,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* .variantSubtle {
|
||||
.variantSubtle {
|
||||
border: 1px solid var(--color-bg-brand-softer);
|
||||
background-color: var(--color-bg-primary);
|
||||
|
||||
.icon {
|
||||
background-color: var(--color-bg-brand-softer);
|
||||
}
|
||||
} */
|
||||
}
|
||||
|
||||
.variantFeature {
|
||||
background-color: var(--color-bg-brand-base);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Combobox } from '@/mastodon/components/form_fields';
|
||||
import { useSearchTags } from '@/mastodon/hooks/useSearchTags';
|
||||
import type { TagSearchResult } from '@/mastodon/hooks/useSearchTags';
|
||||
import { addFeaturedTag } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { addFeaturedTags } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppDispatch } from '@/mastodon/store';
|
||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||
|
||||
@@ -47,7 +47,7 @@ export const AccountEditTagSearch: FC = () => {
|
||||
(item: TagSearchResult) => {
|
||||
resetSearch();
|
||||
setQuery('');
|
||||
void dispatch(addFeaturedTag({ name: item.name }));
|
||||
void dispatch(addFeaturedTags({ names: [item.name] }));
|
||||
},
|
||||
[dispatch, resetSearch],
|
||||
);
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useAccount } from '@/mastodon/hooks/useAccount';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import type { TagData } from '@/mastodon/reducers/slices/profile_edit';
|
||||
import {
|
||||
addFeaturedTag,
|
||||
addFeaturedTags,
|
||||
deleteFeaturedTag,
|
||||
fetchProfile,
|
||||
fetchSuggestedTags,
|
||||
@@ -128,7 +128,7 @@ const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const handleAddTag = useCallback(() => {
|
||||
void dispatch(addFeaturedTag({ name }));
|
||||
void dispatch(addFeaturedTags({ names: [name] }));
|
||||
}, [dispatch, name]);
|
||||
return <Tag name={name} onClick={handleAddTag} disabled={disabled} />;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,10 @@ import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
|
||||
import { RemoteHint } from '@/mastodon/components/remote_hint';
|
||||
import StatusList from '@/mastodon/components/status_list';
|
||||
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
|
||||
import { useAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import {
|
||||
useAccountId,
|
||||
useCurrentAccountId,
|
||||
} from '@/mastodon/hooks/useAccountId';
|
||||
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
|
||||
import { selectTimelineByKey } from '@/mastodon/selectors/timelines';
|
||||
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||
@@ -34,6 +37,7 @@ import {
|
||||
usePinnedStatusIds,
|
||||
} from './pinned_statuses';
|
||||
import classes from './styles.module.scss';
|
||||
import { TagSuggestions } from './tags_suggestions';
|
||||
|
||||
const emptyList = ImmutableList<string>();
|
||||
|
||||
@@ -135,6 +139,7 @@ const Prepend: FC<{
|
||||
accountId: string;
|
||||
forceEmpty: boolean;
|
||||
}> = ({ forceEmpty, accountId }) => {
|
||||
const me = useCurrentAccountId();
|
||||
if (forceEmpty) {
|
||||
return <AccountHeader accountId={accountId} hideTabs />;
|
||||
}
|
||||
@@ -144,6 +149,7 @@ const Prepend: FC<{
|
||||
<AccountHeader accountId={accountId} hideTabs />
|
||||
<AccountFilters />
|
||||
<FeaturedTags accountId={accountId} />
|
||||
{me === accountId && <TagSuggestions />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -49,8 +49,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
.tagsWrapper,
|
||||
.tagSuggestions {
|
||||
margin: 0 24px 8px;
|
||||
}
|
||||
|
||||
.tagsWrapper {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { FC } from 'react';
|
||||
import { useEffect, useCallback, useState } from 'react';
|
||||
|
||||
import { FormattedMessage, FormattedList } from 'react-intl';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fetchFeaturedTags } from '@/mastodon/actions/featured_tags';
|
||||
import { Callout } from '@/mastodon/components/callout';
|
||||
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
|
||||
import { useDismissible } from '@/mastodon/hooks/useDismissible';
|
||||
import {
|
||||
fetchProfile,
|
||||
fetchSuggestedTags,
|
||||
addFeaturedTags,
|
||||
} from '@/mastodon/reducers/slices/profile_edit';
|
||||
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
|
||||
|
||||
import classes from './styles.module.scss';
|
||||
|
||||
const MAX_SUGGESTED_TAGS = 3;
|
||||
|
||||
export const TagSuggestions: FC = () => {
|
||||
const { dismiss, wasDismissed } = useDismissible(
|
||||
'profile/featured_tag_suggestions',
|
||||
);
|
||||
|
||||
const suggestedTags = useAppSelector((state) =>
|
||||
state.profileEdit.tagSuggestions?.slice(0, MAX_SUGGESTED_TAGS),
|
||||
);
|
||||
const existingTagCount = useAppSelector(
|
||||
(state) => state.profileEdit.profile?.featuredTags.length,
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const isLoading = !suggestedTags || existingTagCount === undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
void dispatch(fetchProfile());
|
||||
void dispatch(fetchSuggestedTags());
|
||||
}
|
||||
}, [dispatch, isLoading]);
|
||||
|
||||
const me = useCurrentAccountId();
|
||||
const [showSuccessNotice, setSuccessNotice] = useState(false);
|
||||
|
||||
const handleAdd = useCallback(() => {
|
||||
if (!suggestedTags?.length || !me) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addTags = async () => {
|
||||
await dispatch(
|
||||
addFeaturedTags({ names: suggestedTags.map((tag) => tag.name) }),
|
||||
);
|
||||
await dispatch(fetchFeaturedTags({ accountId: me }));
|
||||
setSuccessNotice(true);
|
||||
dismiss();
|
||||
};
|
||||
void addTags();
|
||||
}, [dismiss, dispatch, me, suggestedTags]);
|
||||
|
||||
const handleDismissSuccessNotice = useCallback(() => {
|
||||
setSuccessNotice(false);
|
||||
}, []);
|
||||
|
||||
if (showSuccessNotice) {
|
||||
return (
|
||||
<Callout
|
||||
variant='subtle'
|
||||
className={classes.tagSuggestions}
|
||||
onClose={handleDismissSuccessNotice}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.added'
|
||||
defaultMessage='Manage your featured hashtags at any time under <link>Edit Profile > Featured hashtags</link>.'
|
||||
values={{
|
||||
link: (chunks) => <Link to='/profile/featured_tags'>{chunks}</Link>,
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isLoading ||
|
||||
!suggestedTags.length ||
|
||||
existingTagCount > 0 ||
|
||||
wasDismissed
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Callout
|
||||
id='featured_tags.suggestions'
|
||||
variant='subtle'
|
||||
className={classes.tagSuggestions}
|
||||
onPrimary={handleAdd}
|
||||
primaryLabel={
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.add'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
}
|
||||
onSecondary={dismiss}
|
||||
secondaryLabel={
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions.dismiss'
|
||||
defaultMessage='No thanks'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='featured_tags.suggestions'
|
||||
defaultMessage='Lately you’ve posted about {items}. Add these as featured hashtags?'
|
||||
values={{
|
||||
items: (
|
||||
<FormattedList
|
||||
value={suggestedTags.map(({ name }) => `#${name}`)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
@@ -628,6 +628,10 @@
|
||||
"featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}",
|
||||
"featured_carousel.slide": "Post {current, number} of {max, number}",
|
||||
"featured_tags.more_items": "+{count}",
|
||||
"featured_tags.suggestions": "Lately you’ve posted about {items}. Add these as featured hashtags?",
|
||||
"featured_tags.suggestions.add": "Add",
|
||||
"featured_tags.suggestions.added": "Manage your featured hashtags at any time under <link>Edit Profile > Featured hashtags</link>.",
|
||||
"featured_tags.suggestions.dismiss": "No thanks",
|
||||
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
|
||||
"filter_modal.added.context_mismatch_title": "Context mismatch!",
|
||||
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
|
||||
|
||||
@@ -104,24 +104,24 @@ const profileEditSlice = createSlice({
|
||||
state.isPending = false;
|
||||
});
|
||||
|
||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
||||
builder.addCase(addFeaturedTags.pending, (state) => {
|
||||
state.isPending = true;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.rejected, (state) => {
|
||||
builder.addCase(addFeaturedTags.rejected, (state) => {
|
||||
state.isPending = false;
|
||||
});
|
||||
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
||||
builder.addCase(addFeaturedTags.fulfilled, (state, action) => {
|
||||
if (!state.profile) {
|
||||
return;
|
||||
}
|
||||
|
||||
state.profile.featuredTags = [
|
||||
...state.profile.featuredTags,
|
||||
transformTag(action.payload),
|
||||
...action.payload.map(transformTag),
|
||||
].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
if (state.tagSuggestions) {
|
||||
state.tagSuggestions = state.tagSuggestions.filter(
|
||||
(tag) => tag.name !== action.meta.arg.name,
|
||||
(tag) => !action.meta.arg.names.includes(tag.name),
|
||||
);
|
||||
}
|
||||
state.isPending = false;
|
||||
@@ -350,20 +350,11 @@ export const fetchSuggestedTags = createDataLoadingThunk(
|
||||
{ useLoadingBar: false },
|
||||
);
|
||||
|
||||
export const addFeaturedTag = createDataLoadingThunk(
|
||||
export const addFeaturedTags = createDataLoadingThunk(
|
||||
`${profileEditSlice.name}/addFeaturedTag`,
|
||||
({ name }: { name: string }) => apiPostFeaturedTag(name),
|
||||
{
|
||||
condition(arg, { getState }) {
|
||||
const state = getState();
|
||||
return (
|
||||
!!state.profileEdit.profile &&
|
||||
!state.profileEdit.profile.featuredTags.some(
|
||||
(tag) => tag.name === arg.name,
|
||||
)
|
||||
);
|
||||
},
|
||||
},
|
||||
({ names }: { names: string[] }) =>
|
||||
Promise.all(names.map((n) => apiPostFeaturedTag(n))),
|
||||
{ useLoadingBar: false },
|
||||
);
|
||||
|
||||
export const deleteFeaturedTag = createDataLoadingThunk(
|
||||
|
||||
Reference in New Issue
Block a user