mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-12 23:38:20 +00:00
Compare commits
39 Commits
cd71fdcdff
...
i18n/crowd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f4a8a2af4 | ||
|
|
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);
|
||||
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
{
|
||||
"about.fork_disclaimer": "Glitch-soc er fri programvare med åpen kildekode, forgrenet fra Mastodon.",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.html_meta": "Formater innleggene dine med HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
"compose.content-type.markdown_meta": "Formater innleggene dine med Markdown",
|
||||
"compose.content-type.plain": "Ren tekst",
|
||||
"compose.disable_threaded_mode": "Deaktiver trådmodus",
|
||||
"compose.enable_threaded_mode": "Aktiver trådmodus",
|
||||
"confirmations.deprecated_settings.confirm": "Bruk Mastodon-innstillinger",
|
||||
"federation.federated.short": "Føderert",
|
||||
"federation.local_only.short": "Kun lokalt",
|
||||
"navigation_bar.app_settings": "App-innstillinger",
|
||||
"settings.always_show_spoilers_field": "Aktiver alltid feltet for innholdsvarsler",
|
||||
"settings.close": "Lukk",
|
||||
"settings.content_warnings": "Content warnings",
|
||||
"settings.preferences": "Preferences"
|
||||
"settings.content_warnings.regexp": "Regulært uttrykk",
|
||||
"settings.pop_in_left": "Venstre",
|
||||
"settings.pop_in_player": "Aktiver flytende avspiller",
|
||||
"settings.pop_in_right": "Høyre",
|
||||
"settings.preferences": "Preferences",
|
||||
"settings.side_arm.none": "Ingen"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,23 @@
|
||||
{
|
||||
"settings.content_warnings": "Content warnings",
|
||||
"settings.preferences": "Preferences"
|
||||
"about.fork_disclaimer": "Glitch-soc er fri programvare med åpen kildekode, forgrenet fra Mastodon.",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.html_meta": "Formater innleggene dine med HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
"compose.content-type.markdown_meta": "Formater innleggene dine med Markdown",
|
||||
"compose.content-type.plain": "Ren tekst",
|
||||
"compose.disable_threaded_mode": "Deaktiver trådmodus",
|
||||
"compose.enable_threaded_mode": "Aktiver trådmodus",
|
||||
"confirmations.deprecated_settings.confirm": "Bruk Mastodon-innstillinger",
|
||||
"federation.federated.short": "Føderert",
|
||||
"federation.local_only.short": "Kun lokalt",
|
||||
"navigation_bar.app_settings": "App-innstillinger",
|
||||
"settings.always_show_spoilers_field": "Aktiver alltid feltet for innholdsvarsler",
|
||||
"settings.close": "Lukk",
|
||||
"settings.content_warnings": "Innholdsvarsler",
|
||||
"settings.content_warnings.regexp": "Regulært uttrykk",
|
||||
"settings.pop_in_left": "Venstre",
|
||||
"settings.pop_in_player": "Aktiver flytende avspiller",
|
||||
"settings.pop_in_right": "Høyre",
|
||||
"settings.preferences": "Brukerinnstillinger",
|
||||
"settings.side_arm.none": "Ingen"
|
||||
}
|
||||
|
||||
@@ -9,13 +9,25 @@
|
||||
"column.reblogged_by": "Inpulsionado por",
|
||||
"column_header.profile": "Perfil",
|
||||
"community.column_settings.allow_local_only": "Mostrar os toots apenas locais",
|
||||
"compose.attach.doodle": "Desenhe algo",
|
||||
"compose.change_federation": "Alterar configurações de federação",
|
||||
"compose.content-type.change": "Alterrar opções avançadas de formatação",
|
||||
"compose.content-type.html": "HTML",
|
||||
"compose.content-type.html_meta": "Formatar suas publicações usando HTML",
|
||||
"compose.content-type.markdown": "Markdown",
|
||||
"compose.content-type.markdown_meta": "Formatar suas publicações usando Markdown",
|
||||
"compose.content-type.plain": "Texto sem formatação",
|
||||
"compose.content-type.plain_meta": "Escrever sem formatação avançada",
|
||||
"confirmation_modal.do_not_ask_again": "Não pedir confirmação novamente",
|
||||
"confirmations.deprecated_settings.confirm": "Usar preferências do Mastodon",
|
||||
"confirmations.deprecated_settings.message": "Alguns dos {app_settings} específicos do dispositivo que você está usando foram substituídos por Mastodon {preferences} e serão substituídos:",
|
||||
"direct.group_by_conversations": "Agrupar por conversa",
|
||||
"favourite_modal.favourite": "Favoritar publicação?",
|
||||
"federation.federated.long": "Permitir que esta publicação alcance outros servidores",
|
||||
"federation.federated.short": "Federado",
|
||||
"federation.local_only.long": "Evitar que esta publicação alcance outros servidores",
|
||||
"federation.local_only.short": "Somente local",
|
||||
"firehose.column_settings.allow_local_only": "Exibir publicações somente locais em \"Todas\"",
|
||||
"home.column_settings.advanced": "Avançado",
|
||||
"home.column_settings.filter_regex": "Filtrar com uma expressão regular",
|
||||
"home.column_settings.show_direct": "Mostrar DMs",
|
||||
@@ -24,6 +36,7 @@
|
||||
"keyboard_shortcuts.secondary_toot": "para enviar toot usando a configuração de privacidade secundária",
|
||||
"moved_to_warning": "Esta conta foi como movida para {moved_to_link} e, portanto, pode não aceitar novos seguidores.",
|
||||
"navigation_bar.app_settings": "Configurações do aplicativo",
|
||||
"notifications.column_settings.filter_bar.show_bar": "Exibir barra de filtro",
|
||||
"settings.always_show_spoilers_field": "Sempre ativar o campo Aviso de Conteúdo",
|
||||
"settings.close": "Fechar",
|
||||
"settings.compose_box_opts": "Caixa de composição",
|
||||
@@ -37,6 +50,8 @@
|
||||
"settings.content_warnings_unfold_opts": "Opções de auto-revelar",
|
||||
"settings.deprecated_setting": "Essa configuração agora é controlada pelo {settings_page_link} do Mastodon",
|
||||
"settings.enable_content_warnings_auto_unfold": "Revelar automaticamente os avisos de conteúdo",
|
||||
"settings.fullwidth_view": "Estender colunas para preencher a largura (somente modo Desktop)",
|
||||
"settings.fullwidth_view_hint": "Estende as colunas para ocupar todo o espaço disponível.",
|
||||
"settings.general": "Geral",
|
||||
"settings.hicolor_privacy_icons": "Ícones de privacidade com cores de alto contraste",
|
||||
"settings.hicolor_privacy_icons.hint": "Exibir ícones de privacidade em cores brilhantes e facilmente distinguíveis",
|
||||
@@ -84,11 +99,14 @@
|
||||
"settings.tag_misleading_links.hint": "Acrescentar uma indicação visual com o link hospedeiro alvo a cada link que não o mencione explicitamente",
|
||||
"settings.wide_view": "Visualização ampla (apenas no Modo desktop)",
|
||||
"settings.wide_view_hint": "Estica as colunas para preencher melhor o espaço disponível.",
|
||||
"status.filtered": "Filtrado",
|
||||
"status.has_audio": "Possui um arquivo de áudio anexado",
|
||||
"status.has_pictures": "Possui uma imagem anexada",
|
||||
"status.has_preview_card": "Possui uma pré-visualização anexada",
|
||||
"status.has_video": "Possui um vídeo anexado",
|
||||
"status.hide": "Ocultar publicação",
|
||||
"status.in_reply_to": "Este toot é uma resposta",
|
||||
"status.is_poll": "Este toot é uma enquete",
|
||||
"status.local_only": "Visível apenas em sua instância"
|
||||
"status.local_only": "Visível apenas em sua instância",
|
||||
"status.show_filter_reason": "Mostrar mesmo assim"
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -31,7 +31,7 @@ el:
|
||||
glitch_guide_link_text: Και ομοίως για το glitch-soc!
|
||||
auth:
|
||||
captcha_confirmation:
|
||||
hint_html: Απλώς ένα βήμα ακόμη! Για να επιβεβαιώσεις τον λογαριασμό σου, αυτός ο διακομιστής απαιτεί να λύσεις ένα CAPTCHA. Μπορείς να <a href="/about/more">επικοινωνήσεις με τον διαχειριστή του διακομιστή</a> αν έχεις ερωτήσεις ή χρειάζεσαι βοήθεια με την επιβεβαίωση του λογαριασμού σου.
|
||||
hint_html: Απλώς ένα βήμα ακόμα! Για να επιβεβαιώσεις τον λογαριασμό σου, αυτός ο διακομιστής απαιτεί να λύσεις ένα CAPTCHA. Μπορείς να <a href="/about/more">επικοινωνήσεις με τον διαχειριστή του διακομιστή</a> αν έχεις ερωτήσεις ή χρειάζεσαι βοήθεια με την επιβεβαίωση του λογαριασμού σου.
|
||||
title: Επαλήθευση χρήστη
|
||||
generic:
|
||||
use_this: Χρησιμοποιήστε αυτό
|
||||
|
||||
@@ -1 +1,32 @@
|
||||
---
|
||||
nn:
|
||||
admin:
|
||||
custom_emojis:
|
||||
batch_copy_error: 'En feil oppsto ved kopiering av noen av de valgte emojiene: %{message}'
|
||||
batch_error: 'En feil oppsto: %{message}'
|
||||
settings:
|
||||
flavour_and_skin:
|
||||
title: Variant og tema
|
||||
hide_followers_count:
|
||||
desc_html: Ikke vis følgerantallet på brukerprofiler
|
||||
title: Skjul følgerantall
|
||||
other:
|
||||
preamble: Diverse innstillinger for glitch-soc som ikke passer i andre kategorier.
|
||||
title: Andre
|
||||
outgoing_spoilers:
|
||||
title: Innholdsvarsel for utgående innlegg
|
||||
show_reblogs_in_public_timelines:
|
||||
desc_html: Vis offentlige fremhevinger av offentlige innlegg i lokale og offentlige tidslinjer.
|
||||
title: Vis fremhevinger i offentlige tidslinjer
|
||||
show_replies_in_public_timelines:
|
||||
title: Vis svar i offentlige tidslinjer
|
||||
trending_status_cw:
|
||||
title: Tillat innlegg med innholdsvarsler å trende
|
||||
appearance:
|
||||
localization:
|
||||
glitch_guide_link: https://crowdin.com/project/glitch-soc
|
||||
glitch_guide_link_text: Det gjelder også glitch-soc!
|
||||
generic:
|
||||
use_this: Bruk dette
|
||||
settings:
|
||||
flavours: Varianter
|
||||
|
||||
@@ -1 +1,32 @@
|
||||
---
|
||||
'no':
|
||||
admin:
|
||||
custom_emojis:
|
||||
batch_copy_error: 'En feil oppsto ved kopiering av noen av de valgte emojiene: %{message}'
|
||||
batch_error: 'En feil oppsto: %{message}'
|
||||
settings:
|
||||
flavour_and_skin:
|
||||
title: Variant og tema
|
||||
hide_followers_count:
|
||||
desc_html: Ikke vis følgerantallet på brukerprofiler
|
||||
title: Skjul følgerantall
|
||||
other:
|
||||
preamble: Diverse innstillinger for glitch-soc som ikke passer i andre kategorier.
|
||||
title: Andre
|
||||
outgoing_spoilers:
|
||||
title: Innholdsvarsel for utgående innlegg
|
||||
show_reblogs_in_public_timelines:
|
||||
desc_html: Vis offentlige fremhevinger av offentlige innlegg i lokale og offentlige tidslinjer.
|
||||
title: Vis fremhevinger i offentlige tidslinjer
|
||||
show_replies_in_public_timelines:
|
||||
title: Vis svar i offentlige tidslinjer
|
||||
trending_status_cw:
|
||||
title: Tillat innlegg med innholdsvarsler å trende
|
||||
appearance:
|
||||
localization:
|
||||
glitch_guide_link: https://crowdin.com/project/glitch-soc
|
||||
glitch_guide_link_text: Det gjelder også glitch-soc!
|
||||
generic:
|
||||
use_this: Bruk dette
|
||||
settings:
|
||||
flavours: Varianter
|
||||
|
||||
@@ -8,7 +8,7 @@ el:
|
||||
setting_default_content_type_markdown: Κατά τη γραφή των τουτς, υποθέτει ότι χρησιμοποιείται το Markdown για μορφοποίηση πλούσιου κειμένου, εκτός αν ορίζεται διαφορετικά
|
||||
setting_default_content_type_plain: Κατά τη γραφή των τουτς, υποθέτει ότι είναι απλό κείμενο χωρίς ειδική μορφοποίηση, εκτός αν ορίζεται διαφορετικά (προεπιλεγμένη συμπεριφορά Mastodon)
|
||||
setting_default_language: Η γλώσσα των τουτ σας μπορεί να εντοπιστεί αυτόματα, αλλά δεν είναι πάντα ακριβής
|
||||
setting_show_followers_count: Εμφάνιση του αριθμού ακολούθων σας στο προφίλ σας. Αν αποκρύψετε τον αριθμό των ακολούθων σας, θα είναι κρυμμένος ακόμη και από εσάς, και μερικές εφαρμογές μπορεί να εμφανίσουν έναν αρνητικό αριθμό ακολούθων.
|
||||
setting_show_followers_count: Εμφάνιση του αριθμού ακολούθων σας στο προφίλ σας. Αν αποκρύψετε τον αριθμό των ακολούθων σας, θα είναι κρυμμένος ακόμα και από εσάς, και μερικές εφαρμογές μπορεί να εμφανίσουν έναν αρνητικό αριθμό ακολούθων.
|
||||
labels:
|
||||
defaults:
|
||||
setting_default_content_type: Προεπιλεγμένη μορφή για τουτς
|
||||
|
||||
@@ -8,7 +8,7 @@ ko:
|
||||
setting_default_content_type_markdown: 게시물을 작성할 때, 형식을 지정하지 않았다면, 마크다운이라고 가정합니다
|
||||
setting_default_content_type_plain: 게시물을 작성할 때, 형식을 지정하지 않았다면, 일반적인 텍스트라고 가정합니다. (마스토돈의 기본 동작)
|
||||
setting_default_language: 작성하는 게시물의 언어는 자동으로 설정될 수 있습니다, 하지만 언제나 정확하지는 않습니다
|
||||
setting_show_followers_count: 팔로워 카운트를 프로필에서 숨깁니다. 팔로워 수를 숨기면 나에게도 보이지 않으며 몇몇 앱에서는 팔로워 수가 음수로 표시될 수 있습니다.
|
||||
setting_show_followers_count: 팔로워 카운트를 프로필에 표시합니다. 팔로워 수를 숨기면 나에게도 보이지 않으며 몇몇 앱에서는 팔로워 수가 음수로 표시될 수 있습니다.
|
||||
setting_skin: 선택한 마스토돈 풍미의 스킨을 바꿉니다
|
||||
labels:
|
||||
defaults:
|
||||
|
||||
@@ -1 +1,15 @@
|
||||
---
|
||||
nn:
|
||||
simple_form:
|
||||
glitch_only: glitch-soc
|
||||
hints:
|
||||
defaults:
|
||||
setting_default_language: Språket i innleggene deres kan oppdages automatisk, men det er ikke alltid nøyaktig
|
||||
labels:
|
||||
defaults:
|
||||
setting_default_content_type: Standardformat for innlegg
|
||||
setting_default_content_type_html: HTML
|
||||
setting_default_content_type_markdown: Markdown
|
||||
setting_default_content_type_plain: Ren tekst
|
||||
setting_show_followers_count: Vis følgerantallet deres
|
||||
setting_skin: Tema
|
||||
|
||||
@@ -1 +1,15 @@
|
||||
---
|
||||
'no':
|
||||
simple_form:
|
||||
glitch_only: glitch-soc
|
||||
hints:
|
||||
defaults:
|
||||
setting_default_language: Språket i innleggene deres kan oppdages automatisk, men det er ikke alltid nøyaktig
|
||||
labels:
|
||||
defaults:
|
||||
setting_default_content_type: Standardformat for innlegg
|
||||
setting_default_content_type_html: HTML
|
||||
setting_default_content_type_markdown: Markdown
|
||||
setting_default_content_type_plain: Ren tekst
|
||||
setting_show_followers_count: Vis følgerantallet deres
|
||||
setting_skin: Tema
|
||||
|
||||
@@ -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