mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-12 23:38:20 +00:00
Compare commits
38 Commits
cd71fdcdff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88c0f52e99 | ||
|
|
a56b739c68 | ||
|
|
183a42a5ee | ||
|
|
303a5478af | ||
|
|
dfbf908870 | ||
|
|
aa067370d8 | ||
|
|
5e0db46b2a | ||
|
|
c06eb371e6 | ||
|
|
53617cef5a | ||
|
|
d730f6b0c5 | ||
|
|
addeb28292 | ||
|
|
5e3387539e | ||
|
|
4323963053 | ||
|
|
5651900b89 | ||
|
|
d1b996b7e3 | ||
|
|
fed26a41fa | ||
|
|
37d309bcaf | ||
|
|
d25f672c50 | ||
|
|
15c9088761 | ||
|
|
da1505a495 | ||
|
|
d1f690f50c | ||
|
|
8b418b84d0 | ||
|
|
f817300d8d | ||
|
|
35a89a0173 | ||
|
|
b5721dbd4a | ||
|
|
38f623eee7 | ||
|
|
17ba99e5de | ||
|
|
da2b75bdcd | ||
|
|
adf8a3601d | ||
|
|
d6f2a3ac8d | ||
|
|
c42b9f6996 | ||
|
|
76184c998c | ||
|
|
8137ce87ce | ||
|
|
37426288d9 | ||
|
|
801fee7593 | ||
|
|
6838497fe8 | ||
|
|
7b8a5d42f1 | ||
|
|
91500a7f53 |
42
.github/workflows/build-releases.yml
vendored
42
.github/workflows/build-releases.yml
vendored
@@ -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}}
|
||||
|
||||
20
CHANGELOG.md
20
CHANGELOG.md
@@ -2,6 +2,26 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [4.5.3] - 2025-12-08
|
||||
|
||||
### Security
|
||||
|
||||
- Fix inconsistent error handling leaking information on existence of private posts ([GHSA-gwhw-gcjx-72v8](https://github.com/mastodon/mastodon/security/advisories/GHSA-gwhw-gcjx-72v8))
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix “Delete and Redraft” on a non-quote being treated as a quote post in some cases (#37140 by @ClearlyClaire)
|
||||
- Fix YouTube embeds by sending referer (#37126 by @ChaosExAnima)
|
||||
- Fix streamed quoted polls not being hydrated correctly (#37118 by @ClearlyClaire)
|
||||
- Fix creation of duplicate conversations (#37108 by @oneiros)
|
||||
- Fix extraneous `noreferrer` in external links (#37107 by @ChaosExAnima)
|
||||
- Fix edge case error handling in some database migrations (#37079 by @ClearlyClaire)
|
||||
- Fix error handling when re-fetching already-known statuses (#37077 by @ClearlyClaire)
|
||||
- Fix post navigation in single-column mode when Advanced UI is enabled (#37044 by @diondiondion)
|
||||
- Fix `tootctl status remove` removing quoted posts and remote quotes of local posts (#37009 by @ClearlyClaire)
|
||||
- Fix known expensive S3 batch delete operation failing because of short timeouts (#37004 by @ClearlyClaire)
|
||||
- Fix compose autosuggest always lowercasing input token (#36995 by @ClearlyClaire)
|
||||
|
||||
## [4.5.2] - 2025-11-20
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -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)
|
||||
@@ -855,7 +856,7 @@ GEM
|
||||
unicode-display_width (>= 1.1.1, < 4)
|
||||
terrapin (1.1.1)
|
||||
climate_control
|
||||
test-prof (1.4.4)
|
||||
test-prof (1.5.0)
|
||||
thor (1.4.0)
|
||||
tilt (2.6.1)
|
||||
timeout (0.4.3)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||
import api, { getLinks } from 'flavours/glitch/api';
|
||||
import { compareId } from 'flavours/glitch/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||
@@ -7,7 +8,7 @@ import { toServerSideType } from 'flavours/glitch/utils/filters';
|
||||
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import {timelineDelete} from './timelines_typed';
|
||||
import { timelineDelete } from './timelines_typed';
|
||||
|
||||
export { disconnectTimeline } from './timelines_typed';
|
||||
|
||||
@@ -25,9 +26,16 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
|
||||
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
|
||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||
export const TIMELINE_GAP = null;
|
||||
|
||||
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
TIMELINE_WRAPSTODON,
|
||||
];
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
timeline,
|
||||
@@ -135,6 +143,7 @@ export function expandTimeline(timelineId, path, params = {}) {
|
||||
|
||||
if (timelineId === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
dispatch(reinsertAnnualReport())
|
||||
}
|
||||
} catch(error) {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'flavours/glitch/initial_state';
|
||||
|
||||
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
||||
|
||||
export function isNonStatusId(value: unknown) {
|
||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||
}
|
||||
|
||||
export const disconnectTimeline = createAction(
|
||||
'timeline/disconnect',
|
||||
({ timeline }: { timeline: string }) => ({
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div ref={wrapperRef} onClickCapture={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import {
|
||||
accountFactoryState,
|
||||
annualReportFactory,
|
||||
statusFactoryState,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
|
||||
const SAMPLE_HASHTAG = {
|
||||
name: 'Mastodon',
|
||||
count: 14,
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AnnualReport',
|
||||
component: AnnualReport,
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ display_name: 'Freddie Fruitbat' }),
|
||||
},
|
||||
statuses: {
|
||||
'1': statusFactoryState(),
|
||||
},
|
||||
annualReport: annualReportFactory({
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof AnnualReport>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Standalone: Story = {
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
};
|
||||
|
||||
export const InModal: Story = {
|
||||
args: {
|
||||
context: 'modal',
|
||||
},
|
||||
};
|
||||
|
||||
export const ArchetypeOracle: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'oracle',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'booster',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPosts: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'pollster',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPostsNoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'replier',
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import classNames from 'classnames';
|
||||
|
||||
import { Avatar } from '@/flavours/glitch/components/avatar';
|
||||
import { Button } from '@/flavours/glitch/components/button';
|
||||
import { me } from '@/flavours/glitch/initial_state';
|
||||
import type { Account } from '@/flavours/glitch/models/account';
|
||||
import type {
|
||||
AnnualReport,
|
||||
@@ -112,11 +111,11 @@ const illustrations = {
|
||||
export const Archetype: React.FC<{
|
||||
report: AnnualReport;
|
||||
account?: Account;
|
||||
canShare: boolean;
|
||||
}> = ({ report, account, canShare }) => {
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ report, account, context }) => {
|
||||
const intl = useIntl();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isSelfView = account?.id === me;
|
||||
const isSelfView = context === 'modal';
|
||||
|
||||
const [isRevealed, setIsRevealed] = useState(!isSelfView);
|
||||
const reveal = useCallback(() => {
|
||||
@@ -209,7 +208,7 @@ export const Archetype: React.FC<{
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{isRevealed && canShare && <ShareButton report={report} />}
|
||||
{isRevealed && isSelfView && <ShareButton report={report} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
@@ -19,7 +23,8 @@ const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||
|
||||
export const HighlightedPost: React.FC<{
|
||||
data: TopStatuses;
|
||||
}> = ({ data }) => {
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ data, context }) => {
|
||||
const { by_reblogs, by_favourites, by_replies } = data;
|
||||
|
||||
const statusId = by_reblogs || by_favourites || by_replies;
|
||||
@@ -28,6 +33,24 @@ export const HighlightedPost: React.FC<{
|
||||
statusId ? getStatus(state, { id: statusId }) : undefined,
|
||||
);
|
||||
|
||||
const handleClick = useCallback<
|
||||
ComponentPropsWithoutRef<typeof InterceptStatusClicks>['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 <div className={classNames(styles.box, styles.mostBoostedPost)} />;
|
||||
}
|
||||
@@ -68,10 +91,12 @@ export const HighlightedPost: React.FC<{
|
||||
defaultMessage='Most popular post'
|
||||
/>
|
||||
</h2>
|
||||
<p>{label}</p>
|
||||
{context === 'modal' && <p>{label}</p>}
|
||||
</div>
|
||||
|
||||
<StatusQuoteManager showActions={false} id={`${statusId}`} />
|
||||
<InterceptStatusClicks onPreventedClick={handleClick}>
|
||||
<StatusQuoteManager showActions={false} id={statusId} />
|
||||
</InterceptStatusClicks>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,62 +1,99 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
@font-face {
|
||||
font-family: silkscreen-wrapstodon;
|
||||
src: url('@/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2')
|
||||
format('woff2');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.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: 0;
|
||||
}
|
||||
|
||||
.loading-indicator .circular-progress {
|
||||
color: var(--lime);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
|
||||
--default-icon-color: var(--color-bg-primary);
|
||||
--default-bg-color: var(--color-text-primary);
|
||||
--hover-icon-color: var(--color-bg-primary);
|
||||
--hover-bg-color: var(--color-text-primary);
|
||||
--corner-distance: 18px;
|
||||
|
||||
position: absolute;
|
||||
top: var(--corner-distance);
|
||||
right: var(--corner-distance);
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
--corner-distance: 16px;
|
||||
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 < 600px) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: inherit;
|
||||
border-radius: inherit;
|
||||
filter: blur(20px);
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-inline: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +102,16 @@
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-family: monospace;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
font-family: silkscreen-wrapstodon, monospace;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
padding-inline: 40px; // Prevent overlap with close button
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -89,7 +129,7 @@
|
||||
|
||||
.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%);
|
||||
@@ -123,7 +163,6 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
@@ -137,6 +176,10 @@
|
||||
text-transform: uppercase;
|
||||
color: #c2c8ff;
|
||||
font-weight: 500;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.statLarge {
|
||||
@@ -150,11 +193,15 @@
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mostBoostedPost {
|
||||
padding: 0;
|
||||
padding-top: 8px;
|
||||
padding-top: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -166,7 +213,7 @@
|
||||
'followers hashtag'
|
||||
'new-posts hashtag';
|
||||
|
||||
@media (width < 680px) {
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
'followers new-posts'
|
||||
@@ -191,7 +238,7 @@
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas: 'number hashtag';
|
||||
|
||||
@media (width < 680px) {
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-areas:
|
||||
'number number'
|
||||
'hashtag hashtag';
|
||||
@@ -229,7 +276,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
max-width: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
.archetypeArtboard {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
@@ -23,12 +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!',
|
||||
});
|
||||
|
||||
// Share = false when using the embedded version of the report.
|
||||
export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
context = 'standalone',
|
||||
}) => {
|
||||
@@ -67,23 +61,16 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
0,
|
||||
);
|
||||
|
||||
const newFollowerCount = report.data.time_series.reduce(
|
||||
(sum, item) => sum + item.followers,
|
||||
0,
|
||||
);
|
||||
const newFollowerCount =
|
||||
context === 'modal' &&
|
||||
report.data.time_series.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const topHashtag = report.data.top_hashtags[0];
|
||||
|
||||
return (
|
||||
<div className={moduleClassNames(styles.wrapper, 'theme-dark')}>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.title'
|
||||
defaultMessage='Wrapstodon {year}'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
</h1>
|
||||
<h1>Wrapstodon {report.year}</h1>
|
||||
{account && <p>@{account.acct}</p>}
|
||||
{context === 'modal' && (
|
||||
<IconButton
|
||||
@@ -100,7 +87,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
</div>
|
||||
|
||||
<div className={styles.stack}>
|
||||
<HighlightedPost data={report.data.top_statuses} />
|
||||
<HighlightedPost data={report.data.top_statuses} context={context} />
|
||||
<div
|
||||
className={moduleClassNames(styles.statsGrid, {
|
||||
noHashtag: !topHashtag,
|
||||
@@ -110,13 +97,15 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
>
|
||||
{!!newFollowerCount && <Followers count={newFollowerCount} />}
|
||||
{!!newPostCount && <NewPosts count={newPostCount} />}
|
||||
{topHashtag && <MostUsedHashtag hashtag={topHashtag} />}
|
||||
{topHashtag && (
|
||||
<MostUsedHashtag
|
||||
hashtag={topHashtag}
|
||||
name={account?.display_name}
|
||||
context={context}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Archetype
|
||||
report={report}
|
||||
account={account}
|
||||
canShare={context === 'modal'}
|
||||
/>
|
||||
<Archetype report={report} account={account} context={context} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<React.MouseEventHandler<HTMLDivElement>>(
|
||||
(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
|
||||
<div
|
||||
className={classNames(
|
||||
'modal-root__modal',
|
||||
styles.modalWrapper,
|
||||
'theme-dark',
|
||||
)}
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<AnnualReport context='modal' />
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,9 @@ import styles from './index.module.scss';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
hashtag: NameAndCount;
|
||||
}> = ({ hashtag }) => {
|
||||
name: string | undefined;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ hashtag, name, context }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.box, styles.mostUsedHashtag, styles.content)}
|
||||
@@ -23,11 +25,21 @@ export const MostUsedHashtag: React.FC<{
|
||||
<div className={styles.statExtraLarge}>#{hashtag.name}</div>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count'
|
||||
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count }}
|
||||
/>
|
||||
{context === 'modal' ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count'
|
||||
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count }}
|
||||
/>
|
||||
) : (
|
||||
name && (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count_public'
|
||||
defaultMessage='{name} included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count, name }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 <Button text='Share here' onClick={handleShareClick} />;
|
||||
return (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.share_on_mastodon)}
|
||||
onClick={handleShareClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.wrapper {
|
||||
max-width: max-content;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
max-width: 600px;
|
||||
margin-inline: auto;
|
||||
padding: 40px 10px;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-top: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
a:any-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { IconLogo } from '@/flavours/glitch/components/logo';
|
||||
import { me } from '@/flavours/glitch/initial_state';
|
||||
|
||||
import { AnnualReport } from './index';
|
||||
import classes from './shared_page.module.css';
|
||||
import classes from './shared_page.module.scss';
|
||||
|
||||
export const WrapstodonSharedPage: FC = () => {
|
||||
return (
|
||||
@@ -11,7 +14,33 @@ export const WrapstodonSharedPage: FC = () => {
|
||||
<AnnualReport />
|
||||
<footer className={classes.footer}>
|
||||
<IconLogo className={classes.logo} />
|
||||
Generated with ♥ by the Mastodon team
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.footer'
|
||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||
values={{ heart: '♥' }}
|
||||
/>
|
||||
<nav className={classes.nav}>
|
||||
<a href='/about'>
|
||||
<FormattedMessage
|
||||
id='footer.about_this_server'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
</a>
|
||||
{!me && (
|
||||
<a href='https://joinmastodon.org/servers'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.sign_up'
|
||||
defaultMessage='Sign up'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<a href='https://joinmastodon.org/sponsors'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.donate'
|
||||
defaultMessage='Donate'
|
||||
/>
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,10 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/flavours/glitch/actions/timelines';
|
||||
import { scrollTopTimeline, loadPending } from '@/flavours/glitch/actions/timelines';
|
||||
import { isNonStatusId } from '@/flavours/glitch/actions/timelines_typed';
|
||||
import StatusList from '@/flavours/glitch/components/status_list';
|
||||
import { me } from '@/flavours/glitch/initial_state';
|
||||
import { TIMELINE_WRAPSTODON } from '@/flavours/glitch/reducers/slices/annual_report';
|
||||
|
||||
const getRegex = createSelector([
|
||||
(state, { regex }) => regex,
|
||||
@@ -29,7 +29,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
||||
getRegex,
|
||||
], (columnSettings, statusIds, statuses, regex) => {
|
||||
return statusIds.filter(id => {
|
||||
if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true;
|
||||
if (isNonStatusId(id)) return true;
|
||||
|
||||
const statusForId = statuses.get(id);
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface TimeSeriesMonth {
|
||||
}
|
||||
|
||||
export interface TopStatuses {
|
||||
by_reblogs: number;
|
||||
by_favourites: number;
|
||||
by_replies: number;
|
||||
by_reblogs: string;
|
||||
by_favourites: string;
|
||||
by_replies: string;
|
||||
}
|
||||
|
||||
export type Archetype =
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
importFetchedStatuses,
|
||||
} from '@/flavours/glitch/actions/importer';
|
||||
import { insertIntoTimeline } from '@/flavours/glitch/actions/timelines';
|
||||
import { timelineDelete } from '@/flavours/glitch/actions/timelines_typed';
|
||||
import type { ApiAnnualReportState } from '@/flavours/glitch/api/annual_report';
|
||||
import {
|
||||
apiGetAnnualReport,
|
||||
@@ -78,6 +79,25 @@ export const checkAnnualReport = createAppThunk(
|
||||
},
|
||||
);
|
||||
|
||||
export const reinsertAnnualReport = createAppThunk(
|
||||
`${annualReportSlice.name}/reinsertAnnualReport`,
|
||||
(_arg: unknown, { dispatch, getState }) => {
|
||||
dispatch(
|
||||
timelineDelete({
|
||||
statusId: TIMELINE_WRAPSTODON,
|
||||
accountId: '',
|
||||
references: [],
|
||||
reblogOf: null,
|
||||
}),
|
||||
);
|
||||
const { state } = getState().annualReport;
|
||||
if (!state || state === 'ineligible') {
|
||||
return;
|
||||
}
|
||||
dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1));
|
||||
},
|
||||
);
|
||||
|
||||
const fetchReportState = createDataLoadingThunk(
|
||||
`${annualReportSlice.name}/fetchReportState`,
|
||||
async (_arg: unknown, { getState }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
||||
import { timelineDelete } from 'flavours/glitch/actions/timelines_typed';
|
||||
import { timelineDelete, isNonStatusId } from 'flavours/glitch/actions/timelines_typed';
|
||||
|
||||
import {
|
||||
blockAccountSuccess,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
TIMELINE_MARK_AS_PARTIAL,
|
||||
TIMELINE_INSERT,
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
disconnectTimeline,
|
||||
} from '../actions/timelines';
|
||||
import { compareId } from '../compare_id';
|
||||
@@ -36,7 +35,6 @@ const initialTimeline = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
});
|
||||
|
||||
const isPlaceholder = value => value === TIMELINE_GAP || value === TIMELINE_SUGGESTIONS;
|
||||
|
||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
|
||||
// This method is pretty tricky because:
|
||||
@@ -69,20 +67,20 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
||||
// First, find the furthest (if properly sorted, oldest) item in the timeline that is
|
||||
// newer than the oldest fetched one, as it's most likely that it delimits the gap.
|
||||
// Start the gap *after* that item.
|
||||
const lastIndex = oldIds.findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.last()) >= 0) + 1;
|
||||
const lastIndex = oldIds.findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.last()) >= 0) + 1;
|
||||
|
||||
// Then, try to find the furthest (if properly sorted, oldest) item in the timeline that
|
||||
// is newer than the most recent fetched one, as it delimits a section comprised of only
|
||||
// items older or within `newIds` (or that were deleted from the server, so should be removed
|
||||
// anyway).
|
||||
// Stop the gap *after* that item.
|
||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.first()) > 0) + 1;
|
||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.first()) > 0) + 1;
|
||||
|
||||
let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => {
|
||||
// It is possible, though unlikely, that the slice we are replacing contains items older
|
||||
// than the elements we got from the API. Get them and add them back at the back of the
|
||||
// slice.
|
||||
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isPlaceholder(id) && compareId(id, newIds.last()) < 0);
|
||||
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isNonStatusId(id) && compareId(id, newIds.last()) < 0);
|
||||
insertedIds.union(olderIds);
|
||||
|
||||
// Make sure we aren't inserting duplicates
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
html {
|
||||
@include base.palette;
|
||||
|
||||
&[data-user-theme='system'] {
|
||||
&:where([data-user-theme='system']) {
|
||||
color-scheme: dark light;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
100
app/javascript/fonts/silkscreen-wrapstodon/OFL.txt
Normal file
100
app/javascript/fonts/silkscreen-wrapstodon/OFL.txt
Normal file
@@ -0,0 +1,100 @@
|
||||
Below you'll find the original License file for the Silkscreen font.
|
||||
The file used on Mastodon is a custom file subset to only include the
|
||||
characters "Wrapstodon 0123456789" using the Font Squirrel Font-face Generator
|
||||
(https://www.fontsquirrel.com/tools/webfont-generator)
|
||||
|
||||
-----------------------------------------------------------
|
||||
|
||||
Copyright 2001 The Silkscreen Project Authors (https://github.com/googlefonts/silkscreen)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
BIN
app/javascript/images/archetypes/previews/booster.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/booster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
BIN
app/javascript/images/archetypes/previews/lurker.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/lurker.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
BIN
app/javascript/images/archetypes/previews/oracle.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/oracle.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 167 KiB |
BIN
app/javascript/images/archetypes/previews/pollster.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/pollster.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 165 KiB |
BIN
app/javascript/images/archetypes/previews/replier.jpg
Normal file
BIN
app/javascript/images/archetypes/previews/replier.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 171 KiB |
@@ -1,12 +1,13 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
|
||||
|
||||
import { reinsertAnnualReport, TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
||||
import api, { getLinks } from 'mastodon/api';
|
||||
import { compareId } from 'mastodon/compare_id';
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
|
||||
import { importFetchedStatus, importFetchedStatuses } from './importer';
|
||||
import { submitMarkers } from './markers';
|
||||
import {timelineDelete} from './timelines_typed';
|
||||
import { timelineDelete } from './timelines_typed';
|
||||
|
||||
export { disconnectTimeline } from './timelines_typed';
|
||||
|
||||
@@ -24,9 +25,16 @@ export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
|
||||
export const TIMELINE_MARK_AS_PARTIAL = 'TIMELINE_MARK_AS_PARTIAL';
|
||||
export const TIMELINE_INSERT = 'TIMELINE_INSERT';
|
||||
|
||||
// When adding new special markers here, make sure to update TIMELINE_NON_STATUS_MARKERS in actions/timelines_typed.js
|
||||
export const TIMELINE_SUGGESTIONS = 'inline-follow-suggestions';
|
||||
export const TIMELINE_GAP = null;
|
||||
|
||||
export const TIMELINE_NON_STATUS_MARKERS = [
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
TIMELINE_WRAPSTODON,
|
||||
];
|
||||
|
||||
export const loadPending = timeline => ({
|
||||
type: TIMELINE_LOAD_PENDING,
|
||||
timeline,
|
||||
@@ -124,6 +132,7 @@ export function expandTimeline(timelineId, path, params = {}) {
|
||||
|
||||
if (timelineId === 'home') {
|
||||
dispatch(submitMarkers());
|
||||
dispatch(reinsertAnnualReport())
|
||||
}
|
||||
} catch(error) {
|
||||
dispatch(expandTimelineFail(timelineId, error, isLoadingMore));
|
||||
|
||||
@@ -2,6 +2,12 @@ import { createAction } from '@reduxjs/toolkit';
|
||||
|
||||
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
|
||||
|
||||
import { TIMELINE_NON_STATUS_MARKERS } from './timelines';
|
||||
|
||||
export function isNonStatusId(value: unknown) {
|
||||
return TIMELINE_NON_STATUS_MARKERS.includes(value as string | null);
|
||||
}
|
||||
|
||||
export const disconnectTimeline = createAction(
|
||||
'timeline/disconnect',
|
||||
({ timeline }: { timeline: string }) => ({
|
||||
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div ref={wrapperRef} onClickCapture={handleClick}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
|
||||
import {
|
||||
accountFactoryState,
|
||||
annualReportFactory,
|
||||
statusFactoryState,
|
||||
} from '@/testing/factories';
|
||||
|
||||
import { AnnualReport } from '.';
|
||||
|
||||
const SAMPLE_HASHTAG = {
|
||||
name: 'Mastodon',
|
||||
count: 14,
|
||||
};
|
||||
|
||||
const meta = {
|
||||
title: 'Components/AnnualReport',
|
||||
component: AnnualReport,
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
parameters: {
|
||||
state: {
|
||||
accounts: {
|
||||
'1': accountFactoryState({ display_name: 'Freddie Fruitbat' }),
|
||||
},
|
||||
statuses: {
|
||||
'1': statusFactoryState(),
|
||||
},
|
||||
annualReport: annualReportFactory({
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof AnnualReport>;
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Standalone: Story = {
|
||||
args: {
|
||||
context: 'standalone',
|
||||
},
|
||||
};
|
||||
|
||||
export const InModal: Story = {
|
||||
args: {
|
||||
context: 'modal',
|
||||
},
|
||||
};
|
||||
|
||||
export const ArchetypeOracle: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'oracle',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'booster',
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPosts: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'pollster',
|
||||
top_hashtag: SAMPLE_HASHTAG,
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const NoNewPostsNoHashtag: Story = {
|
||||
...InModal,
|
||||
parameters: {
|
||||
state: {
|
||||
annualReport: annualReportFactory({
|
||||
archetype: 'replier',
|
||||
without_posts: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -12,7 +12,6 @@ import replier from '@/images/archetypes/replier.png';
|
||||
import space_elements from '@/images/archetypes/space_elements.png';
|
||||
import { Avatar } from '@/mastodon/components/avatar';
|
||||
import { Button } from '@/mastodon/components/button';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import type { Account } from '@/mastodon/models/account';
|
||||
import type {
|
||||
AnnualReport,
|
||||
@@ -112,11 +111,11 @@ const illustrations = {
|
||||
export const Archetype: React.FC<{
|
||||
report: AnnualReport;
|
||||
account?: Account;
|
||||
canShare: boolean;
|
||||
}> = ({ report, account, canShare }) => {
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ report, account, context }) => {
|
||||
const intl = useIntl();
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const isSelfView = account?.id === me;
|
||||
const isSelfView = context === 'modal';
|
||||
|
||||
const [isRevealed, setIsRevealed] = useState(!isSelfView);
|
||||
const reveal = useCallback(() => {
|
||||
@@ -209,7 +208,7 @@ export const Archetype: React.FC<{
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
{isRevealed && canShare && <ShareButton report={report} />}
|
||||
{isRevealed && isSelfView && <ShareButton report={report} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 'mastodon/components/status/intercept_status_clicks';
|
||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
|
||||
import type { TopStatuses } from 'mastodon/models/annual_report';
|
||||
import { makeGetStatus } from 'mastodon/selectors';
|
||||
@@ -19,7 +23,8 @@ const getStatus = makeGetStatus() as unknown as (arg0: any, arg1: any) => any;
|
||||
|
||||
export const HighlightedPost: React.FC<{
|
||||
data: TopStatuses;
|
||||
}> = ({ data }) => {
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ data, context }) => {
|
||||
const { by_reblogs, by_favourites, by_replies } = data;
|
||||
|
||||
const statusId = by_reblogs || by_favourites || by_replies;
|
||||
@@ -28,6 +33,24 @@ export const HighlightedPost: React.FC<{
|
||||
statusId ? getStatus(state, { id: statusId }) : undefined,
|
||||
);
|
||||
|
||||
const handleClick = useCallback<
|
||||
ComponentPropsWithoutRef<typeof InterceptStatusClicks>['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 <div className={classNames(styles.box, styles.mostBoostedPost)} />;
|
||||
}
|
||||
@@ -68,10 +91,12 @@ export const HighlightedPost: React.FC<{
|
||||
defaultMessage='Most popular post'
|
||||
/>
|
||||
</h2>
|
||||
<p>{label}</p>
|
||||
{context === 'modal' && <p>{label}</p>}
|
||||
</div>
|
||||
|
||||
<StatusQuoteManager showActions={false} id={`${statusId}`} />
|
||||
<InterceptStatusClicks onPreventedClick={handleClick}>
|
||||
<StatusQuoteManager showActions={false} id={statusId} />
|
||||
</InterceptStatusClicks>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,62 +1,99 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
@font-face {
|
||||
font-family: silkscreen-wrapstodon;
|
||||
src: url('@/fonts/silkscreen-wrapstodon/silkscreen-regular.woff2')
|
||||
format('woff2');
|
||||
font-weight: normal;
|
||||
font-display: swap;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.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: 0;
|
||||
}
|
||||
|
||||
.loading-indicator .circular-progress {
|
||||
color: var(--lime);
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
right: 24px;
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
|
||||
--default-icon-color: var(--color-bg-primary);
|
||||
--default-bg-color: var(--color-text-primary);
|
||||
--hover-icon-color: var(--color-bg-primary);
|
||||
--hover-bg-color: var(--color-text-primary);
|
||||
--corner-distance: 18px;
|
||||
|
||||
position: absolute;
|
||||
top: var(--corner-distance);
|
||||
right: var(--corner-distance);
|
||||
padding: 8px;
|
||||
border-radius: 100%;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
--corner-distance: 16px;
|
||||
|
||||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.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 < 600px) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
background: inherit;
|
||||
border-radius: inherit;
|
||||
filter: blur(20px);
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-inline: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,13 +102,16 @@
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-family: monospace;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
font-size: 30px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 8px;
|
||||
font-family: silkscreen-wrapstodon, monospace;
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
padding-inline: 40px; // Prevent overlap with close button
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -89,7 +129,7 @@
|
||||
|
||||
.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%);
|
||||
@@ -123,7 +163,6 @@
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 16px;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
text-wrap: balance;
|
||||
@@ -137,6 +176,10 @@
|
||||
text-transform: uppercase;
|
||||
color: #c2c8ff;
|
||||
font-weight: 500;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.statLarge {
|
||||
@@ -150,11 +193,15 @@
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
overflow-wrap: break-word;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mostBoostedPost {
|
||||
padding: 0;
|
||||
padding-top: 8px;
|
||||
padding-top: 24px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -166,7 +213,7 @@
|
||||
'followers hashtag'
|
||||
'new-posts hashtag';
|
||||
|
||||
@media (width < 680px) {
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-areas:
|
||||
'followers new-posts'
|
||||
@@ -191,7 +238,7 @@
|
||||
grid-template-columns: 1fr 2fr;
|
||||
grid-template-areas: 'number hashtag';
|
||||
|
||||
@media (width < 680px) {
|
||||
@media (width < $mobile-breakpoint) {
|
||||
grid-template-areas:
|
||||
'number number'
|
||||
'hashtag hashtag';
|
||||
@@ -229,7 +276,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
gap: 16px;
|
||||
|
||||
p {
|
||||
max-width: 460px;
|
||||
}
|
||||
}
|
||||
|
||||
.archetypeArtboard {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { defineMessage, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useIntl } from 'react-intl';
|
||||
|
||||
import { useLocation } from 'react-router';
|
||||
|
||||
@@ -23,12 +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!',
|
||||
});
|
||||
|
||||
// Share = false when using the embedded version of the report.
|
||||
export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
context = 'standalone',
|
||||
}) => {
|
||||
@@ -67,23 +61,16 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
0,
|
||||
);
|
||||
|
||||
const newFollowerCount = report.data.time_series.reduce(
|
||||
(sum, item) => sum + item.followers,
|
||||
0,
|
||||
);
|
||||
const newFollowerCount =
|
||||
context === 'modal' &&
|
||||
report.data.time_series.reduce((sum, item) => sum + item.followers, 0);
|
||||
|
||||
const topHashtag = report.data.top_hashtags[0];
|
||||
|
||||
return (
|
||||
<div className={moduleClassNames(styles.wrapper, 'theme-dark')}>
|
||||
<div className={styles.header}>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.title'
|
||||
defaultMessage='Wrapstodon {year}'
|
||||
values={{ year: report.year }}
|
||||
/>
|
||||
</h1>
|
||||
<h1>Wrapstodon {report.year}</h1>
|
||||
{account && <p>@{account.acct}</p>}
|
||||
{context === 'modal' && (
|
||||
<IconButton
|
||||
@@ -100,7 +87,7 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
</div>
|
||||
|
||||
<div className={styles.stack}>
|
||||
<HighlightedPost data={report.data.top_statuses} />
|
||||
<HighlightedPost data={report.data.top_statuses} context={context} />
|
||||
<div
|
||||
className={moduleClassNames(styles.statsGrid, {
|
||||
noHashtag: !topHashtag,
|
||||
@@ -110,13 +97,15 @@ export const AnnualReport: FC<{ context?: 'modal' | 'standalone' }> = ({
|
||||
>
|
||||
{!!newFollowerCount && <Followers count={newFollowerCount} />}
|
||||
{!!newPostCount && <NewPosts count={newPostCount} />}
|
||||
{topHashtag && <MostUsedHashtag hashtag={topHashtag} />}
|
||||
{topHashtag && (
|
||||
<MostUsedHashtag
|
||||
hashtag={topHashtag}
|
||||
name={account?.display_name}
|
||||
context={context}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Archetype
|
||||
report={report}
|
||||
account={account}
|
||||
canShare={context === 'modal'}
|
||||
/>
|
||||
<Archetype report={report} account={account} context={context} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
import { useAppDispatch } from '@/mastodon/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<React.MouseEventHandler<HTMLDivElement>>(
|
||||
(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
|
||||
<div
|
||||
className={classNames(
|
||||
'modal-root__modal',
|
||||
styles.modalWrapper,
|
||||
'theme-dark',
|
||||
)}
|
||||
onClick={handleCloseModal}
|
||||
>
|
||||
<AnnualReport context='modal' />
|
||||
</div>
|
||||
|
||||
@@ -8,7 +8,9 @@ import styles from './index.module.scss';
|
||||
|
||||
export const MostUsedHashtag: React.FC<{
|
||||
hashtag: NameAndCount;
|
||||
}> = ({ hashtag }) => {
|
||||
name: string | undefined;
|
||||
context: 'modal' | 'standalone';
|
||||
}> = ({ hashtag, name, context }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.box, styles.mostUsedHashtag, styles.content)}
|
||||
@@ -23,11 +25,21 @@ export const MostUsedHashtag: React.FC<{
|
||||
<div className={styles.statExtraLarge}>#{hashtag.name}</div>
|
||||
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count'
|
||||
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count }}
|
||||
/>
|
||||
{context === 'modal' ? (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count'
|
||||
defaultMessage='You included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count }}
|
||||
/>
|
||||
) : (
|
||||
name && (
|
||||
<FormattedMessage
|
||||
id='annual_report.summary.most_used_hashtag.used_count_public'
|
||||
defaultMessage='{name} included this hashtag in {count, plural, one {one post} other {# posts}}.'
|
||||
values={{ count: hashtag.count, name }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 '@/mastodon/actions/compose';
|
||||
import { closeModal } from '@/mastodon/actions/modal';
|
||||
@@ -9,9 +9,19 @@ import { Button } from '@/mastodon/components/button';
|
||||
import type { AnnualReport as AnnualReportData } from '@/mastodon/models/annual_report';
|
||||
import { useAppDispatch } from '@/mastodon/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 <Button text='Share here' onClick={handleShareClick} />;
|
||||
return (
|
||||
<Button
|
||||
text={intl.formatMessage(messages.share_on_mastodon)}
|
||||
onClick={handleShareClick}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.wrapper {
|
||||
max-width: max-content;
|
||||
margin: 40px auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
$mobile-breakpoint: 540px;
|
||||
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
max-width: 600px;
|
||||
margin-inline: auto;
|
||||
padding: 40px 10px;
|
||||
|
||||
@media (width < $mobile-breakpoint) {
|
||||
padding-top: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-top: 2rem;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
a:any-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.2em;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { IconLogo } from '@/mastodon/components/logo';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
|
||||
import { AnnualReport } from './index';
|
||||
import classes from './shared_page.module.css';
|
||||
import classes from './shared_page.module.scss';
|
||||
|
||||
export const WrapstodonSharedPage: FC = () => {
|
||||
return (
|
||||
@@ -11,7 +14,33 @@ export const WrapstodonSharedPage: FC = () => {
|
||||
<AnnualReport />
|
||||
<footer className={classes.footer}>
|
||||
<IconLogo className={classes.logo} />
|
||||
Generated with ♥ by the Mastodon team
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.footer'
|
||||
defaultMessage='Generated with {heart} by the Mastodon team'
|
||||
values={{ heart: '♥' }}
|
||||
/>
|
||||
<nav className={classes.nav}>
|
||||
<a href='/about'>
|
||||
<FormattedMessage
|
||||
id='footer.about_this_server'
|
||||
defaultMessage='About'
|
||||
/>
|
||||
</a>
|
||||
{!me && (
|
||||
<a href='https://joinmastodon.org/servers'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.sign_up'
|
||||
defaultMessage='Sign up'
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
<a href='https://joinmastodon.org/sponsors'>
|
||||
<FormattedMessage
|
||||
id='annual_report.shared_page.donate'
|
||||
defaultMessage='Donate'
|
||||
/>
|
||||
</a>
|
||||
</nav>
|
||||
</footer>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,10 @@ import { connect } from 'react-redux';
|
||||
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { scrollTopTimeline, loadPending, TIMELINE_SUGGESTIONS } from '@/mastodon/actions/timelines';
|
||||
import { scrollTopTimeline, loadPending } from '@/mastodon/actions/timelines';
|
||||
import { isNonStatusId } from '@/mastodon/actions/timelines_typed';
|
||||
import StatusList from '@/mastodon/components/status_list';
|
||||
import { me } from '@/mastodon/initial_state';
|
||||
import { TIMELINE_WRAPSTODON } from '@/mastodon/reducers/slices/annual_report';
|
||||
|
||||
const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state, { type }) => state.getIn(['settings', type], ImmutableMap()),
|
||||
@@ -15,7 +15,7 @@ const makeGetStatusIds = (pending = false) => createSelector([
|
||||
(state) => state.get('statuses'),
|
||||
], (columnSettings, statusIds, statuses) => {
|
||||
return statusIds.filter(id => {
|
||||
if (id === null || id === TIMELINE_SUGGESTIONS || id === TIMELINE_WRAPSTODON) return true;
|
||||
if (isNonStatusId(id)) return true;
|
||||
|
||||
const statusForId = statuses.get(id);
|
||||
|
||||
|
||||
@@ -117,6 +117,9 @@
|
||||
"annual_report.announcement.action_view": "View my Wrapstodon",
|
||||
"annual_report.announcement.description": "Discover more about your engagement on Mastodon over the past year.",
|
||||
"annual_report.announcement.title": "Wrapstodon {year} has arrived",
|
||||
"annual_report.shared_page.donate": "Donate",
|
||||
"annual_report.shared_page.footer": "Generated with {heart} by the Mastodon team",
|
||||
"annual_report.shared_page.sign_up": "Sign up",
|
||||
"annual_report.summary.archetype.booster.desc_public": "{name} stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
||||
"annual_report.summary.archetype.booster.desc_self": "You stayed on the hunt for posts to boost, amplifying other creators with perfect aim.",
|
||||
"annual_report.summary.archetype.booster.name": "The Archer",
|
||||
@@ -146,11 +149,12 @@
|
||||
"annual_report.summary.most_used_app.most_used_app": "most used app",
|
||||
"annual_report.summary.most_used_hashtag.most_used_hashtag": "most used hashtag",
|
||||
"annual_report.summary.most_used_hashtag.used_count": "You included this hashtag in {count, plural, one {one post} other {# posts}}.",
|
||||
"annual_report.summary.most_used_hashtag.used_count_public": "{name} included this hashtag in {count, plural, one {one post} other {# posts}}.",
|
||||
"annual_report.summary.new_posts.new_posts": "new posts",
|
||||
"annual_report.summary.percentile.text": "<topLabel>That puts you in the top</topLabel><percentage></percentage><bottomLabel>of {domain} users.</bottomLabel>",
|
||||
"annual_report.summary.percentile.we_wont_tell_bernie": "We won't tell Bernie.",
|
||||
"annual_report.summary.share_message": "I got the {archetype} archetype!",
|
||||
"annual_report.summary.title": "Wrapstodon {year}",
|
||||
"annual_report.summary.share_on_mastodon": "Share on Mastodon",
|
||||
"attachments_list.unprocessed": "(unprocessed)",
|
||||
"audio.hide": "Hide audio",
|
||||
"block_modal.remote_users_caveat": "We will ask the server {domain} to respect your decision. However, compliance is not guaranteed since some servers may handle blocks differently. Public posts may still be visible to non-logged-in users.",
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface TimeSeriesMonth {
|
||||
}
|
||||
|
||||
export interface TopStatuses {
|
||||
by_reblogs: number;
|
||||
by_favourites: number;
|
||||
by_replies: number;
|
||||
by_reblogs: string;
|
||||
by_favourites: string;
|
||||
by_replies: string;
|
||||
}
|
||||
|
||||
export type Archetype =
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
importFetchedStatuses,
|
||||
} from '@/mastodon/actions/importer';
|
||||
import { insertIntoTimeline } from '@/mastodon/actions/timelines';
|
||||
import { timelineDelete } from '@/mastodon/actions/timelines_typed';
|
||||
import type { ApiAnnualReportState } from '@/mastodon/api/annual_report';
|
||||
import {
|
||||
apiGetAnnualReport,
|
||||
@@ -78,6 +79,25 @@ export const checkAnnualReport = createAppThunk(
|
||||
},
|
||||
);
|
||||
|
||||
export const reinsertAnnualReport = createAppThunk(
|
||||
`${annualReportSlice.name}/reinsertAnnualReport`,
|
||||
(_arg: unknown, { dispatch, getState }) => {
|
||||
dispatch(
|
||||
timelineDelete({
|
||||
statusId: TIMELINE_WRAPSTODON,
|
||||
accountId: '',
|
||||
references: [],
|
||||
reblogOf: null,
|
||||
}),
|
||||
);
|
||||
const { state } = getState().annualReport;
|
||||
if (!state || state === 'ineligible') {
|
||||
return;
|
||||
}
|
||||
dispatch(insertIntoTimeline('home', TIMELINE_WRAPSTODON, 1));
|
||||
},
|
||||
);
|
||||
|
||||
const fetchReportState = createDataLoadingThunk(
|
||||
`${annualReportSlice.name}/fetchReportState`,
|
||||
async (_arg: unknown, { getState }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable';
|
||||
|
||||
import { timelineDelete } from 'mastodon/actions/timelines_typed';
|
||||
import { timelineDelete, isNonStatusId } from 'mastodon/actions/timelines_typed';
|
||||
|
||||
import {
|
||||
blockAccountSuccess,
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
TIMELINE_MARK_AS_PARTIAL,
|
||||
TIMELINE_INSERT,
|
||||
TIMELINE_GAP,
|
||||
TIMELINE_SUGGESTIONS,
|
||||
disconnectTimeline,
|
||||
} from '../actions/timelines';
|
||||
import { compareId } from '../compare_id';
|
||||
@@ -36,7 +35,6 @@ const initialTimeline = ImmutableMap({
|
||||
items: ImmutableList(),
|
||||
});
|
||||
|
||||
const isPlaceholder = value => value === TIMELINE_GAP || value === TIMELINE_SUGGESTIONS;
|
||||
|
||||
const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent, usePendingItems) => {
|
||||
// This method is pretty tricky because:
|
||||
@@ -69,20 +67,20 @@ const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, is
|
||||
// First, find the furthest (if properly sorted, oldest) item in the timeline that is
|
||||
// newer than the oldest fetched one, as it's most likely that it delimits the gap.
|
||||
// Start the gap *after* that item.
|
||||
const lastIndex = oldIds.findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.last()) >= 0) + 1;
|
||||
const lastIndex = oldIds.findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.last()) >= 0) + 1;
|
||||
|
||||
// Then, try to find the furthest (if properly sorted, oldest) item in the timeline that
|
||||
// is newer than the most recent fetched one, as it delimits a section comprised of only
|
||||
// items older or within `newIds` (or that were deleted from the server, so should be removed
|
||||
// anyway).
|
||||
// Stop the gap *after* that item.
|
||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isPlaceholder(id) && compareId(id, newIds.first()) > 0) + 1;
|
||||
const firstIndex = oldIds.take(lastIndex).findLastIndex(id => !isNonStatusId(id) && compareId(id, newIds.first()) > 0) + 1;
|
||||
|
||||
let insertedIds = ImmutableOrderedSet(newIds).withMutations(insertedIds => {
|
||||
// It is possible, though unlikely, that the slice we are replacing contains items older
|
||||
// than the elements we got from the API. Get them and add them back at the back of the
|
||||
// slice.
|
||||
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isPlaceholder(id) && compareId(id, newIds.last()) < 0);
|
||||
const olderIds = oldIds.slice(firstIndex, lastIndex).filter(id => !isNonStatusId(id) && compareId(id, newIds.last()) < 0);
|
||||
insertedIds.union(olderIds);
|
||||
|
||||
// Make sure we aren't inserting duplicates
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
html {
|
||||
@include base.palette;
|
||||
|
||||
&[data-user-theme='system'] {
|
||||
&:where([data-user-theme='system']) {
|
||||
color-scheme: dark light;
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import { Map as ImmutableMap, List } from 'immutable';
|
||||
|
||||
import type { ApiRelationshipJSON } from '@/mastodon/api_types/relationships';
|
||||
import type { ApiStatusJSON } from '@/mastodon/api_types/statuses';
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
UnicodeEmojiData,
|
||||
} from '@/mastodon/features/emoji/types';
|
||||
import { createAccountFromServerJSON } from '@/mastodon/models/account';
|
||||
import type { AnnualReport } from '@/mastodon/models/annual_report';
|
||||
import type { Status } from '@/mastodon/models/status';
|
||||
import type { ApiAccountJSON } from 'mastodon/api_types/accounts';
|
||||
|
||||
@@ -75,16 +76,18 @@ export const statusFactory: FactoryFunction<ApiStatusJSON> = ({
|
||||
mentions: [],
|
||||
tags: [],
|
||||
emojis: [],
|
||||
content: '<p>This is a test status.</p>',
|
||||
contentHtml: '<p>This is a test status.</p>',
|
||||
...data,
|
||||
});
|
||||
|
||||
export const statusFactoryState = (
|
||||
options: FactoryOptions<ApiStatusJSON> = {},
|
||||
) =>
|
||||
ImmutableMap<string, unknown>(
|
||||
statusFactory(options) as unknown as Record<string, unknown>,
|
||||
) as unknown as Status;
|
||||
ImmutableMap<string, unknown>({
|
||||
...(statusFactory(options) as unknown as Record<string, unknown>),
|
||||
account: options.account?.id ?? '1',
|
||||
tags: List(options.tags),
|
||||
}) as unknown as Status;
|
||||
|
||||
export const relationshipsFactory: FactoryFunction<ApiRelationshipJSON> = ({
|
||||
id,
|
||||
@@ -130,3 +133,119 @@ export function customEmojiFactory(
|
||||
...data,
|
||||
};
|
||||
}
|
||||
|
||||
interface AnnualReportState {
|
||||
state: 'available';
|
||||
report: AnnualReport;
|
||||
}
|
||||
|
||||
interface AnnualReportFactoryOptions {
|
||||
account_id?: string;
|
||||
status_id?: string;
|
||||
archetype?: AnnualReport['data']['archetype'];
|
||||
year?: number;
|
||||
top_hashtag?: AnnualReport['data']['top_hashtags'][0];
|
||||
without_posts?: boolean;
|
||||
}
|
||||
|
||||
export function annualReportFactory({
|
||||
account_id = '1',
|
||||
status_id = '1',
|
||||
archetype = 'lurker',
|
||||
year,
|
||||
top_hashtag,
|
||||
without_posts = false,
|
||||
}: AnnualReportFactoryOptions = {}): AnnualReportState {
|
||||
return {
|
||||
state: 'available',
|
||||
report: {
|
||||
schema_version: 2,
|
||||
share_url: '#',
|
||||
account_id,
|
||||
year: year ?? 2025,
|
||||
data: {
|
||||
archetype,
|
||||
time_series: [
|
||||
{
|
||||
month: 1,
|
||||
statuses: 0,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 2,
|
||||
statuses: 0,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 3,
|
||||
statuses: 0,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 4,
|
||||
statuses: 0,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 5,
|
||||
statuses: without_posts ? 0 : 1,
|
||||
followers: 1,
|
||||
following: 3,
|
||||
},
|
||||
{
|
||||
month: 6,
|
||||
statuses: without_posts ? 0 : 7,
|
||||
followers: 1,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 7,
|
||||
statuses: without_posts ? 0 : 2,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 8,
|
||||
statuses: without_posts ? 0 : 2,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
{
|
||||
month: 9,
|
||||
statuses: without_posts ? 0 : 11,
|
||||
followers: 0,
|
||||
following: 1,
|
||||
},
|
||||
{
|
||||
month: 10,
|
||||
statuses: without_posts ? 0 : 12,
|
||||
followers: 0,
|
||||
following: 1,
|
||||
},
|
||||
{
|
||||
month: 11,
|
||||
statuses: without_posts ? 0 : 6,
|
||||
followers: 0,
|
||||
following: 1,
|
||||
},
|
||||
{
|
||||
month: 12,
|
||||
statuses: without_posts ? 0 : 4,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
},
|
||||
],
|
||||
top_hashtags: top_hashtag ? [top_hashtag] : [],
|
||||
top_statuses: {
|
||||
by_reblogs: status_id,
|
||||
by_replies: status_id,
|
||||
by_favourites: status_id,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3,29 +3,24 @@
|
||||
class AnnualReport::TimeSeries < AnnualReport::Source
|
||||
def generate
|
||||
{
|
||||
time_series: (1..12).map do |month|
|
||||
{
|
||||
month: month,
|
||||
statuses: statuses_per_month[month] || 0,
|
||||
following: following_per_month[month] || 0,
|
||||
followers: followers_per_month[month] || 0,
|
||||
}
|
||||
end,
|
||||
time_series: [
|
||||
{
|
||||
month: 12,
|
||||
statuses: statuses_this_year,
|
||||
followers: followers_this_year,
|
||||
},
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def statuses_per_month
|
||||
@statuses_per_month ||= report_statuses.group(:period).pluck(date_part_month.as('period'), Arel.star.count).to_h
|
||||
def statuses_this_year
|
||||
@statuses_this_year ||= report_statuses.count
|
||||
end
|
||||
|
||||
def following_per_month
|
||||
@following_per_month ||= annual_relationships_by_month(@account.active_relationships)
|
||||
end
|
||||
|
||||
def followers_per_month
|
||||
@followers_per_month ||= annual_relationships_by_month(@account.passive_relationships)
|
||||
def followers_this_year
|
||||
@followers_this_year ||= @account.passive_relationships.where(created_in_year, @year).count
|
||||
end
|
||||
|
||||
def date_part_month
|
||||
@@ -34,14 +29,6 @@ class AnnualReport::TimeSeries < AnnualReport::Source
|
||||
SQL
|
||||
end
|
||||
|
||||
def annual_relationships_by_month(relationships)
|
||||
relationships
|
||||
.where(created_in_year, @year)
|
||||
.group(:period)
|
||||
.pluck(date_part_month.as('period'), Arel.star.count)
|
||||
.to_h
|
||||
end
|
||||
|
||||
def created_in_year
|
||||
Arel.sql(<<~SQL.squish)
|
||||
DATE_PART('year', created_at) = ?
|
||||
|
||||
@@ -5,14 +5,14 @@ class AnnualReport::TopStatuses < AnnualReport::Source
|
||||
{
|
||||
top_statuses: {
|
||||
by_reblogs: status_identifier(most_reblogged_status),
|
||||
by_favourites: status_identifier(most_favourited_status),
|
||||
by_replies: status_identifier(most_replied_status),
|
||||
by_favourites: nil,
|
||||
by_replies: nil,
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
def eligible?
|
||||
report_statuses.public_visibility.exists?
|
||||
report_statuses.distributable_visibility.exists?
|
||||
end
|
||||
|
||||
private
|
||||
@@ -43,7 +43,7 @@ class AnnualReport::TopStatuses < AnnualReport::Source
|
||||
|
||||
def base_scope
|
||||
report_statuses
|
||||
.public_visibility
|
||||
.distributable_visibility
|
||||
.joins(:status_stat)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -451,6 +451,10 @@ class Account < ApplicationRecord
|
||||
save!
|
||||
end
|
||||
|
||||
def featureable?
|
||||
local? && discoverable?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def prepare_contents
|
||||
|
||||
@@ -32,6 +32,8 @@ class CollectionItem < ApplicationRecord
|
||||
validates :account, presence: true, if: :accepted?
|
||||
validates :object_uri, presence: true, if: -> { account.nil? }
|
||||
|
||||
before_validation :set_position, on: :create
|
||||
|
||||
scope :ordered, -> { order(position: :asc) }
|
||||
scope :with_accounts, -> { includes(account: [:account_stat, :user]) }
|
||||
scope :not_blocked_by, ->(account) { where.not(accounts: { id: account.blocking }) }
|
||||
@@ -39,4 +41,12 @@ class CollectionItem < ApplicationRecord
|
||||
def local_item_with_remote_account?
|
||||
local? && account&.remote?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_position
|
||||
return if position_changed?
|
||||
|
||||
self.position = self.class.where(collection_id:).maximum(:position).to_i + 1
|
||||
end
|
||||
end
|
||||
|
||||
@@ -64,4 +64,8 @@ class AccountPolicy < ApplicationPolicy
|
||||
def review?
|
||||
role.can?(:manage_taxonomies)
|
||||
end
|
||||
|
||||
def feature?
|
||||
record.featureable? && !current_account.blocking?(record) && !record.blocking?(current_account)
|
||||
end
|
||||
end
|
||||
|
||||
23
app/services/add_account_to_collection_service.rb
Normal file
23
app/services/add_account_to_collection_service.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddAccountToCollectionService
|
||||
def call(collection, account)
|
||||
raise ArgumentError unless collection.local?
|
||||
|
||||
@collection = collection
|
||||
@account = account
|
||||
|
||||
raise Mastodon::NotPermittedError, I18n.t('accounts.errors.cannot_be_added_to_collections') unless AccountPolicy.new(@collection.account, @account).feature?
|
||||
|
||||
create_collection_item
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_collection_item
|
||||
@collection.collection_items.create!(
|
||||
account: @account,
|
||||
state: :accepted
|
||||
)
|
||||
end
|
||||
end
|
||||
4
app/views/wrapstodon/_og_description.html.haml
Normal file
4
app/views/wrapstodon/_og_description.html.haml
Normal file
@@ -0,0 +1,4 @@
|
||||
- description = t('wrapstodon.description', name: display_name(account))
|
||||
|
||||
%meta{ name: 'description', content: description }/
|
||||
= opengraph 'og:description', description
|
||||
6
app/views/wrapstodon/_og_image.html.haml
Normal file
6
app/views/wrapstodon/_og_image.html.haml
Normal file
@@ -0,0 +1,6 @@
|
||||
- if %w(lurker booster pollster replier oracle).include?(report.data['archetype'])
|
||||
= opengraph 'og:image', frontend_asset_url("images/archetypes/previews/#{report.data['archetype']}.jpg")
|
||||
= opengraph 'og:image:type', 'image/jpeg'
|
||||
= opengraph 'og:image:width', 1200
|
||||
= opengraph 'og:image:height', 630
|
||||
= opengraph 'twitter:card', 'summary_large_image'
|
||||
@@ -4,8 +4,14 @@
|
||||
%meta{ name: 'robots', content: 'noindex, noarchive' }/
|
||||
|
||||
= opengraph 'og:site_name', site_title
|
||||
= opengraph 'og:type', 'article'
|
||||
= opengraph 'og:title', t('wrapstodon.title', name: display_name(@account), year: @generated_annual_report.year)
|
||||
= opengraph 'profile:username', acct(@account)[1..]
|
||||
|
||||
= render 'og_description', account: @account
|
||||
= render 'og_image', report: @generated_annual_report
|
||||
|
||||
= render_initial_state
|
||||
= flavoured_vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous'
|
||||
|
||||
- content_for :html_classes, 'theme-dark'
|
||||
|
||||
@@ -7,6 +7,8 @@ en:
|
||||
hosted_on: Mastodon hosted on %{domain}
|
||||
title: About
|
||||
accounts:
|
||||
errors:
|
||||
cannot_be_added_to_collections: This account cannot be added to collections.
|
||||
followers:
|
||||
one: Follower
|
||||
other: Followers
|
||||
@@ -2187,4 +2189,5 @@ en:
|
||||
otp_required: To use security keys please enable two-factor authentication first.
|
||||
registered_on: Registered on %{date}
|
||||
wrapstodon:
|
||||
description: See how %{name} used Mastodon this year!
|
||||
title: Wrapstodon %{year} for %{name}
|
||||
|
||||
@@ -59,7 +59,7 @@ services:
|
||||
web:
|
||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||
# build: .
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.2
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.3
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec puma -C config/puma.rb
|
||||
@@ -83,7 +83,7 @@ services:
|
||||
# build:
|
||||
# dockerfile: ./streaming/Dockerfile
|
||||
# context: .
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.2
|
||||
image: ghcr.io/glitch-soc/mastodon-streaming:v4.5.3
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: node ./streaming/index.js
|
||||
@@ -102,7 +102,7 @@ services:
|
||||
sidekiq:
|
||||
# You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes
|
||||
# build: .
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.2
|
||||
image: ghcr.io/glitch-soc/mastodon:v4.5.3
|
||||
restart: always
|
||||
env_file: .env.production
|
||||
command: bundle exec sidekiq
|
||||
|
||||
@@ -13,7 +13,7 @@ RSpec.describe AnnualReport::TimeSeries do
|
||||
expect(subject.generate)
|
||||
.to include(
|
||||
time_series: match(
|
||||
include(followers: 0, following: 0, month: 1, statuses: 0)
|
||||
include(followers: 0, month: 12, statuses: 0)
|
||||
)
|
||||
)
|
||||
end
|
||||
@@ -37,7 +37,7 @@ RSpec.describe AnnualReport::TimeSeries do
|
||||
expect(subject.generate)
|
||||
.to include(
|
||||
time_series: match(
|
||||
include(followers: 1, following: 1, month: 1, statuses: 1)
|
||||
include(followers: 1, month: 12, statuses: 1)
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -40,8 +40,8 @@ RSpec.describe AnnualReport::TopStatuses do
|
||||
.to include(
|
||||
top_statuses: include(
|
||||
by_reblogs: reblogged_status.id.to_s,
|
||||
by_favourites: favourited_status.id.to_s,
|
||||
by_replies: replied_status.id.to_s
|
||||
by_favourites: nil,
|
||||
by_replies: nil
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -781,4 +781,37 @@ RSpec.describe Account do
|
||||
expect(subject.reload.followers_count).to eq 15
|
||||
end
|
||||
end
|
||||
|
||||
describe '#featureable?' do
|
||||
subject { Fabricate.build(:account, domain: (local ? nil : 'example.com'), discoverable:) }
|
||||
|
||||
context 'when account is local' do
|
||||
let(:local) { true }
|
||||
|
||||
context 'when account is discoverable' do
|
||||
let(:discoverable) { true }
|
||||
|
||||
it 'returns `true`' do
|
||||
expect(subject.featureable?).to be true
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not discoverable' do
|
||||
let(:discoverable) { false }
|
||||
|
||||
it 'returns `false`' do
|
||||
expect(subject.featureable?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is remote' do
|
||||
let(:local) { false }
|
||||
let(:discoverable) { true }
|
||||
|
||||
it 'returns `false`' do
|
||||
expect(subject.featureable?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,4 +38,23 @@ RSpec.describe CollectionItem do
|
||||
it { is_expected.to validate_presence_of(:object_uri) }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'Creation' do
|
||||
let(:collection) { Fabricate(:collection) }
|
||||
let(:other_collection) { Fabricate(:collection) }
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:other_account) { Fabricate(:account) }
|
||||
|
||||
it 'automatically sets the `position` if absent' do
|
||||
first_item = collection.collection_items.create(account:)
|
||||
second_item = collection.collection_items.create(account: other_account)
|
||||
unrelated_item = other_collection.collection_items.create(account:)
|
||||
custom_item = other_collection.collection_items.create(account: other_account, position: 7)
|
||||
|
||||
expect(first_item.position).to eq 1
|
||||
expect(second_item.position).to eq 2
|
||||
expect(unrelated_item.position).to eq 1
|
||||
expect(custom_item.position).to eq 7
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -156,4 +156,36 @@ RSpec.describe AccountPolicy do
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
permissions :feature? do
|
||||
context 'when account is featureable?' do
|
||||
it 'permits' do
|
||||
expect(subject).to permit(alice, john)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is not featureable' do
|
||||
before { allow(alice).to receive(:featureable?).and_return(false) }
|
||||
|
||||
it 'denies' do
|
||||
expect(subject).to_not permit(john, alice)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is blocked' do
|
||||
before { alice.block!(john) }
|
||||
|
||||
it 'denies' do
|
||||
expect(subject).to_not permit(alice, john)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when account is blocking' do
|
||||
before { john.block!(alice) }
|
||||
|
||||
it 'denies' do
|
||||
expect(subject).to_not permit(alice, john)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -75,6 +75,33 @@ RSpec.describe 'Credentials' do
|
||||
end
|
||||
end
|
||||
|
||||
context 'with client credentials' do
|
||||
let(:application) { Fabricate(:application, scopes: 'read admin:write') }
|
||||
let(:token) { Fabricate(:client_credentials_token, application: application, scopes: 'read admin:write') }
|
||||
let(:headers) { { 'Authorization' => "Bearer #{token.token}" } }
|
||||
|
||||
it 'returns http success and returns app information' do
|
||||
subject
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response.content_type)
|
||||
.to start_with('application/json')
|
||||
|
||||
expect(response.parsed_body).to match(
|
||||
a_hash_including(
|
||||
id: token.application.id.to_s,
|
||||
name: token.application.name,
|
||||
website: token.application.website,
|
||||
scopes: token.application.scopes.map(&:to_s),
|
||||
redirect_uris: token.application.redirect_uris,
|
||||
# Deprecated properties as of 4.3:
|
||||
redirect_uri: token.application.redirect_uri.split.first,
|
||||
vapid_key: Rails.configuration.x.vapid.public_key
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without an oauth token' do
|
||||
let(:headers) { {} }
|
||||
|
||||
|
||||
35
spec/services/add_account_to_collection_service_spec.rb
Normal file
35
spec/services/add_account_to_collection_service_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe AddAccountToCollectionService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:collection) { Fabricate.create(:collection) }
|
||||
|
||||
describe '#call' do
|
||||
context 'when given a featurable account' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
it 'creates a new CollectionItem in the `accepted` state' do
|
||||
expect do
|
||||
subject.call(collection, account)
|
||||
end.to change(collection.collection_items, :count).by(1)
|
||||
|
||||
new_item = collection.collection_items.last
|
||||
expect(new_item.state).to eq 'accepted'
|
||||
expect(new_item.account).to eq account
|
||||
end
|
||||
end
|
||||
|
||||
context 'when given an account that is not featureable' do
|
||||
let(:account) { Fabricate(:account, discoverable: false) }
|
||||
|
||||
it 'raises an error' do
|
||||
expect do
|
||||
subject.call(collection, account)
|
||||
end.to raise_error(Mastodon::NotPermittedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -120,6 +120,8 @@ export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
manifest: true,
|
||||
outDir,
|
||||
assetsDir: 'assets',
|
||||
assetsInlineLimit: (filePath, _) =>
|
||||
/\.woff2?$/.exec(filePath) ? false : undefined,
|
||||
rollupOptions: {
|
||||
input: await findEntrypoints(),
|
||||
output: {
|
||||
|
||||
211
yarn.lock
211
yarn.lock
@@ -12,10 +12,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@acemir/cssom@npm:^0.9.23":
|
||||
version: 0.9.24
|
||||
resolution: "@acemir/cssom@npm:0.9.24"
|
||||
checksum: 10c0/1c7bf8a61a74d9ecbc3b12fba697384461b3234441ed5a10f5c34aef91fdf4f1e3322fcd6659a8eaddd591eddc2259efd278212236100d90a6e16f77794d98bd
|
||||
"@acemir/cssom@npm:^0.9.28":
|
||||
version: 0.9.28
|
||||
resolution: "@acemir/cssom@npm:0.9.28"
|
||||
checksum: 10c0/1e192d216c4236171d9930b42b9a965052d4578b23c6ddaa17c7c3d0820ffb872258544a83af163ae2d41b3bdccd6b6c4c14b2d32eb9f8b8b63972d74f46bd83
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -46,29 +46,29 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@asamuzakjp/css-color@npm:^4.0.3":
|
||||
version: 4.0.4
|
||||
resolution: "@asamuzakjp/css-color@npm:4.0.4"
|
||||
"@asamuzakjp/css-color@npm:^4.1.0":
|
||||
version: 4.1.0
|
||||
resolution: "@asamuzakjp/css-color@npm:4.1.0"
|
||||
dependencies:
|
||||
"@csstools/css-calc": "npm:^2.1.4"
|
||||
"@csstools/css-color-parser": "npm:^3.0.10"
|
||||
"@csstools/css-color-parser": "npm:^3.1.0"
|
||||
"@csstools/css-parser-algorithms": "npm:^3.0.5"
|
||||
"@csstools/css-tokenizer": "npm:^3.0.4"
|
||||
lru-cache: "npm:^11.1.0"
|
||||
checksum: 10c0/5a4eb3c8594f58f3df06c867a6cda4a33f702f5cd682d6afa5074813f16fd05e732653ac79bd6fc66390554e158ac478103ad5e885fd9cf154b69bb67639e82f
|
||||
lru-cache: "npm:^11.2.2"
|
||||
checksum: 10c0/097b9270a5befb765885dda43d6914ccbaa575565525d307e8ba3ba07f98e466062f4a77b9657e65160db9110c41c59cf5a6937479647f73637aeddf5c421579
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@asamuzakjp/dom-selector@npm:^6.7.4":
|
||||
version: 6.7.5
|
||||
resolution: "@asamuzakjp/dom-selector@npm:6.7.5"
|
||||
"@asamuzakjp/dom-selector@npm:^6.7.6":
|
||||
version: 6.7.6
|
||||
resolution: "@asamuzakjp/dom-selector@npm:6.7.6"
|
||||
dependencies:
|
||||
"@asamuzakjp/nwsapi": "npm:^2.3.9"
|
||||
bidi-js: "npm:^1.0.3"
|
||||
css-tree: "npm:^3.1.0"
|
||||
is-potential-custom-element-name: "npm:^1.0.1"
|
||||
lru-cache: "npm:^11.2.2"
|
||||
checksum: 10c0/72ac4dc45aac9165222345aacc1db51a84094f159e9e2fa71bc89befd2d78fd00a76c7ff9a8a1ceb60e7ce198a4ec0275a4d878bea67b756cadbf3d9680162c4
|
||||
lru-cache: "npm:^11.2.4"
|
||||
checksum: 10c0/1715faae0787f0c8430b3a0ff3db8576a5b9a4f964408d0808fc2060ab01e0c2f5d8e26409de54b8641433c891dab8b561b196e58798811146084c561a4954ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -1275,7 +1275,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@csstools/css-color-parser@npm:^3.0.10, @csstools/css-color-parser@npm:^3.1.0":
|
||||
"@csstools/css-color-parser@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "@csstools/css-color-parser@npm:3.1.0"
|
||||
dependencies:
|
||||
@@ -1297,7 +1297,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@csstools/css-syntax-patches-for-csstree@npm:^1.0.14":
|
||||
"@csstools/css-syntax-patches-for-csstree@npm:1.0.14":
|
||||
version: 1.0.14
|
||||
resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.14"
|
||||
peerDependencies:
|
||||
@@ -1670,6 +1670,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@csstools/postcss-position-area-property@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@csstools/postcss-position-area-property@npm:1.0.0"
|
||||
peerDependencies:
|
||||
postcss: ^8.4
|
||||
checksum: 10c0/38f770454d46bfed01d43a3f5e7ac07d3111399b374a7198ae6503cdb6288e410c7b4199f5a7af8f16aeb688216445ade97be417c084313d6c56f55e50d34559
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@csstools/postcss-progressive-custom-properties@npm:^4.2.1":
|
||||
version: 4.2.1
|
||||
resolution: "@csstools/postcss-progressive-custom-properties@npm:4.2.1"
|
||||
@@ -1746,6 +1755,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@csstools/postcss-system-ui-font-family@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@csstools/postcss-system-ui-font-family@npm:1.0.0"
|
||||
dependencies:
|
||||
"@csstools/css-parser-algorithms": "npm:^3.0.5"
|
||||
"@csstools/css-tokenizer": "npm:^3.0.4"
|
||||
peerDependencies:
|
||||
postcss: ^8.4
|
||||
checksum: 10c0/6a81761ae3cae643659b1416a7a892cf1505474896193b8abc26cff319cb6b1a20b64c5330d64019fba458e058da3abc9407d0ebf0c102289c0b79ef99b4c6d6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@csstools/postcss-text-decoration-shorthand@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.3"
|
||||
@@ -3320,10 +3341,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@rolldown/pluginutils@npm:1.0.0-beta.47":
|
||||
version: 1.0.0-beta.47
|
||||
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47"
|
||||
checksum: 10c0/eb0cfa7334d66f090c47eaac612174936b05f26e789352428cb6e03575b590f355de30d26b42576ea4e613d8887b587119d19b2e4b3a8909ceb232ca1cf746c8
|
||||
"@rolldown/pluginutils@npm:1.0.0-beta.53":
|
||||
version: 1.0.0-beta.53
|
||||
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.53"
|
||||
checksum: 10c0/e8b0a7eb76be22f6f103171f28072de821525a4e400454850516da91a7381957932ff0ce495f227bcb168e86815788b0c1d249ca9e34dca366a82c8825b714ce
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -4809,18 +4830,18 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"@vitejs/plugin-react@npm:^5.0.0":
|
||||
version: 5.1.1
|
||||
resolution: "@vitejs/plugin-react@npm:5.1.1"
|
||||
version: 5.1.2
|
||||
resolution: "@vitejs/plugin-react@npm:5.1.2"
|
||||
dependencies:
|
||||
"@babel/core": "npm:^7.28.5"
|
||||
"@babel/plugin-transform-react-jsx-self": "npm:^7.27.1"
|
||||
"@babel/plugin-transform-react-jsx-source": "npm:^7.27.1"
|
||||
"@rolldown/pluginutils": "npm:1.0.0-beta.47"
|
||||
"@rolldown/pluginutils": "npm:1.0.0-beta.53"
|
||||
"@types/babel__core": "npm:^7.20.5"
|
||||
react-refresh: "npm:^0.18.0"
|
||||
peerDependencies:
|
||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||
checksum: 10c0/e590efaea1eabfbb1beb6e8c9fac0742fd299808e3368e63b2825ce24740adb8a28fcb2668b14b7ca1bdb42890cfefe94d02dd358dcbbf8a27ddf377b9a82abf
|
||||
checksum: 10c0/d788f269cdf7474425071ba7c4ea7013f174ddaef12b758defe809a551a03ac62a4a80cd858872deb618e7936ccc7cffe178bc12b62e9c836a467e13f15b9390
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5399,13 +5420,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"autoprefixer@npm:^10.4.21":
|
||||
version: 10.4.21
|
||||
resolution: "autoprefixer@npm:10.4.21"
|
||||
"autoprefixer@npm:^10.4.22":
|
||||
version: 10.4.22
|
||||
resolution: "autoprefixer@npm:10.4.22"
|
||||
dependencies:
|
||||
browserslist: "npm:^4.24.4"
|
||||
caniuse-lite: "npm:^1.0.30001702"
|
||||
fraction.js: "npm:^4.3.7"
|
||||
browserslist: "npm:^4.27.0"
|
||||
caniuse-lite: "npm:^1.0.30001754"
|
||||
fraction.js: "npm:^5.3.4"
|
||||
normalize-range: "npm:^0.1.2"
|
||||
picocolors: "npm:^1.1.1"
|
||||
postcss-value-parser: "npm:^4.2.0"
|
||||
@@ -5413,7 +5434,7 @@ __metadata:
|
||||
postcss: ^8.1.0
|
||||
bin:
|
||||
autoprefixer: bin/autoprefixer
|
||||
checksum: 10c0/de5b71d26d0baff4bbfb3d59f7cf7114a6030c9eeb66167acf49a32c5b61c68e308f1e0f869d92334436a221035d08b51cd1b2f2c4689b8d955149423c16d4d4
|
||||
checksum: 10c0/2ae8d135af2deaaa5065a3a466c877787373c0ed766b8a8e8259d7871db79c1a7e1d9f6c9541c54fa95647511d3c2066bb08a30160e58c9bfa75506f9c18f3aa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5538,12 +5559,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"baseline-browser-mapping@npm:^2.8.3":
|
||||
version: 2.8.6
|
||||
resolution: "baseline-browser-mapping@npm:2.8.6"
|
||||
"baseline-browser-mapping@npm:^2.9.0":
|
||||
version: 2.9.2
|
||||
resolution: "baseline-browser-mapping@npm:2.9.2"
|
||||
bin:
|
||||
baseline-browser-mapping: dist/cli.js
|
||||
checksum: 10c0/ea628db5048d1e5c0251d4783e0496f5ce8de7a0e20ea29c8876611cb0acf58ffc76bf6561786c6388db22f130646e3ecb91eebc1c03954552a21d38fa38320f
|
||||
checksum: 10c0/4f9be09e20261ed26f19e9b95454dcb8d8371b87983c57cd9f70b9572e9b3053577f0d8d6d91297bdb605337747680686e22f62522a6e57ae2488fcacf641188
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5635,18 +5656,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"browserslist@npm:^4.24.0, browserslist@npm:^4.24.4, browserslist@npm:^4.25.1, browserslist@npm:^4.26.0":
|
||||
version: 4.26.2
|
||||
resolution: "browserslist@npm:4.26.2"
|
||||
"browserslist@npm:^4.24.0, browserslist@npm:^4.25.1, browserslist@npm:^4.27.0, browserslist@npm:^4.28.0":
|
||||
version: 4.28.1
|
||||
resolution: "browserslist@npm:4.28.1"
|
||||
dependencies:
|
||||
baseline-browser-mapping: "npm:^2.8.3"
|
||||
caniuse-lite: "npm:^1.0.30001741"
|
||||
electron-to-chromium: "npm:^1.5.218"
|
||||
node-releases: "npm:^2.0.21"
|
||||
update-browserslist-db: "npm:^1.1.3"
|
||||
baseline-browser-mapping: "npm:^2.9.0"
|
||||
caniuse-lite: "npm:^1.0.30001759"
|
||||
electron-to-chromium: "npm:^1.5.263"
|
||||
node-releases: "npm:^2.0.27"
|
||||
update-browserslist-db: "npm:^1.2.0"
|
||||
bin:
|
||||
browserslist: cli.js
|
||||
checksum: 10c0/1146339dad33fda77786b11ea07f1c40c48899edd897d73a9114ee0dbb1ee6475bb4abda263a678c104508bdca8e66760ff8e10be1947d3e20d34bae01d8b89b
|
||||
checksum: 10c0/545a5fa9d7234e3777a7177ec1e9134bb2ba60a69e6b95683f6982b1473aad347c77c1264ccf2ac5dea609a9731fbfbda6b85782bdca70f80f86e28a402504bd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -5753,10 +5774,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001702, caniuse-lite@npm:^1.0.30001741":
|
||||
version: 1.0.30001743
|
||||
resolution: "caniuse-lite@npm:1.0.30001743"
|
||||
checksum: 10c0/1bd730ca10d881a1ca9f55ce864d34c3b18501718c03976e0d3419f4694b715159e13fdef6d58ad47b6d2445d315940f3a01266658876828c820a3331aac021d
|
||||
"caniuse-lite@npm:^1.0.30001754, caniuse-lite@npm:^1.0.30001759":
|
||||
version: 1.0.30001759
|
||||
resolution: "caniuse-lite@npm:1.0.30001759"
|
||||
checksum: 10c0/b0f415960ba34995cda18e0d25c4e602f6917b9179290a76bdd0311423505b78cc93e558a90c98a22a1cc6b1781ab720ef6beea24ec7e29a1c1164ca72eac3a2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6216,10 +6237,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cssdb@npm:^8.4.2":
|
||||
version: 8.4.2
|
||||
resolution: "cssdb@npm:8.4.2"
|
||||
checksum: 10c0/3c88610ba9e3f87f9ecf068b72261e90de8bb1f5d1dceefc79ff42b2e19f5814135937ad057b7f8c4bf58212f911e5f9d2f6f0910af3da127170009f1f75689c
|
||||
"cssdb@npm:^8.5.2":
|
||||
version: 8.5.2
|
||||
resolution: "cssdb@npm:8.5.2"
|
||||
checksum: 10c0/12f7ed29dda0d74b209d6470acd246b335aac507c2786c17f20709f856eabb24e6d43ff44507898f5a1b0a101b286b997d95682e44b06f4c7cb4bd7081db7c32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6232,14 +6253,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cssstyle@npm:^5.3.3":
|
||||
version: 5.3.3
|
||||
resolution: "cssstyle@npm:5.3.3"
|
||||
"cssstyle@npm:^5.3.4":
|
||||
version: 5.3.4
|
||||
resolution: "cssstyle@npm:5.3.4"
|
||||
dependencies:
|
||||
"@asamuzakjp/css-color": "npm:^4.0.3"
|
||||
"@csstools/css-syntax-patches-for-csstree": "npm:^1.0.14"
|
||||
"@asamuzakjp/css-color": "npm:^4.1.0"
|
||||
"@csstools/css-syntax-patches-for-csstree": "npm:1.0.14"
|
||||
css-tree: "npm:^3.1.0"
|
||||
checksum: 10c0/0e082992851a1ded3662bda420f86dc1c90510a21cf237ddf573a1e121a722a3f78bb8f6eb46b33f267da25162e8e1fe968f7002114c9ab1d0d4e11dad9c5ee8
|
||||
checksum: 10c0/7499ea8cbc2f759ded275428e0811d147baa6a964a44577711cee5edabee2230cf76b6bd20a556603f99ebc6fff80afdcba6c00bcbb1d41ae50cd09cd9fe9a2d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -6551,10 +6572,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-to-chromium@npm:^1.5.218":
|
||||
version: 1.5.222
|
||||
resolution: "electron-to-chromium@npm:1.5.222"
|
||||
checksum: 10c0/a81eb8d2b171236884faf9b5dd382c66d9250283032cb89a3e555d788bf3956f7f4f6bf7bf30b3daf9e5c945ef837bfcd1be21b3f41cfe186ed2f25da13c9af3
|
||||
"electron-to-chromium@npm:^1.5.263":
|
||||
version: 1.5.266
|
||||
resolution: "electron-to-chromium@npm:1.5.266"
|
||||
checksum: 10c0/74ada92ada1ace76ec5b7da8a9cc2d7f03db122a64ac8e12ae30eba3e358ffec443c0c5265bc6edcdeebfa73f449b21c361080c064eb1eec437db2d71fc03248
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -7652,10 +7673,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fraction.js@npm:^4.3.7":
|
||||
version: 4.3.7
|
||||
resolution: "fraction.js@npm:4.3.7"
|
||||
checksum: 10c0/df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711
|
||||
"fraction.js@npm:^5.3.4":
|
||||
version: 5.3.4
|
||||
resolution: "fraction.js@npm:5.3.4"
|
||||
checksum: 10c0/f90079fe9bfc665e0a07079938e8ff71115bce9462f17b32fc283f163b0540ec34dc33df8ed41bb56f028316b04361b9a9995b9ee9258617f8338e0b05c5f95a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8938,12 +8959,12 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"jsdom@npm:^27.0.0":
|
||||
version: 27.2.0
|
||||
resolution: "jsdom@npm:27.2.0"
|
||||
version: 27.3.0
|
||||
resolution: "jsdom@npm:27.3.0"
|
||||
dependencies:
|
||||
"@acemir/cssom": "npm:^0.9.23"
|
||||
"@asamuzakjp/dom-selector": "npm:^6.7.4"
|
||||
cssstyle: "npm:^5.3.3"
|
||||
"@acemir/cssom": "npm:^0.9.28"
|
||||
"@asamuzakjp/dom-selector": "npm:^6.7.6"
|
||||
cssstyle: "npm:^5.3.4"
|
||||
data-urls: "npm:^6.0.0"
|
||||
decimal.js: "npm:^10.6.0"
|
||||
html-encoding-sniffer: "npm:^4.0.0"
|
||||
@@ -8966,7 +8987,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
checksum: 10c0/52d847e1aef099071d66d1d9aedcdd2f15e7ea781da9cfb41dc0d4caf741c5870c346396f8d1182d611427ae47a53f69a6f16410c698950e5809d3fed5a1672d
|
||||
checksum: 10c0/b022ed8f6ce175afd97fbd42eb65b03b2be3b23df86cf87f018b6d2e757682fe8348e719a14780d6fa3fe8a65e531ba71b38db80f312818a32b77f01e31f267e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9373,10 +9394,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.1.0, lru-cache@npm:^11.2.2":
|
||||
version: 11.2.2
|
||||
resolution: "lru-cache@npm:11.2.2"
|
||||
checksum: 10c0/72d7831bbebc85e2bdefe01047ee5584db69d641c48d7a509e86f66f6ee111b30af7ec3bd68a967d47b69a4b1fa8bbf3872630bd06a63b6735e6f0a5f1c8e83d
|
||||
"lru-cache@npm:^11.0.0, lru-cache@npm:^11.2.2, lru-cache@npm:^11.2.4":
|
||||
version: 11.2.4
|
||||
resolution: "lru-cache@npm:11.2.4"
|
||||
checksum: 10c0/4a24f9b17537619f9144d7b8e42cd5a225efdfd7076ebe7b5e7dc02b860a818455201e67fbf000765233fe7e339d3c8229fc815e9b58ee6ede511e07608c19b2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9908,10 +9929,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-releases@npm:^2.0.21":
|
||||
version: 2.0.21
|
||||
resolution: "node-releases@npm:2.0.21"
|
||||
checksum: 10c0/0eb94916eeebbda9d51da6a9ea47428a12b2bb0dd94930c949632b0c859356abf53b2e5a2792021f96c5fda4f791a8e195f2375b78ae7dba8d8bc3141baa1469
|
||||
"node-releases@npm:^2.0.27":
|
||||
version: 2.0.27
|
||||
resolution: "node-releases@npm:2.0.27"
|
||||
checksum: 10c0/f1e6583b7833ea81880627748d28a3a7ff5703d5409328c216ae57befbced10ce2c991bea86434e8ec39003bd017f70481e2e5f8c1f7e0a7663241f81d6e00e2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10870,8 +10891,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"postcss-preset-env@npm:^10.1.5":
|
||||
version: 10.4.0
|
||||
resolution: "postcss-preset-env@npm:10.4.0"
|
||||
version: 10.5.0
|
||||
resolution: "postcss-preset-env@npm:10.5.0"
|
||||
dependencies:
|
||||
"@csstools/postcss-alpha-function": "npm:^1.0.1"
|
||||
"@csstools/postcss-cascade-layers": "npm:^5.0.2"
|
||||
@@ -10900,21 +10921,23 @@ __metadata:
|
||||
"@csstools/postcss-nested-calc": "npm:^4.0.0"
|
||||
"@csstools/postcss-normalize-display-values": "npm:^4.0.0"
|
||||
"@csstools/postcss-oklab-function": "npm:^4.0.12"
|
||||
"@csstools/postcss-position-area-property": "npm:^1.0.0"
|
||||
"@csstools/postcss-progressive-custom-properties": "npm:^4.2.1"
|
||||
"@csstools/postcss-random-function": "npm:^2.0.1"
|
||||
"@csstools/postcss-relative-color-syntax": "npm:^3.0.12"
|
||||
"@csstools/postcss-scope-pseudo-class": "npm:^4.0.1"
|
||||
"@csstools/postcss-sign-functions": "npm:^1.1.4"
|
||||
"@csstools/postcss-stepped-value-functions": "npm:^4.0.9"
|
||||
"@csstools/postcss-system-ui-font-family": "npm:^1.0.0"
|
||||
"@csstools/postcss-text-decoration-shorthand": "npm:^4.0.3"
|
||||
"@csstools/postcss-trigonometric-functions": "npm:^4.0.9"
|
||||
"@csstools/postcss-unset-value": "npm:^4.0.0"
|
||||
autoprefixer: "npm:^10.4.21"
|
||||
browserslist: "npm:^4.26.0"
|
||||
autoprefixer: "npm:^10.4.22"
|
||||
browserslist: "npm:^4.28.0"
|
||||
css-blank-pseudo: "npm:^7.0.1"
|
||||
css-has-pseudo: "npm:^7.0.3"
|
||||
css-prefers-color-scheme: "npm:^10.0.0"
|
||||
cssdb: "npm:^8.4.2"
|
||||
cssdb: "npm:^8.5.2"
|
||||
postcss-attribute-case-insensitive: "npm:^7.0.1"
|
||||
postcss-clamp: "npm:^4.1.0"
|
||||
postcss-color-functional-notation: "npm:^7.0.12"
|
||||
@@ -10942,7 +10965,7 @@ __metadata:
|
||||
postcss-selector-not: "npm:^8.0.1"
|
||||
peerDependencies:
|
||||
postcss: ^8.4
|
||||
checksum: 10c0/3c081a66ebde19ae2f915f4eb103b85097085799b43103e5dd1699ed807bd54c80d633c7d4b525badaf21e9d0b217e6ca169ee306e2b720bb70b7414ad375387
|
||||
checksum: 10c0/4e9881478b465e8eb7493c1240cb2df8523944135728672e8feeb8bb3f6a48b00d67d007ee8fbdcee648ab9ebdfca10a7591f42e3c6b9076cbf7f355f8ad1574
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -13880,9 +13903,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"update-browserslist-db@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "update-browserslist-db@npm:1.1.3"
|
||||
"update-browserslist-db@npm:^1.2.0":
|
||||
version: 1.2.2
|
||||
resolution: "update-browserslist-db@npm:1.2.2"
|
||||
dependencies:
|
||||
escalade: "npm:^3.2.0"
|
||||
picocolors: "npm:^1.1.1"
|
||||
@@ -13890,7 +13913,7 @@ __metadata:
|
||||
browserslist: ">= 4.21.0"
|
||||
bin:
|
||||
update-browserslist-db: cli.js
|
||||
checksum: 10c0/682e8ecbf9de474a626f6462aa85927936cdd256fe584c6df2508b0df9f7362c44c957e9970df55dfe44d3623807d26316ea2c7d26b80bb76a16c56c37233c32
|
||||
checksum: 10c0/39c3ea08b397ffc8dc3a1c517f5c6ed5cc4179b5e185383dab9bf745879623c12062a2e6bf4f9427cc59389c7bfa0010e86858b923c1e349e32fdddd9b043bb2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -14054,8 +14077,8 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.1.1":
|
||||
version: 7.2.6
|
||||
resolution: "vite@npm:7.2.6"
|
||||
version: 7.2.7
|
||||
resolution: "vite@npm:7.2.7"
|
||||
dependencies:
|
||||
esbuild: "npm:^0.25.0"
|
||||
fdir: "npm:^6.5.0"
|
||||
@@ -14104,7 +14127,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
vite: bin/vite.js
|
||||
checksum: 10c0/d444a159ab8f0f854d596d1938f201b449d59ed4d336e587be9dc89005467214d85848c212c2495f76a8421372ffe4d061d023d659600f1aaa3ba5ac13e804f7
|
||||
checksum: 10c0/0c502d9eb898d9c05061dbd8fd199f280b524bbb4c12ab5f88c7b12779947386684a269e4dd0aa424aa35bcd857f1aa44aadb9ea764702a5043af433052455b5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user