[Glitch] Profile redesign: Nudge to add featured tags

Port 931da0c327 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo
2026-03-23 10:07:53 +01:00
committed by Claire
parent 60236bffe6
commit 5bc6630132
8 changed files with 165 additions and 31 deletions

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ import { defineMessages, useIntl } from 'react-intl';
import { Combobox } from '@/flavours/glitch/components/form_fields';
import { useSearchTags } from '@/flavours/glitch/hooks/useSearchTags';
import type { TagSearchResult } from '@/flavours/glitch/hooks/useSearchTags';
import { addFeaturedTag } from '@/flavours/glitch/reducers/slices/profile_edit';
import { addFeaturedTags } from '@/flavours/glitch/reducers/slices/profile_edit';
import { useAppDispatch } from '@/flavours/glitch/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],
);

View File

@@ -9,7 +9,7 @@ import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import type { TagData } from '@/flavours/glitch/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} />;
};

View File

@@ -18,7 +18,10 @@ import { LoadingIndicator } from '@/flavours/glitch/components/loading_indicator
import { RemoteHint } from '@/flavours/glitch/components/remote_hint';
import StatusList from '@/flavours/glitch/components/status_list';
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
import { useAccountId } from '@/flavours/glitch/hooks/useAccountId';
import {
useAccountId,
useCurrentAccountId,
} from '@/flavours/glitch/hooks/useAccountId';
import { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibility';
import { selectTimelineByKey } from '@/flavours/glitch/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/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 />}
</>
);
};

View File

@@ -49,8 +49,12 @@
}
}
.tagsWrapper {
.tagsWrapper,
.tagSuggestions {
margin: 0 24px 8px;
}
.tagsWrapper {
display: flex;
flex-wrap: nowrap;
justify-content: flex-start;

View File

@@ -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 '@/flavours/glitch/actions/featured_tags';
import { Callout } from '@/flavours/glitch/components/callout';
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
import { useDismissible } from '@/flavours/glitch/hooks/useDismissible';
import {
fetchProfile,
fetchSuggestedTags,
addFeaturedTags,
} from '@/flavours/glitch/reducers/slices/profile_edit';
import { useAppSelector, useAppDispatch } from '@/flavours/glitch/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 youve posted about {items}. Add these as featured hashtags?'
values={{
items: (
<FormattedList
value={suggestedTags.map(({ name }) => `#${name}`)}
/>
),
}}
/>
</Callout>
);
};

View File

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