[Glitch] Profile redesign: Nudge to add featured tags
Port 931da0c3277b353a74f4ef74155c90badc8be0f3 to glitch-soc Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
parent
60236bffe6
commit
5bc6630132
@ -1,4 +1,4 @@
|
|||||||
import type { FC, ReactNode } from 'react';
|
import type { FC, ReactElement, ReactNode } from 'react';
|
||||||
|
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ import classes from './styles.module.css';
|
|||||||
export interface CalloutProps {
|
export interface CalloutProps {
|
||||||
variant?:
|
variant?:
|
||||||
| 'default'
|
| 'default'
|
||||||
// | 'subtle'
|
| 'subtle'
|
||||||
| 'feature'
|
| 'feature'
|
||||||
| 'inverted'
|
| 'inverted'
|
||||||
| 'success'
|
| 'success'
|
||||||
@ -31,9 +31,9 @@ export interface CalloutProps {
|
|||||||
/** Set to false to hide the icon. */
|
/** Set to false to hide the icon. */
|
||||||
icon?: IconProp | boolean;
|
icon?: IconProp | boolean;
|
||||||
onPrimary?: () => void;
|
onPrimary?: () => void;
|
||||||
primaryLabel?: string;
|
primaryLabel?: string | ReactElement;
|
||||||
onSecondary?: () => void;
|
onSecondary?: () => void;
|
||||||
secondaryLabel?: string;
|
secondaryLabel?: string | ReactElement;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
id?: string;
|
id?: string;
|
||||||
extraContent?: ReactNode;
|
extraContent?: ReactNode;
|
||||||
@ -41,7 +41,7 @@ export interface CalloutProps {
|
|||||||
|
|
||||||
const variantClasses = {
|
const variantClasses = {
|
||||||
default: classes.variantDefault as string,
|
default: classes.variantDefault as string,
|
||||||
// subtle: classes.variantSubtle as string,
|
subtle: classes.variantSubtle as string,
|
||||||
feature: classes.variantFeature as string,
|
feature: classes.variantFeature as string,
|
||||||
inverted: classes.variantInverted as string,
|
inverted: classes.variantInverted as string,
|
||||||
success: classes.variantSuccess as string,
|
success: classes.variantSuccess as string,
|
||||||
|
|||||||
@ -32,6 +32,10 @@
|
|||||||
.body {
|
.body {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
@ -51,6 +55,7 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
text-wrap: nowrap;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
transition: color 0.1s ease-in-out;
|
transition: color 0.1s ease-in-out;
|
||||||
|
|
||||||
@ -80,14 +85,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .variantSubtle {
|
.variantSubtle {
|
||||||
border: 1px solid var(--color-bg-brand-softer);
|
border: 1px solid var(--color-bg-brand-softer);
|
||||||
background-color: var(--color-bg-primary);
|
background-color: var(--color-bg-primary);
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
background-color: var(--color-bg-brand-softer);
|
background-color: var(--color-bg-brand-softer);
|
||||||
}
|
}
|
||||||
} */
|
}
|
||||||
|
|
||||||
.variantFeature {
|
.variantFeature {
|
||||||
background-color: var(--color-bg-brand-base);
|
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 { Combobox } from '@/flavours/glitch/components/form_fields';
|
||||||
import { useSearchTags } from '@/flavours/glitch/hooks/useSearchTags';
|
import { useSearchTags } from '@/flavours/glitch/hooks/useSearchTags';
|
||||||
import type { TagSearchResult } 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 { useAppDispatch } from '@/flavours/glitch/store';
|
||||||
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
import SearchIcon from '@/material-icons/400-24px/search.svg?react';
|
||||||
|
|
||||||
@ -47,7 +47,7 @@ export const AccountEditTagSearch: FC = () => {
|
|||||||
(item: TagSearchResult) => {
|
(item: TagSearchResult) => {
|
||||||
resetSearch();
|
resetSearch();
|
||||||
setQuery('');
|
setQuery('');
|
||||||
void dispatch(addFeaturedTag({ name: item.name }));
|
void dispatch(addFeaturedTags({ names: [item.name] }));
|
||||||
},
|
},
|
||||||
[dispatch, resetSearch],
|
[dispatch, resetSearch],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { useAccount } from '@/flavours/glitch/hooks/useAccount';
|
|||||||
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
import { useCurrentAccountId } from '@/flavours/glitch/hooks/useAccountId';
|
||||||
import type { TagData } from '@/flavours/glitch/reducers/slices/profile_edit';
|
import type { TagData } from '@/flavours/glitch/reducers/slices/profile_edit';
|
||||||
import {
|
import {
|
||||||
addFeaturedTag,
|
addFeaturedTags,
|
||||||
deleteFeaturedTag,
|
deleteFeaturedTag,
|
||||||
fetchProfile,
|
fetchProfile,
|
||||||
fetchSuggestedTags,
|
fetchSuggestedTags,
|
||||||
@ -128,7 +128,7 @@ const SuggestedTag: FC<{ name: string; disabled?: boolean }> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const handleAddTag = useCallback(() => {
|
const handleAddTag = useCallback(() => {
|
||||||
void dispatch(addFeaturedTag({ name }));
|
void dispatch(addFeaturedTags({ names: [name] }));
|
||||||
}, [dispatch, name]);
|
}, [dispatch, name]);
|
||||||
return <Tag name={name} onClick={handleAddTag} disabled={disabled} />;
|
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 { RemoteHint } from '@/flavours/glitch/components/remote_hint';
|
||||||
import StatusList from '@/flavours/glitch/components/status_list';
|
import StatusList from '@/flavours/glitch/components/status_list';
|
||||||
import BundleColumnError from '@/flavours/glitch/features/ui/components/bundle_column_error';
|
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 { useAccountVisibility } from '@/flavours/glitch/hooks/useAccountVisibility';
|
||||||
import { selectTimelineByKey } from '@/flavours/glitch/selectors/timelines';
|
import { selectTimelineByKey } from '@/flavours/glitch/selectors/timelines';
|
||||||
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/store';
|
||||||
@ -34,6 +37,7 @@ import {
|
|||||||
usePinnedStatusIds,
|
usePinnedStatusIds,
|
||||||
} from './pinned_statuses';
|
} from './pinned_statuses';
|
||||||
import classes from './styles.module.scss';
|
import classes from './styles.module.scss';
|
||||||
|
import { TagSuggestions } from './tags_suggestions';
|
||||||
|
|
||||||
const emptyList = ImmutableList<string>();
|
const emptyList = ImmutableList<string>();
|
||||||
|
|
||||||
@ -135,6 +139,7 @@ const Prepend: FC<{
|
|||||||
accountId: string;
|
accountId: string;
|
||||||
forceEmpty: boolean;
|
forceEmpty: boolean;
|
||||||
}> = ({ forceEmpty, accountId }) => {
|
}> = ({ forceEmpty, accountId }) => {
|
||||||
|
const me = useCurrentAccountId();
|
||||||
if (forceEmpty) {
|
if (forceEmpty) {
|
||||||
return <AccountHeader accountId={accountId} hideTabs />;
|
return <AccountHeader accountId={accountId} hideTabs />;
|
||||||
}
|
}
|
||||||
@ -144,6 +149,7 @@ const Prepend: FC<{
|
|||||||
<AccountHeader accountId={accountId} hideTabs />
|
<AccountHeader accountId={accountId} hideTabs />
|
||||||
<AccountFilters />
|
<AccountFilters />
|
||||||
<FeaturedTags accountId={accountId} />
|
<FeaturedTags accountId={accountId} />
|
||||||
|
{me === accountId && <TagSuggestions />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -49,8 +49,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tagsWrapper {
|
.tagsWrapper,
|
||||||
|
.tagSuggestions {
|
||||||
margin: 0 24px 8px;
|
margin: 0 24px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagsWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: flex-start;
|
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;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.addCase(addFeaturedTag.pending, (state) => {
|
builder.addCase(addFeaturedTags.pending, (state) => {
|
||||||
state.isPending = true;
|
state.isPending = true;
|
||||||
});
|
});
|
||||||
builder.addCase(addFeaturedTag.rejected, (state) => {
|
builder.addCase(addFeaturedTags.rejected, (state) => {
|
||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
});
|
});
|
||||||
builder.addCase(addFeaturedTag.fulfilled, (state, action) => {
|
builder.addCase(addFeaturedTags.fulfilled, (state, action) => {
|
||||||
if (!state.profile) {
|
if (!state.profile) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
state.profile.featuredTags = [
|
state.profile.featuredTags = [
|
||||||
...state.profile.featuredTags,
|
...state.profile.featuredTags,
|
||||||
transformTag(action.payload),
|
...action.payload.map(transformTag),
|
||||||
].toSorted((a, b) => a.name.localeCompare(b.name));
|
].toSorted((a, b) => a.name.localeCompare(b.name));
|
||||||
if (state.tagSuggestions) {
|
if (state.tagSuggestions) {
|
||||||
state.tagSuggestions = state.tagSuggestions.filter(
|
state.tagSuggestions = state.tagSuggestions.filter(
|
||||||
(tag) => tag.name !== action.meta.arg.name,
|
(tag) => !action.meta.arg.names.includes(tag.name),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
state.isPending = false;
|
state.isPending = false;
|
||||||
@ -350,20 +350,11 @@ export const fetchSuggestedTags = createDataLoadingThunk(
|
|||||||
{ useLoadingBar: false },
|
{ useLoadingBar: false },
|
||||||
);
|
);
|
||||||
|
|
||||||
export const addFeaturedTag = createDataLoadingThunk(
|
export const addFeaturedTags = createDataLoadingThunk(
|
||||||
`${profileEditSlice.name}/addFeaturedTag`,
|
`${profileEditSlice.name}/addFeaturedTag`,
|
||||||
({ name }: { name: string }) => apiPostFeaturedTag(name),
|
({ names }: { names: string[] }) =>
|
||||||
{
|
Promise.all(names.map((n) => apiPostFeaturedTag(n))),
|
||||||
condition(arg, { getState }) {
|
{ useLoadingBar: false },
|
||||||
const state = getState();
|
|
||||||
return (
|
|
||||||
!!state.profileEdit.profile &&
|
|
||||||
!state.profileEdit.profile.featuredTags.some(
|
|
||||||
(tag) => tag.name === arg.name,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const deleteFeaturedTag = createDataLoadingThunk(
|
export const deleteFeaturedTag = createDataLoadingThunk(
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user