diff --git a/app/javascript/flavours/glitch/components/callout/index.tsx b/app/javascript/flavours/glitch/components/callout/index.tsx index a9232ec3a7..fe088e2a83 100644 --- a/app/javascript/flavours/glitch/components/callout/index.tsx +++ b/app/javascript/flavours/glitch/components/callout/index.tsx @@ -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, diff --git a/app/javascript/flavours/glitch/components/callout/styles.module.css b/app/javascript/flavours/glitch/components/callout/styles.module.css index 7f33c96eae..14003ccf5d 100644 --- a/app/javascript/flavours/glitch/components/callout/styles.module.css +++ b/app/javascript/flavours/glitch/components/callout/styles.module.css @@ -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); diff --git a/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx b/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx index 0752c3fa5d..7708012f94 100644 --- a/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx +++ b/app/javascript/flavours/glitch/features/account_edit/components/tag_search.tsx @@ -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], ); diff --git a/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx b/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx index 2eaaca3cf2..701f68ca72 100644 --- a/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx +++ b/app/javascript/flavours/glitch/features/account_edit/featured_tags.tsx @@ -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 ; }; diff --git a/app/javascript/flavours/glitch/features/account_timeline/v2/index.tsx b/app/javascript/flavours/glitch/features/account_timeline/v2/index.tsx index 3bf8ee67f6..295cb1427d 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/v2/index.tsx +++ b/app/javascript/flavours/glitch/features/account_timeline/v2/index.tsx @@ -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(); @@ -135,6 +139,7 @@ const Prepend: FC<{ accountId: string; forceEmpty: boolean; }> = ({ forceEmpty, accountId }) => { + const me = useCurrentAccountId(); if (forceEmpty) { return ; } @@ -144,6 +149,7 @@ const Prepend: FC<{ + {me === accountId && } ); }; diff --git a/app/javascript/flavours/glitch/features/account_timeline/v2/styles.module.scss b/app/javascript/flavours/glitch/features/account_timeline/v2/styles.module.scss index b39892beec..1df19feb1d 100644 --- a/app/javascript/flavours/glitch/features/account_timeline/v2/styles.module.scss +++ b/app/javascript/flavours/glitch/features/account_timeline/v2/styles.module.scss @@ -49,8 +49,12 @@ } } -.tagsWrapper { +.tagsWrapper, +.tagSuggestions { margin: 0 24px 8px; +} + +.tagsWrapper { display: flex; flex-wrap: nowrap; justify-content: flex-start; diff --git a/app/javascript/flavours/glitch/features/account_timeline/v2/tags_suggestions.tsx b/app/javascript/flavours/glitch/features/account_timeline/v2/tags_suggestions.tsx new file mode 100644 index 0000000000..138ccc4cf2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account_timeline/v2/tags_suggestions.tsx @@ -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 ( + + {chunks}, + }} + /> + + ); + } + + if ( + isLoading || + !suggestedTags.length || + existingTagCount > 0 || + wasDismissed + ) { + return null; + } + + return ( + + } + onSecondary={dismiss} + secondaryLabel={ + + } + > + `#${name}`)} + /> + ), + }} + /> + + ); +}; diff --git a/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts b/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts index c42951c605..aa7f67fd50 100644 --- a/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts +++ b/app/javascript/flavours/glitch/reducers/slices/profile_edit.ts @@ -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(