diff --git a/src/components/ICONS.jsx b/src/components/ICONS.jsx index 99577626..224b5737 100644 --- a/src/components/ICONS.jsx +++ b/src/components/ICONS.jsx @@ -178,5 +178,6 @@ export const ICONS = { schedule: () => import('@iconify-icons/mingcute/calendar-time-add-line'), month: () => import('@iconify-icons/mingcute/calendar-month-line'), day: () => import('@iconify-icons/mingcute/calendar-day-line'), + 'fav-boost-celebrate': () => import('@iconify-icons/mingcute/celebrate-line'), camera: () => import('@iconify-icons/mingcute/camera-line'), }; diff --git a/src/components/status.css b/src/components/status.css index 26f5d834..e53dd70d 100644 --- a/src/components/status.css +++ b/src/components/status.css @@ -2252,6 +2252,30 @@ a.card:is(:hover, :focus):visited { .status .action > button.plain.favourite-button.checked .icon { animation: hearted 1s ease-out; } +.status .action > button.plain.favourite-boost-button:not(:disabled):is(:hover, :focus) { + color: var(--celebrate-color); +} +.status .action > button.plain.favourite-boost-button.checked { + color: var(--celebrate-color); + border-color: var(--celebrate-color); +} +@keyframes favouriteAndBoosted { + 15% { + transform: scale(1.25) translateY(-1px); + } + 30% { + transform: scale(1) rotate(10deg); + } + 45% { + transform: scale(1.5) translateY(-2px) rotate(-10deg); + } + 100% { + transform: scale(1); + } +} +.status .action > button.plain.favourite-boost-button.checked .icon { + animation: favouriteAndBoosted 1s ease-out; +} .status .action > button.plain.bookmark-button.checked { color: var(--link-color); border-color: var(--link-color); diff --git a/src/components/status.jsx b/src/components/status.jsx index 44aad700..55fbda89 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -787,6 +787,81 @@ function Status({ } }; + const favouriteAndBoostStatus = async () => { + if (!sameInstance || !authenticated) { + alert(unauthInteractionErrorMessage); + return false; + } + try { + // Case 1: Neither liked nor boosted - add both + // Case 2: Either liked or boosted - preserve existing and add other + // Case 3: Both liked and boosted - remove both + const newFavourited = !(favourited && reblogged); + const newReblogged = !(favourited && reblogged); + + states.statuses[sKey] = { + ...status, + favourited: newFavourited, + favouritesCount: favouritesCount + (newFavourited ? 1 : -1), + reblogged: newReblogged, + reblogsCount: reblogsCount + (newReblogged ? 1 : -1), + }; + + // Execute actions based on state changes + const actions = []; + if (newFavourited !== favourited) { + actions.push( + newFavourited + ? masto.v1.statuses.$select(id).favourite() + : masto.v1.statuses.$select(id).unfavourite() + ); + } + if (newReblogged !== reblogged) { + actions.push( + newReblogged + ? masto.v1.statuses.$select(id).reblog() + : masto.v1.statuses.$select(id).unreblog() + ); + } + + const results = await Promise.all(actions); + + // If we're turning off both actions, refresh the status to ensure UI sync + if (!newFavourited && !newReblogged) { + const refreshedStatus = await masto.v1.statuses.$select(id).fetch(); + saveStatus(refreshedStatus, instance); + } else if (results.length) { + const lastResult = results[results.length - 1]; + saveStatus(lastResult, instance); + } + + return true; + } catch (e) { + console.error(e); + // Revert optimistic update + states.statuses[sKey] = status; + return false; + } + }; + + const favouriteAndBoostStatusNotify = async () => { + try { + const success = await favouriteAndBoostStatus(); + if (success) { + showToast( + !favourited && !reblogged + ? t`Liked and boosted!` + : favourited && reblogged + ? t`Removed like and boost` + : t`Updated status` + ); + } + } catch (e) { + console.error(e); + showToast(t`Unable to update status`); + } + }; + const favouriteStatus = async () => { if (!sameInstance || !authenticated) { alert(unauthInteractionErrorMessage); @@ -2491,6 +2566,17 @@ function Status({ onClick={favouriteStatus} /> +
+ +
{supports('@mastodon/post-bookmark') && (