diff --git a/.github/workflows/build-releases.yml b/.github/workflows/build-releases.yml index 7f2875cbcb..487e3fda30 100644 --- a/.github/workflows/build-releases.yml +++ b/.github/workflows/build-releases.yml @@ -9,7 +9,44 @@ permissions: packages: write jobs: + check-latest-stable: + runs-on: ubuntu-latest + outputs: + latest: ${{ steps.check.outputs.is_latest_stable }} + steps: + # Repository needs to be cloned to list branches + - name: Clone repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Check latest stable + shell: bash + id: check + run: | + ref="${GITHUB_REF#refs/tags/}" + + if [[ "$ref" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then + current="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" + else + echo "tag $ref is not semver" + echo "is_latest_stable=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + latest=$(git for-each-ref --format='%(refname:short)' "refs/remotes/origin/stable-*.*" \ + | sed -E 's#^origin/stable-##' \ + | sort -Vr \ + | head -n1) + + if [[ "$current" == "$latest" ]]; then + echo "is_latest_stable=true" >> "$GITHUB_OUTPUT" + else + echo "is_latest_stable=false" >> "$GITHUB_OUTPUT" + fi + build-image: + needs: check-latest-stable uses: ./.github/workflows/build-container-image.yml with: file_to_build: Dockerfile @@ -20,13 +57,14 @@ jobs: # Only tag with latest when ran against the latest stable branch # This needs to be updated after each minor version release flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }} + latest=${{ needs.check-latest-stable.outputs.latest }} tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} secrets: inherit build-image-streaming: + needs: check-latest-stable uses: ./.github/workflows/build-container-image.yml with: file_to_build: streaming/Dockerfile @@ -37,7 +75,7 @@ jobs: # Only tag with latest when ran against the latest stable branch # This needs to be updated after each minor version release flavor: | - latest=${{ startsWith(github.ref, 'refs/tags/v4.5.') }} + latest=${{ needs.check-latest-stable.outputs.latest }} tags: | type=pep440,pattern={{raw}} type=pep440,pattern=v{{major}}.{{minor}} diff --git a/Gemfile.lock b/Gemfile.lock index 8a94dd6075..6b02122c97 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,7 +53,7 @@ GEM erubi (~> 1.11) rails-dom-testing (~> 2.2) rails-html-sanitizer (~> 1.6) - active_model_serializers (0.10.15) + active_model_serializers (0.10.16) actionpack (>= 4.1) activemodel (>= 4.1) case_transform (>= 0.2) @@ -481,7 +481,7 @@ GEM addressable (~> 2.8) nokogiri (~> 1.12) omniauth (~> 2.1) - omniauth-rails_csrf_protection (2.0.0) + omniauth-rails_csrf_protection (2.0.1) actionpack (>= 4.2) omniauth (~> 2.0) omniauth-saml (2.2.4) @@ -839,7 +839,8 @@ GEM stackprof (0.2.27) starry (0.2.0) base64 - stoplight (5.6.0) + stoplight (5.7.0) + concurrent-ruby zeitwerk stringio (3.1.8) strong_migrations (2.5.1) diff --git a/app/javascript/flavours/glitch/components/status/intercept_status_clicks.tsx b/app/javascript/flavours/glitch/components/status/intercept_status_clicks.tsx new file mode 100644 index 0000000000..b0dbc3c693 --- /dev/null +++ b/app/javascript/flavours/glitch/components/status/intercept_status_clicks.tsx @@ -0,0 +1,45 @@ +import { useCallback, useRef } from 'react'; + +export const InterceptStatusClicks: React.FC<{ + onPreventedClick: ( + clickedArea: 'account' | 'post', + event: React.MouseEvent, + ) => void; + children: React.ReactNode; +}> = ({ onPreventedClick, children }) => { + const wrapperRef = useRef(null); + + const handleClick = useCallback( + (e: React.MouseEvent) => { + const clickTarget = e.target as Element; + const allowedElementsSelector = + '.video-player, .audio-player, .media-gallery, .content-warning'; + const allowedElements = wrapperRef.current?.querySelectorAll( + allowedElementsSelector, + ); + const isTargetClickAllowed = + allowedElements && + Array.from(allowedElements).some((element) => { + return clickTarget === element || element.contains(clickTarget); + }); + + if (!isTargetClickAllowed) { + e.preventDefault(); + e.stopPropagation(); + + const wasAccountAreaClicked = !!clickTarget.closest( + 'a.status__display-name', + ); + + onPreventedClick(wasAccountAreaClicked ? 'account' : 'post', e); + } + }, + [onPreventedClick], + ); + + return ( +
+ {children} +
+ ); +}; diff --git a/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx b/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx index 9ca64d40ba..9e03c7e327 100644 --- a/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx +++ b/app/javascript/flavours/glitch/features/annual_report/highlighted_post.tsx @@ -4,10 +4,14 @@ @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ +import type { ComponentPropsWithoutRef } from 'react'; +import { useCallback } from 'react'; + import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; +import { InterceptStatusClicks } from 'flavours/glitch/components/status/intercept_status_clicks'; import { StatusQuoteManager } from 'flavours/glitch/components/status_quoted'; import type { TopStatuses } from 'flavours/glitch/models/annual_report'; import { makeGetStatus } from 'flavours/glitch/selectors'; @@ -29,6 +33,24 @@ export const HighlightedPost: React.FC<{ statusId ? getStatus(state, { id: statusId }) : undefined, ); + const handleClick = useCallback< + ComponentPropsWithoutRef['onPreventedClick'] + >( + (clickedArea) => { + const link: string = + clickedArea === 'account' + ? status.getIn(['account', 'url']) + : status.get('url'); + + if (context === 'standalone') { + window.location.href = link; + } else { + window.open(link, '_blank'); + } + }, + [status, context], + ); + if (!status) { return
; } @@ -72,7 +94,9 @@ export const HighlightedPost: React.FC<{ {context === 'modal' &&

{label}

}
- + + + ); }; diff --git a/app/javascript/flavours/glitch/features/annual_report/index.module.scss b/app/javascript/flavours/glitch/features/annual_report/index.module.scss index 95ebb72729..9e9a6464c1 100644 --- a/app/javascript/flavours/glitch/features/annual_report/index.module.scss +++ b/app/javascript/flavours/glitch/features/annual_report/index.module.scss @@ -10,18 +10,17 @@ $mobile-breakpoint: 540px; } .modalWrapper { - position: absolute; - inset: 0; + box-sizing: border-box; display: flex; flex-direction: column; align-items: center; + width: 100%; padding: 40px; overflow-y: auto; - pointer-events: none; scrollbar-color: var(--color-text-secondary) var(--color-bg-secondary); @media (width < $mobile-breakpoint) { - padding-inline: 10px; + padding: 0; } .loading-indicator .circular-progress { @@ -50,37 +49,51 @@ $mobile-breakpoint: 540px; } .wrapper { + --gradient-strength: 0.4; + + box-sizing: border-box; position: relative; max-width: 600px; padding: 24px; + padding-top: 40px; contain: layout; flex: 0 0 auto; - pointer-events: auto; + pointer-events: all; color: var(--color-text-primary); background: var(--color-bg-primary); background: - radial-gradient(at 40% 87%, #240c9a99 0, transparent 50%), - radial-gradient(at 19% 10%, #6b0c9a99 0, transparent 50%), - radial-gradient(at 90% 27%, #9a0c8299 0, transparent 50%), - radial-gradient(at 16% 95%, #1e948299 0, transparent 50%), - radial-gradient(at 80% 91%, #16dae499 0, transparent 50%) + radial-gradient( + at 10% 27%, + rgba(83, 12, 154, var(--gradient-strength)) 0, + transparent 50% + ), + radial-gradient( + at 91% 10%, + rgba(30, 24, 223, var(--gradient-strength)) 0, + transparent 25% + ), + radial-gradient( + at 10% 91%, + rgba(22, 218, 228, var(--gradient-strength)) 0, + transparent 40% + ), + radial-gradient( + at 75% 87%, + rgba(37, 31, 217, var(--gradient-strength)) 0, + transparent 20% + ), + radial-gradient( + at 84% 60%, + rgba(95, 30, 148, var(--gradient-strength)) 0, + transparent 40% + ) var(--color-bg-primary); border-radius: 40px; @media (width < $mobile-breakpoint) { padding-inline: 12px; padding-bottom: 12px; - border-radius: 28px; - } - - &::after { - content: ''; - position: absolute; - inset: 0; - z-index: -1; - background: inherit; - border-radius: inherit; - filter: blur(20px); + border-radius: 0; } } @@ -92,7 +105,7 @@ $mobile-breakpoint: 540px; font-family: silkscreen-wrapstodon, monospace; font-size: 28px; line-height: 1; - margin-bottom: 8px; + margin-bottom: 4px; padding-inline: 40px; // Prevent overlap with close button @media (width < $mobile-breakpoint) { @@ -116,7 +129,7 @@ $mobile-breakpoint: 540px; .box { position: relative; - padding: 16px; + padding: 24px; border-radius: 16px; background: rgb(from var(--color-bg-primary) r g b / 60%); box-shadow: inset 0 0 0 1px rgb(from var(--color-text-primary) r g b / 40%); @@ -150,7 +163,6 @@ $mobile-breakpoint: 540px; flex-direction: column; justify-content: center; gap: 8px; - padding: 16px; font-size: 14px; text-align: center; text-wrap: balance; @@ -164,6 +176,10 @@ $mobile-breakpoint: 540px; text-transform: uppercase; color: #c2c8ff; font-weight: 500; + + &:last-child { + margin-bottom: -3px; + } } .statLarge { @@ -185,7 +201,7 @@ $mobile-breakpoint: 540px; .mostBoostedPost { padding: 0; - padding-top: 8px; + padding-top: 24px; overflow: hidden; } @@ -260,7 +276,7 @@ $mobile-breakpoint: 540px; display: flex; flex-direction: column; align-items: center; - gap: 12px; + gap: 16px; p { max-width: 460px; diff --git a/app/javascript/flavours/glitch/features/annual_report/index.tsx b/app/javascript/flavours/glitch/features/annual_report/index.tsx index d5133b3cb3..b1d7fc5585 100644 --- a/app/javascript/flavours/glitch/features/annual_report/index.tsx +++ b/app/javascript/flavours/glitch/features/annual_report/index.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import type { FC } from 'react'; -import { defineMessage, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import { useLocation } from 'react-router'; @@ -23,11 +23,6 @@ import { NewPosts } from './new_posts'; const moduleClassNames = classNames.bind(styles); -export const shareMessage = defineMessage({ - id: 'annual_report.summary.share_message', - defaultMessage: 'I got the {archetype} archetype!', -}); - export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({ context = 'standalone', }) => { diff --git a/app/javascript/flavours/glitch/features/annual_report/modal.tsx b/app/javascript/flavours/glitch/features/annual_report/modal.tsx index 262584b7b9..14d13ac958 100644 --- a/app/javascript/flavours/glitch/features/annual_report/modal.tsx +++ b/app/javascript/flavours/glitch/features/annual_report/modal.tsx @@ -1,7 +1,10 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import classNames from 'classnames'; +import { closeModal } from '@/flavours/glitch/actions/modal'; +import { useAppDispatch } from '@/flavours/glitch/store'; + import { AnnualReport } from '.'; import styles from './index.module.scss'; @@ -12,13 +15,30 @@ const AnnualReportModal: React.FC<{ onChangeBackgroundColor('var(--color-bg-media-base)'); }, [onChangeBackgroundColor]); + const dispatch = useAppDispatch(); + const handleCloseModal = useCallback>( + (e) => { + if (e.target === e.currentTarget) + dispatch( + closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false }), + ); + }, + [dispatch], + ); + return ( + // It's fine not to provide a keyboard handler here since there is a global + // [Esc] key listener that will close open modals. + // This onClick handler is needed since the modalWrapper styles overlap the + // default modal backdrop, preventing clicks to pass through. + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
diff --git a/app/javascript/flavours/glitch/features/annual_report/share_button.tsx b/app/javascript/flavours/glitch/features/annual_report/share_button.tsx index 497d15d1e8..652c8af913 100644 --- a/app/javascript/flavours/glitch/features/annual_report/share_button.tsx +++ b/app/javascript/flavours/glitch/features/annual_report/share_button.tsx @@ -1,7 +1,7 @@ import { useCallback } from 'react'; import type { FC } from 'react'; -import { useIntl } from 'react-intl'; +import { defineMessages, useIntl } from 'react-intl'; import { resetCompose, focusCompose } from '@/flavours/glitch/actions/compose'; import { closeModal } from '@/flavours/glitch/actions/modal'; @@ -9,9 +9,19 @@ import { Button } from '@/flavours/glitch/components/button'; import type { AnnualReport as AnnualReportData } from '@/flavours/glitch/models/annual_report'; import { useAppDispatch } from '@/flavours/glitch/store'; -import { shareMessage } from '.'; import { archetypeNames } from './archetype'; +const messages = defineMessages({ + share_message: { + id: 'annual_report.summary.share_message', + defaultMessage: 'I got the {archetype} archetype!', + }, + share_on_mastodon: { + id: 'annual_report.summary.share_on_mastodon', + defaultMessage: 'Share on Mastodon', + }, +}); + export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { const intl = useIntl(); const dispatch = useAppDispatch(); @@ -21,7 +31,7 @@ export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { archetypeNames[report.data.archetype], ); const shareLines = [ - intl.formatMessage(shareMessage, { + intl.formatMessage(messages.share_message, { archetype: archetypeName, }), ]; @@ -37,5 +47,10 @@ export const ShareButton: FC<{ report: AnnualReportData }> = ({ report }) => { dispatch(closeModal({ modalType: 'ANNUAL_REPORT', ignoreFocus: false })); }, [report, intl, dispatch]); - return