mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
[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:
@@ -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 '@/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],
|
||||
);
|
||||
|
||||
@@ -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} />;
|
||||
};
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 '@/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 you’ve posted about {items}. Add these as featured hashtags?'
|
||||
values={{
|
||||
items: (
|
||||
<FormattedList
|
||||
value={suggestedTags.map(({ name }) => `#${name}`)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Callout>
|
||||
);
|
||||
};
|
||||
@@ -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